Threat reportVulnerabilityTL-2026-4938
lz1 LZ77 Decompression Stack Buffer Overflow — Public Exploit PoC with Full ROP Chain to Reverse Shell (STAR Labs Summer Pwnables 2025)
lz1 LZ77 Decompression Stack Buffer Overflow (TL-2026-4938), also tracked as lz1 Summer Pwnables Challenge Solution, is a medium-severity software vulnerability, first published 2026-10-31. It has no confirmed attribution, affects andyherbert (GitHub individual maintainer) lz1 (LZ77, maps to 11 MITRE ATT&CK techniques (T1059.004, T1095, T1106), and is covered by 9 detection rules and 16 indicators of compromise.
- Severity
- MEDIUMAssessed severity
- CVEs
- 0None referenced
- Techniques
- 11MITRE ATT&CK
- Actors
- 0Not attributed
- Detection rules
- 9SPL · KQL · Sigma
- IOCs
- 16Indicators of compromise
Key facts for TL-2026-4938
- Threat ID
- TL-2026-4938
- Also known as
- lz1 Summer Pwnables Challenge Solution, Summer Pwnables 2025: lz1, andyherbert/lz1 Decompression Stack Overflow
- Severity
- MEDIUM
- Status
- TRACKING
- Category
- VULNERABILITY
- First published
- Last reviewed
- Attribution confidence
- LOW
- Motivation
- UNKNOWN
- Target sectors
- education
- Target regions
- singapore
- Detection rules
- 9
- Indicators of compromise
- 16
Malware and tooling in lz1 LZ77 Decompression Stack Buffer Overflow
Malware and tooling: CTF, afl-fuzz, pwntools
How lz1 LZ77 Decompression Stack Buffer Overflow works
STAR Labs published a complete solution writeup for its 'lz1' Summer Pwnables 2025 challenge, documenting a stack buffer overflow in the andyherbert/lz1 educational LZ77 decompressor and providing full proof-of-concept exploit code: a ROP chain that calls _dl_make_stacks_executable to make the stack executable, pivots via a jmp rsp gadget, and lands shellcode that opens a reverse TCP shell. No CVE has been assigned and the maintainer has declined to fix the underlying bug.
In September 2025, STAR Labs (a Singapore-based offensive security research firm) published a detailed technical writeup, authored by Zafir Rasyidi Taufik, solving its own 'lz1' challenge from the 'Summer Pwnables 2025' competition series (announced publicly on 2025-08-12 via the post 'When the Heat Rises, So Do the C-Shells', restricted to currently-enrolled Singapore students, with a cash/prize incentive for first exploiters and best writeups).
The challenge target is andyherbert/lz1, a small C implementation of the LZ77 lossless compression algorithm (45 GitHub stars, 12 forks) explicitly described by its author as educational code that 'lacks boundary and error checks for the sake of brevity.' The original implementation used fixed 16-bit pointers (12-bit offset / 4-bit length) with a default 4096-byte window; the library was later extended to support configurable pointer bit-widths, and the challenge instance uses a 7-bit position / 9-bit length split. The root-cause defect was in fact publicly flagged nine years earlier: on 2016-08-06, GitHub user rwhitworth opened issue andyherbert/lz1#1, reporting that AFL-fuzz had found ten distinct crashing input files in the decompression routine (attaching a crashes.zip of test cases but no root-cause analysis). Maintainer andyherbert closed the issue the same day as won't-fix, replying that 'the code is meant to be instructional, and lacks boundary and error checks for the sake of brevity' — confirming the omissions were intentional rather than an oversight.
The decompressor reads a 4-byte uncompressed_size value directly from attacker-controlled compressed input and uses it to bound the outer decompression loop counter (coding_pos is bounded at this check site). However, within that loop, LZ77 'pointer' tokens (16-bit values encoding a back-reference position and length) are copied into the stack-allocated uncompressed_text buffer via a raw per-byte copy loop that is bounded only by the token's own length field, not by the destination buffer's actual size (unbounded at the copy site) — and because copied bytes can themselves become valid back-reference sources mid-copy, the read side can overlap already-written output. With a 9-bit length field, a pointer's length can independently reach up to 511-512 bytes regardless of the small uncompressed_size used to pass the outer-loop check, so a crafted ~0xc0-byte compressed stream can write far past the end of the stack buffer (allocated via alloca, alongside compressed_text, uncompressed_size/compressed_size, saved FILE pointers, saved RBP, and the return address, in that stack order), corrupting saved registers and the return address.
STAR Labs' PoC targets a statically linked, non-PIE binary built against glibc 2.39 with no stack protector. Because the fixed, predictable addresses of a no-ASLR/no-PIE static binary made a hardcoded gadget chain feasible, the PoC builds an approximately 0x80-byte ROP chain using fixed gadgets including pop_rdi_rbp (0x40dfa1), pop_rax (0x4027c5), pop_rcx (0x40266b), pop_rbx (0x46da97), pop_rsi_rbp (0x40d402), pop_rdx (0x453095), a plain ret (0x4018da), and a mov_dword_rax_7_ecx / xchg_edx_eax_xor_eax_eax / add_edx_eax_pop_rbx sequence used to synthesize a custom RDX value. Because the target binary runs with a non-executable stack guarded by glibc's read-only __stack_prot flag (hardened in glibc 2.39), the chain first constructs this crafted RDX permission value and calls the libc-internal function _dl_make_stacks_executable (0x4647d0, invoked via a secondary gadget at 0x4647ef) to mark the stack executable at runtime, defeating the NX protection without ever writing to __stack_prot directly. The chain then redirects execution through a jmp rsp gadget (0x4441d6) directly onto attacker-supplied shellcode (at most 0x3c bytes) staged in the overflow data. That shellcode, built with pwntools' shellcraft, issues raw syscalls — SYS_socket (AF_INET/SOCK_STREAM), SYS_connect to an attacker-controlled IP/port (the published exploit.py ships with the IP placeholder value 'ffffffff' intended for manual substitution), and SYS_execve with dup2 file-descriptor redirection onto the socket — to open an outbound TCP connection and spawn an interactive Unix shell, i.e., a classic shellcode-based reverse shell that avoids any dependency on libc shell-spawning helpers.
The writeup documents that an initial exploitation attempt targeting heap-adjacent memory was a dead end, and the working approach pivoted to targeting the stack buffers allocated via alloca inside the decompression function. The published PoC artifact set includes exploit.py (builds the LZ77-encoded ROP payload and interacts with pwntools), listen.py (a reverse-shell listener bound to TCP port 8080), the intermediate compressed-ROP-chain file rop_payload.lz77, and the final delivery artifact payload-exp submitted to the remote challenge server.
The original challenge distribution was lz1.zip (SHA256 5b0c41b916530b16a2a7d87eff4fb426357fefba0e8c0cf41c06bd6725c2b8fc), password-protected with the password 'baby_blastpass', downloadable from the STAR Labs 'star-sg/challenges' GitHub repository, with a live remote submission target at 159.223.33.156:9101 where participants submitted working payloads to retrieve a flag, and a Google Form (https://forms.gle/r9UF5LMqE7vsHpjo7) used to submit exploit source code and writeups for prize consideration.
No CVE identifier has been assigned to this issue. There is no evidence of in-the-wild exploitation; this is a CTF/educational-context disclosure with full public PoC source and a from-scratch, reproducible attack chain. However, because the vulnerable library is small, genuinely named for LZ77/LZ1 compression, and publicly available on GitHub without disclaimers prominent outside the issue tracker, any project that vendors or imports andyherbert/lz1 to process untrusted compressed input inherits an unauthenticated, remotely triggerable memory-corruption primitive with a public, working, fully-chained exploit (overflow → NX bypass → arbitrary code execution → reverse shell).
MITRE ATT&CK techniques used in TL-2026-4938
Execution
T1059.004 Command and Scripting Interpreter: Unix Shell; T1106 Native API; T1203 Exploitation for Client Execution
Command and Control
T1095 Non-Application Layer Protocol; T1571 Non-Standard Port
Initial Access
T1190 Exploit Public-Facing Application
Defense Evasion
T1211 Exploitation for Stealth; T1620 Reflective Code Loading
Resource Development
T1587.004 Develop Capabilities: Exploits; T1588.002 Obtain Capabilities: Tool; T1588.006 Obtain Capabilities: Vulnerabilities
Affected products and versions in lz1 LZ77 Decompression Stack Buffer Overflow
- andyherbert (GitHub individual maintainer) — lz1 (LZ77 compressor/decompressor, educational reference implementation)
Vulnerable versions: all versions / all commits (no tagged releases; library has no bounds checking by design)
Fixed in: none released; maintainer closed the originating bug report (issue #1) as won't-fix
Remediation for lz1 LZ77 Decompression Stack Buffer Overflow
Patches
- No vendor patch exists; the upstream maintainer closed the originating GitHub issue (andyherbert/lz1#1) as won't-fix, stating the code is intentionally left without boundary/error checks for instructional purposes
Immediate actions
- Identify any internal projects that vendor, fork, or import andyherbert/lz1 for decompression and stop feeding it untrusted/externally sourced compressed input
- Treat the lz1 library as unsafe-by-design for any non-educational use; do not deploy it to process network- or file-supplied compressed data
Workarounds
- Enforce a hard cap on the compressed input size and an independent sanity check on the self-reported uncompressed_size before invoking the decompressor
- Run any lz1-based decompression in an isolated, seccomp-restricted, non-executable-stack sandboxed process to contain a successful overflow
- Where feasible, compile any custom decompressor with PIE, ASLR, and stack canaries enabled to break the fixed-address ROP chain demonstrated in the public PoC
Longer-term hardening
- Replace the educational lz1/LZ77 implementation with an audited, hardened compression library (e.g., zlib, zstd, lz4) that validates pointer length/position against destination buffer bounds before copying
- Add explicit bounds checks in any custom LZ77-style decompressor: verify coding_pos + pointer_length never exceeds the allocated destination buffer size before performing the back-reference copy
- Build and run decompression code with stack canaries, full ASLR, non-executable stack (NX), and RELRO; avoid statically linked, no-PIE builds for any code that parses untrusted input
- Fuzz any in-house or third-party decompression code path with AFL/libFuzzer as part of CI before accepting untrusted compressed input in production
- Harden or restrict runtime availability of _dl_make_stacks_executable-style internal libc primitives where feasible, since this exploit chain relies on calling it directly to defeat an otherwise non-executable stack
Weaknesses (CWE) in lz1 LZ77 Decompression Stack Buffer Overflow
Timeline of lz1 LZ77 Decompression Stack Buffer Overflow
- Maintainer andyherbert closes issue #1 the same day, stating the code is intentionally instructional and lacks boundary/error checks by design; no fix issued.
- GitHub user rwhitworth opens andyherbert/lz1 issue #1, reporting that AFL-fuzz found ten distinct crashing inputs in the decompression code, attaching a crashes.zip of test cases.
- STAR Labs publicly announces the 'lz1' Summer Pwnables 2025 challenge via 'When the Heat Rises, So Do the C-Shells', distributing lz1.zip (SHA256 5b0c41b916530b16a2a7d87eff4fb426357fefba0e8c0cf41c06bd6725c2b8fc, password baby_blastpass) via the star-sg/challenges GitHub repo, open to Singapore-based students, with a remote submission target at 159.223.33.156:9101 and a Google Form for writeup/exploit submission.
- In-person deadline for collecting prizes (signed book plus SGD $50-100) for first successful exploiters and best writeup submissions for the Summer Pwnables 2025 series.
- STAR Labs publishes the full 'lz1 Solution' writeup by Zafir Rasyidi Taufik, including complete PoC exploit source (exploit.py, listen.py, rop_payload.lz77, payload-exp) implementing the ROP chain, executable-stack bypass via a jmp rsp pivot, and syscall-based reverse-shell shellcode.
- PoC constructs a ~0x80-byte ROP chain using fixed gadgets (pop_rdi_rbp, pop_rax, pop_rcx, pop_rbx, pop_rsi_rbp, pop_rdx) to synthesize a custom RDX value and call glibc-internal _dl_make_stacks_executable, marking the stack executable and defeating the glibc 2.39 __stack_prot NX guard.
- Exploitation approach pivots to targeting the stack buffers (uncompressed_text/compressed_text) allocated via alloca inside the decompression function, where the unbounded pointer-token copy enables overwriting the saved return address.
- Writeup documents that an initial exploitation attempt targeting heap-adjacent memory was explored and abandoned as a dead end before the working approach was found.
Sources cited for lz1 LZ77 Decompression Stack Buffer Overflow
- Summer Pwnables: lz1 Solution (Zafir Rasyidi Taufik, STAR Labs)
- Summer Pwnables: When the Heat Rises, So Do the C-Shells (challenge announcement incl. lz1)
- andyherbert/lz1 GitHub Issue #1 — Decompression crashes (AFL-fuzz findings, closed won't-fix)
- andyherbert/lz1 — LZ77 compressor/decompressor source repository
- LZ77 compression algorithm background reference (cited in the STAR Labs writeup)
- STAR Labs Summer Pwnables 2025 challenge archive (lz1.zip, star-sg/challenges repository)
- Summer Pwnables 2025 exploit/writeup submission form (Google Forms)
Detection coverage for TL-2026-4938
As of 2026-10-31, Threadlinqs Intelligence publishes 9 detection rule(s) for TL-2026-4938 across Splunk SPL, Microsoft KQL and Sigma, covering 16 indicator(s) of compromise. The whole corpus is readable without an account; a free account unlocks full detection query text in Splunk SPL, Microsoft KQL and Sigma; paid tiers add raw indicator values, correlation and the MCP server. Threadlinqs MCP server · View plans.