English · Ελληνικά
scriptPath — σε δύο privileged χρήστες, που το BloodHound (μόνο επιπέδου-αντικειμένου) ποτέ δεν αναδεικνύει. Εκείνο το ένα write καταλαμβάνει ένα NTFS-εγγράψιμο logon script SYSVOL για να πυροδοτήσει ένα reset ForceChangePassword, που αλυσιδώνεται μέσω ενός AddSelf μέσα σε RODC Administrators. Από εκεί είναι RBCD + ένα pivot σε SYSTEM στον read-only DC, κάνοντας dump το αφιερωμένο κλειδί krbtgt_8245 του. Το "read-only" αποδεικνύεται ψέμα: αφού ξαναγράψει το Password Replication Policy του RODC, εκείνο το κλειδί πλαστογραφεί ένα RODC Golden Ticket (Rubeus 2.3.3) που ο εγγράψιμος DC δέχεται ευχαρίστως — Domain Admin. Όλο αυτό οδηγείται από macOS, που μετέτρεψε την τελευταία φάση σε έναν δεύτερο, πιο ήσυχο καβγά ενάντια στο δικό μου toolchain.
Μια πλήρης σάρωση επιστρέφει το συνηθισμένο περίγραμμα Domain Controller — DNS, Kerberos, LDAP, SMB, RPC, WinRM. Το πιστοποιητικό RDP και το banner SMB ονομάζουν τον host και το domain.
Δύο γεγονότα να καταγράψω: το SMB signing απαιτείται (σκοτώνει το SMB relay), και το box είναι ένας DC Windows Server 2019 ονομασμένος DC01.garfield.htb. Το ρολόι είναι επίσης ~8 ώρες μπροστά μου — άσχετο τώρα, ένας επαναλαμβανόμενος πονοκέφαλος αργότερα για κάθε βήμα Kerberos από macOS.
Αυτό είναι ένα box assume-breach: σου δίνουν j.arbuckle : <assigned-pass>. Τα credentials δουλεύουν, αλλά ο λογαριασμός δεν είναι τίποτα ιδιαίτερο — χωρίς Pwn3d!, στάνταρ shares, read-only στο SYSVOL/NETLOGON.
Η λίστα χρηστών λέει μια ιστορία. Οι l.wilson και l.wilson_adm είναι κλασικό admin tiering — ένα άτομο και το privileged δίδυμό του. Και το krbtgt_8245 δεν είναι καθόλου πρόσωπο: είναι ο αφιερωμένος λογαριασμός κλειδιού KDC για έναν Read-Only Domain Controller. Υπάρχει ένας δεύτερος DC κρυμμένος κάπου, με το δικό του υλικό κλειδιού Kerberos. Κατάγραψε και αυτό.
Το SYSVOL, read-only, κρατά ένα μη-default artifact — ένα logon script στον φάκελο scripts:
Αθώο από μόνο του — ένα απόθεμα εκτυπωτή. Αυτό που έχει σημασία δεν είναι το περιεχόμενο του script· είναι ποιος μπορεί να αλλάξει σε τι δείχνει. Κράτα αυτή τη σκέψη.
Το BloodHound είναι το αντανακλαστικό εδώ, οπότε συνέλεξα και έκανα import. Ο κόμβος j.arbuckle ήταν ένα φάντασμα με έγκυρη κάρτα: μέλος των Users, Domain Users, IT Support — και Outbound Object Control: 0. Χωρίς GenericAll, χωρίς GenericWrite, χωρίς ForceChangePassword. Το "Shortest path to Domain Admins" επέστρεψε Path not found.
Έκαψα πραγματικό χρόνο στα κλασικά πριν αμφισβητήσω τον χάρτη:
GetUserSPNs επέστρεψε No entries found. Ούτε ένα SPN στο domain.Κάθε στάνταρ τεχνική συμφωνούσε: αυτός ο χρήστης είναι ανίσχυρος. Αλλά ένα Δύσκολο box δεν σου δίνει credentials και δεν αφήνει κανέναν δρόμο. Η σωστή ερώτηση δεν ήταν "τι άλλο μπορώ να δοκιμάσω;" — ήταν "τι δεν μου δείχνει το BloodHound;"
Το BloodHound (Legacy και CE) κάνει καταμέτρηση ACLs στο επίπεδο αντικειμένου — πιάνει GenericAll, GenericWrite, WriteDACL, WriteOwner. Αλλά τα δικαιώματα AD είναι πολύ πιο κοκκώδη: μπορείς να δώσεις write σε ένα μοναδικό attribute, και εκτός αν ένα εργαλείο κάνει query DACLs επιπέδου-attribute, εκείνο το δικαίωμα είναι αόρατο.
Το bloodyAD ελέγχει δικαιώματα write ανά-attribute — ακριβώς τα ψιλά γράμματα που παραλείπει το BloodHound. Το get writable του εξερράγη στο terminal με τον συνηθισμένο θόρυβο self-service (τηλέφωνα, ταχυδρομικές διευθύνσεις, attributes cloud-extension που κάθε χρήστης μπορεί να επεξεργαστεί στον εαυτό του). Θαμμένο μέσα, σε αντικείμενα που δεν ήταν δικά μου να αγγίξω, ένα attribute ξεχώρισε:
Ο j.arbuckle μπορεί να γράψει το attribute scriptPath και στον l.wilson και στον l.wilson_adm. Το scriptPath είναι το logon script που τρέχει κάθε φορά που εκείνος ο χρήστης κάνει sign in. Όχι GenericWrite, όχι ένα password reset — ένα attribute. Και ήταν αρκετό για να ξεφτίσει το domain.
Το σχέδιο: δείξε το scriptPath του l.wilson σε ένα script που ελέγχω, περίμενε το box να προσομοιώσει το logon εκείνου του χρήστη, και τρέξε κώδικα σαν l.wilson. Δύο πράγματα έπρεπε να είναι αληθινά. Πρώτο, χρειαζόμουν να γράψω ένα script στον φάκελο scripts του netlogon — και το --shares έλεγε READ. Αλλά τα δικαιώματα επιπέδου-share και επιπέδου-NTFS είναι διαφορετικά πράγματα, και δεν δοκίμασα ποτέ το ACL του NTFS. Ήταν εγγράψιμο όλη την ώρα:
Δεύτερο — και αυτό είναι το κομμάτι που μου αρέσει — δεν χρειάστηκα ποτέ ένα reverse shell. Ένα ζωντανό callback είναι το πιο θορυβώδες πράγμα που μπορείς να κάνεις, και είναι περιττό εδώ. Το logon script τρέχει σαν l.wilson, και ο l.wilson κρατά ForceChangePassword πάνω στον l.wilson_adm (μια ακμή που εμφανίζεται μόνο αφού λειτουργείς σαν εκείνον). Οπότε το ίδιο το script κάνει κατάχρηση εκείνου του δικαιώματος: κάνει reset το password του admin διδύμου σε κάτι που ξέρω, in-process, request/response, τίποτα να πιάσω.
Από ένα μοναδικό attribute write σε έναν ελεγχόμενο privileged λογαριασμό. Το WinRM σαν l.wilson_adm δίνει το user flag:
Με τον l.wilson_adm στο χέρι, εμφανίζεται μια νέα ακμή που το graph θα μπορούσε να δει: AddSelf σε μια ομάδα ονομασμένη RODC Administrators. Όχι "πρόσθεσε οποιονδήποτε" — απλά "πρόσθεσε τον εαυτό σου". Μία εντολή:
Αυτό δίνει διοικητικό έλεγχο πάνω στον RODC01. Αλλά ο RODC01 ζει στο <RODC-IP> — ένα εσωτερικό segment στο οποίο ο host μου δεν έχει route. Ο DC01 μπορεί να το φτάσει· εγώ είμαι απέξω. Τίτλος admin RODC, χωρίς τρόπο να τον χρησιμοποιήσω. Ακόμα.
Για να πάρω SYSTEM στον RODC01 χρησιμοποίησα Resource-Based Constrained Delegation: δημιούργησε έναν λογαριασμό machine από το πουθενά, πες στον RODC01 να τον εμπιστευτεί για delegation (έχω write πάνω στο αντικείμενο computer του RODC01 σαν admin του), μετά χρησιμοποίησε εκείνη την εμπιστοσύνη για να κάνεις impersonate τον Administrator.
Το getST μιλάει μόνο στο KDC (DC01, προσβάσιμο), οπότε το ticket Administrator-για-cifs/RODC01 βγαίνει χωρίς να αγγίξει το εσωτερικό segment. Εκείνο το wrapper faketime είναι η πρώτη εμφάνιση του clock skew ~8h — κάθε βήμα Kerberos από macOS το παίρνει, χρησιμοποιώντας ένα απόλυτο string ώρας DC (αυτό το build του libfaketime απορρίπτει τη μορφή offset +8h).
Ο DC01 είναι το pivot: ένα πόδι στο δίκτυό μου, ένα στο <internal-net>. Ένας reverse SOCKS proxy μέσα από αυτόν φέρνει τον RODC01 σε εμβέλεια. Το upload του Evil-WinRM είναι διαβόητα flaky, οπότε τράβηξα το binary με certutil:
Ο στόχος είναι το αφιερωμένο κλειδί KDC του RODC01, krbtgt_8245 — η ωμή ύλη για ένα RODC Golden Ticket. Σε Kali αυτό είναι μία γραμμή. Σε macOS, αυτή η φάση έγινε ένας καβγάς τριών γύρων με το δικό μου toolchain, και αξίζει να το καταγράψω γιατί κάθε γύρος είναι ένα επαναχρησιμοποιήσιμο μάθημα.
Το πρώτο μου ένστικτο ήταν secretsdump -use-vss με το ticket Administrator — snapshot το ntds.dit, κάνε το parse τοπικά. Πέρασε μέσα από SAM και LSA (κάνοντας dump τα κλειδιά machine του RODC01$ και τα cached secrets) και μετά πέθανε, επανειλημμένα, με τρεις διαφορετικούς τρόπους:
Με και τα τρία διορθωμένα, το VSS επιτέλους δούλεψε και άρχισε να τραβάει το ntds.dit — και μετά το flaky tunnel έριξε τη σύνδεση στη μέση του transfer (Bad file descriptor). Το download του ntds.dit ήταν απλά πολύ μεγάλο για ένα link που κατέρρεε κάθε λίγα δευτερόλεπτα. Οπότε σταμάτησα να παλεύω με το μέγεθος transfer και έκανα αυτό που πραγματικά ζητούσε η κατάσταση: τρέξε mimikatz πάνω στον ίδιο τον RODC και τράβηξε μόνο τον έναν λογαριασμό που χρειαζόμουν. Μικροσκοπικό push, μικροσκοπικό output.
Όλα μέσα από το tunnel χρησιμοποίησαν -target-ip <RODC-IP> για να αποφύγουν name-resolution μέσω SOCKS, ενώ κράτησαν το FQDN στο principal ώστε το SPN Kerberos να ταιριάζει. Να το: το κλειδί AES256, το SID του domain, και — από το msDS-SecondaryKrbTgtNumber του λογαριασμού — ο αριθμός branch RODC 8245. Τρία κομμάτια για να πλαστογραφήσω ένα ticket σαν οποιονδήποτε στο domain.
Ένα RODC Golden Ticket έχει μια παγίδα που δεν έχει ένα κανονικό: το Password Replication Policy. Όταν ο DC01 λαμβάνει ένα TGT υπογεγραμμένο από το krbtgt ενός RODC, ελέγχει αν εκείνος ο χρήστης επιτρέπεται να γίνει cached σε εκείνον τον RODC. Ο Administrator — και κάθε privileged ομάδα — κάθεται στη λίστα deny του RODC by default. Οπότε έπρεπε να ξαναγράψω τους κανόνες. Σαν admin RODC έχω write πάνω στα δύο attributes που είναι η πολιτική: καθάρισε πρώτα τη λίστα deny (το deny πάντα νικά το allow), μετά πρόσθεσε τον Administrator στη λίστα allow:
Σημείωσε ότι το bloodyAD set αντικαθιστά ολόκληρο το attribute — σε ένα πραγματικό περιβάλλον, το να αντικαταστήσεις τη λίστα allow με μία μόνο καταχώρηση θα έσπαγε τη replication για κάθε νόμιμα cached λογαριασμό. Σε ένα lab είναι εντάξει, αλλά το set είναι σφυρί, όχι νυστέρι.
Τώρα το ticket. Το ticketer του Impacket δεν μπορεί να κωδικοποιήσει τον αριθμό branch RODC μέσα στο KVNO του ticket, οπότε αυτό το βήμα χρειάζεται Rubeus — συγκεκριμένα v2.3.3. Το v2.2.0 έχει ένα bug PAC-signing που παράγει KRB_AP_ERR_BAD_INTEGRITY ακόμα και με τέλειο κλειδί· ίδιο κλειδί, ίδιες παράμετροι, διαφορετική έκδοση εργαλείου, εντελώς διαφορετικό αποτέλεσμα. Το έτρεξα στον DC01 (σωστό ρολόι, απευθείας πρόσβαση KDC):
Το injection /ptt αποτυγχάνει μέσα στο Evil-WinRM (ένα network logon δεν έχει session να κάνει inject — Error 1312), οπότε το πλαστογράφησα με /nowrap, ζήτησα το TGS με τον ίδιο τρόπο, μετά το μετέτρεψα και το χρησιμοποίησα από τον host μου. Ο DC01 δέχτηκε ένα ticket υπογεγραμμένο από το κλειδί ενός read-only DC, γιατί η πολιτική που υποτίθεται θα το σταματούσε τώρα έλεγε "allow":
SYSTEM στον DC01. Από έναν χρήστη domain με μηδέν outbound control στο BloodHound σε πλήρη Domain Admin — μέσα από ένα ACL επιπέδου-attribute, ένα hijack logon-script, μια αλυσίδα password-reset, RBCD, ένα dump credentials RODC, και ένα πλαστογραφημένο Golden Ticket.
Το Garfield έσπασε σχεδόν κάθε υπόθεση με την οποία μπήκα. Αυτό που μου έμεινε:
scriptPath που ποτέ δεν θα σχεδιάσει. Μην εμπιστεύεσαι ένα μόνο εργαλείο σαν τη μόνη πηγή αλήθειας σου — το bloodyAD get writable είναι η δεύτερη γνώμη που διαβάζει τα ψιλά γράμματα.--shares έλεγε READ στο SYSVOL· ο φάκελος scripts ήταν NTFS-εγγράψιμος όλη την ώρα. Τα δικαιώματα επιπέδου-share και επιπέδου-NTFS είναι διαφορετικά πράγματα. Ένα μόνο put θα είχε βρει το foothold ώρες νωρίτερα.msDS-RevealOnDemandGroup / msDS-NeverRevealGroup. Και το deny πάντα νικά το allow — καθάρισε πρώτα τη λίστα deny, η σειρά έχει σημασία.BAD_INTEGRITY και ένα έγκυρο ticket. Το impacket σε python 3.14 εναντίον 3.12 είναι η διαφορά ανάμεσα σε ένα crash και ένα καθαρό session signing SMB3. Όταν μια επίθεση αποτυγχάνει και όλα φαίνονται σωστά, υποψιάσου το εργαλείο πριν τον εαυτό σου.ntds.dit δεν επρόκειτο ποτέ να επιβιώσει σε ένα tunnel που κατέρρεε· το dump ενός-λογαριασμού του mimikatz ήταν μικροσκοπικό και ανθεκτικό. Σε ένα κακό link, ελαχιστοποίησε τι μετακινείς.Από macOS το box ήταν πραγματικά δύο boxes: η αλυσίδα AD, και ένας πιο ήσυχος καβγάς να κάνω το Kerberos, το SOCKS, και το Cryptodome να συνυπάρξουν κάτω από DYLD_INSERT_LIBRARIES. Άξιζαν και τα δύο.