Skip to content

Commit 12a7fa0

Browse files
Merge branch 'main' into marshall-options-fix
Signed-off-by: Marshall Hallenbeck <Marshall.Hallenbeck@gmail.com>
2 parents 16508f8 + 2191dee commit 12a7fa0

11 files changed

Lines changed: 134 additions & 66 deletions

File tree

nxc/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ def gen_cli_args():
126126
print(f"{VERSION} - {CODENAME} - {COMMIT}")
127127
sys.exit(1)
128128

129+
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
130+
if hasattr(args, "get_output_tries"):
131+
args.get_output_tries = args.get_output_tries * 10
132+
129133
return args
130134

131135

nxc/modules/wcc.py

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,6 @@
2828
REG_VALUE_TYPE_UNICODE_STRING_SEQUENCE = 7
2929
REG_VALUE_TYPE_64BIT_LE = 11
3030

31-
# Setup file logger
32-
if "wcc_logger" not in globals():
33-
wcc_logger = logging.getLogger("WCC")
34-
wcc_logger.propagate = False
35-
log_filename = nxc_logger.init_log_file()
36-
log_filename = log_filename.replace("log_", "wcc_")
37-
wcc_logger.setLevel(logging.INFO)
38-
wcc_file_handler = logging.FileHandler(log_filename)
39-
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
40-
wcc_logger.addHandler(wcc_file_handler)
41-
4231

4332
class ConfigCheck:
4433
"""Class for performing the checks and holding the results"""
@@ -75,7 +64,7 @@ def run(self):
7564
def log(self, context):
7665
result = "passed" if self.ok else "did not pass"
7766
reasons = ", ".join(self.reasons)
78-
wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
67+
self.module.wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
7968
if self.module.quiet:
8069
return
8170

@@ -99,6 +88,19 @@ class NXCModule:
9988
supported_protocols = ["smb"]
10089
opsec_safe = True
10190
multiple_hosts = True
91+
92+
def __init__(self):
93+
self.context = None
94+
self.module_options = None
95+
96+
self.wcc_logger = logging.getLogger("WCC")
97+
self.wcc_logger.propagate = False
98+
log_filename = nxc_logger.init_log_file()
99+
log_filename = log_filename.replace("log_", "wcc_")
100+
self.wcc_logger.setLevel(logging.INFO)
101+
wcc_file_handler = logging.FileHandler(log_filename)
102+
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
103+
self.wcc_logger.addHandler(wcc_file_handler)
102104

103105
def options(self, context, module_options):
104106
"""
@@ -156,15 +158,9 @@ def __init__(self, context, connection):
156158
self.dce = remoteOps._RemoteOperations__rrp
157159

158160
def run(self):
159-
# Prepare checks
160161
self.init_checks()
161-
162-
# Perform checks
163162
self.check_config()
164-
165-
# Check methods #
166-
#################
167-
163+
168164
def init_checks(self):
169165
# Declare the checks to do and how to do them
170166
self.checks = [
@@ -483,9 +479,6 @@ def check_applocker(self):
483479

484480
return success, reasons
485481

486-
# Methods for getting values from the remote registry #
487-
#######################################################
488-
489482
def _open_root_key(self, dce, connection, root_key):
490483
ans = None
491484
retries = 1
@@ -595,9 +588,6 @@ def get_value(subkey_handle, dwIndex=0):
595588
return data
596589
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
597590

598-
# Methods for getting values from SAMR and SCM #
599-
################################################
600-
601591
def get_service(self, service_name, connection):
602592
"""Get the service status and configuration for specified service"""
603593
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
@@ -645,23 +635,15 @@ def ls(self, smb, path="\\", share="C$"):
645635
self.context.log.error(f"ls(): C:\\{path} {e}\n")
646636
return file_listing
647637

648-
649-
# Comparison operators #
650-
########################
651-
652-
653638
def le(reg_sz_string, number):
654639
return int(reg_sz_string[:-1]) <= number
655640

656-
657641
def in_(obj, seq):
658642
return obj in seq
659643

660-
661644
def startswith(string, start):
662645
return string.startswith(start)
663646

664-
665647
def not_(boolean_operator):
666648
def wrapper(*args, **kwargs):
667649
return not boolean_operator(*args, **kwargs)

nxc/protocols/ldap.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from impacket.krb5.types import Principal, KerberosException
2929
from impacket.ldap import ldap as ldap_impacket
3030
from impacket.ldap import ldapasn1 as ldapasn1_impacket
31+
from impacket.ldap.ldap import LDAPFilterSyntaxError
3132
from impacket.smb import SMB_DIALECT
3233
from impacket.smbconnection import SMBConnection, SessionError
3334

@@ -1054,6 +1055,36 @@ def kerberoasting(self):
10541055
self.logger.highlight("No entries found!")
10551056
self.logger.fail("Error with the LDAP account used")
10561057

1058+
def query(self):
1059+
"""
1060+
Query the LDAP server with the specified filter and attributes.
1061+
Example usage:
1062+
--query "(sAMAccountName=Administrator)" "sAMAccountName pwdLastSet memberOf"
1063+
"""
1064+
search_filter = self.args.query[0]
1065+
attributes = [attr.strip() for attr in self.args.query[1].split(" ")]
1066+
if len(attributes) == 1 and attributes[0] == "":
1067+
attributes = None
1068+
if not search_filter:
1069+
self.logger.fail("No filter specified")
1070+
return
1071+
self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}")
1072+
try:
1073+
resp = self.search(search_filter, attributes, 0)
1074+
except LDAPFilterSyntaxError as e:
1075+
self.logger.fail(f"LDAP Filter Syntax Error: {e}")
1076+
return
1077+
for item in resp:
1078+
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
1079+
continue
1080+
self.logger.success(f"Response for object: {item['objectName']}")
1081+
for attribute in item["attributes"]:
1082+
attr = f"{attribute['type']}:"
1083+
vals = str(attribute["vals"]).replace("\n", "")
1084+
if "SetOf: " in vals:
1085+
vals = vals.replace("SetOf: ", "")
1086+
self.logger.highlight(f"{attr:<20} {vals}")
1087+
10571088
def trusted_for_delegation(self):
10581089
# Building the search filter
10591090
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"

nxc/protocols/ldap/proto_args.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def proto_args(parser, parents):
1616
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
1717

1818
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
19+
vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes")
1920
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
2021
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
2122
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")

nxc/protocols/smb.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,8 @@ def wmi(self, wmi_query=None, namespace=None):
11261126
record = wmi_results.getProperties()
11271127
records.append(record)
11281128
for k, v in record.items():
1129-
self.logger.highlight(f"{k} => {v['value']}")
1129+
if k != "TimeGenerated": # from the wcc module, but this is a small hack to get it to stop spamming - TODO: add in method to disable output for this function
1130+
self.logger.highlight(f"{k} => {v['value']}")
11301131
except Exception as e:
11311132
if str(e).find("S_FALSE") < 0:
11321133
raise e

nxc/protocols/smb/atexec.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def execute_handler(self, command, fileless=False):
133133

134134
xml = self.gen_xml(command, fileless)
135135

136-
self.logger.info(f"Task XML: {xml}")
136+
self.logger.debug(f"Task XML: {xml}")
137137
taskCreated = False
138138
self.logger.info(f"Creating task \\{tmpName}")
139139
try:
@@ -181,22 +181,35 @@ def execute_handler(self, command, fileless=False):
181181
else:
182182
":".join(map(str, self.__rpctransport.get_socket().getpeername()))
183183
smbConnection = self.__rpctransport.get_smb_connection()
184-
tries = 1
184+
185+
tries = 0
186+
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
187+
sleep(0.4)
185188
while True:
186189
try:
187190
self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}")
188191
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
189192
break
190193
except Exception as e:
191-
if tries >= self.__tries:
194+
if tries > self.__tries:
192195
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
193196
break
194-
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
197+
if "STATUS_BAD_NETWORK_NAME" in str(e):
195198
self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
196199
break
197-
if str(e).find("SHARING") > 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
198-
sleep(3)
200+
elif "STATUS_VIRUS_INFECTED" in str(e):
201+
self.logger.fail("Command did not run because a virus was detected")
202+
break
203+
# When executing powershell and the command is still running, we get a sharing violation
204+
# We can use that information to wait longer than if the file is not found (probably av or something)
205+
if "STATUS_SHARING_VIOLATION" in str(e):
206+
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} left, retrying...")
199207
tries += 1
208+
sleep(1)
209+
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
210+
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
211+
tries += 10
212+
sleep(1)
200213
else:
201214
self.logger.debug(str(e))
202215

nxc/protocols/smb/mmcexec.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,23 +247,35 @@ def get_output_remote(self):
247247
if self.__retOutput is False:
248248
self.__outputBuffer = ""
249249
return
250-
tries = 1
250+
251+
tries = 0
252+
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
253+
sleep(0.4)
251254
while True:
252255
try:
253256
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
254257
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
255258
break
256259
except Exception as e:
257-
if tries >= self.__tries:
260+
if tries > self.__tries:
258261
self.logger.fail("MMCEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
259262
break
260-
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
263+
if "STATUS_BAD_NETWORK_NAME" in str(e):
261264
self.logger.fail(f"MMCEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
262265
break
263-
if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
264-
# Output not finished, let's wait
265-
sleep(2)
266+
elif "STATUS_VIRUS_INFECTED" in str(e):
267+
self.logger.fail("Command did not run because a virus was detected")
268+
break
269+
# When executing powershell and the command is still running, we get a sharing violation
270+
# We can use that information to wait longer than if the file is not found (probably av or something)
271+
if "STATUS_SHARING_VIOLATION" in str(e):
272+
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
266273
tries += 1
274+
sleep(1)
275+
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
276+
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
277+
tries += 10
278+
sleep(1)
267279
else:
268280
self.logger.debug(str(e))
269281

nxc/protocols/smb/proto_args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def proto_args(parser, parents):
6969
cmd_exec_group = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
7070
cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode")
7171
cmd_exec_group.add_argument("--dcom-timeout", help="DCOM connection timeout", type=int, default=5)
72-
cmd_exec_group.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results", type=int, default=5)
72+
cmd_exec_group.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results", type=int, default=10)
7373
cmd_exec_group.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec")
7474
cmd_exec_group.add_argument("--no-output", action="store_true", help="do not retrieve command output")
7575

nxc/protocols/smb/smbexec.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ def execute_remote(self, data):
9999
batch_file.write(command)
100100

101101
self.logger.debug("Hosting batch file with command: " + command)
102-
103102
self.logger.debug("Command to execute: " + command)
104-
105103
self.logger.debug(f"Remote service {self.__serviceName} created.")
106104

107105
try:
@@ -138,23 +136,35 @@ def get_output_remote(self):
138136
if self.__retOutput is False:
139137
self.__outputBuffer = ""
140138
return
141-
tries = 1
139+
140+
# TODO: It looks like the service is hanging anyway until the command is finished, so all this timeout logic is likely not needed
141+
# Still adding this for now to keep the structure similar until we can confirm the above
142+
tries = 0
142143
while True:
143144
try:
144145
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
145146
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
146147
break
147148
except Exception as e:
148-
if tries >= self.__tries:
149+
if tries > self.__tries:
149150
self.logger.fail("SMBEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
150151
break
151-
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
152+
if "STATUS_BAD_NETWORK_NAME" in str(e):
152153
self.logger.fail(f"SMBEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
153154
break
154-
if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
155-
# Output not finished, let's wait
156-
sleep(2)
155+
elif "STATUS_VIRUS_INFECTED" in str(e):
156+
self.logger.fail("Command did not run because a virus was detected")
157+
break
158+
# When executing powershell and the command is still running, we get a sharing violation
159+
# We can use that information to wait longer than if the file is not found (probably av or something)
160+
if "STATUS_SHARING_VIOLATION" in str(e):
161+
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
157162
tries += 1
163+
sleep(1)
164+
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
165+
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
166+
tries += 10
167+
sleep(1)
158168
else:
159169
self.logger.debug(str(e))
160170

nxc/protocols/smb/wmiexec.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,28 +140,36 @@ def get_output_remote(self):
140140
self.__outputBuffer = ""
141141
return
142142

143-
tries = 1
143+
tries = 0
144+
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
145+
sleep(0.4)
144146
while True:
145147
try:
146148
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
147149
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
148150
break
149151
except Exception as e:
150-
if tries >= self.__tries:
152+
if tries > self.__tries:
151153
self.logger.fail("wmiexec: Could not retrieve output file, it may have been detected by AV. If it is still failing, try the 'wmi' protocol or another exec method")
152154
break
153-
elif str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
155+
elif "STATUS_BAD_NETWORK_NAME" in str(e):
154156
self.logger.fail(f"SMB connection: target has blocked {self.__share} access (maybe command executed!)")
155157
break
156-
elif str(e).find("STATUS_VIRUS_INFECTED") >= 0:
158+
elif "STATUS_VIRUS_INFECTED" in str(e):
157159
self.logger.fail("Command did not run because a virus was detected")
158160
break
159-
160-
if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
161-
sleep(2)
161+
# When executing powershell and the command is still running, we get a sharing violation
162+
# We can use that information to wait longer than if the file is not found (probably av or something)
163+
elif "STATUS_SHARING_VIOLATION" in str(e):
164+
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
165+
sleep(1)
166+
tries += 1
167+
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
168+
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
169+
tries += 10
170+
sleep(1)
162171
else:
163172
self.logger.debug(f"Exception when trying to read output file: {e}")
164-
tries += 1
165173

166174
if self.__outputBuffer:
167175
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")

0 commit comments

Comments
 (0)