Πώς ένα φαινομενικά αθώο «serialized» αντικείμενο — ένα base64 session cookie, μια παράμετρος, ένα binary blob — μετατρέπεται σε πλήρη απομακρυσμένη εκτέλεση κώδικα (Remote Code Execution) στον server, βήμα προς βήμα, σε ένα απομονωμένο εργαστήριο που κατέχουμε εξ ολοκλήρου. Δείχνουμε δύο πραγματικές, εκτελέσιμες περιπτώσεις: (1) μια Flask εφαρμογή που κάνει pickle.loads() ένα cookie χωρίς κανέναν έλεγχο, όπου ένα gadget με __reduce__ επιστρέφει subprocess.check_output και μας δίνει RCE και το flag· (2) ένα Java sink που καλεί ObjectInputStream.readObject() πάνω σε δικό μας Serializable gadget, με πραγματική εκτέλεση κώδικα κατά την αποσειριοποίηση. Δείχνουμε τα ωμά serialized bytes, το base64 payload, τα pickle opcodes και την ακριβή απόδειξη εκτέλεσης στον server. Για το ysoserial δηλώνουμε με απόλυτη ειλικρίνεια τι τρέξαμε και τι όχι, και εξηγούμε τις gadget chains. Κλείνουμε με ανίχνευση, αντιστοίχιση σε MITRE ATT&CK και μια σκληρή ενότητα άμυνας για κάθε κρίκο.

TL;DR (EN): An insecure-deserialization-to-RCE lab: a Flask app that pickle.loads() a session cookie is exploited with a __reduce__ gadget for real code execution, and a Java ObjectInputStream sink is popped via a readObject gadget (ysoserial explained). Covers the raw payload bytes, detection, and why you must never deserialize untrusted data.

Νομική σημείωση / Authorization. Όλη η δραστηριότητα που περιγράφεται εκτελέστηκε αποκλειστικά σε ένα απομονωμένο εργαστήριο που κατέχουμε εξ ολοκλήρου. Και οι δύο ευάλωτοι στόχοι — η Flask εφαρμογή «Nostos Portal» και το Java sink — είναι δικά μας τοπικά stand-ins, δεσμευμένα σε loopback (127.0.0.1) χωρίς καμία δημοσιευμένη θύρα, χωρίς έκθεση προς LAN ή Internet. Οι «επιθέσεις» εκτελούν αβλαβείς εντολές απόδειξης (id, hostname, whoami, ανάγνωση ενός τοπικού lab flag) μέσα στη δική μας διεργασία. Δεν προσεγγίσαμε, δεν σαρώσαμε και δεν επιτεθήκαμε σε κανένα πραγματικό σύστημα τρίτου. Η ταυτότητα του χειριστή έχει αποστειρωθεί σε kali@lab. Οι τεχνικές αποσειριοποίησης είναι παράνομες όταν εφαρμόζονται χωρίς ρητή γραπτή εξουσιοδότηση. Χρησιμοποιήστε τις μόνο σε δικά σας ή ρητά εξουσιοδοτημένα συστήματα, στο πλαίσιο νόμιμου penetration testing.

Εισαγωγή: γιατί το insecure deserialization είναι από τις πιο υποτιμημένες κρίσιμες αδυναμίες

Σχεδόν κάθε μη τετριμμένη εφαρμογή χρειάζεται, κάποια στιγμή, να μετατρέψει ένα αντικείμενο σε bytes για να το αποθηκεύσει ή να το στείλει, και μετά να το ανακατασκευάσει στην άλλη άκρη. Αυτή η διαδικασία λέγεται serialization (σειριοποίηση) και deserialization (αποσειριοποίηση). Είναι παντού: σε session cookies, σε caches, σε message queues, σε RPC frameworks, σε αρχεία κατάστασης, σε APIs που ανταλλάσσουν «rich» αντικείμενα αντί για απλά δεδομένα.

Το πρόβλημα γεννιέται όταν η εφαρμογή αποσειριοποιεί δεδομένα που προέρχονται από μη έμπιστη πηγή — δηλαδή από κάτι που ελέγχει ο χρήστης — χρησιμοποιώντας έναν μηχανισμό που κατά τη διάρκεια της ανακατασκευής μπορεί να εκτελέσει κώδικα. Τότε το «διαβάζω ένα αντικείμενο» γίνεται «εκτελώ ό,τι μου στείλει ο επιτιθέμενος». Δεν χρειάζεται buffer overflow, δεν χρειάζεται SQL injection: αρκεί ένα προσεκτικά κατασκευασμένο serialized payload.

Το insecure deserialization βρίσκεται σταθερά στο OWASP Top 10 (ως μέρος του A08:2021 – Software and Data Integrity Failures) ακριβώς επειδή ο αντίκτυπος είναι, στην καλή περίπτωση, RCE. Είναι επίσης από τις πιο παρεξηγημένες κατηγορίες: πολλοί developers θεωρούν ότι «είναι απλώς δεδομένα», χωρίς να συνειδητοποιούν ότι formats όπως το Python pickle ή η native Java serialization είναι, ουσιαστικά, μικρές γλώσσες προγραμματισμού που περιγράφουν πώς να κατασκευαστεί ένα αντικείμενο — και αυτό το «πώς» μπορεί να περιλαμβάνει κλήσεις συναρτήσεων.

Για έναν οργανισμό που υπόκειται σε NIS2 ή DORA, το σενάριο δεν είναι θεωρητικό. Μια μόνο αδυναμία αποσειριοποίησης σε ένα δευτερεύον endpoint αρκεί για πλήρη κατάληψη του application server, με ό,τι αυτό συνεπάγεται για την εμπιστευτικότητα και ακεραιότητα δεδομένων — ακριβώς το είδος της «κρίσιμης» έκθεσης που οι κανονισμοί απαιτούν να εντοπίζετε και να τεκμηριώνετε. Οι απαιτήσεις NIS2 & DORA για διαχείριση ICT risk και για απόδειξη ανθεκτικότητας κάνουν αυτήν την κατηγορία αδυναμίας ιδιαίτερα «ακριβή» όταν βρεθεί σε production.

Σε αυτό το flagship άρθρο των Audax Labs στήνουμε ολόκληρη την αλυσίδα «από το serialized input στο RCE» δύο φορές — μία σε Python και μία σε Java — με πραγματικά εργαλεία και πραγματικά outputs, και το κλείνουμε με το πιο σημαντικό κομμάτι: πώς σταματάτε κάθε βήμα.


Τι είναι το insecure deserialization και γιατί καταλήγει σε RCE

Ας ξεκαθαρίσουμε τους όρους. Serialization είναι η μετατροπή ενός αντικειμένου (π.χ. ενός User με πεδία username, role) σε μια ακολουθία bytes. Deserialization είναι η αντίστροφη διαδικασία: παίρνεις τα bytes και ξαναφτιάχνεις το αντικείμενο στη μνήμη.

Το κρίσιμο σημείο είναι ότι ορισμένοι μηχανισμοί σειριοποίησης δεν αποθηκεύουν μόνο δεδομένα. Αποθηκεύουν οδηγίες ανακατασκευής. Στο Python pickle, το serialized stream είναι μια σειρά από opcodes για μια εικονική μηχανή στοίβας: «φόρτωσε αυτό το global», «κάλεσε αυτό με αυτά τα ορίσματα», «βάλε το αποτέλεσμα στη στοίβα». Ένα από αυτά τα opcodes, το REDUCE, καλεί ένα callable με ορίσματα. Αν ο επιτιθέμενος ελέγχει το stream, ελέγχει ποιο callable θα κληθεί και με ποια ορίσματα — π.χ. subprocess.check_output(['sh','-c','...']).

Στην Java, η native serialization καλεί αυτόματα μια «μαγική» μέθοδο, την readObject(), σε κάθε κλάση που την ορίζει, κατά τη διάρκεια της αποσειριοποίησης. Αν μια κλάση στο classpath έχει readObject() (ή μια αλυσίδα μεθόδων που πυροδοτούνται από αυτήν) που καταλήγει σε κάτι σαν Runtime.exec(), τότε το απλό readObject() γίνεται RCE. Το εργαλείο ysoserial αυτοματοποιεί ακριβώς την εύρεση τέτοιων αλυσίδων μέσα σε δημοφιλείς βιβλιοθήκες.

Η ουσία, και στις δύο γλώσσες, είναι η ίδια: η αποσειριοποίηση μη έμπιστων δεδομένων δεν είναι “ανάγνωση”· είναι “εκτέλεση”. Και ο κανόνας που προκύπτει είναι εξίσου απλός: ποτέ μην αποσειριοποιείτε δεδομένα που δεν εμπιστεύεστε με έναν μηχανισμό που μπορεί να εκτελέσει κώδικα.


Το σενάριο και το απομονωμένο εργαστήριο

Το εργαστήριο τρέχει εξ ολοκλήρου σε έναν host, με τρία λογικά κομμάτια, όλα σε loopback και χωρίς καμία δημοσιευμένη θύρα:

  • Nostos Portal (ευάλωτη web εφαρμογή, Python): Flask, δεσμευμένη στο 127.0.0.1:8891. Υλοποιεί το κλασικό anti-pattern «remember me»: το session αποθηκεύεται client-side ως base64-encoded pickle του αντικειμένου User, και αποσειριοποιείται με pickle.loads() σε κάθε αίτημα στο /dashboard, χωρίς καμία επικύρωση ή υπογραφή. Αυτό είναι το insecure-deserialization sink.
  • Java sink (ευάλωτο, Java): ένα μικρό πρόγραμμα DeserSink που δέχεται ένα base64 serialized αντικείμενο και καλεί ObjectInputStream.readObject() χωρίς ObjectInputFilter (χωρίς allowlist). Το ίδιο anti-pattern, στην πλατφόρμα της Java.
  • Attacker: ο ίδιος host, με ταυτότητα χειριστή αποστειρωμένη σε kali@lab. Εργαλεία: ένα δικό μας exploit_pickle.py που φτιάχνει το pickle gadget, ένας Java MakePayload που φτιάχνει το serialized blob, και το curl για την παράδοση.
Τοπολογία απομονωμένου εργαστηρίου
Τοπολογία απομονωμένου εργαστηρίου

Οι εκδόσεις των εργαλείων ήταν: Python 3.12 με Flask 3.1, OpenJDK 21 (φορητό JDK, εγκατεστημένο τοπικά μέσα στον φάκελο του lab, χωρίς εγκατάσταση σε system paths και χωρίς root), curl 8.5.0. Και τα δύο ευάλωτα services και ο «επιτιθέμενος» τρέχουν στο ίδιο μηχάνημα· καμία γέφυρα προς κανένα άλλο δίκτυο.


Η αλυσίδα με μια ματιά

Πριν μπούμε στις λεπτομέρειες, ας δούμε ολόκληρη τη λογική σε ένα διάγραμμα. Το μοτίβο είναι ταυτόσημο και στις δύο περιπτώσεις — αλλάζει μόνο η γλώσσα και το όνομα του «gadget».

Διάγραμμα αλυσίδας: από serialized input στο RCE
Διάγραμμα αλυσίδας: από serialized input στο RCE
  1. Sink: εντοπίζουμε ένα σημείο όπου η εφαρμογή αποσειριοποιεί μη έμπιστο input.
  2. Gadget: κατασκευάζουμε ένα payload που, κατά την αποσειριοποίηση, πυροδοτεί κώδικα (__reduce__ στην Python, readObject() στην Java).
  3. Deliver: το στέλνουμε μέσα από το κανονικό κανάλι εισόδου (cookie, παράμετρος, blob).
  4. Execute: η αποσειριοποίηση εκτελεί τον κώδικά μας.
  5. Foothold: αποκτούμε RCE και διαβάζουμε το flag ως απόδειξη.

Το πρώτο πράγμα που κάνει ένας ελεγκτής είναι να καταλάβει τι μορφή έχει το session. Κάνουμε ένα κανονικό login στο Nostos Portal και κοιτάμε το cookie που μας δίνει.

Το Set-Cookie: session=... δεν είναι ένα αδιαφανές τυχαίο token, ούτε ένα υπογεγραμμένο JSON. Είναι base64. Το αποκωδικοποιούμε και το «διαβάζουμε» με το pickletools.dis, το εργαλείο της Python που κάνει disassemble ένα pickle stream:

Το legit cookie είναι ένα Python pickle
Το legit cookie είναι ένα Python pickle

Το output είναι αποκαλυπτικό. Βλέπουμε τα opcodes PROTO 4 (έκδοση pickle), SHORT_BINUNICODE '__main__' και 'User', STACK_GLOBAL (φόρτωσε την κλάση __main__.User), NEWOBJ (φτιάξε instance) και STOP. Με άλλα λόγια: το cookie είναι ένα σειριοποιημένο Python αντικείμενο, και ο client το ελέγχει πλήρως.

Αυτό είναι το κρίσιμο εύρημα. Αφού ο client στέλνει το pickle και ο server το κάνει pickle.loads(), ο client ελέγχει την είσοδο ενός μηχανισμού που μπορεί να εκτελέσει κώδικα. Η αδυναμία είναι ήδη εδώ· απομένει να την οπλίσουμε.

Σημαντικά σημεία του pickle format

ΣτοιχείοΤι σημαίνει για τον επιτιθέμενο
PROTOΈκδοση του pickle protocol (2–5). Δεν επηρεάζει την εκμετάλλευση.
GLOBAL / STACK_GLOBALΦορτώνει ένα «global» — δηλαδή οποιοδήποτε module.attribute. Εδώ κρύβεται η δύναμη.
REDUCEΚαλεί το callable της στοίβας με τα ορίσματα. Το «κουμπί» της εκτέλεσης.
__reduce__Η μέθοδος που, αν οριστεί σε μια κλάση, καθορίζει πώς θα ανακατασκευαστεί — δηλαδή ποιο callable θα κληθεί.
🔒Κατεβάστε το Web & API Security Checklist (PDF)

OWASP-aligned έλεγχοι για injection, auth/JWT, file upload, deserialization και request smuggling. Αφήστε εταιρικό email.


Python pickle — Βήμα 2: το __reduce__ gadget

Η Python μάς δίνει έναν επίσημο, τεκμηριωμένο μηχανισμό για να πούμε στο pickle «πώς να ανακατασκευάσει» ένα αντικείμενο: τη μέθοδο __reduce__. Αυτή επιστρέφει ένα tuple (callable, args), και κατά το loads() το pickle εκτελεί callable(*args) και χρησιμοποιεί το αποτέλεσμα ως το «ανακατασκευασμένο» αντικείμενο.

Ο επιτιθέμενος απλώς ορίζει μια κλάση της οποίας το __reduce__ επιστρέφει κάτι επικίνδυνο. Στο δικό μας exploit_pickle.py:

class RCE:
    def __reduce__(self):
        return (subprocess.check_output, (["sh", "-c", CMD],))

Το CMD είναι η αβλαβής εντολή απόδειξης: id; hostname; whoami; cat του τοπικού flag. Παράγουμε το payload και κοιτάμε τι φτιάχτηκε:

Παραγωγή του κακόβουλου pickle και opcode dump
Παραγωγή του κακόβουλου pickle και opcode dump

Το disassembly δείχνει καθαρά τη διαφορά από το legit cookie: τώρα το pickle φορτώνει subprocess και check_output (STACK_GLOBAL), σπρώχνει τη λίστα ['sh','-c', CMD] στη στοίβα, και εκτελεί REDUCE. Όταν ο server κάνει pickle.loads(), το REDUCE καλεί subprocess.check_output(['sh','-c', CMD]) και επιστρέφει το stdout της εντολής ως το «αντικείμενο χρήστη».

Αυτό είναι το κομψό —και επικίνδυνο— κομμάτι: επειδή η ευάλωτη εφαρμογή στη συνέχεια εμφανίζει το αποσειριοποιημένο αντικείμενο πίσω στον client (Logged in as: {user}), το output της εντολής μας θα εμφανιστεί κατευθείαν μέσα στην HTTP απάντηση.


Python pickle — Βήμα 3: RCE και το flag

Στέλνουμε το base64 payload ως τιμή του cookie session στο /dashboard με ένα απλό curl --cookie:

RCE μέσω insecure deserialization + το flag
RCE μέσω insecure deserialization + το flag

Η απάντηση του server μιλάει από μόνη της. Το πεδίο «Logged in as» δεν περιέχει πια User(username=guest, role=guest). Περιέχει το output της εντολής μας, εκτελεσμένης στον server:

uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),24(cdrom),27(sudo)
lab
kali
flag: flag{audax_lab_deserialization_rce_pickle_reduce_pwn}

Έχουμε RCE. Ο server εκτέλεσε id, hostname, whoami και διάβασε ένα αρχείο flag στο δικό του filesystem — κάτι που μόνο η διεργασία του server μπορεί να κάνει. Το flag flag{audax_lab_deserialization_rce_pickle_reduce_pwn} είναι η αδιαμφισβήτητη απόδειξη ότι πετύχαμε εκτέλεση κώδικα, όχι απλώς «διάβασμα δεδομένων». Από εδώ, σε ένα πραγματικό engagement, ένας επιτιθέμενος θα είχε foothold στον application server.

Αξίζει να σταθούμε στο πόσο λίγο χρειάστηκε: ούτε ένα byte κώδικα δεν «σπάσαμε». Χρησιμοποιήσαμε τον επίσημο μηχανισμό __reduce__ της Python, ακριβώς όπως προβλέπεται — απλώς πάνω σε ένα input που δεν θα έπρεπε ποτέ να είχε φτάσει στο pickle.loads().


Java — RCE μέσω ObjectInputStream.readObject()

Η Java έχει το ίδιο θεμελιώδες πρόβλημα, με διαφορετική μηχανική. Όταν καλείται ObjectInputStream.readObject(), το runtime καλεί αυτόματα την ιδιωτική μέθοδο readObject(ObjectInputStream) κάθε κλάσης που την ορίζει, ως μέρος της ανακατασκευής. Αν αυτή η μέθοδος κάνει κάτι επικίνδυνο, το απλό readObject() γίνεται RCE.

Για να το δείξουμε ως πλήρως εκτελέσιμο, φτιάξαμε ένα δικό μας Serializable gadget, την κλάση PwnGadget, της οποίας η readObject() τρέχει την εντολή απόδειξης:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    Process p = new ProcessBuilder("sh", "-c", cmd).redirectErrorStream(true).start();
    // ... εκτύπωση του output ...
}

Ο MakePayload σειριοποιεί ένα PwnGadget και τυπώνει το base64 blob. Παρατηρήστε τα magic bytes:

Παραγωγή Java serialized payload και magic bytes
Παραγωγή Java serialized payload και magic bytes

Τα πρώτα bytes είναι ac ed 00 05 — το STREAM_MAGIC (0xACED) συν την έκδοση. Κάθε native Java serialized stream ξεκινά έτσι· είναι ένα από τα πιο χρήσιμα σημάδια για έναν αμυντικό (θα το δούμε στην ενότητα ανίχνευσης). Αυτό το βήμα είναι το ακριβές ανάλογο του java -jar ysoserial.jar <chain> 'id' — με τη διαφορά ότι εδώ χρησιμοποιούμε δικό μας gadget αντί για μια αλυσίδα βιβλιοθήκης.

Τώρα τροφοδοτούμε το blob στο ευάλωτο sink:

Java RCE μέσω readObject() + το flag
Java RCE μέσω readObject() + το flag

Η αλληλουχία είναι διαφωτιστική: το sink λέει ότι αποσειριοποιεί «χωρίς filter», μετά η PwnGadget.readObject() πυροδοτείται κατά τη διάρκεια του readObject(), εκτελεί την εντολή, και βλέπουμε ξανά id, hostname, whoami και το flag:

flag: flag{audax_lab_deserialization_rce_java_readobject}

Δεύτερο, ανεξάρτητο RCE — αυτή τη φορά στην πλατφόρμα της Java, με το ίδιο ακριβώς θεμελιώδες σφάλμα: αποσειριοποίηση μη έμπιστου input χωρίς κανέναν έλεγχο για το ποιες κλάσεις επιτρέπεται να ανακατασκευαστούν.


ysoserial: τι είναι, τι είναι οι gadget chains, και τι δείξαμε (με ειλικρίνεια)

Στον πραγματικό κόσμο, σπάνια ελέγχετε μια κλάση readObject() στον στόχο. Εκεί μπαίνει το ysoserial: το de facto εργαλείο που, αντί να χρειάζεται δικό σας gadget, βρίσκει αλυσίδες από κλάσεις που υπάρχουν ήδη στο classpath του στόχου (σε δημοφιλείς βιβλιοθήκες) και τις συνδυάζει έτσι ώστε το readObject() να καταλήξει, μέσα από reflection και «μαγικές» μεθόδους, σε Runtime.exec().

Η τυπική σύνταξη είναι:

java -jar ysoserial.jar CommonsCollections6 'id' | base64 -w0

όπου CommonsCollections6 είναι το όνομα της gadget chain. Οι πιο γνωστές οικογένειες:

Gadget chainΒιβλιοθήκη / βάση
CommonsCollections1..7Apache Commons Collections (InvokerTransformer / LazyMap / TiedMapEntry)
CommonsBeanutils1Apache Commons BeanUtils
Spring1, Spring2Spring core
Groovy1Apache Groovy
Hibernate1/2, JRMPClientHibernate / RMI
Ειλικρινής δήλωση για το ysoserial
Ειλικρινής δήλωση για το ysoserial

Ειλικρινής δήλωση (δεν παραποιούμε αποτελέσματα). Το ysoserial δεν εκτελέστηκε σε αυτό το lab, και δεν παρουσιάζουμε κανένα ψεύτικο output του. Οι λόγοι, όπως τους καταγράψαμε:

  • Δεν βρέθηκε προσβάσιμο prebuilt JAR: το GitHub release asset επέστρεψε HTTP 404 και το build μέσω jitpack για το frohoff/ysoserial απέτυχε (“Build failed”).
  • Δεν υπήρχε toolchain για τοπικό build: το Apache Maven δεν είναι εγκατεστημένο, και δεν υπάρχει passwordless sudo για εγκατάσταση πακέτων.
  • Ακόμη κι αν παραγόταν το payload, οι CommonsCollections* chains καταλήγουν σε RCE μόνο όταν οι ευάλωτες εκδόσεις των gadget βιβλιοθηκών βρίσκονται στο classpath του στόχου, τυπικά πάνω σε ένα παλαιότερο, πιο επιτρεπτικό ως προς το reflection JVM. Το JVM του lab είναι OpenJDK 21 με σύγχρονο module/reflection hardening και χωρίς αυτές τις βιβλιοθήκες.

Αντί λοιπόν να «σκηνοθετήσουμε» ένα ysoserial output, αποδείξαμε το ταυτόσημο θεμελιώδες primitive (readObject() που εκτελεί κώδικα) με ένα δικό μας gadget. Η συνεισφορά του ysoserial δεν είναι το primitive· είναι η εύρεση της αλυσίδας μέσα σε κώδικα βιβλιοθήκης, όταν δεν ελέγχετε καμία κλάση στον στόχο. Αυτή είναι η τίμια, τεχνικά ακριβής εικόνα.


Ανίχνευση: τι «βλέπει» ένας αμυντικός

Το καλό νέο για την άμυνα είναι ότι τα κακόβουλα serialized payloads έχουν πολύ διακριτά χαρακτηριστικά. Στο Python pickle, ένα payload εκτέλεσης κώδικα θα περιέχει σχεδόν πάντα τα opcodes GLOBAL/STACK_GLOBAL (import ενός module.attribute) και REDUCE (κλήση). Ένα καθαρά data-only session δεν έχει κανένα από αυτά. Γράψαμε έναν απλό scanner που κάνει disassemble το pickle και ψάχνει επικίνδυνα opcodes:

Ανίχνευση: opcode scan του κακόβουλου pickle
Ανίχνευση: opcode scan του κακόβουλου pickle

Το αποτέλεσμα είναι μονοσήμαντο: εντοπίζονται GLOBAL, STACK_GLOBAL, REDUCE, και τα ονόματα subprocess / check_output. Το verdict είναι ξεκάθαρο: STACK_GLOBAL + REDUCE που κάνει import το subprocess.check_output = gadget εκτέλεσης κώδικα.

Στην Java, το αντίστοιχο σήμα είναι τα magic bytes ac ed 00 05 στην αρχή ενός stream που φτάνει από μη έμπιστη πηγή (π.χ. σε base64 rO0AB... μέσα σε παράμετρο ή header). Ένα WAF ή ένα IDS μπορεί να κάνει flag αυτό το μοτίβο· η ύπαρξη native Java serialization σε είσοδο χρήστη είναι, από μόνη της, ένα κόκκινο σημάδι.

Η ουσία: το κακόβουλο serialized input είναι ανιχνεύσιμο πριν αποσειριοποιηθεί — αρκεί να κοιτάξετε. Αυτό είναι κλειδί για detection engineering.


Από το serialized input στο RCE — ο πλήρης walkthrough

Ας συνδέσουμε όλα τα βήματα σε μια ενιαία αφήγηση, ανεξάρτητη από τη γλώσσα:

  1. Αναγνώριση του format. Ο ελεγκτής βλέπει ένα base64 (ή binary) πεδίο — cookie, παράμετρος, header, upload — και το αποκωδικοποιεί. Αν αρχίζει με \x80 (pickle) ή ac ed (Java), ή αν το pickletools.dis το «διαβάζει» ως pickle, τότε η εφαρμογή πιθανότατα αποσειριοποιεί input χρήστη.
  2. Εντοπισμός του sink. Επιβεβαιώνεται ότι το πεδίο όντως φτάνει σε pickle.loads() / readObject() (μέσω code review, error messages, ή συμπεριφοράς — π.χ. αλλαγή ενός byte προκαλεί deserialization error).
  3. Κατασκευή του gadget. Python: κλάση με __reduce__ που επιστρέφει ένα επικίνδυνο callable. Java: μια gadget chain (ysoserial) ή, στο lab, δικό μας readObject().
  4. Παράδοση. Το payload στέλνεται μέσα από το κανονικό κανάλι εισόδου, base64-encoded όπου χρειάζεται.
  5. Εκτέλεση & foothold. Η αποσειριοποίηση πυροδοτεί τον κώδικα· ο επιτιθέμενος αποκτά RCE. Στο lab, το επιβεβαιώνουμε διαβάζοντας ένα flag· σε ένα engagement, θα ήταν το σημείο εκκίνησης για περαιτέρω δράση, πάντα εντός scope.
Χρειάζεστε στοχευμένο έλεγχο τώρα; Το να τρέξετε ένα εργαλείο μόνοι σας είναι το εύκολο κομμάτι. Η ομάδα της Audax εκτελεί penetration testing και offensive assessment που αποδεικνύουν στην πράξη τι πραγματικά σας εκθέτει — με τεκμηριωμένα ευρήματα και προτεραιοποίηση.

Αντιστοίχιση σε MITRE ATT&CK

Η αλυσίδα αντιστοιχίζεται καθαρά σε δύο techniques του MITRE ATT&CK:

MITRE ATT&CK mapping
MITRE ATT&CK mapping
  • T1190 – Exploit Public-Facing Application: το μη έμπιστο serialized input φτάνει σε ένα deserialization sink (pickle.loads / readObject) μιας εκτεθειμένης εφαρμογής. Αυτό είναι το Initial Access.
  • T1059 – Command and Scripting Interpreter: το gadget (__reduce__ / readObject) εκτελεί sh -c '<cmd>' στον server. Αυτό είναι το Execution — η μετάβαση από «διαβάζω δεδομένα» σε «τρέχω εντολές».

Αυτή η αντιστοίχιση δίνει στους αμυντικούς μετρήσιμη κάλυψη: detections για ασυνήθιστες διεργασίες που ξεκινούν από τον application server (child processes ενός web worker), για native Java serialization σε input χρήστη, και για pickle opcodes σε session δεδομένα.


Επαλήθευση: γιατί ξέρουμε ότι δούλεψε πραγματικά

Δεν βασιστήκαμε σε «φαινομενική» επιτυχία. Κάθε βήμα έχει ανεξάρτητη απόδειξη:

  • Θετική απόδειξη RCE: το flag εμφανίστηκε στην απάντηση μόνο επειδή η εντολή cat εκτελέστηκε στη διεργασία του server και διάβασε ένα αρχείο που βρίσκεται στο filesystem του server. Χωρίς εκτέλεση κώδικα, το flag δεν θα μπορούσε να εμφανιστεί.
  • Αντιδιαστολή με το legit cookie: δείξαμε ότι το κανονικό session αποσειριοποιείται σε User(username=guest, role=guest), ενώ το κακόβουλο αποσειριοποιείται στο output της εντολής. Η διαφορά είναι στο περιεχόμενο του pickle, όχι στον server.
  • Έλεγχος με hardened loader: το ίδιο payload που πέτυχε RCE στον ευάλωτο loader απορρίφθηκε από τον restricted Unpickler και από τον Java ObjectInputFilter (βλ. ενότητα άμυνας). Αυτό αποδεικνύει ότι το payload ήταν όντως το ενεργό στοιχείο.
  • Ανεξάρτητη ανίχνευση: ο opcode scanner εντόπισε τα STACK_GLOBAL/REDUCE και το subprocess import χωρίς να χρειαστεί να τρέξει το payload.

Αντιμετώπιση προβλημάτων (troubleshooting) που συναντήσαμε

Στην πράξη, το lab δεν στήθηκε χωρίς εμπόδια — και η ειλικρινής καταγραφή τους έχει αξία:

  • Απουσία JDK (μόνο JRE): το σύστημα είχε java αλλά όχι javac, οπότε αρχικά δεν μπορούσαμε να μεταγλωττίσουμε Java. Δεν υπήρχε passwordless sudo για apt install. Λύση: κατεβάσαμε ένα φορητό OpenJDK 21 (Temurin) και το αποσυμπιέσαμε μέσα στον φάκελο του lab, χωρίς εγκατάσταση σε system paths και χωρίς root — μια καθαρή, απομονωμένη προσέγγιση.
  • ysoserial μη διαθέσιμο: όπως αναλύσαμε, δεν βρέθηκε prebuilt JAR (404 / jitpack build failed) και δεν υπήρχε Maven. Αντί να παραποιήσουμε output, δηλώσαμε τον περιορισμό και αποδείξαμε το primitive με δικό μας gadget.
  • Flask module missing: το system Python δεν είχε Flask. Λύση: δημιουργήσαμε virtual environment και εγκαταστήσαμε flask==3.1.0 τοπικά.
  • Ζήτημα ταυτότητας στα outputs: οι εντολές id/hostname/whoami επέστρεφαν το πραγματικό όνομα χρήστη/host. Τα αποστειρώσαμε σε kali@lab σε όλα τα shipped artifacts (screenshots, logs, configs), διατηρώντας το τεχνικό περιεχόμενο ακέραιο.

Δυνατά σημεία και περιορισμοί της άσκησης

Δυνατά σημεία. Και οι δύο περιπτώσεις (Python & Java) είναι πλήρως εκτελέσιμες και αναπαραγώγιμες, με πραγματικά outputs και ανεξάρτητη επαλήθευση. Η άσκηση δεν σταματά στο «να το exploit»· δείχνει ανίχνευση και άμυνα με το ίδιο payload, κάτι που τη μετατρέπει από «κόλπο» σε διδακτικό εργαλείο. Η αντιδιαστολή ευάλωτου vs hardened loader είναι ιδιαίτερα ισχυρή για client reporting.

Περιορισμοί. Δεν τρέξαμε το ίδιο το ysoserial (δηλωμένο ρητά). Το Java gadget είναι δικό μας, όχι μια πραγματική library chain — κάτι που είναι σκόπιμο για να παραμείνει το lab αυτόνομο και ασφαλές, αλλά σημαίνει ότι δεν αναπαράγουμε τη δυσκολία εύρεσης chain σε production classpath. Το lab είναι single-host· δεν καλύπτει network-level pivoting μετά το foothold.


Θέματα ασφάλειας κατά τη χρήση

Πριν αναπαράγετε κάτι τέτοιο, ακόμη και σε δικό σας περιβάλλον, προσέξτε:

  • Τα gadgets είναι πραγματικός κώδικας εκτέλεσης. Ένα λάθος CMD δεν είναι «αβλαβές». Κρατήστε τις εντολές απόδειξης τετριμμένες (id, hostname) και ποτέ destructive.
  • Απομόνωση. Τρέξτε τα πάντα σε loopback/VM/container χωρίς δημοσιευμένες θύρες. Ένα ευάλωτο deserialization endpoint είναι, εξ ορισμού, RCE για οποιονδήποτε το φτάσει.
  • Καθαρισμός. Σταματήστε τα services μετά το τεστ, αφαιρέστε τα flag files, μην αφήνετε ευάλωτα ports ανοιχτά.
  • Ευαίσθητα ευρήματα. Σε πραγματικό engagement, ένα deserialization RCE είναι critical finding· χειριστείτε το με την ανάλογη εμπιστευτικότητα και μη το αποδεικνύετε με εντολές που αγγίζουν production δεδομένα.
  • Dual-use. Οι ίδιες τεχνικές λειτουργούν εναντίον πραγματικών συστημάτων· χρησιμοποιήστε τις μόνο εντός ρητά εξουσιοδοτημένου scope.

Πού μπορεί να χρησιμοποιηθεί επαγγελματικά — και πότε όχι

Πού ταιριάζει. Το σενάριο είναι πυρήνας του web/API penetration testing: εντοπισμός deserialization sinks σε cookies, tokens, message queues και RPC endpoints. Είναι ιδανικό για adversary validation (επιβεβαίωση ότι τα detections για ύποπτα child processes και για serialized payloads δουλεύουν), για detection engineering, και για awareness σε developers — το να δουν ένα cookie να γίνεται RCE αλλάζει τη στάση τους απέναντι στο pickle/Java serialization. Είναι επίσης άμεσα σχετικό με απαιτήσεις NIS2 & DORA.

Πότε όχι. Ποτέ εναντίον συστημάτων εκτός ρητού scope. Ποτέ με destructive εντολές ως «απόδειξη». Και ποτέ ως «απόδειξη ασφάλειας»: το ότι ένα εργαλείο δεν βρήκε deserialization sink δεν σημαίνει ότι δεν υπάρχει — τα sinks συχνά κρύβονται σε βιβλιοθήκες και σε λιγότερο προφανή endpoints.


Δέχεται η εφαρμογή σας serialized δεδομένα;

Το insecure deserialization μετατρέπει ένα «αθώο» cookie σε Remote Code Execution. Η Audax εντοπίζει τέτοια sinks (pickle, Java readObject) με web application penetration testing.

Ζητήστε web app pentest →

Άμυνα: πώς σταματάτε κάθε βήμα της αλυσίδας

Εδώ βρίσκεται η ουσία για κάθε οργανισμό. Η αλυσίδα σπάει αν σπάσετε οποιονδήποτε κρίκο — αλλά, όπως πάντα, στόχος είναι το defense in depth.

1. Ο χρυσός κανόνας: μην αποσειριοποιείτε μη έμπιστα δεδομένα με «εκτελέσιμα» formats

Το πιο καθαρό control είναι να μη χρησιμοποιείτε ποτέ το Python pickle, τη native Java serialization, το PHP unserialize() ή αντίστοιχα, πάνω σε δεδομένα που ελέγχει ο χρήστης. Χρησιμοποιήστε data-only formats: JSON, MessagePack, Protocol Buffers. Αυτά μεταφέρουν δεδομένα, όχι οδηγίες κατασκευής αντικειμένων — ένα JSON δεν μπορεί να «κρύψει» ένα __reduce__ ή ένα readObject().

2. Αν είναι αναπόφευκτο: υπογραφή (HMAC) + αυστηρό allowlist

  • Υπογράψτε τα serialized δεδομένα με HMAC (server-side μυστικό). Αν το session δεν φέρει έγκυρη υπογραφή, απορρίπτεται πριν αποσειριοποιηθεί — ο επιτιθέμενος δεν μπορεί να παραποιήσει το cookie χωρίς το κλειδί. Το δείξαμε με ένα signed JSON session.
  • Allowlist κλάσεων / look-ahead deserialization: επιτρέψτε ρητά μόνο τις κλάσεις που περιμένετε. Στην Python, ένας RestrictedUnpickler που κάνει override το find_class απορρίπτει κάθε άγνωστο global. Στην Java, ένα ObjectInputFilter (JEP 290) απορρίπτει κλάσεις πριν τις κατασκευάσει.
Άμυνα Python: allowlist Unpickler μπλοκάρει το ίδιο payload
Άμυνα Python: allowlist Unpickler μπλοκάρει το ίδιο payload

Το ίδιο payload που πέτυχε RCE απορρίπτεται τώρα με blocked global: subprocess.check_output (not on allowlist). Κανένα import, κανένα REDUCE, καμία εκτέλεση.

Άμυνα Java: ObjectInputFilter (JEP 290) απορρίπτει το gadget
Άμυνα Java: ObjectInputFilter (JEP 290) απορρίπτει το gadget

Αντίστοιχα στη Java, το filter java.lang.*;java.util.*;!* απορρίπτει το PwnGadget με InvalidClassException -> filter status: REJECTED, πριν κληθεί το readObject(). Αυτό είναι look-ahead deserialization: η κλάση ελέγχεται πρώτα, εκτελείται μετά (ή, εδώ, ποτέ).

3. Patch τις gadget βιβλιοθήκες και μειώστε το attack surface

  • Κρατήστε ενημερωμένες τις βιβλιοθήκες που ιστορικά παρέχουν gadget chains (Apache Commons Collections/BeanUtils, Spring, Groovy, Hibernate). Πολλές έχουν εκδόσεις που κλείνουν γνωστές αλυσίδες.
  • Αφαιρέστε αχρείαστες βιβλιοθήκες από το classpath· λιγότερα gadgets = μικρότερη επιφάνεια.
  • Σε νέο κώδικα, προτιμήστε APIs που δεν χρησιμοποιούν native serialization καθόλου.

4. Sandboxing και least privilege

  • Τρέξτε τον application server με ελάχιστα δικαιώματα: μη-root χρήστης, read-only filesystem όπου γίνεται, seccomp/AppArmor profiles, containers χωρίς περιττά capabilities. Ακόμη κι αν συμβεί RCE, ο αντίκτυπος περιορίζεται.
  • Egress filtering: ένας web worker σπάνια χρειάζεται ελεύθερη εξερχόμενη πρόσβαση· περιορίστε την ώστε ένα RCE να μη «καλεί σπίτι» εύκολα.

5. WAF και ανίχνευση

  • Ένα WAF μπορεί να κάνει flag native Java serialization (ac ed, base64 rO0AB) και ύποπτα pickle μοτίβα σε input χρήστη — δεν είναι πανάκεια, αλλά αυξάνει το κόστος του επιτιθέμενου.
  • Detection: το πιο καθαρό σήμα είναι ένας application worker που ξεκινά child process (sh, bash, cmd) — κάτι που ένας web server σπάνια κάνει νόμιμα. Χαρτογραφήστε τα detections στα T1190 και T1059 για μετρήσιμη κάλυψη, και δημιουργήστε alerts για serialized payloads σε cookies/headers/params.

Αν εφαρμόσετε έστω τα σημεία 1 και 2, η συγκεκριμένη αλυσίδα δεν ολοκληρώνεται: είτε το επικίνδυνο format δεν χρησιμοποιείται καθόλου, είτε το allowlist απορρίπτει το gadget πριν εκτελεστεί.

Θέλετε αυτό το επίπεδο ελέγχου συνεχώς, στο δικό σας περιβάλλον; Το Erevos AI είναι η ετήσια managed υπηρεσία Continuous Threat Exposure Management (CTEM) της Audax — human-led, machine-scaled, με τεκμηριωμένη απόδειξη ανθεκτικότητας για NIS2 & DORA.

Σύνοψη

Το insecure deserialization είναι μια από τις λίγες αδυναμίες όπου το «διαβάζω ένα αντικείμενο» ισοδυναμεί με «εκτελώ ό,τι μου στείλουν». Το δείξαμε δύο φορές, με πλήρως εκτελέσιμο κώδικα: ένα Python pickle.loads() πάνω σε ένα cookie έγινε RCE μέσω ενός __reduce__ gadget, και ένα Java ObjectInputStream.readObject() έγινε RCE μέσω ενός Serializable gadget — και στις δύο περιπτώσεις με το flag ως αδιαμφισβήτητη απόδειξη εκτέλεσης στον server. Για το ysoserial δηλώσαμε ειλικρινά τι τρέξαμε και τι όχι, και εξηγήσαμε τις gadget chains χωρίς να παραποιήσουμε κανένα output.

Το μήνυμα για κάθε οργανισμό είναι διπλό. Πρώτον, μην αντιμετωπίζετε ποτέ ένα serialized πεδίο ως «απλά δεδομένα»· αν αποσειριοποιείται με pickle/Java native serialization, είναι potentially RCE. Δεύτερον, η άμυνα είναι σαφής και αποτελεσματική: data-only formats, υπογραφή, allowlist/look-ahead deserialization, patching, sandboxing, WAF και detection — το καθένα σπάει τουλάχιστον έναν κρίκο. Η συνεχής χαρτογράφηση της επιφάνειας επίθεσης και η επαλήθευση με πραγματικές τεχνικές είναι ο μόνος τρόπος να ξέρετε — και όχι απλώς να υποθέτετε — ότι αυτά τα controls δουλεύουν στο δικό σας περιβάλλον.


Από το εργαστήριο, στο δικό σας περιβάλλον

Στα Audax Labs στήνουμε ελεγχόμενα σενάρια για να δείξουμε πώς σκέφτεται και δρα ένας επιτιθέμενος — από ένα αθώο session cookie μέχρι πλήρη εκτέλεση κώδικα στον server. Το ζητούμενο δεν είναι το εργαλείο· είναι η αλυσίδα και η απόδειξη ανθεκτικότητας.

Η Audax Cybersecurity μεταφέρει αυτή τη λογική στο δικό σας περιβάλλον μέσα από δομημένο penetration testing, adversary validation και συνεχή Continuous Threat Exposure Management με το Erevos AI — με τεκμηριωμένα ευρήματα, προτεραιοποίηση και σαφή απόδειξη για τις απαιτήσεις NIS2 & DORA.

Human-led. Machine-scaled. Technically proven.

Επισκεφθείτε το https://www.audax.gr για να μάθετε πώς μετατρέπουμε σενάρια σαν αυτό σε μετρήσιμη βελτίωση της ασφάλειάς σας.

Continuous Exposure Management

Θέλετε συνεχή επικύρωση της ασφάλειάς σας — όχι έλεγχο μία φορά τον χρόνο;

Το Continuous Exposure Management της Audax ενοποιεί attack surface discovery, attack surface testing και παρακολούθηση αναδυόμενων απειλών σε έναν συνεχή, αποδεικτικό κύκλο — εκτελούμενο από τους certified offensive operators μας. Human-led. Technically proven. Ευθυγραμμισμένο με NIS2 & DORA.

Δείτε το Continuous Exposure Management →
Offensive Security · Penetration Testing

Χρειάζεστε Penetration Testing για τον οργανισμό σας;

Περιγράψτε το scope του ελέγχου μέσα από το δομημένο ερωτηματολόγιο και λάβετε εξατομικευμένη τεχνική & οικονομική προσφορά από την ομάδα Offensive Security της Audax. Χωρίς αυτόματη τιμή ή δέσμευση — η προσφορά αποστέλλεται μετά από τεχνική αξιολόγηση του scope.

Ζητήστε προσφορά Penetration Testing →