JWT security testing: alg:none, algorithm confusion και αδύναμα secrets

Τα JSON Web Tokens είναι παντού στα σύγχρονα APIs: ένα υπογεγραμμένο token που ο client κουβαλά σε κάθε αίτημα, και ο server το εμπιστεύεται χωρίς να κρατά session state. Ακριβώς επειδή ο server «εμπιστεύεται την υπογραφή», τα λάθη στην επαλήθευσή της είναι κρίσιμα: ένα κακώς ελεγμένο JWT σημαίνει πλαστοπροσωπία οποιουδήποτε χρήστη. Ακολουθεί η μεθοδολογία ελέγχου, πάντα σε συστήματα με γραπτή εξουσιοδότηση και δοκιμαστικούς λογαριασμούς.

Η ανατομία ενός JWT

Τρία μέρη χωρισμένα με τελείες, κωδικοποιημένα σε Base64URL: header (ο αλγόριθμος, π.χ. {“alg”:“HS256”,“typ”:“JWT”}), payload (τα claims: ποιος είναι ο χρήστης, πότε λήγει, τι ρόλο έχει) και signature. Σημαντικό: τα δύο πρώτα μέρη είναι απλώς κωδικοποιημένα, όχι κρυπτογραφημένα. Οποιοσδήποτε τα διαβάζει. Η ασφάλεια στηρίζεται αποκλειστικά στην υπογραφή.

Δύο οικογένειες αλγορίθμων: συμμετρική (HS256, ένα κοινό μυστικό που υπογράφει και επαληθεύει) και ασύμμετρη (RS256, ιδιωτικό κλειδί υπογράφει, δημόσιο επαληθεύει).

Οι κλασικές επιθέσεις

  • alg: none. Μερικές παλιές ή κακώς ρυθμισμένες βιβλιοθήκες δέχονται token με “alg”:“none” και κενή υπογραφή. Αλλάζετε τα claims, βάζετε none, αφαιρείτε την υπογραφή, και αν ο server το δεχτεί, έχετε πλήρη πλαστοπροσωπία.

  • Algorithm confusion (RS256 προς HS256). Αν ο server περιμένει RS256 αλλά δεν επιβάλλει τον αλγόριθμο, μπορεί κανείς να φτιάξει ένα token HS256 και να το υπογράψει χρησιμοποιώντας το δημόσιο κλειδί του server ως κοινό μυστικό. Ο server, που επαληθεύει με το ίδιο δημόσιο κλειδί, το δέχεται. Το δημόσιο κλειδί συχνά είναι όντως δημόσιο: σε ένα endpoint JWKS, σε ένα πιστοποιητικό, στο documentation.

  • Αδύναμο secret σε HS256. Αν το κοινό μυστικό είναι μια αδύναμη λέξη, σπάει offline. Εργαλεία όπως το hashcat (mode 16500) δοκιμάζουν εκατομμύρια secrets το δευτερόλεπτο πάνω σε ένα μόνο υποκλαπέν token, χωρίς κανένα αίτημα στον server.

  • Παραποίηση του `kid`. Το πεδίο kid (key ID) δείχνει ποιο κλειδί να χρησιμοποιηθεί. Αν περνά ανεξέλεγκτο σε query ή path, μπορεί να οδηγήσει σε SQL injection, path traversal (π.χ. kid που δείχνει σε /dev/null, οπότε το secret γίνεται κενό), ή command injection.

  • Ενσωματωμένο ή απομακρυσμένο κλειδί (`jwk`, `jku`, `x5u`). Τρία πεδία του header μπορούν να υποδείξουν το κλειδί επαλήθευσης. Το jwk ενσωματώνει δημόσιο κλειδί μέσα στο ίδιο το token (CVE-2018-0114): φτιάχνετε δικό σας ζεύγος κλειδιών, βάζετε το δημόσιό σας στο jwk, υπογράφετε με το ιδιωτικό σας, και αν ο server το εμπιστευτεί, περνά. Τα jku (JWK Set URL) και x5u (X.509 URL) δείχνουν σε απομακρυσμένο JWKS: αν ο server τα ακολουθεί χωρίς έλεγχο, τα στρέφετε σε δικό σας server, ή τα χρησιμοποιείτε για SSRF.

  • Αγνοημένο expiry. Αν ο server δεν ελέγχει το claim exp, ένα υποκλαπέν token ισχύει για πάντα.

Η ροή του ελέγχου

Αποκτήστε ένα έγκυρο token ως χαμηλόβαθμος χρήστης. Στο Burp, το extension JWT Editor (ή το jwt_tool στη γραμμή εντολών) αποκωδικοποιεί, τροποποιεί και υπογράφει ξανά tokens. Δοκιμάστε με τη σειρά:

  1. Αλλάξτε ένα claim χωρίς να αγγίξετε την υπογραφή (π.χ. “role”:“admin”). Αν γίνει δεκτό, η υπογραφή δεν ελέγχεται καθόλου. Κρίσιμο εύρημα.

  2. Δοκιμάστε `alg:none` σε όλες τις παραλλαγές (None, NONE, nOnE: ορισμένοι έλεγχοι είναι case-sensitive).

  3. Αν ο server χρησιμοποιεί RS256, βρείτε το δημόσιο κλειδί (δοκιμάστε /.well-known/jwks.json) και επιχειρήστε algorithm confusion προς HS256.

  4. Αν χρησιμοποιεί HS256, δοκιμάστε offline brute force του secret με μια λίστα συνηθισμένων τιμών.

  5. Πειράξτε τα πεδία κλειδιού του header. Δοκιμάστε injection στο kid (SQL, path traversal, /dev/null), ενσωμάτωση δικού σας κλειδιού με jwk (-X i στο jwt_tool), και παραπομπή των jku/x5u σε δικό σας JWKS (-X s). Το JWT Editor του Burp τα αυτοματοποιεί (Attack → Embedded JWK, jku/x5u payloads).

  6. Ελέγξτε το `exp`: στείλτε ένα ληγμένο token και δείτε αν γίνεται δεκτό.

Προσοχή στα ψευδώς θετικά: μια απάντηση 200 δεν σημαίνει πάντα επιτυχία. Επιβεβαιώστε ότι όντως εκτελέσατε ενέργεια ως άλλος χρήστης.

Οι σωστές άμυνες

  • Επιβάλλετε έναν συγκεκριμένο αλγόριθμο στην επαλήθευση· μη δέχεστε τον αλγόριθμο από το header του token.

  • Ποτέ `none` σε production.

  • Ισχυρά, τυχαία secrets για HS256 (τουλάχιστον 256 bit), ή ασύμμετρα κλειδιά.

  • Ελέγχετε πάντα `exp`, και ιδανικά iss και aud.

  • Σύντομη διάρκεια ζωής και μηχανισμός ανάκλησης (refresh tokens, denylist), γιατί ένα JWT δεν «ακυρώνεται» από μόνο του.

  • Αγνοήστε ή ελέγξτε αυστηρά τα `kid`/`jku`/`x5u`: allowlist για το kid, και μόνο γνωστά, προκαθορισμένα URLs για jku/x5u.

  • Χρησιμοποιήστε συντηρημένη βιβλιοθήκη που δεν έχει τα παραπάνω ελαττώματα· μην υλοποιείτε μόνοι σας την επαλήθευση.

Για συζήτηση

  1. Ποια εργαλεία χρησιμοποιείτε για JWT testing, το JWT Editor του Burp, το jwt_tool, ή κάτι άλλο;

  2. Έχετε συναντήσει algorithm confusion σε πραγματικό engagement; Πόσο εύκολο ήταν να βρείτε το δημόσιο κλειδί;

  3. Προτιμάτε σύντομης διάρκειας JWTs με refresh tokens, ή επιστροφή σε server-side sessions; Πού βάζετε το όριο;