上一篇写到了关于python flask SSTI的总结文章,看了沙箱逃逸之后,发现这两者的方法和payload很相似,所以把python的沙箱逃逸和服务端模板注入放在一起总结。
沙箱:沙箱是一种按照安全策略限制程序行为的执行环境。
沙箱逃逸:就是在给我们的一个代码执行环境下,脱离种种过滤和限制,最终成功拿到shell权限的过程。其实就是闯过重重黑名单,最终拿到系统命令执行权限的过程。
先来给上一道例题源码:
1 | #!/usr/bin/env python |
一般而言沙箱逃逸的题目考到的知识点无非下面5个:
那么与之相对应的解题思路大致分为5步:
如果我们想在沙箱中getshell的话,必不可少的是要引入Python中执行命令的包,例如os,sys,subprocess等。
有些沙箱使用比较初级的办法,通过正则对输入代码内容进行过滤,如下所示:
1 | import re |
这个时候,我们突破这种封锁,首先要学习的是Python的各种导包方法。
一般比较常见的是以下几种方法:
第一个和第二个比较熟悉,不用过多赘述,__import__
作为一个函数,只能接受字符串参数,返回值可以直接用来操作,通常在动态加载的时候使用这个函数,python2和python3通用:
importlib模块是对import和__import__
的补充,它也可以通过传入字符串来引入一个模块,python2和python3使用方法一样:
imp库的使用方法:
reload 的用法比较有意思,假如沙箱导入了os模块,但是删除了system方法,强行使用system执行命令会报错:
而我又想用system方法执行命令的话,可以使用reload重新加载os模块,恢复对system方法的引用。
导包说到本质上其实是python 读取指定包的py文件,并将其加载到解释器的过程。在模块导入的时候,默认在当前目录下查找,然后再在系统中查找,系统查找的范围是sys.path
下的所有路径。
我使用的是Anaconda管理python版本,在一些常见的Linux发行版本上,路径一般都是在/usr/lib/python3.X
目录下。
因此我们可以直接执行对应包的文件,从而实现包的导入。在py2中有execfile这个函数:
在python3中没有execfile这个函数,但是又exec,可以通过读文件交给exec执行的方式导入包:
上面说到导包的本质是python读取指定的文件,import的本质是:搜索modules并绑定到局部变量
import module_name
实质是将module_name.py
中的全部代码加载到内存并赋值给与模块同名的变量写在当前文件中,这个变量的类型是module
现在设置一下modules中os
的值为None:
发现把os从modules
中删去就不能直接引入了。但是,我们可以接着设置os
的模块的路径,从而引入该模块:
另外,我们将 sys.modules 中的os 删除即可,这样import 发现 sys.modules没有os这个模块,就会重新创建。
(1) eval/exec/execfile
在上文中,已经讲解了exec/execfile的用法。这里再总结一下:
eval用来执行简单的python表达式返回表达式的结果,示例如下:
1 | eval('__import__("os").system("whoami")') |
(2) pickle 序列化
1 | import pickle |
保存序列化之后的字符串,然后通过pickle.loads加载即可完成代码的执行。
1 | import pickle |
(3) timeit 这个模块是用来测试代码的执行时间的,可以动态执行代码,代码是字符串形式。
1 | import timeit |
(1) os模块
可以通过os.system(cmd),os.popen(cmd)调用系统命令,例如:
1 | os.system("whoami") |
(2) commands 模块
1 | print(commands.getoutput('whoami')) |
(3) subprocess模块
subprocess模块是相对比较复杂的,有很多执行命令的函数:
(4) platform 模块
可以调用platform 模块 中的 popen 这个函数执行命令。
1 | import platform |
(5) pty 模块
pty模块可以生成一个伪终端,可以简单理解为bash,因此是可以执行命令的。
1 | import ptypty.spawn('ls') |
(6) cgi 模块
1 | import cgi |
(1)open(python2,python3)
1 | open(__file__).read() |
(2)file(python2)
1 | file(__file__).read() |
(1)codecs模块(python2,python3)
1 | import codecs |
(2)types模块(python2)
1 | import types |
(3)os.open(python2,python3)
1 | import os |
(4)file协议
python2
1 | import urllib |
python3
1 | import urllib |
(5)fileinput模块
1 | import fileinput |
如果沙箱不让我们导入外部模块,或者是要导入的模块被禁用,那我们只能求助于Python的内部模块__builtins__
( 即Python 本身默认已经导入模块中的函数)。
dir内置函数可以列出一个模块/类/对象下面所有的属性和函数,查看一下__builtins__
中的函数:
例如,我们可以引用__import__
来导入os,并执行命令:
由于内置模块中的危险函数过多,比如eval,exec等,导致上文使用的沙箱对*****builtins*****进行了处理,通过 del 关键字将里面的所有函数引用都删除了。
如果保留reload内置函数,我们还可以通过 reload( __builtins__)
恢复,但是现在通过__builtins__
来进行逃逸已经不现实了。
删除的是只是函数引用,而不是函数本身,如果你们熟悉C语言的话,函数引用可以理解为函数指针,既然__builtins__
中的引用没了,那我们就需要从其他地方找到敏感函数的引用,从而实现逃逸。
关于这一块的内容,其实和flask SSTI的内容是一样的,也是通过python的内置类型的继承链来寻找更多的引用,以下字段是寻找继承链的关键:
名称 | 介绍 |
---|---|
__dict__ |
这个属性中存放着类的属性和方法对应的键值对,实测module也有这个属性 |
__class__ |
返回一个实例对应的类型 |
__base__ |
返回一个类所继承的基类 |
__subclasses__() |
返回该类的所有子类 |
__mro__ |
python支持多重继承,在解析__init__ 时,定义解析顺序的是子类的__mro__ 属性(值是类的元组) |
__slots__ |
限制类动态添加属性 |
__getattribute__() |
获取属性或方法,对模块和类都有效 |
__getitem__() |
以索引取值或者键取值 |
__globals__ |
返回函数所在模块命名空间中的所有变量 |
其他的具体内容参考flask SSTI即可。
https://blog.szfszf.top/article/15/
https://www.m00nback.xyz/2020/02/16/Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/