TCTT 2026 wrapped up this week. Ran the event as an orchestrator + dispatched solver setup (Claude, Codex, opencode racing challenges in parallel), 7 flags landed. Writeups below, ordered roughly by category. Full solver transcripts and artifacts are in the event repo.
Oracle Miracle (Web, 495pt)
Tarot-reading site, free tier gives one card, premium gives a 3-card spread with the flag. No session cookie anywhere, gate check was a real server-side 403 — so the obvious plays (JWT/cookie tampering, mass-assignment, IDOR count params, race conditions) were all dead ends, there was no session state to attack.
Fuzzing headers against /api/premium-access found the actual discriminator: the server special-cased search-engine crawlers so they could index premium content, keyed entirely off User-Agent.
curl -X POST http://168.144.247.111/api/premium-oracle -H "User-Agent: Googlebot/2.1"Flag was in the returned memberMessage field: TCTT2026{Pl34s3_Ch3ck_Y0ur_L1pst1ck_B3f0r3_T4lk1ng2M3}
Insecure Password Generator (Cryptography)
Registration flow: send a mobile number, server encrypts {"userid":"<mobile>"} with AES-128-ECB and hands the ciphertext back as your “password.” ECB block independence turns registration into a chosen-plaintext oracle. The JSON prefix {"userid":" is 11 bytes, so block 1 holds the first 5 digits of the number and block 2 starts with the rest.
Registered two accounts sharing one 5-digit half each with the target number (0890345536), then spliced their two ciphertext blocks together to forge the target’s exact password and logged in as them.
forged_password = first_password[:32] + second_password[32:]
session.post(BASE, data={"csrf": csrf, "action": "login",
"mobile": "0890345536", "password": forged_password})Flag: TCTT2026{3CBShuffl1ng1$EZ}
Gemini Cryptography
A CRT-monitor photo with an encrypted payload burned into the bottom of the screen. This one was almost entirely a transcription problem — decode chain was trivial (Base64 → ROT47) once the text was correctly read off the glowing scanlines.
One earlier pass on the same image misread a single digit in the noise (5/4 transposition plus a 6→7 slip), which still decoded to a correctly shaped flag — flag{wtctt-dcodf-master-2025} instead of the real flag{wtctt-docde-master-2025}. A well-formed output is not proof the transcription was right; the ambiguous glyphs here were digits/punctuation (@ 4 5 6 7), not the usual 0/O, 1/l letter-pair warning.
raw = base64.b64decode(payload + "=" * (-len(payload) % 4))
def rot47(text):
return "".join(chr(33 + (ord(c) - 33 + 47) % 94) if 33 <= ord(c) <= 126 else c for c in text)
flag = rot47(raw.decode())Flag: flag{wtctt-docde-master-2025}
Legacy Signer (Cryptography, 500pt)
463 ECDSA/secp256k1 signatures plus a changelog documenting five eras of nonce generation for the same private key. Two of those eras masked the top bits of the nonce k to fit a legacy wire field (top 12 bits zeroed in v1, top 8 bits in v2) — textbook biased-nonce ECDSA, a Hidden Number Problem instance.
Built the standard Boneh–Venkatesan / Howgrave-Graham–Smart lattice (Babai embedding) from 10 v1 signatures + 30 v2 signatures, LLL-reduced it, and read the private key straight off the short vector’s last coordinate:
d = 0x14911ee05a623cb056bf9f43430dbfda855194b3486202ea7db686643b1370b4Verified two independent ways (recomputed d*G == Q in Sage, and again through the challenge’s own point-arithmetic module, plus a live sign/verify round trip) before trusting it. Flag is sha256(hex(d)).hexdigest()[:32]:
TCTT2026{eec35ec95f3c4683ccf9a13554090b10}
InsecureDev (OSINT)
“Review the outsource developer company building the E-Service site” — an OSINT pivot challenge where the target site itself isn’t the point, the third-party vendor that built it is. The flag string is a pun on the technique: TCTT{0$1NTCANB3CHA1N} — OSINT chains, one public data point about the vendor leading to the next.
The Weakest Link (Forensics)
PCAP of HTTPS traffic to an internal portal, flagged for “dangerously weak TLS” plus “additional operational security failures.” Two things confirmed both findings directly:
- A leaked 512-bit RSA server private key (trivially weak, and just handed over as a file rather than needing factoring) — decrypts any static-RSA-suite session in the capture straight from the key.
- An NSS keylog file (
CLIENT_RANDOM ...) sitting next to the capture — a keylog existing outside a debug environment at all is itself the “operational security failure.”
Loaded both into Wireshark’s TLS decrypt (RSA key list + pre-master-secret log file) to get plaintext, and pulled the three-part flag out of it:
TCTT2026{md5:53917362945dd1b80292019ec239277e}
Phantom Packets (Forensics)
The most involved chain of the event, four stages deep:
- DNS exfiltration. Hundreds of sequential DNS queries to
status.update-svc-cdn.com, one hex-encoded chunk per label (0001.status...,0002.status..., …). Reassembling them in order rebuilds a full PEM RSA private key. - Static-RSA TLS decrypt. That private key decrypts a session to
172.28.0.10:443using static-RSA key exchange (no forward secrecy). - Git clone inside the decrypted traffic. The plaintext is a
git cloneofnexline-deploy.git— rebuilt the pack/objects from the decrypted stream into a working repo. - Two planted tokens across git history, one live, one deleted-but-recoverable:
deploy/staging-vars.conf→DEPLOY_TOKEN=TCTT2026_pt1{5b9437e03df7b252}config/credentials.bak, added then deleted in a later commit (blob still reachable from history) →api_secret=TCTT2026_pt2{9562ad4705a54963}
The flag format (TCTT2026{md5}) was a red herring — several MD5-of-the-tokens combinations were tried and rejected before landing on the actual rule: just concatenate the two inner 16-hex-char values directly, no hashing.
TCTT2026{5b9437e03df7b2529562ad4705a54963}
Full per-challenge detail, solver scripts, and raw artifacts (including a couple of partially-reconstructed writeups for challenges where I didn’t keep a transcript) are in the withclaude/challenges/ directory of the event repo.
