
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.
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.pypython3 pack.py exploit.lua exploit.spx
python3 solve.py # or: ncat --ssl sparxie-vanishing-encore.chal.uiuc.tf 1337 < exploit.spxDefensive 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
Related in Malware & RE
Reverse Engineering the Six Stages of MacSync Stealer and RAT
Post-DEF CON Phishing Uses Malicious Google Doc to Deliver Malware
ClickFix Removes Your Background but Leaves the Malware
