The short version
Velociraptor 0.77.1 enforced EXECVE when an investigator collected
Linux.Sys.BashShell normally, but failed to enforce it when the same artifact
was installed as a client-monitoring query. A stock investigator could cause a
command to run repeatedly on selected endpoints-or across an organization-as
the Velociraptor agent account.
The vulnerability was fixed before 0.77.2 and is tracked as CVE-2026-19583.
At a glance
| Field | Detail |
|---|---|
| Tested version | Velociraptor 0.77.1, commit 3137c7f71 |
| Fixed version | 0.77.2 |
| Vulnerability class | Incorrect permission assignment, CWE-732 |
| Vendor affected range | Velociraptor on Linux before 0.77.2 |
| Required access | Stock investigator with COLLECT_CLIENT |
| Demonstrated impact | Recurring endpoint commands as the agent account |
| Researcher estimate | Critical; proposed CVSS 3.1 score 9.9 |
| Credits | Yuval Miller and Leon Kayaliev |
Normal collection versus monitoring
The shipped Linux.Sys.BashShell artifact requires EXECVE. An investigator
normally has permission to collect safe client artifacts, but not to execute
arbitrary commands. The ordinary collection path enforced that distinction.
Client monitoring follows a different path. It stores queries that endpoints run continuously and can target an entire organization or a labeled subset. That made a missing permission check particularly consequential: the bypass was persistent and could become fleet-wide.
Why I started with a negative control
The direct collection denial established that the role and artifact metadata
were configured correctly: the investigator really lacked EXECVE, and
Linux.Sys.BashShell really required it. I then changed only the dispatch path.
That made the result an authorization inconsistency rather than a debate about
the stock role definition.
I used a temporary endpoint label and recorded the existing monitoring table
before the test. SetClientMonitoringState replaces the complete table, so a
careless reproduction could disrupt legitimate monitoring. The label also
kept the proof to one disposable client even though the vulnerable primitive
could be applied organization-wide.
Reproduction
The actor had only the stock investigator role. Direct collection provided the negative control:
SELECT collect_client(
client_id='CLIENT_ID',
artifacts=['Linux.Sys.BashShell'])
FROM scope()
Velociraptor rejected this with an EXECVE permission error.
For a bounded demonstration, the disposable endpoint was assigned a temporary
label. The investigator then used set_client_monitoring() or the equivalent
SetClientMonitoringState API to install an ordinary client artifact in the
label-specific monitoring table:
{
"artifacts": {"artifacts": []},
"label_events": [{
"label": "esc",
"artifacts": {
"artifacts": ["Linux.Sys.BashShell"],
"specs": [{
"artifact": "Linux.Sys.BashShell",
"parameters": {"env": [
{"key": "Command", "value": "echo UNIQUE_MARKER; /usr/bin/id"},
{"key": "Stateful", "value": "N"}
]}
}]
}
}]
}
The update was accepted without an EXECVE denial or artifact-type error. The
endpoint began the monitoring query, and its results contained the unique
marker and real id output. The test agent intentionally ran as an ordinary
lab user; production agents commonly run as root on Linux or SYSTEM on
Windows.
Removing the label condition would have applied the monitoring query across the organization. The original monitoring table, label, and proof artifact were restored or removed after testing.
Root cause
Two controls were missing:
services/client_monitoring/client_monitoring.gocompiled monitoring artifacts withNullACLManager, whose access checks always succeeded. Required permissions declared by the artifact were therefore bypassed.- Only one helper verified that a monitoring artifact had the
CLIENT_EVENTtype. The shared state-setting service and API accepted an ordinaryCLIENTartifact supplied in a complete monitoring table.
The application enforced the security rule at one entry point rather than at the shared state-changing boundary.
Code path
The vulnerable path lives in
services/client_monitoring/client_monitoring.go.
compileArtifactCollectorArgs() constructed the launcher compiler with
NullACLManager. That object deliberately approves access checks, so artifact
requirements such as EXECVE had no relationship to the user modifying the
monitoring table.
compileState() applied that compiler to global and label-specific entries.
setClientMonitoringState() then cloned and stored the complete table without
requiring every submitted artifact to be a CLIENT_EVENT artifact or checking
the caller against each artifact’s declared permissions.
The direct and monitoring paths therefore reached the same behavior under different authority:
direct collection -> investigator ACL manager -> EXECVE denied
monitoring update -> NullACLManager -> EXECVE accepted
Because the API replaces the complete monitoring table, authorization must cover every artifact in the proposed state, not only the newly added row. This also explains an operational consequence of the fix: a user changing one entry must have the permissions required by existing entries they resubmit.
Detection notes
Audit changes to client-monitoring state and flag ordinary CLIENT artifacts
where only CLIENT_EVENT artifacts should appear. Any executable artifact-such
as Linux.Sys.BashShell-in monitoring state is high priority.
Correlate the modifying principal, its permissions at the time, the labels or organization scope, and subsequent monitoring results. A monitoring change followed by shell output or process creation across several endpoints is more meaningful than either signal alone.
Remediation
Upgrade to Velociraptor 0.77.2 or later. Monitoring changes should be compiled
with an ACL manager bound to the modifying principal. The shared
SetClientMonitoringState service should validate artifact type and the full
set of required permissions so every API and VQL entry point receives the same
enforcement.
The vendor fix in
c0c9dd60
replaces NullACLManager with NewServerACLManager(config, principal) and adds
a validation pass before storage. The new check rejects artifacts whose type
is not MODE_CLIENT_EVENT and calls the launcher access check for every
artifact. A rejected update is stopped before the table is cloned and
persisted.
Disclosure timeline
| Date | Event |
|---|---|
| 2026-08-05 | Vulnerability reported to Rapid7 |
| 2026-09-10 | Rapid7 published CVE-2026-19583 |