Ghostlink — HackTheBox Machine Writeup
An nmap scan identifies MQTT (1883) on an AD Domain Controller. Anonymous subscription to MQTT healthcheck topics leaks internal vhosts (gpz-op26-secure, gpz-op26-toolkits) and fingerprints Gogs 0.13.3.
Repointing the MQTT healthcheck URL triggers NTLM authentication from svc_canary. Relaying NTLM to the secureshare vhost yields SOCKS access, where a double URL-encoded path traversal leaks an ntuser.dat hive and KeePass vault.
Extracted KeePass credentials log into Gogs as vroth. Abusing CVE-2025-8110 (symlink overwrite of .git/config core.sshCommand) achieves initial command execution as git.
Extracting nvirelli's PBKDF2 hash from gogs.db and cracking it via trimmed wordlist yields SSH access. Certipy enumerates ESC11 (ICPR unencrypted RPC); coercing DC01$ and relaying to ICPR mints a DC certificate for DCSync.
Attached Exploit Toolkit
All 6 custom scripts developed for this machine are attached below. Click any script to launch the Interactive Code Explorer to inspect syntax-highlighted code, search with Ctrl+F, copy, or download the files directly.
Ghostlink Exploit Toolkit (/autosolve/Ghostlink_scripts)
6 FilesStandalone exploit scripts, coercion helpers, and custom relay proxy bridges used in this writeup.
Connects to anonymous MQTT broker and repoints healthcheck URL to coerce svc_canary NTLM authentication.
Custom ntlmrelayx HTTP attack that serializes requests through a local proxy on 127.0.0.1:18080 to prevent socket desync.
Performs double URL-encoded path traversal against /api/download/ to exfiltrate arbitrary files over the relayed session.
Parses leaked ntuser.dat registry hive to inspect RecentDocs, ComDlg32, and TypedPaths for operational files.
Automated CVE-2025-8110 exploit: authenticates as vroth, plants .git/config symlink, and overwrites core.sshCommand for reverse shell.
Authoritative standalone Gogs CVE-2025-8110 reference exploit with registration, CSRF extraction, and rich logging.
Reconnaissance
We start with a full TCP port scan against 10.129.62.59.
The box is clearly a Domain Controller — it's running DNS, Kerberos, LDAP, SMB and WinRM. Two things jump out as non-default: port 1883/tcp (MQTT) and an IIS web server on port 80.
Why MQTT is interesting. MQTT is a lightweight pub/sub protocol normally used by IoT devices. Finding it on a domain controller is unusual — it suggests some internal component is publishing messages. If the broker allows anonymous connections, we can subscribe to # (all topics) and read everything being published.
Browsing to the IIS server on port 80 gives nothing but a static "Ghost Protocol Zero" landing page — the box is themed around a fictional APT group.
MQTT Intelligence
The MQTT broker on 1883 allows anonymous access. We subscribe to every topic and dump the retained messages:

The broker is acting as a node-tracking / health-monitoring bus for the group's infrastructure. The interesting topics are:
This leaks three hosts that are not in DNS from the outside:
| Host | IP | Role |
|---|---|---|
gpz-op26-secure.ghostlink.htb | 172.16.20.10 | Secure file-sharing app |
gpz-op26-toolkits.ghostlink.htb | 172.16.20.20 | Gogs code host |
dc01.ghostlink.htb | 10.129.62.59 | The DC itself |
We add these to /etc/hosts (they're virtual-hosted behind the DC's IIS/ARR reverse proxy) and browse:
- ▹
gpz-op26-secure→ 401, requires NTLM authentication. - ▹
gpz-op26-toolkits→ a Gogs instance (self-hosted Git service), anonymously reachable.

Fingerprinting Gogs without a version string. Gogs doesn't advertise its version in the page footer. But it does load its static assets with a git commit hash as a cache-buster: gogs.js?v=5084b4a9b77a506f5e287e82e945e1c6882b827a. Searching that hash maps it to Gogs 0.13.3 — which is vulnerable to the authenticated RCE CVE-2025-8110 (we'll come back to this).
NTLM Coercion via the MQTT Health-Check
The secureshare health-check topic contains a url field that the secureshare host periodically fetches to confirm it's still alive. The clever bit: the topic is writable, so we can repoint that url at our own box. When the health-check worker performs its HTTP request, it authenticates with NTLM — and we can capture (or relay) that authentication.
Publishes a modified telemetry JSON payload over MQTT to redirect the healthcheck URL to our attacker listener.
We run Responder in parallel to see what comes back:

Within a minute, the worker fetches our URL and hands us an NTLMv2 hash:
The hash is uncrackable — svc_canary has a strong machine-style password. Capturing it isn't the goal; relaying it is. NTLM relay lets us take that inbound authentication and bounce it against another service that will accept svc_canary's credentials, without ever knowing the password.
Relaying to the Secure File-Share
We point ntlmrelayx at http://gpz-op26-secure.ghostlink.htb, republish the MQTT topic with our url, and the relay completes:

We're now authenticated to the file-share app as svc_canary.

The app is a Blazor/ASP.NET front-end with an upload endpoint (/api/upload) and a download endpoint (/api/download/<file>), as revealed by app.js.
A real-world gotcha we hit. The default ntlmrelayx SOCKS plugin reuses the raw relayed socket and desyncs on keep-alive (each request returns the previous request's body). To get reliable multi-request browsing, we wrote a small custom ntlmrelayx HTTP attack (relay_bridge.py) that serializes every request through the relayed connection with a lock — a local proxy on 127.0.0.1:18080.
Custom Impacket HTTPAttack plugin creating a thread-locked local HTTP proxy on 127.0.0.1:18080 to prevent SOCKS desync.
Double URL-Encoded Path Traversal
The download endpoint takes a filename directly in the URL:
We suspect it resolves <file> against an upload directory on the internal host. If it naively joins the path, a ..\ traversal would escape. Let's test the escalation ladder.
Attempt 1 — raw traversal (blocked):
Attempt 2 — single URL-encoding (still blocked):
Attempt 3 — double URL-encoding (works):
Automates double URL-encoding path traversal against the secureshare API through the local relay bridge.

::How (and why) the bug works
URL-encoding turns special bytes into %xx. The trick is in how many times the server decodes the path before using it:
- ▹IIS decodes the incoming URL once before handing the path to the ASP.NET app.
- ▹The app (or its routing/framework) decodes it a second time when it builds the file path.
So:
| What we send | After IIS decodes once | After the app decodes again |
|---|---|---|
..\..\windows\win.ini | (unchanged) | → filtered: 403 |
%2e%2e%5c... | ..\..\windows\win.ini | → filtered: 403 |
%252e%252e%255c... | %2e%2e%5c... | ..\..\windows\win.ini → 200 |
The filter only ever sees the once-decoded form. It looks for a literal ..\ and, finding %2e%2e instead, lets the request through. The second decode pass then silently converts it back into a real traversal. This is the classic "double-encoding" filter bypass.
Registry Forensics → KeePass Vault
Now authenticated as svc_canary, we can pull its ntuser.dat (the per-user registry hive that records what the account has been doing):
We inspect the hive's recently opened documents using our forensics inspector:
Extracts and decodes MRU lists, RecentDocs, and ComDlg32 values from raw Windows NTUSER.DAT hives.

svc_canary recently opened db.zip. RecentDocs entries map to .lnk shortcuts in %USERPROFILE%\AppData\Roaming\Microsoft\Windows\Recent\, so we pull the shortcut to learn the file's full path:
Then we download the archive itself:
It's a KeePass vault protected by the key file (no master password). Opening it:

Two things matter:
- ▹
vrothstill has a password — every other account was migrated to a "centralized password manager", but vroth was forgotten.vroth:mOo03jpsqx8JQYMBwvFPis our ticket into Gogs. - ▹The Recycle Bin contains a deleted entry with a
passpol.pdfattachment — the Default Domain Policy report. It tells us the minimum password length is 20 characters, which we'll use to speed up hash-cracking later.
Gogs RCE — CVE-2025-8110
Gogs 0.13.3 is vulnerable to CVE-2025-8110, an authenticated remote-code-execution via a symbolic-link bypass in its PutContents API.
::The bug, simply
Gogs lets users commit symlinks into a repository (normal git behavior). It also exposes a PutContents API that writes file content outside the normal git flow. When CVE-2024-55947 was patched, the fix validated the path — but it forgot to validate where a symlink in that path points.
So the attack is:
- ▹Create a repo and commit a symlink that points at
.git/config. - ▹Use
PutContentsto write through that symlink — Gogs follows it and overwrites the repo's.git/config, which lives outside the intended working-tree directory. - ▹In the injected
.git/config, we setcore.sshCommandto our reverse-shell command. The next time Gogs runs a git operation over SSH, that command executes.
Automated Python exploit for CVE-2025-8110 against Gogs 0.13.3 using vroth credentials.

On our listener we get a shell as the git service account:
Standalone reference exploit by zAbuQasem for CVE-2025-8110 supporting proxying and rich terminal feedback.
Lateral Movement — from git to nvirelli
The Gogs host has a local user nvirelli. Gogs stores user password hashes (PBKDF2-SHA256) in its SQLite database gogs.db, so we exfiltrate it:
We pull nvirelli's hash out:
And convert it to hashcat format (PBKDF2-HMAC-SHA256 = mode 10900), where the salt is base64-encoded and the hex digest is base64-encoded:
Using the leaked password policy. PBKDF2 is deliberately slow, so cracking a huge wordlist is painful. But passpol.pdf told us the minimum length is 20 characters, so we trim rockyou.txt to only 20+ character candidates — from 14 million words down to 46,602 — and the crack finishes in seconds.

We su to nvirelli and grab the user flag:
Domain Escalation — ESC11 (ADCS)
nvirelli is a domain account, and the network has an Active Directory Certificate Services (ADCS) CA. We pivot through the Gogs host with chisel (reverse SOCKS proxy) so we can reach the CA on the internal 172.16.20.10, then enumerate ADCS for vulnerabilities:

::What ESC11 means
The CA accepts certificate requests over RPC (ICPR). Because it does not enforce encryption on those requests, we can relay a machine account's NTLM authentication directly into a certificate request. The classic ESC8 needs HTTP web enrollment; ESC11 does the same thing over the raw RPC interface.
Our target is the Domain Controller machine account (DC01$), which is allowed to request a DomainController certificate — and that certificate can impersonate the DC.
The attack is a two-step relay:
- ▹Coerce
DC01$to authenticate to us (usingcoercerwith the MS-RPRN / MS-DFSNM methods). - ▹Relay that authentication to the CA's ICPR interface with
ntlmrelayx, requesting a DomainController certificate.

We now hold a DomainController certificate for DC01$. We use it (via PKINIT) to obtain the DC machine account's NT hash, then run DCSync to dump the Administrator hash:

Finally, we pass the Administrator hash into WinRM and read the root flag:

References

Red Team Consultant · Penetration Tester · Bug Bounty Hunter
Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.