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

ArtifactDescription
Login formSix-digit TOTP pin only — no username, no password field
No rate limitUnthrottled pin submission — guesses fired until one landed
update_usernameEndpoint that renamed the session with no uniqueness check
reset_totpSelf-service reset that returned the account’s own new seed
/feedback/viewAdmin-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.

FlowWhat a guess is checked againstOdds 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 onceBrute-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_username endpoint 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_totp returned 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

ArtifactDescription
Front-end proxyFramed the request by Content-Length
Back-end serverFramed the request by Transfer-Encoding: chunked
CL.TE desyncThe two ends parsed one message’s length differently — the smuggling primitive
408-vs-200 oracleTiming pair that proved the desync before it was trusted
/robots.txtNamed 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 sentResponseWhat it proves
Length promises bytes that never arrive408 (back-end waits, times out)The two ends parsed the length differently
Benign framing200Baseline — 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

ArtifactDescription
One model, two rolesThe same LLM both chatted and chose Black’s moves — chat was the attack surface
Fool’s MateTarget end state; requires Black to play g5 and f6 — moves no engine makes
Six-move budgetThe steering had to reach mate within the turn limit; three of six moves spent
Per-turn imperativesOne small, reasonable-sounding single-move instruction each turn
FEN, not chatThe 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.