php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #76047 Use-after-free when accessing already destructed backtrace arguments
Submitted: 2018-03-04 09:50 UTC Modified: 2020-01-31 08:50 UTC
Votes:4
Avg. Score:5.0 ± 0.0
Reproduced:3 of 4 (75.0%)
Same Version:2 (66.7%)
Same OS:1 (33.3%)
From: kenashkov at gmail dot com Assigned: nikic (profile)
Status: Closed Package: Reproducible crash
PHP Version: 7.2.3 OS: CentOS Linux release 7.0.1406 (C
Private report: No CVE-ID: None
 [2018-03-04 09:50 UTC] kenashkov at gmail dot com
Description:
------------
Reproducible crash on every execution. The script is too big and I cant yet isolate a small reproducible example but I did narrow down a little change in the code that prevents the crash. Just assigning the value of a string variable to something (not even using this new var at all) goes around the problem:
-------
$object->overloaded_property = $some_string;//produces a crash
-------
$something = $some_string;//no longer crashes
$object->overloaded_property = $some_string;
-------
//and again a crash if $something is unset
$something = $some_string;
unset($something);//this triggers the crash again
$object->overloaded_property = $some_string;
------

Maybe it has to do with the refcount. Other modifications of the code resulted in crashes in different places. I have given below the original bt (crash in PDOStatement->fetchAll()) but after modifications I got a crash in the file() function. 

PHP is compiled with:
'./configure' '--prefix=/web/php7.2' '--with-apxs2=/web/apache2.4-php7.2/bin/apxs' '--enable-bcmath' '--enable-calendar' '--enable-dbase' '--enable-exif' '--enable-ftp' '--enable-libxml' '--enable-mbstring' '--enable-pdo' '--enable-soap' '--enable-sockets' '--enable-zip' '--with-bz2' '--with-curl' '--with-gd' '--with-mcrypt' '--with-mhash' '--with-mime-magic' '--with-mysql=mysqlnd' '--with-openssl' '--with-pdo-sqlite=shared' '--with-pgsql' '--with-sqlite=shared' '--with-tidy' '--with-xsl' '--with-zlib' '--with-zlib-dir' '--with-pdo-mysql=mysqlnd' '--with-pdo-pgsql' '--with-jpeg-dir' '--with-png-dir' '--with-xpm-dir' '--with-ttf' '--with-freetype-dir' '--with-t1lib' '--enable-gd-native-ttf' '--with-mysqli=mysqlnd' '--with-imap-ssl' '--with-kerberos' '--with-gettext' '--enable-sysvsem' '--enable-sysvshm' '--enable-sysvmsg' '--enable-pcntl' '--enable-opcache' '--enable-debug' 

No opcache or xdebug enabled. Redis is the only PECL extension used by the code but I modified it to remove the need for it and the crashes are reproducible.

Test script:
---------------
I dont have a short one yet. I post this in the hope that someone may give me a hint what else to test, look for or trace.

Actual result:
--------------
First segfault without any code modifications:

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffd47e8700 (LWP 5004)]
0x00007ffff2b796da in zend_mm_alloc_small (heap=0x7fffd3200040, size=232, bin_num=15, __zend_filename=0x7ffff341fa18 "/web/php-7.2.3/Zend/zend_string.h", __zend_lineno=134, __zend_orig_filename=0x0, __zend_orig_lineno=0)
    at /web/php-7.2.3/Zend/zend_alloc.c:1273
1273                    heap->free_slot[bin_num] = p->next_free_slot;
Missing separate debuginfos, use: debuginfo-install libtidy-0.99.0-31.20091203.el7.x86_64
(gdb) bt
#0  0x00007ffff2b796da in zend_mm_alloc_small (heap=0x7fffd3200040, size=232, bin_num=15, __zend_filename=0x7ffff341fa18 "/web/php-7.2.3/Zend/zend_string.h", __zend_lineno=134, __zend_orig_filename=0x0, __zend_orig_lineno=0)
    at /web/php-7.2.3/Zend/zend_alloc.c:1273
#1  0x00007ffff2b7996d in zend_mm_alloc_heap (heap=0x7fffd3200040, size=232, __zend_filename=0x7ffff341fa18 "/web/php-7.2.3/Zend/zend_string.h", __zend_lineno=134, __zend_orig_filename=0x0, __zend_orig_lineno=0)
    at /web/php-7.2.3/Zend/zend_alloc.c:1344
#2  0x00007ffff2b7c44d in _emalloc (size=200, __zend_filename=0x7ffff341fa18 "/web/php-7.2.3/Zend/zend_string.h", __zend_lineno=134, __zend_orig_filename=0x0, __zend_orig_lineno=0) at /web/php-7.2.3/Zend/zend_alloc.c:2433
#3  0x00007ffff2af218a in zend_string_alloc (len=172, persistent=0) at /web/php-7.2.3/Zend/zend_string.h:134
#4  0x00007ffff2af21f3 in zend_string_init (
    str=0x7fffd18de779 "KcGAXUg7vPDIT5DN7eu1wkT7eBz79ilDqpZfFhjgGruEl0hMPb+HhqJ70JxckyokN1ntznk98g6ZIP1fYB+b3lhh3E7w25mmwVhcM947/jFz4U437B95yPw/5Wv7wbsl6iCRLKmIOxH1agonv+pHrSa0nahmHuMt5p/kzvOv71E=\025XXXXXXXXXXXXXXXXXXXXX", len=172,
    persistent=0) at /web/php-7.2.3/Zend/zend_string.h:170
#5  0x00007ffff2af5193 in ps_fetch_string (zv=0x7fffd264f7a8, field=0x7fffa1dd0488, pack_len=0, row=0x7fffd47e5c20) at /web/php-7.2.3/ext/mysqlnd/mysqlnd_ps_codec.c:347
#6  0x00007ffff2a7fa13 in php_mysqlnd_rowp_read_binary_protocol (row_buffer=0x7fffd212cb08, fields=0x7fffd264f728, field_count=82, fields_metadata=0x7fffa1dd0008, as_int_or_float=0 '\000', stats=0x7fffd317de10)
    at /web/php-7.2.3/ext/mysqlnd/mysqlnd_wireprotocol.c:1581
#7  0x00007ffff2adeb5b in mysqlnd_stmt_fetch_row_buffered (result=0x7fffa1d15e88, param=0x7fffa1d749a8, flags=0, fetched_anything=0x7fffd47e5f2f "") at /web/php-7.2.3/ext/mysqlnd/mysqlnd_ps.c:784
#8  0x00007ffff2ab5b85 in mysqlnd_mysqlnd_res_fetch_row_pub (result=0x7fffa1d15e88, param=0x7fffa1d749a8, flags=0, fetched_anything=0x7fffd47e5f2f "") at /web/php-7.2.3/ext/mysqlnd/mysqlnd_result.c:1275
#9  0x00007ffff2ae40c1 in mysqlnd_mysqlnd_stmt_fetch_pub (s=0x7fffa1d749a8, fetched_anything=0x7fffd47e5f2f "") at /web/php-7.2.3/ext/mysqlnd/mysqlnd_ps.c:1234
#10 0x00007ffff2803abe in pdo_mysql_stmt_fetch (stmt=0x7fffa1db2700, ori=PDO_FETCH_ORI_NEXT, offset=0) at /web/php-7.2.3/ext/pdo_mysql/mysql_statement.c:623
#11 0x00007ffff27f1ec6 in do_fetch_common (stmt=0x7fffa1db2700, ori=PDO_FETCH_ORI_NEXT, offset=0, do_bind=1) at /web/php-7.2.3/ext/pdo/pdo_stmt.c:671
#12 0x00007ffff27f254e in do_fetch (stmt=0x7fffa1db2700, do_bind=1, return_value=0x7fffd47e62a0, how=PDO_FETCH_ASSOC, ori=PDO_FETCH_ORI_NEXT, offset=0, return_all=0x0) at /web/php-7.2.3/ext/pdo/pdo_stmt.c:828
#13 0x00007ffff27f55d2 in zim_PDOStatement_fetchAll (execute_data=0x7fffd322c010, return_value=0x7fffd322be50) at /web/php-7.2.3/ext/pdo/pdo_stmt.c:1505
#14 0x00007ffff2c2b799 in ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER () at /web/php-7.2.3/Zend/zend_vm_execute.h:1032
#15 0x00007ffff2cb245c in execute_ex (ex=0x7fffd32249b0) at /web/php-7.2.3/Zend/zend_vm_execute.h:59752
#16 0x00007ffff2ba3a74 in zend_call_function (fci=0x7fffd47e65c0, fci_cache=0x7fffd47e6590) at /web/php-7.2.3/Zend/zend_execute_API.c:819
#17 0x00007ffff29604aa in zif_call_user_func_array (execute_data=0x7fffd3224940, return_value=0x7fffd47e6690) at /web/php-7.2.3/ext/standard/basic_functions.c:4905
#18 0x00007ffff2c2a4b3 in ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_UNUSED_HANDLER () at /web/php-7.2.3/Zend/zend_vm_execute.h:738
#19 0x00007ffff2cb2435 in execute_ex (ex=0x7fffd3221030) at /web/php-7.2.3/Zend/zend_vm_execute.h:59743
#20 0x00007ffff2cb78fe in zend_execute (op_array=0x7fffd3278100, return_value=0x0) at /web/php-7.2.3/Zend/zend_vm_execute.h:63760
#21 0x00007ffff2bc0254 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /web/php-7.2.3/Zend/zend.c:1496
#22 0x00007ffff2b00eae in php_execute_script (primary_file=0x7fffd47e7a90) at /web/php-7.2.3/main/main.c:2590
#23 0x00007ffff2cba763 in php_handler (r=0x7fffcc014c50) at /web/php-7.2.3/sapi/apache2handler/sapi_apache2.c:701
#24 0x0000000000452680 in ap_run_handler (r=r@entry=0x7fffcc014c50) at config.c:170
#25 0x0000000000452bc9 in ap_invoke_handler (r=r@entry=0x7fffcc014c50) at config.c:434
#26 0x0000000000466d6c in ap_internal_redirect (new_uri=<optimized out>, r=<optimized out>) at http_request.c:765
#27 0x00007ffff385ce5c in handler_redirect (r=0x7fffcc002970) at mod_rewrite.c:5254
#28 0x0000000000452680 in ap_run_handler (r=r@entry=0x7fffcc002970) at config.c:170
#29 0x0000000000452bc9 in ap_invoke_handler (r=r@entry=0x7fffcc002970) at config.c:434
#30 0x00000000004679ba in ap_process_async_request (r=r@entry=0x7fffcc002970) at http_request.c:436
#31 0x0000000000463fb1 in ap_process_http_async_connection (c=0x7fffe0039600) at http_core.c:154
#32 ap_process_http_connection (c=0x7fffe0039600) at http_core.c:248
#33 0x000000000045c090 in ap_run_process_connection (c=c@entry=0x7fffe0039600) at connection.c:42
#34 0x000000000046fcdc in process_socket (thd=thd@entry=0x6d3e38, p=<optimized out>, sock=<optimized out>, cs=<optimized out>, my_child_num=my_child_num@entry=0, my_thread_num=my_thread_num@entry=24) at event.c:1021
#35 0x0000000000470709 in worker_thread (thd=0x6d3e38, dummy=<optimized out>) at event.c:2014
#36 0x00007ffff6c97dc5 in start_thread (arg=0x7fffd47e8700) at pthread_create.c:308
#37 0x00007ffff67c0ced in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:113

At frame 4 I just replaced with XXX some sensitive data in the string.

Here is also the valgrind output:
https://pastebin.com/DW32bATf

The code runs correctly without any modifications when running with valgrind and:
USE_ZEND_ALLOC=0
ZEND_DONT_UNLOAD_MODULES=1

So I presume is an issue with the zend memory manager.

And here is another BT of a crash (in the file() function, the provided path is a valid and readable one) I got after some modifications:
https://pastebin.com/0Q7dA1MV

A bug that could be related to this one is:
https://bugs.php.net/bug.php?id=69326

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2018-03-04 11:49 UTC] nikic@php.net
Based on the valgrind trace, I'd say that what happens is that a destructor throws an exception, which captures a backtrace, which contains the function arguments, some of which have already been destroyed by that point.
 [2018-03-04 12:22 UTC] kenashkov at gmail dot com
After some testing I found that it matters if $some_string passed as an argument or not even if after that it is initialized. It also matters the refcount - if it is 2 it fails, 1 passes.

This partial code produces crash:

function f1($some_string) {
$object = some_singleton_class::get_instance();
$some_string = date('Y-m-d');//needs to be a function here so that refcount is 2, having just plain string is refcount=1 and doesnt produce the crash
$object->overloaded_property = $some_string;
}
f1('bla');

While this does not:
function f1() {
$object = some_singleton_class::get_instance();
$some_string = date('Y-m-d');//needs to be a function here so that refcount is 2
$object->overloaded_property = $some_string;
}
f1();

Of course there are plenty other things around that which Im trying to remove. But I was wondering can the above provide some clue as to what may be happening.
 [2018-03-14 10:21 UTC] kenashkov at gmail dot com
As suggested by Niki, I can confirm that the bug is related to an exception in a destructor. The exception is only created (not thrown) for the purpose of preserving the backtrace for other use if need arises (it may be thrown at a much later stage)... And as suggested it is extremely probable that the generated backtrace at the exception creation contains references to destroyed arguments. We still dont have a short reproduction case.
Is there are way to still create an exception in this case?
 [2020-01-30 18:22 UTC] theodore at phpexperts dot pro
Heads up! Someone posted an active exploit against this bug on GitHub in late October 2019 and then posted it on reddit today, 2019-01-29.

This bug needs to be fixed ASAP since it affects PHP 7.0-7.4 and people are already exploiting it in the wild. 

The person who uploaded the exploit, without notifying you guys, really needs to be publicly shamed. This is reprehensible and illegal in the United States.

* https://github.com/mm0r1/exploits/tree/master/php7-backtrace-bypass
* https://www.reddit.com/r/PHP/comments/ew83rx/php_7074_disable_functions_bypass_0day_poc/
 [2020-01-30 19:26 UTC] nikic@php.net
-Status: Open +Status: Verified
 [2020-01-30 19:26 UTC] nikic@php.net
Reduced test case:

<?php

class Vuln {
    public $a;
    public function __destruct() { 
        global $backtrace; 
        unset($this->a);
        $backtrace = (new Exception)->getTrace(); 
    }
}

function trigger_uaf($arg) {
    $arg = str_shuffle(str_repeat('A', 79));
    $vuln = new Vuln();
    $vuln->a = $arg;
}

trigger_uaf('x');

Valgrind:

==19523== Invalid read of size 4
==19523==    at 0x95ACE4: zend_gc_addref (zend_types.h:1035)
==19523==    by 0x95AE04: zval_addref_p (zend_types.h:1070)
==19523==    by 0x963E3B: debug_backtrace_get_args (zend_builtin_functions.c:2156)
==19523==    by 0x9654C8: zend_fetch_debug_backtrace (zend_builtin_functions.c:2551)
==19523==    by 0x96CA84: zend_default_exception_new_ex (zend_exceptions.c:215)
==19523==    by 0x96CD2B: zend_default_exception_new (zend_exceptions.c:246)
==19523==    by 0x944F5E: _object_and_properties_init (zend_API.c:1417)
==19523==    by 0x944FCC: object_init_ex (zend_API.c:1431)
==19523==    by 0x9C097F: ZEND_NEW_SPEC_CONST_UNUSED_HANDLER (zend_vm_execute.h:9225)
==19523==    by 0xA1797F: execute_ex (zend_vm_execute.h:54571)
==19523==    by 0x924E0A: zend_call_function (zend_execute_API.c:812)
==19523==    by 0x98E5EA: zend_objects_destroy_object (zend_objects.c:179)
==19523==  Address 0x124f1d40 is 0 bytes inside a block of size 104 free'd
==19523==    at 0x4C30D3B: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==19523==    by 0x902F35: _efree_custom (zend_alloc.c:2425)
==19523==    by 0x903080: _efree (zend_alloc.c:2545)
==19523==    by 0x939CFC: zend_string_destroy (zend_variables.c:67)
==19523==    by 0x939BFB: rc_dtor_func (zend_variables.c:57)
==19523==    by 0x939B7E: i_zval_ptr_dtor (zend_variables.h:44)
==19523==    by 0x939D90: zval_ptr_dtor (zend_variables.c:84)
==19523==    by 0x992DB7: zend_std_unset_property (zend_object_handlers.c:1127)
==19523==    by 0x9F4460: ZEND_UNSET_OBJ_SPEC_UNUSED_CONST_HANDLER (zend_vm_execute.h:32155)
==19523==    by 0xA19883: execute_ex (zend_vm_execute.h:56539)
==19523==    by 0x924E0A: zend_call_function (zend_execute_API.c:812)
==19523==    by 0x98E5EA: zend_objects_destroy_object (zend_objects.c:179)
==19523==  Block was alloc'd at
==19523==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==19523==    by 0x903F80: __zend_malloc (zend_alloc.c:2975)
==19523==    by 0x902EC8: _malloc_custom (zend_alloc.c:2416)
==19523==    by 0x903006: _emalloc (zend_alloc.c:2535)
==19523==    by 0x78B0ED: zend_string_alloc (zend_string.h:133)
==19523==    by 0x78B235: zend_string_init (zend_string.h:155)
==19523==    by 0x7A9CAF: zif_str_shuffle (string.c:6096)
==19523==    by 0x9B10AC: ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER (zend_vm_execute.h:1314)
==19523==    by 0xA16DF9: execute_ex (zend_vm_execute.h:53797)
==19523==    by 0xA1AF38: zend_execute (zend_vm_execute.h:57913)
==19523==    by 0x93E39D: zend_execute_scripts (zend.c:1665)
==19523==    by 0x89FE54: php_execute_script (main.c:2617)
 [2020-01-30 20:50 UTC] stas@php.net
Could you please specify what you mean by "exploiting it in the wild"? what exactly is being exploited? I see that the specific code can trigger UAF, but there's no security issue there, it's just a regular crash. Are you saying there's some way to remotely exploit this problem on an arbitrary (or common) PHP code? If so, could you refer to a specific example of that?
 [2020-01-30 21:56 UTC] nikic@php.net
-Block user comment: No +Block user comment: Yes
 [2020-01-31 08:50 UTC] nikic@php.net
-Summary: Reproducible crash in zend_string_alloc +Summary: Use-after-free when accessing already destructed backtrace arguments -Status: Verified +Status: Assigned -Assigned To: +Assigned To: nikic
 [2020-01-31 09:31 UTC] nikic@php.net
Automatic comment on behalf of nikita.ppv@gmail.com
Revision: http://git.php.net/?p=php-src.git;a=commit;h=ef1e4891b47949c8dc0f9482eef9454a0ecdfa1d
Log: Fix bug #76047
 [2020-01-31 09:31 UTC] nikic@php.net
-Status: Assigned +Status: Closed
 
PHP Copyright © 2001-2020 The PHP Group
All rights reserved.
Last updated: Thu Apr 02 23:01:23 2020 UTC