Skip to content

Commit db41c08

Browse files
Merge pull request Pennyw0rth#317 from Pennyw0rth/neff-command-execution
Improving execution speed and misc command execution improvements
2 parents 06b1e06 + de2541e commit db41c08

7 files changed

Lines changed: 91 additions & 38 deletions

File tree

nxc/cli.py

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

115+
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
116+
if hasattr(args, "get_output_tries"):
117+
args.get_output_tries = args.get_output_tries * 10
118+
115119
return args
116120

117121

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: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ def proto_args(parser, std_parser, module_parser):
99
dgroup = smb_parser.add_mutually_exclusive_group()
1010
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain to authenticate to")
1111
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
12-
smb_parser.add_argument("--port", type=int, choices={445, 139}, default=445, help="SMB port (default: 445)")
13-
smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share (default: C$)")
12+
smb_parser.add_argument("--port", type=int, choices={445, 139}, default=445, help="SMB port (default: %(default)s)")
13+
smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share (default: %(default)s)")
1414
smb_parser.add_argument("--smb-server-port", default="445", help="specify a server port for SMB", type=int)
1515
smb_parser.add_argument("--gen-relay-list", metavar="OUTPUT_FILE", help="outputs all hosts that don't require SMB signing to the specified file")
16-
smb_parser.add_argument("--smb-timeout", help="SMB connection timeout, default 2 secondes", type=int, default=2)
16+
smb_parser.add_argument("--smb-timeout", help="SMB connection timeout, default %(default)s secondes", type=int, default=2)
1717
smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
1818
self_delegate_arg.make_required = [delegate_arg]
1919

@@ -45,7 +45,7 @@ def proto_args(parser, std_parser, module_parser):
4545
egroup.add_argument("--pass-pol", action="store_true", help="dump password policy")
4646
egroup.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RID's (default: 4000)")
4747
egroup.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query")
48-
egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace (default: root\\cimv2)")
48+
egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace (default: %(default)s)")
4949

5050
sgroup = smb_parser.add_argument_group("Spidering", "Options for spidering shares")
5151
sgroup.add_argument("--spider", metavar="SHARE", type=str, help="share to spider")
@@ -65,9 +65,9 @@ def proto_args(parser, std_parser, module_parser):
6565

6666
cgroup = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
6767
cgroup.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default=None, help="method to execute the command. Ignored if in MSSQL mode (default: wmiexec)")
68-
cgroup.add_argument("--dcom-timeout", help="DCOM connection timeout, default is 5 secondes", type=int, default=5)
69-
cgroup.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results, default is 5", type=int, default=5)
70-
cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default: utf-8). 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")
68+
cgroup.add_argument("--dcom-timeout", help="DCOM connection timeout, default is %(default)s secondes", type=int, default=5)
69+
cgroup.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results, default is %(default)s", type=int, default=10)
70+
cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default: %(default)s). 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")
7171
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
7272

7373
cegroup = cgroup.add_mutually_exclusive_group()

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

tests/e2e_commands.txt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds
2121
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa
2222
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
2323
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
24+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method atexec
25+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method smbexec
26+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method mmcexec
2427
##### SMB PowerShell
2528
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
29+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --exec-method atexec
30+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --exec-method smbexec
31+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --exec-method mmcexec
2632
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32
2733
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs
2834
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs
@@ -36,9 +42,9 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
3642
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --no-encode
3743
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode
3844
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode
39-
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --exec-method atexec
40-
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --exec-method smbexec
41-
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --exec-method mmcexec
45+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode --exec-method atexec
46+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode --exec-method smbexec
47+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode --exec-method mmcexec
4248
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --clear-obfscripts # current we don't really use?
4349
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi "select Name from win32_computersystem"
4450
netexec --jitter 2 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS

0 commit comments

Comments
 (0)