ECHO · Write-ups · BrunnerCTF 2026
Three Solves Worth Writing Down
BrunnerCTF 2026 (Global) closed with ECHO 146th of 1,103 teams on 4,185 points — the exact figures the organisers’ certificate settled. Forty-one challenges fell across the weekend. Most were clean and quick. Three were the kind you want to write down, because each one turns on a single idea that shows up constantly in real systems: an authentication flow that forgot who it was protecting, a proxy and a server that disagreed about where one request ended, and a language model that would not say it was cheating while it cheated on the board.
Flags are shown because the event is closed and the challenges are archived. Nothing here is a technique against a live third-party system — it is the write-up of a sanctioned competition.
Provenance
Worked during BrunnerCTF 2026 (Global), 20–22 August 2026, as ECHO Club. Final placing 146th of 1,103 teams on 4,185 points, per the organisers’ certificate. Every technique, flag and timestamp below is drawn from the club’s own solve record, not reconstructed after the fact. Supervising writer: Digital Orukami.
The three challenges set here — Dumb-factor Authentication, Welcome Aboard and Checkmate — were each worth 100 points and were chosen for the write-up not because they were hard but because each isolates one idea worth teaching.
Disclaimer. This report documents challenge solutions completed in a sanctioned, closed CTF for educational purposes. All work was performed against the competition’s own archived infrastructure. ECHOClub is not responsible for the organisation or administration of BrunnerCTF.
Three solves
One idea, three categories
Username-less 2FA · CL.TE request smuggling · LLM chess opponent
Challenge 1 · Web / authentication · 100 pts · Solved 21 Aug 17:03 · Threat level: critical
Dumb-factor Authentication
The login page asked only for a six-digit code. No username, no password — just the TOTP pin. That single design choice is the whole vulnerability, and it is worth slowing down on because it is a mistake real products ship.
A TOTP code is six digits: a million possibilities. That sounds like a lot until you notice what “username-less” does to the maths. Normally a code is checked against one account — your code has to match your secret. Here, with no username, the server checked the submitted pin against every employee’s current code at once. And there was no rate limit.
Evidence
| Artifact | Description |
|---|---|
| Login form | Six-digit TOTP pin only — no username, no password field |
| No rate limit | Unthrottled pin submission — guesses fired until one landed |
| update_username | Endpoint that renamed the session with no uniqueness check |
| reset_totp | Self-service reset that returned the account’s own new seed |
| /feedback/view | Admin-only HR tickets, gated by a string compare of the name to "admin" |
Methodology
Why the brute force works
Stripping the username out of the flow is what collapses the search. With a username, a guess only wins if it matches one specific secret. Without one, a guess wins if it matches anybody’s live code — and hundreds of employees each have a valid code at any moment.
| Flow | What a guess is checked against | Odds a random pin wins |
|---|---|---|
| Username + TOTP (normal) | One account’s current code | ~1 in 1,000,000 |
| TOTP only (this challenge) | Every employee’s current code at once | Brute-forceable in seconds |
The team fired random pins until one landed an arbitrary employee’s session. That is the foothold.
The takeover chain
From that foothold, the chain is a tour of missing checks:
- Username takeover. The
update_usernameendpoint let the hijacked session rename itself to anything, with no uniqueness check — so you could become “admin” by asking. - Seed handback. The self-service
reset_totpreturned the account’s own new seed, so you could mint a fresh, valid session that now carried the renamed identity. - The string-compared gate. The admin-only
/feedback/view— the private HR tickets — was guarded by comparing your name to"admin"as a string. The rename made that comparison pass.
Flagbrunner{ch1ef_duck_0ff1c3r_4ppr0v3d_th1s_fl4g}
The lesson, for our own builds
Multi-factor is not a feature you add; it is a second independent proof of a specific identity. Strip the identity out of the flow and a second factor guards a global namespace, which guards nothing. Every link after that — unthrottled guessing, a rename with no uniqueness, a self-service secret handback, an authorization gate that is a string compare — is a check someone decided they did not need. This is the shape of most real account-takeover chains: not one dramatic hole, but a row of small “good enough”s.
Challenge 2 · Web / HTTP request smuggling · 100 pts · Solved 21 Aug 17:30 · Threat level: high
Welcome Aboard
Request smuggling is the most fun a web person can have, and it is genuinely hard to build a clean example, so a good CTF version is a gift. The premise: a front-end proxy sits in front of a back-end server. They read the same bytes off the same connection but disagree about where one HTTP request stops and the next begins. Once they disagree, you can hide a whole second request inside the first — and the front-end’s access rules never see it.
The variant here was CL.TE: the front-end trusts the Content-Length
header, the back-end trusts Transfer-Encoding: chunked. Craft one message that
satisfies both readings differently and the tail of your request becomes the head of
the next one, as far as the back-end is concerned.
Evidence
| Artifact | Description |
|---|---|
| Front-end proxy | Framed the request by Content-Length |
| Back-end server | Framed the request by Transfer-Encoding: chunked |
| CL.TE desync | The two ends parsed one message’s length differently — the smuggling primitive |
| 408-vs-200 oracle | Timing pair that proved the desync before it was trusted |
| /robots.txt | Named the internal path the front-end ACL was protecting |
Methodology
Proving the desync exists
The write-up-worthy part is not the payload, it is how the team proved the desync existed before trusting it. A smuggling bug is invisible until you can measure it. Send a request the back-end will sit and wait on — because your framing tells it more bytes are coming that never arrive — and it times out; send the benign framing and it answers normally. That timing difference is the proof.
| Framing sent | Response | What it proves |
|---|---|---|
| Length promises bytes that never arrive | 408 (back-end waits, times out) | The two ends parsed the length differently |
| Benign framing | 200 | Baseline — no desync on this request |
You do not guess a smuggling bug — you demonstrate it.
The bypass and the target
- The bypass. With the desync direction confirmed, a complete smuggled request rode past the front-end’s path ACL — the front-end thought it was request body, the back-end ran it.
- Finding the target. The internal path the ACL was protecting was named, of all
places, in
/robots.txt— the file whose entire job is to write down the URLs you would rather nobody visit.
Flagbrunner{00ps_th4t_p4g3_w4s_1nt3rn4l}
The lesson
Any time two systems parse the same input with different rules, the gap between them is an
attack surface — this is true of HTTP proxies, of course, but also of anything that
re-parses: a WAF and an app, a serialiser and a deserialiser, a URL parser and a router. And
robots.txt is not access control; it is a hand-drawn map to the interesting
doors.
Challenge 3 · Misc / AI · 100 pts · Solved 22 Aug 03:04 · Threat level: medium
Checkmate
This one is a sign of where CTF is going, and it is the most quietly instructive of the three. The challenge was an LLM playing chess as Black — and the same model both chatted with you and chose its own moves. So the attack surface was the conversation, and the target was the board.
The goal was to lose the model into Fool’s Mate — the fastest checkmate in chess — inside a six-move budget:
1. e4 g5 2. d4 f6 3. Qh5#
Evidence
| Artifact | Description |
|---|---|
| One model, two roles | The same LLM both chatted and chose Black’s moves — chat was the attack surface |
| Fool’s Mate | Target end state; requires Black to play g5 and f6 — moves no engine makes |
| Six-move budget | The steering had to reach mate within the turn limit; three of six moves spent |
| Per-turn imperatives | One small, reasonable-sounding single-move instruction each turn |
| FEN, not chat | The board-state string was the only reliable signal of compliance — the prose lied |
Methodology
Steering, not asking
Those Black moves (g5, f6) are terrible; no engine plays them. The
team did not try to ask the bot to lose — a model told “let me win”
refuses. Instead they steered it with per-turn, single-move imperatives: each turn, one
small, reasonable-sounding instruction that nudged Black’s piece exactly where
Fool’s Mate needs it, spending three of the six moves.
Read the board, not the talk
The detail that makes this a real write-up: the bot refused in prose while complying on the board. It would say, in chat, that it would not throw the game — and then make the losing move anyway. So you could not read success from what the model said. Compliance had to be read from the FEN — the board-state string — not the chatter. The words were the model’s guardrail; the board was the model’s actual behaviour, and the two did not match.
Flagbrunner{th3_du0l1ng0_ch355m45t3r_5tr1k35_4g41n!}
The lesson, and it is the important one for a security club in 2026
When a language model is wired to take actions — move a piece, call a tool, send a request, approve a ticket — its polished refusal in the chat window is not evidence of anything. The guardrail lives in the prose channel; the behaviour lives in the action channel; an attacker works the gap between them. “The assistant said no” is the AI-era version of trusting a client-side check. Watch the board, not the talk.
In common
What the three share
A trust boundary that only existed in one of the two places it was needed
Different categories, one idea: a trust boundary that only exists in one of the two places it needed to. The 2FA that authenticated a code but not an identity. The proxy and server that agreed on the bytes but not on the boundaries. The model that refused in language but acted in defiance of its own refusal. Find the place where a system thinks it is checking something and isn’t, and you have the solve — in a CTF, and in the systems ECHO is here to learn to defend.
ECHO Club · BrunnerCTF 2026 (Global) · 146th of 1,103 · 4,185 pts
Supervising writer: Digital Orukami · sanctioned CTF · educational use only. Full solve list and the team’s placing sit on the competition record.