An application’s interaction with the file system is always highly security sensitive since minor functional bugs can easily be the source of exploitable vulnerabilities. This observation is especially true in the case of web file managers, whose role is to replicate the features of a complete file system and expose it to the client’s browser in a transparent way.
elFinder is a popular web file manager often used in CMS and frameworks, such as WordPress plugins (wp-file-manager) or Symfony bundles, to allow easy operations on both local and remote files. In the past, elFinder has been part of active in-the-wild attacks targeting unsafe configuration or actual code vulnerabilities. Thus, elFinder is published with a safe default configuration to prevent any malicious use by attackers.
As part of our regular assessment of widely deployed open-source projects, we discovered multiple new code vulnerabilities in elFinder. In the following case study of common code vulnerabilities in web file managers, we describe five different vulnerability chains and demonstrate how they could be exploited to gain control of the underlying server and its data. We will also discuss some of the patches that were later implemented by the vendor to show how to prevent them in your own code.
Impact
We worked on the development branch, commit f9c906d. Findings were also confirmed on release 2.1.57; all affect the default configuration (unless specified otherwise in this article) and do not require prior authentication. As we mentioned, the exploitation of these vulnerabilities can let an attacker execute arbitrary PHP code on the server where elFinder is installed, ultimately leading to its compromise.
The findings we discuss in this blog post (all assigned to CVE-2021-32682) and successfully exploited to gain code execution are:
- Deleting Arbitrary Files
- Moving Arbitrary Files
- Uploading PHP Files
- Argument Injection
- Race Condition
All these bug classes are very common in software that exposes filesystems to users, and are likely to impact a broad range of products, not only elFinder.
elFinder released version 2.1.59 to address all the bugs we responsibly disclosed. There is no doubt these vulnerabilities will also be exploited in the wild, because exploits targeting old versions have been publicly released and the connectors filenames are part of compilations of paths to look for when trying to compromise websites. Hence, we highly recommend that all users immediately upgrade elFinder to the latest version.
Technical Details
elFinder comes with a back end (also called connector) written in PHP and a front end written in HTML and JavaScript. The connector is the main script that dispatches the actions of the front end code to the right back end code to implement file system features. Connectors can be configured to disallow dangerous actions, restrict uploads to specific MIME types: two different ones are part of the default install. We detected vulnerabilities in the so-called “minimal” connector. It only allows image and plain text uploads and FTP is the only supported remote virtual filesystem: this is presumably the safest one and the most likely to be deployed.
To give a better understanding of the code snippets we will use to demonstrate our findings, we will first describe how elFinder’s routing works. Like in many modern PHP applications, the connector (e.g. connector.minimal.php
) is the only entry point. It declares configuration directives and closures and then instantiates both elFinder
(the core) and elFinderConnector
(the interface between elFinder
and the transport channel, here HTTP).
The attribute elFinder::$commands
contains every valid action and the expected arguments:
php/elFinder.class.php
The user can call any of these commands by providing the cmd
parameter with the required command parameter via PATH_INFO
, GET
, or POST
. In each command handler, parameters are accessed using $args
.
To allow remote filesystems (FTP, Dropbox, etc.) to be used with local ones, elFinder implements a filesystem abstraction layer (elFinderVolumeDriver
) on top of which all drivers are built. Files are then referenced by their volume name (e.g. t1_
is the trash, l1_
the default local volume) and the URL-safe Base64 of their name.
Let’s first dig into an arbitrary file deletion bug chain, composed of two distinct issues.
Deleting Arbitrary Files
The PHP core does not provide an effective way to run background threads, or perform synchronization and inter-process communication. elFinder tries to balance this by heavily using temporary files and post-request hooks. For instance, users can abort
ongoing actions by calling the method of the same name:
php/elFinder.class.php
Here, a code vulnerability is present at [1]
and [2]
: a user-controlled parameter is concatenated into a full path without prior checks. For [1]
, it can end up creating an empty file with a fully controllable name, and in [2]
it can be used to remove an arbitrary file. SonarQube Cloud issues for both bugs are available: [1] and [2].
There is a catch: the filename resulting from [1]
will be prefixed by elfreq
. In a path traversal attack, POSIX systems will fail path resolution if any predecessor in the path does not exist or is not a directory. For instance, resolving /tmp/i_do_not_exist/../
or /tmp/i_am_a_file/../
will respectively fail with ENOENT
and ENOTDIR
. This prerequisite makes the exploitation of these two vulnerabilities impossible as-is, and will require another bug, such as the ability to create an arbitrary directory.
An attacker could then look into the command mkdir
and discover a primitive that allows this exact behaviour. Here is its top-level handler, before it goes through the filesystem abstraction layer:
php/elFinder.class.php
A generic implementation is present in elFinderVolumeDriver
to handle both the volume and path that should be created. It will call the volume-specific implementation at [1]
with the volume absolute path on the filesystem as the first parameter and the target name as the second parameter:
php/elFinderVolumeDriver.class.php
It is defined as follows:
php/elFinderVolumeLocalFileSystem.class.php
elFinderVolumeLocalFileSystem::_joinPath()
is doing a mere concatenation of the two values, leading to a path traversal vulnerability. This gives a primitive to create arbitrary, empty folders on the local filesystem. While not being a vulnerability in itself, it will allow the exploitation of the aforementioned behaviour.
It is also worth noting the presence of a full path disclosure in the rm
command, disclosing the absolute path of a given file on the local filesystem:
php/elFinderVolumeDriver.class.php
The impact of this vulnerability is quite dependent on the environment: it could be chained with other elFinder bugs, used to trigger interesting behaviors in other applications (e.g. remove WordPress’ wp-config.php file to gain code execution) or used to affect existing security measures (e.g. removing .htaccess
files).
This vulnerability has been fixed by improving the implementation of elFinderVolumeLocalFileSystem::_joinPath(
) to assert that the final path won’t be outside of the base one. Several calls to basename()
across the codebase were also added as a hardening measure.
Moving Arbitrary Files
This same elFinderVolumeLocalFileSystem::_joinPath()
method is used in other actions, such as rename
: it combines a volume base directory and a user-provided destination name. It is thus vulnerable to the bug we just described.
The following snippet is the actual implementation of elFinderVolumeLocalFileSystem::rename()
, after executing all the code responsible for decoding the paths and ensuring that the destination extension is allowed:
php/elFinderVolumeLocalFileSystem.class.php
While the destination extension is still strictly limited by MIME checks, this primitive can be enough for an unauthenticated attacker to gain command execution on the server, depending on the environment, by overriding files like authorized_keys
, composer.json
, etc. This bug has been fixed with the same patch as the previous bug we discussed.
Uploading PHP Files
As for most PHP applications, the biggest threat faced by elFinder is that an attacker could be able to upload PHP scripts to the server, since nothing (except quite a hardened web server configuration) would prevent them from accessing it directly to execute its contents. The maintainers initially tried to defend against that by crafting a block-list that associated dangerous MIME types to the relevant extensions:
php/elFinderVolumeDriver.class.php
In our test environment (Apache HTTP 2.4.46-1ubuntu1 on Ubuntu 20.10), the default configuration declares that .phar
files should be treated as application/x-httpd-php ([1]
) and be interpreted:
This configuration was also observed on Debian’s stable release. While another pass of MIME type detection is performed on the contents of the file, this can be easily circumvented as the PHP interpreter allows statements anywhere in the interpreted files (e.g. <?php
can be placed after some dummy data).
The fix is straightforward: it declares that .phar
files are associated with the MIME text/x-php
, which are disallowed by default.
Argument Injection
Among the default features that make elFinder so powerful, users can select multiple files and archive them using external tools such as zip
, rar
, and 7z
. This functionality is exposed under the action named archive
:
php/elFinder.class.php
Note that users can create archives even if their upload is forbidden, by calling the archive
command on existing files. The implementation is specific to the virtual filesystem in use. We will focus solely on the default one, since it is inherited by elFinderVolumeLocalFileSystem
which crafts the full command line ([1]
) and executes it with the default shell ([2]
):
php/elFinderVolumeLocalFileSystem.class.php
Here, the value of $name
comes from the user-controlled parameter $_GET['name']
. While properly escaped with escapeshellarg()
to prevent the use of command substitution sequences, the program will try to parse this value as a flag (--foo=bar
) and then as a positional argument. It is also worth noting that the user's value is suffixed with .zip
in the case in which the ZIP archiver is selected.
The command zip
implements an integrity test feature (-T
) that can be used along with -TT
to specify the test command to run. In the present case, it gives the attacker a way to execute arbitrary commands using this parameter injection.
To be able to exploit this vulnerability, the attacker needs to create a dummy file (e.g. a.txt
), archive it to create a.zip
and then invoke the archive
action with both the original file and the archive as targets, using a name like -TmTT="$(id>out.txt)foooo"
.
The resulting command line will be zip -r9 -q '-TmTT="$(id>out.txt)foooo".zip' './a.zip' './a.txt'
, thus executing id
and logging its standard output into out.txt
— this file will be available with the other documents in elFinder’s interface.
When it came time to fix this bug, zip
wasn't very friendly. The usual method based on POSIX’s --
(see our previous article about a parameter injection in Composer for an in-depth explanation) can’t be applied here, since zip
will exit with the following error:
The maintainers then decided to prefix the archive name with ./ to prevent any risk of parameter injection. They also decided to harden the calls to the other archivers (7z
, rar
, etc.) in the same patch.
Quarantine and Race Condition
Let’s have a look at our last finding of this case study. While this vulnerability in the quarantine feature cannot be exploited in the default configuration since archives can’t be uploaded; the feature could have been responsible for future security issues because of its design.
The rationale behind the quarantine is that archives may contain unwanted files (mostly PHP scripts) that should not be extracted in the current folder without first running security checks (e.g. with MIME validation). So instead, elFinder chose to extract archives into a folder named .quarantine
, placed under the files/
folder, and elFinderVolumeLocalFileSystem::_extract()
generates a random directory name for each archive extraction (at [1]
):
php/elFinderVolumeLocalFileSystem.class.php
This can be confirmed dynamically thanks to strace
or the inotify
suite, for instance here with an archive containing a PHP file:
This trace can be understood as:
- A folder named
efbf975ccbac8727f434574610a0f1b6
is created, - A file named
win.php
is created withinefbf975ccbac8727f434574610a0f1b6
, - Data is written into
win.php
, win.php
is deleted,efbf975ccbac8727f434574610a0f1b6
is deleted.
If the server is configured to list directories, this behavior can easily be exploited, since dangerous files (e.g. .php
) can be accessed right before the MIME validation step and their removal. The race condition window is however too small to think of an attack involving brute force if the random directory name can’t be found that way.
An attacker could discover that the duplicate
action can be used on the internal folders, like .quarantine
, and copy any file regardless of its contents. While being a harmless functional bug on its own, it can be chained with the quarantine feature to duplicate the folder containing our extracted archive just before its deletion. The duplicated folder is then visible in the interface, and allows an attacker to get around the random name to access the malicious script, ultimately granting arbitrary code execution.
As a fix, the maintainers decided to move the .quarantine
folder outside of files/. The elFinderVolumeLocalFileSystem
abstraction layer is not aware of anything outside of this folder, preventing any unintended action on .quarantine
.
Timeline
Date | Action |
2021-03-22 | These 5 issues are reported to maintainers |
2021-06-10 | The maintainers acknowledge all our findings |
2021-06-13 | elFinder 2.1.59 is released, fixing the bugs we reported |
2021-06-13 | CVE-2021-32682 and CVE-2021-23394 are assigned |
Summary
In this case study we looked at critical code vulnerabilities that are commonly found in web file managers. We presented several of our real-world findings in the latest version of elFinder available at the time, including their potential impact and how they were fixed by the vendor. It allowed us to demonstrate that innocuous bugs can often be combined to gain arbitrary code execution. We believe it is important to document and report these vulnerabilities to break future bug chains and reduce the risk of similar issues.
We also learned that working with paths is not easy and that extra measures should be taken: performing additional checks in the “low-level” functions, using basename()
and dirname()
with confidence (and knowing their limits!) and always validating user-controlled data. Such bugs are very common in web file managers, and you should always have such bugs in mind when working with them.
While we don’t plan to release any exploits for these bugs, we would still like to bring your attention to the fact that arbitrary code execution was easily demonstrated and attackers won’t have much trouble replicating it. We urge you to immediately upgrade to elFinder 2.1.59. We also advise enforcing strong access control on the connector (e.g. basic access authentication).
Finally, we would like to thank the maintainers of elFinder for acknowledging our advisory and fixing these vulnerabilities in a timely and professional manner.