630 words
3 minutes
CryptoNite CTF 2026 - AES Stuff - Cryptography Writeup

Category: Cryptography Flag: TACHYON{w3lp_3cp_1s_bu5t3ed_L0l_d23d3cf}

Challenge Description#

We built a “secure” encryption service using Advanced Encryption Standard. To keep things simple, it runs in Electronic Codebook mode. The service works like this: -You send arbitrary plaintext. -The server appends a secret flag to your input. -The combined message is padded and encrypted. -The ciphertext is returned to you as hex. -The encryption key is fixed and unknown to you.

You may query the oracle as many times as you like. Can you recover the secret flag?

Analysis#

This was the classic ECB oracle pattern: controllable prefix from me, unknown secret suffix from the server, and deterministic encryption under one fixed key. As soon as I saw that shape, the solve path became byte-at-a-time extraction of the appended secret. I started by probing the API to confirm how it wanted input and what field contained ciphertext.

curl -i "https://aes-challenge-thingy.vercel.app/api/oracle"
HTTP/2 405
{"error":"POST only"}

Then I tested JSON field names and found the accepted one was input, which returned a hex ciphertext field.

curl -i -X POST "https://aes-challenge-thingy.vercel.app/api/oracle" \
  -H "Content-Type: application/json" \
  -d '{"input":"A"}'
HTTP/2 200
{"ciphertext":"18fd95136877ad37cccd9272ef4b840312080649b992f7458ad4c78cac2f24bf0791fcecb5e0d1811734593e42510c60"}

At that point the challenge was surprisingly clean: detect block size by watching ciphertext length jumps, confirm ECB via repeated-block collision, and then recover one byte at a time with a dictionary of candidate last bytes for each aligned block.

smile

I implemented exactly that in Python with requests. The script queried the oracle with controlled prefixes, matched target blocks against guessed blocks, and appended characters whenever block equality hit. The run showed a 16-byte block size, confirmed ECB mode, and then recovered the full TACHYON{...} token directly from oracle output.

wink

python solve_aes_stuff.py
[*] Block size: 16
[*] Base ciphertext length: 48
[*] First length increase at input length: 8
[*] ECB detected: True
[*] Estimated secret length: 40
[+] Recovered so far: T
[+] Recovered so far: TA
[+] Recovered so far: TAC
...
[+] Recovered so far: TACHYON{w3lp_3cp_1s_bu5t3ed_L0l_d23d3cf}
[*] Final recovered text: TACHYON{w3lp_3cp_1s_bu5t3ed_L0l_d23d3cf}
[FLAG] TACHYON{w3lp_3cp_1s_bu5t3ed_L0l_d23d3cf}

Solution#

# solve_aes_stuff.py
import re
import requests

URL = "https://aes-challenge-thingy.vercel.app/api/oracle"


def oracle(user_input: str) -> bytes:
    r = requests.post(URL, json={"input": user_input}, timeout=15)
    r.raise_for_status()
    data = r.json()
    return bytes.fromhex(data["ciphertext"])


def detect_block_size() -> tuple[int, int, int]:
    base = len(oracle(""))
    for i in range(1, 129):
        cur = len(oracle("A" * i))
        if cur > base:
            return cur - base, base, i
    raise RuntimeError("Failed to detect block size")


def is_ecb(block_size: int) -> bool:
    probe = "A" * (block_size * 8)
    ct = oracle(probe)
    blocks = [ct[i : i + block_size] for i in range(0, len(ct), block_size)]
    return len(blocks) != len(set(blocks))


def estimate_secret_len(block_size: int, base_len: int, first_increase_at: int) -> int:
    if first_increase_at == 1:
        return base_len - block_size
    return base_len - first_increase_at


def recover_secret(block_size: int, max_bytes: int) -> str:
    charset = "".join(chr(i) for i in range(32, 127))
    recovered = ""

    for idx in range(max_bytes):
        prefix_len = block_size - 1 - (idx % block_size)
        prefix = "A" * prefix_len

        target = oracle(prefix)
        block_index = idx // block_size
        target_block = target[block_index * block_size : (block_index + 1) * block_size]

        for ch in charset:
            test_ct = oracle(prefix + recovered + ch)
            test_block = test_ct[block_index * block_size : (block_index + 1) * block_size]
            if test_block == target_block:
                recovered += ch
                print(f"[+] Recovered so far: {recovered}")
                break
        else:
            break

        if recovered.endswith("}") and "{" in recovered:
            break

    return recovered


def main() -> None:
    block_size, base_len, first_inc = detect_block_size()
    print(f"[*] Block size: {block_size}")
    print(f"[*] Base ciphertext length: {base_len}")
    print(f"[*] First length increase at input length: {first_inc}")

    ecb = is_ecb(block_size)
    print(f"[*] ECB detected: {ecb}")

    est_secret_len = estimate_secret_len(block_size, base_len, first_inc)
    recovered = recover_secret(block_size, max_bytes=max(est_secret_len, 96))
    print(f"[*] Final recovered text: {recovered}")

    m = re.search(r"TACHYON\{[^}]+\}", recovered)
    if m:
        print(f"[FLAG] {m.group(0)}")


if __name__ == "__main__":
    main()
python solve_aes_stuff.py
[*] Block size: 16
[*] Base ciphertext length: 48
[*] First length increase at input length: 8
[*] ECB detected: True
[*] Estimated secret length: 40
[+] Recovered so far: TACHYON{w3lp_3cp_1s_bu5t3ed_L0l_d23d3cf}
[*] Final recovered text: TACHYON{w3lp_3cp_1s_bu5t3ed_L0l_d23d3cf}
[FLAG] TACHYON{w3lp_3cp_1s_bu5t3ed_L0l_d23d3cf}
CryptoNite CTF 2026 - AES Stuff - Cryptography Writeup
https://blog.rei.my.id/posts/70/cryptonite-ctf-2026-aes-stuff-cryptography-writeup/
Author
Reidho Satria
Published at
2026-03-10
License
CC BY-NC-SA 4.0