Files
ARTEX/scripts/poc/php2shell/docs/ANALYSIS.md
BaaRai 750f4eb091 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).
2026-10-21 14:52:40 +02:00

6.8 KiB

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)

// 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 adds !PS(use_only_cookies) && check.

3.2 Unvalidated Filename

// 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)

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)

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

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

# 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

; 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