CVE 2021-26814: from path-traversal to hero on Wazuh
Table of contents
- A brief introduction
- Wazuh and the API Service
- Finding and exploiting the vulnerability
- Patch time!
- Conclusions
- Timeline
- References
A brief introduction
Greetings! This post will introduce you to my latest finding,CVE-2021-26814, and how it was found, exploited and fixed after reporting it.
In order to offer a wide range of quality services, every product in CYS4 portfolio is deeply analyzed in different areas including the security perimeter. One of such solutions is Wazuh. For those who does not know about it, here it follows have a small description for their website:
Wazuh is a free, open source and enterprise-ready security monitoring solution for threat detection, integrity monitoring, incident response and compliance.
This solution has been adopted through the years by large enterprise companies: after a rigorous examination, we decided to offer Wazuh to our clients, with the aim to comply with the requirements imposed of the ECB (European Central Bank).
As a core feature of its architecture, the Wazuh RESTful API service provides an interface to manage and monitor the configuration of the manager and agents. This interface was exposing several vulnerabilities. Due to incorrect user input validation, an authenticated attacker could craft a malicious series of requests to upload and execute arbitrary code with root permissions on the target server hosting the Wazuh Manager service.
Before going deeper into the analysis, let’s have a quick view of Wazuh.
Wazuh and the API Service
Wazuh is widely used by thousands of organizations around the world, from small businesses to large enterprises, to protect workloads across on-premises, virtualized, containerized and cloud-based environments. Wazuh solution consists of two main components:
- an endpoint security agent, deployed to the monitored systems
- a management server, which collects and analyzes data gathered by the agents.
On the management side of operations, Wazuh has been fully integrated with the Elastic Stack, providing a search engine and data visualization tool that allows users to navigate through their security alerts.
The Wazuh API is RESTful API that allows several types of interactions with the Wazuh manager in a simple and controlled way. This interface can be used to easily perform everyday actions such as adding an agent, restarting the manager(s) or agent(s), or looking up syscheck details.
Given the importance of such module to the UI itself, from v.4.0.0 the Wazuh API will be installed along the Wazuh manager by default. Access to the API itself is regulated through a Role-based access control.
RBAC is based on the relationship between three components: users, roles and policies or permissions. Policies are associated with roles, and each user can belong to one or more roles.
After configuring RBAC, users will be able to see and do certain actions on specified resources that have previously been established. For example, members of a Security-team may have ‘read’ access to all agents, while the Sales-team may ‘read’ and ‘modify’ permissions only to agents in their department. Without further ado, let’s jump right into technical details!
Finding and exploiting the vulnerability
Wazuh API endpoints require a simple form of authentication.
After providing the required username and password of our user, we will get a JWT token that will be needed in the next calls directed to our beloved API service.
Since the main vulnerability lies in one of the exposed API endpoints, access to such resources is needed by possessing at least a pair of valid credentials or a valid JWT token.
Obtaining a valid JWT token is as simple as performing an HTTP request:
TOKEN=$(curl -u <user>:<password> -k -X GET "https://SERVERIP:55000/security/user/authenticate?raw=true")
The obtained token will give us access all the resources associated to our user RBAC profile. This is where our journey begins! While having a look at the list of availables API, the /manager/files endpoint1 immediately caught my attention. The API description says “Return file contents from any file”: pretty interesting, huh? Let’s dig deeper on how this function works under the hood.
First, we have to take a look into the parameters we can provide to this API. The “path” parameter seems a good starting point: it would be to good if we could specify anything there, right?
Of course, it did not work, but we kind of expected it! So, time to have a look on the code too see if we find some interesting caveats.
As it stands, the path parameter gets validated several times. In particular, it goes through 3 main checks:
-
inside the
format_etc_and_ruleset_file_path
function (/api/api/validator.py), due to the OpenAPI 2 specs (format schema field); -
through the
get_file function
(/framework/wazuh/manager.py); -
finally, in the
validate_cdb_list
|validate_xml
(/framework/wazuh/manager.py), depending on which file we are asking for: we can skip this check since the “validate” variable is set to False by default.
To understand if this function can be exploitable, we should definitely have a look at these functions.
The first function checks two more things:
- if the path is considered “safe” through the “is_safe_path” function; indeed, after resolving symlinks, it checks if the final path starts with the installation folder (default is
/var/ossec
); - then, it validates the path against the following regex:
_etc_and_ruleset_path = (^(etc|ruleset)\/(decoders|rules|lists(\/[\w\-\_]+)*))$
after looking at this checks, I started wondering if the path variable may be vulnerable to path traversal: indeed, the is_safe_path
function joins strings by a simple concatenation, so this may be worth a shot.
Plus, look at the regex! The path needs to start with etc
or ruleset
, but interestingly on the 2nd capture group it is possible to use “any” character after choosing the list
subpath.
Summing it up, our path
variable should:
- start with
etc/lists
to validate against the regex we saw before; - once resolved with the concatenation using the variable common.ossec_path (basically
/var/ossec
), it should be a subpath of the installation folder.
As to confirm our hypothesis, let’s try to read the ossec.conf file from the /var/ossec/etc
folder:
Wow, it really worked!
Now, let’s try to read a file in another folder, as the /var/ossec/logs/api.log
file:
Aaaand… To my surprise, this did not work!
Why does it say it is not a valid _etc_and_ruleset_file_path? After having a better look at the involved checks, I roughly understood why this happened. If you look closely, the path is built differently in the is_safe_path and get_file functions:
- the first one uses string concatenation (
c = a + b
) - the second uses the join function (
c = join(a,b)
)
this makes all the difference in the world! On legit paths, everything will work with no problems at all.
Plus, we may still be able to read stuff on the etc folder, but nothing more than that.
Indeed our case, if we input something as etc/lists/../../logs/api.log
, the two functions will yield different outputs:
is_safe_path -> /var/ossec + etc/lists/../../logs/api.log = /var/ossecetc/lists/../../logs/api.log = /var/logs/api.log
get_file -> join (/var/ossec,etc/lists/../../logs/api.log) = /var/ossec/etc/lists/../../logs/api.log = /var/ossec/logs/api.log
Even though our input will pass all checks in the get_file function, it will be rejected by the is_safe_path check. So, to update the list of our input features, it should behave in a way so that concatenation | join will bring the same output. To do this, I had the simplest idea ever: going further back in the original path. As we hit root, we can climb our way back to the installation folder by selecting the path. Let’s try this to see if my intuition was correct.
Amazing, it worked!
Now we can read ANY file inside the /var/ossec folder, granted we have enough permissions to do so. After reaching this result, the first thing I have tried to do was reading the jwt_secret file stored inside the /api/configuration/security folder.
The code used to generate users JWT is pretty simple: given a set of attributes, they will be encoded with a specific algorithm using the JWT secret stored inside the jwt_secret
file.
If we get our hands onto such encryption key, it would be possible to forge a valid token for any user in the RBAC model: this would mean more access towards all functions exposed by the rest API (and trust me, there’s plenty interesting more).
Yep, that is our beloved encryption key!
Now, we can use this to escalate our privileges through the RBAC model of the rest API. If we did not have total access to every function in the API list, now we surely do. While this is definitely huge, I wanted to make sure I achieved the highest impact as possible on the current context. As such, I kept looking on more APIs, and I found several interesting stuff! In particular, the /manager/files API exposes the PUT method for file upload:
This is definitely interesting!
In particular, it accepts a path variable: what if we could exploit our little path traversal here as well?
To understand more, we can look at the code as we did previously.
So, the OpenAPI controls seems to be pretty much the same of what we had before: the is_safe_path and regex validation functions are used as before.
This time the regex is a little different (^etc\/(ossec\.conf|(rules|decoders)\/[\w\-\/]+\.xml|lists\/[\w\-\.\/]+)$
),
but we still have our wonderful semi-arbitrary capture group in the end if we choose the etc/lists subpath.
How wonderful!
Thus, after such controls, the content of the uploaded file is validated inside the upload_list function (since we are bounded to start our path with etc/lists)
The main goal of this function is to make sure the uploaded file is formatted as the application expects:
- first, a temporary file is created by copying our file stripping out all empty lines;
- second, the main validation function (validate_cdb_list) is called.
In any case, what the heck is a cdb list?
In a few words, a cdb list is a simple plain text file where each line is written in the key1:value1 format.
The main use case of such files is to create a white/black list of users, file hashes, IPs or domain names, as explained in the Wazuh documentation.
Back to our analysis, the validate_cdb_list function checks if our file is in the cdb list format by checking the following regex against each line: ^[^:]+:[^:]*$
.
Basically, if each line contains at least an arbitrary character and a semicolon (e.g a:
), the file will be considered valid and it will be uploaded.
Let’s try our assumptions by trying to upload a simple cdb file inside an arbitrary path in the /var/ossec/tmp folder:
How wonderful! The upload has been successful, and our file was uploaded with a custom extension, inside an arbitrary folder and with -rw-rw----
permission as the ossec user.
Interestingly, we can even overwrite existing files, if we have enough permissions to do so, if we add the overwrite=true
parameter to the HTTP PUT request.
Now, the next logical step is trying to overwrite some interesting file in the /var/ossec
folder.
In particular, inside such folder there are several files that are executed during the API lifecycle by the main service nonetheless!
So, how about overwriting one of those? As an example, a good target may be the wazuh-apid.py
file located inside the /var/ossec/api/scripts
folder.
This script seems to be the one responsible to start the API service daemon: so whenever the API service is launched, this file should be executed.
Time to overwrite it!
Unfortunately, seems like the API service did not like our command.
Looking closely at the permission of the wazuh-apid.py
file, it has rw permission only for the root
user, so that is why we cannot overwrite it as the ossecr
user.
Is there a way to overwrite the file, but doing it as root?
Luckily for us, there are more tricks in our sleeve to overcome this limitation: one of the available APIs has indeed the option to update the configuration of the API service itself, that will be reloaded when the service is restarted.
As shown in the documentation, we can upload a .yaml
file containing some interesting configuration attributes!
In particular, the drop privileges
attribute is the one responsible of forcing the API service to run as the ossecr user or not.
This is exactly what we needed! But how can we restart the API service remotely?
Luckily enough, there is yet another API to do that! Indeed, by sending a PUT request to the /manager/restart URL we can solve this problem.
Such APIs are very powerful, and it safe to assume that in a real world scenario, they may be subjected to some strict RBAC profile.
However, as long as we can access the get_files API, we can still read the jwt_secret
and forge a JWT token with enough permission to use them, so no problem!
Now, let’s see if we can upload the new configuration, restart the server, and upload a new file, checking if this time it will be created by the root
user.
Awesome, the assumption was correct! Now, wrapping all the steps together, it is finally possible to achieve Remote Code Execution as root. In particular, we need to:
-
Obtain unprivileged access token with at least access to GET
/manager/files
API; -
(OPTIONAL) Escalate privileges (if necessary) to gain access to privileged API by reading JWT secret through GET
/manager/files
API; -
Update server configuration with
"drop_privileges": False
option; -
Restart the API service (now it will run as root / sudoers user);
-
Read
/var/ossec/api/scripts/wazuh-apid.py
(to restore it later) through GET/manager/files
API; -
Overwrite the
/var/ossec/api/scripts/wazuh-apid.py
by uploading our malicious python payload; -
Restart the API service (now it will run our payload).
After gaining code exec, it may be necessary to write back the previous version of /var/ossec/api/scripts/wazuh-apid.py
file and restart the manager (service wazuh-manager restart
). This is necessary in order to restore the service to the previous state.
To perform these steps, I have written down a simple python PoC (available here 3, go check it out!) that follows the previously mentioned steps.
As a side note, it is worth mentioning that this vulnerability can be exploited to achieve Local Privilege Escalation: indeed, it provides a way from an unprivileged user to run code as root on the machine hosting the API service.
Patch time!
To fix the identified vulnerabilities, there were several changes in the applied patches 4. In a few words:
- the applied regex were evaluated again and fixed;
- the is_safe_path function introduced a new check to look for path traversal escape sequences (
./
,../
)
Plus, from v. 4.1.0, the /manager/files endpoint has been removed since it gave too much control to the API user while it was not needed. Good catch there!
Conclusions
The process of investigating this vulnerability was very interesting and challenging for me.
I understood that even if a bug may be simple to find and exploit from a single perspective, that does not mean we should stop on looking forward, aiming to achieve the best possible result given the current condition.
Exploring different perspectives is always a good path to choose, and that does not stop on bug-hunting / vulnerability research.
As a closing note, I would like to thank my colleagues on CYS4 for the support, and the Wazuh team, in particular Santiago Basset (Founder & CEO), Pedro de Castro (CTO) and Víctor Manuel Fernández Castro (Director Of Engineering): thanks to their support and collaboration, the whole process of reporting, fixing, requesting a CVE ID and finally publishing this blog post was as fast and smooth as possible.
Finally, I would like to confirm that Wazuh is a very solid solution: with dozens of useful modules and features, it can give a huge hand to manage the security of any organization, regardless of the characteristics of the company itself.
Timeline
- 08-01-2021 - Vulnerability disclosed
- 08-01-2021 - Initial vendor contact
- 10-01-2021 - Bug is fixed in dev
- 14-01-2021 - SaaS gets upgraded and clients get notified by the Wazuh Team, patch is released on Wazuh 4.0.4.
- 04-02-2021 - CVE request submitted to MITRE
- 05-03-2021 - CVE ID CVE-2021-26814 has been assigned by MITRE
- 10-03-2021 - Announcement on Twitter
- 16-05-2021 - Blog post is published