CVE-2016-5773: Use After Free (CWE-416)
value: php_zip.c in the zip extension in PHP before 5.5.37, 5.6.x before 5.6.23, and 7.x before 7.0.8 improperly interacts with the unserialize implementation and garbage collection, which allows remote attackers to execute arbitrary code or cause a denial of service (use-after-free and application crash) via crafted serialized data containing a ZipArchive object.
CWE description: The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
CVSS v3 base score: 9.8
Published: 2016-08-07T10:59Z
Repository: https://github.com/php/php-src
Language: C
Files changed: ext/standard/tests/strings/bug72434.phpt, ext/zip/php_zip.c
Commit date: 2016-06-20 21:35:22 -0700
Commit message: Fix bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize

Diff: diff --git a/ext/standard/tests/strings/bug72434.phpt b/ext/standard/tests/strings/bug72434.phpt
new file mode 100644
index 00000000000..1408b8f578a
--- /dev/null
+++ b/ext/standard/tests/strings/bug72434.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
+--SKIPIF--
+<?php
+if(!class_exists('zip')) die('ZipArchive');
+?>
+--FILE--
+<?php
+// The following array will be serialized and this representation will be freed later on.
+$free_me = array(new StdClass());
+// Create our payload and unserialize it.
+$serialized_payload = 'a:3:{i:1;N;i:2;O:10:"ZipArchive":1:{s:8:"filename";'.serialize($free_me).'}i:1;R:4;}';
+$unserialized_payload = unserialize($serialized_payload);
+gc_collect_cycles();
+// The reference counter for $free_me is at -1 for PHP 7 right now.
+// Increment the reference counter by 1 -> rc is 0
+$a = $unserialized_payload[1];
+// Increment the reference counter by 1 again -> rc is 1
+$b = $a;
+// Trigger free of $free_me (referenced by $m[1]).
+unset($b);
+$fill_freed_space_1 = "filler_zval_1";
+$fill_freed_space_2 = "filler_zval_2";
+$fill_freed_space_3 = "filler_zval_3";
+$fill_freed_space_4 = "filler_zval_4";
+debug_zval_dump($unserialized_payload[1]);
+?>
+--EXPECTF--
+array(1) refcount(1){
+  [0]=>
+  object(stdClass)#%d (0) refcount(3){
+  }
+}
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index 99c293c6d74..57d060f4ff3 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -1013,10 +1013,18 @@ static int php_zip_has_property(zval *object, zval *member, int type, const zend
 	}
 	return retval;
 }
 /* }}} */
 
+static HashTable *php_zip_get_gc(zval *object, zval ***gc_data, int *gc_data_count TSRMLS_DC) /* {{{ */
+{
+	*gc_data = NULL;
+	*gc_data_count = 0;
+	return zend_std_get_properties(object TSRMLS_CC);
+}
+/* }}} */
+
 static HashTable *php_zip_get_properties(zval *object TSRMLS_DC)/* {{{ */
 {
 	ze_zip_object *obj;
 	zip_prop_handler *hnd;
 	HashTable *props;
@@ -2775,10 +2783,11 @@ static PHP_MINIT_FUNCTION(zip)
 
 	memcpy(&zip_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
 	zip_object_handlers.clone_obj		= NULL;
 	zip_object_handlers.get_property_ptr_ptr = php_zip_get_property_ptr_ptr;
 
+	zip_object_handlers.get_gc          = php_zip_get_gc;
 	zip_object_handlers.get_properties = php_zip_get_properties;
 	zip_object_handlers.read_property	= php_zip_read_property;
 	zip_object_handlers.has_property	= php_zip_has_property;
 
 	INIT_CLASS_ENTRY(ce, "ZipArchive", zip_class_functions);

Fixed code: --TEST--
Bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
--SKIPIF--
<?php
if(!class_exists('zip')) die('ZipArchive');
?>
--FILE--
<?php
// The following array will be serialized and this representation will be freed later on.
$free_me = array(new StdClass());
// Create our payload and unserialize it.
$serialized_payload = 'a:3:{i:1;N;i:2;O:10:"ZipArchive":1:{s:8:"filename";'.serialize($free_me).'}i:1;R:4;}';
$unserialized_payload = unserialize($serialized_payload);
gc_collect_cycles();
// The reference counter for $free_me is at -1 for PHP 7 right now.
// Increment the reference counter by 1 -> rc is 0
$a = $unserialized_payload[1];
// Increment the reference counter by 1 again -> rc is 1
$b = $a;
// Trigger free of $free_me (referenced by $m[1]).
unset($b);
$fill_freed_space_1 = "filler_zval_1";
$fill_freed_space_2 = "filler_zval_2";
$fill_freed_space_3 = "filler_zval_3";
$fill_freed_space_4 = "filler_zval_4";
debug_zval_dump($unserialized_payload[1]);
?>
--EXPECTF--
array(1) refcount(1){
  [0]=>
  object(stdClass)#%d (0) refcount(3){
  }
}
static HashTable *php_zip_get_gc(zval *object, zval ***gc_data, int *gc_data_count TSRMLS_DC) /* {{{ */
{
	*gc_data = NULL;
	*gc_data_count = 0;
	return zend_std_get_properties(object TSRMLS_CC);
}
/* }}} */

	zip_object_handlers.get_gc          = php_zip_get_gc;