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
86 changes: 86 additions & 0 deletions agent/exploits/cve_2024_9487.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Agent Asteroid implementation for CVE-2024-9487"""

import re
from urllib import parse as urlparse

import requests
from packaging import version
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
from requests import exceptions as requests_exceptions

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

MAX_REDIRECTS = 2
DEFAULT_TIMEOUT = 90
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
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"
FIXED_VERSIONS = {
"3.11": "3.11.6",
"3.12": "3.12.10",
"3.13": "3.13.5",
"3.14": "3.14.2",
}


@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)
except requests_exceptions.RequestException:
return vulnerabilities

if (matched := self.version_pattern.findall(resp.text)) is not None:
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
for extracted_version in matched:
if isinstance(extracted_version, tuple):
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
extracted_version = extracted_version[0]
if version.parse(extracted_version) < version.parse("3.15"):
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
if version.parse(extracted_version) < version.parse("3.10"):
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
vulnerability = self._create_vulnerability(target)
vulnerabilities.append(vulnerability)
continue
version_prefix = ".".join(extracted_version.split(".")[:2])
if version.parse(extracted_version) < version.parse(
FIXED_VERSIONS[version_prefix]
):
vulnerability = self._create_vulnerability(target)
vulnerabilities.append(vulnerability)
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
return vulnerabilities
109 changes: 109 additions & 0 deletions tests/exploits/cve_2024_9487_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""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 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 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.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"
)


def testCVE20249487_whenMultiVersions_doNotCrash(
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.2</span>
<span class="f6 color-fg-muted">GitHub Enterprise Server 3.14.3</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
Loading