def addon_analysis(self, addon_type: str) -> List[Addon]: temp_directory = uCMS.TempDir.create() addons = [] addons_path = "" LOGGER.print_cms( "info", "#######################################################" + "\n\t\t" + addon_type + " analysis" + "\n#######################################################", "", 0, ) # Get the list of addon to work with if addon_type == "plugins": addons_path = self.plugins_dir elif addon_type == "themes": addons_path = self.themes_dir addons_name = uCMS.fetch_addons( os.path.join(self.dir_path, addons_path), "standard") for addon_name in addons_name: addon = Addon() addon.type = addon_type addon.name = addon_name addon.filename = addon_name + self.addon_extension LOGGER.print_cms("info", "[+] " + addon_name, "", 0) addon_path = os.path.join(self.dir_path, addons_path, addon_name) try: # Get addon version self.get_addon_version(addon, addon_path, self.regex_version_addon, '"') # Check addon last version self.get_addon_last_version(addon) # Check if there are known CVE self.check_vulns_addon(addon) # Check if the addon have been altered self.check_addon_alteration(addon, addon_path, temp_directory) addons.append(addon) except Exception as e: LOGGER.debug(str(e)) addons.append(addon) pass if addon_type == "plugins": self.plugins = addons elif addon_type == "themes": self.themes = addons return addons
def extract_core_last_version(self, response) -> str: tree = etree.fromstring(response.content) last_version_core = tree.xpath("/project/releases/release/tag")[0].text LOGGER.print_cms("info", f"[+] Last CMS version: {last_version_core}", "", 0) self.core.last_version = last_version_core return last_version_core
def check_addon_alteration(self, addon: Addon, addon_path: str, temp_directory: str) -> str: addon_url = self.get_addon_url(addon) LOGGER.print_cms("default", f"To download the addon: {addon_url}", "", 1) altered = "" try: response = requests.get(addon_url) response.raise_for_status() if response.status_code == 200: zip_file = zipfile.ZipFile(io.BytesIO(response.content), "r") zip_file.extractall(temp_directory) zip_file.close() project_dir_hash = dirhash(addon_path, "sha1") ref_dir = os.path.join(temp_directory, addon.name) ref_dir_hash = dirhash(ref_dir, "sha1") if project_dir_hash == ref_dir_hash: altered = "NO" LOGGER.print_cms("good", f"Different from sources : {altered}", "", 1) else: altered = "YES" LOGGER.print_cms("alert", f"Different from sources : {altered}", "", 1) dcmp = dircmp(addon_path, ref_dir, self.ignored_files_addon) uCMS.diff_files(dcmp, addon.alterations, addon_path) addon.altered = altered if addon.alterations is not None: LOGGER.print_cms( "info", f"[+] For further analysis, archive downloaded here : {ref_dir}", "", 1, ) except requests.exceptions.HTTPError as e: addon.notes = "The download link is not standard. Search manually !" LOGGER.print_cms("alert", addon.notes, "", 1) LOGGER.debug(str(e)) return addon.notes return altered
def verify_path(dir_path: str, to_check: List) -> None: for directory in to_check: if not os.path.exists(os.path.join(dir_path, directory)): LOGGER.print_cms( "alert", "[-] The path provided does not seem to be a CMS directory. " "Please check the path !", "", 0, ) sys.exit()
def fetch_addons(input: str, type: str) -> List[str]: plugins_name = [] if not os.path.exists(input): LOGGER.print_cms( "alert", f"[+] Addons path {input} does not exist ! (it may be normal)", "", 0 ) return [] if type == "standard": plugins_name = next(os.walk(input))[1] elif type == "mu": plugins_name = [name.split(".php")[0] for name in next(os.walk(input))[2]] return plugins_name
def get_core_last_version(self) -> str: """ Fetch information on last release """ url_release = self.get_url_release() try: response = requests.get(url_release) response.raise_for_status() if response.status_code == 200: self.last_version = self.extract_core_last_version(response) except requests.exceptions.HTTPError as e: LOGGER.print_cms("alert", "[-] Unable to retrieve last version. Search manually !", "", 1) LOGGER.debug(str(e)) pass return self.last_version
def ask_delete_tmp(cls): clear_tmp_dir = input( "Do you want to keep temp directories containing downloaded core and " "plugins for further analysis ? (yes/no) " ).lower() if clear_tmp_dir == "no": LOGGER.print_cms("alert", "Deleting tmp directories !", "", 0) cls.delete_all() elif clear_tmp_dir == "yes": dir_list_str = "" for tmp_dir in cls.tmp_dir_list: dir_list_str = dir_list_str + "\n" + tmp_dir LOGGER.print_cms("info", "Keeping tmp directories ! Here they are :" + dir_list_str, "", 0) else: cls.ask_delete_tmp()
def parse_conf(conf_file: str) -> Dict: config = configparser.ConfigParser() config_dict = {} try: with open(Path(conf_file)) as file: config.read_file(file) except FileNotFoundError: LOGGER.print_cms( "alert", "[-] The conf file does not exist. " "Please check the path !", "", 0, ) sys.exit() for key, value in config.items("Configuration"): config_dict[key] = value return config_dict
def check_core_alteration(self, core_url: str) -> List[Alteration]: self.get_archive_name() alterations = [] temp_directory = uCMS.TempDir.create() LOGGER.print_cms("info", "[+] Checking core alteration", "", 0) try: response = requests.get(core_url) response.raise_for_status() if response.status_code == 200: zip_file = zipfile.ZipFile(io.BytesIO(response.content), "r") zip_file.extractall(temp_directory) zip_file.close() except requests.exceptions.HTTPError as e: LOGGER.print_cms( "alert", "[-] Unable to find the original archive. Search manually !", "", 0 ) self.core.alterations = alterations LOGGER.debug(str(e)) return self.core.alterations clean_core_path = os.path.join(temp_directory, Path(self.get_archive_name())) dcmp = dircmp(clean_core_path, self.dir_path, self.core.ignored_files) uCMS.diff_files(dcmp, alterations, self.dir_path) # type: ignore # ignore for "dcmp" variable self.core.alterations = alterations if alterations is not None: msg = "[+] For further analysis, archive downloaded here : " + clean_core_path LOGGER.print_cms("info", msg, "", 0) return self.core.alterations
def diff_files(dcmp: dircmp, alterations: List, target: str) -> None: for name in dcmp.diff_files: alteration = Alteration() altered_file = os.path.join(target, str(name)) LOGGER.print_cms("alert", altered_file, " was altered !", 1) alteration.target = target alteration.file = name alteration.type = "altered" alterations.append(alteration) for name in dcmp.right_only: alteration = Alteration() altered_file = os.path.join(target, str(name)) LOGGER.print_cms("warning", altered_file, " has been added !", 1) alteration.target = target alteration.file = name alteration.type = "added" alterations.append(alteration) for name in dcmp.left_only: alteration = Alteration() altered_file = os.path.join(target, str(name)) LOGGER.print_cms("warning", altered_file, " deleted !", 1) alteration.target = target alteration.file = name alteration.type = "deleted" alterations.append(alteration) for current_dir, sub_dcmp in zip(dcmp.subdirs.keys(), dcmp.subdirs.values()): current_target = os.path.join(target, str(current_dir)) diff_files(sub_dcmp, alterations, current_target)
def get_addon_version( self, addon: Addon, addon_path: str, version_file_regexp: Pattern[str], to_strip: str ) -> str: version = "" try: path = os.path.join(addon_path, addon.filename) with open(path, encoding="utf8") as addon_info: for line in addon_info: version = version_file_regexp.search(line) if version: candidate_version = str(version.group(1).strip(to_strip)) if candidate_version != "VERSION": # Drupal specific addon.version = candidate_version LOGGER.print_cms("default", "Version : " + addon.version, "", 1) break except FileNotFoundError as e: msg = "No standard extension file. Search manually !" LOGGER.print_cms("alert", "[-] " + msg, "", 1) addon.notes = msg return "" return addon.version
def core_analysis(self) -> Core: LOGGER.print_cms( "info", "#######################################################" + "\n\t\tCore analysis" + "\n#######################################################", "", 0, ) # Check current CMS version if self.core.version == "": self.get_core_version() # Get the last released version self.get_core_last_version() # Check for vuln on the CMS version self.check_vulns_core() # Check if the core have been altered self.check_core_alteration(self.download_core_url + self.core.version + ".zip") return self.core
def get_addon_last_version(self, addon: Addon) -> str: releases_url = f"{self.site_url}/project/{addon.name}/releases" if addon.version == "VERSION": addon.notes = "This is a default addon. Analysis is not yet implemented !" LOGGER.print_cms("alert", addon.notes, "", 1) return "" try: response = requests.get(releases_url, allow_redirects=False) response.raise_for_status() if response.status_code == 200: page = response.text last_version_result = self.regex_version_addon_web.search(page) date_last_release_result = self.regex_date_last_release.search( page) if last_version_result and date_last_release_result: addon.last_version = last_version_result.group(3) addon.last_release_date = date_last_release_result.group(2) addon.link = releases_url if addon.last_version == addon.version: LOGGER.print_cms("good", "Up to date !", "", 1) else: LOGGER.print_cms( "alert", "Outdated, last version: ", f"{addon.last_version} ({addon.last_release_date}) \n\tCheck : {releases_url}", 1, ) except requests.exceptions.HTTPError as e: addon.notes = "Addon not on official site. Search manually !" LOGGER.print_cms("alert", f"[-] {addon.notes}", "", 1) raise e return addon.last_version
def get_core_version(self) -> str: suspects = [] try: with open( os.path.join(self.dir_path, self.core_suspect_file_path)) as version_file: for line in version_file: version_core_match = self.regex_version_core.search(line) if version_core_match: suspects.append(version_core_match.group(1).strip()) break except FileNotFoundError as e: LOGGER.debug(str(e)) pass suspects_length = len(suspects) if suspects_length == 0: LOGGER.print_cms("alert", "[-] Version not found. Search manually !", "", 0) return "" elif suspects_length == 1: LOGGER.print_cms("info", "[+] Version used : " + suspects[0], "", 0) self.core.version = suspects[0] self.core.version_major = suspects[0].split(".")[0] return suspects[0] else: for suspect in suspects: LOGGER.print_cms( "alert", "[-] Multiple versions found." + suspect + " You " "should probably check by yourself manually.", "", 0, ) return ""
def check_vulns_core(self) -> List[Vulnerability]: # TODO LOGGER.print_cms("alert", "[-] CVE check not yet implemented !", "", 0) return []
def main(): args = uCMS.parse_args() if "conf" in args: config = uCMS.parse_conf(args["conf"]) args = {**config, **args} # Colored output ? if args["no_color"]: LOGGER.set_nocolor_policy(args["no_color"]) if args["debug"]: LOGGER.set_debug_policy(args["debug"]) if "logfile" in args: LOGGER.set_file(args["logfile"]) if not args["dir"]: LOGGER.print_cms("alert", "No path received !", "", 0) sys.exit() dir_path = args["dir"] wp_content = "" plugins_dir = "" themes_dir = "" no_check = False wpvulndb_token = "" version = "" version_major = "" if "wp_content" in args: wp_content = args["wp_content"] if "plugins_dir" in args: plugins_dir = args["plugins_dir"] if "themes_dir" in args: themes_dir = args["themes_dir"] if "no_check" in args: no_check = args["no_check"] if "wpvulndb_token" in args: wpvulndb_token = args["wpvulndb_token"] if "version" in args: version = args["version"] if "version_major" in args: version_major = args["version_major"] # Verify if the CMS is really the one given by the user if args["cms"] == "wordpress": if not no_check: to_check = ["wp-includes", "wp-admin"] uCMS.verify_path(dir_path, to_check) cms = WordPress.WP(dir_path, wp_content, plugins_dir, themes_dir, wpvulndb_token, version, version_major) elif args["cms"] == "drupal": if not no_check: to_check = [ "sites", "modules", "profiles", "themes", "web.config", "update.php" ] uCMS.verify_path(dir_path, to_check) # Try to detect Drupal major version tmp_cms = GenericDrupal.GenericDPL(dir_path, plugins_dir, themes_dir, version, version_major) version_major_detected = tmp_cms.detect_core_major_version() del tmp_cms if version_major_detected != "" and version_major == "": version_major = version_major_detected if version_major == "7": cms = Drupal7.DPL7(dir_path, plugins_dir, themes_dir, version, version_major) elif version_major == "8": cms = Drupal8.DPL8(dir_path, plugins_dir, themes_dir, version, version_major) else: LOGGER.print_cms("alert", "Major Drupal version unknown !", "", 0) sys.exit() else: LOGGER.print_cms("alert", "CMS unknown or unsupported !", "", 0) sys.exit() # Analyse the core if not args["skip_core"]: cms.core_analysis() # Analyse plugins if not args["skip_plugins"]: cms.addon_analysis("plugins") # Analyse themes if not args["skip_themes"]: cms.addon_analysis("themes") # Save results to a file if args["type"] == "CSV" and args["output"]: # Initialize the output file result_csv = ComissionCSV(args["output"]) # Add data and generate result file result_csv.add_data(cms.core, cms.plugins, cms.themes) elif args["type"] == "XLSX" and args["output"]: # Initialize the output file result_xlsx = ComissionXLSX(args["output"]) # Add data result_xlsx.add_data(cms.core, cms.plugins, cms.themes) # Generate result file result_xlsx.generate_xlsx() elif args["type"] == "JSON" and args["output"]: # Initialize the output file result_json = ComissionJSON(args["output"]) # Add data result_json.add_data(cms.core, cms.plugins, cms.themes) # Generate result file result_json.generate_json() elif args["type"] == "STDOUT": # Do nothing pass else: LOGGER.print_cms("alert", "Output type unknown or missing filename !", "", 0) sys.exit() # Keep or clean temp dir uCMS.TempDir.ask_delete_tmp()
def check_vulns_addon(self, addon: Addon) -> List[Addon]: # TODO LOGGER.print_cms("alert", "[-] CVE check not yet implemented !", "", 1) return []