BaseCamp
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.
$ ls tags/ techniques/
BaseCamp — alfactf
Description
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.
Analysis
Source review showed the important flow:
POST /api/auth/registerandPOST /api/auth/logincreate a normal user session.POST /api/courses/4/lessons/15/request-accessissues a short-lived JWT with rolevip,one_time=true, and a slug for demo lesson 15.GET /api/courses/4returns lesson metadata. When the JWT role isvip, 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 JWTjti.
Course 4 contained:
- lesson 13: free
- lesson 14: free
- lesson 15: demo
- lesson 16: vip
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.
Solution
- Register and log in as a normal user.
- Repeatedly call
POST /api/courses/4/lessons/15/request-accessto mint many one-time VIP tokens. - Repeatedly spend those tokens on
GET /api/courses/4.- Each successful use triggers post-response revocation attempts.
- The revocation list keeps growing.
- File-backed check/revoke operations become slower and eventually time out.
- Once revocation checks are unstable, perform the real access sequence with a fresh token:
- request a new one-time VIP token from lesson 15,
- call
GET /api/courses/4with that token to obtain the slug for lesson 16, - immediately reuse the same token on
GET /api/courses/4/lessons/access/{lesson16slug}.
- Because revocation checking now times out and the middleware ignores that error, the same token remains usable long enough to open lesson 16 and read the flag.
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.
Lessons Learned / Remediation
- Never fail open on security-critical backend checks. If token revocation cannot be verified, the request should be denied.
- One-time token invalidation must be atomic and authoritative, not best-effort after the protected action already completed.
- A file-backed linear scan is fragile under load. Use a proper datastore with bounded lookup latency.
- Timeouts should not silently degrade authorization guarantees.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [web][Pro]Code Control— undutmaning
- [web][free]Six-Seven— alfactf
- [misc][free]Chrono Mind— HackTheBox
- [web][free]Никто, конечно, не чиллил— alfactf
- [web][Pro]Board of Secrets Revenge— miptctf