[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 main
import (
"fmt"
"net/http"
"strings"
"text/template"
)
type User struct {
Id int
Name string
Passwd 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 int
Name string
Passwd 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 int
Name string
Passwd string
}
user := &User{1, "admin", "123456"}
如图所示
4.[LineCTF2022]gotm
1.题目源码
main.go
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))
}
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