Go下template的SSTI分析

July 23, 2025
7 min read
By bx

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

Go下template的SSTI分析

Go下template的SSTI分析

基本认识

Go 标准库中提供的 html/templatetext/template 两种模板引擎

具体语法

这里主要是记录一下📝

支持模板语法,比如:

  • {{ . }}:当前上下文
  • {{ .Field }}:访问字段
  • {{ if }}{{ else }}{{ end }}:条件
  • {{ range }}{{ end }}:循环
  • {{ define }}{{ template }}:模板嵌套

数据求值

  1. 访问字段: {{ .FieldName }}

如果 . 是一个结构体或指向结构体的指针,这会访问其名为 FieldName 的导出字段(首字母大写)。

  1. 访问嵌套字段: {{ .StructField.NestedField }}
  2. 访问 Map 中的值:

{{ .MapName "keyName" }} (如果 Map 的 key 是字符串)

{{ index .MapName $keyVariable }} (使用 index 内建函数,更通用)

  1. 访问 Slice 或 Array 中的元素:

{{ .SliceName 0 }} (Go 1.11+ 支持直接索引)

{{ index .SliceName $indexVariable }} (使用 index 内建函数)

  1. 方法调用: {{ .MethodName arg1 arg2 }}

可以调用当前对象 . 上的方法。

方法必须只有一个返回值,或者有两个返回值且第二个是 error 类型。模板会自动检查 error,如果非 nil 则中断执行。

这个常试一试

  1. 根数据对象: 在模板的顶层,. 通常指向 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

  1. 还可以用管道符号
  2. 调用预定义的内置函数或开发者通过 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, &lt;script&gt;alert(&#39;Go Go Go!!&#39;)&lt;/script&gt;</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}}

参考文章

https://xz.aliyun.com/news/15003

https://xz.aliyun.com/news/12088