php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #71331 Uninitialized pointer in phar_make_dirstream()
Submitted: 2016-01-10 17:51 UTC Modified: 2016-04-28 16:59 UTC
From: manhluat at vnsecurity dot net Assigned: stas
Status: Closed Package: PHAR related
PHP Version: 5.6.17 OS: Linux, Mac
Private report: No CVE-ID: 2016-4343
 [2016-01-10 17:51 UTC] manhluat at vnsecurity dot net
Description:
------------
There is a bug in phar_make_dirstream, when it tries to parse malicious ".tar" file.

The problem starts here:

tar.c

345		if (!last_was_longlink && hdr->typeflag == 'L') {
			last_was_longlink = 1;
			/* support the ././@LongLink system for storing long filenames */
			entry.filename_len = entry.uncompressed_filesize;

You can see that if hdr->typeflag == 'L', it will parse input .tar file as longlink filename and entry.filename_len will be assigned with value of entry.uncompressed_filesize (which is retrieved from .tar file also).

Assume that I can set entry.filename_len=entry.uncompressed_filesize=0.

And a bit later, it will call phar_make_dirstream().

===========================

dirstream.c

static php_stream *phar_make_dirstream(char *dir, HashTable *manifest TSRMLS_DC) /* {{{ */
{
...
char *entry, *found, *save, *str_key;uint keylen;// Uninitialized variables
...

201	while (FAILURE != zend_hash_has_more_elements(manifest)) {
		if (HASH_KEY_NON_EXISTENT == zend_hash_get_current_key_ex(manifest, &str_key, &keylen, &unused, 0, NULL)) {
			break;
		}
..
216 if (keylen >= sizeof(".phar")-1 && !memcmp(str_key, ".phar", sizeof(".phar")-1)) {
...



=======================

zend_hash.c

1068 ZEND_API int zend_hash_get_current_key_ex(const HashTable *ht, char **str_index, uint *str_length, ulong *num_index, zend_bool duplicate, HashPosition *pos)
{
	Bucket *p;

	p = pos ? (*pos) : ht->pInternalPointer;

	IS_CONSISTENT(ht);

	if (p) {
		if (p->nKeyLength) {
			if (duplicate) {
				*str_index = estrndup(p->arKey, p->nKeyLength - 1);
			} else {
				*str_index = (char*)p->arKey;
			}
			if (str_length) {
				*str_length = p->nKeyLength;
			}
			return HASH_KEY_IS_STRING;
		} else {
			*num_index = p->h;
			return HASH_KEY_IS_LONG;
		}
	}
	return HASH_KEY_NON_EXISTENT;
}

As you can see above, if p->nKeyLength is zero (entry.filename_len), it will try to parse current bucket in hashtable as HASH_KEY_IS_LONG. Ok get back to dirstream.c

After zend_hash_get_current_key_ex() is called, str_key/key_len will be exactly the old value if the current_hash is INT value, zend_hash_get_current_key_ex() will just assign the 3rd value as a longint value.

The problem is here...

The 201th line in dirstream.c doesn't check whether return type of zend_hash_get_current_key_ex() is STRING or INT. (as you see, it just check if it is HASH_KEY_NON_EXISTENT or not). And use it as string pointer in further (216th line).

So attacker,by somehow, he could control the stack frame at the moment before calling zend_hash_get_current_key_ex() then he could take over these variables (str_key/key_len) and it could lead to some evil scenario.

PoC works with PHP latest version (php-5.6.17) in Linux and Mac as well.

PoC can be downloaded at http://l4w.io/files/PHP-PoC/93aa2719f76a916b118fa77c62981f5f/PoC/

* How to fix:
I think we should check return type of zend_hash_get_current_key_ex() in 
phar_make_dirstream() is whether STRING or LONGINT.
And check entry.filename_len>0 when parsing longlink filename also.

Test script:
---------------
<?php
new PharData($argv[1]);
?>

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

Actual result:
--------------
gdb-peda$ r ./PoC/poc.php ./PoC/000.tar 
Starting program: /root/test/php-5.6.17/sapi/cli/php ./PoC/poc.php ./PoC/000.tar

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x16 
EBX: 0x8804000 --> 0x8803dd8 --> 0x1 
ECX: 0x5 
EDX: 0x86fbc93 (".phar")
ESI: 0xf7bd9254 --> 0x8 
EDI: 0xf7d5a75f (<__memcpy_ssse3_rep+47>:       add    ebx,0x37561)
EBP: 0xf7bd94d8 --> 0xf7bd002f --> 0x0 
ESP: 0xffff9c78 --> 0x8804000 --> 0x8803dd8 --> 0x1 
EIP: 0xf7d68fd0 (<__memcmp_sse4_2+64>:  mov    bl,BYTE PTR [eax])
EFLAGS: 0x10293 (CARRY parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xf7d68fc7 <__memcmp_sse4_2+55>:     add    ebx,DWORD PTR [ebx+ecx*4]
   0xf7d68fca <__memcmp_sse4_2+58>:     jmp    ebx
   0xf7d68fcc <__memcmp_sse4_2+60>:     lea    esi,[esi+eiz*1+0x0]
=> 0xf7d68fd0 <__memcmp_sse4_2+64>:     mov    bl,BYTE PTR [eax]
   0xf7d68fd2 <__memcmp_sse4_2+66>:     cmp    bl,BYTE PTR [edx]
   0xf7d68fd4 <__memcmp_sse4_2+68>:     jne    0xf7d6901f <__memcmp_sse4_2+143>
   0xf7d68fd6 <__memcmp_sse4_2+70>:     mov    bl,BYTE PTR [eax+0x1]
   0xf7d68fd9 <__memcmp_sse4_2+73>:     cmp    bl,BYTE PTR [edx+0x1]
[------------------------------------stack-------------------------------------]
0000| 0xffff9c78 --> 0x8804000 --> 0x8803dd8 --> 0x1 
0004| 0xffff9c7c --> 0x8169a4c (<phar_make_dirstream+988>:      mov    ecx,DWORD PTR [esp+0x30])
0008| 0xffff9c80 --> 0x16 
0012| 0xffff9c84 --> 0x86fbc93 (".phar")
0016| 0xffff9c88 --> 0x5 
0020| 0xffff9c8c --> 0xffff9cc8 --> 0x1505 
0024| 0xffff9c90 --> 0x0 
0028| 0xffff9c94 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
__memcmp_sse4_2 () at ../sysdeps/i386/i686/multiarch/memcmp-sse4.S:114
114     ../sysdeps/i386/i686/multiarch/memcmp-sse4.S: No such file or directory.
gdb-peda$ bt
#0  __memcmp_sse4_2 () at ../sysdeps/i386/i686/multiarch/memcmp-sse4.S:114
#1  0x08169a4c in phar_make_dirstream (dir=0xf7bd94d8 "/", manifest=0xf7bd9254) at /root/test/php-5.6.17/ext/phar/dirstream.c:216
#2  0x08169e14 in phar_wrapper_open_dir (wrapper=0x8805220 <php_stream_phar_wrapper>, path=0xf7bd9448 "phar:///root/test/PoC/000.tar/", mode=0x87241b8 "r", options=0x0, opened_path=0x0, context=0x0)
    at /root/test/php-5.6.17/ext/phar/dirstream.c:357
#3  0x08268dae in _php_stream_opendir (path=path@entry=0xf7bd9448 "phar:///root/test/PoC/000.tar/", options=options@entry=0x8, context=0x0) at /root/test/php-5.6.17/main/streams/streams.c:1986
#4  0x081bdf88 in spl_filesystem_dir_open (intern=intern@entry=0xf7bd9f58, path=0xf7bd9448 "phar:///root/test/PoC/000.tar/") at /root/test/php-5.6.17/ext/spl/spl_directory.c:250
#5  0x081c18af in spl_filesystem_object_construct (ht=ht@entry=0x2, return_value=return_value@entry=0xf7bd8a30, return_value_ptr=return_value_ptr@entry=0xffff9f4c, this_ptr=this_ptr@entry=0xf7bd8a68, 
    return_value_used=return_value_used@entry=0x1, ctor_flags=ctor_flags@entry=0x1) at /root/test/php-5.6.17/ext/spl/spl_directory.c:731
#6  0x081c1977 in zim_spl_RecursiveDirectoryIterator___construct (ht=0x2, return_value=0xf7bd8a30, return_value_ptr=0xffff9f4c, this_ptr=0xf7bd8a68, return_value_used=0x1) at /root/test/php-5.6.17/ext/spl/spl_directory.c:1597
#7  0x082a3fc1 in zend_call_function (fci=fci@entry=0xffff9f7c, fci_cache=fci_cache@entry=0xffff9f68) at /root/test/php-5.6.17/Zend/zend_execute_API.c:847
#8  0x082cc101 in zend_call_method (object_pp=object_pp@entry=0xffffa02c, obj_ce=<optimized out>, fn_proxy=fn_proxy@entry=0x88a3c84, function_name=function_name@entry=0x836d76a "__construct", 
    function_name_len=function_name_len@entry=0xb, retval_ptr_ptr=retval_ptr_ptr@entry=0x0, param_count=param_count@entry=0x2, arg1=arg1@entry=0xffffa030, arg2=arg2@entry=0xffffa040)
    at /root/test/php-5.6.17/Zend/zend_interfaces.c:97
#9  0x081754e1 in zim_Phar___construct (ht=0x1, return_value=0xf7bd8a4c, return_value_ptr=0xf7bbf088, this_ptr=0xf7bd8a68, return_value_used=0x0) at /root/test/php-5.6.17/ext/phar/phar_object.c:1255
#10 0x083615c0 in zend_do_fcall_common_helper_SPEC (execute_data=<optimized out>) at /root/test/php-5.6.17/Zend/zend_vm_execute.h:558
#11 0x082f1446 in execute_ex (execute_data=execute_data@entry=0xf7bbf0c4) at /root/test/php-5.6.17/Zend/zend_vm_execute.h:363
#12 0x0835f272 in zend_execute (op_array=0xf7bd8f58) at /root/test/php-5.6.17/Zend/zend_vm_execute.h:388
#13 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
#14 0x0824ef3e in php_execute_script (primary_file=primary_file@entry=0xffffc438) at /root/test/php-5.6.17/main/main.c:2597
#15 0x08363473 in do_cli (argc=argc@entry=0x3, argv=argv@entry=0x8820878) at /root/test/php-5.6.17/sapi/cli/php_cli.c:994
#16 0x08063f04 in main (argc=0x3, argv=0x8820878) at /root/test/php-5.6.17/sapi/cli/php_cli.c:1378
#17 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
#18 0x08063f8a in _start ()

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2016-01-15 07:00 UTC] stas@php.net
-Assigned To: +Assigned To: stas
 [2016-01-15 07:00 UTC] stas@php.net
Fix in https://gist.github.com/smalyshev/853621a4627e19e57f2c and security repo commit 4c2424eb24b0178456acc404dbfff528cdc44197
 [2016-02-02 03:57 UTC] stas@php.net
-Status: Assigned +Status: Closed
 [2016-02-02 03:57 UTC] stas@php.net
The fix for this bug has been committed.

Snapshots of the sources are packaged every three hours; this change
will be in the next snapshot. You can grab the snapshot at
http://snaps.php.net/.

 For Windows:

http://windows.php.net/snapshots/
 
Thank you for the report, and for helping us make PHP better.


 [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=4c2424eb24b0178456acc404dbfff528cdc44197
Log: Fixed bug #71331 - Uninitialized pointer in phar_make_dirstream()
 [2016-02-05 04:36 UTC] manhluat at vnsecurity dot net
Looks like i cant see this on the latest patches :D
 [2016-02-05 05:00 UTC] stas@php.net
Please explain - what don't you see in latest patches?
 [2016-02-07 05:47 UTC] manhluat at vnsecurity dot net
Ah, I'm sorry. I just didnt see it on http://php.net/ChangeLog-5.php
But the patches still exists in commit.
 [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-4343
 [2016-05-02 14:51 UTC] jpauli@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=9649ca1630433473a307d015ba1a79a4a7a779f5
Log: Fixed bug #71331 - Uninitialized pointer in phar_make_dirstream()
 [2016-05-25 00:21 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=9649ca1630433473a307d015ba1a79a4a7a779f5
Log: Fixed bug #71331 - Uninitialized pointer in phar_make_dirstream()
 [2016-05-25 03:51 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=9649ca1630433473a307d015ba1a79a4a7a779f5
Log: Fixed bug #71331 - Uninitialized pointer in phar_make_dirstream()
 [2016-05-25 03:52 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=9649ca1630433473a307d015ba1a79a4a7a779f5
Log: Fixed bug #71331 - Uninitialized pointer in phar_make_dirstream()
 [2016-05-25 03:53 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=9649ca1630433473a307d015ba1a79a4a7a779f5
Log: Fixed bug #71331 - Uninitialized pointer in phar_make_dirstream()
 
PHP Copyright © 2001-2017 The PHP Group
All rights reserved.
Last updated: Tue Aug 29 15:01:52 2017 UTC