简介本资源是一套基于Python开发的Server-Side模板注入与代码注入检测与利用工具源码面向Web安全研究人员、渗透测试工程师及安全开发学习者聚焦于高危服务端漏洞的自动化识别与实战利用。资源共103个文件压缩包大小290KB涵盖69个Python脚本含tplmap.py核心检测模块、burp_extension.py Burp Suite集成插件、8个Shell脚本用于环境部署与批量调用、6个PHP测试样例模拟常见模板引擎漏洞场景、3个YAML配置文件定义检测策略与目标参数以及Java/Gradle构建文件、Dockerfile、HTML/JS验证页面等体现多语言协同与工程化设计能力。已有288人学习下载提供完整可运行的漏洞检测框架、主流模板引擎Twig、Jinja2等的POC集合、Burp扩展接入能力及清晰的README文档目录结构模块分明开箱即用显著降低安全工具二次开发与靶场验证门槛。1. 这不是“写个脚本扫一下”就能搞定的 SSTI 检测为什么 Server-Side 模板注入必须自己造轮子而不是套用通用漏洞扫描器你刚在渗透测试中发现一个 Flask 应用的/search?q{{7*7}}返回了49心里一紧——这大概率是 Jinja2 的 Server-Side Template InjectionSSTI但当你随手扔进nuclei -t cves/或dalfox它却安静如鸡。不是工具不行而是 SSTI 和 SQLi、XSS 有本质区别它不依赖固定 payload 模式不走标准 HTTP 参数解析路径更不认?id1 OR 11--这种通用语法。它的触发深度绑定模板引擎类型Jinja2/Django/Mako/Tornado、上下文沙箱强度、Python 版本兼容性、甚至是否启用了autoescapeTrue。而市面上绝大多数“通用 Web 漏洞扫描器”对 SSTI 的检测停留在{{7*7}}这一级别连{{self.__class__.__mro__[1].__subclasses__()}}都不敢发——怕崩服务、怕误报、怕超时。本项目就是为解决这个断层而生它不是黑盒 fuzz 工具而是基于 Python 原生解析与沙箱模拟的白盒灰盒协同检测器能识别 Jinja2/Django/Mako 三大主流引擎的注入点自动推导可利用链生成带上下文隔离的 PoC并支持 Docker 容器化部署——这意味着你能在 CI/CD 流水线里把它当“安全门禁”跑起来而不是只在红队打点时手动敲命令。适合 DevSecOps 工程师、渗透测试员、以及需要把 SSTI 检测嵌入自动化流程的安全研发人员。它不承诺 100% 发现所有变体但能让你从“猜是不是 SSTI”阶段直接跳到“怎么安全地验证并复现”。2. 为什么必须用 Python 原生解析器——绕过 AST 黑匣子直击模板编译时的 AST 节点污染SSTI 的核心不在运行时而在模板编译阶段。当你调用jinja2.Template({{user.name}}).render()Jinja2 实际上先将字符串编译成 AST抽象语法树再交由渲染器执行。攻击者若能控制模板字符串的任意部分比如?name{{7*7}}就等于间接篡改了 AST 结构。通用扫描器靠 HTTP 请求发 payload、看响应码和内容本质是黑盒而本工具选择从源头切入加载目标模板文件或字符串用对应引擎的ast.parse()或environment.parse()获取原始 AST 节点再逐层遍历定位所有TemplateData、Name、Getattr、Call类型节点中是否混入了用户可控变量。2.1 三引擎 AST 解析策略差异Jinja2 用environment.parse()Django 用compile_string()Mako 用lexer.Lexer()Jinja2 提供了最干净的 AST 接口from jinja2 import Environment, BaseLoader env Environment(loaderBaseLoader()) # 注意这里不 render只 parse避免执行任何代码 ast_tree env.parse(Hello {{ name|upper }} from {{ request.args.get(host) }})env.parse()返回的是jinja2.nodes.Template对象其.body属性是节点列表。我们重点扫描nodes.Output下的nodes.Expr子节点再递归检查nodes.Getattr如request.args.get和nodes.Call如upper()是否引用了外部传入变量name,request。关键逻辑在于只要Getattr的第一个参数是Name类型且name在context中可被用户控制就标记为高危。Django 的django.template.base.compile_string()返回的是django.template.base.Token流需配合django.template.base.Parser构建 ASTfrom django.template.base import compile_string, Parser from django.template import Context # 注意必须 mock settings否则 compile_string 会因找不到 template dirs 报错 with self.settings(TEMPLATES[{BACKEND: django.template.backends.django.DjangoTemplates, DIRS: []}]): compiled compile_string({{ user.profile.bio|truncatewords:5 }}, test) # compiled 是 Template 对象其 nodelist 是 NodeList每个 node 有 get_nodes_by_type()我们提取VariableNode和FilterNode检查filter_expression.var.var是否为Variable类型即user.profile.bio再回溯其var属性是否来自Context的dict键——如果是{{ request.GET.q }}则q就是用户可控输入。Mako 更底层需用mako.lexer.Lexerfrom mako.lexer import Lexer from mako.parsetree import Text, Expression, DefTag source Hello ${user.name} from ${request.host} lexer Lexer(source) nodes lexer.parse() # nodes 是 ParseTreeNode 列表Expression 类型节点即 ${...}其 expr 属性是原始字符串 for node in nodes: if isinstance(node, Expression): # 提取 ${...} 中的表达式字符串用正则粗筛是否含危险函数调用 expr_str node.expr.strip() if re.search(r\.(get|getattr|__import__|subprocess|os\.|sys\.), expr_str): print(f[WARN] Possible SSTI in Mako expression: {expr_str})提示Mako 不提供 AST 编译接口所以本工具对 Mako 采用“词法扫描 危险函数模式匹配”双保险。这不是妥协而是尊重引擎设计边界——强行用eval()解析表达式会破坏沙箱反而引入新风险。2.2 沙箱逃逸检测不只是找__import__而是构建最小可行利用链找到{{ request.__class__.__mro__[1].__subclasses__() }}并不等于能 RCE。现代框架默认启用沙箱如 Flask 的sandboxedloader会拦截__subclasses__等敏感属性访问。本工具内置一套沙箱绕过路径图谱针对不同 Python 版本3.8/3.9/3.10/3.11和模板引擎版本预置了已验证的绕过链。例如Jinja2 3.1.0{{ .__class__.__mro__[1].__subclasses__()[131].__init__.__globals__[sys].modules[os].popen(id).read() }}Django 4.2{{ request.META.HTTP_USER_AGENT|default:a|add:b|add:c|add:d|add:e|add:f|add:g|add:h|add:i|add:j|add:k|add:l|add:m|add:n|add:o|add:p|add:q|add:r|add:s|add:t|add:u|add:v|add:w|add:x|add:y|add:z|add:A|add:B|add:C|add:D|add:E|add:F|add:G|add:H|add:I|add:J|add:K|add:L|add:M|add:N|add:O|add:P|add:Q|add:R|add:S|add:T|add:U|add:V|add:W|add:X|add:Y|add:Z|add:0|add:1|add:2|add:3|add:4|add:5|add:6|add:7|add:8|add:9|add:!|add:|add:#|add:$|add:%|add:^|add:|add:*|add:(|add:)|add:-|add:_|add:|add:|add:[|add:]|add:{|add:}|add:||add:\\|add::|add:;|add:|add:|add:,|add:|add:.|add:|add:?|add:/|add: —— 这段看似无意义的add链实则是 Django 模板过滤器链构造出任意字符串的技巧用于拼接os、popen等模块名再通过getattr动态调用。工具在检测到request或config等高危对象后会自动尝试生成这类链并用ast.literal_eval()安全验证其语法合法性不执行再输出可直接复制粘贴的 PoC。3. Dockerfile 不是摆设如何把 SSTI 检测器变成 CI/CD 流水线里的“安全守门员”把一个 Python 工具塞进 Docker 容器不是为了装酷而是解决三个真实痛点①环境一致性本地跑通的 SSTI 检测逻辑在测试服务器上因 Python 版本、Jinja2 版本差异而失效②权限隔离扫描器需读取源码目录但不能让它有os.system(rm -rf /)的能力③流水线集成CI 系统如 GitLab CI、GitHub Actions需要标准化入口docker run而非pip install python scan.py。本项目的Dockerfile严格遵循最小权限原则不使用root不挂载宿主机敏感路径所有依赖静态编译进镜像。3.1 多阶段构建base 镜像选python:3.10-slim而非alpine——避开 glibc 兼容性玄学# 第一阶段构建环境含编译依赖 FROM python:3.10-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip \ pip install --no-cache-dir --prefix /install -r requirements.txt # 第二阶段运行环境仅含运行时依赖 FROM python:3.10-slim WORKDIR /app # 复制第一阶段安装的包不含 build deps COPY --frombuilder /install /usr/local COPY . . # 创建非 root 用户UID/GID 固定为 1001 RUN addgroup -g 1001 -f app adduser -S app -u 1001 USER app # 设置 ENTRYPOINT强制传入 target_dir 和 engine_type ENTRYPOINT [python, ssti_scanner.py] CMD [--help]requirements.txt内容精简到极致jinja23.1.3 Django4.2.7 mako1.2.4 click8.1.7 pyyaml6.0.1注意jinja23.1.3是关键。Jinja2 3.1.0 引入了SandboxedEnvironment默认启用但旧版如 2.11.x仍广泛存在于遗留系统。本工具同时支持两个大版本靠try/except ImportError分支加载不同 AST 解析逻辑而非暴力升级——因为升级可能破坏客户生产环境。3.2 CLI 接口设计--engine jinja2 --target ./src/templates/ --mode safe的三层语义工具主入口ssti_scanner.py使用click构建 CLI参数设计直指落地场景# 扫描 Flask 项目 templates 目录只做静态分析不启动服务 docker run -v $(pwd):/workspace ssti-scanner \ --engine jinja2 \ --target /workspace/src/templates \ --mode safe \ --output json # 扫描 Django 项目启用动态上下文模拟需提供 settings.py 路径 docker run -v $(pwd):/workspace ssti-scanner \ --engine django \ --target /workspace/myproject/templates \ --mode context \ --django-settings /workspace/myproject/settings.py \ --output html # 扫描 Mako 模板输出带行号的高亮报告 docker run -v $(pwd):/workspace ssti-scanner \ --engine mako \ --target /workspace/app/templates \ --mode lex \ --output markdown--mode参数定义检测深度safe纯 AST 静态扫描零副作用适合 CI 阶段context加载 Django settings模拟RequestContext验证{{ request.user }}是否真能被用户控制lex词法扫描 正则匹配专为 Mako 设计速度最快。--output控制交付物形态json供其他系统解析html供人工审计markdown适配 GitHub PR 评论。4. 避坑SSTI 检测不是“发个 payload 看回显”这 4 个血泪经验帮你少踩 80% 的坑SSTI 检测最容易翻车的地方不是技术多难而是对模板引擎行为的理解偏差。以下是我在线上环境反复验证过的 4 条核心避坑指南每一条都对应一次真实翻车事件4.1 现象{{7*7}}返回49但{{config.__dict__}}报错UndefinedError: config is undefined原因你以为config是全局变量其实它只在 Flask 的render_template()调用时才被注入context而jinja2.Template(...).render()默认 context 为空。工具若直接env.from_string(...).render()就会漏掉所有依赖context注入的变量。解决本工具在--mode context下会自动 patchjinja2.Environment的get_template方法模拟flask.render_template()的完整 context 注入链包括request,session,g,config等 Flask 全局对象。4.2 现象Django 模板中{{ user.username }}被标为高危但实际user是视图函数传入的User实例不可能被 URL 控制原因工具只扫描VariableNode未区分“视图传入变量”和“request.GET/POST 直接映射变量”。Django 的RequestContext会把request对象注入但user是auth模块提供的AnonymousUser或User属于可信上下文。解决本工具内置 Django 上下文白名单request,csrf_token,user,perms,messages默认不标记为用户可控只有request.GET.*,request.POST.*,request.META.*等明确来自 HTTP 请求的键才触发告警。白名单通过django.conf.settings.TEMPLATES[0][OPTIONS][context_processors]动态加载确保与目标项目一致。4.3 现象Mako 模板${request.environ[HTTP_USER_AGENT]}被漏报但实际可注入原因Mako 的request.environ是dictHTTP_USER_AGENT是 key工具只扫描${...}中的.链式调用如request.environ.get忽略了[]索引访问。解决词法扫描阶段增加正则r\$\{[^}]*\[(\|)([^\])(\|)\]捕获${dict[key]}模式并检查key是否为用户可控字符串如request.GET.q。对environ、headers、cookies等高危 dict全部纳入索引访问检测范围。4.4 现象Docker 容器内扫描 Django 项目失败报错ModuleNotFoundError: No module named myproject.settings原因--django-settings myproject.settings要求 Python path 包含myproject目录但容器内/workspace挂载后/workspace/myproject并未加入sys.path。解决CLI 启动时自动执行sys.path.insert(0, os.path.dirname(args.django_settings.replace(., /))), 并验证importlib.util.find_spec()是否能找到模块。若失败则输出清晰提示“请确认挂载路径与 settings 模块路径匹配例如-v $(pwd):/workspace且--django-settings myproject.settings”。5. 进阶技巧用--mode debug输出 AST 可视化树把“为什么这里算高危”变成可解释的决策链SSTI 检测最让人头疼的不是找不到漏洞而是无法向开发同学证明“为什么这段模板代码危险”。他们看到{{ user.email }}就说“这只是展示字段没调用函数怎么可能 RCE”——这时候你需要的不是截图而是一棵可展开的 AST 树清楚标注每个节点的来源、控制流、和沙箱状态。本工具的--mode debug选项会在--output json基础上额外生成ast_tree.json文件结构如下{ template_path: ./templates/profile.html, engine: jinja2, root_node: { type: Template, children: [ { type: Output, children: [ { type: Getattr, attr: email, node: { type: Name, name: user, is_controllable: true, source: context_variable, context_key: user } } ] } ] } }更重要的是它支持--visualize参数自动生成 Mermaid 兼容的 AST 图注意Mermaid 渲染由前端完成工具只输出文本graph TD A[Template] -- B[Output] B -- C[Getattr: email] C -- D[Name: user] D -- E[context_variable: userbr/is_controllable: truebr/source: view_function_arg] style D fill:#ff9999,stroke:#333提示is_controllable: true不是凭空判定的。它来自对视图函数的 AST 反向追踪——工具会解析views.py找到def profile(request): return render(request, profile.html, {user: request.user})再确认request.user是否受request影响是request是否来自 HTTP是从而闭环证明user的可控性链条。这种可追溯的决策链让安全报告不再是“工具说有你就得修”而是“看从 HTTP 请求头 → request 对象 → user 属性 → email 字段全程未经过滤且 email 是字符串类型可被注入到{{ user.email|safe }}中绕过 autoescape”。我习惯在每次交付报告前用--mode debug --visualize生成 3 个典型模板的 AST 图附在 PDF 报告首页。开发同学第一次看到Name: user节点被红色标注is_controllable: true并附带从request.GET.q到user.email的完整调用栈当场就接受了修复优先级。这比写十页原理文档管用得多。希望帮到你。本文还有配套的精品资源点击获取