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}"
とすればよいらしいです.
===============広告=================