Fortune
Summary
The attacker found an OpenBSD web server hosting a fortune-cookie CGI application that passed the user-supplied database name directly to the OS shell without any validation, allowing unauthenticated command execution as the _fortune web service account.
Because outbound connections from the target were blocked, every subsequent file read was performed in-band by injecting commands that printed file contents between marker strings and extracting the block from the HTTP response.
Using that channel, the attacker read the intermediate Certificate Authority private key from a directory accessible to the web process, then forged a client certificate trusted by the server's HTTPS endpoint and obtained an SSH private key for the nfsuser account.
Logging in as nfsuser triggered OpenBSD's authpf, which dynamically opened firewall rules and exposed the previously hidden NFS /home share.
That share used AUTH_SYS authentication, blindly trusting whatever UID the client declared; the attacker mounted it as UID 1000 (charlie's server UID), planted an SSH key, and obtained an interactive charlie shell.
From charlie's session, the pgAdmin4 SQLite credential database was copied and the stored PostgreSQL DBA password reversed using pgAdmin4's known-weak key derivation scheme.
That same password had been set as the root account password, completing full system compromise.
Attack path — how the box was taken
Mapped exposed services and identified the fortune CGI application, then Confirmed unauthenticated OS command injection via the /select CGI parameter, then Exfiltrated the intermediate CA private key and certificate chain via in-band injection, then Forged a trusted mTLS client certificate and retrieved the authpf SSH identity from /generate, then Triggered authpf to open PF firewall rules and expose the NFS share, then Spoofed charlie's UID over NFS to plant an SSH key and obtain a user shell, then Decrypted the DBA password from pgAdmin4's SQLite credential store, then Used the recovered DBA credential as the root password to gain full system control.
Exact commands 2
nmap -sV -sC -p- --min-rate 2000 -oN fortune_full.txt $TARGETcurl -sS -i http://$TARGET/ | head -40Exact commands 2
curl -i -sS --max-time 10 -X POST -d 'db=fortunes%3Bid' http://$TARGET/selectfor spec in "http://$TARGET/select|db" "http://$TARGET/cgi-bin/fortune|f" "http://$TARGET/cgi-bin/fortune|file"; do u=${spec%|*}; p=${spec#*|}; echo "===== $u $p ====="; curl -ksS --max-time 8 -i -X POST --data-urlencode "$p=;id" "$u"; echo; doneExact commands 1
openssl rsa -in int.key -check -noout && openssl x509 -in int.crt -noout -subject -issuerExact commands 2
openssl genrsa -out attacker.key 2048 && openssl req -new -key attacker.key -out attacker.csr -subj '/CN=attacker/O=Fortune CA'openssl x509 -req -in attacker.csr -CA int.crt -CAkey int.key -CAcreateserial -out attacker.crt -days 365 -sha256Exact commands 2
ssh -i nfsuser.key -o IdentitiesOnly=yes -o StrictHostKeyChecking=no nfsuser@$TARGET &rpcinfo -p $TARGETExact commands 2
sudo mount -t nfs -o vers=3,nolock $TARGET:/home /mnt/fortunesudo useradd -u 1000 -M fakecharlie 2>/dev/null; sudo -u fakecharlie ssh-keygen -t rsa -f /tmp/charlie_id -N '' -qExact commands 2
scp -i /tmp/charlie_id charlie@$TARGET:/var/appsrv/pgadmin4/pgadmin4.db /tmp/pgadmin4.dbsqlite3 /tmp/pgadmin4.db "SELECT s.name, s.password, u.email, u.password FROM server s JOIN user u ON s.user_id = u.id;"Attack patterns used
The transferable techniques behind the compromise.
OS Command InjectionExploitationT1059.004
What it is
The fortune application's /select endpoint passed the 'db' POST parameter directly into a shell command without sanitisation. Appending a semicolon and an OS command (e.g., ;id) caused the server to execute it and return output in the HTTP response body, confirming code execution as the _fortune service account. Because outbound TCP connections from the target were blocked, all subsequent file exfiltration used an in-band marker pattern — injecting 'echo BEGIN; cat FILE; echo END' and isolating the result from the HTTP response with sed.
Why it works
Replace the shell-out with a safe parameterised alternative: maintain a server-side allowlist of valid fortune-file names and select from that list rather than passing raw user input to a shell. If a shell invocation is unavoidable, validate 'db' strictly against the allowlist before use and reject any non-matching value with an HTTP 400. Additionally, run the CGI process as a dedicated low-privilege account (e.g., _fortune) with read access limited only to the fortune files directory — never /home or /etc — and apply OpenBSD pledge(2)/unveil(2) or equivalent OS-level confinement to restrict what the process can access even if a future bug is discovered.
Private key theft via OS command injectionCredential AccessT1552.004
What it is
With code execution confirmed, the attacker enumerated /home/bob/ca/ and found a Fortune Intermediate CA (bob@fortune.htb, signed by Fortune Root CA) whose private key was stored with permissions readable by the _fortune web service account. Each file was exfiltrated in-band: injecting 'echo BEGINKEY; cat <file>; echo ENDKEY' into the db parameter and extracting the marked block from the HTTP response with sed. The intermediate private key, its certificate, and the CA chain were all recovered, granting the ability to issue new certificates the server would trust.
Why it works
Move the CA private key off the web server entirely. Store it on a dedicated offline or air-gapped signing host, or in a hardware security module (HSM). If the key must remain on the server for operational reasons, restrict ownership to root (mode 0400) and ensure it is never accessible to any web-facing or service account. Immediately revoke all certificates issued by this intermediate CA, generate a new intermediate CA key pair from a secure environment, and reissue all legitimate client certificates. Review and harden the /generate endpoint to ensure it cannot be misused to issue shell identities to forged certificate holders.
NFS AUTH_SYS UID spoofing for unauthorized file writeLateral MovementT1210
What it is
NFSv3 with AUTH_SYS authentication trusts the numeric UID provided by the client machine without any cryptographic verification. The attacker created a local OS account with UID 1000 — matching charlie's server UID — mounted /home from the NFS export, and used that UID-trusted access to write an attacker-controlled SSH public key into charlie's .ssh/authorized_keys through the mount point. A subsequent SSH login as charlie with the matching private key succeeded, providing an interactive shell.
Why it works
Migrate the NFS export to Kerberos authentication (sec=krb5p) so that user identity is cryptographically proven rather than client-asserted. In /etc/exports, replace the wildcard (*) with an explicit list of trusted management host IPs. Enable root_squash and all_squash to remap all client-supplied UIDs to an unprivileged anonymous account. For user home directories specifically, evaluate whether NFS export is operationally necessary; if not, remove the export entirely. As a defence-in-depth measure, firewall NFS ports (111, 2049) to only those specific management IPs even when authpf rules are active.
Recoverable credential extraction from pgAdmin4 SQLite databaseCredential AccessT1555
What it is
Enumeration from charlie's session revealed pgAdmin4 running on localhost:8081 (confirmed via /etc/httpd.conf), backed by a SQLite database at /var/appsrv/pgadmin4/pgadmin4.db. pgAdmin4 stores server passwords encrypted with AES-CFB8 using a key derived from a predictable application constant with no per-installation randomness — a scheme documented publicly and reversible offline by anyone with access to the database file. The attacker copied the file via scp and ran a decrypt script adapted from public pgAdmin4 research, recovering the plaintext password for the 'dba' PostgreSQL server entry from the joined server/user table rows and the embedded AES IV.
Why it works
Remove all saved server passwords from pgAdmin4. Supply database credentials at connection time using PostgreSQL pgpass files (owned by the connecting OS user, mode 0600, never shared) or a dedicated secrets manager such as HashiCorp Vault. Restrict /var/appsrv/pgadmin4/ to the pgAdmin4 service account only (mode 0700) so that no other OS user can read the database file. Upgrade pgAdmin4 to the latest release, which includes improved credential handling. Immediately rotate the exposed DBA password and audit all systems where that credential was in use.
Credential reuse across database service account and OS rootPrivilege EscalationT1078
What it is
The plaintext password recovered from pgAdmin4 was also configured as the root account password on the operating system. From charlie's interactive SSH session, running 'su - root' with the recovered credential immediately produced a root shell with no further exploitation required. Both flags were read from disk, confirming complete system compromise.
Why it works
Enforce an organisational policy explicitly prohibiting password reuse across database accounts, service accounts, and OS user accounts. Deploy a privileged access management (PAM) solution to generate and rotate unique, high-entropy passwords for each account class. For root access specifically, disable password-based su and sudo and require SSH key-based authentication or time-limited PAM-brokered elevation. Immediately change the root password and all other accounts sharing the compromised value, and audit access logs for any prior unauthorised use.
Findings
Exposed services
| External surface | A full TCP port scan of <retired-instance-ip> revealed OpenBSD httpd on ports 80 (HTTP) and 443 (HTTPS), SSH on port 22, and NFS-related services on ports 111 (rpcbind) and 2049 (nfs). Browsing port 80 surfaced a 'Your Fortune' web application with a /select endpoint that accepted a POST parameter named 'db'. The HTTPS site on port 443 required a client certificate, limiting unauthenticated access until a trusted certificate could be obtained. |