Challenge: leftovers · CTF: GPN CTF 2026 · Category: Reversing · Difficulty: Medium
Summary
A Javalin “fridge tracker” web app is shipped as leftovers.jar + cache.aot + a custom fastdebug JDK and run with -XX:AOTCache=cache.aot. The jar decompiles to a /set-image-dir endpoint protected by the password supersecret, but the live service rejects it — the AOT cache overrides Server.class with a different validator. Dumping the actually-loaded class with the HotSpot Serviceability Agent reveals the real password algorithm; reversing it unlocks a file-read primitive that reads /flag.
Solution
Step 1: The cached class != the jar
POST /set-image-dir {"password":"supersecret",...} returns Invalid password, even though that’s exactly what the decompiled jar checks. The hint is in the name and the Dockerfile: the app runs with -XX:AOTCache=cache.aot, and an AOT/CDS cache stores pre-linked class bytecode that takes precedence over the jar. The “leftovers” are the overriding classes inside the cache.
The interned string supersecret is present and intact in the cache’s archived heap, so it’s the bytecode, not a constant, that differs. To see what actually runs, attach the Serviceability Agent to the live JVM and dump the loaded class (relax ptrace_scope so SA can attach to a sibling):
| |
The dumped Server.class (8570 bytes vs 8212 in the jar) contains the real check: password -> ROT13(letters; digits pass through) -> reverse -> XOR(key), compared against a fixed target array.
Step 2: Reverse the password and run the file-read chain
Invert the transform to get the password (algomaster99), then abuse the app’s file-read: set-image-dir to /, register a product named flag with no imageUrl (so nothing overwrites the file), and GET /images/flag. The name sanitizer maps [^a-zA-Z0-9_-] to _, so only a dotless path component like /flag is reachable.
| |
Output:
| |
Flag
| |