Skip to content

Back to the catalog

UIUCTF 2026 — Firefly & Sparxie: Lua Sandbox Escape via Bytecode Patching and WASM UAF
cybersecurityelite.comwriteup

UIUCTF 2026 — Firefly & Sparxie: Lua Sandbox Escape via Bytecode Patching and WASM UAF

A UIUCTF 2026 pwn writeup covering two sandbox-escape challenges: Firefly, where Lua 5.5's empty luai_verifycode lets patched bytecode abuse OP_FORLOOP's untyped register read plus chgivalue's tag-preserving payload update to build type confusion, GOT-redirect fclose to fflush under partial RELRO, and heap-groom a leaked /flag.txt buffer; and Sparxie, a WASM/Emscripten Lua sandbox where a BLAKE2s seal that omits a 16-byte route field lets an archive pass be forged into a relay permit, and a duplicate-clip timeline dedup bug causes a freed studio page to be reused by a queue allocation, yielding a lens over a static draft pool and an in-Lua BLAKE2s witness forgery that reaches sparxie_redeem(). It includes full exploit scripts, binary offsets, and a linked GitHub repo (Abdelkad3r/UIUCTF-2026) with handouts and solvers for both challenges.

TL;DR Deep-dive CTF pwn writeup: Lua 5.5 bytecode-patching type confusion (Firefly) and a WASM BLAKE2s-seal-gap + freed-page-reuse chain (Sparxie), both with full exploit code.
Document

What it is

Full UIUCTF 2026 pwn writeup for two Lua sandbox-escape challenges: Firefly (stock Lua 5.5.0, native x86-64 PIE, partial RELRO) and Sparxie (Lua-over-WebAssembly/Emscripten under Node.js). Both resolve with specific flags (uiuctf{1_sh4ll_s3t_th3_s34s_4bl4z3} and uiuctf{c0m3_w17h_5p4rx13_71ll_7h3_3nd_0f_7h3_w0rld}) and runnable solvers.

How it works

Firefly: luai_verifycode is the empty stock stub, so bytecode is structurally validated but never checked against runtime type invariants. OP_FORLOOP reads R[A]/R[A+2] without tag checks and updates via chgivalue, which changes payload but preserves tag — three patched FORLOOP A,0 instructions (via a LOADI A,2 marker swapped for opcode 73) turn a long string into an arbitrary-read TString, a table into an arbitrary-write Table, and print into the still-linked loadfile. PIE leaks via string.format("%p", print) (offset print - 0x25ad0); libc leaks via the puts@GOT entry (PIE+0x3c038, puts - 0x87cc0). Because partial RELRO leaves .got.plt writable, fclose@GOT (PIE+0x3c068) is redirected to libc fflush (libc+0x858d0) so /flag.txt's stdio buffer survives the parser's rejection of the flag as invalid Lua source; a heap-grooming scan then recovers it via string.match(heap_window, "uiuctf%b{}").

Sparxie: the archive-pass BLAKE2s seal covers only header[8:16] and body[0:28], omitting the 16-byte route field, so an archive pass's route bytes can be swapped for a forged relay route (computed offline, no seal recomputation needed) to mint a real relay permit. Separately, the timeline dedupes clips by (owner, offset, length) rather than object identity, so two distinct clip objects with identical (0,4096) cause render() to update only one's pointer — the other still points at the freed page, which the next sparxie.queue(draft) call deterministically reuses (LIFO static free list). Writing 0x1f720 into all 63 queue-lens slot pointers redirects the lens onto the static draft-object pool; 4-byte markers (\xf2\xa6\xd8\x31 for draft, \xa3\xc7\x51\x9e for authority) locate the live records. A BLAKE2s implementation written entirely in Lua (using surviving string.pack/unpack/sub/rep) forges the runtime witness for draft:publish(), which calls the WASM host import sparxie_redeem() that reads /flag.txt.

Preconditions / Affected

Firefly: Lua 5.5.0 embedders that accept untrusted precompiled bytecode without defining luai_verifycode, combined with partial RELRO. Sparxie: custom native Lua bindings (permits/studios/clips/timelines/drafts/queues) with a signature scheme that doesn't cover all routing fields, plus a deterministic LIFO allocator shared across object types.

What to do

Full exploit chains, a bytecode patcher (build_exploit.py), solvers, and handouts (binaries, luac, libc, dynamic linker, sparxicle.wasm, Dockerfiles) are at github.com/Abdelkad3r/UIUCTF-2026. Run commands given verbatim:

./handout/ld-linux-x86-64.so.2 --library-path ./handout \
  ./handout/luac -o exploit.luac exploit.lua
python3 build_exploit.py
python3 solve.py
python3 pack.py exploit.lua exploit.spx
python3 solve.py                # or: ncat --ssl sparxie-vanishing-encore.chal.uiuc.tf 1337 < exploit.spx

Defensive takeaways stated explicitly: define luai_verifycode for any embedder loading untrusted bytecode; treat partial-RELRO + one arbitrary write as enough to hijack fclose→fflush and retain freed stdio buffers; sign every byte that influences routing/destination, not just payload; deduplicate objects by identity rather than by coordinate tuples; be wary of deterministic/LIFO allocators handing freed pages back to attacker-triggered allocations of a different type.

Caveats

Firefly's exploit requires Linux x86-64 (or Docker) specifically to run the challenge's own luac. Flag/offsets (e.g., print - 0x25ad0, puts - 0x87cc0) are specific to the shipped binary/libc and won't transfer directly to other builds.

Organisations named here

  • Abdelkad3r/UIUCTF-2026
  • Firefly: Complete Combustion
  • Sparxie: Vanishing Encore
  • UIUCTF 2026

Technologies named here

  • luac
  • Lua
  • pack.py
  • sparxicle.wasm
  • build_exploit.py
  • Emscripten
  • solve.py
  • Node.js
  • ncat
  • WebAssembly
  • BLAKE2s

Techniques named here

  • Bytecode patching after compilation
  • Payload-arithmetic type confusion via chgivalue
  • GOT redirection under partial RELRO
  • Heap grooming and pattern-based memory scan
  • Light C function pointer leak
  • Signature/seal coverage gap exploitation
  • Truncated authenticator forgery
  • Marker-based object discovery in memory
  • In-sandbox cryptographic witness forgery