CVE-2022-29844: A Classic Buffer Overflow on the Western Digital My Cloud Pro Series PR4100
April 20, 2023 | Guest BloggerThis post covers an exploit chain demonstrated by Luca Moro (@johncool__) during Pwn2Own Toronto 2022. At the contest, he used a classic buffer overflow to gain code execution on the My Cloud Pro Series PR4100 Network Attached Storage (NAS) device. He also displayed a nifty message on the device. Luca’s successful entry earned him $40,000 and 4 points towards Master of Pwn. All Pwn2Own entries are accompanied by a full whitepaper describing the vulnerabilities being used and how they were exploited. The following blog is an excerpt from that whitepaper detailing CVE-2022-29844 with minimal modifications.
Prior to being patched by Western Digital, a memory corruption vulnerability existed in the FTP service of the My Cloud Pro Series PR4100. It allowed unauthenticated attackers to read arbitrary files and, in certain cases, write them. This vulnerability could allow the full compromise of the NAS and gives remote command execution capabilities.
The exploitation requires the FTP service to be activated. While the ability to read arbitrary files is always possible, writing needs at least one share to be “Public” and accessible via FTP. Such setting is the default configuration when a share is made available on FTP, so it should be a common case.
Here is Network Services panel showing FTP Access enabled:
Here is the control panel for configuring FTP shares. Note that the needed settings are enabled by default when sharing a folder via FTP.
Technical Analysis
The issue was discovered by reverse engineering the firmware and source code auditing. The firmware used during this event can be downloaded here. The archive can be carved to extract a SquashFS filesystem. The GPL source code and the modifications made by Western Digital can be downloaded here.
The FTP service used by the NAS is based on the Pure-FTPd open source implementation with a few custom patches made by Western Digital. After extracting the GPL tar archive, one can find the custom modification in the WDMyCloud_PR4100_GPL_v5.24.108_20220826/open-source-packages/pure-ftpd/pure-ftpd-1.0.47/patch/
path within the archive listed above.
To apply the WD patches, run the following commands:
One of the modifications concerns the function douser()
, which is called when a user issues the ftp command “user const char \*username
is user controlled.
We can spot a buffer overflow vulnerability in the first strncpy
. When username
is larger than 2048 bytes an overwrite occurs after auth_name
which is located in the .bss
section. The overwritten data is fully controlled by the user. The only requirement is that the data should not contain any NULL bytes beside the finishing one.
Here are some of the relevant global variables located after auth_name
:
The global variable loggedin
is especially useful as it represents whether a user was authenticated. The string wd
is also valuable as it represents the path of the working directory. By overwriting and abusing both of these variables, it is possible to achieve an arbitrary file read and write by using the FTP commands cwd
, retr
, and stor
.
Exploitation
Given the previous analysis, the overall strategy for getting command execution would be to read or write an arbitrary file in order to install a backdoor served by the NAS webserver.
The first step would be to find a way to authenticate without valid credentials. This section explains how to do so given the vulnerability.
Then, to read or write a file, it seems reasonable to try to use the FTP features and commands. This would require changing the current working directory of the FTP session by using the command “cwd
Step One: The Authentication Bypass
As expected, the attacker does not have any credential on the NAS. Moreover, we must assume the FTP service is not accessible to anonymous user. Therefore, it is necessary to find a way to bypass the authentication.
Usually, authentication is achieved in two ftp commands:
-- 'user douser()
.
-- 'pass dopass()
.
It is worth mentioning that without first authenticating, it is impossible to use other FTP commands to fetch or store files.
A simplified description of the dopass()
implementation is to check the user password and set the global variable loggedin
to 1 and eventually to use setuid()
to change the user ID of the running process.
As previously seen, the vulnerability allows the attacker to overflow the variable loggedin
. It is possible to make the server think that the user has already logged in by issuing only a malicious user
command with a name long enough so that loggedin
becomes something other than 0, which is sufficient.
One side effect is that the process will not use setuid()
so the user root will remain the user of the running process. This will have some consequences later.
Step Two: Access control
An Overview of the implementation
Before retrieving or storing files, we need to change the current working directory of the FTP session by using the cwd path
command. This command handling is implemented by the function docwd()
, which was patched by Western Digital to include access control checks. Here are some of the relevant parts:
The main access control check revolves around the call to check_allowed(wd_tmp, allowed)
, where wd_tmp
is the concatenation of the global string wd
and the user-controlled argument dir
. The general idea is that check_allowed()
returns something greater than 0 if the user is allowed to enter the directory.
In a similar fashion, the function check_allowed()
is called when the user tries to read (retr
) or write a file (stor
). To provide some additional detail, check_allowed()
is specific to Western Digital and is implemented in the closed source /wd/usr/lib/libftp_allow.so
as seen in the example below:
This function checks whether the current user (using getuid()
and getpwuid()
) has access to the required path. The function relies on the Get_Share_Permission()
function to determine the permissions over a path representing a share.
We can sum up the check_allowed()
method as follows:
-- If the path is /
, then check_allowed()
returns 1, which means the path is readable.
-- If path looks like /<some_path>
, then check_allowed()
returns the value of Get_Share_Permission(some_path, pw_name)
.
-- Otherwise, check_allowed()
returns its second argument perm
, which is the global variable allowed
that was passed along in the call docwd()
.
The last case is interesting because it means that for paths that do not start with /
, no check is really enforced. Indeed, in that case, the access control check only relies on the global variable allowed
. This in peculiar will be used later.
The Get_Share_Permission()
function is also closed source and specific to Western Digital. Because of this, we won’t dig deeper here. The gist of that function is that it opens the file /etc/NAS_CFG/ftp.xml
, which specifies the authorizations on the share.
The Get_Share_Permission()
function returns 1 for readable shares for a given username and 2 for readable and writable files and 0 for other states. The file /etc/NAS_CFG/ftp.xml
maps the user’s permissions and is an image of what is configured in the “Shares” tab of the administration web page. Here is an example of this file:
In that example, Get_Share_Permission()
would:
-- return 2 for the path “share1” for any account.
-- return 2 for the path “share2” for the account user2
.
-- return 1 for the path “share2” for the account admin
.
-- return 0 for the path “share2” for all other accounts.
In our context with the previous authentication bypass, the user that is being passed to the Get_Share_Permission()
function is root
. This may seem ideal, but it is not in the context of the access control. That is because root
is different than the default administrator account, which is admin
. Additionally, the user root
does not makes sense in the ACL context. Consequently, it is likely that Get_Share_Permission()
does not give permission to our user root
.
That being said, in the previous example, “share1” would still return 2 (making it writable) for the user root
. That is because of the parameter #@allaccount#
appearing in its write_list
. This is because share1
represents a share configured as “Public” (see Figure 1 above). This means that everyone with valid credentials can access this share via FTP. In fact, this is the default configuration for FTP shares.
Please note that Public
in this context is a different concept than anonymous access since you would still require a username and password to write or read anything on share1
. In that sense, it is fair to expect some kind of security for Public
shares.
Step Three: Reaching Arbitrary Directories to Read or Write
At this point, we already bypassed the FTP authentication, but we cannot yet use the cwd
command on an arbitrary directory as the check_allowed()
function is preventing that command for most folders that are not a Share. For example, we cannot reach the /etc/
directory.
As seen earlier, check_allowed()
is called with the variable wd_tmp
(path) and allowed
(int). Additionally, when the user given path dir
does not start with '/', the content of wd_tmp
is the concatenation of the global path wd
and dir
. This is interesting because using the vulnerability, we can rewrite wd_tmp
. To summarize, the path argument of check_allowed()
can be fully chosen by the attacker.
Using the peculiarities of the check_allowed()
function with the control of the first argument, the following strategy allows reaching arbitrary folders with write access:
- Use the
user
vulnerability to rewriteloggedin
and bypass authentication. - Use
cwd share1
, which is writable by root. Therefore, this setsallowed
to 2. - Use the
user
vulnerability to rewrite and erasewd
so thatwd_tmp[0] == 0
. As explained previously, this will preventcheck_allowed()
from checking anything and forces it to return its second argument (allowed
). - Use
cwd
to any path without starting slash (e.g.,etc/
). Because of the previous step, this will be allowed, and the server will think that the user has write permissions to the folder.
After that, it is possible to use the stor
command to achieve an arbitrary file write. In accordance with the FTP protocol, this command is to be preceded by a port
command so that the server will connect to the client on the given port (FTP active mode).
However, there is one limitation with the previous strategy. The attacker must know a valid and writable (i.e., Public
) share name. One way to get around this is to first obtain an arbitrary file read capability in order to read the /etc/NAS_CFG/ftp.xml
file. With the contents of that file, the attacker can find valid share names to use in the write strategy.
By noticing that the check_allowed()
function always returns 1 when called with /
as path, we can construct a similar strategy to achieve arbitrary file read:
- Use the
user
vulnerability to rewriteloggedin
and bypass the authentication. - Use the
cwd /
command, which is always “readable”, therefore settingallowed
to 1. - Use the
user
vulnerability to rewrite and erasewd_tmp
so thatwd_tmp[0] == 0
. As explained previously, this will preventcheck_allowed()
from checking anything. It will return its second argument (allowed
). - Use the
cwd
command to any path without starting slash (for exampleetc/NAS_CFG/
). Because of the previous step, this will be allowed, and the server will think that the user has read permissions on the folder.
After that, it is possible to use the retr
command to achieve an arbitrary file read. Here again, one must use the port
command per FTP standards.
To summarize, the vulnerability is used to rewrite a global stored path in order to control an argument of check_allowed()
. This allows the attacker to circumvent the check and force check_allowed()
to return the value of its previous invocation, stored in the value allowed
. By using the cwd
command in a valid directory then using this attack, it is possible to reach any folder with the permissions of the valid directory. Since the root directory (/
) is readable, we can reach /etc/NAS_CFG/
with read permissions to get a read access to the ftp.xml
file and learn a writable directory name. From there, we apply the same strategy to access any location with writable permissions.
Getting Remote Code Execution
At that point, we have both arbitrary file read and write access on the NAS. It is the time to investigate getting remote code execution (RCE).
It is worth mentioning that an arbitrary file read might be sufficient to achieve command execution of a sort. For instance, one might find ways to steal user passwords, session cookies, or other secrets stored on the filesystem. However, getting RCE with the file write is easier and only adds a small requirement since FTP shares are Public
by default. Therefore, the exploit uses the file write technique, though we know that RCE might be possible without it.
The command execution is achieved in a couple of steps. To begin, a PHP web shell is uploaded to /var/www/cmd.php
. This makes the web server serve the url /cmd.php
and executes the GET argument cmd
. While this might be sufficient, a proper reverse Python shell is uploaded in /tmp/python_shell.py
and is executed thanks to the web shell. The Python shell connects back to the attacker device and provides a root shell.
Conclusion
This vulnerability was patched by Western Digital in firmware version 5.26.119 and assigned CVE-2022-29844. Based on the writeup from the vendor, the vulnerability was addressed by correcting the memory corruption condition that could allow an attacker to read and write arbitrary files. During the Pwn2Own contest, Luca was able to demonstrate success both with a reverse shell and a special light show on the device:
Thanks again to Luca Moro for providing this write-up and for his participation in Pwn2Own Toronto. He has participated in multiple events, and we certainly hope to see more submissions from him in the future. Until then, follow the team on Twitter, Mastodon, LinkedIn, or Instagram for the latest in exploit techniques and security patches.