darallium's tech blog
home
blog
tech

qiita: @darallium
github: darallium
twitter: /darallium/
instagram: yu_kyu_n
email
Home tech satoki-ctf-2024
  • SatokiCTF write up

    1. 目次
    2. zzz
    3. minibank

    Satoki CTFのwriteupです. Satokiさん、お誕生日おめでとうございます!

    zzz

    FROM ubuntu:22.04
    
    ENV DEBIAN_FRONTEND noninteractive
    
    RUN apt-get update && apt-get -y install openssh-server
    
    # thanks to https://github.com/SECCON/SECCON2022_online_CTF/blob/46742099d094a69c214f35498718b5c9ba900b26/misc/txtchecker/build/Dockerfile#L10
    WORKDIR /app
    
    RUN groupadd -r ctf && useradd -m -r -g ctf ctf
    RUN echo "ctf:ctf" | chpasswd
    
    RUN echo 'ForceCommand "/app/zzz.sh"' >> /etc/ssh/sshd_config
    RUN echo 'Port 5000' >> /etc/ssh/sshd_config
    RUN mkdir /var/run/sshd
    
    COPY flag.txt /
    COPY zzz.sh /app/
    
    RUN chmod 444 /flag.txt
    RUN chmod 555 /app/zzz.sh
    
    CMD /sbin/sshd -D
    #!/bin/bash
    echo "I'm going to sleep for a while. I will give you the flag when I wake up. Oyasumi!"
    sleep infinity
    cat /flag.txt

    zzz.shをみると、sleep infinityとありこれのみを終了させることが目標です. ただし、Force Command "/app/zzz.sh"とあるので、このシェル自体を殺してしまうとcatされずにコネクションが消えてしまうので、sleepだけを終了させる必要がありそうです.

    ssh上でctrl+4を押すとSIGQUITが送信されます. shell上ではSIGQUITは現在の行のプログラムを止めるだけなので、

    minibank

    import os
    import jwt
    import random
    from flask import Flask, jsonify, request, make_response, render_template
    
    
    app = Flask(__name__)
    
    FLAG = os.environ.get("FLAG", "flag{*****REDACTED*****}")
    
    
    def determine_status(balance):
        status = FLAG
        if balance > 0:
            status = "rich"
        if balance <= 0:
            status = "poor"
        return f"You are a {status} person, aren't you?"
    
    
    # omg ;(
    KEY = str(random.randint(1, 10**6))
    
    
    def encode_jwt(balance):
        payload = {"balance": balance}
        return jwt.encode(payload, KEY, algorithm="HS256")
    
    
    def decode_jwt(token):
        try:
            payload = jwt.decode(token, KEY, algorithms="HS256")
            return payload.get("balance")
        except:
            return None
    
    
    @app.route("/")
    def index():
        token = request.cookies.get("account")
        if token:
            balance = decode_jwt(token)
            if balance is not None:
                status = determine_status(balance)
                return render_template("index.html", balance=balance, status=status)
        resp = make_response(
            render_template(
                "index.html",
                balance=1000,
                status="Welcome! Setting your initial balance to $1000.",
            )
        )
        resp.set_cookie("account", encode_jwt(1000))
        return resp
    
    
    @app.route("/transaction", methods=["POST"])
    def transaction():
        data = request.get_json()
        if "amount" in data and isinstance(data["amount"], int):
            amount = data["amount"]
            token = request.cookies.get("account")
            balance = decode_jwt(token)
            if balance is not None:
                balance += amount
                status = determine_status(balance)
                new_token = encode_jwt(balance)
                resp = jsonify({"balance": balance, "status": status})
                resp.set_cookie("account", new_token)
                return resp
            else:
                return jsonify({"error": "Invalid token."}), 400
        else:
            return (
                jsonify({"error": "Invalid amount specified. Amount must be an integer."}),
                400,
            )
    
    
    if __name__ == "__main__":
        app.run(debug=False, host="0.0.0.0", port=4445)

    ソースコードを眺めていると、jwtがcookieに代入されていて、HS256でencryptionされていることがわかる. HS256ということは、複合鍵と暗号鍵が同じであることに着目して、KEYをパクることができればうまくいきそうという方針. KEY = str(random.randint(1, 10**6))とあり、10**6は十分に全探索可能なので探索する.

    └─$ cat solver.py
    import jwt
    import base64
    import hmac
    import hashlib
    import json
    import shutil
    
    width=shutil.get_terminal_size().columns
    
    orig = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.p0K5vNd1T4wlB8YxnoVPE6ZZ2qdLA-Z_5Rqbtpia-_Y"
    
    def encode_jwt(balance, KEY):
    
        header = '{"typ":"JWT","alg":"HS256"}'
        payl = '{"balance":1000}'
    
        def base64url_encode(data):
            return base64.urlsafe_b64encode(data).decode('utf-8').rstrip("=")
    
        encoded_header = base64url_encode(header.encode())
        encoded_payload = base64url_encode(payl.encode())
        message = f"{encoded_header}.{encoded_payload}"
        sign = base64url_encode(hmac.new(KEY.encode(), message.encode(), hashlib.sha256).digest())
        jwt_token = f"{message}.{sign}"
        return jwt_token
    
    print("Starting bruteforce")
    print(f"the original token is:\t {orig}")
    print(f"the max key is:\t {10**6}")
    for i in range(0, 10**6):
        KEY = str(i)
        token = encode_jwt(i, KEY)
        print((f"Trying key:\t {i} on {token}"[:width-8]), end="\r")
        if token == orig:
            print('')
            print(f"{i} is the key!")
            print(token)
            break

    続けて、if balance > 0かつif balance <= 0:を満たすbalanceがjwtから生成されればよい. intのサブクラスでないと話が始まらないので、組み込み変数を見ているとnanがこれを満たすので、nanを生成する. ちなみに、jwtでnanを生成するには

    "{\"balance\":NaN}"

    とすればよいらしいです.

    ===============広告=================

    darallium

    Fri Aug 30 2024 18:55:50 GMT+0900 (Japan Standard Time)