Skip to content

Commit 19a54d5

Browse files
authored
Merge pull request Pennyw0rth#820 from Pennyw0rth/neff-fix-wmi
Refactor WMI protocol execution
2 parents a4d54d1 + 65d3723 commit 19a54d5

4 files changed

Lines changed: 139 additions & 34 deletions

File tree

nxc/protocols/wmi.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
208208
username = ccache.credentials[0].header["client"].prettyPrint().decode().split("@")[0]
209209
self.username = username
210210
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
211-
211+
212212
try:
213213
self.logger.debug(f"Attempting to connect via WMI to {self.host}")
214214
self.conn.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash, aesKey=self.aesKey)
@@ -369,7 +369,7 @@ def hash_login(self, domain, username, ntlm_hash):
369369
@requires_admin
370370
def wmi(self, wql=None, namespace=None):
371371
"""Execute WQL syntax via WMI
372-
372+
373373
This is done via the --wmi flag
374374
"""
375375
records = []
@@ -409,7 +409,7 @@ def wmi(self, wql=None, namespace=None):
409409
return records
410410

411411
@requires_admin
412-
def execute(self, command=None, get_output=False):
412+
def execute(self, command=None, get_output=False, use_powershell=False):
413413
output = ""
414414

415415
# Execution via -x
@@ -428,21 +428,38 @@ def execute(self, command=None, get_output=False):
428428

429429
if self.args.exec_method == "wmiexec":
430430
exec_method = wmiexec.WMIEXEC(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.host, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
431-
output = exec_method.execute(command, get_output)
431+
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
432432

433433
elif self.args.exec_method == "wmiexec-event":
434434
exec_method = wmiexec_event.WMIEXEC_EVENT(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.host, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
435-
output = exec_method.execute(command, get_output)
435+
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
436436

437437
self.conn.disconnect()
438-
if output == "" and get_output:
439-
self.logger.fail("Execute command failed, probabaly got detection by AV.")
440-
return ""
441-
elif self.args.execute and get_output:
438+
if self.args.execute and get_output:
442439
self.logger.success(f'Executed command: "{command}" via {self.args.exec_method}')
443440
buf = StringIO(output).readlines()
444441
for line in buf:
445-
self.logger.highlight(line.strip())
442+
if line.strip():
443+
self.logger.highlight(line.strip())
446444
return output
447-
elif get_output:
445+
else:
448446
return output
447+
448+
def execute_psh(self, command=None, get_output=False):
449+
# Execution via -X
450+
if not command and self.args.execute_psh:
451+
command = self.args.execute_psh
452+
if not self.args.no_output:
453+
get_output = True
454+
455+
output = self.execute(command, get_output, use_powershell=True)
456+
457+
if self.args.execute_psh and get_output:
458+
self.logger.success(f'Executed PowerShell command: "{command}" via {self.args.exec_method}')
459+
buf = StringIO(output).readlines()
460+
for line in buf:
461+
if line.strip():
462+
self.logger.highlight(line.strip())
463+
return output
464+
else:
465+
return output

nxc/protocols/wmi/proto_args.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ def proto_args(parser, parents):
1616
cgroup = wmi_parser.add_argument_group("Command Execution", "Options for executing commands")
1717
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
1818
cgroup.add_argument("-x", metavar="COMMAND", dest="execute", type=str, help="Creates a new cmd process and executes the specified command with output")
19+
cgroup.add_argument("-X", metavar="COMMAND", dest="execute_psh", type=str, help="Creates a new PowerShell process and executes the specified command with output")
1920
cgroup.add_argument("--exec-method", choices={"wmiexec", "wmiexec-event"}, default="wmiexec", help="method to execute the command. (default: wmiexec). [wmiexec (win32_process + StdRegProv)]: get command results over registry instead of using smb connection. [wmiexec-event (T1546.003)]: this method is not very stable, highly recommend use this method in single host, using on multiple hosts may crash (just try again if it crashed).")
20-
cgroup.add_argument("--exec-timeout", default=3, metavar="exec_timeout", dest="exec_timeout", type=int, help="Set timeout (in seconds) when executing a command, minimum 5 seconds is recommended. Default: %(default)s")
21+
cgroup.add_argument("--exec-timeout", default=2, metavar="exec_timeout", dest="exec_timeout", type=int, help="Set timeout (in seconds) when executing a command, minimum 5 seconds is recommended. Default: %(default)s")
2122
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")
2223
return parser
2324

nxc/protocols/wmi/wmiexec.py

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,17 @@ def __init__(self, target, username, password, domain, lmhash, nthash, doKerbero
5151
iWbemLevel1Login.RemRelease()
5252
self.__win32Process, _ = self.__iWbemServices.GetObject("Win32_Process")
5353

54-
def execute(self, command, output=False):
55-
if output:
54+
def execute(self, command, output=False, use_powershell=False):
55+
"""Execute a command on the remote host using WMI.
56+
Options:
57+
- No output
58+
- Output with bash (limited to ~1MB)
59+
- Output with PowerShell (recommended for larger outputs)
60+
"""
61+
if output and not use_powershell:
5662
self.execute_WithOutput(command)
63+
elif output and use_powershell:
64+
self.execute_WithOutput_psh(command)
5765
else:
5866
command = self.__shell + command
5967
self.execute_remote(command)
@@ -73,35 +81,112 @@ def execute_WithOutput(self, command):
7381
result_output = f"C:\\windows\\temp\\{uuid.uuid4()!s}.txt"
7482
result_output_b64 = f"C:\\windows\\temp\\{uuid.uuid4()!s}.txt"
7583
keyName = str(uuid.uuid4())
76-
self.__registry_Path = f"Software\\Classes\\{gen_random_string(6)}"
77-
78-
commands = [
79-
f"{self.__shell} {command} 1> {result_output} 2>&1",
80-
f"{self.__shell} certutil -encodehex -f {result_output} {result_output_b64} 0x40000001",
81-
f'{self.__shell} for /F "usebackq" %G in ("{result_output_b64}") do reg add HKLM\\{self.__registry_Path} /v {keyName} /t REG_SZ /d "%G" /f',
82-
f"{self.__shell} del /q /f /s {result_output} {result_output_b64}",
83-
]
84-
85-
for cmd in commands:
86-
self.execute_remote(cmd)
87-
time.sleep(0.5)
88-
self.logger.info(f"Waiting {self.__exec_timeout}s for command completely executed.")
84+
self.__registry_Path = f"Software\\Classes\\{gen_random_string(8)}"
85+
86+
# 1. Run the command and write output to file
87+
self.execute_remote(f'{self.__shell} {command} 1> "{result_output}" 2>&1')
88+
self.logger.info(f"Waiting {self.__exec_timeout}s for command to complete.")
8989
time.sleep(self.__exec_timeout)
9090

91+
# 2. Base64 encode the file
92+
self.execute_remote(f"{self.__shell} certutil -encodehex -f {result_output} {result_output_b64} 0x40000001")
93+
time.sleep(0.5)
94+
95+
# 3. Store content in registry
96+
self.execute_remote(f'{self.__shell} for /F "usebackq" %G in ("{result_output_b64}") do reg add HKLM\\{self.__registry_Path} /v {keyName} /t REG_SZ /d "%G" /f')
97+
time.sleep(0.1)
98+
9199
self.queryRegistry(keyName)
100+
self.clean_up(result_output, result_output_b64)
92101

93102
def queryRegistry(self, keyName):
94103
try:
95-
self.logger.debug(f"Querying registry key: HKLM\\{self.__registry_Path}")
104+
# Spawn an instance of StdRegProv to access the registry
105+
self.logger.debug(f"Retrieving output from: HKLM\\{self.__registry_Path}")
96106
descriptor, _ = self.__iWbemServices.GetObject("StdRegProv")
97107
descriptor = descriptor.SpawnInstance()
98-
retVal = descriptor.GetStringValue(0x80000002, self.__registry_Path, keyName)
99-
self.__outputBuffer = base64.b64decode(retVal.sValue).decode(self.__codec, errors="replace").rstrip("\r\n")
108+
109+
# Retrieve the base64 content from the registry
110+
for _ in range(10):
111+
self.logger.debug(f"Retrieving key: {keyName}")
112+
outputBuffer_b64 = descriptor.GetStringValue(0x80000002, self.__registry_Path, keyName).sValue
113+
if outputBuffer_b64 is not None:
114+
break
115+
time.sleep(1)
116+
self.__outputBuffer = base64.b64decode(outputBuffer_b64).decode(self.__codec, errors="replace").rstrip("\r\n")
100117
except Exception:
101-
self.logger.fail("WMIEXEC: Could not retrieve output file, it may have been detected by AV. Please try increasing the timeout with the '--exec-timeout' option. If it is still failing, try the 'smb' protocol or another exec method")
118+
self.logger.fail("WMIEXEC: Could not retrieve output file! Either command timed out or AV killed the process. Please try increasing the timeout: '--exec-timeout 10'")
119+
120+
def execute_WithOutput_psh(self, command):
121+
"""Same functionality as execute_WithOutput, but uses PowerShell to handle larger outputs by splitting the base64 content into chunks and storing it in the registry."""
122+
result_output = f"C:\\windows\\temp\\{uuid.uuid4()!s}.txt"
123+
result_output_b64 = f"C:\\windows\\temp\\{uuid.uuid4()!s}.txt"
124+
keyName = str(uuid.uuid4())
125+
self.__registry_Path = f"Software\\Classes\\{gen_random_string(8)}"
126+
127+
# 1. Run the command and write output to file
128+
if not command.lower().startswith("powershell"):
129+
command = f"powershell -Command {command}"
130+
self.execute_remote(f'{command} > "{result_output}" 2>&1')
131+
self.logger.info(f"Waiting {self.__exec_timeout}s for command to complete.")
132+
time.sleep(self.__exec_timeout)
133+
134+
# 2. Base64 encode the file using PowerShell
135+
self.execute_remote(f'powershell -Command "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\'{result_output}\')) | Out-File -Encoding ASCII \'{result_output_b64}\'"')
136+
time.sleep(0.5)
137+
138+
# 3. Use PowerShell to split base64 content into 16KB chunks and store in registry
139+
self.execute_remote(
140+
f'powershell -Command "$b64 = Get-Content -Raw \'{result_output_b64}\'; '
141+
f'$chunksize = 16000; '
142+
f'$count = [math]::Ceiling($b64.Length / $chunksize); '
143+
f'for ($i = 0; $i -lt $count; $i++) {{ '
144+
f' $chunk = $b64.Substring($i * $chunksize, [math]::Min($chunksize, $b64.Length - ($i * $chunksize))); '
145+
f' $name = \\"{keyName}_chunk_$i\\"; '
146+
f' reg add \\"HKLM\\{self.__registry_Path}\\" /v $name /t REG_SZ /d $chunk /f }}; '
147+
f'reg add \\"HKLM\\{self.__registry_Path}\\" /v \\"{keyName}\\" /t REG_DWORD /d $count /f"'
148+
)
149+
time.sleep(0.1)
150+
151+
self.queryRegistry_psh(keyName)
152+
self.clean_up(result_output, result_output_b64)
153+
154+
def queryRegistry_psh(self, keyName):
155+
try:
156+
# Spawn an instance of StdRegProv to access the registry
157+
self.logger.debug(f"Retrieving output from: HKLM\\{self.__registry_Path}")
158+
descriptor, _ = self.__iWbemServices.GetObject("StdRegProv")
159+
descriptor = descriptor.SpawnInstance()
160+
161+
# Get the number of chunks stored in the registry
162+
num_chunks = None
163+
for _ in range(10):
164+
self.logger.debug(f"Retrieving number of chunks for key: {keyName}")
165+
num_chunks = descriptor.GetDWORDValue(0x80000002, self.__registry_Path, keyName).uValue
166+
if num_chunks is not None:
167+
break
168+
time.sleep(1)
169+
170+
self.logger.debug(f"Number of chunks: {num_chunks}")
171+
172+
# Retrieve each chunk and decode the base64 content
173+
outputBuffer_b64 = ""
174+
for i in range(num_chunks):
175+
chunk_name = f"{keyName}_chunk_{i}"
176+
self.logger.debug(f"Retrieving chunk: {chunk_name}")
177+
outputBuffer_b64 += descriptor.GetStringValue(0x80000002, self.__registry_Path, chunk_name).sValue
178+
self.__outputBuffer = base64.b64decode(outputBuffer_b64).decode("utf-16le", errors="replace").rstrip("\r\n").lstrip("\ufeff") # Remove BOM if present
179+
except Exception:
180+
self.logger.fail("WMIEXEC: Could not retrieve output file! Either command timed out or AV killed the process. Please try increasing the timeout: '--exec-timeout 10'")
181+
182+
def clean_up(self, result_output, result_output_b64):
183+
"""Deletes the output file, the base64 output file, and the registry path where the base64 content was stored."""
184+
self.execute_remote(f'{self.__shell} del /q /f "{result_output}" "{result_output_b64}"')
102185

103186
try:
104187
self.logger.debug(f"Removing temporary registry path: HKLM\\{self.__registry_Path}")
105-
retVal = descriptor.DeleteKey(0x80000002, self.__registry_Path)
188+
descriptor, _ = self.__iWbemServices.GetObject("StdRegProv")
189+
descriptor = descriptor.SpawnInstance()
190+
descriptor.DeleteKey(0x80000002, self.__registry_Path)
106191
except Exception as e:
107-
self.logger.debug(f"Target: {self.__target} removing temporary registry path error: {e!s}")
192+
self.logger.fail(f"Target: {self.__target} removing temporary registry path error: {e!s}")

nxc/protocols/wmi/wmiexec_event.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ def __init__(self, target, username, password, domain, lmhash, nthash, doKerbero
5757
self.__iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL)
5858
iWbemLevel1Login.RemRelease()
5959

60-
def execute(self, command, output=False):
60+
def execute(self, command, output=False, use_powershell=False):
6161
if "'" in command:
6262
command = command.replace("'", r'"')
63+
if use_powershell:
64+
command = f"powershell.exe -Command {command}"
6365
self.__retOutput = output
6466
self.execute_handler(command)
6567

0 commit comments

Comments
 (0)