之前打铁三,看了别的师傅的wp之后发现go也有ssti,于是找了个go ssti题来学学

Dockerfile中flag是假的,所以直接审源码

有4个路由:

1
2
3
4
5
6
7
8
9
10
func main() {
admin := Account{admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)

http.HandleFunc("/", root_handler)
http.HandleFunc("/auth", auth_handler)
http.HandleFunc("/flag", flag_handler)
http.HandleFunc("/regist", regist_handler)
log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}

先审最开始的’/‘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {

return
}
}

先获取Token,如果有Token则用jwt解密,如果解密成功则显示用户的id,如果没有token直接返回空白

审’/auth’

1
2
3
4
5
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {
return
}

获取用户传入的id和pw

1
2
3
4
5
6
7
8
9
10
11
12
13
user_acc := get_account(uid)
if user_acc.id != "" && user_acc.pw == upw {
token, err := jwt_encode(user_acc.id, user_acc.is_admin)
if err != nil {
return
}
p := TokenResp{true, token}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}

如果输入了正确的id和pw(get_account有匹配的正确结果),返回一个TokenResp的对象(json形式),里面存储了状态status,和jwt token,内容是id和是否为admin

1
2
3
4
type TokenResp struct {
Status bool `json:"status"`
Token string `json:"token"`
}

/flag路由解码jwt,is_admin为true给flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func flag_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, is_admin := jwt_decode(token)
if is_admin == true {
p := Resp{true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
} else {
w.WriteHeader(http.StatusForbidden)
return
}
}
}

最后是/regist路由,路由中默认给is_admin为false,所以我们要想办法给is_admin为true

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
func regist_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")

if uid == "" || upw == "" {
return
}

if get_account(uid).id != "" {
w.WriteHeader(http.StatusForbidden)
return
}
if len(acc) > 4 {
clear_account()
}
new_acc := Account{uid, upw, false, secret_key}
acc = append(acc, new_acc)

p := Resp{true, ""}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}

现在,我们的思路是:用go ssti注入获取key然后伪造jwt

先说一下go的ssti,和jinja2的ssti类似,都是因为直接渲染拼接的字符导致插入了模板语言后执行

Go 语言内置了 text/template 和 html/template 两个模板库。如果开发人员没有正确使用这些库,可能会导致 SSTI 注入。例如,如果使用 text/template 处理用户输入,并且未对输入进行转义,攻击者可以插入恶意模板代码。

1
{{.}}
1
个模板语句的作用是在模板渲染中输出当前上下文中的数据。如果传递给模板引擎的是一个结构体类型的变量,使用“{{.}}”将会引用整个结构体的数据。而如果您想引用结构体中的某个字段,可以使用“{{.FieldName}}”来引用。
1
2
3
4
5
6
type Account struct {
id string
pw string
is_admin bool
secret_key string
}

这里我们要用

1
{{.}}

来显示整个结构体来获取到secret_key

第一步,访问regist注册一个id为

1
{{.}}

的账户,因为存在ssti漏洞,acc.id会被直接渲染

注册后,访问auth,获得token

在header中加入X-TOKEN访问’/‘路由,查询到key

去jwt.io,伪造token,输入key,把is_admin改为true然后带着token访问/即可拿到flag