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-2024-9487 #123

Merged
merged 11 commits into from
Oct 18, 2024
100 changes: 100 additions & 0 deletions agent/exploits/cve_2024_9487.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import datetime
import re
from urllib import parse as urlparse

import requests
from requests import exceptions as requests_exceptions
from semver import Version

from agent import definitions
from agent import exploits_registry
from agent.exploits import webexploit

MAX_REDIRECTS = 2
DEFAULT_TIMEOUT = datetime.timedelta(seconds=90)
VULNERABILITY_TITLE = "GITHUB ENTERPRISE SERVER AUTHENTICATION BYPASS"
VULNERABILITY_REFERENCE = "CVE-2024-9487"
VULNERABILITY_DESCRIPTION = """A cryptographic signature verification flaw in GitHub Enterprise Server allowed bypassing SAML SSO authentication,
leading to unauthorized user access. Exploitation required encrypted assertions, direct network access, and a signed SAML response or metadata.
It affected versions before 3.15 and was fixed in 3.11.16, 3.12.10, 3.13.5, and 3.14.2. The vulnerability was reported through GitHub's Bug Bounty program."""
RISK_RATING = "CRITICAL"

VULNERABLE_RANGES = [
(None, "3.10.17"),
("3.11.0", "3.11.5"),
("3.12.0", "3.12.9"),
("3.13.0", "3.13.4"),
("3.14.0", "3.14.1"),
]


@exploits_registry.register
class CVE20249487Exploit(webexploit.WebExploit):
accept_request = definitions.Request(method="GET", path="/")
check_request = definitions.Request(method="GET", path="/")
accept_pattern = [re.compile("GitHub\sEnterprise\sServer\s\d+\.\d+\.\d+")]
version_pattern = re.compile("GitHub\sEnterprise\sServer\s(\d+\.\d+\.\d+)")

metadata = definitions.VulnerabilityMetadata(
title=VULNERABILITY_TITLE,
description=VULNERABILITY_DESCRIPTION,
reference=VULNERABILITY_REFERENCE,
risk_rating=RISK_RATING,
)

def check(self, target: definitions.Target) -> list[definitions.Vulnerability]:
"""Rule to detect specific vulnerability on a specific target.

Args:
target: Target to scan

Returns:
List of identified vulnerabilities.
"""
session = requests.Session()
session.max_redirects = MAX_REDIRECTS
session.verify = False

vulnerabilities: list[definitions.Vulnerability] = []

target_endpoint = urlparse.urljoin(target.origin, self.check_request.path)

try:
req = requests.Request(
method=self.check_request.method,
url=target_endpoint,
data=self.check_request.data,
).prepare()
resp = session.send(req, timeout=DEFAULT_TIMEOUT.seconds)
except requests_exceptions.RequestException:
return vulnerabilities

Check warning on line 70 in agent/exploits/cve_2024_9487.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_9487.py#L69-L70

Added lines #L69 - L70 were not covered by tests

if (matched := self.version_pattern.findall(resp.text)) != []:
extracted_version = matched[0]

if _is_vulnerable(extracted_version) is True:
vulnerability = self._create_vulnerability(target)
vulnerabilities.append(vulnerability)

return vulnerabilities


def _is_vulnerable(extracted_version: str) -> bool:
"""Check if the extracted version is in the list of vulnerable ranges.

Args:
extracted_version: Version of GitHub Enterprise Server

Returns:
True if the version is vulnerable, False otherwise.
"""
extracted_ver = Version.parse(extracted_version)

for min_version, max_version in VULNERABLE_RANGES:
min_ver = Version.parse(min_version) if min_version else None
max_ver = Version.parse(max_version)

if (min_ver is None or extracted_ver >= min_ver) and extracted_ver <= max_ver:
return True

return False
85 changes: 85 additions & 0 deletions tests/exploits/cve_2024_9487_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Unit tests for Agent Asteroid: CVE_2024_9487"""

import requests_mock as req_mock

from agent import definitions
from agent.exploits import cve_2024_9487


def testCVE20249487_whenVulnerable_reportFinding(
requests_mock: req_mock.mocker.Mocker,
) -> None:
"""CVE_2024_9487 unit test: case when target is vulnerable."""
requests_mock.get(
"http://localhost:80/",
text="""
<div class="d-flex flex-justify-center py-2">
<span class="f6 color-fg-muted">GitHub Enterprise Server 3.14.1</span>
</div>
""",
status_code=200,
)
exploit_instance = cve_2024_9487.CVE20249487Exploit()
target = definitions.Target("http", "localhost", 80)

accept = exploit_instance.accept(target)
vulnerabilities = exploit_instance.check(target)

assert accept is True
vulnerability = vulnerabilities[0]
assert vulnerability.entry.title == "GITHUB ENTERPRISE SERVER AUTHENTICATION BYPASS"
assert vulnerability.technical_detail == (
"http://localhost:80 is vulnerable to CVE-2024-9487, "
"GITHUB ENTERPRISE SERVER AUTHENTICATION BYPASS"
)


def testCVE20249487_whenNotVulnerable_reportNothing(
requests_mock: req_mock.mocker.Mocker,
) -> None:
"""CVE_2024_9487 unit test: case when target is not vulnerable."""
requests_mock.get(
"http://localhost:80/",
text="""
<div class="d-flex flex-justify-center py-2">
<span class="f6 color-fg-muted">GitHub Enterprise Server 3.14.2</span>
</div>
""",
status_code=200,
)
exploit_instance = cve_2024_9487.CVE20249487Exploit()
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 testCVE20249487_whenVersionVeryOld_reportFinding(
requests_mock: req_mock.mocker.Mocker,
) -> None:
"""CVE_2024_9487 unit test: case when matched version is older that 3.11."""
requests_mock.get(
"http://localhost:80/",
text="""
<div class="d-flex flex-justify-center py-2">
<span class="f6 color-fg-muted">GitHub Enterprise Server 3.9.1</span>
</div>
""",
status_code=200,
)
exploit_instance = cve_2024_9487.CVE20249487Exploit()
target = definitions.Target("http", "localhost", 80)

accept = exploit_instance.accept(target)
vulnerabilities = exploit_instance.check(target)

assert accept is True
vulnerability = vulnerabilities[0]
assert vulnerability.entry.title == "GITHUB ENTERPRISE SERVER AUTHENTICATION BYPASS"
assert vulnerability.technical_detail == (
"http://localhost:80 is vulnerable to CVE-2024-9487, "
"GITHUB ENTERPRISE SERVER AUTHENTICATION BYPASS"
)
Loading