bookmaker
umdctf
Task: a stripped PIE QuickJS service executes attacker-supplied JavaScript with native Ledger and Wire bindings, including a recycled Ledger whose stale ArrayBuffer alias remains valid. Solution: overlap freed Ledger storage with a new ArrayBuffer, corrupt its metadata to leak PIE and gain arbitrary read/write, overwrite the global settle callback with win, then trigger settle() to spawn a shell and read the 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.
bookmaker — UMDCTF
Description
No separate organizer description was present in the provided workspace files.
English summary: the service accepts JavaScript and executes it inside an embedded QuickJS environment until the terminator token __END_MARKET_SCRIPT__. Several native bindings are exposed to script code, and one of them makes it possible to turn a heap use-after-free into code execution.
Reconnaissance
Initial triage showed an amd64 PIE ELF with:
- Full RELRO
- NX
- no stack canary
- stripped symbols
The binary also embeds QuickJS and exposes the following JavaScript bindings:
LedgerWiremintWirewireReadwireWritewireDispatchpoke64printhelpsettle
The remote service reads JavaScript source until __END_MARKET_SCRIPT__, then executes it. That immediately suggests looking for mistakes in the native objects that are wrapped into JavaScript values, especially anything returning ArrayBuffer views into native memory.
Ledger was the interesting object. Its view() method returns an ArrayBuffer alias to native backing storage, and recycle() frees that storage. That combination is already suspicious: if the engine keeps the old JavaScript object alive after the native free, the script may still access freed heap memory.
Vulnerability
The core bug is a use-after-free in Ledger backing storage.
The vulnerable pattern is:
- Create a
Ledger. - Call
ledger.view()to get anArrayBufferpointing at the Ledger's native bytes. - Call
ledger.recycle(). - Continue using the old
ArrayBuffer.
The stale ArrayBuffer remains accessible even though the native allocation has already been freed.
For a Ledger(0x30), the freed chunk can be immediately reclaimed with new ArrayBuffer(8). That places a new QuickJS-managed ArrayBuffer object over the old Ledger backing region. A typed array over the stale view then exposes the overlapped metadata:
...
$ grep --similar
Similar writeups
- [pwn][free]velvet-table— umdctf
- [pwn][Pro]Piece of cake— hackerlab
- [pwn][Pro]Secrets— grodno_new_year_2026
- [pwn][free]vkexchange— umdctf
- [pwn][free]Void— hackthebox