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

Adding CVE-2024-6633 & Fixing errors encountered caused by it when it… #109

Merged
merged 5 commits into from
Sep 4, 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
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
FROM python:3.11-slim as base
RUN apt-get update && apt-get install -y openjdk-11-jdk
ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
ENV CLASSPATH=/app/agent/Resources/hsqldb.jar:$CLASSPATH
FROM base as builder
RUN mkdir /install
WORKDIR /install
Expand Down
130 changes: 130 additions & 0 deletions agent/exploits/cve_2024_6633.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Agent Asteroid implementation for CVE-2024-6633"""

import logging
import socket
import jaydebeapi # type: ignore

from ostorlab.agent.kb import kb
from ostorlab.agent.mixins import agent_report_vulnerability_mixin

from agent import definitions
from agent import exploits_registry

VULNERABILITY_TITLE = "Default Credentials in Fortra FileCatalyst Workflow HSQLDB"
VULNERABILITY_REFERENCE = "CVE-2024-6633"
VULNERABILITY_DESCRIPTION = (
"Fortra FileCatalyst Workflow versions less than 5.1.7 are vulnerable to a critical issue where "
"default credentials for the internal HSQLDB are exposed. Attackers can exploit this by accessing "
"the database remotely using these credentials via TCP ports 4406 and 9001."
)

DEFAULT_PORTS = [4406, 9001]
DEFAULT_USER = "sa"
DEFAULT_PASSWORD = ""

logger = logging.getLogger(__name__)


def _create_vulnerability(target: definitions.Target) -> definitions.Vulnerability:
"""Creates a vulnerability report based on the detection."""
entry = kb.Entry(
title=VULNERABILITY_TITLE,
risk_rating="CRITICAL",
short_description=VULNERABILITY_DESCRIPTION,
description=VULNERABILITY_DESCRIPTION,
references={
"nvd.nist.gov": f"https://nvd.nist.gov/vuln/detail/{VULNERABILITY_REFERENCE}",
},
recommendation=(
"- Upgrade FileCatalyst Workflow to version 5.1.7 or later.\n"
"- Restrict access to TCP ports 4406 and 9001 from untrusted sources.\n"
"- Monitor database access and consider replacing the default HSQLDB."
),
security_issue=True,
privacy_issue=False,
has_public_exploit=False,
targeted_by_malware=False,
targeted_by_ransomware=False,
targeted_by_nation_state=False,
)
technical_detail = (
f"{target.origin} is vulnerable to {VULNERABILITY_REFERENCE}: "
f"{VULNERABILITY_TITLE}"
)
return definitions.Vulnerability(
entry=entry,
technical_detail=technical_detail,
risk_rating=agent_report_vulnerability_mixin.RiskRating.CRITICAL,
)


def _is_port_open(host: str, port: int) -> bool:
"""Check if the specified port is open on the target host."""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = s.connect_ex((host, port))
s.close()
return result == 0
except socket.error as error:
logger.error("Socket error occurred while checking port %s: %s", port, error)
return False


def _attempt_db_connection(db_host: str, port: int) -> bool:
"""Attempt to connect to the database using the specified port."""
try:
conn = jaydebeapi.connect(
"org.hsqldb.jdbcDriver",
f"jdbc:hsqldb:hsql://{db_host}:{port}/hsqldb",
[DEFAULT_USER, DEFAULT_PASSWORD],
"../Resources/hsqldb.jar",
)
conn.close()
return True

except jaydebeapi.DatabaseError as e:
logger.error("Database error occurred while connecting: %s", e)
return False
# It was handled like that because the error is coming from a java class java.sql.SQLTransientConnectionException.
except Exception as e:
logger.error("Unexpected error occurred: %s", e)
return False

Check warning on line 91 in agent/exploits/cve_2024_6633.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_6633.py#L89-L91

Added lines #L89 - L91 were not covered by tests


def _detect_vulnerability(host: str) -> bool:
"""Detects if the target host is vulnerable to CVE-2024-6633."""
for port in DEFAULT_PORTS:
if _is_port_open(host, port) is True:
logger.debug("Port %s is open. Attempting to connect...", port)
if _attempt_db_connection(host, port) is True:
logger.debug(
"Vulnerable: Default HSQLDB credentials are active and accessible on port %s.",
port,
)
return True
else:
logger.debug(
"Port %s is open but could not connect with default credentials.",
port,
)
else:
logger.debug("Port %s is not open. Host may not be vulnerable.", port)
return False


@exploits_registry.register
class CVE20246633Exploit(definitions.Exploit):
"""Exploit for CVE-2024-6633: Default Credentials in Fortra FileCatalyst Workflow HSQLDB"""

def accept(self, target: definitions.Target) -> bool:
"""Check if the target is valid for this exploit."""
return any(_is_port_open(target.host, port) for port in DEFAULT_PORTS)

def check(self, target: definitions.Target) -> list[definitions.Vulnerability]:
"""Checks for vulnerabilities in the target."""
vulnerabilities = []

if _detect_vulnerability(target.host) is True:
vulnerabilities.append(_create_vulnerability(target))

return vulnerabilities
3 changes: 2 additions & 1 deletion requirement.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ packaging
pygments
semantic_version
impacket
pysnmp==4.4.6
pysnmp==4.4.6
jaydebeapi
187 changes: 187 additions & 0 deletions tests/exploits/cve_2024_6633_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""Unit tests for Agent Asteroid: CVE-2024-6633"""

import socket
from unittest import mock
import jaydebeapi # type: ignore

from agent import definitions
from agent.exploits import cve_2024_6633


def testAccept_whenPortIsOpen_reportTrue() -> None:
"""Test the accept method when the target port is open."""
with mock.patch("agent.exploits.cve_2024_6633._is_port_open", return_value=True):
exploit_instance = cve_2024_6633.CVE20246633Exploit()
target = definitions.Target("tcp", "192.168.1.1", 4406)
assert exploit_instance.accept(target) is True


def testAccept_whenPortIsClosed_reportFalse() -> None:
"""Test the accept method when the target port is closed."""
with mock.patch("agent.exploits.cve_2024_6633._is_port_open", return_value=False):
exploit_instance = cve_2024_6633.CVE20246633Exploit()
target = definitions.Target("tcp", "192.168.1.1", 4406)
assert exploit_instance.accept(target) is False


def testCheck_whenVulnerable_reportFinding() -> None:
"""Test the check method when a vulnerability is found."""
with mock.patch(
"agent.exploits.cve_2024_6633._detect_vulnerability", return_value=True
):
exploit_instance = cve_2024_6633.CVE20246633Exploit()
target = definitions.Target("tcp", "192.168.1.1", 4406)
vulnerabilities = exploit_instance.check(target)
assert len(vulnerabilities) == 1
assert (
vulnerabilities[0].entry.title
== "Default Credentials in Fortra FileCatalyst Workflow HSQLDB"
)


def testCheck_whenNotVulnerable_reportNothing() -> None:
"""Test the check method when no vulnerability is found."""
with mock.patch(
"agent.exploits.cve_2024_6633._detect_vulnerability", return_value=False
):
exploit_instance = cve_2024_6633.CVE20246633Exploit()
target = definitions.Target("tcp", "192.168.1.1", 4406)
vulnerabilities = exploit_instance.check(target)
assert len(vulnerabilities) == 0


@mock.patch("agent.exploits.cve_2024_6633.socket.socket")
def testIsPortOpen_whenPortIsOpen_reportTrue(mock_socket: mock.MagicMock) -> None:
"""Test _is_port_open when the port is open."""
mock_socket_instance = mock_socket.return_value
mock_socket_instance.connect_ex.return_value = 0 # Simulate port being open

result = cve_2024_6633._is_port_open("192.168.1.1", 4406)

assert (
result is True
), f"Expected True, but got {result}. The port should be reported as open."


@mock.patch("agent.exploits.cve_2024_6633.socket.socket")
def testIsPortOpen_whenPortIsClosed_reportFalse(mock_socket: mock.MagicMock) -> None:
"""Test _is_port_open when the port is closed."""
mock_socket_instance = mock_socket.return_value
mock_socket_instance.connect_ex.return_value = 1 # Simulate port being closed

result = cve_2024_6633._is_port_open("192.168.1.1", 4406)

assert (
result is False
), f"Expected False, but got {result}. The port should be reported as closed."


@mock.patch("agent.exploits.cve_2024_6633.jaydebeapi.connect")
def testAttemptDbConnection_whenSuccessful_reportTrue(
mock_connect: mock.MagicMock,
) -> None:
"""Test _attempt_db_connection when the connection is successful."""
mock_conn = mock.MagicMock()
mock_connect.return_value = mock_conn

result = cve_2024_6633._attempt_db_connection("192.168.1.1", 9001)

assert result is True
mock_conn.close.assert_called_once()


@mock.patch("agent.exploits.cve_2024_6633.jaydebeapi.connect")
@mock.patch("agent.exploits.cve_2024_6633.logger")
def testAttemptDbConnection_whenConnectionFails_reportFalse(
mock_logger: mock.MagicMock,
mock_connect: mock.MagicMock,
) -> None:
"""Test _attempt_db_connection when the connection fails due to DatabaseError."""
mock_connect.side_effect = jaydebeapi.DatabaseError("Test DB error")

result = cve_2024_6633._attempt_db_connection("192.168.1.1", 9001)

assert result is False, "Expected False when connection fails"
mock_logger.error.assert_called_once_with(
"Database error occurred while connecting: %s", mock.ANY
)
error_message = mock_logger.error.call_args[0][1]
assert "Test DB error" in str(
error_message
), "Error message should contain the specific database error"


@mock.patch("agent.exploits.cve_2024_6633.jaydebeapi.connect")
@mock.patch("agent.exploits.cve_2024_6633.logger")
def testAttemptDbConnection_whenJavaExceptionOccurs_reportFalse(
mock_logger: mock.MagicMock,
mock_connect: mock.MagicMock,
) -> None:
"""Test _attempt_db_connection when a Java exception occurs."""
mock_connect.side_effect = jaydebeapi.DatabaseError(
"java.sql.SQLTransientConnectionException"
)

result = cve_2024_6633._attempt_db_connection("192.168.1.1", 9001)

assert result is False, "Expected False when a Java exception occurs"
mock_logger.error.assert_called_once_with(
"Database error occurred while connecting: %s", mock.ANY
)
error_message = mock_logger.error.call_args[0][1]
assert "java.sql.SQLTransientConnectionException" in str(
error_message
), "Error message should contain the specific Java exception"


@mock.patch("agent.exploits.cve_2024_6633.socket.socket")
def testIsPortOpen_whenSocketErrorOccurs_reportFalse(
mock_socket: mock.MagicMock,
) -> None:
"""Test _is_port_open when a socket.error occurs."""
mock_socket_instance = mock_socket.return_value
mock_socket_instance.connect_ex.side_effect = socket.error("Test socket error")

result = cve_2024_6633._is_port_open("192.168.1.1", 4406)

assert (
result is False
), f"Expected False, but got {result}. The function should return False when a socket.error occurs."


def testDetectVulnerability_whenPortOpenAndVulnerable_reportTrue() -> None:
"""Test _detect_vulnerability when the port is open and the target is vulnerable."""
with mock.patch(
"agent.exploits.cve_2024_6633._is_port_open", return_value=True
), mock.patch(
"agent.exploits.cve_2024_6633._attempt_db_connection", return_value=True
):
result = cve_2024_6633._detect_vulnerability("192.168.1.1")

assert (
result is True
), f"Expected True, but got {result}. The function should return True when the target is vulnerable."


def testDetectVulnerability_whenPortOpenButNotVulnerable_reportFalse() -> None:
"""Test _detect_vulnerability when the port is open but the target is not vulnerable."""
with mock.patch(
"agent.exploits.cve_2024_6633._is_port_open", return_value=True
), mock.patch(
"agent.exploits.cve_2024_6633._attempt_db_connection", return_value=False
):
result = cve_2024_6633._detect_vulnerability("192.168.1.1")

assert (
result is False
), f"Expected False, but got {result}. The function should return False when the target is not vulnerable."


def testDetectVulnerability_whenPortClosed_reportFalse() -> None:
"""Test _detect_vulnerability when the port is closed."""
with mock.patch("agent.exploits.cve_2024_6633._is_port_open", return_value=False):
result = cve_2024_6633._detect_vulnerability("192.168.1.1")

assert (
result is False
), f"Expected False, but got {result}. The function should return False when the port is closed."
Loading