Featured image of post Puppet - SummerRush CTF

Puppet - SummerRush CTF

Click to expand challenge code
 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
import os
import secrets
import sys
import time
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

SERVER_KEY = secrets.token_bytes(16)
FLAG = os.environ.get("FLAG", "FL1TZ{????????????}")

def issue_token(user_id: str) -> bytes:
    iv = secrets.token_bytes(16)
    payload = f"uid={user_id}&role=puppet".encode()
    cipher = AES.new(SERVER_KEY, AES.MODE_CBC, iv=iv)
    return iv + cipher.encrypt(pad(payload,16))

def check_token(token: bytes) -> str:
    iv = token[:16]
    ct = token[16:]
    cipher = AES.new(SERVER_KEY, AES.MODE_CBC, iv=iv)
    pt = cipher.decrypt(ct)
    print(f"Decrypted token: {pt!r}")
    clean = unpad(pt,16).decode(errors="ignore")
    parts = {
        kv.split("=",1)[0]: kv.split("=",1)[1]
        for kv in clean.split("&")
        if "=" in kv
    }
    return parts.get("role","")

def main():
    guest_id = f"guest{secrets.randbelow(9000000)+1000000}"
    print(f"— you're wired in as: {guest_id}\n")
    print("[1] Present your token")
    print("[2] Get a fresh token")
    print("[3] Whisper to support")

    while True:
        choice = input("> ").strip()
        if choice == "1":
            hex_tok = input("Token> ").strip()
            try:
                role = check_token(bytes.fromhex(hex_tok))
            except Exception:
                print("…that doesn't look like a real token.\n")
                continue

            if role == "master":
                print(FLAG)
                sys.exit(0)

            else:
                print(f"Access denied, you are still role={role}\n")

        elif choice == "2":
            tok = issue_token(guest_id)
            print("Fresh strings:", tok.hex(), "\n")

        elif choice == "3":
            print("Support is busy. Please leave a message at support@your.mom\n")
            time.sleep(1)

        else:
            print("Uhhhhh, bye then.\n")
            sys.exit(0)

if __name__ == "__main__":
    main()

This is also a freebie challenge to warm up. A also had to include an AES task (even though I HATE THEM soooo much)


Analysis

Before doing anything, we need to understand under what condition we’re awarded with the flag.

1
2
3
if role == "master":
    print(FLAG)
    sys.exit(0)

So it’s pretty obvious that we’re gonna recieve the flag if we have the role master in our decrypted token, so we need to somehow swap our current role with master

Seeing that the AES is in CBC mode, we know that we can substitute a block by just xoring the ^ difference between the current plaintext and the desired one with the corresponding ciphertext block.

This mostly works when the value we want to swap in spans a perfect AES block (aka: the start index of the block and its lengths are multiples of the AES.BLOCK_SIZE, which is 16 in this case)
Luckily for us:

1
len("&role=puppet") == len("&role=master") == 16

With that, we can easily format our solution

Solution

The plan is pretty straightforward:

  • Recieve a token
  • Split it into blocks
  • Calculate the delta between the current block and desired one
  • Xor the delta with the corresponding ciphertext block (which is the last one)
  • Substitute the new block
  • PROFIT

Solver

Click to expand solver code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
from Crypto.Util.strxor import strxor

io = remote("20.218.234.6", 1002)

io.recv(timeout=1)
io.sendline(b"2")
line = io.recvline().strip()
hex_tok = line.split(b": ")[1]
token = bytes.fromhex(hex_tok.decode())
iv = token[:16]
ct = token[16:]
orig_block2 = ct[:16]
delta = strxor(b"&role=puppet", b"&role=master")
new_block2 = strxor(
    orig_block2[:12]
    , delta
) + orig_block2[12:]

forged = iv  + new_block2 + ct[16:]
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"Token> ", forged.hex().encode())
io.interactive()

FL1TZ{Str1ngs_4ttAch3d_t1l_I_cut_Th3m_l00se}