How Serialized Cookies Led to RCE on a WordPress Website

May 9, 2024 Haoxi Tan

In this article, we'll talk about a critical bug report where a hacker found a Remote Code Execution (RCE) on Nextcloud's WordPress website in the source code of its custom theme. It spins a cautionary tale of using unsafe deserialization in PHP and tells a success story of how bugs are frequently found in live web targets when the source code is also available.

What Is Insecure Deserialization?

Web applications often need to pass around structured information for use as variables in the backend server, such as form and user data. It's convenient to serialize them in formats that the backend programming language can easily use, but it can often be dangerous if that data is controlled by user input. This is also known as insecure deserialization.

Insecure deserialization occurs when user-controlled serialized data is loaded in an unsafe manner, which can lead to RCE and an attacker gaining full access to the web application by running code on the server. This means the integrity, confidentiality, and availability of the vulnerable web service and associated data (including potentially sensitive customer information) are all at risk. Testing and remediating this class of bugs takes hackers with a good understanding of the application's data model, technology stack, and sometimes even access to source code. 

Business Impact of Remote Code Execution

  • Data Breach: When attackers gain access to your organization's web servers via RCE, they can access data with the same context and permission of the web application. This means access to things like sensitive customer credentials, PII (Personally Identifiable Information), and API keys for crucial third-party services such as cloud providers and payment gateways. This can lead to financial loss, reputational damage, and legal troubles from regulatory non-compliance.
  • Service Disruption: Attackers can also utilize access gained from RCE to disrupt the availability of your website. This can take the form of a shutdown or even defacement. Depending on the criticality of the web application to your business, this can cause serious financial loss and reputational damage.
  • Harm to Customers: Popular attacks such as web skimming often utilize RCE vulnerabilities in WordPress to deploy skimmers that are designed to steal payment information such as customer credit cards and personal details. Being compromised via RCE means attackers can inject arbitrary scripts and other malware that visitors execute, causing harm to your own customers.

Details: The Bug Report

RCE Wordpress bug

In this particular case, the bug was an RCE found on Nextcloud's website via a custom theme they had developed. The hacker, Lukas Reschke, wrote a very concise report explaining that the cause is unserializing user input from cookies and provided the exact line of vulnerable code in a GitHub commit permalink. Let's have a look:

function nc_change_nf_default_value( $default_value, $field_type, $field_settings ) {
     $nc_form_fields = unserialize(base64_decode($_COOKIE['nc_form_fields']));
      if( str_contains($field_settings['key'], 'name') && !str_contains($field_settings['key'], 'organization') ){
             if(isset($nc_form_fields['nc_form_name'])) {
                 $default_value = $nc_form_fields['nc_form_name'];

It looks like this function does something with form fields, but the form fields are controlled by user input in a cookie called nc_form_fields and Lukas accessed the fields inside of it after deserialization via a call to unserialize.

Lukas also mentioned a separate location in the large code base of nextcloud-theme that also contained the same vulnerability, hinting to us that he found it by looking for the keyword unserialize in the entire codebase: 

$pref_lang = '';
$nc_form_fields = unserialize(base64_decode($_COOKIE['nc_form_fields']));
if( isset($nc_form_fields['nc_form_lang'])){
     $pref_lang = $nc_form_fields['nc_form_lang'];
} else {
$pref_lang = $browser_lang;

Let's have a look at the PHP documentation for the unserialize function:


It creates a PHP value from a stored representation. It's a convenient way to store and retrieve values and pass them between different functions and pages (e.g., serve users a form, have them fill it in, and send it back). The documentation also states in a red banner to not pass untrusted user input into unserialize(), since a hacker can load code and execute it during object instantiation.

The Exploit

And load code for execution is exactly what Lucas did. He tested some payloads the gadget chain with Monolog worked, since it was installed in this instance of WordPress. Gadgets are a Return Oriented Programming (ROP) concept where functions belonging to the running code and loaded libraries are used to achieve an intended effect. Let's have a look at his payload:

Monolog payload

This is the base64 decoded value of what Lukas put in nc_form_fields:


Reading this decoded payload revealed the internal PHP structure of serialized variables in the form of ::. For example, it starts with O:37:"Monolog\Handler\FingersCrossedHandler" which is an Object (O), denoted by a value 37 bytes long, being "Monolog\Handler\FingersCrossedHandler". Other types such as string (s) and integer (i) can be seen throughout the structure. Following that logic we can pretty much tell that this payload calls system somewhere with id as a parameter, as the hacker described in his submission:


To generate PHP deserialization payloads, a popular tool is phpggc. A quick search of that repo revealed multiple RCE chains using Monolog:

Monolog code

In fact, running phpggc with the Monolog/rce7 option generates a payload that looks exactly like the one Lukas had:

git clone
cd phpggc
docker build -t phpggc .
docker run phpggc Monolog/rce7 'system' 'id'


Being able to be abused as an ROP gadget is not a vulnerability in Monolog in this case, as if someone could load arbitrary classes via unserialize, they could use a lot of other ways to execute code. In this case, the Monolog class FingersCrossedHandler constructor included a parameter, $handler, that allows the caller to set an arbitrary function to be called, for example system that executes commands.

Fingerscrossed handler

Even if Monolog wasn't installed, plenty of gadgets could still be used for RCE via insecure deserialization on a WordPress website. There are gadgets in WordPress itself:

phpggc wordpress

How Hackers Find PHP Insecure Deserialization Bugs in Source Code

To start similar bugs in scope, we as hackers can start with searching programs with "Source Code" targets in scope:

source code opportunity discovery

Then, we can filter by the targets that have PHP as a technology:

PHP as technology

Then, look through the scopes of the programs and download all the source code. Now you can grep through all of them with this command using ripgrep:

rg -F 'unserialize(' .

Now look through the results and find user inputs. To speed up the process of finding user input that's passed to unserialize, you can add keywords such as $_COOKIE, $_GET, $_POST and $_REQUEST, which are PHP special variables that may contain untrust user input from requests:

rg 'unserialize.*(COOKIE|GET|POST|REQUEST).*' .

We can make a test.php file with some vulnerable code to validate that our regex is correct:

    echo unserialize($_GET['id']);

$ rg 'unserialize.*(COOKIE|GET|POST|REQUEST).*' .
2:    echo unserialize($_GET['id']);

Finding PHP Serialized Cookies Dynamically

To find PHP serialized cookies in HTTP history, we can use Burp with its new feature called "Bambas" to filter HTTP traffic by Java code. Since the entire request and response body is exposed to us, we can loop through each cookie and check if it contains potentially PHP serialized data.

To do this, go to Burp's Proxy -> HTTP history and click on Filter settings:

HTTP proxy filter

Now click on "Bambda mode", and paste in this Bambda snippet, then click Apply:

if (requestResponse.finalRequest().hasHeader("Cookie")) {
   Pattern PHPOBJ_PATTERN = Pattern.compile("^(.*[a-zA-Z]:[0-9]{0,5}:\".*)$");
for (String kv : requestResponse.finalRequest().headerValue("Cookie").split("; ")) {

     String cookieValue = kv.split("=")[1];
     try {
           // check for base64 encoded PHP serialized object
           String decoded = new String(Base64.getUrlDecoder().decode(cookieValue));
           if (PHPOBJ_PATTERN.matcher(decoded).matches()) {
             return true;
       } catch(IllegalArgumentException e) {
           // check for plain PHP object
           if (PHPOBJ_PATTERN.matcher(cookieValue).matches()) {
      return true;
     // END OF LOOP
return false;

Essentially, it loops through each cookie value in the request header and checks for plain text or base64 encoded PHP serialized object patterns using a regular expression. Note that there will be false positives since the regex uses a simple pattern to capture a wide range of PHP serialized payloads, of which there are many variations.

nextcloud request

And just like that, we've found three requests that contain nc_form_fields with suspicious-looking base64 encoded cookie values that we can test.


To avoid RCE caused by insecure deserialization, developers can replace all instances of insecure functions, such as unserialize, with safer encoding techniques that are stricter on what the objects can contain. This is the patch that Nextcloud promptly came up with for this particular vulnerability, just a few days after the bug report: 

Nextcloud patch

The developers replaced the unsafe function unserialize with json_decode, which cannot contain arbitrary code that could be loaded for RCE. We believe that the availability of source code that allowed Lucas to find this bug also greatly improved the ability for the developers to respond with a patch, since it enabled the hacker to highlight exactly where every instance of this bug was in the code, guiding Nextcloud's patching efforts.


A single function call to unserialize opened up the whole website to RCE, with the potential for an attacker to gain a shell on the web server. Finding this class of vulnerability in PHP isn't hard across code bases in scope, and neither is creating a working payload for it; in fact, it highlights that the hacker, Lucas, had a great approach to finding bugs in source code assets, not just in PHP but for any language. It begins with grepping for dangerous function calls, taking user-controlled input, then validating the vulnerability against a live target, and finally demonstrating security impact by executing a harmless command on the server. 

Secure Your Organization From Insecure Deserialization With HackerOne

This is only one example of the dangerous impact of an insecure deserialization vulnerability, and how easy it was for an attacker to exploit. HackerOne and our community of ethical hackers are best equipped to help organizations identify and remediate insecure deserialization and other vulnerabilities, whether through bug bountyPentest as a Service (PTaaS)Code Security Audit, or other solutions by considering the attacker's mindset on discovering a vulnerability.

Download the 7th Annual Hacker Powered Security Report to learn more about the impact of the top 10 HackerOne vulnerabilities, or contact HackerOne to get started taking on insecure deserialization vulnerabilities at your organization.

Previous Article
A Guide to Get the Most Out of Your One-on-ones
A Guide to Get the Most Out of Your One-on-ones

Before we dive into the tips and strategies for different types of 1:1s (e.g. 1:1s with your manager, your ...

Next Article
ISO 27001 and Pentesting: What You Need to Know
ISO 27001 and Pentesting: What You Need to Know

Today, most organizations have some level of information security, but often it consists of point solutions...