[Go语言]SSTI从0到1
- 1.Go-web基础及示例
- 2.参数处理
- 3.模版引擎
- 3.1 text/template
- 3.2 SSTI
- 4.[LineCTF2022]gotm
- 1.题目源码
- 2.WP
1.Go-web基础及示例
package main
import ("fmt""net/http"
)
func sayHello(w http.ResponseWriter, r *http.Request) { // 定义一个fmt.Fprintln(w, "hello world!")
}
func main() {http.HandleFunc("/hello", sayHello) //浏览器访问/hello,将会转到sayHello去处理http.ListenAndServe(":8888", nil) // 监听端口并处理请求
}
其中 “net/http” 是Go自带的实现web服务的模块,可以处理http请求,其中http.HandleFunc是用来绑定路由函数,相当于python的flask的app.route函数
然后就是路由函数sayHello了,这个函数接受两个参数,其中 w 是web接口参数,该变量可发送到前端web页面,r 是http报文参数,用于存放http请求的UA头,表单数据等信息
然后 fmt.Fprintln 函数是用于输出在器(io.Writer
)中例如web接口
这里输出 “hello world”,如图所示
2.参数处理
我们将sayHello函数改动如下func sayHello(w http.ResponseWriter, r *http.Request) {var name = r.FormValue("name")var para = ("hello," + name + "!")fmt.Fprintln(w, para)
}
r.FormValue为接收表单数据的数组,包括GET与POST
处理表单请求的方法如下
1.直接获得
PostFormValue或FormValue方法前者只能解析Post请求
2.ParseForm()方法间接获得
r.ParseForm() 解析请求的主体,化为键值对组合
键值对会被存储在 r.Form 和 r.PostForm 字段中
- r.Form 是一个 url.Values 类型,它表示 URL 查询参数和 POST 表单字段的集合。可以通过 r.Form.Get(key)、r.Form[key] 或 r.FormValue(key) 方法来获取表单字段的值。
- r.PostForm 是一个 url.Values 类型,它只包含 POST 表单字段的数据。与 r.Form 不同的是,r.PostForm 不会自动解析 URL 查询参数或其他非 POST 提交的数据。
示例如下:
3.模版引擎
GO语言提供了两个模板包,一个是 html/template 模块,另一个是 text/template 模块,其中text/template模板引擎与Go语言的SSTI有关
3.1 text/template
示例:
package mainimport ("fmt""net/http""strings""text/template"
)type User struct {Id intName stringPasswd string
}func StringTplExam(w http.ResponseWriter, r *http.Request) {user := &User{1, "admin", "123456"}r.ParseForm()arg := strings.Join(r.PostForm["name"], "")tpl1 := fmt.Sprintf(`<h1>Hi, ` + arg + `</h1> Your name is ` + arg + `!`)html, err := template.New("login").Parse(tpl1)html = template.Must(html, err)html.Execute(w, user)
}func main() {server := http.Server{Addr: "127.0.0.1:8080",}http.HandleFunc("/login", StringTplExam)server.ListenAndServe()
}
首先讲解一下这段代码,它使用了 “text/template” 模板引擎,该模板引擎并没有对传入的数据进行html编码,可导致SSTi,然后定义了一个User结构体,用于给模版传参
type User struct {Id intName stringPasswd string
}
然后通过fmt.Sprintf函数将传入的name参数拼接到模版当中,再通过以下语句,利用template.New(“login”)创建了一个名为 login 的模板,然后使用 Parse(tpl1) 方法,被作为模版的字符串存储到login模板中
html, err := template.New("login").Parse(tpl1)
其中html就代表刚才创建的login模板,然后err代表创建模板过程中的报错信息(一般不会有),然后通过以下语句判断模板是否创建成功,若创建成功则继续运行,失败则退出程序并报错
html = template.Must(html, err)
最后通过 Execute 方法进行模版翻译,将user结构体中的值翻译到html的模板中,然后输出到w接口,也就是web前端
html.Execute(w, user)
3.2 SSTI
我们以上面的web源码为例
首先访问 login 路由,然后POST传入一个参数,可以看到回显正常
我们再传入 name={{.Name}},其中双重大括号为该模板引擎的占位符,可以被翻译,我们使用 {{.参数名}} 的格式,可以访问构建模版时所传入的参数
type User struct {Id intName stringPasswd string
}user := &User{1, "admin", "123456"}
如图所示
4.[LineCTF2022]gotm
1.题目源码
main.go
package mainimport ("encoding/json""fmt""log""net/http""os""text/template""github.com/golang-jwt/jwt"
)type Account struct {id stringpw stringis_admin boolsecret_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))
}
2.WP
审计代码可得该web网站共有以下路由
http.HandleFunc("/", root_handler) //模板http.HandleFunc("/auth", auth_handler) //登陆http.HandleFunc("/flag", flag_handler)http.HandleFunc("/regist", regist_handler) //注册
先分析下flag路由
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 {}
发现需要进行jwt伪造,让 is_admin 的值为ture
访问regist路由注册账号,形成原始jwt
访问auth路由,登陆刚才注册的账号,得到一段jwt加密的字符串
对其解密得,is_admin的值默认为false
由此可得我们只需添加 X-Token 的请求头即可,内容为修改后的jwt字符串,接下来我们需要通过根路由进行SSTI,得到jwt加密的密钥,然后进行伪造
token := r.Header.Get("X-Token")
分析根路由可得,注入点在jwt加密数据的id部分
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)}
我们再次访问regist路由,发送以下payload,来获得acc结构体的所有信息
regist?id={{.}}&pw=123
//不能使用{{.secret_key}}注入得到key字段,因为root_handler函数中得到的acc是数组中的地址,也就是get_account函数通过在全局变量acc数组中查找我们的用户,这种情况下直接注入{{.secret_key}}会返回空
访问author路由,得到SSTI的密文如下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss
再次访问根路由,并添加 X-Token 请求头,值为刚才的jwt密文,得到jwt加密密钥(this_is_f4Ke_key)
回到 jwt.io网站,将is_admin的值设为true,并加密
得到密文
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo
访问flag路由添加X-Token请求头
成功得到flag