Python Flask 服务端模板注入(SSTI)
与SQL注入、XSS等安全漏洞一样,SSTI(Server Site Template Injection)也是因为对用户输入过滤不当导致的,当然还有其他原因,如今的动态站点都会用到模板渲染,这就会有SSTI的风险。
Python服务端模板注入是相对Flask框架而言的,之前并没有用过该Web框架,是在做XCTF中碰到这类漏洞相关题目,于是现学现卖,写一篇SSTI的学习总结,顺便写出XCTF中两道与该漏洞相关题目的WriteUp
Flask基础
Flask是python中的一个轻量级Web框架,能够很轻易搭建一个Web站点,使用Jinja2作为模板引擎。
所谓模板引擎就是在动态站点中,将显示内容和用户数据分离,不同用户查看同一页面返回的是自己的用户数据通过模板渲染后的页面内容,所以一般模板文件都是标准的HTML文件。
Flask默认的Jinja2引擎存在以下三种语法:
- 控制结构
{% %}
- 变量取值
{{ }}
- 注释 ``
使用 {{ }}
语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换 。如果 {{ }}
中的内容是用户可控的,那么输入的恶意数据就会带入并执行,造成SSTI。
下面是一个简单的模板文件示例
1 |
|
所以在上边的模板文件中,当进行渲染时就会将查询/传入的name和age进行替换,再输出到页面。
在Flask中有以下两种方式进行渲染:
render_template_string(str):直接对str进行渲染
1
2
3def index():
template = "<h1>name: %s, age: %d</h1>" % (name, age)
return render_template_string(template)render_template(file):调用模板文件file进行渲染
1
2def index():
return render_template("index.html", name=name, age=age)
两种渲染方式效果是一样的。
SSTI环境搭建及测试
大致了解Flask相关知识后,搭建一个简单的SSTI漏洞测试环境:
1 | from flask import Flask, request, render_template_string |
实现的功能就是GET传参name,然后使用Jinja2做模板渲染,再输出到页面。
传入正常数据:
传入测试数据:
可见**{{}}
中的表达式被成功执行,即表示存在SSTI,继续输入{{config}}
**,即可执行config查看Flask相关配置:
当然SSTI的危害不止于此,利用该漏洞甚至可以达到命令执行、GetShell,这就需要沙盒逃逸。
沙盒逃逸
所谓沙盒/沙箱,其实就是将程序运行在独立的环境中,来减小程序异常(病毒等)时造成的危害,通常沙盒环境都会对环境中的可用的功能做以限制。
因此,沙盒逃逸即绕过沙盒环境中的种种限制和过滤,拿到主机的权限或shell。
对于python中SSTI的沙盒逃逸利用,则需要先了解以下知识点:
内建函数
python内部已经定义并创建了许多函数可供使用,不需要用户再定义,称之为内建函数,比如range()、print()、input()、type()
等
这些可使用的内建函数存储在__builtins__
内建对象中,当开启python解释器时程序会自动将该对象导入到命名空间,也就是自动导入该模块(import __builtin__
)所以可以直接使用其中的内建函数。
使用dir()函数查看可使用的内建函数
1 | for i in enumerate(dir(__builtins__)): print(i) |
魔术方法
__dict__
:
类的__dict__
里存放类的静态函数、类函数、普通函数、全局变量以及一些内置的属性。
对象的__dict__
中存储一些self.xxx的一些东西
1 | class Test: |
所以可以利用__dict__
方法查看指定类的函数,比如此前的查看所有内建函数:__builtins__.__dict__
__globals__
和__getattribute__()
在参考文章中有详述。
类继承
python中对一个变量应用class方法从一个变量实例化为对象类型后,类有以下三种关于继承关系的方法
__base__
//对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法__mro__
//同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]
可以获取到__subclasses__()
//继承此对象的子类,返回一个列表
有这些类继承的方法,我们就可以从任何一个变量,回溯到基类中去,再获得到此基类所有实现的类,就能使用所有基类下的子类及其方法了。
比如一个字符串,首先获得其当前类,得到str类;再获得其基类,也就是object类,再获得object类的所有子类,得到以下内容:
1 | "".__class__ # 获得当前类 |
利用方式
综上,沙盒逃逸的利用方式可归纳为以下流程:
- 变量 -> 对象 -> 类 -> 基类 -> 所有子类 -> 函数方法
以读取文件为例,Python2中读取文件在file
类中,而python3中取消了该类,分别演示:
python2:利用file类中的read方法
1 | "".__class__.__mro__[-1].__subclasses__()[40] |
当然file类中还有其他方法,比如readline()、write()
等都可以利用,查看file类的内置函数
1 | for i in enumerate(file.__dict__): print(i) |
python3:没有file类,利用内置函数open()来读取文件
1 | "".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__["open"]("/etc/passwd").read() |
当然还有命令执行、文件写入等操作,只需要找到相应的函数方法即可,比如os.popen()、system()
等
XCTF中SSTI相关题目
Web_python_template_injection
题目页面提示为python模板注入,但是直接穿参没有效果,尝试访问index.php发现会有not found提示,于是在index.php后添加模板注入参数,成功解析:
尝试读取flag文件无果后,利用命令执行来查看当前目录下文件,payload:
1 | "".__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read() |
得到flag文件名:fl4g
读取flag,payload:
1 | "".__class__.__mro__[-1].__subclasses__()[40]("fl4g").read() |
这道题目还用到了 __init__
和__globals__
__init__
: 类的初始化方法__globals__
: 对包含函数全局变量的字典的引用
easytornado
题目页面有三个文件链接,分别访问:
- flag.txt:提示 flag in /fllllllllllllag
- welcome.txt:提示 render
- hint.txt:提示 md5(cookie_secret+md5(filename))
而且访问该文件时,url会出现filename和filehash参数:
1 | http://220.249.52.133:46061/file?filename=/hints.txt&filehash=4016066a51c646157d04c91b76864a4d |
由此断定,需要利用fllllllllllllag文件名构造得到filehash来访问得到flag;由hint提示的构造方法,得知构造filehash需要cookie_secret变量的值;由welcome提示的render以及题目描述Tornado框架,可知在Tornado中可利用handler.settings访问得到Web关键字参数
修改文件名为不存在,可触发error页面,msg参数传入模板解析数据,得到cookie_secret
利用已知数据及构造方式,得到filehash,访问即可得到flag
1 | http://220.249.52.133:46061/file?filename=/fllllllllllllag&filehash=362f231da5422730e87f8c009eb25f91 |
当然在实际中会对输入做安全过滤,绕过过滤的方法详见这几位师傅的博文:
https://www.cnblogs.com/zaqzzz/p/10263396.html
https://www.cnblogs.com/20175211lyz/p/11425368.html
参考:
https://www.anquanke.com/post/id/188172
https://www.cnblogs.com/Xy--1/p/12841941.html