diff --git a/envs/monkey_maker/README.md b/envs/monkey_maker/README.md new file mode 100644 index 000000000..d80a211e1 --- /dev/null +++ b/envs/monkey_maker/README.md @@ -0,0 +1,57 @@ +# Monkey maker + +## About + +Monkey maker is an environment on AWS that +is designed for monkey binary building. +This environment is deployed using terraform scripts +located in this directory. + +## Setup + +To setup you need to put `accessKeys` file into `./aws_keys` directory. + +Contents of `accessKeys` file should be as follows: + +```ini +[default] +aws_access_key_id = <...> +aws_secret_access_key = <...> +``` +Also review `./terraform/config.tf` file. + +Launch the environment by going into `terraform` folder and running +``` +terraform init +terraform apply +``` + +## Usage + +To login to windows use Administrator: %HwuzI!Uzsyfa=cB*XaQ6xxHqopfj)h) credentials + +You'll find docker files in `/home/ubuntu/docker_envs/linux/...` + +To build docker image for 32 bit linux: +``` +cd /home/ubuntu/docker_envs/linux/py3-32 +sudo docker build -t builder32 . +``` + +To build docker image for 64 bit linux: +``` +cd /home/ubuntu/docker_envs/linux/py3-64 +sudo docker build -t builder64 . +``` + +To build 32 bit monkey binary: +``` +cd /home/ubuntu/monkey_folder/monkey +sudo docker run -v "$(pwd):/src" builder32 -c "export SRCDIR=/src/infection_monkey && /entrypoint.sh" +``` + +To build 64 bit monkey binary: +``` +cd /home/ubuntu/monkey_folder/monkey +sudo docker run -v "$(pwd):/src" builder64 -c "export SRCDIR=/src/infection_monkey && /entrypoint.sh" +``` diff --git a/envs/monkey_maker/aws_keys/.gitignore b/envs/monkey_maker/aws_keys/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/envs/monkey_maker/aws_keys/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/envs/monkey_maker/terraform/config.tf b/envs/monkey_maker/terraform/config.tf new file mode 100644 index 000000000..9884c24a2 --- /dev/null +++ b/envs/monkey_maker/terraform/config.tf @@ -0,0 +1,5 @@ +provider "aws" { + version = "~> 2.0" + region = "eu-central-1" + shared_credentials_file = "../aws_keys/accessKeys" +} diff --git a/envs/monkey_maker/terraform/infra.tf b/envs/monkey_maker/terraform/infra.tf new file mode 100644 index 000000000..39937f974 --- /dev/null +++ b/envs/monkey_maker/terraform/infra.tf @@ -0,0 +1,61 @@ +resource "aws_vpc" "monkey_maker" { + cidr_block = "10.0.0.0/24" + enable_dns_support = true + tags = { + Name = "monkey_maker_vpc" + } +} + +resource "aws_internet_gateway" "monkey_maker_gateway" { + vpc_id = "${aws_vpc.monkey_maker.id}" + + tags = { + Name = "monkey_maker_gateway" + } +} + +// create routing table which points to the internet gateway +resource "aws_route_table" "monkey_maker_route" { + vpc_id = "${aws_vpc.monkey_maker.id}" + + route { + cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.monkey_maker_gateway.id}" + } + + tags = { + Name = "monkey_maker_route" + } +} + +// associate the routing table with the subnet +resource "aws_route_table_association" "subnet-association" { + subnet_id = "${aws_subnet.main.id}" + route_table_id = "${aws_route_table.monkey_maker_route.id}" +} + +resource "aws_subnet" "main" { + vpc_id = "${aws_vpc.monkey_maker.id}" + cidr_block = "10.0.0.0/24" + + tags = { + Name = "Main" + } +} + +resource "aws_security_group" "monkey_maker_sg" { + name = "monkey_maker_sg" + description = "Allow remote access to the island" + vpc_id = "${aws_vpc.monkey_maker.id}" + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "monkey_maker_sg" + } +} diff --git a/envs/monkey_maker/terraform/instances.tf b/envs/monkey_maker/terraform/instances.tf new file mode 100644 index 000000000..b46f04910 --- /dev/null +++ b/envs/monkey_maker/terraform/instances.tf @@ -0,0 +1,25 @@ +resource "aws_instance" "island_windows" { + ami = "ami-033b3ef27f8d1881d" + instance_type = "t2.micro" + private_ip = "10.0.0.251" + subnet_id = "${aws_subnet.main.id}" + key_name = "monkey_maker" + tags = { + Name = "monkey_maker_windows" + } + vpc_security_group_ids = ["${aws_security_group.monkey_maker_sg.id}"] + associate_public_ip_address = true +} + +resource "aws_instance" "island_linux" { + ami = "ami-0495203541087740a" + instance_type = "t2.micro" + private_ip = "10.0.0.252" + subnet_id = "${aws_subnet.main.id}" + key_name = "monkey_maker" + tags = { + Name = "monkey_maker_linux" + } + vpc_security_group_ids = ["${aws_security_group.monkey_maker_sg.id}"] + associate_public_ip_address = true +} diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index a8c0687fc..e788bb36c 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -546,6 +546,38 @@ fullTest.conf is a good config to start, because it covers all machines. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 12 Tunneling M4

+

(10.2.0.12)

(Exploitable)
OS:Windows server 2019 x64
Default service’s port:445
Root password:t67TC5ZDmz
Server’s config:Default
Notes:Accessible only trough Nr.10
+ diff --git a/envs/os_compatibility/README.md b/envs/os_compatibility/README.md new file mode 100644 index 000000000..6b97b6612 --- /dev/null +++ b/envs/os_compatibility/README.md @@ -0,0 +1,79 @@ +# OS compatibility + +## About + +OS compatibility is an environment on AWS that +is designed to test monkey binary compatibility on +different operating systems. +This environment is deployed using terraform scripts +located in this directory. + +## Setup + +To setup you need to put `accessKeys` file into `./aws_keys` directory. + +Contents of `accessKeys` file should be as follows: + +``` +[default] +aws_access_key_id = <...> +aws_secret_access_key = <...> +``` +Also review `./terraform/config.tf` file. + +Launch the environment by going into `terraform` folder and running +```angular2html +terraform init +terraform apply +``` + +## Usage + +0. Add your machine's IP to the `os_compat_island` security group ingress rules. +1. Launch os_compat_ISLAND machine and upload your binaries/update island. Reset island environment. +2. Launch/Reboot all other os_compat test machines (Can be filtered with tag "Purpose: os_compat_instance") +3. Wait until machines boot and run monkey +4. Launch `test_compatibility.py` pytest script with island ip parameter +(e.g. `test_compatibility.py --island 111.111.111.111:5000`) + +## Machines + +Since island machine is built from custom AMI it already has the following credentials: + +Administrator: %tPHGz8ZuQsBnEUgdpz!6f&elGnFy?;. + +For windows_2008_r2 Administrator:AGE(MP..txL + +The following machines does not download monkey automatically, so you'll have to manually check them: + +- os_compat_kali_2019 +- os_compat_oracle_6 +- os_compat_oracle_7 +- windows_2003_r2_32 +- windows_2003 +- windows_2008_r2 + +A quick reference for usernames on different machines (if in doubt check official docs): +- Ubuntu: ubuntu +- Oracle: clckwrk +- CentOS: centos +- Everything else: ec2-user + +To manually verify the machine is compatible use commands to download and execute the monkey. +Also, add your IP to `os_compat_instance` security group. + +Example commands: + - Powershell: +```cmd +[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy +Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction SilentlyContinue +Invoke-WebRequest -Uri 'https://10.0.0.251:5000/api/monkey/download/monkey-windows-64.exe' -OutFile 'C:\windows\temp\monkey-windows-64.exe' -UseBasicParsing +C:\windows\temp\monkey-windows-64.exe m0nk3y -s 10.0.0.251:5000 +``` + + - Bash: +```shell script +wget --no-check-certificate -q https://10.0.0.251:5000/api/monkey/download/monkey-linux-64 -O ./monkey-linux-64 || curl https://10.0.0.251:5000/api/monkey/download/monkey-linux-64 -k -o monkey-linux-64 +chmod +x ./monkey-linux-64 +./monkey-linux-64 m0nk3y -s 10.0.0.251:5000 +``` diff --git a/envs/os_compatibility/aws_keys/.gitignore b/envs/os_compatibility/aws_keys/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/envs/os_compatibility/aws_keys/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/envs/os_compatibility/conftest.py b/envs/os_compatibility/conftest.py new file mode 100644 index 000000000..13aabf5b6 --- /dev/null +++ b/envs/os_compatibility/conftest.py @@ -0,0 +1,11 @@ +import pytest + + +def pytest_addoption(parser): + parser.addoption("--island", action="store", default="", + help="Specify the Monkey Island address (host+port).") + + +@pytest.fixture(scope='module') +def island(request): + return request.config.getoption("--island") diff --git a/envs/os_compatibility/terraform/config.tf b/envs/os_compatibility/terraform/config.tf new file mode 100644 index 000000000..9884c24a2 --- /dev/null +++ b/envs/os_compatibility/terraform/config.tf @@ -0,0 +1,5 @@ +provider "aws" { + version = "~> 2.0" + region = "eu-central-1" + shared_credentials_file = "../aws_keys/accessKeys" +} diff --git a/envs/os_compatibility/terraform/infra.tf b/envs/os_compatibility/terraform/infra.tf new file mode 100644 index 000000000..f4b458694 --- /dev/null +++ b/envs/os_compatibility/terraform/infra.tf @@ -0,0 +1,92 @@ +resource "aws_vpc" "os_compat_vpc" { + cidr_block = "10.0.0.0/24" + enable_dns_support = true + tags = { + Name = "os_compat_vpc" + } +} + +resource "aws_internet_gateway" "os_compat_gateway" { + vpc_id = "${aws_vpc.os_compat_vpc.id}" + + tags = { + Name = "os_compat_gateway" + } +} + +// create routing table which points to the internet gateway +resource "aws_route_table" "os_compat_route" { + vpc_id = "${aws_vpc.os_compat_vpc.id}" + + route { + cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.os_compat_gateway.id}" + } + + tags = { + Name = "os_compat_route" + } +} + +// associate the routing table with the subnet +resource "aws_route_table_association" "subnet-association" { + subnet_id = "${aws_subnet.main.id}" + route_table_id = "${aws_route_table.os_compat_route.id}" +} + +resource "aws_subnet" "main" { + vpc_id = "${aws_vpc.os_compat_vpc.id}" + cidr_block = "10.0.0.0/24" + + tags = { + Name = "Main" + } +} + +resource "aws_security_group" "os_compat_island" { + name = "os_compat_island" + description = "Allow remote access to the island" + vpc_id = "${aws_vpc.os_compat_vpc.id}" + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["10.0.0.0/24"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "os_compat_island" + } +} + +resource "aws_security_group" "os_compat_instance" { + name = "os_compat_instance" + description = "Allow remote access to the machines" + vpc_id = "${aws_vpc.os_compat_vpc.id}" + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["10.0.0.0/24"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "os_compat_instance" + } +} diff --git a/envs/os_compatibility/terraform/instance_template/main.tf b/envs/os_compatibility/terraform/instance_template/main.tf new file mode 100644 index 000000000..5a4e08fa7 --- /dev/null +++ b/envs/os_compatibility/terraform/instance_template/main.tf @@ -0,0 +1,14 @@ +resource "aws_instance" "os_test_machine" { + ami = "${var.ami}" + instance_type = "${var.type}" + private_ip = "${var.ip}" + subnet_id = "${data.aws_subnet.main.id}" + key_name = "os_compat" + tags = { + Name = "os_compat_${var.name}" + Purpose = "os_compat_instance" + } + vpc_security_group_ids = ["${data.aws_security_group.os_compat_instance.id}"] + associate_public_ip_address = true + user_data = "${var.user_data}" +} diff --git a/envs/os_compatibility/terraform/instance_template/variables.tf b/envs/os_compatibility/terraform/instance_template/variables.tf new file mode 100644 index 000000000..aba55739d --- /dev/null +++ b/envs/os_compatibility/terraform/instance_template/variables.tf @@ -0,0 +1,25 @@ +variable "ami" {type=string} +variable "ip" {type=string} +variable "name" {type=string} +variable "type" { + type=string + default="t2.micro" +} +variable "user_data" { + type=string + default="" +} +variable "env_vars" { + type = object({ + subnet_id = string + vpc_security_group_ids = string + }) +} + +data "aws_subnet" "main" { + id = "${var.env_vars.subnet_id}" +} + +data "aws_security_group" "os_compat_instance" { + id = "${var.env_vars.vpc_security_group_ids}" +} diff --git a/envs/os_compatibility/terraform/instances.tf b/envs/os_compatibility/terraform/instances.tf new file mode 100644 index 000000000..44b2f8a3d --- /dev/null +++ b/envs/os_compatibility/terraform/instances.tf @@ -0,0 +1,371 @@ +// Instances of machines in os_compat environment +// !!! Don't forget to add machines to test_compatibility.py if you add here !!! + + +resource "aws_instance" "island" { + ami = "ami-004f0217ce761fc9a" + instance_type = "t2.micro" + private_ip = "10.0.0.251" + subnet_id = "${aws_subnet.main.id}" + key_name = "os_compat" + tags = { + Name = "os_compat_ISLAND" + } + vpc_security_group_ids = ["${aws_security_group.os_compat_island.id}"] + associate_public_ip_address = true + root_block_device { + volume_size = "30" + volume_type = "standard" + delete_on_termination = true + } +} + +locals { + env_vars = { + subnet_id = "${aws_subnet.main.id}" + vpc_security_group_ids = "${aws_security_group.os_compat_instance.id}" + } + + user_data_linux_64 = < +add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ +[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy +Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction SilentlyContinue +Invoke-WebRequest -Uri 'https://10.0.0.251:5000/api/monkey/download/monkey-windows-64.exe' -OutFile 'C:\windows\temp\monkey-windows-64.exe' -UseBasicParsing +C:\windows\temp\monkey-windows-64.exe m0nk3y -s 10.0.0.251:5000 + +true +EOF + + user_data_windows_32 = < +add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ +[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy +Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction SilentlyContinue +Invoke-WebRequest -Uri 'https://10.0.0.251:5000/api/monkey/download/monkey-windows-32.exe' -OutFile 'C:\windows\temp\monkey-windows-32.exe' -UseBasicParsing +C:\windows\temp\monkey-windows-32.exe m0nk3y -s 10.0.0.251:5000 + +true +EOF +} + +module "centos_6" { + source = "./instance_template" + name = "centos_6" + ami = "ami-07fa74e425f2abf29" + ip = "10.0.0.36" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "centos_7" { + source = "./instance_template" + name = "centos_7" + ami = "ami-0034b52a39b9fb0e8" + ip = "10.0.0.37" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "centos_8" { + source = "./instance_template" + name = "centos_8" + ami = "ami-0034c84e4e9c557bd" + ip = "10.0.0.38" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "suse_12" { + source = "./instance_template" + name = "suse_12" + ami = "ami-07b12b913a7e36b08" + ip = "10.0.0.42" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "suse_11" { + source = "./instance_template" + name = "suse_11" + ami = "ami-0083986c" + ip = "10.0.0.41" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "kali_2019" { + source = "./instance_template" + name = "kali_2019" + ami = "ami-05d64b1d0f967d4bf" + ip = "10.0.0.99" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +// Requires m3.medium which usually isn't available +//module "rhel_5" { +// source = "./instance_template" +// name = "rhel_5" +// ami = "ami-a48cbfb9" +// type = "m3.medium" +// ip = "10.0.0.85" +// env_vars = "${local.env_vars}" +// user_data = "${local.user_data_linux_64}" +//} + +module "rhel_6" { + source = "./instance_template" + name = "rhel_6" + ami = "ami-0af3f0e0918f47bcf" + ip = "10.0.0.86" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "rhel_7" { + source = "./instance_template" + name = "rhel_7" + ami = "ami-0b5edb134b768706c" + ip = "10.0.0.87" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "rhel_8" { + source = "./instance_template" + name = "rhel_8" + ami = "ami-0badcc5b522737046" + ip = "10.0.0.88" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "debian_7" { + source = "./instance_template" + name = "debian_7" + ami = "ami-0badcc5b522737046" + ip = "10.0.0.77" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "debian_8" { + source = "./instance_template" + name = "debian_8" + ami = "ami-0badcc5b522737046" + ip = "10.0.0.78" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "debian_9" { + source = "./instance_template" + name = "debian_9" + ami = "ami-0badcc5b522737046" + ip = "10.0.0.79" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "oracle_6" { + source = "./instance_template" + name = "oracle_6" + ami = "ami-0f9b69f34108a3770" + ip = "10.0.0.66" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "oracle_7" { + source = "./instance_template" + name = "oracle_7" + ami = "ami-001e494dc0f3372bc" + ip = "10.0.0.67" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "ubuntu_12" { + source = "./instance_template" + name = "ubuntu_12" + ami = "ami-003d0b1d" + ip = "10.0.0.22" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +// Requires m3.medium instance which usually isn't available +// module "ubuntu_12_32" { +// source = "./instance_template" +// name = "ubuntu_12_32" +// ami = "ami-06003c1b" +// ip = "10.0.0.23" +// env_vars = "${local.env_vars}" +// user_data = "${local.user_data_linux_32}" +// } + +module "ubuntu_14" { + source = "./instance_template" + name = "ubuntu_14" + ami = "ami-067ee10914e74ffee" + ip = "10.0.0.24" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "ubuntu_19" { + source = "./instance_template" + name = "ubuntu_19" + ami = "ami-001b87954b72ea3ac" + ip = "10.0.0.29" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_linux_64}" +} + +module "windows_2003_r2_32" { + source = "./instance_template" + name = "windows_2003_r2_32" + ami = "ami-01e4fa6d" + ip = "10.0.0.4" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_64}" +} + +module "windows_2003" { + source = "./instance_template" + name = "windows_2003" + ami = "ami-9e023183" + ip = "10.0.0.5" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_64}" +} + +module "windows_2008_r2" { + source = "./instance_template" + name = "windows_2008_r2" + ami = "ami-05af5509c2c73e36e" + ip = "10.0.0.8" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_64}" +} + + +module "windows_2008_32" { + source = "./instance_template" + name = "windows_2008_32" + ami = "ami-3606352b" + ip = "10.0.0.6" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_32}" +} + +module "windows_2012" { + source = "./instance_template" + name = "windows_2012" + ami = "ami-0d8c60e4d3ca36ed6" + ip = "10.0.0.12" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_64}" +} + +module "windows_2012_r2" { + source = "./instance_template" + name = "windows_2012_r2" + ami = "ami-08dcceb529e70f875" + ip = "10.0.0.11" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_64}" +} + +module "windows_2016" { + source = "./instance_template" + name = "windows_2016" + ami = "ami-02a6791b44938cfcd" + ip = "10.0.0.116" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_64}" +} + +module "windows_2019" { + source = "./instance_template" + name = "windows_2019" + ami = "ami-09fe2745618d2af42" + ip = "10.0.0.119" + env_vars = "${local.env_vars}" + user_data = "${local.user_data_windows_64}" +} diff --git a/envs/os_compatibility/test_compatibility.py b/envs/os_compatibility/test_compatibility.py new file mode 100644 index 000000000..51a9f5ebb --- /dev/null +++ b/envs/os_compatibility/test_compatibility.py @@ -0,0 +1,62 @@ +import pytest + +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient + + +machine_list = { + "10.0.0.36": "centos_6", + "10.0.0.37": "centos_7", + "10.0.0.38": "centos_8", + "10.0.0.42": "suse_12", + "10.0.0.41": "suse_11", + "10.0.0.99": "kali_2019", + "10.0.0.86": "rhel_6", + "10.0.0.87": "rhel_7", + "10.0.0.88": "rhel_8", + "10.0.0.77": "debian_7", + "10.0.0.78": "debian_8", + "10.0.0.79": "debian_9", + "10.0.0.66": "oracle_6", + "10.0.0.67": "oracle_7", + "10.0.0.22": "ubuntu_12", + "10.0.0.24": "ubuntu_14", + "10.0.0.29": "ubuntu_19", + "10.0.0.4": "windows_2003_r2_32", + "10.0.0.5": "windows_2003", + "10.0.0.8": "windows_2008", + "10.0.0.6": "windows_2008_32", + "10.0.0.12": "windows_2012", + "10.0.0.11": "windows_2012_r2", + "10.0.0.116": "windows_2016", + "10.0.0.119": "windows_2019", +} + + +@pytest.fixture(scope='class') +def island_client(island): + island_client_object = MonkeyIslandClient(island) + yield island_client_object + + +@pytest.mark.usefixtures('island_client') +# noinspection PyUnresolvedReferences +class TestOSCompatibility(object): + + def test_os_compat(self, island_client): + print() + all_monkeys = island_client.get_all_monkeys_from_db() + ips_that_communicated = [] + for monkey in all_monkeys: + for ip in monkey['ip_addresses']: + if ip in machine_list: + ips_that_communicated.append(ip) + break + for ip, os in machine_list.items(): + if ip not in ips_that_communicated: + print("{} didn't communicate to island".format(os)) + + if len(ips_that_communicated) < len(machine_list): + assert False + + + diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index 7a75c1b7e..7dd61cd19 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -5,7 +5,7 @@ requests odict paramiko psutil -PyInstaller +git+https://github.com/guardicore/pyinstaller ecdsa netifaces ipaddress diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index ae7a6934c..20b481f31 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -7,9 +7,12 @@ import urllib from logging import getLogger from urllib.parse import urlsplit +import requests + import infection_monkey.monkeyfs as monkeyfs from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time from infection_monkey.network.tools import get_interface_to_target +import infection_monkey.control __author__ = 'hoffer' @@ -108,12 +111,35 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): class HTTPConnectProxyHandler(http.server.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" + + def do_POST(self): + try: + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length).decode() + LOG.info("Received bootloader's request: {}".format(post_data)) + try: + dest_path = self.path + r = requests.post(url=dest_path, + data=post_data, + verify=False, + proxies=infection_monkey.control.ControlClient.proxies) + self.send_response(r.status_code) + except requests.exceptions.ConnectionError as e: + LOG.error("Couldn't forward request to the island: {}".format(e)) + self.send_response(404) + except Exception as e: + LOG.error("Failed to forward bootloader request: {}".format(e)) + finally: + self.end_headers() + self.wfile.write(r.content) + except Exception as e: + LOG.error("Failed receiving bootloader telemetry: {}".format(e)) def version_string(self): return "" def do_CONNECT(self): + LOG.info("Received a connect request!") # just provide a tunnel, transfer the data with no modification req = self req.path = "https://%s/" % req.path.replace(':443', '') diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 2af0e2230..3a1134930 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -19,6 +19,7 @@ from monkey_island.cc.resources.island_configuration import IslandConfiguration 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.node_states import NodeStates from monkey_island.cc.resources.remote_run import RemoteRun from monkey_island.cc.resources.reporting.report import Report from monkey_island.cc.resources.root import Root @@ -30,6 +31,7 @@ from monkey_island.cc.resources.version_update import VersionUpdate from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.attack.attack_config import AttackConfiguration from monkey_island.cc.resources.attack.attack_report import AttackReport +from monkey_island.cc.resources.bootloader import Bootloader from monkey_island.cc.services.database import Database from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.representations import output_json @@ -87,6 +89,7 @@ def init_app_url_rules(app): def init_api_resources(api): api.add_resource(Root, '/api') api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/') + api.add_resource(Bootloader, '/api/bootloader/') api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/') api.add_resource(ClientRun, '/api/client-monkey', '/api/client-monkey/') api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') @@ -97,6 +100,7 @@ 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(NodeStates, '/api/netmap/nodeStates') # report_type: zero_trust or security api.add_resource( diff --git a/monkey/monkey_island/cc/bootloader_server.py b/monkey/monkey_island/cc/bootloader_server.py new file mode 100644 index 000000000..5adfb3d22 --- /dev/null +++ b/monkey/monkey_island/cc/bootloader_server.py @@ -0,0 +1,49 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn +from urllib import parse +import urllib3 +import logging + +import requests +import pymongo + +from monkey_island.cc.environment import Environment + +# Disable "unverified certificate" warnings when sending requests to island +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +logger = logging.getLogger(__name__) + + +class BootloaderHttpServer(ThreadingMixIn, HTTPServer): + + def __init__(self, mongo_url): + self.mongo_client = pymongo.MongoClient(mongo_url) + server_address = ('', 5001) + super().__init__(server_address, BootloaderHTTPRequestHandler) + + +class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler): + + def do_POST(self): + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length).decode() + island_server_path = BootloaderHTTPRequestHandler.get_bootloader_resource_url(self.request.getsockname()[0]) + island_server_path = parse.urljoin(island_server_path, self.path[1:]) + r = requests.post(url=island_server_path, data=post_data, verify=False) + + try: + if r.status_code != 200: + self.send_response(404) + else: + self.send_response(200) + self.end_headers() + self.wfile.write(r.content) + except Exception as e: + logger.error("Failed to respond to bootloader: {}".format(e)) + finally: + self.connection.close() + + @staticmethod + def get_bootloader_resource_url(server_ip): + return "https://" + server_ip + ":" + str(Environment._ISLAND_PORT) + "/api/bootloader/" + diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 045f5452e..791fe384c 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -3,6 +3,7 @@ import os.path import sys import time import logging +from threading import Thread MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0" @@ -26,10 +27,21 @@ from monkey_island.cc.environment.environment import env from monkey_island.cc.database import is_db_server_up, get_db_version from monkey_island.cc.resources.monkey_download import MonkeyDownload from common.version import get_version +from monkey_island.cc.bootloader_server import BootloaderHttpServer from monkey_island.cc.setup import setup def main(): + logger.info("Starting bootloader server") + mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) + bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True) + + bootloader_server_thread.start() + start_island_server() + bootloader_server_thread.join() + + +def start_island_server(): from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop diff --git a/monkey/monkey_island/cc/resources/bootloader.py b/monkey/monkey_island/cc/resources/bootloader.py new file mode 100644 index 000000000..733b35651 --- /dev/null +++ b/monkey/monkey_island/cc/resources/bootloader.py @@ -0,0 +1,38 @@ +import json +from typing import Dict + +import flask_restful +from flask import request, make_response + +from monkey_island.cc.services.bootloader import BootloaderService + + +class Bootloader(flask_restful.Resource): + + # Used by monkey. can't secure. + def post(self, os): + if os == 'linux': + data = Bootloader._get_request_contents_linux(request.data) + elif os == 'windows': + data = Bootloader._get_request_contents_windows(request.data) + else: + return make_response({"status": "OS_NOT_FOUND"}, 404) + + result = BootloaderService.parse_bootloader_telem(data) + + if result: + return make_response({"status": "RUN"}, 200) + else: + return make_response({"status": "ABORT"}, 200) + + @staticmethod + def _get_request_contents_linux(request_data: bytes) -> Dict[str, str]: + parsed_data = json.loads(request_data.decode().replace("\"\n", "") + .replace("\n", "") + .replace("NAME=\"", "") + .replace("\":\",", "\":\"\",")) + return parsed_data + + @staticmethod + def _get_request_contents_windows(request_data: bytes) -> Dict[str, str]: + return json.loads(request_data.decode("utf-16", "ignore")) diff --git a/monkey/monkey_island/cc/resources/bootloader_test.py b/monkey/monkey_island/cc/resources/bootloader_test.py new file mode 100644 index 000000000..41ce9304c --- /dev/null +++ b/monkey/monkey_island/cc/resources/bootloader_test.py @@ -0,0 +1,56 @@ +from unittest import TestCase + +from monkey_island.cc.resources.bootloader import Bootloader +from monkey_island.cc.services.utils.bootloader_config import SUPPORTED_WINDOWS_VERSIONS + + +class TestBootloader(TestCase): + + def test_get_request_contents_linux(self): + data_without_tunnel = b'{"system":"linux", ' \ + b'"os_version":"NAME="Ubuntu"\n", ' \ + b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' \ + b'"hostname":"test-TEST", ' \ + b'"tunnel":false, ' \ + b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' + data_with_tunnel = b'{"system":"linux", ' \ + b'"os_version":"NAME="Ubuntu"\n", ' \ + b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' \ + b'"hostname":"test-TEST", ' \ + b'"tunnel":"192.168.56.1:5002", ' \ + b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' + + result1 = Bootloader._get_request_contents_linux(data_without_tunnel) + self.assertTrue(result1['system'] == "linux") + self.assertTrue(result1['os_version'] == "Ubuntu") + self.assertTrue(result1['glibc_version'] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") + self.assertTrue(result1['hostname'] == "test-TEST") + self.assertFalse(result1['tunnel']) + self.assertTrue(result1['ips'] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) + + result2 = Bootloader._get_request_contents_linux(data_with_tunnel) + self.assertTrue(result2['system'] == "linux") + self.assertTrue(result2['os_version'] == "Ubuntu") + self.assertTrue(result2['glibc_version'] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") + self.assertTrue(result2['hostname'] == "test-TEST") + self.assertTrue(result2['tunnel'] == "192.168.56.1:5002") + self.assertTrue(result2['ips'] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) + + def test_get_request_contents_windows(self): + windows_data = b'{\x00"\x00s\x00y\x00s\x00t\x00e\x00m\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o' \ + b'\x00w\x00s\x00"\x00,\x00 \x00"\x00o\x00s\x00_\x00v\x00e\x00r\x00s\x00i\x00o\x00n' \ + b'\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o\x00w\x00s\x008\x00_\x00o\x00r\x00_\x00g\x00r' \ + b'\x00e\x00a\x00t\x00e\x00r\x00"\x00,\x00 \x00"\x00h\x00o\x00s\x00t\x00n\x00a\x00m\x00e\x00"' \ + b'\x00:\x00"\x00D\x00E\x00S\x00K\x00T\x00O\x00P\x00-\x00P\x00J\x00H\x00U\x003\x006\x00B\x00"' \ + b'\x00,\x00 \x00"\x00t\x00u\x00n\x00n\x00e\x00l\x00"\x00:\x00f\x00a\x00l\x00s\x00e\x00,\x00 ' \ + b'\x00"\x00i\x00p\x00s\x00"\x00:\x00 \x00[\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x005' \ + b'\x006\x00.\x001\x00"\x00,\x00 \x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x004\x009' \ + b'\x00.\x001\x00"\x00,\x00 \x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x001\x007\x00.' \ + b'\x001\x00"\x00]\x00}\x00' + + result = Bootloader._get_request_contents_windows(windows_data) + self.assertTrue(result['system'] == "windows") + self.assertTrue(result['os_version'] == "windows8_or_greater") + self.assertTrue(result['hostname'] == "DESKTOP-PJHU36B") + self.assertFalse(result['tunnel']) + self.assertTrue(result['ips'] == ["192.168.56.1", "192.168.249.1", "192.168.217.1"]) diff --git a/monkey/monkey_island/cc/resources/node_states.py b/monkey/monkey_island/cc/resources/node_states.py new file mode 100644 index 000000000..c7c99cc94 --- /dev/null +++ b/monkey/monkey_island/cc/resources/node_states.py @@ -0,0 +1,11 @@ +from flask import request +import flask_restful + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.utils.node_states import NodeStates as NodeStateList + + +class NodeStates(flask_restful.Resource): + @jwt_required() + def get(self): + return {'node_states': [state.value for state in NodeStateList]} diff --git a/monkey/monkey_island/cc/services/bootloader.py b/monkey/monkey_island/cc/services/bootloader.py new file mode 100644 index 000000000..49dbe3154 --- /dev/null +++ b/monkey/monkey_island/cc/services/bootloader.py @@ -0,0 +1,69 @@ +from typing import Dict, List + +from bson import ObjectId + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.node import NodeService, NodeCreationException +from monkey_island.cc.services.utils.node_states import NodeStates +from monkey_island.cc.services.utils.bootloader_config import SUPPORTED_WINDOWS_VERSIONS, MIN_GLIBC_VERSION + + +class BootloaderService: + + @staticmethod + def parse_bootloader_telem(telem: Dict) -> bool: + telem['ips'] = BootloaderService.remove_local_ips(telem['ips']) + if telem['os_version'] == "": + telem['os_version'] = "Unknown OS" + + telem_id = BootloaderService.get_mongo_id_for_bootloader_telem(telem) + mongo.db.bootloader_telems.update({'_id': telem_id}, telem, upsert=True) + + will_monkey_run = BootloaderService.is_os_compatible(telem) + try: + node = NodeService.get_or_create_node_from_bootloader_telem(telem, will_monkey_run) + except NodeCreationException: + # Didn't find the node, but allow monkey to run anyways + return True + + node_group = BootloaderService.get_next_node_state(node, telem['system'], will_monkey_run) + if 'group' not in node or node['group'] != node_group.value: + NodeService.set_node_group(node['_id'], node_group) + return will_monkey_run + + @staticmethod + def get_next_node_state(node: Dict, system: str, will_monkey_run: bool) -> NodeStates: + group_keywords = [system, 'monkey'] + if 'group' in node and node['group'] == 'island': + group_keywords.extend(['island', 'starting']) + else: + group_keywords.append('starting') if will_monkey_run else group_keywords.append('old') + node_group = NodeStates.get_by_keywords(group_keywords) + return node_group + + @staticmethod + def get_mongo_id_for_bootloader_telem(bootloader_telem) -> ObjectId: + ip_hash = hex(hash(str(bootloader_telem['ips'])))[3:15] + hostname_hash = hex(hash(bootloader_telem['hostname']))[3:15] + return ObjectId(ip_hash + hostname_hash) + + @staticmethod + def is_os_compatible(bootloader_data) -> bool: + if bootloader_data['system'] == 'windows': + return BootloaderService.is_windows_version_supported(bootloader_data['os_version']) + elif bootloader_data['system'] == 'linux': + return BootloaderService.is_glibc_supported(bootloader_data['glibc_version']) + + @staticmethod + def is_windows_version_supported(windows_version) -> bool: + return SUPPORTED_WINDOWS_VERSIONS.get(windows_version, True) + + @staticmethod + def is_glibc_supported(glibc_version_string) -> bool: + glibc_version_string = glibc_version_string.lower() + glibc_version = glibc_version_string.split(' ')[-1] + return glibc_version >= str(MIN_GLIBC_VERSION) and 'eglibc' not in glibc_version_string + + @staticmethod + def remove_local_ips(ip_list) -> List[str]: + return [i for i in ip_list if not i.startswith("127")] diff --git a/monkey/monkey_island/cc/services/bootloader_test.py b/monkey/monkey_island/cc/services/bootloader_test.py new file mode 100644 index 000000000..03df2be97 --- /dev/null +++ b/monkey/monkey_island/cc/services/bootloader_test.py @@ -0,0 +1,35 @@ +from unittest import TestCase + +from monkey_island.cc.services.bootloader import BootloaderService + +WINDOWS_VERSIONS = { + "5.0": "Windows 2000", + "5.1": "Windows XP", + "5.2": "Windows XP/server 2003", + "6.0": "Windows Vista/server 2008", + "6.1": "Windows 7/server 2008R2", + "6.2": "Windows 8/server 2012", + "6.3": "Windows 8.1/server 2012R2", + "10.0": "Windows 10/server 2016-2019" +} + +MIN_GLIBC_VERSION = 2.14 + + +class TestBootloaderService(TestCase): + + def test_is_glibc_supported(self): + str1 = "ldd (Ubuntu EGLIBC 2.15-0ubuntu10) 2.15" + str2 = "ldd (GNU libc) 2.12" + str3 = "ldd (GNU libc) 2.28" + str4 = "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23" + self.assertTrue(not BootloaderService.is_glibc_supported(str1) and + not BootloaderService.is_glibc_supported(str2) and + BootloaderService.is_glibc_supported(str3) and + BootloaderService.is_glibc_supported(str4)) + + def test_remove_local_ips(self): + ips = ["127.1.1.1", "127.0.0.1", "192.168.56.1"] + ips = BootloaderService.remove_local_ips(ips) + self.assertEqual(["192.168.56.1"], ips) + diff --git a/monkey/monkey_island/cc/services/edge.py b/monkey/monkey_island/cc/services/edge.py index d5e38768d..a8016f5a8 100644 --- a/monkey/monkey_island/cc/services/edge.py +++ b/monkey/monkey_island/cc/services/edge.py @@ -2,7 +2,7 @@ from bson import ObjectId from monkey_island.cc.database import mongo import monkey_island.cc.services.node -from monkey_island.cc.models import Monkey +from monkey_island.cc.models.monkey import Monkey, MonkeyNotFoundError __author__ = "itay.mizeretz" @@ -145,7 +145,10 @@ class EdgeService: from_id = edge["from"] to_id = edge["to"] - from_label = Monkey.get_label_by_id(from_id) + try: + from_label = Monkey.get_label_by_id(from_id) + except MonkeyNotFoundError: + from_label = node_service.get_node_by_id(from_id)['domain_name'] if to_id == ObjectId("000000000000000000000000"): to_label = 'MonkeyIsland' diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 3109f7a78..3ad0a270b 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -1,4 +1,6 @@ from datetime import datetime, timedelta +from typing import Dict +import socket from bson import ObjectId @@ -6,9 +8,9 @@ import monkey_island.cc.services.log from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.edge import EdgeService -from monkey_island.cc.utils import local_ip_addresses -import socket +from monkey_island.cc.utils import local_ip_addresses, is_local_ips from monkey_island.cc import models +from monkey_island.cc.services.utils.node_states import NodeStates __author__ = "itay.mizeretz" @@ -122,20 +124,25 @@ class NodeService: @staticmethod def get_monkey_group(monkey): + keywords = [] if len(set(monkey["ip_addresses"]).intersection(local_ip_addresses())) != 0: - monkey_type = "island_monkey" + keywords.extend(["island", "monkey"]) else: monkey_type = "manual" if NodeService.get_monkey_manual_run(monkey) else "monkey" + keywords.append(monkey_type) - monkey_os = NodeService.get_monkey_os(monkey) - monkey_running = "" if Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead() else "_running" - return "%s_%s%s" % (monkey_type, monkey_os, monkey_running) + keywords.append(NodeService.get_monkey_os(monkey)) + if not Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead(): + keywords.append("running") + return NodeStates.get_by_keywords(keywords).value @staticmethod - def get_node_group(node): + def get_node_group(node) -> str: + if 'group' in node and node['group']: + return node['group'] node_type = "exploited" if node.get("exploited") else "clean" node_os = NodeService.get_node_os(node) - return "%s_%s" % (node_type, node_os) + return NodeStates.get_by_keywords([node_type, node_os]).value @staticmethod def monkey_to_net_node(monkey, for_report=False): @@ -166,6 +173,12 @@ class NodeService: "os": NodeService.get_node_os(node) } + @staticmethod + def set_node_group(node_id: str, node_group: NodeStates): + mongo.db.node.update({"_id": node_id}, + {'$set': {'group': node_group.value}}, + upsert=False) + @staticmethod def unset_all_monkey_tunnels(monkey_id): mongo.db.monkey.update( @@ -207,6 +220,43 @@ class NodeService: }) return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) + @staticmethod + def create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool): + new_node_insert_result = mongo.db.node.insert_one( + { + "ip_addresses": bootloader_telem['ips'], + "domain_name": bootloader_telem['hostname'], + "will_monkey_run": will_monkey_run, + "exploited": False, + "creds": [], + "os": + { + "type": bootloader_telem['system'], + "version": bootloader_telem['os_version'] + } + }) + return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) + + @staticmethod + def get_or_create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool) -> Dict: + if is_local_ips(bootloader_telem['ips']): + raise NodeCreationException("Bootloader ran on island, no need to create new node.") + + new_node = mongo.db.node.find_one({"ip_addresses": {"$in": bootloader_telem['ips']}}) + if new_node is None: + new_node = NodeService.create_node_from_bootloader_telem(bootloader_telem, will_monkey_run) + if bootloader_telem['tunnel']: + dst_node = NodeService.get_node_or_monkey_by_ip(bootloader_telem['tunnel']) + else: + dst_node = NodeService.get_monkey_island_node() + edge = EdgeService.get_or_create_edge(new_node['_id'], dst_node['id']) + mongo.db.edge.update({"_id": edge["_id"]}, + {'$set': {'tunnel': bool(bootloader_telem['tunnel']), + 'ip_address': bootloader_telem['ips'][0], + 'group': NodeStates.get_by_keywords(['island']).value}}, + upsert=False) + return new_node + @staticmethod def get_or_create_node(ip_address, domain_name=''): new_node = mongo.db.node.find_one({"ip_addresses": ip_address}) @@ -354,3 +404,6 @@ class NodeService: @staticmethod def get_hostname_by_id(node_id): return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) + +class NodeCreationException(Exception): + pass diff --git a/monkey/monkey_island/cc/services/utils/bootloader_config.py b/monkey/monkey_island/cc/services/utils/bootloader_config.py new file mode 100644 index 000000000..cb9ff04a6 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/bootloader_config.py @@ -0,0 +1,12 @@ +MIN_GLIBC_VERSION = 2.14 + +SUPPORTED_WINDOWS_VERSIONS = { + "xp_or_lower": False, + "vista": False, + "vista_sp1": False, + "vista_sp2": True, + "windows7": True, + "windows7_sp1": True, + "windows8_or_greater": True, +} + diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py new file mode 100644 index 000000000..db8dd6429 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from enum import Enum +from typing import List +import collections + + +class NodeStates(Enum): + CLEAN_UNKNOWN = 'clean_unknown' + CLEAN_LINUX = 'clean_linux' + CLEAN_WINDOWS = 'clean_windows' + EXPLOITED_LINUX = 'exploited_linux' + EXPLOITED_WINDOWS = 'exploited_windows' + ISLAND = 'island' + ISLAND_MONKEY_LINUX = 'island_monkey_linux' + ISLAND_MONKEY_LINUX_RUNNING = 'island_monkey_linux_running' + ISLAND_MONKEY_LINUX_STARTING = 'island_monkey_linux_starting' + ISLAND_MONKEY_WINDOWS = 'island_monkey_windows' + ISLAND_MONKEY_WINDOWS_RUNNING = 'island_monkey_windows_running' + ISLAND_MONKEY_WINDOWS_STARTING = 'island_monkey_windows_starting' + MANUAL_LINUX = 'manual_linux' + MANUAL_LINUX_RUNNING = 'manual_linux_running' + MANUAL_WINDOWS = 'manual_windows' + MANUAL_WINDOWS_RUNNING = 'manual_windows_running' + MONKEY_LINUX = 'monkey_linux' + MONKEY_LINUX_RUNNING = 'monkey_linux_running' + MONKEY_WINDOWS = 'monkey_windows' + MONKEY_WINDOWS_RUNNING = 'monkey_windows_running' + MONKEY_WINDOWS_STARTING = 'monkey_windows_starting' + MONKEY_LINUX_STARTING = 'monkey_linux_starting' + MONKEY_WINDOWS_OLD = 'monkey_windows_old' + MONKEY_LINUX_OLD = 'monkey_linux_old' + + @staticmethod + def get_by_keywords(keywords: List) -> NodeStates: + potential_groups = [i for i in NodeStates if NodeStates._is_state_from_keywords(i, keywords)] + if len(potential_groups) > 1: + raise MultipleGroupsFoundException("Multiple groups contain provided keywords. " + "Manually build group string to ensure keyword order.") + elif len(potential_groups) == 0: + raise NoGroupsFoundException("No groups found with provided keywords. " + "Check for typos and make sure group codes want to find exists.") + return potential_groups[0] + + @staticmethod + def _is_state_from_keywords(group, keywords) -> bool: + group_keywords = group.value.split("_") + return collections.Counter(group_keywords) == collections.Counter(keywords) + + +class MultipleGroupsFoundException(Exception): + pass + + +class NoGroupsFoundException(Exception): + pass diff --git a/monkey/monkey_island/cc/services/utils/node_states_test.py b/monkey/monkey_island/cc/services/utils/node_states_test.py new file mode 100644 index 000000000..7a8b7dfd1 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/node_states_test.py @@ -0,0 +1,15 @@ +from unittest import TestCase + +from monkey_island.cc.services.utils.node_states import NodeStates, NoGroupsFoundException + + +class TestNodeGroups(TestCase): + + def test_get_group_by_keywords(self): + self.assertEqual(NodeStates.get_by_keywords(['island']), NodeStates.ISLAND) + self.assertEqual(NodeStates.get_by_keywords(['running', 'linux', 'monkey']), NodeStates.MONKEY_LINUX_RUNNING) + self.assertEqual(NodeStates.get_by_keywords(['monkey', 'linux', 'running']), NodeStates.MONKEY_LINUX_RUNNING) + with self.assertRaises(NoGroupsFoundException): + NodeStates.get_by_keywords(['bogus', 'values', 'from', 'long', 'list', 'should', 'fail']) + + diff --git a/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js b/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js index 7e4805797..742d061f4 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js @@ -1,16 +1,11 @@ -const groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island', - 'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_windows', 'island_monkey_windows_running', - 'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux', - 'monkey_linux_running', 'monkey_windows', 'monkey_windows_running']; - -let getGroupsOptions = () => { +let getGroupsOptions = (stateList) => { let groupOptions = {}; - for (let groupName of groupNames) { - groupOptions[groupName] = + for (let stateName of stateList) { + groupOptions[stateName] = { shape: 'image', size: 50, - image: require('../../images/nodes/' + groupName + '.png') + image: require('../../images/nodes/' + stateName + '.png') }; } @@ -52,11 +47,11 @@ export const basic_options = { } }; -export const options = (() => { +export function getOptions(stateList) { let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ - opts.groups = getGroupsOptions(); + opts.groups = getGroupsOptions(stateList); return opts; -})(); +} export const optionsPth = (() => { let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js deleted file mode 100644 index a9fa7a6d6..000000000 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js +++ /dev/null @@ -1,246 +0,0 @@ -import React from 'react'; -import Toggle from 'react-toggle'; -import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import download from 'downloadjs' -import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; - -class InfMapPreviewPaneComponent extends PreviewPaneComponent { - - osRow(asset) { - return ( - - - - - ); - } - - ipsRow(asset) { - return ( - - - - - ); - } - - servicesRow(asset) { - return ( - - - - - ); - } - - accessibleRow(asset) { - return ( - - - - - ); - } - - statusRow(asset) { - return ( - - - - - ); - } - - forceKill(event, asset) { - let newConfig = asset.config; - newConfig['alive'] = !event.target.checked; - this.authFetch('/api/monkey/' + asset.guid, - { - method: 'PATCH', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({config: newConfig}) - }); - } - - forceKillRow(asset) { - return ( - - - - - ); - } - - unescapeLog(st) { - return st.substr(1, st.length - 2) // remove quotation marks on beginning and end of string. - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\t/g, '\t') - .replace(/\\b/g, '\b') - .replace(/\\f/g, '\f') - .replace(/\\"/g, '\"') - .replace(/\\'/g, '\'') - .replace(/\\&/g, '\&'); - } - - downloadLog(asset) { - this.authFetch('/api/log?id=' + asset.id) - .then(res => res.json()) - .then(res => { - let timestamp = res['timestamp']; - timestamp = timestamp.substr(0, timestamp.indexOf('.')); - let filename = res['monkey_label'].split(':').join('-') + ' - ' + timestamp + '.log'; - let logContent = this.unescapeLog(res['log']); - download(logContent, filename, 'text/plain'); - }); - - } - - downloadLogRow(asset) { - return ( - - - - - ); - } - - exploitsTimeline(asset) { - if (asset.exploits.length === 0) { - return (
); - } - - return ( -
-

- Exploit Timeline  - {this.generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')} -

- -
- ) - } - - assetInfo(asset) { - return ( -
-
Operating System{asset.os.charAt(0).toUpperCase() + asset.os.slice(1)}
IP Addresses{asset.ip_addresses.map(val =>
{val}
)}
Services{asset.services.map(val =>
{val}
)}
- Accessible From  - {this.generateToolTip('List of machine which can access this one using a network protocol')} - {asset.accessible_from_nodes.map(val =>
{val}
)}
Status{(asset.dead) ? 'Dead' : 'Alive'}
- Force Kill  - {this.generateToolTip('If this is on, monkey will die next time it communicates')} - - this.forceKill(e, asset)}/> - -
- Download Log - - this.downloadLog(asset)}>Download -
- - {this.osRow(asset)} - {this.ipsRow(asset)} - {this.servicesRow(asset)} - {this.accessibleRow(asset)} - -
- {this.exploitsTimeline(asset)} - - ); - } - - infectedAssetInfo(asset) { - return ( -
- - - {this.osRow(asset)} - {this.statusRow(asset)} - {this.ipsRow(asset)} - {this.servicesRow(asset)} - {this.accessibleRow(asset)} - {this.forceKillRow(asset)} - {this.downloadLogRow(asset)} - -
- {this.exploitsTimeline(asset)} -
- ); - } - - scanInfo(edge) { - return ( -
- - - - - - - - - - - - - - - -
Operating System{edge.os.type}
IP Address{edge.ip_address}
Services{edge.services.map(val =>
{val}
)}
- { - (edge.exploits.length === 0) ? - '' : -
-

Timeline

- -
- } -
- ); - } - - islandEdgeInfo() { - return ( -
-
- ); - } - - getInfoByProps() { - switch (this.props.type) { - case 'edge': - return this.scanInfo(this.props.item); - case 'node': - return this.props.item.group.includes('monkey', 'manual') ? - this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item); - case 'island_edge': - return this.islandEdgeInfo(); - } - - return null; - } -} - -export default InfMapPreviewPaneComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index a5d4d98fa..dd99c8503 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -253,8 +253,15 @@ class PreviewPaneComponent extends AuthComponent { info = this.scanInfo(this.props.item); break; case 'node': - info = this.props.item.group.includes('monkey', 'manual') ? this.infectedAssetInfo(this.props.item) : - this.props.item.group !== 'island' ? this.assetInfo(this.props.item) : this.islandAssetInfo(); + if (this.props.item.group.includes('monkey') && this.props.item.group.includes('starting')) { + info = this.assetInfo(this.props.item); + } else if (this.props.item.group.includes('monkey', 'manual')) { + info = this.infectedAssetInfo(this.props.item) + } else if (this.props.item.group !== 'island') { + info = this.assetInfo(this.props.item) + } else { + info = this.islandAssetInfo(); + } break; case 'island_edge': info = this.islandEdgeInfo(); diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js deleted file mode 100644 index a5ffa3020..000000000 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; - -class PthPreviewPaneComponent extends PreviewPaneComponent { - nodeInfo(asset) { - return ( -
- - - - - - - - - - - - - - - - - - - -
Hostname{asset.hostname}
IP Addresses{asset.ips.map(val =>
{val}
)}
Services{asset.services.map(val =>
{val}
)}
Compromised Users{asset.users.map(val =>
{val}
)}
-
- ); - } - - edgeInfo(edge) { - return ( -
- - - - - - - -
Compromised Users{edge.users.map(val =>
{val}
)}
-
- ); - } - - getInfoByProps() { - switch (this.props.type) { - case 'edge': - return this.edgeInfo(this.props.item); - case 'node': - return this.nodeInfo(this.props.item); - } - - return null; - } -} - -export default PthPreviewPaneComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index f75091270..3839e55fd 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -3,9 +3,9 @@ import {Col, Modal} from 'react-bootstrap'; import {Link} from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faStopCircle, faMinus } from '@fortawesome/free-solid-svg-icons' -import InfMapPreviewPaneComponent from 'components/map/preview-pane/InfMapPreviewPane'; +import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import {options, edgeGroupToColor} from 'components/map/MapOptions'; +import {getOptions, edgeGroupToColor} from 'components/map/MapOptions'; import AuthComponent from '../AuthComponent'; class MapPageComponent extends AuthComponent { @@ -13,6 +13,7 @@ class MapPageComponent extends AuthComponent { super(props); this.state = { graph: {nodes: [], edges: []}, + nodeStateList:[], selected: null, selectedType: null, killPressed: false, @@ -33,6 +34,7 @@ class MapPageComponent extends AuthComponent { }; componentDidMount() { + this.getNodeStateListFromServer(); this.updateMapFromServer(); this.interval = setInterval(this.timedEvents, 5000); } @@ -41,6 +43,14 @@ class MapPageComponent extends AuthComponent { clearInterval(this.interval); } + getNodeStateListFromServer = () => { + this.authFetch('/api/netmap/nodeStates') + .then(res => res.json()) + .then(res => { + this.setState({nodeStateList: res.node_states}); + }); + }; + timedEvents = () => { this.updateMapFromServer(); this.updateTelemetryFromServer(); @@ -201,7 +211,7 @@ class MapPageComponent extends AuthComponent { {this.renderTelemetryConsole()}
- +
{this.renderTelemetryLineCount()} @@ -226,7 +236,7 @@ class MapPageComponent extends AuthComponent { : ''} - + ); 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 451b69213..909659e87 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -3,8 +3,6 @@ import '../../styles/report/ReportPage.scss'; import React from 'react'; import {Route} from 'react-router-dom'; import {Col, Nav, NavItem} from 'react-bootstrap'; -import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import {edgeGroupToColor, options} from 'components/map/MapOptions'; import AuthComponent from '../AuthComponent'; import MustRunMonkeyWarning from '../report-components/common/MustRunMonkeyWarning'; import AttackReport from '../report-components/AttackReport' diff --git a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js index c175becd3..3199385ba 100644 --- a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js +++ b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js @@ -3,10 +3,18 @@ import Graph from 'react-graph-vis'; import Dimensions from 'react-dimensions' class GraphWrapper extends React.Component { + + constructor(props) { + super(props); + } + render() { - let newOptions = this.props.options; - newOptions.height = this.props.containerHeight.toString() + 'px'; - newOptions.width = this.props.containerWidth.toString() + 'px'; + let newOptions = null; + if(this.props.options !== undefined){ + newOptions = this.props.options; + newOptions.height = this.props.containerHeight.toString() + 'px'; + newOptions.width = this.props.containerWidth.toString() + 'px'; + } return () } } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index b44f367e1..63bde5203 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -3,7 +3,7 @@ import BreachedServers from 'components/report-components/security/BreachedServe 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 {edgeGroupToColor, getOptions} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/security/StolenPasswords'; import CollapsibleWellComponent from 'components/report-components/security/CollapsibleWell'; import {Line} from 'rc-progress'; @@ -54,14 +54,24 @@ class ReportPageComponent extends AuthComponent { super(props); this.state = { report: props.report, - graph: {nodes: [], edges: []} + graph: {nodes: [], edges: []}, + nodeStateList: [] }; } componentDidMount() { + this.getNodeStateListFromServer(); this.updateMapFromServer(); } + getNodeStateListFromServer = () => { + this.authFetch('/api/netmap/nodeStates') + .then(res => res.json()) + .then(res => { + this.setState({nodeStateList: res.node_states}); + }); + }; + componentWillUnmount() { clearInterval(this.interval); } @@ -396,7 +406,7 @@ class ReportPageComponent extends AuthComponent { Island Communication
- +
diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png new file mode 100644 index 000000000..cd53d7cb7 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png new file mode 100644 index 000000000..67129b5cc Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png new file mode 100644 index 000000000..d582c8ecf Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png new file mode 100644 index 000000000..a7aa12793 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_old.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_old.png new file mode 100644 index 000000000..58de7ff52 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_old.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png new file mode 100644 index 000000000..1545280b4 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png differ diff --git a/monkey/monkey_island/cc/utils.py b/monkey/monkey_island/cc/utils.py index 01c69e648..5504c34b6 100644 --- a/monkey/monkey_island/cc/utils.py +++ b/monkey/monkey_island/cc/utils.py @@ -1,5 +1,7 @@ import socket import sys +from typing import List +import collections import array @@ -49,6 +51,11 @@ else: return result +def is_local_ips(ips: List) -> bool: + filtered_local_ips = [ip for ip in local_ip_addresses() if not ip.startswith('169.254')] + return collections.Counter(ips) == collections.Counter(filtered_local_ips) + + # The local IP addresses list should not change often. Therefore, we can cache the result and never call this function # more than once. This stopgap measure is here since this function is called a lot of times during the report # generation.