资讯详情

资讯详情

SSTI模板注入实战:fenjing自动化穿越WAF的绕过工具指南

前阵子做一次授权渗透测试目标是个Flask写的内部系统。参数丢个{{7*7}}进去页面直接回显49SSTI模板注入实锤。但高兴没两秒——关键字被过滤点号和下划线被过滤连单双引号都给我拦了。手工试了一个多小时从__class__绕到request.argspayload改了一堆WAF纹丝不动。最后是fenjing这个工具救场几分钟内给我生成了一段能用的payload。这篇文章不聊高大上的原理图就聊聊fenjing到底怎么用、内部思路是什么以及在vulhub靶场上怎么一步步复现。如果你是做Web安全测试的同行或者刚接触模板注入想找个趁手工具的这篇应该能帮到你。1. 为什么SSTI测试这么依赖工具1.1 一条典型的Jinja2注入链是怎么跑通的先说原理。SSTIServer-Side Template Injection服务端模板注入。大多数情况下出现在Python的Flask Jinja2组合里也有Java的Freemarker、PHP的Twig、Node的Nunjucks等。原理说白了就是开发者把用户输入当成模板内容的一部分交给模板引擎渲染。模板引擎不光能显示变量还能执行表达式这一下就给了测试人员代码执行的机会。在Jinja2里表达式写在{{ }}里逻辑写在{% %}里。你输入{{ 7*7 }}如果参数值被拼进模板并且模板渲染了它页面会返回49。这就是最基础的探测方式也是判断注入点时首先要做的一步。确认注入后下一步就是要从模板语法往Python对象上爬。Jinja2的变量查找规则里{{ .__class__ }}可以拿到字符串的类{{ .__class__.__mro__ }}可以看到继承链顺着继承链可以找到object基类从object.__subclasses__()可以拿到进程中所有类的列表再从里面找到os._wrap_close、subprocess.Popen这类和目标环境相关的类通过它们的模块引用去调os.popen执行命令。一个常见的利用链长这样{{ lipsum.__globals__[os].popen(id).read() }}lipsum是Jinja2内置的一个全局函数对象不依赖用户输入就能访问到通过它拿__globals__字典直接进os模块执行命令并读取输出。原理不复杂但真正落到一个真实网站里形态就完全不一样了。你永远不知道前面挡着什么规则可能是云WAF可能是应用自己的黑名单可能是统一接入的网关过滤。每条规则都可能让你的payload死在半路上。1.2 绕不过去的过滤规则是最大的拦路虎我整理了一下在真实项目里最常见的过滤点基本就这几类过滤关键字class、globals、os、popen、import等直接替换或删除过滤特殊字符下划线_、点号.、方括号[]、单双引号、{{和}}过滤空格把空格删掉或用、/**/等替代过滤数字0-9全部拦截十六进制里的0x也被拦过滤字母这个最狠意味着变量名、方法名、关键字全不能用你以为绕过一个点就结束了实际是环环相扣的组合题。比如过滤下划线你想到用十六进制\x5f来绕过结果十六进制里的字母x和f也在过滤名单里你想用request.args从URL参数里取字符串结果参数值也被WAF扫描了一遍。这种情况下手工构造一个能用的payload可能要折腾几个小时而且很可能最后还是不行。因为人脑很难同时跟踪十多个会被过滤的字符还要保证整条表达式在Python语法层面完全合法、在Jinja2语法层面能正确解析。这就是我为什么会去尝试自动化工具。不是手工不行而是在规则组合很复杂的场景下工具可以用程序的方式系统地搜索payload空间效率完全不是一个量级。1.3 从手工到自动化fenjing的出现逻辑SSTI绕过的本质是在期望执行的代码和黑名单规则之间找一条合法路径。这条路可以看作一个约束满足问题目标函数是拥有RCE能力的Jinja2表达式约束条件是不能出现某些字符/关键字。人工解这种问题靠的是经验和试错程序解这种问题靠的是自动枚举和反馈。fenjing项目就是顺着这个思路做的。它不是一个简单的payload字典匹配工具而是会根据目标回显实时调整、重组绕过程度的自动化工具。它的定位很明确给已经确认存在SSTI、但手工绕不过WAF的测试人员提供一个相对省心的payload生成能力。这是它和普通exp脚本最大的区别——你给它的不是一个固定的payload而是一个目标它负责在当前约束条件下把payload算出来。2. fenjing的工作逻辑从探测到利用的自动化链路2.1 fenjing的项目定位与适用场景fenjing在GitHub上的定位是SSTI绕过检测工具主要针对Flask/Jinja2应用。我用下来最直观的感受是它把检测注入点、生成绕过payload、验证执行结果三个环节串成了一条流水线。适用场景很明确已经通过{{7*7}}或者其他方式确认存在SSTI存在WAF或应用本身的字符过滤手工payload搞不定或者想快速批量处理多个参数测试目标是Jinja2/Flask系其他模板引擎的支持度需要自己看它不擅长的事情也要说清楚如果你连注入点都没确定它不是一个漏洞扫描器。虽然有扫描模式但主力用法还是确认注入后的payload生成。所以别指望拿它像nuclei那样全站扫漏洞——它干的是精细活不是广撒网。2.2 三步走的内部流程探测、payload生成、验证用我的理解拆一下fenjing的内部逻辑。第一步是探测Probe。它会向目标注入点发不同的探测字符串比如{{7*7}}、{{7*7}}、${7*7}、%7*7%这些观察响应里的变化。这一步判断两件事是不是模板注入、是哪种模板引擎。第二步是生成payloadPayload Generation。这是最核心的部分。它会基于当前探测到的回显方式、已有的规则列表动态尝试构造可执行的Jinja2表达式。在这个阶段工具会做大量的拆解—重组操作把目标表达式拆成若干片段每个片段用不同的方式生成再把片段用合法的Jinja2语法拼回去。比如__class__被过滤就尝试用十六进制、字符串拼接、attr过滤器等方式替代点号被过滤就尝试用方括号加字符串索引的方式替代。第三步是验证Validation。生成的payload发到目标上看返回结果是否符合预期。符合就保留不符合就换一种组合方式。这个过程是迭代的反馈周期极短。一台普通的主机跑完一轮可能只需要几十秒。顺带说一句工具输出的payload不一定是以最短为目标的。它更倾向于在满足规则的前提下能跑通所以如果你对payload长度有要求或者目标服务器限制参数长度后面需要自己再优化。2.3 环境安装与依赖坑fenjing是Python写的安装本身不难。我建议在虚拟环境里装避免跟你本地的其他Python包冲突。python3 -m venv fenjing_env source fenjing_env/bin/activate pip install fenjing装完之后可以直接敲fenjing --help看参数。如果是从源码跑则需要先拉仓库再装依赖git clone https://github.com/Marven11/Fenjing.git cd Fenjing pip install -r requirements.txt python3 fenjing.py --help这里有个我实际踩过的坑有些机器上默认的python3指向的系统Python在装依赖时权限不够或者环境混杂容易把包装到奇怪的地方去。我第二次用的时候就是环境问题折腾了一阵。我的建议是全程使用venv不要在系统Python里直接pip install。另外如果目标环境的Jinja2不是标准版本某些高级特性可能用不上工具会自动降级但你最好有心理准备不是所有payload生成逻辑都对每个Flask版本生效。3. 实操在vulhub环境里复现完整的SSTI测试3.1 靶场搭建与确认漏洞存在先说靶场。vulhub里有一个flask/ssti环境就是为SSTI测试准备的。如果你的机器上已经装了docker、docker-compose搭建过程非常快git clone https://github.com/vulhub/vulhub.git cd vulhub/flask/ssti docker-compose up -d环境起来后访问http://your-ip:8000/?nameguest页面会输出类似Hello guest的内容。这个时候你手工试一下http://your-ip:8000/?name{{7*7}}如果页面上出现Hello 49漏洞确认模板环境是Jinja2。vulhub这个环境默认没有额外加WAF所以直接跑任意payload都是通的。我们的重点是用它熟悉fenjing的操作流以及后面通过修改应用来模拟过滤规则。这里多说一句如果你要测试更复杂的过滤场景vulhub环境里的app.py是可以直接改的。改完重启容器就能得到一个带特定过滤规则的靶机。这个技巧很好用我后面讲无字母数字场景时也是这么干的。3.2 用fenjing跑出第一条可用payload确认漏洞后用一个命令行就能让fenjing跑起来。我的用法是这样的fenjing -u http://your-ip:8000/?nameguest --exec id工具会先探测注入点然后生成payload再把id命令的执行结果返回给你。正常的输出会包含你这条命令的完整payload以及目标回显的内容。第一次跑的时候看到返回结果里有一个很长但结构清晰的payload第一反应是这也能通然后放到浏览器里一验居然真的能执行命令。那一刻我才意识到工具的价值——它生成的不只是一个payload而是一整套在当前过滤条件下合法的表达式。3.3 查看生成结果读懂工具背后的绕过思路fenjing跑出来的payload不一定短但结构上通常是可读的。我截一个典型的生成逻辑来说全局对象部分优先使用lipsum、cycler、joiner、namespace这些Jinja2内置对象因为它们在模板上下文中本来就能用不需要额外导入属性访问部分__class__、__globals__这类属性如果被过滤工具会尝试用attr过滤器配合字符串值来访问字符串值本身则用\x5f之类的转义或拼接生成模块访问部分os这类模块名被过滤时会尝试通过__globals__这个字典的键来做字符串查找键的字符串值同样经过编码处理这样拆解下来你就知道为什么某些payload长得奇形怪状——它是在满足所有过滤条件的情况下尽可能短的合法表达式。读懂这个结构后你甚至可以手动把payload里的lipsum换成cycler因为不同环境里这两个对象的可用性可能不一样换一下也许能让payload在另一个环境里也跑通。3.4 从读文件到执行命令vulhub环境的完整利用在vulhub的flask/ssti环境里目标通常是一个Python容器上面有若干环境变量和文件。比较常见的验证方式是读一下应用本身的关键文件或者直接执行命令看系统信息。fenjing -u http://your-ip:8000/?nameguest --exec cat /etc/passwd执行命令这块有一个细节要注意popen(id).read()这个链路里read()是必须的否则命令输出不会出现在回显里如果目标不回显stdout但是会回显报错还可以用popen(id).read()[:-1]之类的方式来处理多余的空行。这些细节在工具自动生成时都被处理过了但你自己手工改payload时要记得。4. 当字母和数字都被过滤无字母数字payload的绕过原理4.1 过滤规则真正可怕的是什么过滤字母数字听起来极端但实网上确实存在。很多云WAF或硬编码的过滤逻辑直接把大小写字母和数字全部列入黑名单。这样带来的问题是你连class、os、popen这些单词都写不出来更别提__class__这种带下划线的属性名了。等于说攻击者手里只剩下一堆符号{}[]()%-*/~|^等等。这些符号要拼出可执行的表达式难度非常大。但在Python/Jinja2的语法体系里有一个非常有用的突破口——运算符和字符串格式化。~是字符串拼接运算符%是格式化字符串的占位符加上int、chr这类功能理论上是能无中生有地把字符拼出来的。4.2 用异或与取反从空气里变出字符在Python里整数可以通过取反运算~或异或运算^调整成特定的ASCII值再配合某些能动态生成字符的语法可以从纯符号表达式中构造出任意字符。举个例子你需要在payload中出现下划线_它的ASCII码是95。你可以先构造一个数字95再用%c格式化输出成字符{{ %c % 95 }}问题是怎么在纯符号环境里得到95这个数字。方法很多比如利用字符串的长度{{ |length }} # 得到0但如果连数字都被过滤直接用数字字面量95不行就需要用运算符从现有的数字里算出来。~-2取反得到1~-3得到2按这个规律可以一步步加起来乘起来用*、、~组合出任意数字。有了数字再用格式化输出变成字符字符再拼成字符串字符串再作为属性名去访问对象。这整套操作组合起来就是无字母数字payload的基本盘。这个思路听起来复杂但实现起来就是一组固定的模板模式。fenjing在无字母数字场景下做的事情本质上就是自动枚举这些模式找到在目标上实际可用的那一种。4.3 fenjing在这种场景下的特殊处理在纯符号环境下工具要做的一个关键决策是优先选择哪种基础对象来作为访问的起点。如果字母被过滤lipsum、cycler这种内置对象名本身也是字母就没法直接用。这时候需要换一种思路——通过request对象从HTTP请求里取字符串。request对象在Jinja2上下文中全局可用而且可以通过request.args从URL参数里拿到值。于是你可以把一段payload写成{{ ()[(request.args.a)] }}实际上隐藏在URL参数里的a__class__也会被WAF扫描这就又回到了编码问题。fenjing会把这种参数值做多层编码组合找到一个WAF无法识别但Jinja2能够正确解码的形式。这里也要提醒一句完全无字母数字的利用链最后真正RCE的难度依然很高。因为哪怕你从符号空间里拼出了__class__、__globals__、popen这些字符串还需要让Jinja2把它们当成可调用的代码路径去执行。大多数时候最终的payload会很长长到可能超过服务器对参数长度的限制。所以无字母数字很多时候不是靠一个无敌payload解决的而是靠工具和手工配合在性能和可行性之间找平衡。5. 把fenjing集成到自己的工作流里5.1 常用的命令行参数组合fenjing的CLI提供了不少参数我自己最常用的组合是这几组-u指定带注入点的URL这是最基本的目标参数--exec接要执行的系统命令--output把生成的payload输出到文件--interactive进入交互模式一边探测一边手工调整另外它的payload替换逻辑有时候不能满足非常规需求这时候我建议打开交互模式把工具生成的payload作为基础自己在里面改。改完之后再放回工具里验证效率比自己从零写高得多。如果你要测试的命令本身比较长记得把整条命令用引号包起来避免shell把你的空格和特殊字符拆散。我见过好几个人在--exec cat /etc/passwd这里忘记加引号结果命令被shell解释成两个参数工具自然处理不对。5.2 用Python脚本批量测试多个注入点如果你的页面上一排参数都疑似存在SSTI一个个跑太费事。可以用Python脚本包一层import subprocess urls [ http://your-ip:8000/?nameguest, http://your-ip:8000/?searchguest, http://your-ip:8000/?msgguest, ] for url in urls: result subprocess.run( [fenjing, -u, url, --exec, id], capture_outputTrue, textTrue, timeout60 ) print(url, result.stdout)不过批量跑要注意目标服务的日志会瞬间多出一大堆异常请求如果是有防护的线上系统很容易触发封禁策略。所以我建议批量场景优先在测试环境里跑线上验证就老老实实挑关键参数手动来。另外一个批量场景的技巧是如果几个注入点只是参数名不同但路径和端口一致可以先跑通一个然后手工把payload里的注入参数值替换到其他参数上去验证不一定每个参数都要重新跑一轮工具。这样既能减少请求量又能快速覆盖多个位置。5.3 与其他工具的配合fenjing定位是payload生成和绕过验证它不是万能的。我实际项目中经常是这样的组合Burp Suite先抓包、分析和修改请求手工做基础探测确认漏洞存在fenjing生成绕过payload拿到payload后在Burp Repeater里手动放观察完整交互如果涉及多个目标或复杂流程再用脚本批量验证这套流程的好处是每一步都有明确的输出物工具只是替换了最耗时的手工构造环节而不是把整个测试流程交给一个黑盒。黑盒工具的问题在于你不知道它在干什么、出了问题也不知道怎么调试人工参与的部分始终要有。6. 我在实际使用中踩过的坑6.1 payload长度被截断SSTI利用里非常常见的一个坑是参数长度限制。很多应用对GET参数长度做了上限比如512或1024字节。fenjing默认生成的payload在规则简单时可能很短可一旦过滤规则复杂payload长度就会指数级上涨直接被服务器或网关截断。遇到这种情况我的经验是先看能否减少层级。比如优先用cycler而不是lipsum优先用__init__.__globals__而不是层层__mro__去找object。Jinja2里能一步到位的模块有很多关键是找到目标环境里存在且权限允许的那一个。payload的优化本质上就是把访问链剪短。6.2 误报与漏报的常见来源fenjing的探测结果也不是100%可靠。我遇到过两种情况一种是把普通报错页面误判为注入成功。有些框架渲染出错时会把原始表达式原样回显你输入{{7*7}}页面也回显{{7*7}}看起来像成功了其实是报错信息里包含了输入。这时候一定要看页面里是否真的有49这个计算结果而不是只看输入是否被回显。另一种是payload生成成功但目标并不执行。这种情况多出现在模板引擎虽然有Jinja2语法支持但应用开启了沙箱模式禁用了部分内置函数。此时工具生成的payload可能在探测阶段通过了但在真正执行rce时被沙箱拦下来。所以拿到payload后不要直接信任一定要自己验证一遍完整链路。6.3 授权边界与合规提醒最后说一点我认为非常重要的边界意识。SSTI相关的工具和技术只能在你有明确授权的目标上测试比如自己搭的靶场、公司授权的红队项目、客户签了测试授权书的渗透项目。vulhub这类靶场环境是最好的练习场地不要在真实互联网上对着你没权限的目标跑任何自动化工具。尤其像fenjing这种能直接生成RCE payload的工具一旦在未授权目标上使用性质已经完全变了。我不反对你学习技术、在靶场里把每个功能都摸透但摸透和上手目标网之间隔着明确的法律和道德红线。希望看到这里的同行都能守住这条线。我在实际使用中还摸索出一个习惯每次测试完把目标环境里生成的payload按过滤规则类型分类记录下来。下次遇到相似规则时直接翻自己的笔记比重新跑工具快得多。比如过滤下划线 过滤了class关键字这种情况我笔记里就存了一两条最优解。工具负责解决新问题笔记负责沉淀老问题两者配合才是真正的效率。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →