# REPRO-2026-00325: Wazuh cluster DAPI deserialization of untrusted data — RCE via sort_casting builtin resolution (getattr(builtins, 'exec')) in result merging ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-502 Deserialization of Untrusted Data (Deserialization of Untrusted Data) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00325 CVE: CVE-2026-44901 ## Package Name: wazuh/wazuh Ecosystem: github Affected: wazuh-manager >= 4.0.0, < 4.14.6 (all cluster-mode deployments) Fixed: Unknown ## Root Cause ## Summary CVE-2026-44901 is a Wazuh cluster Distributed API (DAPI) deserialization vulnerability in which a malicious or compromised worker node can return a crafted serialized `AffectedItemsWazuhResult` to a master node. In vulnerable code, `AffectedItemsWazuhResult.decode_json()` accepts attacker-controlled `sort_casting` values and `merge()` resolves those values with `getattr(builtins, type_)`. A worker response containing `sort_casting=["exec"]` therefore makes the master call Python `exec()` on attacker-controlled item data while merging results from multiple nodes. ## Impact - **Affected package/component:** Wazuh manager cluster framework, specifically `framework/wazuh/core/results.py` as reached through `framework/wazuh/core/cluster/dapi/dapi.py` and the cluster TCP channel handled by `wazuh.core.cluster.master.MasterHandler` / `wazuh.core.cluster.common.Handler`. - **Affected versions:** Wazuh manager cluster deployments before the fix commit `b29849f8abb08d78f257e6106b6111a8a1b0e621` (reported as fixed in 4.14.6 and later). The reproduced vulnerable revision is the fixed commit parent: `24609e140155d7fd2bddd4ebb045dbde5bea320f`. - **Risk level and consequences:** High. A malicious/compromised worker node, or an attacker with the shared cluster key able to act as a worker on the cluster channel, can cause code/command execution in the master-side Wazuh process when a distributed API response is merged. ## Impact Parity - **Disclosed/claimed maximum impact:** Code execution on the Wazuh cluster master via the TCP/1516 Fernet-encrypted cluster channel and DAPI result merging. - **Reproduced impact from this run:** Code execution/command execution on the master-side Wazuh process. The payload executed via `exec()` and wrote unique marker files during both vulnerable attempts. - **Parity:** `full` - **Not demonstrated:** No additional privilege escalation beyond the privileges of the reproduced master-side process was claimed or required for this proof. The proof uses a minimally configured product cluster runtime rather than full Dockerized Wazuh service containers because Docker is unavailable in this environment, but it exercises the original Wazuh cluster TCP/Fernet framing, `MasterHandler`, `DistributedAPI.forward_request`, `json.loads(..., object_hook=as_wazuh_object)`, and `results.py` merge sink. ## Root Cause The root cause is unsafe deserialization and later use of trusted-as-code type names from a worker-provided JSON result object. In the vulnerable commit `24609e140155d7fd2bddd4ebb045dbde5bea320f`: - `AffectedItemsWazuhResult.decode_json()` in `framework/wazuh/core/results.py` stores `obj['sort_casting']` directly into the result object. - During DAPI result merging, `AffectedItemsWazuhResult.__or__()` calls `merge(..., types=self.sort_casting)`. - `merge()` constructs casters using `getattr(builtins, type_)` without an allowlist. - `_goes_before_than()` applies each caster to sort values. If the worker supplied `sort_casting=["exec"]`, the caster becomes Python built-in `exec`, and the corresponding item value is executed as Python source. The product path that reaches this is: 1. Master forwards a distributed request to the worker through `MasterHandler.execute(command=b'dapi_fwd', ...)`. 2. The worker response is delivered through the original Wazuh cluster string protocol (`new_str`, `str_upd`, `dapi_res`) over the Fernet-encrypted cluster TCP channel. 3. `dapi.py` parses the worker response with `json.loads(..., object_hook=c_common.as_wazuh_object)`. 4. `as_wazuh_object()` calls `AffectedItemsWazuhResult.decode_json()`. 5. With more than one node response, `dapi.py` merges results using `reduce(or_, response)`, triggering `results.py` sorting/casting. The fix commit is: - `b29849f8abb08d78f257e6106b6111a8a1b0e621` That commit adds validation in `decode_json()` and an explicit `ALLOWED_CASTERS` map in `merge()`, allowing only `int`, `float`, `str`, and `bool`. The same malicious `sort_casting=["exec"]` response is rejected with `WazuhInternalError: Invalid sort_casting type 'exec'. Allowed types: bool, float, int, str`. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. 2. The script: - Reuses the prepared Wazuh repository at `/pruva/project-cache/repo` when available. - Resolves the fixed commit and vulnerable parent. - Verifies the vulnerable revision still contains `getattr(builtins, type_)` and the fixed revision contains the new `sort_casting` allowlist. - Creates worktrees for `24609e140155d7fd2bddd4ebb045dbde5bea320f` and `b29849f8abb08d78f257e6106b6111a8a1b0e621`. - Starts a minimally configured real Wazuh master cluster TCP listener using `wazuh.core.cluster.master.MasterHandler` and `wazuh.core.cluster.common.Handler` with Fernet enabled. It binds to `127.0.0.1:1516` when available. - Connects a malicious worker peer over the original Wazuh cluster frame format, completes the encrypted `hello` handshake, receives the master `b'dapi'` forwarded request, and returns the crafted JSON through the original `new_str` / `str_upd` / `dapi_res` sequence. - Runs two vulnerable attempts and two fixed attempts. 3. Expected evidence: - Vulnerable attempts produce `repro/markers/product_marker_vuln_1.txt` and `repro/markers/product_marker_vuln_2.txt`, each containing the unique marker value selected for that process. - Fixed attempts reach the same DAPI/object-hook path but reject `sort_casting='exec'` and do not create marker files. - `bundle/repro/validation_verdict.json` reports `claim_outcome=confirmed`, `validated_surface=network_protocol`, `evidence_scope=production_path`, and `observed_impact_class=code_execution`. ## Evidence Primary runtime artifacts are listed and digest-bound in `bundle/repro/runtime_manifest.json`. Key files from the final successful run: - `bundle/logs/product_patch_check.log` — commit identity and patch absence/presence check. - `bundle/logs/product_vuln_1.log` and `bundle/logs/product_vuln_2.log` — vulnerable product-path attempts. - `bundle/logs/product_fixed_1.log` and `bundle/logs/product_fixed_2.log` — fixed negative-control attempts. - `bundle/repro/observations/product_vuln_1.json` and `bundle/repro/observations/product_vuln_2.json` — structured observations showing `fernet_enabled=true`, `master_listened=true`, `dapi_forward_request_received_by_worker=true`, `worker_response_delivered_via_send_string=true`, `dapi_json_object_hook_path_reached=true`, and `marker_present=true`. - `bundle/repro/observations/product_fixed_1.json` and `bundle/repro/observations/product_fixed_2.json` — structured observations showing the same network/DAPI path was reached but `sort_casting_rejected=true` and `marker_present=false`. - `bundle/repro/markers/product_marker_vuln_1.txt` and `bundle/repro/markers/product_marker_vuln_2.txt` — command-execution markers written by the vulnerable master-side process. - `bundle/repro/evil_worker_response.json` — the attacker-controlled worker JSON response containing `sort_casting=["exec"]` and the payload in `affected_items[*].x`. Representative vulnerable evidence from the product logs: - `MASTER_LISTENING original_wazuh_cluster_tcp=127.0.0.1:1516 ... fernet_key_len=32` - `WORKER_HELLO_ACCEPTED response=b'Client worker01 added'` - `WORKER_GOT_DAPI_REQUEST request_id=... json_len=550 ...` - `WORKER_SEND_STRING_UPDATED malicious JSON stored in master MasterHandler.in_str` - `WORKER_DAPI_RES_ACKNOWLEDGED master accepted dapi_res and released pending DistributedAPI request` - `MASTER_DAPI_RESULT type=AffectedItemsWazuhResult ... '_sort_casting': ['exec'] ...` - `MARKER_CHECK ... present=True content='CVE-2026-44901-vuln-...'` Representative fixed evidence: - `MASTER_DAPI_RESULT type=WazuhInternalError ... "Invalid sort_casting type 'exec'. Allowed types: bool, float, int, str"` - `MARKER_CHECK ... present=False content=None` Environment details captured: - Repository URL: `https://github.com/wazuh/wazuh.git` - Vulnerable commit: `24609e140155d7fd2bddd4ebb045dbde5bea320f` - Fixed commit: `b29849f8abb08d78f257e6106b6111a8a1b0e621` - Entrypoint: Wazuh cluster TCP peer (`entrypoint_kind="tcp_peer"`) - Runtime stack: Wazuh `MasterHandler`, Wazuh Fernet frame `Handler`, Wazuh `DistributedAPI.forward_request`, Wazuh `AffectedItemsWazuhResult.merge`, Python 3 ## Recommendations / Next Steps - Use the fixed implementation from `b29849f8abb08d78f257e6106b6111a8a1b0e621` or upgrade to Wazuh 4.14.6 or later. - Keep `sort_casting` validation both at deserialization time and at use time. Only safe, explicit caster names should be accepted. - Avoid resolving attacker-provided strings into arbitrary built-ins or callables. - Add regression tests that replay serialized worker `AffectedItemsWazuhResult` responses with invalid caster names such as `exec`, `eval`, `open`, and non-string/list values. - Consider hardening the DAPI worker-response trust boundary: even authenticated cluster workers should not be able to deserialize data structures that influence executable behavior on the master. ## Additional Notes - The final `bundle/repro/reproduction_steps.sh` was executed twice consecutively and succeeded both times. - Each script run performs two vulnerable attempts and two fixed attempts with fresh per-process marker values. - Docker was not available in this environment, so the proof uses a minimally configured local Wazuh cluster runtime from the real source tree rather than Wazuh container images. It does not reimplement the vulnerable sink or the cluster frame protocol; it imports and executes the real Wazuh modules for the master TCP listener, Fernet framing, DAPI forwarding, JSON object hook, and result merging. - The reproduction is self-contained: it creates runtime helper code and payload files at execution time, installs Python dependencies into `bundle/repro/venv` if needed, and writes all proof diagnostics under `bundle/logs/` and `bundle/repro/`. ## Reproduction Details Reproduced: 2026-08-23T15:38:24.746Z Duration: 5005 seconds Tool calls: 372 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00325 pruva-verify CVE-2026-44901 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00325&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00325/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-44901 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-44901 ## Artifacts - bundle/repro/rca_report.md (analysis, 9996 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 36489 bytes) - bundle/logs/master_fixed_1.log (log, 721 bytes) - bundle/logs/master_fixed_2.log (log, 721 bytes) - bundle/logs/master_vuln_1.log (log, 931 bytes) - bundle/logs/master_vuln_2.log (log, 931 bytes) - bundle/logs/product_fixed_2.log (log, 7998 bytes) - bundle/logs/product_patch_check.log (log, 1358 bytes) - bundle/logs/worker_fixed_1.log (log, 357 bytes) - bundle/logs/worker_fixed_2.log (log, 357 bytes) - bundle/logs/worker_vuln_1.log (log, 357 bytes) - bundle/logs/worker_vuln_2.log (log, 357 bytes) - bundle/repro/evil_worker_response.json (other, 584 bytes) - bundle/repro/harness/master_harness.py (script, 5971 bytes) - bundle/repro/harness/worker_peer.py (script, 2693 bytes) - bundle/repro/observations/product_fixed_1.json (other, 1652 bytes) - bundle/repro/observations/product_fixed_2.json (other, 1652 bytes) - bundle/repro/runtime_manifest.json (other, 3281 bytes) - bundle/repro/validation_verdict.json (other, 860 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00325 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00325/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00325 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev