Пример #1
0
    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
Пример #2
0
    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
Пример #3
0
    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
Пример #4
0
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()
Пример #5
0
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
Пример #6
0
    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
Пример #7
0
    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()
Пример #8
0
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
Пример #9
0
    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
Пример #10
0
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)
Пример #11
0
    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
Пример #12
0
    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
Пример #13
0
    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
Пример #14
0
    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 ""
Пример #15
0
 def check_vulns_core(self) -> List[Vulnerability]:
     # TODO
     LOGGER.print_cms("alert", "[-] CVE check not yet implemented !", "", 0)
     return []
Пример #16
0
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()
Пример #17
0
 def check_vulns_addon(self, addon: Addon) -> List[Addon]:
     # TODO
     LOGGER.print_cms("alert", "[-] CVE check not yet implemented !", "", 1)
     return []