From a36fc81755d47f506fb82fbf24021d538e3d0fe5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 11 Jun 2021 11:37:35 +0300 Subject: [PATCH] Refactored configuration import and added a check to decide if configuration is encrypted or not. This solved a bug where invalid json was treated as credential error. --- monkey/common/utils/exceptions.py | 8 ---- .../cc/resources/configuration_import.py | 46 +++++++++++-------- .../cc/services/utils/config_encryption.py | 16 +++++-- .../ImportConfigModal.tsx | 20 ++++---- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index b13b94e3b..df40f3007 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -54,13 +54,5 @@ class DomainControllerNameFetchError(FailedExploitationError): """ Raise on failed attempt to extract domain controller's name """ -class InvalidCredentialsError(Exception): - """ Raise when credentials supplied are invalid """ - - -class NoCredentialsError(Exception): - """ Raise when no credentials have been supplied """ - - class InvalidConfigurationError(Exception): """ Raise when configuration is invalid """ diff --git a/monkey/monkey_island/cc/resources/configuration_import.py b/monkey/monkey_island/cc/resources/configuration_import.py index d9d39a3f8..50a4cf955 100644 --- a/monkey/monkey_island/cc/resources/configuration_import.py +++ b/monkey/monkey_island/cc/resources/configuration_import.py @@ -6,14 +6,14 @@ from json.decoder import JSONDecodeError import flask_restful from flask import request -from common.utils.exceptions import ( - InvalidConfigurationError, - InvalidCredentialsError, - NoCredentialsError, -) +from common.utils.exceptions import InvalidConfigurationError from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.utils.config_encryption import decrypt_config +from monkey_island.cc.services.utils.config_encryption import ( + InvalidCredentialsError, + decrypt_config, + is_encrypted, +) logger = logging.getLogger(__name__) @@ -21,8 +21,7 @@ logger = logging.getLogger(__name__) class ImportStatuses: UNSAFE_OPTION_VERIFICATION_REQUIRED = "unsafe_options_verification_required" INVALID_CONFIGURATION = "invalid_configuration" - PASSWORD_REQUIRED = "password_required" - WRONG_PASSWORD = "wrong_password" + INVALID_CREDENTIALS = "invalid_credentials" IMPORTED = "imported" @@ -57,7 +56,8 @@ class ConfigurationImport(flask_restful.Resource): ).form_response() except InvalidCredentialsError: return ResponseContents( - import_status=ImportStatuses.WRONG_PASSWORD, message="Wrong password supplied" + import_status=ImportStatuses.INVALID_CREDENTIALS, + message="Invalid credentials provided", ).form_response() except InvalidConfigurationError: return ResponseContents( @@ -65,20 +65,30 @@ class ConfigurationImport(flask_restful.Resource): message="Invalid configuration supplied. " "Maybe the format is outdated or the file has been corrupted.", ).form_response() - except NoCredentialsError: - return ResponseContents( - import_status=ImportStatuses.PASSWORD_REQUIRED, - ).form_response() @staticmethod def _get_plaintext_config_from_request(request_contents: dict) -> dict: - try: - config = json.loads(request_contents["config"]) - except JSONDecodeError: - config = decrypt_config(request_contents["config"], request_contents["password"]) - return config + if ConfigurationImport.is_config_encrypted(request_contents["config"]): + return decrypt_config(request_contents["config"], request_contents["password"]) + else: + try: + return json.loads(request_contents["config"]) + except JSONDecodeError: + raise InvalidConfigurationError @staticmethod def import_config(config_json): if not ConfigService.update_config(config_json, should_encrypt=True): raise InvalidConfigurationError + + @staticmethod + def is_config_encrypted(config: str): + try: + if config.startswith("{"): + return False + elif is_encrypted(config): + return True + else: + raise InvalidConfigurationError + except Exception: + raise InvalidConfigurationError diff --git a/monkey/monkey_island/cc/services/utils/config_encryption.py b/monkey/monkey_island/cc/services/utils/config_encryption.py index 49f5cc187..73c3ea893 100644 --- a/monkey/monkey_island/cc/services/utils/config_encryption.py +++ b/monkey/monkey_island/cc/services/utils/config_encryption.py @@ -6,7 +6,7 @@ from typing import Dict import pyAesCrypt -from common.utils.exceptions import InvalidCredentialsError, NoCredentialsError +from common.utils.exceptions import InvalidConfigurationError BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef @@ -28,9 +28,6 @@ def encrypt_config(config: Dict, password: str) -> str: def decrypt_config(cyphertext: str, password: str) -> Dict: - if not password: - raise NoCredentialsError - cyphertext = base64.b64decode(cyphertext) ciphertext_config_stream = io.BytesIO(cyphertext) dec_plaintext_config_stream = io.BytesIO() @@ -51,6 +48,15 @@ def decrypt_config(cyphertext: str, password: str) -> Dict: raise InvalidCredentialsError else: logger.info("The provided configuration file is corrupt.") - raise ex + raise InvalidConfigurationError plaintext_config = json.loads(dec_plaintext_config_stream.getvalue().decode("utf-8")) return plaintext_config + + +def is_encrypted(ciphertext: str) -> bool: + ciphertext = base64.b64decode(ciphertext) + return ciphertext.startswith(b"AES") + + +class InvalidCredentialsError(Exception): + """ Raise when credentials supplied are invalid """ diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx index c1e12ae86..155b33bf2 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx @@ -40,8 +40,8 @@ const ConfigImportModal = (props: Props) => { }, [configContents]) - function sendConfigToServer(): Promise { - return authComponent.authFetch(configImportEndpoint, + function sendConfigToServer() { + authComponent.authFetch(configImportEndpoint, { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -53,27 +53,30 @@ const ConfigImportModal = (props: Props) => { } ).then(res => res.json()) .then(res => { - if (res['import_status'] === 'password_required') { + if (res['import_status'] === 'invalid_credentials') { setUploadStatus(UploadStatuses.success); - setShowPassword(true); - } else if (res['import_status'] === 'wrong_password') { - setErrorMessage(res['message']); + if (showPassword){ + setErrorMessage(res['message']); + } else { + setShowPassword(true); + setErrorMessage(''); + } } else if (res['import_status'] === 'invalid_configuration') { setUploadStatus(UploadStatuses.error); setErrorMessage(res['message']); } else if (res['import_status'] === 'unsafe_options_verification_required') { + setUploadStatus(UploadStatuses.success); + setErrorMessage(''); if (isUnsafeOptionSelected(res['config_schema'], JSON.parse(res['config']))) { setShowUnsafeOptionsConfirmation(true); setCandidateConfig(res['config']); } else { setUnsafeOptionsVerified(true); - setConfigContents(res['config']); } } else if (res['import_status'] === 'imported'){ resetState(); props.onClose(true); } - return res['import_status']; }) } @@ -93,6 +96,7 @@ const ConfigImportModal = (props: Props) => { } function uploadFile(event) { + setShowPassword(false); let reader = new FileReader(); reader.onload = (event) => { setConfigContents(event.target.result);