Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config encrypt #1204

Merged
merged 41 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6e2da3c
Sketched out the infrastructure of configuration import modal window
VakarisZ May 28, 2021
3402479
Implemented the skeleton of import config modal
VakarisZ May 28, 2021
691dfee
Added an upload status icon (checkmark if successful, red x if error)
VakarisZ Jun 1, 2021
556d082
Added import config modal.
VakarisZ Jun 1, 2021
ff7760f
Altered ConfigurePage.js to use import modal
VakarisZ Jun 1, 2021
5ab0137
Improved a mock endpoint for testing import configuration modal
VakarisZ Jun 1, 2021
7954dbe
Fixed and improved the wording in configuration export and import mod…
VakarisZ Jun 1, 2021
3384047
Add initial implementation of encrypting config and saving it on export
shreyamalviya May 28, 2021
495eb4c
Modify config encryption logic: don't save the file in the backend, j…
shreyamalviya May 28, 2021
308ae3e
Link config encryption backend logic with frontend (partially)
shreyamalviya May 28, 2021
46408e6
Implemented export byte saving to file
VakarisZ May 31, 2021
f4b5d34
Finish up hooking frontend and backend for export config
shreyamalviya Jun 1, 2021
d67e84a
Make sure (1) config is updated before exporting; (2) plaintext confi…
shreyamalviya Jun 1, 2021
b9fb4c6
Add exception handling for config decryption
shreyamalviya Jun 1, 2021
7153b91
Use buffer size directly from pyAesCrypt
shreyamalviya Jun 1, 2021
a94047d
Fixed configuration encryption/decryption to use b64 encoding
VakarisZ Jun 1, 2021
295caca
Add unit tests for config_encryption.py
shreyamalviya Jun 1, 2021
321dd2c
Improved configuration export related code by making it cleaner/more …
VakarisZ Jun 1, 2021
9fcfaac
Improved exceptions thrown in configuration decryption and unit tests.
VakarisZ Jun 2, 2021
51273c4
Removed unused exception
VakarisZ Jun 2, 2021
624fda1
Renamed configuration import resource endpoint(url) and resource itself.
VakarisZ Jun 2, 2021
8b86e40
Improved configuration export and fixed the bug of modal not closing …
VakarisZ Jun 2, 2021
500f270
Fixed, improved and tested configuration import and export.
VakarisZ Jun 2, 2021
fc1f12c
Implemented safety check on import.
VakarisZ Jun 2, 2021
b407094
Reworded the text of UnsafeOptionsConfirmationModal to specify that i…
VakarisZ Jun 3, 2021
c25ea0e
Fixed bugs in config import backend (related to json parsing and stri…
VakarisZ Jun 3, 2021
2f9c6bf
Improved readability in configuration_import.py by removing unused va…
VakarisZ Jun 3, 2021
53bb6f7
Added changes of configuration encryption/decryption to CHANGELOG.md
VakarisZ Jun 3, 2021
c487a27
Fixed a type-hint for a config decryption method
VakarisZ Jun 7, 2021
e918ae1
Renamed a unit test to be more specific: test_decrypt_config__no_pass…
VakarisZ Jun 7, 2021
1125b0f
Added pyAesEncrypt to the Pipfile
VakarisZ Jun 7, 2021
04a35a1
Improved wording in configuration export related logs and UI
VakarisZ Jun 7, 2021
abaeafc
Split one unit test test_encrypt_decrypt_config__malformed into two, …
VakarisZ Jun 7, 2021
b30de00
Update encryption/decryption PR numbers in changelog
mssalvatore Jun 9, 2021
a36fc81
Refactored configuration import and added a check to decide if config…
VakarisZ Jun 11, 2021
5cf002d
Refactored unit tests and added a unit test for a function which chec…
VakarisZ Jun 11, 2021
3450b80
Refactored cyphertext to ciphertext for consistency
VakarisZ Jun 11, 2021
5c7bab7
Refactored json parsing out of encryption/decryption functionality.
VakarisZ Jun 11, 2021
fbe9b4f
Typos and small bugfixes for configuration export/import UI.
VakarisZ Jun 11, 2021
8a673cc
Added the logging for errors encountered in configuration decryption …
VakarisZ Jun 11, 2021
57f35f9
island: Fix typo in ConfigurationImport error logging
mssalvatore Jun 11, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Security
- Address minor issues discovered by Dlint. #1075
- Generate random passwords when creating a new user (create user PBA, ms08_67 exploit). #1174
- Implemented configuration encryption/decryption. #1189, #1204
4 changes: 4 additions & 0 deletions monkey/common/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ class FindingWithoutDetailsError(Exception):

class DomainControllerNameFetchError(FailedExploitationError):
""" Raise on failed attempt to extract domain controller's name """


class InvalidConfigurationError(Exception):
""" Raise when configuration is invalid """
1 change: 1 addition & 0 deletions monkey/monkey_island/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Flask = ">=1.1"
Werkzeug = ">=1.0.1"
ScoutSuite = {git = "https://github.com/guardicode/ScoutSuite"}
PyJWT = "==1.7"
pyaescrypt = "*"

[dev-packages]
virtualenv = ">=20.0.26"
Expand Down
710 changes: 372 additions & 338 deletions monkey/monkey_island/Pipfile.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions monkey/monkey_island/cc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
)
from monkey_island.cc.resources.bootloader import Bootloader
from monkey_island.cc.resources.client_run import ClientRun
from monkey_island.cc.resources.configuration_export import ConfigurationExport
from monkey_island.cc.resources.configuration_import import ConfigurationImport
from monkey_island.cc.resources.edge import Edge
from monkey_island.cc.resources.environment import Environment
from monkey_island.cc.resources.island_configuration import IslandConfiguration
Expand Down Expand Up @@ -131,6 +133,8 @@ def init_api_resources(api):
)
api.add_resource(MonkeyConfiguration, "/api/configuration", "/api/configuration/")
api.add_resource(IslandConfiguration, "/api/configuration/island", "/api/configuration/island/")
api.add_resource(ConfigurationExport, "/api/configuration/export")
api.add_resource(ConfigurationImport, "/api/configuration/import")
api.add_resource(
MonkeyDownload,
"/api/monkey/download",
Expand Down
25 changes: 25 additions & 0 deletions monkey/monkey_island/cc/resources/configuration_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json

import flask_restful
from flask import request

from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.utils.encryption import encrypt_string


class ConfigurationExport(flask_restful.Resource):
@jwt_required
def post(self):
data = json.loads(request.data)
should_encrypt = data["should_encrypt"]

plaintext_config = ConfigService.get_config()

config_export = plaintext_config
if should_encrypt:
password = data["password"]
plaintext_config = json.dumps(plaintext_config)
config_export = encrypt_string(plaintext_config, password)

return {"config_export": config_export, "encrypted": should_encrypt}
98 changes: 98 additions & 0 deletions monkey/monkey_island/cc/resources/configuration_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import json
import logging
from dataclasses import dataclass
from json.decoder import JSONDecodeError

import flask_restful
from flask import request

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.encryption import (
InvalidCiphertextError,
InvalidCredentialsError,
decrypt_ciphertext,
is_encrypted,
)

logger = logging.getLogger(__name__)


class ImportStatuses:
UNSAFE_OPTION_VERIFICATION_REQUIRED = "unsafe_options_verification_required"
INVALID_CONFIGURATION = "invalid_configuration"
INVALID_CREDENTIALS = "invalid_credentials"
IMPORTED = "imported"


@dataclass
class ResponseContents:
import_status: str = ImportStatuses.IMPORTED
message: str = ""
status_code: int = 200
config: str = ""
config_schema: str = ""

def form_response(self):
return self.__dict__


class ConfigurationImport(flask_restful.Resource):
SUCCESS = False

@jwt_required
def post(self):
request_contents = json.loads(request.data)
try:
config = ConfigurationImport._get_plaintext_config_from_request(request_contents)
if request_contents["unsafeOptionsVerified"]:
mssalvatore marked this conversation as resolved.
Show resolved Hide resolved
ConfigurationImport.import_config(config)
return ResponseContents().form_response()
else:
return ResponseContents(
config=json.dumps(config),
config_schema=ConfigService.get_config_schema(),
import_status=ImportStatuses.UNSAFE_OPTION_VERIFICATION_REQUIRED,
).form_response()
except InvalidCredentialsError:
return ResponseContents(
import_status=ImportStatuses.INVALID_CREDENTIALS,
message="Invalid credentials provided",
).form_response()
except InvalidConfigurationError:
return ResponseContents(
import_status=ImportStatuses.INVALID_CONFIGURATION,
message="Invalid configuration supplied. "
"Maybe the format is outdated or the file has been corrupted.",
).form_response()

@staticmethod
def _get_plaintext_config_from_request(request_contents: dict) -> dict:
try:
config = request_contents["config"]
if ConfigurationImport.is_config_encrypted(request_contents["config"]):
config = decrypt_ciphertext(config, request_contents["password"])
return json.loads(config)
except (JSONDecodeError, InvalidCiphertextError):
logger.exception(
"Exception encountered when trying to extract plaintext configuration."
)
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
59 changes: 59 additions & 0 deletions monkey/monkey_island/cc/services/utils/encryption.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import base64
import io
import logging

import pyAesCrypt

BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef

logger = logging.getLogger(__name__)


def encrypt_string(plaintext: str, password: str) -> str:
plaintext_stream = io.BytesIO(plaintext.encode())
ciphertext_stream = io.BytesIO()

pyAesCrypt.encryptStream(plaintext_stream, ciphertext_stream, password, BUFFER_SIZE)

ciphertext_b64 = base64.b64encode(ciphertext_stream.getvalue())
logger.info("String encrypted.")

return ciphertext_b64.decode()


def decrypt_ciphertext(ciphertext: str, password: str) -> str:
ciphertext = base64.b64decode(ciphertext)
ciphertext_stream = io.BytesIO(ciphertext)
plaintext_stream = io.BytesIO()

ciphertext_stream_len = len(ciphertext_stream.getvalue())

try:
pyAesCrypt.decryptStream(
ciphertext_stream,
plaintext_stream,
password,
BUFFER_SIZE,
ciphertext_stream_len,
)
except ValueError as ex:
if str(ex).startswith("Wrong password"):
logger.info("Wrong password provided for decryption.")
raise InvalidCredentialsError
else:
logger.info("The corrupt ciphertext provided.")
raise InvalidCiphertextError
return plaintext_stream.getvalue().decode("utf-8")


def is_encrypted(ciphertext: str) -> bool:
ciphertext = base64.b64decode(ciphertext)
return ciphertext.startswith(b"AES")


class InvalidCredentialsError(Exception):
""" Raised when password for decryption is invalid """


class InvalidCiphertextError(Exception):
""" Raised when ciphertext is corrupted """
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import {Button, Modal, Form} from 'react-bootstrap';
import React, {useState} from 'react';

import FileSaver from 'file-saver';
import AuthComponent from '../AuthComponent';
import '../../styles/components/configuration-components/ExportConfigModal.scss';


type Props = {
show: boolean,
onClick: () => void
onHide: () => void
}

const ConfigExportModal = (props: Props) => {
// TODO implement the back end
const configExportEndpoint = '/api/configuration/export';

const [pass, setPass] = useState('');
Expand All @@ -33,11 +33,25 @@ const ConfigExportModal = (props: Props) => {
})
}
)
.then(res => res.json())
.then(res => {
let configToExport = res['config_export'];
if (res['encrypted']) {
configToExport = new Blob([configToExport]);
} else {
configToExport = new Blob(
[JSON.stringify(configToExport, null, 2)],
{type: 'text/plain;charset=utf-8'}
);
}
FileSaver.saveAs(configToExport, 'monkey.conf');
props.onHide();
})
}

return (
<Modal show={props.show}
onHide={props.onClick}
onHide={props.onHide}
size={'lg'}
className={'config-export-modal'}>
<Modal.Header closeButton>
Expand Down Expand Up @@ -105,7 +119,7 @@ const ExportPlaintextChoiceField = (props: {
/>
<p className={`export-warning text-secondary`}>
Configuration may contain stolen credentials or sensitive data.<br/>
It is advised to use password encryption.
It is recommended that you use the <b>Encrypt with a password</b> option.
</p>
</div>
)
Expand Down
Loading