{"repro_id":"REPRO-2026-00325","version":6,"title":"Wazuh cluster DAPI deserialization of untrusted data — RCE via sort_casting builtin resolution (getattr(builtins, 'exec')) in result merging","repro_type":"security","status":"published","severity":"high","description":"Wazuh deserialization of untrusted data (CWE-502) leading to RCE on the cluster master node. Advisory GHSA-8c6v-7g3w-prrq (published 2026-08-07 by vikman90/Wazuh) is authoritative; the MITRE CVE record is still RESERVED.","root_cause":"## Summary\n\nCVE-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.\n\n## Impact\n\n- **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`.\n- **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`.\n- **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.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Code execution on the Wazuh cluster master via the TCP/1516 Fernet-encrypted cluster channel and DAPI result merging.\n- **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.\n- **Parity:** `full`\n- **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.\n\n## Root Cause\n\nThe root cause is unsafe deserialization and later use of trusted-as-code type names from a worker-provided JSON result object.\n\nIn the vulnerable commit `24609e140155d7fd2bddd4ebb045dbde5bea320f`:\n\n- `AffectedItemsWazuhResult.decode_json()` in `framework/wazuh/core/results.py` stores `obj['sort_casting']` directly into the result object.\n- During DAPI result merging, `AffectedItemsWazuhResult.__or__()` calls `merge(..., types=self.sort_casting)`.\n- `merge()` constructs casters using `getattr(builtins, type_)` without an allowlist.\n- `_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.\n\nThe product path that reaches this is:\n\n1. Master forwards a distributed request to the worker through `MasterHandler.execute(command=b'dapi_fwd', ...)`.\n2. 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.\n3. `dapi.py` parses the worker response with `json.loads(..., object_hook=c_common.as_wazuh_object)`.\n4. `as_wazuh_object()` calls `AffectedItemsWazuhResult.decode_json()`.\n5. With more than one node response, `dapi.py` merges results using `reduce(or_, response)`, triggering `results.py` sorting/casting.\n\nThe fix commit is:\n\n- `b29849f8abb08d78f257e6106b6111a8a1b0e621`\n\nThat 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`.\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh`.\n2. The script:\n   - Reuses the prepared Wazuh repository at `/pruva/project-cache/repo` when available.\n   - Resolves the fixed commit and vulnerable parent.\n   - Verifies the vulnerable revision still contains `getattr(builtins, type_)` and the fixed revision contains the new `sort_casting` allowlist.\n   - Creates worktrees for `24609e140155d7fd2bddd4ebb045dbde5bea320f` and `b29849f8abb08d78f257e6106b6111a8a1b0e621`.\n   - 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.\n   - 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.\n   - Runs two vulnerable attempts and two fixed attempts.\n3. Expected evidence:\n   - 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.\n   - Fixed attempts reach the same DAPI/object-hook path but reject `sort_casting='exec'` and do not create marker files.\n   - `bundle/repro/validation_verdict.json` reports `claim_outcome=confirmed`, `validated_surface=network_protocol`, `evidence_scope=production_path`, and `observed_impact_class=code_execution`.\n\n## Evidence\n\nPrimary runtime artifacts are listed and digest-bound in `bundle/repro/runtime_manifest.json`.\n\nKey files from the final successful run:\n\n- `bundle/logs/product_patch_check.log` — commit identity and patch absence/presence check.\n- `bundle/logs/product_vuln_1.log` and `bundle/logs/product_vuln_2.log` — vulnerable product-path attempts.\n- `bundle/logs/product_fixed_1.log` and `bundle/logs/product_fixed_2.log` — fixed negative-control attempts.\n- `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`.\n- `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`.\n- `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.\n- `bundle/repro/evil_worker_response.json` — the attacker-controlled worker JSON response containing `sort_casting=[\"exec\"]` and the payload in `affected_items[*].x`.\n\nRepresentative vulnerable evidence from the product logs:\n\n- `MASTER_LISTENING original_wazuh_cluster_tcp=127.0.0.1:1516 ... fernet_key_len=32`\n- `WORKER_HELLO_ACCEPTED response=b'Client worker01 added'`\n- `WORKER_GOT_DAPI_REQUEST request_id=... json_len=550 ...`\n- `WORKER_SEND_STRING_UPDATED malicious JSON stored in master MasterHandler.in_str`\n- `WORKER_DAPI_RES_ACKNOWLEDGED master accepted dapi_res and released pending DistributedAPI request`\n- `MASTER_DAPI_RESULT type=AffectedItemsWazuhResult ... '_sort_casting': ['exec'] ...`\n- `MARKER_CHECK ... present=True content='CVE-2026-44901-vuln-...'`\n\nRepresentative fixed evidence:\n\n- `MASTER_DAPI_RESULT type=WazuhInternalError ... \"Invalid sort_casting type 'exec'. Allowed types: bool, float, int, str\"`\n- `MARKER_CHECK ... present=False content=None`\n\nEnvironment details captured:\n\n- Repository URL: `https://github.com/wazuh/wazuh.git`\n- Vulnerable commit: `24609e140155d7fd2bddd4ebb045dbde5bea320f`\n- Fixed commit: `b29849f8abb08d78f257e6106b6111a8a1b0e621`\n- Entrypoint: Wazuh cluster TCP peer (`entrypoint_kind=\"tcp_peer\"`)\n- Runtime stack: Wazuh `MasterHandler`, Wazuh Fernet frame `Handler`, Wazuh `DistributedAPI.forward_request`, Wazuh `AffectedItemsWazuhResult.merge`, Python 3\n\n## Recommendations / Next Steps\n\n- Use the fixed implementation from `b29849f8abb08d78f257e6106b6111a8a1b0e621` or upgrade to Wazuh 4.14.6 or later.\n- Keep `sort_casting` validation both at deserialization time and at use time. Only safe, explicit caster names should be accepted.\n- Avoid resolving attacker-provided strings into arbitrary built-ins or callables.\n- Add regression tests that replay serialized worker `AffectedItemsWazuhResult` responses with invalid caster names such as `exec`, `eval`, `open`, and non-string/list values.\n- 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.\n\n## Additional Notes\n\n- The final `bundle/repro/reproduction_steps.sh` was executed twice consecutively and succeeded both times.\n- Each script run performs two vulnerable attempts and two fixed attempts with fresh per-process marker values.\n- 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.\n- 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/`.\n","cve_id":"CVE-2026-44901","cwe_id":"CWE-502 Deserialization of Untrusted Data","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44901","package":{"name":"wazuh/wazuh","ecosystem":"github","affected_versions":"wazuh-manager >= 4.0.0, < 4.14.6 (all cluster-mode deployments)"},"reproduced_at":"2026-08-23T15:38:24.746494+00:00","duration_secs":5005.0,"tool_calls":372,"handoffs":3,"total_cost_usd":19.464731,"agent_costs":{"judge":1.282399,"learning_policy":0.013629,"repro":11.994361,"support":0.067619,"vuln_variant":6.106723},"cost_breakdown":{"judge":{"gpt-5.5":1.187848,"gpt-5.5-2026-04-23":0.094551},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.013629},"repro":{"accounts/fireworks/models/kimi-k3":2.053041,"gpt-5.5":9.94132},"support":{"accounts/fireworks/models/kimi-k3":0.067619},"vuln_variant":{"gpt-5.5":6.106723}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-23T15:38:25.623226+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":9996,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":36489,"category":"reproduction_script"},{"path":"bundle/logs/master_fixed_1.log","filename":"master_fixed_1.log","size":721,"category":"log"},{"path":"bundle/logs/master_fixed_2.log","filename":"master_fixed_2.log","size":721,"category":"log"},{"path":"bundle/logs/master_vuln_1.log","filename":"master_vuln_1.log","size":931,"category":"log"},{"path":"bundle/logs/master_vuln_2.log","filename":"master_vuln_2.log","size":931,"category":"log"},{"path":"bundle/logs/product_fixed_2.log","filename":"product_fixed_2.log","size":7998,"category":"log"},{"path":"bundle/logs/product_patch_check.log","filename":"product_patch_check.log","size":1358,"category":"log"},{"path":"bundle/logs/worker_fixed_1.log","filename":"worker_fixed_1.log","size":357,"category":"log"},{"path":"bundle/logs/worker_fixed_2.log","filename":"worker_fixed_2.log","size":357,"category":"log"},{"path":"bundle/logs/worker_vuln_1.log","filename":"worker_vuln_1.log","size":357,"category":"log"},{"path":"bundle/logs/worker_vuln_2.log","filename":"worker_vuln_2.log","size":357,"category":"log"},{"path":"bundle/repro/evil_worker_response.json","filename":"evil_worker_response.json","size":584,"category":"other"},{"path":"bundle/repro/harness/master_harness.py","filename":"master_harness.py","size":5971,"category":"script"},{"path":"bundle/repro/harness/worker_peer.py","filename":"worker_peer.py","size":2693,"category":"script"},{"path":"bundle/repro/observations/product_fixed_1.json","filename":"product_fixed_1.json","size":1652,"category":"other"},{"path":"bundle/repro/observations/product_fixed_2.json","filename":"product_fixed_2.json","size":1652,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":3281,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":860,"category":"other"}]}