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

feature/Add-detection-for-CVE-2024-50379 #167

Merged
merged 5 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 53 additions & 16 deletions agent/exploits/cve_2024_50379.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from agent import exploits_registry
from agent.exploits import webexploit
from packaging import version
import asyncio
import aiohttp

DEFAULT_TIMEOUT = datetime.timedelta(seconds=90)

Expand All @@ -28,6 +30,45 @@
(version.parse("10.1.0"), version.parse("10.1.34")),
(version.parse("11.0.0"), version.parse("11.0.2")),
]
TEST_FILE = "detect.Jsp"
HARMLESS_CONTENT = "<!-- detection test -->"


async def _test_race_condition(session: aiohttp.ClientSession, url: str) -> bool:
"""
Test for race condition by making concurrent requests.
"""

async def concurrent_put() -> bool:

Check warning on line 42 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L42

Added line #L42 was not covered by tests
"""Make concurrent PUT requests"""
try:
async with session.put(

Check warning on line 45 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L44-L45

Added lines #L44 - L45 were not covered by tests
f"{url}/{TEST_FILE}",
data=HARMLESS_CONTENT,
headers={"Content-Type": "text/plain"},
) as response:
return 200 <= response.status < 300
except aiohttp.ClientError:
return False

Check warning on line 52 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L50-L52

Added lines #L50 - L52 were not covered by tests

async def concurrent_get() -> bool:

Check warning on line 54 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L54

Added line #L54 was not covered by tests
"""Make GET request to the standard extension"""
try:
async with session.get(f"{url}/{TEST_FILE.lower()}") as response:
return response.status == 200
except aiohttp.ClientError:
return False

Check warning on line 60 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L56-L60

Added lines #L56 - L60 were not covered by tests

# Create two PUT tasks and multiple GET tasks (more GETs as recommended in PoC)
put_tasks = [concurrent_put() for _ in range(2)]
get_tasks = [concurrent_get() for _ in range(5)] # More GET requests as per PoC

Check warning on line 64 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L63-L64

Added lines #L63 - L64 were not covered by tests

# Execute all tasks concurrently
all_tasks = put_tasks + get_tasks
results = await asyncio.gather(*all_tasks, return_exceptions=True)

Check warning on line 68 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L67-L68

Added lines #L67 - L68 were not covered by tests

get_results = results[len(put_tasks) :]
return any(isinstance(r, bool) and r for r in get_results)

Check warning on line 71 in agent/exploits/cve_2024_50379.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_50379.py#L70-L71

Added lines #L70 - L71 were not covered by tests


def _is_version_vulnerable(version_str: str) -> bool:
Expand Down Expand Up @@ -65,30 +106,26 @@
if not (200 <= root_response.status_code < 600 and root_response.text):
return vulnerabilities

# Extract version information
version_match = VERSION_PATTERN.search(root_response.text)
if version_match is None:
return vulnerabilities

tomcat_version = version_match.group(1)
if _is_version_vulnerable(tomcat_version) is False:
return vulnerabilities
# Test for race condition
async def run_race_test() -> None:
async with aiohttp.ClientSession() as session:
is_vulnerable = await _test_race_condition(session, target.origin)
if is_vulnerable is True:
vulnerability = self._create_vulnerability(target)
vulnerabilities.append(vulnerability)

# Test for the PUT capability that enables the race condition
test_response = requests.put(
f"{target.origin}/test.Jsp",
data="<!-- test -->",
timeout=DEFAULT_TIMEOUT.seconds,
verify=False,
)
asyncio.run(run_race_test())

if (
test_response.status_code in (201, 204)
or _is_version_vulnerable(tomcat_version) is True
):
tomcat_version = version_match.group(1)
if _is_version_vulnerable(tomcat_version) is True:
vulnerabilities = [self._create_vulnerability(target)]
return vulnerabilities

except requests.RequestException:
except (requests.RequestException, aiohttp.ClientError, asyncio.TimeoutError):
return vulnerabilities

return vulnerabilities
Expand Down
3 changes: 2 additions & 1 deletion requirement.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ pysnmp==4.4.6
pyasn1==0.6.0
jaydebeapi
cloudscraper
psycopg
psycopg
aiohttp
95 changes: 62 additions & 33 deletions tests/exploits/cve_2024_50379_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Unit tests for Agent Asteroid: CVE-2024-50379"""

import aiohttp
import pytest
import requests
import requests_mock as req_mock
from agent import definitions
from agent.exploits import cve_2024_50379
from unittest import mock


def testCVE202450379_whenVulnerable_reportFinding(
Expand All @@ -19,28 +22,24 @@
status_code=200,
)

# Mock the PUT request test
requests_mock.put(
"http://localhost:80/test.Jsp",
status_code=201,
)
with mock.patch(
"agent.exploits.cve_2024_50379._test_race_condition",
new_callable=mock.AsyncMock,
) as mock_test:
mock_test.return_value = True

target = definitions.Target("http", "localhost", 80)
target = definitions.Target("http", "localhost", 80)

accept = exploit_instance.accept(target)
vulnerabilities = exploit_instance.check(target)
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
== "Apache Tomcat Race Condition Remote Code Execution"
)
assert (
vulnerability.technical_detail
== "http://localhost:80 is vulnerable to CVE-2024-50379, Apache Tomcat Race Condition Remote Code Execution"
)
assert accept is True
assert len(vulnerabilities) > 0
vulnerability = vulnerabilities[0]
assert (
vulnerability.entry.title
== "Apache Tomcat Race Condition Remote Code Execution"
)


def testCVE202450379_whenSafeVersion_reportNothing(
Expand All @@ -55,13 +54,19 @@
status_code=200,
)

target = definitions.Target("http", "localhost", 80)
with mock.patch(
"agent.exploits.cve_2024_50379._test_race_condition",
new_callable=mock.AsyncMock,
) as mock_test:
mock_test.return_value = False

accept = exploit_instance.accept(target)
vulnerabilities = exploit_instance.check(target)
target = definitions.Target("http", "localhost", 80)

assert accept is True
assert len(vulnerabilities) == 0
accept = exploit_instance.accept(target)
vulnerabilities = exploit_instance.check(target)

assert accept is True
assert len(vulnerabilities) == 0


def testCVE202450379_whenNoTomcat_reportNothing(
Expand All @@ -82,27 +87,28 @@
assert accept is False


def testCVE202450379_whenPutNotAllowed_reportNothing(
def testCVE202450379_whenRaceConditionFails_reportNothing(
requests_mock: req_mock.Mocker,
) -> None:
"""CVE-2024-50379 unit test: case when PUT requests are not allowed."""
"""CVE-2024-50379 unit test: case when race condition test fails."""
exploit_instance = cve_2024_50379.CVE202450379Exploit()

requests_mock.get(
"http://localhost:80/",
status_code=200,
)

requests_mock.put(
"http://localhost:80/test.Jsp",
status_code=403,
)
with mock.patch(
"agent.exploits.cve_2024_50379._test_race_condition",
new_callable=mock.AsyncMock,
) as mock_test:
mock_test.return_value = False

target = definitions.Target("http", "localhost", 80)
target = definitions.Target("http", "localhost", 80)

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

assert len(vulnerabilities) == 0
assert len(vulnerabilities) == 0


def testCVE202450379_whenConnectionError_reportNothing(
Expand Down Expand Up @@ -144,3 +150,26 @@

assert accept is True
assert len(vulnerabilities) == 0


@pytest.mark.asyncio
async def test_race_condition_function() -> None:
"""Test the race condition detection function directly."""
async with aiohttp.ClientSession() as session:

Check warning on line 158 in tests/exploits/cve_2024_50379_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_50379_test.py#L158

Added line #L158 was not covered by tests
# Mock the session's put and get methods
session.put = mock.AsyncMock() # type: ignore[method-assign]
session.get = mock.AsyncMock() # type: ignore[method-assign]

Check warning on line 161 in tests/exploits/cve_2024_50379_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_50379_test.py#L160-L161

Added lines #L160 - L161 were not covered by tests

# Configure mock responses
put_response = mock.AsyncMock()
put_response.status = 201
session.put.return_value.__aenter__.return_value = put_response

Check warning on line 166 in tests/exploits/cve_2024_50379_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_50379_test.py#L164-L166

Added lines #L164 - L166 were not covered by tests

get_response = mock.AsyncMock()
get_response.status = 200
session.get.return_value.__aenter__.return_value = get_response

Check warning on line 170 in tests/exploits/cve_2024_50379_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_50379_test.py#L168-L170

Added lines #L168 - L170 were not covered by tests

result = await cve_2024_50379._test_race_condition(

Check warning on line 172 in tests/exploits/cve_2024_50379_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_50379_test.py#L172

Added line #L172 was not covered by tests
session, "http://localhost:80"
)
assert result is True

Check warning on line 175 in tests/exploits/cve_2024_50379_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_50379_test.py#L175

Added line #L175 was not covered by tests
Loading