mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-14 20:20:54 +08:00
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).
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user