← 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.
CVSS v3.1 Base Metrics — Score 9.1
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredHigh
User InteractionNone
ScopeChanged
ConfidentialityHigh
IntegrityHigh
AvailabilityHigh
Affected & Patched Versions
- wazuh-manager >= 3.9.0
- wazuh-manager 4.14.8