From ee0ee11a62c249387b87a96db2bd6a88bf8e413e Mon Sep 17 00:00:00 2001 From: ablakley-r7 <96182471+ablakley-r7@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:27:32 +0000 Subject: [PATCH] [PLGN-406] Sentinelone Path Traversal (#2140) * Update to remove ../ and ..\ from file paths in the Threats Fetch File action * Reformat * Update dockerfile --- plugins/sentinelone/.CHECKSUM | 8 +- plugins/sentinelone/Dockerfile | 2 +- plugins/sentinelone/bin/komand_sentinelone | 2 +- plugins/sentinelone/help.md | 1 + .../actions/move_between_sites/schema.py | 2 +- .../komand_sentinelone/util/api.py | 12 +-- .../komand_sentinelone/util/helper.py | 12 +++ plugins/sentinelone/plugin.spec.yaml | 2 +- plugins/sentinelone/setup.py | 2 +- .../threats_fetch_file_invalid_path.json.inp | 4 + .../activities_malicious_file_path.json.resp | 80 +++++++++++++++++++ .../responses/threat_file_invalid_path.resp | 56 +++++++++++++ .../unit_test/test_threats_fetch_file.py | 5 ++ plugins/sentinelone/unit_test/util.py | 16 +++- 14 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 plugins/sentinelone/unit_test/inputs/threats_fetch_file_invalid_path.json.inp create mode 100644 plugins/sentinelone/unit_test/responses/activities_malicious_file_path.json.resp create mode 100644 plugins/sentinelone/unit_test/responses/threat_file_invalid_path.resp diff --git a/plugins/sentinelone/.CHECKSUM b/plugins/sentinelone/.CHECKSUM index 35525054c5..c982ad17e5 100644 --- a/plugins/sentinelone/.CHECKSUM +++ b/plugins/sentinelone/.CHECKSUM @@ -1,7 +1,7 @@ { - "spec": "dea9bcebb2466c647f32cff50ff770a2", - "manifest": "d22df2cb908420ee272ffd40aa540d14", - "setup": "3d39e9768c2fda42e7f413e0d7681642", + "spec": "6dcb8d0a5d1bac240f2a54503842098d", + "manifest": "fe468b544dd9d9c0578e754075945ea0", + "setup": "e88d857ff461a794516f4019571d3f6c", "schemas": [ { "identifier": "activities_list/schema.py", @@ -93,7 +93,7 @@ }, { "identifier": "move_between_sites/schema.py", - "hash": "da998f50504a8bd40a9696bd6612ebe2" + "hash": "c60390ee96acd2451273a7e9bee1f862" }, { "identifier": "name_available/schema.py", diff --git a/plugins/sentinelone/Dockerfile b/plugins/sentinelone/Dockerfile index 9b3fe60fb4..ac670294df 100755 --- a/plugins/sentinelone/Dockerfile +++ b/plugins/sentinelone/Dockerfile @@ -1,4 +1,4 @@ -FROM rapid7/insightconnect-python-3-38-slim-plugin:5 +FROM rapid7/insightconnect-python-3-plugin:5 LABEL organization=rapid7 LABEL sdk=python diff --git a/plugins/sentinelone/bin/komand_sentinelone b/plugins/sentinelone/bin/komand_sentinelone index e3c5c40ddc..ddd5cb45bf 100644 --- a/plugins/sentinelone/bin/komand_sentinelone +++ b/plugins/sentinelone/bin/komand_sentinelone @@ -6,7 +6,7 @@ from sys import argv Name = "SentinelOne" Vendor = "rapid7" -Version = "9.1.0" +Version = "9.1.1" Description = "The SentinelOne plugin allows you to manage and mitigate all your security operations through SentinelOne" diff --git a/plugins/sentinelone/help.md b/plugins/sentinelone/help.md index e221247a46..85b721a19f 100644 --- a/plugins/sentinelone/help.md +++ b/plugins/sentinelone/help.md @@ -2135,6 +2135,7 @@ Example output: # Version History +* 9.1.1 - `Threats Fetch File`: Updated action to prevent possible movement through file system * 9.1.0 - `Move Agent to Another Site`: Action added * 9.0.0 - Update plugin to allow cloud connections to be configured | Rename URL input to Instance in connection | Code refactor * 8.1.0 - Added New actions: Fetch file for agent ID and Run remote script. Updated description for Trigger resolved field diff --git a/plugins/sentinelone/komand_sentinelone/actions/move_between_sites/schema.py b/plugins/sentinelone/komand_sentinelone/actions/move_between_sites/schema.py index 5b9dea86a2..718861ff99 100644 --- a/plugins/sentinelone/komand_sentinelone/actions/move_between_sites/schema.py +++ b/plugins/sentinelone/komand_sentinelone/actions/move_between_sites/schema.py @@ -4,7 +4,7 @@ class Component: - DESCRIPTION = "Move an agent to another site. This action requires Account or Global level access for your user role" + DESCRIPTION = "Move an agent to another site, This action requires Account or Global level access for your user role" class Input: diff --git a/plugins/sentinelone/komand_sentinelone/util/api.py b/plugins/sentinelone/komand_sentinelone/util/api.py index 252ad50f8a..694d1bea5c 100644 --- a/plugins/sentinelone/komand_sentinelone/util/api.py +++ b/plugins/sentinelone/komand_sentinelone/util/api.py @@ -10,7 +10,7 @@ from typing import List, Any, Dict, Tuple from urllib.parse import urlsplit, unquote from logging import Logger -from komand_sentinelone.util.helper import Helper, clean +from komand_sentinelone.util.helper import Helper, clean, sanitise_url from komand_sentinelone.util.constants import ( DATA_FIELD, API_TOKEN_FIELD, @@ -251,20 +251,20 @@ def download_file(self, agent_filter: dict, fetch_date: str, password: str) -> d break self.logger.info("Waiting 5 seconds for successful threat file upload...") time.sleep(5) - threat_details = activities.get("data", [{}])[0].get("data", {}) file_url = threat_details.get("filePath", "")[1:] file_name = threat_details.get("fileDisplayName") - response = self._call_api("GET", file_url, full_response=True) + sanitised_file_url = sanitise_url(file_url) + response = self._call_api("GET", sanitised_file_url, full_response=True) try: with zipfile.ZipFile(io.BytesIO(response.content)) as downloaded_zipfile: downloaded_zipfile.setpassword(password.encode("UTF-8")) + file_info = downloaded_zipfile.infolist()[-1] + file_content = downloaded_zipfile.read(file_info.filename) return { "filename": file_name, - "content": base64.b64encode(downloaded_zipfile.read(downloaded_zipfile.infolist()[-1])).decode( - "utf-8" - ), + "content": base64.b64encode(file_content).decode("utf-8"), } except Exception as error: raise PluginException( diff --git a/plugins/sentinelone/komand_sentinelone/util/helper.py b/plugins/sentinelone/komand_sentinelone/util/helper.py index 4580a05d00..b307cc3a58 100755 --- a/plugins/sentinelone/komand_sentinelone/util/helper.py +++ b/plugins/sentinelone/komand_sentinelone/util/helper.py @@ -1,3 +1,5 @@ +import os + from insightconnect_plugin_runtime.exceptions import PluginException from insightconnect_plugin_runtime.helper import return_non_empty from typing import Union @@ -12,6 +14,16 @@ def clean(item_to_clean: Union[dict, list]) -> Union[dict, list]: return return_non_empty(item_to_clean.copy()) +def sanitise_url(file_url: str) -> str: + # Sanitise URLs to help guard against path traversal + sanitised_url = file_url.replace("%2e%2e%2f", "../") + sanitised_url = sanitised_url.replace("../", "") + sanitised_url = sanitised_url.replace("%2e%2e%5C", "..\\") + sanitised_url = sanitised_url.replace("..\\", "") + sanitised_url = os.path.normpath(sanitised_url) + return sanitised_url + + class Helper: @staticmethod def join_or_empty(joined_array: list) -> str: diff --git a/plugins/sentinelone/plugin.spec.yaml b/plugins/sentinelone/plugin.spec.yaml index 98318ced31..92bb37abb3 100644 --- a/plugins/sentinelone/plugin.spec.yaml +++ b/plugins/sentinelone/plugin.spec.yaml @@ -3,7 +3,7 @@ extension: plugin products: [insightconnect] name: sentinelone title: SentinelOne -version: 9.1.0 +version: 9.1.1 connection_version: 9 cloud_ready: true sdk: diff --git a/plugins/sentinelone/setup.py b/plugins/sentinelone/setup.py index a9fe023d03..c01daff51b 100644 --- a/plugins/sentinelone/setup.py +++ b/plugins/sentinelone/setup.py @@ -3,7 +3,7 @@ setup(name="sentinelone-rapid7-plugin", - version="9.1.0", + version="9.1.1", description="The SentinelOne plugin allows you to manage and mitigate all your security operations through SentinelOne", author="rapid7", author_email="", diff --git a/plugins/sentinelone/unit_test/inputs/threats_fetch_file_invalid_path.json.inp b/plugins/sentinelone/unit_test/inputs/threats_fetch_file_invalid_path.json.inp new file mode 100644 index 0000000000..74a5246d97 --- /dev/null +++ b/plugins/sentinelone/unit_test/inputs/threats_fetch_file_invalid_path.json.inp @@ -0,0 +1,4 @@ +{ + "id": "1000000000000000001", + "password": "Str0ngP455word" +} diff --git a/plugins/sentinelone/unit_test/responses/activities_malicious_file_path.json.resp b/plugins/sentinelone/unit_test/responses/activities_malicious_file_path.json.resp new file mode 100644 index 0000000000..092cf26711 --- /dev/null +++ b/plugins/sentinelone/unit_test/responses/activities_malicious_file_path.json.resp @@ -0,0 +1,80 @@ +{ + "data": [ + { + "accountId": "1234567898765432112", + "accountName": "Example", + "activityType": 19, + "agentId": "1234567898765432112", + "agentUpdatedVersion": null, + "comments": null, + "createdAt": "2020-12-17T22:39:24.305435Z", + "data": { + "accountName": "Example", + "computerName": "so-agent-win12", + "confidenceLevel": "malicious", + "fileContentHash": "02699626f388ed830012e5b787640e71c56d42d8", + "fileDisplayName": "test.txt", + "filePath": "\\..\\..\\Fake\\HarddiskVolume2\\Users\\Administrator\\Desktop\\test.txt", + "groupName": "Default Group", + "siteName": "Example", + "threatClassification": "Trojan", + "threatClassificationSource": "Cloud", + "username": null + }, + "description": null, + "groupId": "1234567898765432112", + "groupName": "Default Group", + "hash": null, + "id": "1234567898765432112", + "osFamily": null, + "primaryDescription": "Threat with confidence level malicious detected: test.txt", + "secondaryDescription": "02699626f388ed830012e5b787640e71c56d42d8", + "siteId": "1234567898765432112", + "siteName": "Example", + "threatId": "1234567898765432112", + "updatedAt": "2020-12-17T22:39:24.299235Z", + "userId": null + }, + { + "accountId": "1234567898765432112", + "accountName": "Example", + "activityType": 2001, + "agentId": "1234567898765432112", + "agentUpdatedVersion": null, + "comments": null, + "createdAt": "2020-12-17T22:39:24.423814Z", + "data": { + "accountName": "Example", + "computerName": "so-agent-win12", + "fileContentHash": "02699626f388ed830012e5b787640e71c56d42d8", + "fileDisplayName": "test.txt", + "filePath": "\\..\\..\\Fake\\HarddiskVolume2\\Users\\Administrator\\Desktop\\test.txt", + "fullScopeDetails": "Group Default Group in Site Example of Account Example", + "globalStatus": "success", + "groupName": "Default Group", + "scopeLevel": "Group", + "scopeName": "Default Group", + "siteName": "Example", + "threatClassification": "Trojan", + "threatClassificationSource": "Cloud" + }, + "description": null, + "groupId": "1234567898765432112", + "groupName": "Default Group", + "hash": null, + "id": "1234567898765432112", + "osFamily": null, + "primaryDescription": "The agent so-agent-win12 successfully killed the threat: test.txt.", + "secondaryDescription": "\\..\\..\\Fake\\HarddiskVolume2\\Users\\Administrator\\Desktop\\test.txt", + "siteId": "1234567898765432112", + "siteName": "Example", + "threatId": "1234567898765432112", + "updatedAt": "2020-12-17T22:39:24.419607Z", + "userId": null + } + ], + "pagination": { + "nextCursor": null, + "totalItems": 0 + } +} diff --git a/plugins/sentinelone/unit_test/responses/threat_file_invalid_path.resp b/plugins/sentinelone/unit_test/responses/threat_file_invalid_path.resp new file mode 100644 index 0000000000..196edd5db6 --- /dev/null +++ b/plugins/sentinelone/unit_test/responses/threat_file_invalid_path.resp @@ -0,0 +1,56 @@ +{ + "data": [ + { + "accountId": "1000000000000000001", + "accountName": "Account", + "activityType": 86, + "activityUuid": "aaaaa-aa-adsadsad-asadasd", + "agentId": "1000000000000000001", + "agentUpdatedVersion": null, + "comments": null, + "createdAt": "2023-09-27T12:16:43.525758Z", + "data": { + "accountName": "Rapid7", + "commandBatchUuid": "aaaaa-aa-adsadsad-asadasd", + "commandId": 1000000000000000001, + "computerName": "WindowsX64", + "downloadUrl": "../../fake/1000000000000000001/uploads/1000000000000000001", + "escapedMaliciousProcessArguments": null, + "externalIp": "0.0.0.0", + "fileContentHash": "1000000000000000000", + "fileDisplayName": "file.txt", + "filePath": "../../fake/1000000000000000001/uploads/1000000000000000001", + "fileSize": 68, + "filename": "WindowsX64_2023-09-27_12_16_43.503.zip", + "fullScopeDetails": "Details", + "fullScopeDetailsPath": "Details Path", + "groupName": "Default Group", + "ipAddress": null, + "realUser": null, + "siteName": "Example Site", + "sourceType": "API", + "storyline": "1000000000000000001", + "threatClassification": "Malware", + "threatClassificationSource": "Static", + "uploadedFilename": "WindowsX64_2023-09-27_07:16:43.zip" + }, + "description": null, + "groupId": "1000000000000000001", + "groupName": "Default Group", + "hash": null, + "id": "1000000000000000001", + "osFamily": null, + "primaryDescription": "Agent WindowsX64 (0.0.0.0) successfully uploaded a threat file.", + "secondaryDescription": "WindowsX64_2023-09-27_07:16:43.zip (Group ID: 1000000000000000001).", + "siteId": "1000000000000000001", + "siteName": "Example Site", + "threatId": "1000000000000000001", + "updatedAt": "2023-09-27T12:16:43.525095Z", + "userId": null + } + ], + "pagination": { + "nextCursor": null, + "totalItems": 1 + } +} diff --git a/plugins/sentinelone/unit_test/test_threats_fetch_file.py b/plugins/sentinelone/unit_test/test_threats_fetch_file.py index 733f42c671..94e66712f1 100644 --- a/plugins/sentinelone/unit_test/test_threats_fetch_file.py +++ b/plugins/sentinelone/unit_test/test_threats_fetch_file.py @@ -26,6 +26,11 @@ def setUpClass(cls, mock_request) -> None: Util.read_file_to_dict("inputs/threats_fetch_file.json.inp"), Util.read_file_to_dict("expected/threats_fetch_file.json.exp"), ], + [ + "threats_fetch_file_traversal_file_path", + Util.read_file_to_dict("inputs/threats_fetch_file_invalid_path.json.inp"), + Util.read_file_to_dict("expected/threats_fetch_file.json.exp"), + ], ] ) def test_threats_fetch_file(self, mock_request, test_name, input_params, expected): diff --git a/plugins/sentinelone/unit_test/util.py b/plugins/sentinelone/unit_test/util.py index 2e1017a909..c9827f7045 100644 --- a/plugins/sentinelone/unit_test/util.py +++ b/plugins/sentinelone/unit_test/util.py @@ -146,6 +146,8 @@ def raise_for_status(self): return MockResponse(200, "affected_2") return MockResponse(200, "affected_1") elif args[1] == "https://rapid7.sentinelone.net/web/api/v2.1/activities": + if params.get("threatIds") == "1000000000000000001": + return MockResponse(200, "activities_malicious_file_path") if params.get("countOnly"): return MockResponse(200, "activities_count_only") if params == { @@ -294,7 +296,12 @@ def raise_for_status(self): return MockResponse(200, "name_not_available") return MockResponse(200, "name_available") elif args[1] == "https://rapid7.sentinelone.net/web/api/v2.1/threats": - if params.get("ids") in [["valid_threat_id_1"], ["valid_threat_id_2"], ["1000000000000000000"]]: + if params.get("ids") in [ + ["valid_threat_id_1"], + ["valid_threat_id_2"], + ["1000000000000000000"], + ["1000000000000000001"], + ]: return MockResponse(200, "threats") if params.get("ids") == ["same_status_threat_id_1"]: return MockResponse(200, "threats_same_status") @@ -342,6 +349,13 @@ def raise_for_status(self): mock_response = MockResponse(200) mock_response.content = Util.read_file_to_bytes(f"responses/threats_fetch_file_download.zip.resp") return mock_response + elif ( + args[1] + == r"https://rapid7.sentinelone.net/web/api/v2.1/Fake\HarddiskVolume2\Users\Administrator\Desktop\test.txt" + ): + mock_response = MockResponse(200) + mock_response.content = Util.read_file_to_bytes(f"responses/threats_fetch_file_download.zip.resp") + return mock_response elif args[1] == "https://rapid7.sentinelone.net/web/api/v2.1/dv/events": if params.get("queryId") == "q9679169d5a4607cc41a9101234567891": return MockResponse(404)