
1. 先搞清楚我们到底在打什么SSTI 的本质与检测思路1.1 模板注入一开始是怎么“漏”出来的服务端模板注入SSTI说到底就一句话用户输入被当成了模板代码去执行。很多现代 Web 应用为了快速渲染页面都会引入模板引擎比如 Python 的 Jinja2、PHP 的 Twig/Smarty、Java 的 FreeMarker、Ruby 的 ERB 等等。正常逻辑是这样的模板里写死一堆占位符后端拿着用户数据去填充渲染完再返回 HTML。问题就出在——如果开发图省事把用户输入直接拼接进模板字符串里那就等于把代码执行权交了出去。拿一个非常典型的错误示范来说from flask import Flask, request, render_template_string app Flask(__name__) app.route(/) def index(): name request.args.get(name, guest) template fh1Hello, {name}!/h1 return render_template_string(template)这种写法我在测试中见得太多了。开发者本意是动态拼接一个简单的问候语但render_template_string()会把整个字符串当作 Jinja2 模板来解析。传入{{7*7}}的时候页面直接给你渲染出49。到这里SSTI 的雏形就已经出现了。我遇到不少刚开始接触安全测试的朋友一听到“模板注入”就觉得特别抽象不知道它到底能干什么。其实你可以把它理解成服务端直接把你的输入当成“代码草稿”来编译执行。一旦确认了这个行为后续的利用链就是沿着“读表达式 → 读对象 → 找类 → 找函数 → 执行命令/读文件”这条线往下走。1.2 手工探测与判断注入点的几个关键动作在把 fenjing 丢上去自动跑之前我强烈建议你先手工确认一下注入点存不存在。因为自动化工具能帮你省时间但没法帮你理解“为什么这个点能打”。手工确认一般分三步走第一步纯表达式测试。输入{{7*7}}看返回是不是49输入${7*7}看返回是不是49输入% 7*7 %看返回是不是49。这一步能大致判断出用的是哪一类模板引擎语法。第二步看报错信息。输入一个明显不闭合的表达式比如{{如果应用开了调试模式页面上很可能直接抛出异常堆栈里面会写清楚用的什么模板引擎、框架版本、甚至服务器绝对路径。这些信息对于后续选 payload 非常关键。第三步尝试对象探活。比如 Jinja2 环境下输入{{ config }}看看能不能把配置打出来Twig 环境下试试{{ _self }}。这一步的目的是确认基础的对象访问链路是不是通的为下一步深挖做准备。手工确认结束之后你才会真正面临一个头疼的问题WAF 把关键字、字母、数字统统过滤了你辛辛苦苦构造的 payload 发过去直接 403。到这一步就该轮到 fenjing 登场了。2. fenjing 的设计思路它凭什么能绕 WAF2.1 一个专门为 SSTI 绕 WAF 而生的工具fenjing 是一个用 Python 写的开源工具核心目标就是自动分析目标对 SSTI payload 的过滤规则并生成能够绕过这些过滤的 payload。这个工具在 CTF 圈子和红队渗透测试里用得很多因为它解决了一个非常实际的痛点目标 WAF 规则千奇百怪人工去猜过滤了什么、没过滤什么效率低且容易漏。我最初接触 fenjing 的契机是在一次授权测试里目标站点有 WAF而且对常见的__class__、__globals__、os、popen这些关键字做了严格过滤连cat、flag都不能出现。手工拼 payload 拼了一个下午最后还是靠 fenjing 的自动生成功能把命令执行打通了。从那之后我就把 fenjing 列进了自己的 SSTI 测试工具箱。它的工作逻辑你可以理解成一个会“试错”的编译器给它一个目标模板引擎、一个期望执行的命令它在后台不断组合各种绕过方式直到满足目标环境的过滤条件。这个过程对人类来说是体力活但让机器来干就很快。2.2 几个核心的绕过手段fenjing 在生成 payload 时会用到大量的绕过技巧其中几个比较关键的点是字符串构造WAF 过滤了os这种明文字符串那就用request.args.a配合GET参数去传值或者用dict(__globals__x)|join这类从字典键名中提取字符串的办法绕开直接引号。属性访问重写过滤了__class__就用attr过滤器或者[]配合字符串拼接来访问比如[__class__]。下划线与花括号处理很多 WAF 会过滤连续下划线fenjing 会把 payload 转换成\x5f十六进制编码、|string变形、request传参等多种形式让下划线不直接出现在请求里。关键字拆分os.popen被拆成os[popen]或者用getattr去动态取避免关键字整体出现。这些手段单拎出来都不算新鲜但 fenjing 厉害的地方在于它能根据目标实际回显来自动决定用哪一套组合拳而不是固定一个 payload 走天下。2.3 工具模块拆解在深入使用之前先搞清楚 fenjing 的模块划分会帮你省很多事。它主要分成几个部分命令行入口提供--url指定目标、--method指定请求方法、--cookie带入登录态、--header自定义头部等参数。payload 生成引擎负责根据模板引擎类型、过滤规则、可用字符集来生成候选 payload。WAF 探测模块请求目标环境分析响应码和回显内容判断哪些字符或关键字被过滤。注入点管理支持指定 GET/POST 参数名也支持从请求体中动态识别注入位置。交互式控制台--console参数进入一个类似sqlmap的交互界面可以用 SQL 风格的命令去查询当前环境信息、执行命令、写入文件等。理解这些模块之后你就知道 fenjing 不是只会丢一个 payload 的工具而是一套完整的“探测—分析—生成—利用”流水线。3. 环境准备与基本使用把 fenjing 跑起来3.1 安装与首次运行fenjing 的安装很简单直接走 pippip install fenjing执行完查看版本确认装好了fenjing --version如果你的网络环境有代理限制pip 可能会比较慢可以手动指定国内镜像源比如清华源或者阿里源这个根据你自己的实际网络情况来。也可以用python3 -m fenjing这种方式去启动效果和直接敲fenjing一样。首次运行最基础的使用方式就是指定一个目标的 URL 和一个可能存在注入的参数。比如fenjing --url http://target.com/hello?nametest --method GET --inputs name这条命令的含义是去检测http://target.com/hello这个页面里GET 参数name是否存在 SSTI 注入点。如果检测到了fenjing 会继续尝试生成可用的 payload并在终端里输出内容。3.2 命令行参数详解fenjing 的参数不少我把几个高频使用且必须搞清楚的参数列出来参数作用举例--url指定目标 URL--url http://x.com/?a1--method请求方法GET/POST--method POST--inputs指定注入参数名多个用逗号分隔--inputs name,email--data设置 POST 请求体--data useradminpass123--cookie带 Cookie 访问登录态--cookie sessionabc--header自定义请求头可多次指定--header X-Forwarded-For: 1.1.1.1--user-agent自定义 UA--user-agent Mozilla/5.0--level检测深度数字越大探测越细--level 3--delay请求间隔单位秒避免被限速--delay 0.5--console进入交互式控制台--console这里稍微展开一下--level的作用。level 越高引擎会尝试越多样的绕过组合相应地耗时也越长。我平时的习惯是先用--level 1做快速探测确认注入点存在后再针对某个具体目标提级去生成最终可利用的 payload。3.3 一个最简单的实战探测实例假设你拿到了一个目标地址http://vuln-app.com/search?keywordhello怀疑keyword参数存在模板注入。直接跑fenjing --url http://vuln-app.com/search?keywordhello --method GET --inputs keywordfenjing 发探测请求后如果返回结果里出现了类似7*749的渲染迹象它就会判定为存在 SSTI然后自动进入 payload 生成阶段。终端里会不断刷新任务状态最后输出类似这样的一段内容[] 目标存在 SSTI 注入 [] 模板引擎: Jinja2 [] 使用 payload: {{...}}到这一步工具层面的基本链路就通了。不过我得提醒一句直接用默认参数跑只是第一步真实场景里你要面对的往往是要带 Cookie、要带动态 token、要绕过 WAF 的重重限制。接下来我详细讲讲更贴近实战的用法。4. 实战中的核心操作从探测到命令执行4.1 带认证状态的目标怎么打大多数情况下真正有价值的注入点都在登录之后才会出现。比如后台的搜索框、导出功能、模板配置界面。这时候如果你不带 Cookie 去测连页面都访问不了更别提探测注入了。fenjing 处理登录态的方式很直白把浏览器里已有的 Cookie 直接丢给它fenjing --url http://admin-app.com/settings/search?qtest --method GET --inputs q --cookie sessioneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx有些应用还需要额外的自定义 Header比如特定的X-CSRF-Token、内部的认证头。这时候用--header参数一个个加进去就行fenjing --url http://internal-app.com/debug?cmdtest --method GET --inputs cmd --header X-Forwarded-For: 127.0.0.1 --cookie tokenabc123很多内网应用只允许来自特定内网 IP 的访问设置X-Forwarded-For头是很常见的绕访问控制手段之一。当然实际测试中你不能只依赖这个头得结合自己观察到的应用逻辑来做判断。4.2 自动生成绕过 WAF 的 payload现在到关键部分了。假设你遇到一个过滤很严格的目标字母数字不能直接出现__class__这种关键字直接 403单引号双引号也被处理了。这时候手工构造简直要人命但我们让 fenjing 来干。设置一个较高的检测深度并指定要执行的命令fenjing --url http://waf-target.com/index?namefoo --method GET --inputs name --level 5 --execute id这里我解释一下为什么单独给一个--execute参数。fenjing 在执行模式下会针对“执行系统命令 ”这个目标去生成 payload而不是只验证注入点存在。它会尝试各类命令执行的类链比如 Jinja2 环境下经典的{{ config.__init__.__globals__[os].popen(id).read() }}当然这个原始 payload 是不可能直接过 WAF 的。fenjing 会把它做各种变形例如下划线替换成十六进制编码形式os字符串拆开popen改写成属性访问形式甚至把整个 payload 拆成多段藏进request.args里再在模板中聚合拼接。运行结束后fenjing 会在终端输出最终可用的 payload 和一个测试请求。你复制出来丢到 Burp Suite 里重新发一次就能看到命令执行回显了。4.3 交互式控制台的使用技巧对于复杂的利用场景命令行一把梭不一定够用。fenjing 提供了--console交互模式进入之后你可以像操作 shell 一样对已认定的注入点做更细的测试fenjing --url http://target.com/?id1 --method GET --inputs id --console进去之后几个常用指令可以先试一下查看当前识别到的模板引擎信息。测试单条 payload 在当前环境的存活情况。执行系统命令直接读取目标机器上的文件。退出当前会话。这个交互模式的价值在于你可以一条一条地验证 payload而不是每次从头探测。尤其是目标 WAF 规则会变比如频率限制触发后 WAF 升级了规则你能在交互式环境里快速调整策略重新生成绕过组合。4.4 命令执行之后的常见操作一旦你通过 fenjing 拿到了命令执行权限下一步具体做什么取决于你的测试目标。常规的授权测试流程里我会优先做这几件事读取应用配置文件和敏感文件比如/etc/passwd、.env、数据库连接字符串、Web 目录下的配置文件。执行id、uname -a这类系统信息探测确认当前进程权限。列出当前目录和 Web 目录下的文件找找有没有备份文件、源代码压缩包。这里要注意执行命令不能太“贪”。我见过一些人拿到命令执行之后疯狂执行各种下载命令、弹 shell、装工具结果触发主机安全软件告警导致整个测试周期被叫停。授权测试里低调和自律是底线。你只需要拿到能证明危害的命令执行结果记录好时间点、payload、回显内容就可以收工了。5. 深挖绕过机制为什么 fenjing 能搞定“过滤字母数字”这种变态规则5.1 字母数字被过滤后最基本的两板斧热搜词里有两个点特别贴近实战“ssti 过滤字母数字”和“ssti 模板注入”。前者是很多 WAF 的共同特征——特别是针对 Linux 下的安全狗、云锁这类产品你只要在请求里出现连续的字母串就会直接被拦。那怎么绕过第一板斧是用字符编码替代明文。比如 Python 里__class__写成__\x63lass__Java 的 Runtime 用\x52untime。WAF 如果只做明文关键词匹配这种编码过的字符串就能直接穿透。第二板斧是利用模板引擎本身的解析特性去“重组”字符串。比如 Jinja2 里可以借助~拼接符把__cl和ass__拼起来但这个拼接动作发生在模板执行期WAF 看到的是两段互不相干的字符串自然没有拦截理由。fenjing 在生成 payload 时会把这两板斧组合出动。你以为它只是在编码其实它是把“可用字符集”作为约束条件在这个条件下穷举所有能完成目标操作的表达式结构。5.2 动态属性与函数查找的终极形态SSTI 的利用核心分两步第一步是拿到一个基础对象第二步是从这个对象出发通过一层层的属性访问找到危险函数。如果没有 WAF最经典的 Jinja2 命令执行链长这样{{ .__class__.__mro__[1].__subclasses__() }}但 WAF 一看到__class__就连连摇头。这时候的办法是用attr过滤器配合拼接{{ |attr(__class__)|attr(__mro__) }}或者是用request.args传值的方式把真正重要的关键字全部放在请求参数里{{ |attr(request.args.a) }} # GET: ?a__class__fenjing 内部其实维护了一套非常庞大的“利用链模板库”。不同模板引擎、不同 Python 版本、不同字符过滤规则下它都能从库里挑出备选模板再逐条去做“能不能在当前环境跑通”的验证。这就是为什么它能比人手更快地找到可用 payload 的原因。5.3 遇到过滤引号和点号怎么办除了字母数字引号和点号也是 WAF 的常客。如果把和都过滤了你没法直接写字符串字面量如果把.过滤了你没法直接做属性访问。引号的问题fenjing 通常会用request.args或request.values把字符串塞到请求参数里模板里只用request.args这个字面量去取不直接写字符串。点号的问题则用[]加属性名的方式替代# 原始{{ config.__class__ }} # 变形{{ config[__class__] }}但这里又牵扯出一个下划线的问题。连续下划线太多容易被 WAF 盯上。这时可以借助过滤器二次处理{{ config|attr(__class__) }}这种表达式在 WAF 看来就是几个短字符串的拼接完全不像是攻击 payload。fenjing 最恐怖的地方在于它会把这类组合当成默认选项去遍历人类可能想不到的排列方式它几秒钟就跑完一遍了。5.4 模板引擎之间的差异fenjing 也会判断实际测试里你会发现不同模板引擎的利用链差异极大。Jinja2 有__subclasses__()大法Twig 有_self和filter链FreeMarker 有?new()和apiSmarty 有{php}标签。fenjing 在探测阶段会尽量识别目标引擎类型然后只针对这一类引擎生成对应的绕过方案。这跟那种“一个 payload 打天下”的脚本工具完全不同。你用 fenjing 打完一个目标之后最好养成看日志的习惯它的日志里会写明它判定目标是什么引擎、用了哪几条绕过链路、最终哪一条成功了。这些信息是极好的学习材料。6. 常见坑点与排查心得实录6.1 参数带不进去或注入点找错很多人用 fenjing 第一次就跑不通最常见的两类问题一类是参数名写错或用错了请求方式。目标明明是通过 POST 的 JSON 体传参你非要用--data qtest这种普通表单格式发送应用后端根本解析不到你的输入。这种情况先拿 Burp Suite 抓一个正常请求看清楚请求体格式再决定怎么设置 fenjing。另一类是找错了注入点。有些参数是数字类型的后端会对输入做int()转换这种情况下你再怎么注入也没用。你得先通过手工观察响应差异选出应用会把内容“渲染回来”的参数作为目标。6.2 WAF 拦截和自动封 IP 怎么处理目标有 WAF 时你跑了一会儿突然发现后续请求全部 403甚至服务直接不稳定那大概率是被临时封禁了。这种情况处理起来不复杂但需要你提前给自己的测试节奏设好规矩请求间隔加长一点设置--delay 1或更长。加一个看起来正常的 User-Agent别用默认的 Python UA。如果目标对单 IP 有请求频率限制可以在团队授权情况下合规地使用代理池。这里提醒一句这个环节最容易踩到合规红线。永远记住你是在做授权测试请求量要控制在合理范围内不要因为你的探测导致目标业务受损。6.3 payload 生成了却执行不了命令fenjing 生成了一个看起来能绕 WAF 的 payload但你手动放到 Burp Suite 里重放却执行失败。这种情况我碰到过很多次原因主要有三类第一类是上下文差异。fenjing 探测时使用的注入点上下文和你手动重放时用的注入点上下文不一致。比如有的模板里用户输入被放进了双引号包裹的属性值里有的被放进了表达式区域里同样的 payload 在两种场景下解析结果完全不一样。这时不要只复制 payload要把 fenjing 测试时的完整请求上下文都拿过来对比。第二类是编码问题。命令行工具输出的 payload 里可能含有控制字符或十六进制转义你复制到其他工具时被转义变了形。建议用--output参数把 payload 保存到文件里再用脚本读取。第三类是目标引擎版本差异。有些旧版本的 Jinja2 里某些过滤器不能用fenjing 在自动化判断时可能高估了目标环境的能力。这种情况我会手动检查一下目标返回的报错信息确认引擎版本后再手工微调 payload。6.4 快速排查速查表现象常见原因解决方法探测时返回 404/403参数名错误、缺少认证头用 Burp 抓正常请求对比提示存在注入但生成 payload 失败WAF 规则太严格、上下文特殊提高 level、换个注入参数、手工观察报错payload 放 Burp 重放失败URL 编码被改变、请求上下文不同用文件保存 payload、对齐完整请求执行命令回显为空目标命令无回显、沙箱环境改用写文件或 DNS 外带方式验证请求被限速触发 WAF 频率限制加--delay、降低并发、更换合规出口6.5 多试一个“沙箱环境”的思路还有一类目标是 Python 沙箱环境模板引擎被做了严格的白名单限制比如__globals__、os、subprocess全被禁用。这种情况下 fenjing 不一定能直接打通但它的探测结果能告诉我们哪些字符和关键字是安全的。我会手动基于它的输出尝试一些更底层的绕过比如借助format方法的魔术方法链、利用已导入模块的副作用对象等。不过这类深入利用已经超出“用 fenjing 一把梭”的范畴属于需要扎实 Python 对象模型功底的进阶操作。新手阶段先把 fenjing 用熟看明白它生成的 payload 的每一段在做什么比什么都重要。最后再分享一个小技巧在实际使用 fenjing 的过程中有一点是我最后想强调的不要只把它当黑盒工具用。它生成的 payload 一定要自己拆开看把每一段用什么过滤器、为什么要这样绕过、在哪一步突破了 WAF 的哪条规则搞清楚。我带了几个新人他们用 fenjing 用得飞起但问起“为什么这里要加attr过滤器”就答不上来。工具能让你快速拿到结果但真正让你在下次遇到新 WAF 时不抓瞎的还是对绕过原理的理解。如果目标站点是授权测试范围内的拿到命令执行后也别着急收工多花几分钟记录整个利用链从最开始的注入点到最终执行的完整请求包都保留好。这类复现材料以后既可以是报告的素材也可以在你面试讲项目经历时变成最有说服力的案例。根据我的经验SSTI 这条线一旦打通后面很多高难度目标都会变得轻松不少。希望这篇实战记录能帮你在下次遇到 SSTI 过滤时少走点弯路。