Skip to content

Commit 21b1cfb

Browse files
authored
Merge branch 'Pennyw0rth:main' into dumpSecurityQuestionsModule
2 parents 39b877a + 669a55f commit 21b1cfb

6 files changed

Lines changed: 220 additions & 17 deletions

File tree

nxc/modules/bitlocker.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import re
2+
from impacket.dcerpc.v5.dcom import wmi
3+
from impacket.dcerpc.v5.dtypes import NULL
4+
from impacket.dcerpc.v5.dcomrt import DCOMConnection
5+
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
6+
7+
class NXCModule:
8+
name = "bitlocker"
9+
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
10+
supported_protocols = ["smb", "wmi"]
11+
opsec_safe = True
12+
multiple_hosts = True
13+
14+
def __init__(self, context=None, module_options=None):
15+
self.context = context
16+
self.module_options = module_options
17+
18+
def options(self, context, module_options):
19+
"""
20+
USAGE:
21+
22+
NetExec smb <IP> -u <username> -p <password> -M bitlocker
23+
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
24+
"""
25+
26+
def on_admin_login(self, context, connection):
27+
if context.protocol == "smb":
28+
bitlocker_smb = BitLockerSMB(context, connection)
29+
bitlocker_smb.check_bitlocker_status()
30+
elif context.protocol == "wmi":
31+
bitlocker_wmi = BitLockerWMI(context, connection)
32+
bitlocker_wmi.check_bitlocker_status()
33+
34+
35+
class BitLockerSMB:
36+
def __init__(self, context, connection):
37+
self.context = context
38+
self.connection = connection
39+
40+
def check_bitlocker_status(self):
41+
# PowerShell command to check BitLocker volumes status.
42+
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
43+
44+
try:
45+
# Executing the PowerShell command to get BitLocker volumes status.
46+
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
47+
48+
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
49+
self.context.log.fail("BitLockerVolume not found on target.")
50+
return
51+
52+
# Splitting the output into lines.
53+
lines = str(check_bitlocker_command_str_output).splitlines()
54+
data_lines = [line for line in lines if re.match(r"\w:", line)]
55+
56+
for line in data_lines:
57+
# Checking every line for starting with drive
58+
if line[1] == ":":
59+
parts = line.split()
60+
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
61+
62+
# Checking if BitLocker is enabled.
63+
if protection_status == "On":
64+
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
65+
else:
66+
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
67+
except Exception as e:
68+
self.context.log.exception(f"Exception occurred: {e}")
69+
70+
71+
class BitLockerWMI:
72+
def __init__(self, context, connection):
73+
self.context = context
74+
self.connection = connection
75+
76+
def check_bitlocker_status(self):
77+
try:
78+
# Create a DCOM connection
79+
dcom_conn = DCOMConnection(
80+
self.connection.host,
81+
self.connection.username,
82+
self.connection.password,
83+
self.connection.domain,
84+
self.connection.lmhash,
85+
self.connection.nthash,
86+
oxidResolver=True,
87+
doKerberos=self.connection.kerberos,
88+
kdcHost=self.connection.kdcHost)
89+
90+
try:
91+
# CoCreateInstanceEx for WMI login
92+
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
93+
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
94+
95+
# Specify the namespace for BitLocker
96+
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
97+
98+
# NTLM login for WMI
99+
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
100+
101+
# Set authentication level
102+
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
103+
104+
# Query to get BitLocker status
105+
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
106+
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
107+
encryptionTypeMapping = {0: "None", 1: "AES_256_WITH_DIFFUSER", 2: "AES_256_WITH_DIFFUSER", 3: "AES_128", 4: "AES_256", 5: "HARDWARE_ENCRYPTION", 6: "XTS_AES_128", 7: "XTS_AES_256"}
108+
109+
try:
110+
while True:
111+
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
112+
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
113+
if iWbemClassObject[0].ProtectionStatus == 1:
114+
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
115+
else:
116+
if encryptionMethod == 0: # Should be 0 if disabled
117+
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
118+
except Exception:
119+
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
120+
121+
# Release resources
122+
iWbemLevel1Login.RemRelease()
123+
iWbemServices.RemRelease()
124+
dcom_conn.disconnect()
125+
except Exception as e:
126+
if "WBEM_E_INVALID_NAMESPACE" in str(e):
127+
self.context.log.fail("BitLockerNamespace not found on target.")
128+
dcom_conn.disconnect()
129+
except Exception as e:
130+
self.context.log.error(f"Error occurred during BitLocker check: {e}")
131+
dcom_conn.disconnect()

nxc/modules/pso.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def on_login(self, context, connection):
5353
for attrs in resp:
5454
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
5555
continue
56-
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, obersationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
56+
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, observationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
5757
for attr in attrs["attributes"]:
5858
if str(attr["type"]) == "name":
5959
policyName = attr["vals"][0]

nxc/protocols/ldap/proto_args.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def proto_args(parser, parents):
1111
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
1212
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
1313

14-
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
14+
egroup = ldap_parser.add_argument_group("Retrieve hash on the remote DC", "Options to get hashes from Kerberos")
1515
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
1616
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
1717

@@ -26,7 +26,7 @@ def proto_args(parser, parents):
2626
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
2727
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
2828

29-
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
29+
ggroup = ldap_parser.add_argument_group("Retrieve gmsa on the remote DC", "Options to play with gmsa")
3030
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
3131
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
3232
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")

nxc/protocols/smb.py

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
NTDSHashes,
1515
)
1616
from impacket.nmb import NetBIOSError, NetBIOSTimeout
17-
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr
17+
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp
1818
from impacket.dcerpc.v5.rpcrt import DCERPCException
1919
from impacket.dcerpc.v5.transport import DCERPCTransportFactory, SMBTransport
2020
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
@@ -273,7 +273,7 @@ def enum_host_info(self):
273273
self.conn.logoff()
274274
except Exception as e:
275275
self.logger.debug(f"Error logging off system: {e}")
276-
276+
277277
# DCOM connection with kerberos needed
278278
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
279279

@@ -572,6 +572,9 @@ def check_if_admin(self):
572572
self.admin_privs = True
573573
except scmr.DCERPCException:
574574
self.admin_privs = False
575+
except Exception as e:
576+
self.logger.fail(f"Error checking if user is admin on {self.host}: {e}")
577+
self.admin_privs = False
575578

576579
def gen_relay_list(self):
577580
if self.server_os.lower().find("windows") != -1 and self.signing is False:
@@ -707,11 +710,11 @@ def execute(self, payload=None, get_output=False, methods=None):
707710
except UnicodeDecodeError:
708711
self.logger.debug("Decoding error detected, consider running chcp.com at the target, map the result with https://docs.python.org/3/library/codecs.html#standard-encodings")
709712
output = output.decode("cp437")
710-
713+
711714
self.logger.debug(f"Raw Output: {output}")
712715
output = "\n".join([ll.rstrip() for ll in output.splitlines() if ll.strip()])
713716
self.logger.debug(f"Cleaned Output: {output}")
714-
717+
715718
if "This script contains malicious content" in output:
716719
self.logger.fail("Command execution blocked by AMSI")
717720
return None
@@ -732,24 +735,24 @@ def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=Fa
732735
if not payload:
733736
self.logger.error("No command to execute specified!")
734737
return None
735-
738+
736739
response = []
737740
obfs = obfs if obfs else self.args.obfs
738741
encode = encode if encode else not self.args.no_encode
739742
force_ps32 = force_ps32 if force_ps32 else self.args.force_ps32
740743
get_output = True if not self.args.no_output else get_output
741-
744+
742745
self.logger.debug(f"Starting ps_execute(): {payload=} {get_output=} {methods=} {force_ps32=} {obfs=} {encode=}")
743746
amsi_bypass = self.args.amsi_bypass[0] if self.args.amsi_bypass else None
744747
self.logger.debug(f"AMSI Bypass: {amsi_bypass}")
745-
748+
746749
if os.path.isfile(payload):
747750
self.logger.debug(f"File payload set: {payload}")
748751
with open(payload) as commands:
749752
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods) for c in commands]
750753
else:
751754
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods)]
752-
755+
753756
self.logger.debug(f"ps_execute response: {response}")
754757
return response
755758

@@ -834,6 +837,74 @@ def shares(self):
834837
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
835838
return permissions
836839

840+
def interfaces(self):
841+
"""
842+
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
843+
Made by: @Sant0rryu, @NeffIsBack
844+
"""
845+
try:
846+
remoteOps = RemoteOperations(self.conn, False)
847+
remoteOps.enableRegistry()
848+
849+
if remoteOps._RemoteOperations__rrp:
850+
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
851+
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
852+
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
853+
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
854+
855+
self.logger.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
856+
for sub_key in sub_keys:
857+
interface = {}
858+
try:
859+
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
860+
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
861+
862+
# Retrieve Interace Name
863+
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
864+
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
865+
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
866+
interface["Name"] = str(interface_name)
867+
if "Kernel" in interface_name:
868+
continue
869+
870+
# Retrieve DHCP
871+
try:
872+
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
873+
except DCERPCException:
874+
dhcp_enabled = False
875+
interface["DHCP"] = bool(dhcp_enabled)
876+
877+
# Retrieve IPAddress
878+
try:
879+
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
880+
except DCERPCException:
881+
ip_address = None
882+
interface["IPAddress"] = ip_address if ip_address else None
883+
884+
# Retrieve SubnetMask
885+
try:
886+
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
887+
except DCERPCException:
888+
subnetmask = None
889+
interface["SubnetMask"] = subnetmask if subnetmask else None
890+
891+
# Retrieve DefaultGateway
892+
try:
893+
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
894+
except DCERPCException:
895+
default_gateway = None
896+
interface["DefaultGateway"] = default_gateway if default_gateway else None
897+
898+
self.logger.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
899+
900+
except DCERPCException as e:
901+
self.logger.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
902+
903+
with contextlib.suppress(Exception):
904+
remoteOps.finish()
905+
except DCERPCException as e:
906+
self.logger.error(f"Failed to connect to the target: {e!s}")
907+
837908
def get_dc_ips(self):
838909
dc_ips = [dc[1] for dc in self.db.get_domain_controllers(domain=self.domain)]
839910
if not dc_ips:
@@ -1302,7 +1373,7 @@ def put_file_single(self, src, dst):
13021373
self.logger.success(f"Created file {src} on \\\\{self.args.share}\\{dst}")
13031374
except Exception as e:
13041375
self.logger.fail(f"Error writing file to share {self.args.share}: {e}")
1305-
1376+
13061377
def put_file(self):
13071378
for src, dest in self.args.put_file:
13081379
self.put_file_single(src, dest)
@@ -1325,7 +1396,6 @@ def get_file(self):
13251396
for src, dest in self.args.get_file:
13261397
self.get_file_single(src, dest)
13271398

1328-
13291399
def enable_remoteops(self):
13301400
try:
13311401
self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost)
@@ -1408,7 +1478,7 @@ def sccm(self):
14081478
except Exception as e:
14091479
self.logger.debug(f"Could not upgrade connection: {e}")
14101480
return
1411-
1481+
14121482
try:
14131483
self.logger.display("Collecting Machine masterkeys, grab a coffee and be patient...")
14141484
masterkeys_triage = MasterkeysTriage(
@@ -1423,7 +1493,7 @@ def sccm(self):
14231493
if len(masterkeys) == 0:
14241494
self.logger.fail("No masterkeys looted")
14251495
return
1426-
1496+
14271497
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}")
14281498
try:
14291499
# Collect Chrome Based Browser stored secrets
@@ -1613,7 +1683,6 @@ def dpapi(self):
16131683
"Google Refresh Token",
16141684
)
16151685

1616-
16171686
if dump_cookies and cookies:
16181687
self.logger.display("Start Dumping Cookies")
16191688
for cookie in cookies:
@@ -1801,4 +1870,4 @@ def add_ntds_hash(ntds_hash, host_id):
18011870
NTDS.finish()
18021871

18031872
def mark_guest(self):
1804-
return highlight(f"{highlight('(Guest)')}" if self.is_guest else "")
1873+
return highlight(f"{highlight('(Guest)')}" if self.is_guest else "")

nxc/protocols/smb/proto_args.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def proto_args(parser, parents):
3434

3535
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
3636
mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access")
37+
mapping_enum_group.add_argument("--interfaces", action="store_true", help="enumerate network interfaces")
3738
mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
3839
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
3940
mapping_enum_group.add_argument("--sessions", action="store_true", help="enumerate active sessions")

0 commit comments

Comments
 (0)