From 9e801e8bc3267ac5f8675fdb8af169c70221406f Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Mon, 21 Oct 2024 11:10:02 +0100 Subject: [PATCH 1/3] Add version based detection for CVE-2024-9164 --- agent/exploits/cve_2024_9164.py | 170 +++++++++++++++++++++++++++ tests/exploits/cve_2024_9164_test.py | 88 ++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 agent/exploits/cve_2024_9164.py create mode 100644 tests/exploits/cve_2024_9164_test.py diff --git a/agent/exploits/cve_2024_9164.py b/agent/exploits/cve_2024_9164.py new file mode 100644 index 0000000..909c8bf --- /dev/null +++ b/agent/exploits/cve_2024_9164.py @@ -0,0 +1,170 @@ +"""Agent Asteroid implementation for CVE-2024-9164""" + +import datetime +import re +from urllib import parse as urlparse + +import requests +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 = datetime.timedelta(seconds=90) + +VULNERABILITY_TITLE = "Gitlab Run pipelines on arbitrary branches." +VULNERABILITY_REFERENCE = "CVE-2024-9164" +VULNERABILITY_DESCRIPTION = ( + "An issue was discovered in GitLab EE affecting all versions starting from 12.5 prior to 17.2.9," + "starting from 17.3, prior to 17.3.5, and starting from 17.4 prior to 17.4.2, which allows running pipelines on arbitrary branches." +) +RISK_RATING = "CRITICAL" + +VULN_VERSIONS_HASHES = [ + "eb078ffe61726e3898dc9d01ea7955809778bde5be3677d907cbd3b48854e687", + "1626b2999241b5a658bddd1446648ed0b9cc289de4cc6e10f60b39681a0683c4", + "70ce56efa7e602d4b127087b0eca064681ecdd49b57d86665da8b081da39408b", + "40d8ac21e0e120f517fbc9a798ecb5caeef5182e01b7e7997aac30213ef367b3", + "ed4780bb05c30e3c145419d06ad0ab3f48bd3004a90fb99601f40c5b6e1d90fd", + "1084266bd81c697b5268b47c76565aa86b821126a6b9fe6ea7b50f64971fc96f", + "7310c45f08c5414036292b0c4026f281a73cf8a01af82a81257dd343f378bbb5", + "473ef436c59830298a2424616d002865f17bb5a6e0334d3627affa352a4fc117", + "4448d19024d3be03b5ba550b5b02d27f41c4bdba4db950f6f0e7136d820cd9e1", + "ae0edd232df6f579e19ea52115d35977f8bdbfa9958e0aef2221d62f3a39e7d8", + "bf1c397958ee5114e8f1dadc98fa9c9d7ddb031a4c3c030fa00c315384456218", + "c923fa3e71e104d50615978c1ab9fcfccfcbada9e8df638fc27bf4d4eb72d78c", + "655ad8aea57bdaaad10ff208c7f7aa88c9af89a834c0041ffc18c928cc3eab1f", + "81c5f2c7b2c0b0abaeb59585f36904031c21b1702c24349404df52834fbd7ad3", + "30a9dffe86b597151eff49443097496f0d1014bb6695a2f69a7c97dc1c27828f", + "b50bfeb87fe7bb245b31a0423ccfd866ca974bc5943e568ce47efb4cd221d711", + "ac9b38e86b6c87bf8db038ae23da3a5f17a6c391b3a54ad1e727136141a7d4f5", + "e2578590390a9eb10cd65d130e36503fccb40b3921c65c160bb06943b2e3751a", + "015d088713b23c749d8be0118caeb21039491d9812c75c913f48d53559ab09df", + "0993beabc8d2bb9e3b8d12d24989426b909921e20e9c6a704de7a5f1dfa93c59", + "62e4cc014d9d96f9cbf443186289ffd9c41bdfe951565324891dcf38bcca5a51", + "d0850f616c5b4f09a7ff319701bce0460ffc17ca0349ad2cf7808b868688cf71", + "08858ced0ff83694fb12cf155f6d6bf450dcaae7192ea3de8383966993724290", + "4990bb27037f3d5f1bffc0625162173ad8043166a1ae5c8505aabe6384935ce2", + "6ae610d783ba9a520b82263f49d2907a52090fecb3ac37819cea12b67e6d94fb", + "27d2c4c4e2fcf6e589e3e1fe85723537333b087003aa4c1d2abcf74d5c899959", + "e355f614211d036d0b3ffac4cd76da00d89e05717df61629e82571e20ac27488", + "a624c11e908db556820e9b07de96e0a465e9be5d5e6b68cdafe6d5c95c99798b", + "515dc29796a763b500d37ec0c765957a136c9e1f1972bb52c3d7edcf4b6b8bbe", + "c91127b2698c0a2ae0103be3accffe01995b8531bf1027ae4f0a8ad099e7a209", + "383b8952f0627703ada7774dd42f3b901ea2e499fd556fce3ae0c6d604ad72b7", + "e539e07c389f60596c92b06467c735073788196fa51331255d66ff7afde5dfee", + "2ea7e9be931f24ebc2a67091b0f0ff95ba18e386f3d312545bb5caaac6c1a8be", + "5df2cb13ec314995ea43d698e888ddb240dbc7ccb6e635434dc8919eced3e25f", + "a4333a9de660b9fc4d227403f57d46ec275d6a6349a6f5bda0c9557001f87e5d", + "ff058b10a8dce9956247adba2e410a7f80010a236b2269fb53e0df5cd091e61d", + "1d765038b21c5c76ff8492561c29984f3fa5c4b8cfb3a6c7b216ac8ab18b78c7", + "301b60d2c71a595adfb65b22edee9023961c5190e1807f6db7c597675b0a61f0", + "6fa9fec63ba24ec06fcae0ec30d1369619c2c3323fe9ddc4849af86457d59eef", + "f8ba2470fbf1e30f2ce64d34705b8e6615ac964ea84163c8a6adaaf8a91f9eac", + "6eb5eaa5726150b8135a4fd09118cfd6b29f128586b7fa5019a04f1c740e9193", + "6a58066d1bde4b6e661fbd5bde83d2dd90615ab409b8c8c36e04954fbd923424", + "1ae98447c220181b7bd2dfe88018cb6e1b1e4d12d7b8c224d651a48ed2d95dfe", + "a6d68fb0380bece011b0180b2926142630414c1d7a3e268fb461c51523b63778", + "95ae8966ec1e6021f2553c7d275217fcfecd5a7f0b206151c5fb701beb7baf1e", + "1e3d7f89df5b5446401d669796adf858c6742cb23a3d41b53f51a3c312c798cc", + "1caf2b894e48f649fcfd6b20de756e07ce64c1a756b9a20ff4505caeffa1a361", + "5a73588c1bf39ca7421dff5d8a03b887f0ebcb4897d6c136dfe03fa63a11b1ad", + "0ac028d3833c573c3e7cd054219213d6b36474cb4531a251238d9fa25831d97a", + "badd1d1762b624e5c7d251e045a3b2a3693054edbcb0115eb3cc64758fa62e92", + "7ac3b54e2ee2a129462d6702c9857a04f1a405f7254b89a2b3dfc7e73e80a489", + "dac7d8befc64f632fac344ebdca56066e013a4e260c3bdfb375fcf6dd30c3c56", + "f6b592d2e7570ce5d28f3dbf7170c0b3aa19dcb951f8c9e9ebe6cd5ec44691e8", + "c19a43b56f2b869d14e8b8865f2673e99e95a3f9912c95776e2aa44d0de56416", + "e7b1efce983de69e755cecf958ad52fe9c578fee502ec2046fbe4d418e12c237", + "a727ffdf1055442288d2fbc8b2ab23c38185697594725ea899f9faa653fad748", + "fb3d9b8e0a1937690eb35bd8b7a8686b51f36226bc9c29f0ea1621d568030c1a", + "ad779fabb121ac9d0c594b996d2fddb1fee9ce36c886c73c3d0e6fe10233819b", + "106174ae025caa47e54ebda6fb32a305a86857c1a808ab1a76a697e8a286166c", + "198963a1fcb2babf24b04c7c3a57fe439f453a90e1820ef7228d23a420b137ec", + "6687cea99d08705f41c22ba6c0f625668940d80a47e99488bf0d3d4bafa9d398", + "1d30846db8d63a25cb7d1a54a8ef11af9382c4d5e95fcc6ba7c8f34f9581d51d", + "c10b409df4222d166bfe9b34b787e05a671f9116b47ee10265029bfda6c5305b", + "f79ed5a6b0dfecf39281aeefcdd5b15d7cc6d871a3ae20e60c40d6a718377704", + "7649badc780fc4e44c1b7063c6153b3c216a5f6f0d7907eed714fac2a39ede73", + "47ec6054e1998d4cb2623b09faa02993de5f7cc92d34ba60dad2646b3b92a83e", + "e357f1b1a6812a8e72a4aec06b3761062a281129d621bf2e580806a8fca6302d", + "aecafb937628c9745dc285b2cb2c41d8a57846c934c2168b0cfec35b3d44e51f", + "73616daa0479bf77369c603146e745cc4cb9874b3b38f81fb2993818049dbf1a", + "58ebcd8f96ecc2ebf7f29122395e38ee28dc833dfc6d08fb667d2655da971df6", + "05a4322b27a3352f9638610b6a2528a03f90070a19fdb9e0499bb0412aad92fb", + "1b91aa4fc5e5ae49577087b2b42821ac87b863ba4de61cdccdd6b3620f587608", + "64ec030272495820a69a90e76affc1d0c47377c80f267ed21fa039c40404d4c9", + "22918e5e48d718e0977422d6c63347ce3199ac16206c958518b968c239529900", +] + +POTENTIALLY_VULN_VERSIONS_HASHES = [ + "6479d7d7b19cce99a8971e85a3756ea6f1debe89b68d0a86e69cc9c6e7414d7d", + "b52d3dd3b307bb936ad862671cae67163327a698f3dc2c4f232173d621af2a87", + "6a54b783019af16f39492a7c048d885d232d3940d8e5528940d70b6a6d2d0eb0", +] + + +@exploits_registry.register +class CVE20249164Exploit(webexploit.WebExploit): + accept_request = definitions.Request(method="GET", path="/users/sign_in") + check_request = definitions.Request(method="GET", path="/users/sign_in") + accept_pattern = [re.compile("GitLab")] + + 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() + # Requests library standardizes headers before submitting the request + # We override this behavior to allow for unsupported cases like headers with + # Duplicate names. We ignore typing because we assign dict to CaseInsensitiveDict + if self.check_request.headers is not None: + self.check_request.headers.update(req.headers) + req.headers = self.check_request.headers # type: ignore + resp = session.send(req, timeout=DEFAULT_TIMEOUT.seconds, verify=False) + except requests_exceptions.RequestException: + return vulnerabilities + + all_hashes = VULN_VERSIONS_HASHES + POTENTIALLY_VULN_VERSIONS_HASHES + for version_hash in all_hashes: + pattern = re.compile(version_hash) + if pattern.search(resp.text) is not None: + if version_hash in POTENTIALLY_VULN_VERSIONS_HASHES: + self.metadata = definitions.VulnerabilityMetadata( + title=VULNERABILITY_TITLE, + description=VULNERABILITY_DESCRIPTION, + reference=VULNERABILITY_REFERENCE, + risk_rating="POTENTIALLY", + ) + vulnerability = self._create_vulnerability(target) + vulnerabilities.append(vulnerability) + return vulnerabilities + + return vulnerabilities diff --git a/tests/exploits/cve_2024_9164_test.py b/tests/exploits/cve_2024_9164_test.py new file mode 100644 index 0000000..275d561 --- /dev/null +++ b/tests/exploits/cve_2024_9164_test.py @@ -0,0 +1,88 @@ +"""Unit tests for Agent Asteriod: CVE-2024-9164""" + +import requests_mock as req_mock + +from agent import definitions +from agent.exploits import cve_2024_9164 + + +def testCVE20249164_whenVulnerable_reportFinding( + requests_mock: req_mock.mocker.Mocker, +) -> None: + """Test exploit report finding when version is vulnerable.""" + target = definitions.Target("https", "localhost", 8080) + requests_mock.get( + target.url + "users/sign_in", + status_code=200, + text="Sign in · GitLab" + '', + ) + + exploit_instance = cve_2024_9164.CVE20249164Exploit() + 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 == "Gitlab Run pipelines on arbitrary branches." + assert ( + vulnerability.technical_detail + == "https://localhost:8080 is vulnerable to CVE-2024-9164, Gitlab Run pipelines on arbitrary branches." + ) + assert vulnerability.risk_rating.name == "CRITICAL" + + +def testCVE20249164_whenPotentiallyVulnerable_reportFinding( + requests_mock: req_mock.mocker.Mocker, +) -> None: + """Test exploit report finding when version is potentially vulnerable.""" + target = definitions.Target("https", "localhost", 8080) + requests_mock.get( + target.url + "users/sign_in", + status_code=200, + text="Sign in · GitLab" + '', + ) + + exploit_instance = cve_2024_9164.CVE20249164Exploit() + 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 == "Gitlab Run pipelines on arbitrary branches." + assert ( + vulnerability.technical_detail + == "https://localhost:8080 is vulnerable to CVE-2024-9164, Gitlab Run pipelines on arbitrary branches." + ) + assert vulnerability.risk_rating.name == "POTENTIALLY" + + +def testCVE20249164_whenNotVulnerable_reportNoFinding( + requests_mock: req_mock.mocker.Mocker, +) -> None: + """Test exploit don't report finding.""" + target = definitions.Target("https", "localhost", 8080) + requests_mock.get( + target.url + "users/sign_in", + status_code=200, + text="Sign in · GitLab" + '', + ) + + exploit_instance = cve_2024_9164.CVE20249164Exploit() + accept = exploit_instance.accept(target) + vulnerabilities = exploit_instance.check(target) + + assert accept is True + assert len(vulnerabilities) == 0 From 954b77dcf56da2b3adacdecd4e2c858f43a2cbd1 Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Mon, 21 Oct 2024 11:39:21 +0100 Subject: [PATCH 2/3] fix codecov --- agent/exploits/cve_2024_9164.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/agent/exploits/cve_2024_9164.py b/agent/exploits/cve_2024_9164.py index 909c8bf..ad47875 100644 --- a/agent/exploits/cve_2024_9164.py +++ b/agent/exploits/cve_2024_9164.py @@ -142,12 +142,6 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: url=target_endpoint, data=self.check_request.data, ).prepare() - # Requests library standardizes headers before submitting the request - # We override this behavior to allow for unsupported cases like headers with - # Duplicate names. We ignore typing because we assign dict to CaseInsensitiveDict - if self.check_request.headers is not None: - self.check_request.headers.update(req.headers) - req.headers = self.check_request.headers # type: ignore resp = session.send(req, timeout=DEFAULT_TIMEOUT.seconds, verify=False) except requests_exceptions.RequestException: return vulnerabilities From aa6797b491b8a1a506dc20372502016ab23cba1e Mon Sep 17 00:00:00 2001 From: ybadaoui-ostorlab Date: Tue, 22 Oct 2024 10:57:39 +0100 Subject: [PATCH 3/3] Refactore/separate the check between the vulnerable versions and the potentially vulnerable versions --- agent/exploits/cve_2024_9164.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/agent/exploits/cve_2024_9164.py b/agent/exploits/cve_2024_9164.py index ad47875..6d77b9c 100644 --- a/agent/exploits/cve_2024_9164.py +++ b/agent/exploits/cve_2024_9164.py @@ -132,8 +132,6 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: session.max_redirects = MAX_REDIRECTS session.verify = False - vulnerabilities: list[definitions.Vulnerability] = [] - target_endpoint = urlparse.urljoin(target.origin, self.check_request.path) try: @@ -144,21 +142,24 @@ def check(self, target: definitions.Target) -> list[definitions.Vulnerability]: ).prepare() resp = session.send(req, timeout=DEFAULT_TIMEOUT.seconds, verify=False) except requests_exceptions.RequestException: - return vulnerabilities + return [] + + for version_hash in VULN_VERSIONS_HASHES: + pattern = re.compile(version_hash) + if pattern.search(resp.text) is not None: + vulnerability = self._create_vulnerability(target) + return [vulnerability] - all_hashes = VULN_VERSIONS_HASHES + POTENTIALLY_VULN_VERSIONS_HASHES - for version_hash in all_hashes: + for version_hash in POTENTIALLY_VULN_VERSIONS_HASHES: pattern = re.compile(version_hash) if pattern.search(resp.text) is not None: - if version_hash in POTENTIALLY_VULN_VERSIONS_HASHES: - self.metadata = definitions.VulnerabilityMetadata( - title=VULNERABILITY_TITLE, - description=VULNERABILITY_DESCRIPTION, - reference=VULNERABILITY_REFERENCE, - risk_rating="POTENTIALLY", - ) + self.metadata = definitions.VulnerabilityMetadata( + title=VULNERABILITY_TITLE, + description=VULNERABILITY_DESCRIPTION, + reference=VULNERABILITY_REFERENCE, + risk_rating="POTENTIALLY", + ) vulnerability = self._create_vulnerability(target) - vulnerabilities.append(vulnerability) - return vulnerabilities + return [vulnerability] - return vulnerabilities + return []