~11 min read
From Guardian to Gateway: The Hidden Risks of EDR Vulnerabilities
Have you ever wondered how security software looks like through the eyes of an attacker? While these tools are designed to protect your organization, they can also be prime targets for exploitation. Remote management and EDR (Endpoint Detection and Response) software, in particular, are enticing targets for cybercriminals. With functionalities that mirror command-and-control frameworks, these tools can become critical targets in their own right, offering attackers an unexpected foothold into your network.
In this blog post, we take a closer look at the security software Wazuh, where we identified two critical vulnerabilities:
- CVE-2024-32038: A heap buffer overflow that could allow unauthenticated attackers to execute arbitrary commands on the Wazuh server
- CVE-2023-50260: A command injection vulnerability enabling attackers to elevate permissions and move laterally
Together, these vulnerabilities can be chained in an attack to move from initial access to full network compromise. With this, we will illustrate how a defensive tool can become an attack vector.
The Target: Wazuh
Wazuh is an open-source security platform (EDR/XDR) that provides features like endpoint protection, log collection, log analysis, and malware detection.
I chose it as a research target for the following reasons:
- It’s widely used and has been adopted by several large companies
- It’s well maintained, so bugs are more likely to be fixed (which I like)
- Most parts are written in C, which makes it vulnerable to memory safety issues
- The feature set and codebase are quite large and complex
- Depending on the component, the impact can be critical
Wazuh consists of several components: The Wazuh agents monitor the systems on which they are installed. The agents send data, such as log files and events, via TCP to a Wazuh server. The Wazuh server adds the agents’ identifier and passes the received data to the analysis engine. The analysis engine triages the logs and events and triggers actions or alerts, whenever necessary.
Communication between the agent and the server is encrypted and can be authenticated. Authentication, however, is not required and needs to be explicitly enabled and enforced. The analysis engine supports multiple message types, including JSON, XML and plain text, each handled by its own decoder. When triaging, the analysis engine decodes each message and matches them against a pre-defined set of rules.
Since parsing and processing various data formats may be a very complicated process, I thought the analysis engine might be an interesting target…
Fuzzing Setup
I always wanted to get some hands-on experience with fuzzing and fuzz testing just seemed to be the ideal tool here: I can use it to automatically generate many different inputs to stress-test how Wazuh handles unexpected conditions (e.g., weird or incorrect formats). While the fuzzing setup details could be a post on their own, the following section provides an overview, with more in-depth exploration to come in some future blog post.
Targeting the analysis engine presented some challenges, resulting in a rather complex fuzzing setup:
- Launching the target application is slow as it loads a lot of configurations and rules, resulting in a complex state.
- The server uses file-based message queues for communication between processes and threads.
- The analysis engine is highly multi-threaded.
- The message format is complex and required some grammar to reach interesting code paths.
For my final setup, with which I could eventually trigger the bugs, I had to patch the target application several times and implement some custom fuzzing logic. I used LibAFL with QEMU userspace emulation and Nautilus for mutation. However, due to multi-threading and the file-based message queues, the userspace emulation turned out to be suboptimal.
By the way, we are currently working on some tools to make snapshot-based fuzzing with LibAFL and Nyx easier and more approachable. Stay tuned, follow us on Twitter and make sure to check out this Blog from time to time.
Vulnerabilities
My fuzzing setup triggered several crashes. While triaging these crashes, I discovered several NULL pointer dereferences (CVE-2023-49275) leading to DoS, along with a heap buffer overflow vulnerability (CVE-2024-32038) that could be used for unauthenticated remote code execution on the Wazuh management server. I also discovered a command injection vulnerability (CVE-2023-50260), while manually auditing the source code.
In the following sections, we take a closer look at the heap buffer overflow and the command injection vulnerabilities.
Heap Buffer Overflow
Let’s first have a look at how the analysis engine handles messages and processes events, in particular for Windows events (that’s relevant because the bug I found was in the Windows events decoder).
Whenever the analysis engine receives a Windows event from an agent, it invokes the Windows decoder.
If Windows event messages contain unprintable characters, the winevent
decoder escapes those unprintable characters by adding the escape sequence \u00
.
For example, the byte with the value 0x04
is escaped to the string \u0004
.
The decoder applies this procedure to all unprintable characters in a message.
Then, the escaped string is copied back into the message object without adjusting the buffer size.
And, there you go.
// https://github.com/wazuh/wazuh/blob/dd354194d55f4ad116631cadf5399808a8034613/src/analysisd/decoders/winevtchannel.c#L714
returned_event = cJSON_PrintUnformatted(final_event);
if (returned_event){
lf->full_log[strlen(returned_event)] = '\0';
memcpy(lf->full_log, returned_event, strlen(returned_event));
} else {
lf->full_log = NULL;
}
If the escaped string returned_event
exceeds the size of the heap buffer lf->full_log
, then copying it there will overflow the buffer.
Let’s recall that most parts of final_event
, the original message that gets escaped, is controlled by the sender.
Thus, the sender can control the number of bytes in the escaped message by adding unprintable characters.
Further, they can control the content overflowing the buffer, which leaves any attacker with a pretty strong exploit primitive.
For a proof of concept, I built a custom agent that enrolls with the server to then send a Windows event message to crash the server.
While writing the report, I realized that the analysis engine runs in the context of the unprivileged user wazuh
.
Through the exploitation of the vulnerability I just described, we could have unauthenticated code execution on the server, but only in the context of this unprivileged user.
This left me feeling somewhat frustrated, as I thought that “unauthenticated RCE as root” would sound much better on a report. So I decided to spend a couple more days looking for a privilege escalation vulnerability that could be chained with the unauthenticated code execution.
Command Injection
As I had already spent quite a bit of time reading and patching the source code, I had a pretty good overview. So I started to review some code manually. Hoping that it could also provide us with some primitives to compromise clients, I focused on the active response feature.
Active Responses
Active responses are commands that are executed on clients in response to events.
They can be triggered manually via the management interface or automatically based on the configured rule set.
By default, active responses are limited to a set of pre-defined executables and scripts.
On Linux, only the executables stored in /var/ossec/active-response/bin
are allowed to be executed.
Unlike other security solutions, Wazuh does not come with any kind of “RCE as a service” in the form of a response shell (or similar). Therefore, even if the Wazuh server is compromised, an attacker cannot directly execute arbitrary commands on clients. That’s the idea.
However, I found a way to circumvent this protection:
By exploiting a command injection vulnerability in the host_deny
active response, I could bypass the above restrictions to execute arbitrary commands in the context of the execution process running as root
.
Back to the 90’s
The host-deny
active response is used to block connections from specific IP addresses by appending the address to the file /etc/hosts.deny
.
It utilizes TCP wrappers, a rather obsolete technology used before firewalls were a thing, to define network ACLs applied at the application layer.
The rules defined in /etc/hosts.deny
are applied by services that include the wrapper library libwrap
.
Each rule defines a service name and source IP address to which the rule should be applied.
For custom logging, rules in host.deny
can contain shell commands.
When using the spawn
directive, a command gets executed every time the rule triggers.
The host-deny
active response takes a srcip
(passed as part of an event) and adds the line ALL:[srcip]
to /etc/hosts.deny
.
The content of srcip
is not sanitized sufficiently as the only check is whether it contains .
or :
.
if (!strstr(srcip, ".") && !strstr(srcip, ":")) {
memset(log_msg, '\0', OS_MAXSTR);
snprintf(log_msg, OS_MAXSTR -1, "Unable to run active response (invalid IP: '%s')", srcip);
write_debug_file(argv[0], log_msg);
cJSON_Delete(input_json);
return OS_INVALID;
}
Hence, it is possible to inject shell commands into /etc/hosts.deny
via the srcip
parameter.
For example, srcip = "ALL : spawn /usr/bin/whoami >> /tmp/PoC"
adds the new line ALL:ALL : spawn /usr/bin/whoami >> /tmp/PoC
to the file /etc/hosts.deny
.
Then, whenever a service that includes libwrap
receives a connection, the command spawn /usr/bin/whoami >> /tmp/PoC
gets executed.
Although TCP wrappers are obsolete and mostly unused, some common tools such as openssh-server
still support them.
Becoming Root
Let’s remember: Wazuh uses file-based socket queues for inter-process communication. This also applies to the active response component. Whenever an event triggers an active response, the response is either executed locally on the server or forwarded to a remote agent, depending on the event’s origin.
If a local event triggers an active response, the analysis engine writes the active response event directly to its executor queue.
As the analysis engine runs in the context of the user wazuh
, the queue has to be writable by the unprivileged user.
The executor process, that runs as root
, reads from the queue, parses the event and executes the response.
Hence, an unprivileged user can trigger active responses by writing to the executor queue.
By combining this with the command injection in host_deny
, it is possible to execute arbitrary commands in the context of root
.
Own All The Things
In the case of events originating from remote agents, the active response is written to another queue that is used to forward active responses via the remote daemon.
By writing to this queue, it is possible to send active response events to any agent.
If the agent runs on a Linux host that uses TCP wrappers in some way (e.g., via an openssh server), the same command injection described above can be used to execute arbitrary commands in the context of root
on remote agents.
Potential Impact
An unauthenticated attacker who can reach the management server could potentially exploit the heap buffer overflow vulnerability (CVE-2024-32038) to gain arbitrary code execution on the management server in the context of the unprivileged user wazuh
.
By combining this with the command injection (CVE-2023-50260) in host_deny
, it is possible to execute arbitrary commands in the context of root
.
From there, by exploiting the command injection vulnerability again, the attacker can execute arbitrary commands on remote Linux agents.
Recommendation
To mitigate those specific vulnerabilities, we recommend upgrading both Wazuh agents as well as the management server to the latest version.
Both vulnerabilities described were fixed in release 4.7.2
.
Furthermore, to minimize the risk of similar attacks, we recommend enabling the following additional security options:
- Manager identity verification: to make sure that agents do not connect to malicious management servers.
- Agent identity verification: to prevent arbitrary agents from connecting and potentially attacking the management server.
Disclosure Process
The NULL pointer dereference vulnerabilities I discovered through fuzzing, I disclosed directly to Wazuh via a GitHub security advisory.
These issues were fixed in version 4.7.1
.
Both the heap buffer overflow and the command injection vulnerabilities were disclosed via ZDI:
- https://www.zerodayinitiative.com/advisories/ZDI-24-397/
- https://www.zerodayinitiative.com/advisories/ZDI-24-398/
These issues were fixed in version 4.7.2
.
Wazuh has published security advisories on GitHub containing all the relevant information:
- https://github.com/wazuh/wazuh/security/advisories/GHSA-fcpw-v3pg-c327
- https://github.com/wazuh/wazuh/security/advisories/GHSA-mjq2-xf8g-68vw
- https://github.com/wazuh/wazuh/security/advisories/GHSA-4mq7-w9r6-9975
Conclusion
This exploration of Wazuh demonstrates how security tools — often the first line of defense — can themselves become high-value targets for attackers. As shown through our fuzz-testing approach with LibAFL, vulnerabilities in tools like Wazuh offer attackers an unexpected entry point with access often comparable to command and control frameworks. These findings highlight a critical aspect of cybersecurity: the software we trust to protect our systems requires as much scrutiny and hardening as the systems it guards.
It’s important to note that this issue is not unique to Wazuh; many EDR and remote management tools likely face similar risks due to their inherent system access and control capabilities. While these tools are essential for defending against a wide array of threats, it remains crucial to secure them rigorously and to be aware of their potential as targets themselves.