The Problem
Lets start from the top and look at the problem it solved The problem it solves: before RODC existed (was introduced in 2008), if a branch office needed domain authentication they had two options to provide that
- Deploy full writable DC at the branch
- you get full copy of AD database including every password hash in the domain which provides you with fast local authentication
- on the other hand you'd need a physical security at that branch like security guards, cage, physical access, cameras, etc. and if you can't provide that it would be disaster if you left the DC just hanging there
- No DC at the branch
- all authentication goes over WAN which is slower and less reliable if the WAN link drops
because those 2 options weren't good enough, the RODC was built
- give the branch office local authentication without putting the full domain database at risk
so What is RODC ?
- Read-Only Domain Controller is a special DC role that holds a read-only, filtered replica of the Active Directory designed for physically insecure branches
the Read-Only Solution
having RODC in the first place was introduced to separate full writable DCs from RODCs that's why RODCs provide the Following
- the AD Database is read only
- the RODC holds a copy of the
ntds.ditbut the AD replication manager marks it as non-originating replica meaning it can only receive changes from a writable DC but it can't produce changes on its own - if a client tries to do something that requires writes like writing an object, changing a password, or modifying an attribute the RODC either rejects it directly or proxies the request to the writable DC where the write will happen then changes eventually replicated back at the RODC
- the RODC holds a copy of the
- the RODC database if Filtered
- if we replicated the entire DC and marked it as RODC there is still some security risks like having the main DC
krbtgthash stolen or something that's why the database is partial not full database - and the rule is simple → the RODC has all AD objects (users, computers, groups, GPOs, OUs) but the credentials and the Kerberos keys is stripped out unless they are explicitly permitted by the PRP (Password Replication Policy)
- if we replicated the entire DC and marked it as RODC there is still some security risks like having the main DC
- DNS and SYSVOL are read-only
- If the RODC runs DNS, it hosts a read-only copy of the DNS zone, Client dynamic DNS updates get proxied to a writable DC
- SYSVOL modification stays on the RODC and is not replicated out
- Administrator Role Separation
- RODC Administration can be delegated to a domain user account without compromising the security on the active directory and it provides more of its own features but we'll get to the details later on
RODC's own krbtgt Account
lets take a step back and ask why does a KRBTGT account exist at all ?
if you looked at my article about Kerberoasting here you would see a full Kerberos authentication process but what i care about is the AS-REP message
you can see in this AS-REP that the TGT which is opaque to the client is encrypted using the krbtgt hash
in every AD domain there is one special account called krbtgt and It exists for that purpose: its password hash is the secret key used to encrypt and sign every TGT (Ticket Granting Ticket) in the domain
this part is opaque to you so you can't read what's inside it you just carry that TGT and present it around when you need a service ticket and where do you get a TGS ? the KDC at the Domain Controller and guess what he is the only one who can read this TGT because the is only one who has the krbtgt encryption key so he can decrypt it and decide what's gonna happen next ? either to issue a TGS or not
the issue with Kerberos tickets that they can be forged and the only reason they are issued at the KDC is because this is supposed to be the most safe place for that krbtgt hash otherwise each machine can forge a ticket for itself if it has this hash
so it isn't safe for the krbtgt hash to be placed at RODC and that's one of the features RODC provides → its own krbtgt account
when an RODC is promoted, AD automatically creates a new account with the naming convention krbtgt_XXXXXwhere that XXXXX number is derived from the RODC's msDS-SecondaryKrbTgtNumber attribute
lets say this account is krbtgt_12345
so this account:
- lives in
CN=Users,DC=corp,DC=localjust like the main krbtgt - has its own independent password and hash
- owned and managed by that specific RODC
- has
msDS-KrbTgtLinkBlattribute linking it back to the RODC computer object - cannot be used to issue tickets on any other DC legitimately
just keep this in your mind, all the pieces will be puzzled together soon
Password Replication Policy PRP
we mentioned earlier that the RODC holds a partial database and there is not credentials stored there by default except the RODC computer account the the RODC KRBTGT account now some other accounts' credentials can be stored but they have to be added explicitly (much safer this way)
PRP is the mechanism that answers the question which accounts are allowed to have their credentials stored on this specific RODC? every RODC has its own PRP and it works through two lists on each RODC Computer Object in AD (note that it is set on the RODC computer object)
msDS-RevealOnDemandGroupattribute which is like the allow list- accounts or groups listed here may have their credentials stored on this RODC
- and I say may because being on this list doesn't mean the account is cached yet only it is permitted to
msDS-NeverRevealGroupattribute which is the deny list- accounts or groups listed here will never be cached on this RODC
- the deny always wins over allow meaning that if an account appears in both, deny takes priority
- this list has the next groups and accounts by default
Domain Admins
Enterprise Admins
Schema Admins
Group Policy Creator Owners
Domain Controllers (the RWDC group)
krbtgt (domain-wide)
Denied RODC Password Replication Group
the third attribute that is related to this PRP is msDS-RevealedList which is a read-only attribute maintained by the RWDC and it records every account whose credentials have been successfully sent to the RODC
- accounts in the
msDS-RevealOnDemandGroupand not inmsDS-RevealedListare allowed to cache but not yet cached - an account in the
msDS-RevealedList→ the hash sits in thentds.ditat the RODC right now
and this is the entire caching flow ![[diagram-export-4-9-2026-2_10_09-PM.png]]
Ticket Requesting
now with this being said we have all the puzzle pieces, how does the actual ticket requesting happen ? we already now how does it work in a normal DC situation but what if there is a RODC in place, are there any special flags in the TGT ?
Before the flow, lock these three things in your head:
- RODC only holds credentials for accounts in
msDS-RevealedList - RODC signs tickets with
krbtgt_XXXXXwhich is its own key, not the domain KRBTGT - RWDC is the checkpoint that validates everything the RODC issues
we need to know what inside TGT?
the unencrypted header which is visible to anyone:
- client name
- realm (domain name)
- KVNO (Key Version Number) and it identifies which key was used to encrypt the ticket
the encrypted body only the KDC can read this (the box in purple)
- session key (used for future communication between client and KDC)
- auth time, start time, end time, renew-till
- ticket flags (forwardable, renewable, and more)
- client IP (optional)
- the PAC Privilege Attribute Certificate
the PAC is the most important part it contains
- username
- user RID
- group memberships and their SIDs
- logon time, password expiry
- Domain SID
- two signatures which is server signature and KDC signature
this PAC is the part that tells service what are your permissions and the entire authorization system depends on it
TGT issued by the RODC contains a special marker in its structure inside the encrypted part of the ticket there is a field called the KERB-AD-RESTRICTION-ENTRY and the KVNO (Key Version Number) in the ticket header identifies which key was used to sign it
RODC local ticket issuance
the flow starts by client sending normal AS-REQ then the RODC checks if this user is cached in the ntds.dit before even validating the pre-auth
if the user is cached then there is no need to proxy to RWDC and it starts by decrypting the timestamp in the AS-REQ if this succeeded then the RODC starts constructing the PAC of that client
PAC contains:
Username: jsmith
User RID: 1234
Group SIDs: Domain Users, BranchUsers, etc.
Logon time: now
Domain SID: S-1-5-21-XXXXXXX
and we know RODC has all the AD objects replicated RODC starts signing the PAC and we know the PAC needs two
Server signature: HMAC-MD5 signed with krbtgt_12345 key
KDC signature: HMAC-MD5 signed with krbtgt_12345 key
then the RODC starts building the TGT and encrypting it
- generate fresh random session key
- construct full TGT body
- encrypt the entire TGT body with the
krbtgt_12345aes key - sets the KVNO header to 12345
the KVNO header is critical (it isn't encrypted) it is just used to tell every DC that this was signed and encrypted but that KRBTGT version number look up who owns it
and then finally the RODC sends the AS-REP back to the client and the process of the TGS keeps going
now the question arises ? (maybe only in my head but it is a question)
if we can sign a ticket with the krbtgt_12345 and just use it, so what was the point of having it in the first place i mean i can get a ticket for RWDC domain admin using that key and it kinda acts like the main DC krbtgt so it would be valid ?
no it doesn't work that way cause there is a rule and it actually makes so much sense
A TGT generated by an RODC can be used in TGS-REQs to obtain service tickets from the same RODC or from writable Domain Controllers. When a TGT generated by an RODC is presented to a writable Domain Controller, the Domain Controller only accepts it if the ticket was generated for a principal listed in the RODC’s msDS-RevealOnDemandGroup attribute and not listed in the RODC’s msDS-NeverRevealGroup attribute.
How does it make sense? think of it this way to get a TGT issued for a certain user by RODC, this user must be cached in RODC and it can't be cached unless it satisfies those two conditions so when you present a fake TGT to RWDC says you got a TGT for Domain Administrator which doesn't satisfy those conditions by default it would ask how would you get a TGT for domain admin from RODC if this admin hash was never cached there and this is the analogy of validation
now the last part what can we do as attackers if we got access on RODC? we can do next
- RODC golden ticket
- Key list attack
RODC golden ticket
now that you got all pieces if you wanna a better understanding for this try out HTB Garfield machine or checkout my writeup about it once it is published here to get a golden
- You can forge an RODC golden ticket and present it to a writable Domain Controller only for principals listed in the RODC’s msDS-RevealOnDemandGroup attribute and not in the RODC’s msDS-NeverRevealGroup attribute
- When the RODC golden ticket is presented to a writable Domain Controller, the writable DC will regenerate the PAC in the resulting ticket rather than copying from the presented TGT
what we need to do this attack
- NT/SYSTEM on RODC to dump
ntds.ditand get thekrbtgt_xxxxxAES key - KVNO number
- Domain SID
- Principal in
msDS-RevealOnDemandGroupand not inmsDS-NeverRevealGroup - User RID
and then using Rubeus we can get that ticket
Rubeus.exe golden /rodcNumber:$KBRTGT_NUMBER /flags:forwardable,renewable,enc_pa_rep /nowrap /outfile:ticket.kirbi /aes256:$KRBTGT_AES_KEY /user:USER /id:USER_RID /domain:domain.local /sid:DOMAIN_SID
the issue with this attack isn't the Forging itself but how are we gonna get there
so we need to find an account who have write access over those attributes and a way to get NT/SYSTEM on RODC
usually check RODC Administrators group members for Write Access over those attributes
and to get NT/SYSTEM look at the RODC$ machine account and find a way to abuse it
Key List attack
It is possible to retrieve the long term secret of a user by sending a TGS-REQ (service ticket request) to the KRBTGT service with a KERB-KEY-LIST-REQ message type. This was introduced initially to support SSO with legacy protocols with Azure AD on on-premises resources.
just to keep it simple Azure AD has it own new way of Authentication but there is some resources that still use the old way NTLM hashes
now this AzureAD kerberos server is just RODC as Leonardo joked about it in this slides

An attacker can abuse this by forging a RODC golden ticket for a target user and use it to send a TGS-REQ to the KRBTGT service with a padata filed value of 161 (KERB-KEY-LIST-REQ). Knowing the KRBTGT key of the RODC is required here. The TGS-REP will contain the long term secret of the user in the KERB-KEY-LIST-REP key value.
so what we need is
- low privilege access
- RODC KRBTGT number
- RODC KRBTGT AES key
- the virtual RODC and the physical RODC must not belong to the denied group
and then we can use impacket-keylistattack
keylistattack.py -rodcNo "$KBRTGT_NUMBER" -rodcKey "$KRBTGT_AES_KEY" -full "$DOMAIN" /"$USER":"$PASSWORD"@"$RODC-server"
or dump specific user's hash
keylistattack.py -rodcNo "$KBRTGT_NUMBER" -rodcKey "$KRBTGT_AES_KEY" -t "$TARGETUSER" -kdc "$RODC_FQDN" LIST
or if you have a shell get a golden ticket first using the RODC golden ticket technique then dump the hashes
Rubeus.exe asktgs /enctype:aes256 /keyList /ticket:ticket.kirbi /service:krbtgt/domain.local
Mitigation and Detection
in Detection:
- Monitor Event ID 4769 TGS-REQ at RWDC where TGT KVNO maps to
krbtgt_XXXXXbut the principal isn't inmsDS-RevealOnDemandGroup - Monitor Event ID 4656/4663 object access on
ntds.ditor SYSTEM hive on the RODC - Monitor Event ID 4688 suspicious process creation on the RODC (Mimikatz, DSInternals)
- Audit
msDS-RevealedListfor unexpected high-value accounts appearing cached - Monitor Event ID 4769 TGS-REQ targeting the
krbtgtSPN directly (rare in normal environments) - Monitor for
padatatype 161 (KERB-KEY-LIST-REQ) in Kerberos traffic via NDR/IDS - Enable Microsoft Defender for Identity "Suspected Key List attack" alert
in Mitigation:
- Apply LAPS to the RODC local admin and restrict who's in the RODC Administrators group
- Enable Credential Guard on the RODC
- Lock write permissions on
msDS-RevealOnDemandGroupandmsDS-NeverRevealGroup - Keep
msDS-RevealOnDemandGroupminimal branch users and computers only, never privileged accounts - On suspected compromise, rotate
krbtgt_XXXXXtwice with replication delay between rotations - If not using Azure AD Kerberos block or alert on
padatatype 161 unconditionally - Ensure
krbtgt_AzureADstays in the deny list of all physical RODCs - Rotate
krbtgt_AzureADperiodically - Restrict outbound port 88 from RODC to designated hub DCs only
- No privileged interactive logins on the RODC treat it as Tier 1
this mitigation techniques can vary from environment to another so just try to do the best you can
Resources
- https://adsecurity.org/?p=3592
- https://shenaniganslabs.io/2023/01/25/RODCs.html
- https://0xdeaddood.rocks/wp-content/uploads/2022/07/TheKerberosKeyListAttack_Ekoparty2021.pdf
- https://www.thehacker.recipes/ad/movement/credentials/dumping/kerberos-key-list#practice
- https://www.secureauth.com/blog/the-kerberos-key-list-attack-the-return-of-the-read-only-domain-controllers/
