CVE-2016-3078: php: integer overflow in ZipArchive::getFrom*
Apr 28, 2016Details
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.