From ca1131fa15254df98741bbce9596b0cf0763b850 Mon Sep 17 00:00:00 2001 From: nmasdoufi-ol Date: Fri, 20 Dec 2024 17:37:55 +0100 Subject: [PATCH] Add Version-based Detection for CVE-2024-51479 --- agent/exploits/cve_2024_51479.py | 107 ++++++++++++++++++++++++++ tests/exploits/cve_2024_51479_test.py | 107 ++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 agent/exploits/cve_2024_51479.py create mode 100644 tests/exploits/cve_2024_51479_test.py diff --git a/agent/exploits/cve_2024_51479.py b/agent/exploits/cve_2024_51479.py new file mode 100644 index 0000000..742e8f3 --- /dev/null +++ b/agent/exploits/cve_2024_51479.py @@ -0,0 +1,107 @@ +"""Agent Asteroid implementation for CVE-2024-51479""" + +import re +import datetime + +import requests +from packaging import version + +from agent import definitions +from agent import exploits_registry +from agent.exploits import webexploit + +DEFAULT_TIMEOUT = datetime.timedelta(seconds=90) + +VULNERABILITY_TITLE = "Next.js Authorization Bypass via Middleware" +VULNERABILITY_REFERENCE = "CVE-2024-51479" +VULNERABILITY_DESCRIPTION = ( + "A recently disclosed security vulnerability in Next.js, a popular React framework used by millions of developers worldwide, " + "could have allowed unauthorized access to sensitive application data. The vulnerability stemmed from an authorization bypass issue " + "in Next.js middleware when performing authorization checks based on pathname. This affected versions 9.5.5 through 14.2.14 and allowed " + "unauthorized access to pages under the root directory of an application." +) +RISK_RATING = "HIGH" + +CHUNK_FILE_PATTERN = re.compile( + r'' +) +VERSION_PATTERN = re.compile(r't\.version="(\d+\.\d+(?:\.\d+)?)"') +MAX_VULNERABLE_VERSION = version.parse("14.2.14") + + +def _fetch_chunk_file(target_url: str, chunk_path: str) -> str | None: + """ + Fetches the JavaScript chunk file to extract the version information. + + Args: + target_url: The base URL of the target application. + chunk_path: The path to the chunk file. + + Returns: + The content of the chunk file, if accessible, or an empty string. + """ + try: + full_url = f"{target_url}{chunk_path}" + response = requests.get(full_url, timeout=DEFAULT_TIMEOUT.seconds, verify=False) + if response.status_code == 200: + return response.text + except requests.RequestException: + pass + + return None + + +def _extract_version(chunk_content: str) -> str | None: + """ + Extracts the Next.js version from the chunk file content. + + Args: + chunk_content: The content of the fetched chunk file. + + Returns: + The extracted version string, if found, or None. + """ + match = VERSION_PATTERN.search(chunk_content) + if match is not None: + return match.group(1) + return None + + +@exploits_registry.register +class CVE202451479Exploit(webexploit.WebExploit): + accept_request = definitions.Request(method="GET", path="/") + accept_pattern = [re.compile(r"/_next/static")] + + def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: + vulnerabilities: list[definitions.Vulnerability] = [] + + root_response = requests.get( + target.origin, timeout=DEFAULT_TIMEOUT.seconds, verify=False + ) + if root_response.status_code != 200: + return vulnerabilities + + chunk_match = CHUNK_FILE_PATTERN.search(root_response.text) + if chunk_match is None: + return vulnerabilities + + chunk_path = chunk_match.group(1) + chunk_content = _fetch_chunk_file(target.origin, chunk_path) + + if chunk_content is not None: + extracted_version = _extract_version(chunk_content) + if ( + extracted_version is not None + and version.parse(extracted_version) <= MAX_VULNERABLE_VERSION + ): + 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_2024_51479_test.py b/tests/exploits/cve_2024_51479_test.py new file mode 100644 index 0000000..4e2d127 --- /dev/null +++ b/tests/exploits/cve_2024_51479_test.py @@ -0,0 +1,107 @@ +"""Unit tests for Agent Asteroid: CVE-2024-51479""" + +import requests_mock as req_mock +from agent import definitions +from agent.exploits import cve_2024_51479 + + +def testCVE202451479_whenVulnerable_reportFinding( + requests_mock: req_mock.Mocker, +) -> None: + """CVE-2024-51479 unit test: case when target is vulnerable.""" + exploit_instance = cve_2024_51479.CVE202451479Exploit() + + requests_mock.get( + "http://localhost:80/", + text='', + status_code=200, + ) + + requests_mock.get( + "http://localhost:80/_next/static/chunks/main-e9671ac36ff4266e.js", + text='t.version="14.2.14"', + status_code=200, + ) + + target = definitions.Target("http", "localhost", 80) + + accept = exploit_instance.accept(target) + vulnerabilities = exploit_instance.check(target) + + assert accept is True + assert len(vulnerabilities) > 0 + vulnerability = vulnerabilities[0] + assert vulnerability.entry.title == "Next.js Authorization Bypass via Middleware" + assert ( + vulnerability.technical_detail + == "http://localhost:80 is vulnerable to CVE-2024-51479, Next.js Authorization Bypass via Middleware" + ) + + +def testCVE202451479_whenSafe_reportNothing(requests_mock: req_mock.Mocker) -> None: + """CVE-2024-51479 unit test: case when target is safe.""" + exploit_instance = cve_2024_51479.CVE202451479Exploit() + + requests_mock.get( + "http://localhost:80/", + text='', + status_code=200, + ) + + requests_mock.get( + "http://localhost:80/_next/static/chunks/main-e9671ac36ff4266e.js", + text='t.version="14.2.15"', + status_code=200, + ) + + 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 testCVE202451479_whenChunkNotFound_reportNothing( + requests_mock: req_mock.Mocker, +) -> None: + """CVE-2024-51479 unit test: case when chunk file is not found.""" + requests_mock.get( + "http://localhost:80/", + text="No chunk here", + status_code=200, + ) + + exploit_instance = cve_2024_51479.CVE202451479Exploit() + target = definitions.Target("http", "localhost", 80) + + accept = exploit_instance.accept(target) + + assert accept is False + + +def testCVE202451479_whenChunkFileNotAccessible_reportNothing( + requests_mock: req_mock.Mocker, +) -> None: + """CVE-2024-51479 unit test: case when chunk file is inaccessible.""" + + requests_mock.get( + "http://localhost:80/", + text='', + status_code=200, + ) + + requests_mock.get( + "http://localhost:80/_next/static/chunks/main-e9671ac36ff4266e.js", + status_code=404, + ) + + exploit_instance = cve_2024_51479.CVE202451479Exploit() + target = definitions.Target("http", "localhost", 80) + + accept = exploit_instance.accept(target) + vulnerabilities = exploit_instance.check(target) + + assert accept is True + assert len(vulnerabilities) == 0