From 2-Bit Reset to 0-Click RCE in Redis: A Pwn2Own Edition

Benny Isaacs (Senior Security Researcher · Wiz)

Hexacon 2025 · Day 1 · Main Stage

Overview

In a groundbreaking presentation at Hexacon, Benny Isaacs, a Senior Security Researcher at Wiz, detailed a complex zero-click Remote Code Execution (RCE) vulnerability discovered and exploited in Redis, one of the world's most ubiquitous in-memory databases. This talk, which earned Wiz a significant prize at Pwn2Own Berlin, unveiled a bug that lay dormant for 13 years within the Redis Lua scripting engine. The vulnerability allowed attackers to bypass Redis's robust sandboxing mechanisms and achieve full system compromise without any user interaction, merely by submitting a specially crafted Lua script.

Watch on YouTube

Visual summary for From 2-Bit Reset to 0-Click RCE in Redis: A Pwn2Own Edition by Benny Isaacs
Visual summary for From 2-Bit Reset to 0-Click RCE in Redis: A Pwn2Own Edition by Benny Isaacs

Key moments

  1. 0:00 Introduction to Redis Pwn2Own success and CVE
  2. 2:15 Attack vector: Redis Lua scripts and 'load' function
  3. 3:50 Lua internals: GC, TString structure, and mark-and-sweep
  4. 7:30 The critical bug: TString freed, creating dangling pointer
  5. 8:20 Use-After-Free: Dangling pointer used by Proto object
  6. 9:30 Closure construction and pushing to Lua stack

From 2-Bit Reset to 0-Click RCE in Redis: A Pwn2Own Edition

Speakers: Benny Isaacs, Senior Security Researcher, Wiz

Conference: Hexacon

YouTube: https://www.youtube.com/watch?v=-yT4sH_gvj4

Overview

In a groundbreaking presentation at Hexacon, Benny Isaacs, a Senior Security Researcher at Wiz, detailed a complex zero-click Remote Code Execution (RCE) vulnerability discovered and exploited in Redis, one of the world's most ubiquitous in-memory databases. This talk, which earned Wiz a significant prize at Pwn2Own Berlin, unveiled a bug that lay dormant for 13 years within the Redis Lua scripting engine. The vulnerability allowed attackers to bypass Redis's robust sandboxing mechanisms and achieve full system compromise without any user interaction, merely by submitting a specially crafted Lua script.

The significance of this discovery cannot be overstated. Redis is deployed in approximately 75% of cloud environments globally, serving as a critical component for caching, session management, real-time analytics, and more. A 0-click RCE in such a foundational technology presents a severe risk to a vast array of internet-facing applications and infrastructure. Isaacs' presentation not only revealed the intricate details of the vulnerability but also provided a masterclass in advanced heap exploitation techniques required to leverage a subtle 2-bit memory corruption into arbitrary code execution.

The journey from discovery to RCE was a testament to the team's expertise, completed under the intense pressure of Pwn2Own deadlines. The exploit itself was a substantial 2,500 lines of code, meticulously crafted to achieve reliable exploitation. This article delves into the technical intricacies of the vulnerability, the sophisticated exploitation chain, and the critical lessons for defenders operating in cloud and on-premise environments alike.

Background

▶ Watch: Introduction to Redis Pwn2Own success and CVE (0:00)

Redis is an open-source, in-memory data structure store, used as a database, cache, and message broker. Its high performance and versatility have made it a cornerstone of modern web applications. A key feature contributing to its flexibility is the ability to execute Lua scripts directly on the server via the EVAL command. This allows for atomic operations and complex logic to be executed close to the data, reducing network latency.

However, security is paramount in such a widely deployed system. Redis's Lua interpreter is designed as a minimal and sandboxed environment. It intentionally restricts access to system functions, file I/O, network operations, and even basic print statements or loading external libraries. The goal is to ensure that user-supplied scripts can only perform safe, data-centric operations within the database context, preventing malicious scripts from impacting the host system.

The vulnerability discussed in this talk primarily revolves around Lua's internal memory management, specifically its Garbage Collector (GC), and how it interacts with the load function. The load function in Lua takes a callback to incrementally retrieve "code chunks" and, once all chunks are received, compiles them into an executable function (a closure). When the chunkname parameter for load is omitted, it defaults to the string "=load". Understanding Lua's GC is crucial here: it employs a basic mark-and-sweep algorithm. Every Lua object (like strings, tables, functions) is wrapped in a GC header (GCheader), which includes fields like tt (type) and marked. The marked field indicates whether an object is currently referenced and should be preserved or can be freed. Strings in Lua, represented by TString objects, are stored contiguously with their GCheader and data, meaning the string data immediately follows the header without an intervening pointer. This memory layout is key to the exploitation.

Key Findings

▶ Watch: Lua internals: GC, TString structure, and mark-and-sweep (3:50)

The central finding of this research is a Use-After-Free (UAF) vulnerability within the Lua interpreter embedded in Redis, specifically affecting how the load function handles chunk names and interacts with the garbage collector. This UAF, introduced 13 years ago, manifests as a subtle 2-bit reset primitive on a controlled memory region.

Here's the sequence of key findings:

  1. Dangling Pointer Creation: When the load function is called without a specified chunk name, the default string "=load" is first represented as a C string. Later, during parsing, it's wrapped into a Lua TString object, making it trackable by the Lua GC. This TString is then referenced by a lex_state structure, which is allocated on the native stack (not the Lua heap). Crucially, because lex_state is on the native stack, the Lua GC does not track pointers originating from it.
  2. Premature Free: The exploit triggers a major GC cycle (e.g., using collectgarbage()) from within a user-controlled callback provided to load. During this GC cycle, the "=load" TString object, despite being actively referenced by the lex_state on the native stack, appears unreferenced to the Lua GC. Consequently, the GC incorrectly identifies it as garbage and frees the TString object. This leaves the lex_state with a dangling pointer to freed memory.
  3. The "Use" - 2-Bit Reset: The proto object, which encapsulates the compiled function's metadata, later acquires this same dangling pointer to the freed TString for its source field. A second GC cycle is then strategically triggered during error handling (specifically by debug.getinfo called within Redis's custom error handler) when the load function returns an invalid value from the callback. When this second GC traverses the proto object, it attempts to "mark" the source TString (which is already freed). The marking operation, implemented by resetting two specific bits in the marked field of the GCheader, now corrupts the freed memory.
  4. Controlled Corruption Primitive: This 2-bit reset, while seemingly minor, becomes a powerful primitive when the freed TString's memory is reclaimed by a carefully placed Lua table object. By precisely aligning the node pointer of the Lua table with the marked field of the original TString's GCheader, the 2-bit reset directly corrupts the node pointer of the table. This allows the attacker to shift the node pointer by a small, controlled offset (e.g., 0x100, 0x200, 0x300 bytes), effectively making the table point to an attacker-controlled "fake" node in memory. This marks the transition from a UAF to a powerful arbitrary object faking capability.

Technical Deep Dive

▶ Watch: The critical bug: TString freed, creating dangling pointer (7:30)

The journey to RCE begins with a meticulous understanding of Lua's internal object model and garbage collection, coupled with precise timing of GC cycles.

The core vulnerability lies within the LuaB_load C implementation of Lua's load function. When no chunkname is explicitly provided, a default C string "=load" is used. This C string is later converted into a TString object and stored in a lex_state structure, which resides on the native stack. The lex_state contains a pointer to this TString. Crucially, because lex_state is a native stack variable, its pointers are not visible or tracked by the Lua garbage collector.

The exploitation leverages the attacker's ability to supply a callback function to load. Within this callback, a major GC cycle is explicitly triggered using collectgarbage(). At this point, the "=load" TString is still referenced by lex_state. However, from the Lua GC's perspective, this TString appears unreferenced because the lex_state is on the native stack. Consequently, the GC frees the TString object, leaving lex_state with a dangling pointer.

The "use" part of the UAF occurs later. The load function is designed to return a closure, which is an interpreted function. This closure contains a reference to a proto object, which itself holds a source field pointing to the original TString (the chunk name). Since the original TString was freed, the proto object now holds the same dangling pointer.

To trigger the corruption, the attacker's callback returns an invalid value (e.g., not a string). This causes luaL_error to be invoked, which in Redis's custom error handler, calls debug.getinfo. The debug.getinfo function attempts to allocate Lua objects. This allocation attempt, combined with a carefully set GC threshold (collectgarbage("restart") followed by collectgarbage("setstepmul", 0) to fine-tune the GC cycle size), triggers a second GC cycle. During this second GC, the proto object (which is on the Lua stack and thus tracked) is traversed. The traverseproto function attempts to mark its source field. The marking operation, resetting two bits in the marked field of the GCheader, now operates on the already freed TString memory. If the marked field was 0xFF, it might become 0xFC (assuming the two least significant bits are cleared). This is the 2-bit reset primitive.

The next challenge is to transform this subtle 2-bit reset into a powerful memory primitive. The strategy involves heap shaping. The attacker reclaims the memory freed by the TString with a Lua table object. A Lua table is essentially a hash map, comprising an array of nodes, where each node contains a TValue (value) and TKey (key). The table itself has a node pointer to its array of nodes. Through extensive heap spraying and consolidation (tcache filling, heap consolidation), the exploit ensures that the node pointer of the attacker-controlled Lua table precisely overlaps with the marked field of the original TString's GCheader.

When the 2-bit reset occurs, it corrupts the node pointer of the Lua table. For instance, if the original node pointer was 0xdeadbeef, resetting the two least significant bits might shift it to 0xdeadbeec, 0xdeadbea8, etc., depending on the original bits. This shifts the pointer by a small, controlled offset (e.g., by 0x100, 0x200, or 0x300 bytes). Since the exploit has already sprayed the heap with many nodes (from other Lua tables), the corrupted pointer now points to one of these attacker-controlled "fake" node structures. This gives the attacker control over the table's internal node array.

To construct arbitrary objects, an address leak is essential. Lua provides a convenient feature: calling tostring() on a non-string Lua object (like a table) returns its memory address. However, tostring() on a TString does not. To leak TString addresses, the exploit allocates a Lua table followed by the target TString in memory. By calculating table_address + sizeof(table), the TString's address can be inferred. Reliability is improved by allocating a second table after the TString to verify the expected memory layout.

With a corrupted node pointer and address leak, the attacker can now achieve object faking. By controlling the node array, they can create fake TValues and TKeys within their Lua table. A TValue consists of a pointer to data and a tt (type) field. By setting tt to LUA_TSTRING and the data pointer to an arbitrary address (minus the TString header size), an absolute read primitive is created. The string.sub() function can then be used on this fake TString to read data from arbitrary memory locations. Lua's lenience towards garbage TString headers (as long as the reported length is sufficient) makes this reliable.

Finally, to achieve Remote Code Execution, the exploit constructs a ROP (Return-Oriented Programming) chain. This involves reading Redis's ELF binaries to find suitable gadgets and bypass pointer guard protections (e.g., by leaking OpenSSL pointers). The ROP chain is designed to eventually call system() with a controlled command. To trigger this ROP chain, the exploit creates a fake C Closure object. A C Closure in Lua wraps a C function pointer, allowing it to be called from Lua. By faking a C Closure whose internal function pointer points to the start of the ROP chain, and then executing this fake closure from Lua, the ROP chain is triggered, leading to system() execution and a remote shell.

Demo / Proof of Concept

▶ Watch: Use-After-Free: Dangling pointer used by Proto object (8:20)

Benny Isaacs provided a live demonstration of the exploit, showcasing the full chain from the initial Lua script submission to gaining a remote shell on the Redis server. The setup involved a Redis instance running on an EC2 host, a Netcat listener to catch the reverse shell, and the exploit script.

The demo, though slightly slowed by network constraints, clearly illustrated the successful exploitation. Debug information, intentionally verbose for Pwn2Own reliability, scrolled by, showing the crucial steps: the TString address leak (by calculating the difference between two adjacent objects), and the subsequent heap manipulations. After a tense few seconds, the exploit successfully established a reverse shell. Isaacs then demonstrated the shell's capabilities by executing whoami, confirming that the exploit ran as the redis user, thus achieving full RCE. The speaker noted that the exploit incorporates a retry mechanism to account for scenarios where the initial 2-bit reset might not corrupt the node pointer in a usable way (e.g., if the target bits were already zeroed), allowing for multiple attempts without crashing the target, which is critical for Pwn2Own rules.

Defensive Implications

▶ Watch: Closure construction and pushing to Lua stack (9:30)

The discovery of this 0-click RCE in Redis carries significant defensive implications for organizations worldwide. Given Redis's widespread adoption across cloud environments and its role in critical infrastructure, immediate action is warranted.

  1. Prioritize Updates: The most crucial defensive measure is to promptly update Redis instances to the patched version. The vulnerability (CVE, though not explicitly stated in the transcript, was patched and disclosed by Redis) addresses the UAF, rendering this specific exploit ineffective. Organizations should implement robust patch management policies for all their software components, especially those as fundamental as Redis.
  2. Least Privilege Principle: Even with the patch, adhering to the principle of least privilege for Redis deployments is vital. The demo showed RCE as the redis user. Running Redis with minimal permissions (e.g., a dedicated, unprivileged user, restricted network access, and no unnecessary capabilities) can limit the impact of any future compromise.
  3. Network Segmentation: Isolate Redis instances within highly segmented network zones. They should only be accessible from trusted application servers and not directly exposed to the internet or untrusted networks. Firewalls and security groups should strictly control inbound and outbound connections.
  4. Lua Scripting Review: For environments that utilize Redis's Lua scripting capabilities, a thorough review of all deployed scripts is recommended. While the sandbox is generally robust, this exploit demonstrates that subtle interactions with Lua internals can lead to bypasses. Ensure scripts are minimal, audited, and strictly necessary.
  5. Monitoring and Logging: Enhance monitoring and logging for Redis instances. Look for unusual activity, such as unexpected process spawning, outbound network connections from the Redis process, or abnormal resource consumption.
  6. Containerization and Sandboxing: Deploying Redis within containerized environments (e.g., Docker, Kubernetes) with additional sandboxing mechanisms (e.g., seccomp, AppArmor) can add layers of defense, though this exploit demonstrates that even a seemingly robust sandbox like Lua's can be circumvented.
  7. Input Validation: While this was a 0-click RCE through a trusted interface, general best practices for input validation remain important for all user-supplied data, even when interacting with internal components.

Key Takeaways

  • 13-Year-Old Bug: A critical Use-After-Free vulnerability existed in Redis's Lua scripting engine for 13 years, highlighting the long-term persistence of deep-seated bugs in widely used software.
  • Lua Sandbox Bypass: The exploit successfully bypassed Redis's Lua sandbox, demonstrating that even carefully designed sandboxing mechanisms can be vulnerable to complex memory corruption issues.
  • Subtle Primitive, Powerful Impact: A seemingly minor 2-bit reset primitive, achieved through precise GC timing and heap shaping, was escalated into a full Remote Code Execution capability.
  • Multi-Stage Exploitation: The RCE required a sophisticated, multi-stage exploit chain involving heap shaping, an address leak, arbitrary object faking, an absolute read primitive, and a ROP chain triggered by a fake C closure.
  • Critical Cloud Vulnerability: Given Redis's prevalence in 75% of cloud environments, this 0-click RCE posed a significant risk to a vast array of internet-facing applications and infrastructure.
  • Prompt Patching is Essential: Immediate updating of Redis instances to the patched version is paramount for all users to mitigate this and similar vulnerabilities.

About the Speaker(s)

Benny Isaacs is a Senior Security Researcher at Wiz, a leading cloud security company. With 11 years of experience in security research, Benny has a diverse background, having previously focused on Windows, IoT, Android, and Chrome vulnerabilities. His current primary focus at Wiz is on cloud environments, where he applies his deep expertise to uncover critical security flaws. This presentation showcases his significant contribution to the security community, particularly in the realm of complex heap exploitation and bypassing sandboxed environments. He was joined in the discovery and exploitation efforts by his colleague, Neil Braha.

Reviews

Dr. Zero (Offensive Security Researcher) — MUST SEE

Isaacs delivers the real thing: a 13-year-old UAF in the Lua GC, buried deep enough that nobody found it, weaponized into a reliable 0-click RCE against one of the most widely deployed pieces of infrastructure on the planet. The chain is genuinely elegant — GC timing abuse to manufacture a dangling pointer, a 2-bit reset primitive that most researchers would have thrown away as useless, then methodical heap shaping to bootstrap that into arbitrary object faking, an absolute read, and a ROP chain punching through pointer guard. This is the kind of work that makes you question what else is hiding in sandboxed scripting runtimes nobody audits seriously. Wiz earned their Pwn2Own prize.

Heather Calloway (CISO) — WEAK

Technically impressive Pwn2Own research on a 13-year-old Use-After-Free in Redis's Lua scripting engine — real impact, real sophistication. But the talk is written for exploit developers, not for the people responsible for securing the 75% of cloud environments where Redis runs. The defensive section is a checklist of generic hygiene items that any junior analyst could have written without knowing this CVE existed. The governance gap — who owns Redis security posture at the enterprise level, how this class of embedded interpreter risk gets assessed, what it means for cloud-native trust assumptions — goes completely unaddressed. The research deserves a better translation than it got.

→ Top-rated talks at Hexacon 2025

All talks from Hexacon 2025