$ cat writeup.md…
$ cat writeup.md…
alfactf
Task: a Go web app exposed a demo-to-VIP access flow guarded by one-time JWTs and a local revocation microservice. Solution: overload revocation checks until they time out, then reuse a supposed one-time VIP token to list course 4 and open lesson 16.
The challenge provided a web service at
https://basecamp-srv-ude4r2la.alfactf.ru/and a source archive for local analysis.
The target was a Go application with JWT-based auth and a separate local revocation service. The goal was to reach the hidden VIP lesson in course 4 and extract the real flag.
Source review showed the important flow:
POST /api/auth/register and POST /api/auth/login create a normal user session.POST /api/courses/4/lessons/15/request-access issues a short-lived JWT with role vip, one_time=true, and a slug for demo lesson 15.GET /api/courses/4 returns lesson metadata. When the JWT role is vip, it also returns slugs for demo and VIP lessons.GET /api/courses/4/lessons/access/{slug} serves VIP lesson content if the slug matches the JWT jti.Course 4 contained:
The first promising idea was to forge a VIP JWT from secrets found in docker-compose.yml, including a sample JWT_SECRET. That did not work remotely: forged tokens were rejected with 401 invalid token, so this path was a false lead and not the real solve.
The actual bug was in revocation handling.
The auth middleware parsed the JWT, called revocation.Check(jti), and only rejected the request if the service replied successfully with revoked=true. If Check returned an error, the middleware only logged it and continued processing. After the handler completed, any one_time token was revoked via revocation.Revoke(jti).
The revocation backend stored revoked JTIs in a plain text file. Each check scanned that file line by line, while the service wrapped the operation with a very short timeout. Once the file grew and contention increased, revocation requests started timing out. Those timeouts became ordinary errors, and the main middleware failed open by accepting the token anyway.
This meant a token intended for one-time use could be reused as long as revocation checks were slow enough.
POST /api/courses/4/lessons/15/request-access to mint many one-time VIP tokens.GET /api/courses/4.
GET /api/courses/4 with that token to obtain the slug for lesson 16,GET /api/courses/4/lessons/access/{lesson16slug}.The helper used during the solve farmed demo tokens and periodically tested the full flag path. Successful runs were recorded in local logs:
fill_revocation_run3.log: FLAG: alfa{bA5E_sKIll_f0R_gO_1S_dO_n07_allOw_f4Il_0P3n}fill_revocation_run4.log: FLAG: alfa{bA5E_sKIll_f0R_gO_1S_dO_n07_allOw_f4Il_0P3n}Minimal PoC logic:
#!/usr/bin/env python3 import asyncio import httpx BASE = "https://basecamp-srv-ude4r2la.alfactf.ru" async def get_demo_token(client, user_token): r = await client.post( BASE + "/api/courses/4/lessons/15/request-access", headers={"Authorization": f"Bearer {user_token}"}, ) return r.json()["token"] async def get_course(client, token): r = await client.get(BASE + "/api/courses/4", headers={"Authorization": f"Bearer {token}"}) return r.json() async def main(): # 1) warm up the revocation backend by minting and consuming many tokens # 2) get a fresh demo token # 3) use it on /api/courses/4 to obtain lesson 16 slug # 4) immediately reuse the same token on /api/courses/4/lessons/access/{slug} pass asyncio.run(main())
In the full exploit helper, concurrency was used to keep pressure on revocation while a monitor routine repeatedly attempted the final two-request chain with a fresh one-time VIP token.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar