CVE Watchtower

← Back to CVE List

CVE-2026-74040NVD

Vulnerability Summary

Summary `MasterHandler.process_files_from_worker` (`framework/wazuh/core/cluster/master.py`) lets a cluster worker drive the master into overwriting arbitrary master files, including the master's own `ossec.conf`. Two independent defects allow it: 1. Trailing-slash `basename` bypass of the #36296 blocklist. The guard checks `os.path.basename(file_path)` on the raw metadata key, but the write resolves through `safe_join()` → `os.path.normpath()`, which strips a trailing slash. `os.path.basename("etc/ossec.conf/") == ""`, so the `excluded_files` (`['ar.conf','ossec.conf']`) and `client.keys` guards never fire — yet the target still normalises to the real `/var/ossec/etc/ossec.conf`. 2. Missing direction (`extra_valid`) enforcement. The sink only checks that `item_key` exists in `cluster_items['files']` — never that a worker is permitted to send that file. Every entry in `cluster.json` is `source: master, extra_valid: False`, so a worker can push master-sourced files (`agent.conf`, rules, decoders, lists) with no bypass at all. Because `wazuh-modulesd` runs as root, overwriting `ossec.conf` with an injected `<wodle name="command">` yields root RCE on the master on the next config reload. Overwriting `etc/shared/<group>/agent.conf` — which the master merges and pushes to every enrolled agent — turns one worker compromise into fleet-wide root RCE, plus active-response injection (`ar.conf`), detection evasion (rules/decoders/lists), and agent-auth tampering (`client.keys`). Why this is a new issue (not a duplicate of CVE-2026-25770 / CVE-2026-30893) This sink has a history of arbitrary-write→root-RCE reports; this one defeats the latest control and uses a different primitive: - CVE-2026-25770 (GHSA-r4f7-v3p6-79jm, fixed 4.14.3) was in `receive_file` (`common.py`) — a raw `open(WAZUH_PATH + data, 'wb')` at the transport layer — mitigated partly by hardening that path and recommending `chmod 640 root:wazuh` on `ossec.conf`. This report is not that fix bypassed. It is a different function (`process_files_from_worker` → `safe_move`, a later processing stage), defeating a different, later control: the #36296 `excluded_files` blocklist added in 4.14.6, which did not exist when 25770 was fixed. - Different write primitive — and 25770's mitigation does not stop it. 25770 used `open(ossec.conf, 'wb')`, which needs write on the file — exactly what `640 root:wazuh` removes. This report uses `safe_move`, which writes `.ossec.conf.tmp` in `/var/ossec/etc` and then `os.rename`s it over `ossec.conf` (its docstring: "Write permissions are required on target directory"). Rename-over needs directory write, not file write, so the `640`-on-`ossec.conf` mitigation from 25770 does not block this path. - CVE-2026-30893 (fixed 4.14.4) was raw traversal in `decompress_files()`; `safe_join` was its fix. This report is in the downstream sink, not `decompress_files`. - The `extra_valid` defect is entirely outside 25770/30893. 25770 was only about `ossec.conf` on the master. Pushing `agent.conf` → every agent (fleet-wide RCE) and rules/decoders/lists → detection evasion needs no bypass and hits directories the `wazuh` user writes by design during normal sync — so it isn't gated by any of the permission questions above. Requested scoping: new issue / incomplete fix of #36296, referencing CVE-2026-25770 as the prior advisory establishing the `ossec.conf`→root-RCE impact class. Affected component & precondition - Sink: `framework/wazuh/core/cluster/master.py :: MasterHandler.process_files_from_worker` (non-merged branch). - Precondition: a cluster (master + ≥1 worker) where the attacker controls a worker (compromised worker host, malicious insider, or a worker in a lower-trust segment) and therefore holds the 32-char shared cluster key. No API credentials and no root on the master are required. Root cause `process_files_from_worker`, non-merged branch (verbatim, `v4.14.6`): ```python for file_path, data in files_metadata.items(): full_path = safe_join(common.WAZUH_PATH, file_path) # normpath() strips a trailing slash item_key = data['cluster_item_key'] if item_key not in cluster_items['files']: # membership only — direction NOT enforced raise exception.WazuhClusterError(3022, ...) if os.path.basename(file_path) == 'client.keys': # basename of the RAW key raise exception.WazuhClusterError(3007) else: # non-merged expected_base = safe_join(common.WAZUH_PATH, item_key) if not os.path.commonpath([full_path, expected_base]).startswith(expected_base): raise exception.WazuhClusterError(3022, ...) # containment passes for etc/ossec.conf file_basename = os.path.basename(file_path) # #36296 guard, on the RAW key if file_basename in cluster_items['files'].get('excluded_files', []): # ['ar.conf','ossec.conf'] raise exception.WazuhClusterError(3022, ...) # NEVER fires: basename('etc/ossec.conf/')=='' zip_path = safe_join(decompressed_files_path, file_path) utils.safe_move(zip_path, full_path, ...) # -> overwrites /var/ossec/etc/ossec.conf ``` `safe_join` (`framework/wazuh/core/cluster/utils.py`) canonicalises with `normpath`, which strips the very slash the guard relied on: ```python safe_paths = [p.lstrip(os.sep).lstrip("/") for p in paths] # leading separators only; trailing '/' kept final_path = os.path.normpath(os.path.join(base, *safe_paths)) # normpath strips the trailing '/' ``` Trace for a worker-supplied key `"etc/ossec.conf/"` with `cluster_item_key: "etc/"`: | Step | Value | Result | |---|---|---| | `os.path.basename("etc/ossec.conf/")` | `""` | `"" != "client.keys"` and `"" not in ['ar.conf','ossec.conf']` → both guards pass | | `safe_join(WAZUH_PATH, "etc/ossec.conf/")` | `/var/ossec/etc/ossec.conf` | normpath strips the slash → the real protected file | | containment vs `safe_join(WAZUH_PATH,"etc/")` = `/var/ossec/etc` | — | `commonpath` starts with it → passes | | `safe_move(...)` | — | `/var/ossec/etc/ossec.conf` overwritten | The archive content member is keyed without the slash (`etc/ossec.conf`), so it decompresses to `<decompressed>/etc/ossec.conf`, exactly where `zip_path` looks. Proof of concept Validated on stock `wazuh/wazuh-manager:4.14.6` against the real `process_files_from_worker` sink and a real filesystem (video + scripts attached). Chain: 1. Before: pristine master — `ossec.conf` sha256 recorded, `/etc/shadow` is `640 root:shadow`, no proof file. 2. Payload (`files_metadata`, stored as `files_metadata.json`): ```json { "etc/ossec.conf/": { "merged": false, "cluster_item_key": "etc/" } } ``` Content member `etc/ossec.conf` = a valid config with an injected wodle: ```xml <wodle name="command"> <disabled>no</disabled><tag>worker_rce_shadow</tag> <command>/bin/cp /etc/shadow /tmp/PWNED_ROOT_SHADOW</command> <run_on_start>yes</run_on_start><interval>1d</interval> <ignore_output>yes</ignore_output><timeout>0</timeout> </wodle> ``` 3. Exploit: sink accepts the trailing-slash key — `SINK errors: {}`, `ossec.conf overwritten: True`. 4. Negative control (isolates the bug): the identical request with key `etc/ossec.conf` (no slash) is correctly rejected — `Error 3022 - File is in excluded list: etc/ossec.conf`. 5. Trigger → root RCE: on manager restart, `wazuh-modulesd` (root) loads the wodle: ``` wazuh-modulesd:command: INFO: Module command:worker_rce_shadow started ``` Proof: `-r-------- 1 root root 365 /tmp/PWNED_ROOT_SHADOW` containing root-only `/etc/shadow` — i.e. code executed as UID 0. 6. Blast radius: `agent.conf`, `ar.conf` (via slash), and master ruleset all overwritten; `ar.conf` no-slash blocked — confirming `agent.conf`/rules need no bypass (missing `extra_valid`/source enforcement). Remote reachability: the over-the-wire path is demonstrated separately on a 2-node cluster where an unmodified worker ships the archive with its real `send_request(b'syn_e_w_m')` / `send_file` calls over TCP/1516 and the master's unmodified `process_files_from_worker` accepts it (transcript attached). Impact / blast radius | Target (`cluster_item_key`) | Bypass needed | Result | |---|---|---| | `etc/ossec.conf` (`etc/`) | trailing slash | Root RCE on the master (injected command wodle; `wazuh-modulesd` runs as root) | | `etc/shared/<group>/agent.conf` (`etc/shared/`) | none | Master merges & pushes → root RCE on every agent | | `etc/shared/ar.conf` (`etc/shared/`) | trailing slash | Active-response command injection | | `etc/rules` · `etc/decoders` · `etc/lists` | none | Overwrite master detection content → detection/alert evasion | | `etc/client.keys` (`etc/`) | trailing slash | Agent-auth tampering / rogue enrollment / DoS | On the full stack, the injected root command additionally reads `/etc/filebeat/filebeat.yml`, disclosing the wazuh-indexer admin credentials (also the dashboard login) — one worker compromise → full control of manager, indexer, and console. Remediation 1. Canonicalise before checking. Compute `file_path = os.path.normpath(file_path)` (and reject any key whose normalised basename differs from the raw basename) before the `client.keys` and `excluded_files` guards. 2. Enforce direction of travel. In the worker→master (extra-valid) path, reject any file whose `cluster_items['files'][item_key]['extra_valid']` is `False` — a worker must not be able to push master-sourced files (`agent.conf`, rules, decoders, lists) to the master. 3. Defense in depth: validate the reconstructed `full_path`'s basename (not the raw key's) against the blocklist. References - Prior advisory establishing this impact class: GHSA-r4f7-v3p6-79jm / CVE-2026-25770 (`ossec.conf` overwrite via `receive_file` → root RCE, fixed 4.14.3). - CVE-2026-30893 (`decompress_files` traversal, fixed 4.14.4). - Control being bypassed: #36296 ("Improved cluster non-merged file path validation during worker file processing", 4.14.6); related 4.14.6 cluster path-traversal hardening: #36204, #36246, #36460, #36060. Evidence attachments - `F3-worker-to-master-root-rce.mp4` — end-to-end sink → root RCE → blast radius, with negative control. - `cluster-e2e-transcript.txt` — 2-node over-the-wire `syn_e_w_m` run. - `laneB_harness.py`, `laneB_blast.py`, `cluster_e2e.sh` — PoC scripts.
Severity Level
CRITICAL(9.1)
Published Date
Oct 13, 2026
Last Modified
Oct 13, 2026
Exploitation Status
No confirmed exploitation yet
CVE Record Status
Reserved
This CVE ID is referenced in a public advisory, but the CVE Program has not published its official CVE Record yet. Full CVSS/CPE data from NVD will appear here once it does.
EPSS Score (30-Day)
Data Pending
Root Weakness (CWE)
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory.
The software constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended syntax or behavior.
Refer to the official MITRE database for detailed architectural specifications regarding this weakness.
Refer to the official MITRE database for detailed architectural specifications regarding this weakness.
CVSS v3.1 Base Metrics — Score 9.1
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredHigh
User InteractionNone
ScopeChanged
ConfidentialityHigh
IntegrityHigh
AvailabilityHigh

Affected & Patched Versions

Affected Versions
  • wazuh-manager >= 3.9.0
Patched Versions
  • wazuh-manager 4.14.8
🎁7-Day Free Trial — Try Pro or Team, no card required.
📧Email Delivery — Threat intel straight to your inbox.
♾️Unlimited Vendors — Track your entire stack.
🚨All New CVEs — Be the first to know.
📈EPSS Spike Alerts — Catch rising risk before it peaks.
🎯Custom EPSS/CVSS — Filter noise, focus on risk.
🛡️Exploit Intel — A 2nd confirmed-exploit signal beyond KEV.
🐙GitHub Issues — Auto-tracked, no duplicates.
📬Weekly Digest — One clean summary, not inbox spam.
🏷️Watchlist Groups — Tag alerts by team (Infra/AppSec/SOC).
💬Webhooks — Slack & Teams integration.
🔀Smart Routing — Critical alerts to one channel, rest to another.
🚫Ad-Free — Uninterrupted experience.
🎁7-Day Free Trial — Try Pro or Team, no card required.
📧Email Delivery — Threat intel straight to your inbox.
♾️Unlimited Vendors — Track your entire stack.
🚨All New CVEs — Be the first to know.
📈EPSS Spike Alerts — Catch rising risk before it peaks.
🎯Custom EPSS/CVSS — Filter noise, focus on risk.
🛡️Exploit Intel — A 2nd confirmed-exploit signal beyond KEV.
🐙GitHub Issues — Auto-tracked, no duplicates.
📬Weekly Digest — One clean summary, not inbox spam.
🏷️Watchlist Groups — Tag alerts by team (Infra/AppSec/SOC).
💬Webhooks — Slack & Teams integration.
🔀Smart Routing — Critical alerts to one channel, rest to another.
🚫Ad-Free — Uninterrupted experience.