UAF exploit: exhaustive analysis, five independent RCE blockers documented
Type confusion via var_access confirmed: R:N; references resolve stale entries.data[0] without crashing, returning the attacker's own session data from the cached ZendMM chunk (pages retained, not zeroed). Key finding: filler spray and type confusion are mutually exclusive. var_tmp_var reads last_dtor (struct offset 16) BEFORE var_access reads entries (offset 56). Spray corrupts offset 16, crashing before the type confusion can execute. Built-in class audit: SplFileObject and PDO have function pointer calls in free_obj, but constructing fake objects requires libphp.so base (ASLR). Patch audit (8.5.12): all three defense layers are solid, no bypass found. Five RCE blockers: no info leak, filler/type-confusion mutual exclusion, GC header overlap, no usable classes without ASLR bypass, session key validation.
This commit is contained in:
+55
-35
@@ -53,28 +53,46 @@ Requirements:
|
||||
|
||||
Exploitation notes:
|
||||
|
||||
This PoC demonstrates the UAF primitive (stale pointer dereference)
|
||||
and crash (SIGSEGV from corrupted 'last' pointer). Full RCE from
|
||||
this primitive is blocked by an overlap between the struct's critical
|
||||
pointer fields (offsets 0-23: last, first_dtor, last_dtor) and the
|
||||
zend_string GC header (refcount, type_info, hash, len). These
|
||||
fields cannot be directly controlled by the attacker.
|
||||
This PoC demonstrates two distinct UAF primitives:
|
||||
|
||||
Specifically:
|
||||
- 'last' = gc_refcount(4) + gc_type_info(4) -> invalid pointer
|
||||
- 'first_dtor' = hash(8) = 0 initially -> NULL (safe for var_destroy)
|
||||
- 'last_dtor' = len(8) = string length -> low address (crash)
|
||||
1. CRASH (var_push path): Filler fields reclaim the stale struct
|
||||
pages. The zend_string GC header at offset 0 overwrites the
|
||||
'last' pointer with refcount|type_info (not a valid address).
|
||||
var_push reads 'last' and dereferences it -> SIGSEGV.
|
||||
|
||||
Controlled bytes start at offset 24 (allowed_classes, ref_props,
|
||||
max_depth, entries.used_slots, entries.next, entries.data[]), but
|
||||
var_push crashes before these fields are reached because it reads
|
||||
'last' first.
|
||||
2. TYPE CONFUSION (var_access path): Session data using R:N
|
||||
backreferences resolves through var_access, which reads the
|
||||
inline 'entries' at struct offset 56 instead of 'last' at
|
||||
offset 0. Without the filler spray, the stale struct's 'last'
|
||||
pointer self-references &entries (valid), and entries.data[0]
|
||||
holds a dangling pointer to a zval from request 1's heap.
|
||||
PHP reads this stale zval without crashing. The value's type
|
||||
matches whatever the first OOM session entry contained:
|
||||
- String value -> stale IS_STRING (original content intact)
|
||||
- Integer value -> stale IS_LONG (original value)
|
||||
- Object value -> CRASH (freed zend_object dereference)
|
||||
|
||||
Full RCE would require either:
|
||||
a) An info leak to construct a valid 'last' pointer in the GC header
|
||||
position (which would require a separate vulnerability)
|
||||
b) A different allocation type that gives control over offset 0 of
|
||||
a 3-page (8216-byte) ZendMM allocation
|
||||
The two primitives are MUTUALLY EXCLUSIVE: filler spray corrupts
|
||||
struct offset 16 (last_dtor), and var_tmp_var reads last_dtor
|
||||
BEFORE var_access runs. With fillers, var_tmp_var crashes.
|
||||
Without fillers, the type confusion works but reads the attacker's
|
||||
own data from the stale heap (the cached ZendMM chunk retains page
|
||||
data across requests without zeroing). No info leak is possible.
|
||||
|
||||
Full RCE is blocked by:
|
||||
- ASLR: no info leak primitive. The type confusion reads back
|
||||
the attacker's own session data, not heap metadata or code
|
||||
pointers. The cached chunk retains original bytes.
|
||||
- Mutual exclusion: filler spray (needed for controlled content)
|
||||
crashes var_tmp_var before var_access can read the fillers.
|
||||
- GC header overlap at offset 0: prevents pointer control when
|
||||
spraying (var_push reads 'last' before reaching controlled
|
||||
bytes at offset 24+)
|
||||
- No loaded classes during sapi_activate: PHP built-in classes
|
||||
(SplFileObject, PDO) have exploitable handlers but constructing
|
||||
fake objects requires knowing libphp.so base address (ASLR)
|
||||
- Session key validation: PHP's php serializer skips keys
|
||||
containing '|', blocking injection via upload progress name
|
||||
|
||||
Usage:
|
||||
python3 exploit.py --target http://host:port/index.php
|
||||
@@ -544,27 +562,29 @@ def print_summary(patched=False, crash=False):
|
||||
print(" BG(unserialize).level=1, .data=freed heap pointer")
|
||||
print(" Apache prefork worker survives, serves new requests")
|
||||
if crash:
|
||||
print(" [+] Use-After-Free crash on next request")
|
||||
print(" [+] Use-After-Free crash (var_push path)")
|
||||
print(" Filler zend_strings reclaim stale struct pages")
|
||||
print(" var_push dereferences GC header as 'last' pointer -> SIGSEGV")
|
||||
print(" [+] Type confusion (var_access path)")
|
||||
print(" R:N backreferences bypass the corrupted 'last' pointer")
|
||||
print(" var_access reads inline entries at offset 56 (not offset 0)")
|
||||
print(" Stale entries.data[0] resolves to freed heap memory")
|
||||
print(" PHP interprets stale bytes as a live zval (observed: IS_ARRAY)")
|
||||
print()
|
||||
print(" Exploitation status:")
|
||||
print(" The UAF primitive (stale pointer reuse) is confirmed.")
|
||||
if crash:
|
||||
print(" Page reclamation via filler fields is confirmed.")
|
||||
print(" However, full RCE is blocked by an inherent constraint:")
|
||||
print(" the struct's critical pointer fields (offsets 0-23: last,")
|
||||
print(" first_dtor, last_dtor) overlap with the zend_string GC header")
|
||||
print(" (refcount, type_info, hash, len), which cannot be controlled")
|
||||
print(" by the attacker.")
|
||||
print(" Two UAF primitives confirmed: crash (DoS) and type confusion.")
|
||||
print(" Full RCE is blocked by five independent constraints:")
|
||||
print()
|
||||
print(" 'last' = gc_refcount + gc_type_info -> invalid pointer (crash)")
|
||||
print(" 'first_dtor' = hash = 0 initially (safe, dtor chain empty)")
|
||||
print(" 'last_dtor' = len = string length (low address, crash)")
|
||||
print()
|
||||
print(" Full RCE would require an info leak for valid pointer placement")
|
||||
print(" at offset 0, or a different allocation type that gives control")
|
||||
print(" over the first 24 bytes of an 8216-byte ZendMM allocation.")
|
||||
print(" 1. ASLR: type confusion reads attacker's own data, not heap")
|
||||
print(" metadata or code pointers (cached chunk retains original bytes)")
|
||||
print(" 2. Mutual exclusion: filler spray crashes var_tmp_var (offset 16)")
|
||||
print(" before var_access can read controlled data at offset 56+")
|
||||
print(" 3. GC header overlap: uncontrolled bytes at struct offset 0-23")
|
||||
print(" prevent var_push from reaching controlled data at offset 24+")
|
||||
print(" 4. No exploitable classes: SplFileObject/PDO have function pointer")
|
||||
print(" calls in free_obj, but fake objects need libphp.so base (ASLR)")
|
||||
print(" 5. Session key validation: php serializer skips keys with '|',")
|
||||
print(" blocking deserialization injection via upload progress name")
|
||||
print()
|
||||
print(" Affected: PHP 5.4.0 through 8.3.35, 8.4.0-8.4.26, 8.5.0-8.5.11")
|
||||
print(" Fixed in: PHP 8.3.36, 8.4.27, 8.5.12")
|
||||
|
||||
Reference in New Issue
Block a user