Leftover Leftovers
gpnctf2026
Task: hybrid reverse/web challenge on JDK 26 AOT cache (Project Leyden/CDS-AOT); app bytecode lives only inside a 53 MB .aot cache, uploaded to an OuterServer that SHA-256 verifies it before Stage 2 loads it. Solution: recover bytecode via java.lang.instrument + Attach API retransform, find that the integrity hash covers CP slot pointers and bytecode but never the underlying Symbol/heap-String bytes, binary-patch the 'images' folder constant to '//////' at two offsets so Path.of resolves to /flag while keeping the Total hash unchanged, upload, then read /images/flag.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Leftover Leftovers — GPNCTF 2026
Description
You caught me, I was a bit cheeky with the last one! To make up for it, you can now supply me with some delicious, edible, eatable and completely safe food. I hear you had something cooking the other day? It's probably still good! PS: Sorting my leftovers first sounds like a good idea :)
A hybrid reverse-engineering + web challenge built around the JDK 26 AOT cache (Project Leyden / CDS-AOT). This is the sequel to "Leftovers" (leftovers1), where the /set-image-dir route let you point the image directory at /flag. That path is now disabled, forcing exploitation through the AOT cache itself.
Goal: read /flag (also /flag.txt) on the remote.
Analysis
Handout layout
exec.shruns two stages:- Stage 1 (OuterServer):
java -XX:AOTCache=outer-cache.aot -cp leftovers2.jar de.kitctf.gpn24.leftovers2.OuterServer serve /tmp/cache.aot cache.aot - Stage 2 (Server):
java -XX:AOTCache=/tmp/cache.aot -jar leftovers2.jar
- Stage 1 (OuterServer):
leftovers2.jar— Javalin 7.2.0 + Jackson 2.21.2 + Kotlin stdlib + Jetty 12.1.8. Thede.kitctfapplication classes are NOT in the jar (compiled with-g:none); they exist only inside the AOT cache.cache.aot(53 MB) — inner AOT cache with the Stage 2 server bytecode.outer-cache.aot(38 MB) — AOT cache for the OuterServer (Stage 1).my-jdk/— custom OpenJDK 26/27 fastdebug build (Linux x86-64), commit35b0de3d4d4e8212227af5462fafbd464103f058.leftovers-padding.bin(125352 bytes in jar) — unused decoy.
The key insight: whatever cache you upload to Stage 1 becomes /tmp/cache.aot, which Stage 2 then loads. Controlling a string constant in the cache controls Stage 2's runtime behavior — if you can get past the integrity check.
Recovering bytecode that only lives in the AOT cache
...
$ grep --similar
Similar writeups
- [reverse][free]Leftovers— gpnctf2026
- [crypto][free]Just Follow the Recipe— kitctf
- [reverse][Pro]another-onion— DiceCTF 2026 Quals
- [infra][free]Old food— gpnctf24
- [reverse][free]Auto Cooker— GPNCTF 2026