ssti模板注入手法总结python篇(一)
这个是一个适合入门打ctf的同学学习的文章,本文尽量使用通俗的话教学
文章的大概的结构
- 基础
- 漏洞
- 绕过
- 工具
- pin码(扩展)
基础
这个是基础板块,主要讲解python的flask和python的类
这里介绍两个靶场的地址,有兴趣的可以去看看
1 | |
这里介绍另外一个靶场的搭建,也是本教程使用的靶场,使用docker搭建
1 | |
python虚拟环境
当我们搭建完靶场之后先放一放,后面才开始使用这个靶场
首先讲讲python的虚拟环境,这个主要是为了后面自己使用flask做铺垫,避免污染全局python环境
这个是基础中的基础,已经熟悉的同学可以跳过
下面开始
1 | |

可以看到创建完虚拟环境之后出现了一些新的文件
正常我们使用python3 demo.py
是使用的全局python3
如果我们想使用虚拟环境的python3的话
我们可以这样操作
1 | |
安装flask
1 | |
这样安装就是在虚拟环境里面安装了
使用下面这个就可以退出了
1 | |
flask基本框架
进入前面讲的虚拟环境
写上这个代码
1 | |
看看这个代码
解释:这个就是一个简单的页面,返回hello benben
__name__可以理解为这个就是我们这个文件的文件名称
@app.route(‘/‘)路由,当我们访问这个页面的时候,执行下面这个语句
使用python运行这个文件,默认是开启5000端口
注意这个app.run(host=’0.0.0.0’, port=5000)
写成全是0,说明别人是可以访问我们的
对应的是可以写成127.0.0.1
接下来可以观察一下flask的路由是怎么操作的
将代码进行简单的修改
1 | |
我们访问/benben输出的是hello benben
我们访问/dazhuang输出的是hello dazhuang
这里我们就知道了flask的路由是长什么样子的
flask动态路由
前面我们已经知道了静态的路由是如何写的了,现在看动态路由如何写
1 | |
解释这个:我们构造了一个name 的参数,进行接受URL中的字符
然后回显的页面用的也是name 的这个参数
return使用的是格式化字符串,%s是格式化字符串
运行得到

发现我们输入的东西是可以自己控制的
我们构造的变量有三种方式
%s,%d,%f
字符,整数,浮点
所以下面这个代码也是同一个道理
1 | |
这里的int目录下是需要传入整数才行的
flaks接收参数
在demo.py这个文件的同一级目录下创建文件夹templates
在这个文件夹里面写入index.html文件
index.html文件内容如下
1 | |
然后dome文件内容是
1 | |
简单解释一下这个代码
设置一个index文件,接受参数,传入login这个虚拟目录
然后接受是什么传入方法,什么参数
回显到success这个目录的文件里面
flask模板
逻辑处理代码可以理解为后端处理代码,也就是python
模板可以理解为呈现前端是什么样子的代码,也解释html
下面做一个实验
创建文件夹templates,在这个文件夹里面创建文件index.html
1 | |
写入后端代码
1 | |
这个前端代码就使用到了模板的方法
这个就可以动态的呈现后端处理的结果
后端代码页对前段代码提供多个参数
展示结果

flask模板注入漏洞成因
前面已经把基础学了,现在到了学习漏洞长什么样子了
渲染模板时没有严格控制用户的输入
看一下没有漏洞的代码
1 | |
这个代码处理get提交的参数的时候,是先将str转义为字符串之后,再将这个字符串显示到页面上
有问题的代码
1 | |
这个代码是先将参数显示在页面上之后,再将参数转义为字符串
如果是这样操作的话,render_template_string会将我们输入的参数当做python代码进行解析,从而执行

我们通过输入这些来判断是什么模板的注入
绿色是有回显的时候指向的箭头
如果有输入回显则我们跟着绿色的箭头走
如果没有回显跟着红色的箭头走
通过那个图片我们可以很清晰的认识到
ssti远不止有python的flask中的jinjia2
还有很多很多
于是来扫个盲吧
然后我们主要关注python的
1 | |
漏洞
终于来到了漏洞环节
为了避免后面出现使用的混乱于是文章结构还是先基础之后再讲漏洞利用
漏洞基础
python类
直接看源码
1 | |
这个是个什么玩意
我们只需要知道一个
括号里面的是爹就行,所以在上面这个代码里面A是大爹
下面这个是一个清晰的介绍
1 | |
可以发现有一个叫object的类是所有类的祖宗
扩展一些全局类,这个在后面十分重要
1 | |
同样他们的祖宗都是object
python魔术方法
可以注意到上面的代码中有一个东西
1 | |
__class__和__base__
类似于这种的我们称之为魔术方法
魔术方法 = __xxx__ 格式(双下划线包起来)
好的那么有什么含义呢
输出结果是

可以看到输出的结果
第一个是自己的类
第二个是自己的父类,使用的base魔术方法
可想而知,使用__class__是一个自我认识的过程,使用__base__是一个找爹的过程
所以下面的这个代码不能的想到的结果是啥
1 | |
输出的便是爹中之爹,即A
下面这也就轻松知道结果
1 | |
输出object祖宗
魔术方法有很多种
下面展示经常使用的魔术方法
1 | |
要不要全部知道呢?答案是不需要
主要使用的是前面六个
通过代码可以轻松的认识到
1 | |
python函数
lipsum Jinja2 函数
joiner Jinja2 函数
url_for Flask 函数
get_flashed_messages Flask 函数
类似于这种的就是函数
其实很难区分,在很多情况下不知道这个是函数还是类还是对象
在进行ssti 的时候需要多多积累才行
python对象
紧接着就是对象了
config Flask 对象
request Flask 对象
session Flask 对象
g Flask 对象
current_app Flask 对象
像这种的就是对象
类、函数、对象区别
认识与区分
可以看到全局类与函数与对象都是英文单词,非常不好区分,我们为什么要区分这三者呢,因为他们使用globals魔术方法的方式不一样,对于后面构造payload的方式是不一样的
函数
1 | |
- 使用
def关键字定义 - 可以被
()调用 - 有
__globals__属性 - 类型是
<class 'function'>
类
1 | |
- 使用
class关键字定义 - 可以创建实例:
Person() - 类型是
<class 'type'> - 某些类有
__globals__,某些没有
对象
1 | |
- 通过
类名()创建 - 有自己的属性和方法
- 类型是类的实例类型
<class '__main__.Person'> - 对象本身没有
__globals__属性
区分的方法这里介绍两种
法一使用python代码
1 | |
法二使用jinjia2语法
1 | |
用globals不同
函数直接访问
1 | |
1 | |
类部分访问
1 | |
1 | |
为什么有些类可以直接用,但是有些不能直接用
因为
| 类型 | 实现语言 | 是否有 __globals__ |
|---|---|---|
| Python 编写的类 | Python | 有 |
| C 语言编写的类 | C | 没有 |
| Jinja2 类 | Python | 有 |
| Python 内置类 | C | 没有 |
懂否?python的类有的是C语言实现的
对象需要通过类链 🔗
1 | |
1 | |
简单说就是把对象使用__class__转化为类
那我问你:为什么要这么写{{ request.__class__.__init__.`?这个里面的`__init__`是干啥的?
那肯定是有些类不能直接使用`__globals__`,需要使用`__init__`,那么简单的一点说可以理解为使用`__init__`将类转化为了函数
当然不能这样粗俗的不优雅的说,上面那样讲只是为了好理解
下面是比较官方的回答
`{{ config.__class__.__init__ }} 获取的是类的构造函数方法对象,它是一个方法(method),本质上属于可调用对象(callable)
下面这个图可以好好看看
1 | |
神奇的__globals__
这个有多神奇呢?这里可以举一个例子
运行这个简单的代码
1 | |
输出结果
1 | |
我们可以发现__globals__返回的是一个字典
我们还可以发现第一个print输出的结果是
既有全局对象还有内置的模块
所以我们是不是可以知道__globals__的特性了
1 | |
然后我们观察第二个print
在这个里面我们使用了flask里面的函数url_for
结果输出的内容里面有很多的危险的模块,对象,函数
所以说用好__globals__是ssti里面必要的
神奇的__builtins__
神奇在哪里?
依旧使用一个简短的代码看看效果
1 | |
运行结果
1 | |
这些是啥
这些是一些类和函数
所以可以总结一下这个特性
1 | |
在这个里面我们发现了诸多危险的函数
1 | |
我们看到没有这里有一个小技巧
我们在使用的时候可以使用keys()函数,避免出现<built-in function input>这种奇奇怪怪的字符
神奇的__dir__()
这个跟keys()函数很像,是扩展攻击面的,看这个对象有没有危险的东西
我们还是通过一个简短的代码看看效果
1 | |
输出的结果
1 | |
所以我们知道了这个的特性
1 | |
那么这个返回的结果和keys()有什么区别呢
__dir__()返回的是该对象所有属性和方法
keys()这一般经常使用与__builtins__,url_for.__globals__[‘__builtins__‘].keys()返回的是内置的函数和类
漏洞攻击探测
为什么叫做漏洞攻击探测呢,因为我们在做题目的时候会经常遇到一些禁用个别对象或者是禁用个别函数的情况,这个部分主要是要知道ssti的攻击面非常丰富
在前面我们已经了解到了两个神奇的东西
__builtins__和__globals__
这里有一个小的tips
__builtins__的tips
首先介绍一下这个
在Python中,__builtins__是一个特殊的模块或字典,它包含了内置的函数、异常和类型。在全局作用域中,__builtins__通常是对内置命名空间的引用。在大多数Python环境中,__builtins__是 builtins模块(Python 3)或 __builtin__模块(Python 2)的引用
不难理解这个就是一个模块,里面有很多危险函数
例如我们希望利用全局类对象request来获取风险函数,很流畅地进入了,然而我们发现其globals中并不存在os模块。于是我们考虑换一种方式来导入os模块。我们可以发现在globals中的builtins中含有另一个风险函数eval,我们可以利用eval执行风险python语句从而间接导入os模块,于是就有payload:
所以我们有了另外一个方法
通过__globals__找到使用__builtins__来找到危险函数
接下来就是获取该类的方法,然后获取该方法的globals属性,观察有无os或者builtins,这是一项浩瀚的工程,可以借助脚本,也可以记住一些常见的能获取到风险函数的类。
一般来说,在绝大多数环境下,这些类经常被使用:
1 | |
讲到此处我们可以触类旁通地发现,SSTI不只有一种方法去执行,如果我们获取的类本身就足够有特点,那么也不必拘泥于常规
接下来就是需要使用这两个东西构造一个攻击代码
函数探测
lipsum利用
lipsum这个是一个函数没有想到吧
看不出来?没事没事,慢慢积累
函数的话是不是可以直接使用__globals__

查看可用属性
前面有讲过__dir__() 是 Python 的内置方法,用于返回对象的所有属性和方法列表
我们发现了风险函数popen,于是就有了经典的payload:
1 | |
url_for利用
跟上面一样的使用方法,查看属性
1 | |
发现有popen,构造
1 | |
get_fla_me利用
跟上面的一样,查看属性
之后构造
1 | |
对象和类探测
对象和类是差不多的东西使用的方法大差不差
对象构造
什么是一般化构造
1 | |
长这个样子的就是一般化构造
通过对象,找到object类,之后寻找危险函数或者类,执行命令
很显然对象远远不止这一种
1 | |
像这种
1 | |
结果是
1 | |
类构造
1 | |
这些类都可以构造
1 | |
这个脚本可以使用返回索引以及对应的类的信息
1 | |
这个主要的好处是,可以列举类的同时还可以把类的索引列举出来
在这个脚本中因为range是类所以只能寻找object这个类来获得所有的可用类
危险类和函数
类和对象的使用方法差不多
只是因为对象的话多一个步骤而已,需要使用__class__将对象转化为类
frozen_importlib_ext.FileLoader
如果这个类是79的索引的话,可以这样操作
1 | |
进行文件读取
异常类
异常是 Python 程序运行过程中发生的错误事件。当程序出现错误时,Python 会“抛出”一个异常对象
1 | |
使用方法
1 | |
简单理解一下这个
1 | |
内置函数
执行脚本
1 | |
这个脚本可以获得哪些类里面有eval执行函数
1 | |
其中相当于是使用__init__将类转化为函数
之后执行即可
如果想知道那些危险类的类名的话可以使用这个脚本
1 | |
os.py模块
我想要找到os的模块
- 通过 config,调用 os
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}} - 通过 url_for,调用 os
{{url_for.__globals__.os.popen('whoami').read()}} - 还有很多类似的自带的类的方法
1 | |
- 在已经加载 os 模块的子类里直接调用 os 模块
{{".__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls -l /opt").read()}}
使用第三种构造pyload
1 | |
构造pyload
1 | |
这里有一个小tips:不用看这里只使用了__globals__,其实也是可以使用__builtins__配合找到有危险的类或者是函数
1 | |
所以说思路要打开
第三方库importlib类
使用脚本
1 | |
构造pyload
1 | |
linecache函数
只需要修改脚本里面的返回判断的值即可
pyload构造
1 | |
subprocess.Popen 类
1 | |
subprocess.Popen类
subprocess.Popen 是一个类,可以直接实例化来执行命令,不需要通过 __globals__ 获取 os 模块
可以使用这个脚本找到subprocess.Popen类的索引值
1 | |
下面这个是payload
1 | |
执行逻辑
1 | |
warnings.catch_warnings类
通过这个可以找到
1 | |
os._wrap_close类
1 | |
file类(python2)
1 | |
catch_warnings类构造
1 | |
使用这个自己找到类,然后执行
使用这个还可以写入文件读取文件
1 | |
内存马打入
原理讲解需要狠下功夫,这里之后再研究,这里放几篇文章
直接在注入
1 | |
任何URL路径都可以加 ?cmd= 使用
可以直接执行命令
1 | |
本文共计约 3.3 万字,希望能对你理解 ssti 相关知识有所帮助。如在学习过程中遇到问题,欢迎随时与我交流探讨。
后续将更新 SSTI 模板注入手法总结(Python 篇・二),内容将以各类绕过技巧为核心展开讲解。