Skip to content

Commit be20d01

Browse files
authored
Merge pull request Pennyw0rth#676 from Adamkadaban/rdp-exec
2 parents 82b0f97 + 7483a15 commit be20d01

5 files changed

Lines changed: 221 additions & 1 deletion

File tree

nxc/logger.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ def __init__(self, extra=None, merge_extra=False):
101101
logging.getLogger("minidump").disabled = True
102102
logging.getLogger("lsassy").disabled = True
103103
logging.getLogger("dploot").disabled = True
104+
logging.getLogger("aardwolf").disabled = True
105+
logging.getLogger("unicrypto").disabled = True
106+
logging.getLogger("asyncio").setLevel(logging.ERROR)
104107
logging.getLogger("neo4j").setLevel(logging.ERROR)
105108

106109
def format(self, msg, *args, **kwargs):

nxc/netexec.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ def main():
194194
proto_module_paths.append(modules[m]["path"])
195195
protocol_object.module_paths = proto_module_paths
196196

197+
if args.protocol == "rdp" and args.execute:
198+
ans = input(highlight("[!] Executing remote command via RDP will disconnect the Windows session (not log off) if the targeted user is connected via RDP, do you want to continue ? [Y/n] ", "red"))
199+
if ans.lower() not in ["y", "yes", ""]:
200+
exit(1)
201+
197202
if args.jitter and len(targets) > 1:
198203
nxc_logger.highlight(highlight("[!] Jitter is only throttling authentications per target!", "red"))
199204

nxc/protocols/rdp.py

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
from aardwolf.connection import RDPConnection
1616
from aardwolf.commons.queuedata.constants import VIDEO_FORMAT
17+
from aardwolf.commons.queuedata.keyboard import RDP_KEYBOARD_UNICODE
1718
from aardwolf.commons.iosettings import RDPIOSettings
1819
from aardwolf.commons.target import RDPTarget
20+
from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager
1921
from aardwolf.protocol.x224.constants import SUPP_PROTOCOLS
2022
from asyauth.common.credentials.ntlm import NTLMCredential
2123
from asyauth.common.credentials.kerberos import KerberosCredential
@@ -28,7 +30,6 @@ def __init__(self, args, db, host):
2830
self.domain = None
2931
self.server_os = None
3032
self.iosettings = RDPIOSettings()
31-
self.iosettings.channels = []
3233
self.iosettings.video_out_format = VIDEO_FORMAT.RAW
3334
self.iosettings.clipboard_use_pyperclip = False
3435
self.protoflags_nla = [
@@ -356,6 +357,207 @@ def hash_login(self, domain, username, ntlm_hash):
356357
)
357358
return False
358359

360+
async def _send_keystrokes(self, text, delay=0.02):
361+
"""Helper method to send keystrokes to the RDP session"""
362+
for char in text:
363+
key_event = RDP_KEYBOARD_UNICODE()
364+
key_event.char = char
365+
key_event.is_pressed = True
366+
await self.conn.ext_in_queue.put(key_event)
367+
await asyncio.sleep(delay)
368+
369+
async def _send_enter(self):
370+
"""Helper method to send Enter key to the RDP session"""
371+
await self.conn.send_key_virtualkey("VK_RETURN", True, False)
372+
await asyncio.sleep(0.05)
373+
await self.conn.send_key_virtualkey("VK_RETURN", False, False)
374+
375+
async def _send_win_r(self):
376+
"""Helper method to send Windows+R key combination to open Run dialog"""
377+
try:
378+
self.logger.debug("Sending Win+R using scancode method")
379+
380+
layout = KeyboardLayoutManager().get_layout_by_shortname("enus")
381+
382+
win_scancode = layout.vk_to_scancode("VK_LWIN")
383+
await self.conn.send_key_scancode(win_scancode, True, False)
384+
await asyncio.sleep(0.1)
385+
386+
r_scancode = layout.char_to_scancode("r")[0]
387+
await self.conn.send_key_scancode(r_scancode, True, False)
388+
await asyncio.sleep(0.1)
389+
390+
await self.conn.send_key_scancode(r_scancode, False, False)
391+
await asyncio.sleep(0.1)
392+
393+
await self.conn.send_key_scancode(win_scancode, False, False)
394+
395+
await asyncio.sleep(0.5)
396+
397+
self.logger.debug("Win+R sent successfully")
398+
return True
399+
except (ConnectionResetError, ConnectionError, OSError) as e:
400+
self.logger.debug(f"Connection error while waiting for clipboard: {e!s}")
401+
self.logger.fail("Connection was reset by the remote host")
402+
return False
403+
except Exception as e:
404+
self.logger.debug(f"Error sending Win+R: {e!s}")
405+
self.logger.debug("Using fallback approach for opening command prompt")
406+
return False
407+
408+
async def execute_shell(self, payload, get_output, shell_type):
409+
# Append | clip to send output to clipboard
410+
if shell_type == "cmd":
411+
payload_with_clip = f"{payload} | clip & exit"
412+
elif shell_type == "powershell":
413+
payload_with_clip = f"try {{ {payload} 2>&1 | clip}} catch {{ $_ | clip}}; exit"
414+
else:
415+
self.logger.fail(f"Unsupported shell type: {shell_type}")
416+
return None
417+
self.logger.debug(f"Executing command: {payload_with_clip}")
418+
419+
# Create a connection
420+
try:
421+
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
422+
await self.connect_rdp()
423+
except Exception as e:
424+
self.logger.debug(f"Error connecting to RDP: {e!s}")
425+
return None
426+
427+
try:
428+
if get_output:
429+
self.logger.success("Waiting for clipboard to be ready...")
430+
clipboard_ready = False
431+
await asyncio.sleep(self.args.cmd_delay)
432+
433+
timeout_counter = 0
434+
while not clipboard_ready and timeout_counter < (self.args.clipboard_delay * 10): # Convert seconds to deciseconds
435+
try:
436+
data = await asyncio.wait_for(self.conn.ext_out_queue.get(), timeout=0.1)
437+
if hasattr(data, "type") and data.type.name == "CLIPBOARD_READY":
438+
clipboard_ready = True
439+
self.logger.debug("Clipboard is ready!")
440+
break
441+
except asyncio.TimeoutError:
442+
timeout_counter += 1
443+
continue
444+
except (ConnectionResetError, ConnectionError, OSError) as e:
445+
self.logger.debug(f"Connection error while waiting for clipboard: {e!s}")
446+
self.logger.fail("Connection was reset by the remote host")
447+
return ""
448+
except Exception as e:
449+
self.logger.debug(f"Error waiting for clipboard: {e!s}")
450+
self.logger.fail("Warning: Clipboard may not be fully initialized, no output can be retrieved")
451+
return ""
452+
453+
if not clipboard_ready and get_output:
454+
self.logger.fail("Clipboard cannot be initialized, no output can be retrieved")
455+
return ""
456+
else:
457+
self.logger.success("Clipboard is ready, proceeding with command execution")
458+
459+
# Wait for desktop to be available
460+
await asyncio.sleep(self.args.cmd_delay)
461+
462+
try:
463+
# Try to open Run dialog using Windows+R
464+
self.logger.debug("Attempting to open Run dialog")
465+
win_r_success = await self._send_win_r()
466+
467+
if win_r_success:
468+
self.logger.debug(f"Launching {shell_type} via Run dialog")
469+
await self._send_keystrokes(f"{shell_type}.exe")
470+
await self._send_enter()
471+
await asyncio.sleep(self.args.cmd_delay) # Wait for cmd window to open
472+
else:
473+
# Fallback: Try direct command typing (assumes cmd may already be open)
474+
self.logger.debug(f"Sending {shell_type} command directly")
475+
await self._send_keystrokes(f"{shell_type}.exe")
476+
await self._send_enter()
477+
await asyncio.sleep(self.args.cmd_delay)
478+
479+
# Type the command with | clip
480+
self.logger.debug(f"Typing command: {payload_with_clip}")
481+
await self._send_keystrokes(payload_with_clip)
482+
await self._send_enter()
483+
484+
# Wait for command to execute
485+
await asyncio.sleep(self.args.cmd_delay)
486+
487+
if get_output:
488+
# Get the current clipboard text
489+
self.logger.debug("Getting clipboard content...")
490+
clipboard_text = await self.conn.get_current_clipboard_text()
491+
492+
if clipboard_text:
493+
self.logger.debug("Command output retrieved from clipboard:")
494+
for line in clipboard_text.lstrip().strip("\n").splitlines():
495+
self.logger.highlight(line)
496+
else:
497+
self.logger.fail("Clipboard is empty or contains non-text data")
498+
return clipboard_text
499+
500+
self.logger.debug("Command execution completed")
501+
return None
502+
503+
except (ConnectionResetError, ConnectionError, OSError) as e:
504+
self.logger.debug(f"Connection error during command execution: {e!s}")
505+
self.logger.fail("Connection was reset by the remote host during command execution")
506+
return None
507+
except Exception as e:
508+
self.logger.debug(f"Error during command execution: {e!s}")
509+
if "cannot unpack non-iterable NoneType object" in str(e):
510+
self.logger.fail("RDP connection was terminated unexpectedly")
511+
else:
512+
self.logger.fail(f"Command execution failed: {e!s}")
513+
return None
514+
515+
except (ConnectionResetError, ConnectionError, OSError) as e:
516+
self.logger.debug(f"Connection error: {e!s}")
517+
self.logger.fail("Connection was reset by the remote host")
518+
return None
519+
except Exception as e:
520+
self.logger.debug(f"Unexpected error: {e!s}")
521+
self.logger.fail(f"Command execution failed: {e!s}")
522+
return None
523+
finally:
524+
# Always clean up the connection
525+
if self.conn is not None:
526+
self.logger.debug("Terminating RDP connection")
527+
try:
528+
await self.conn.terminate()
529+
except Exception as e:
530+
self.logger.debug(f"Error terminating connection: {e!s}")
531+
532+
def execute(self, payload=None, shell_type="cmd"):
533+
"""Execute a command via RDP"""
534+
if not payload:
535+
payload = self.args.execute
536+
537+
get_output = bool(not self.args.no_output)
538+
539+
self.logger.success(f"Executing command: {payload} with delay {self.args.cmd_delay} seconds")
540+
541+
try:
542+
result = asyncio.run(self.execute_shell(payload, get_output, shell_type))
543+
544+
if result:
545+
self.logger.debug("Command execution completed")
546+
return result
547+
except Exception as e:
548+
self.logger.error(f"Command execution error: {e!s}")
549+
if shell_type == "cmd":
550+
self.logger.info("Cannot execute command via cmd - now switching to PowerShell to attempt execution")
551+
try:
552+
return self.execute(payload, shell_type="powershell")
553+
except Exception as e2:
554+
self.logger.fail(f"Execute command failed, error: {e2!s}")
555+
else:
556+
self.logger.fail(f"Execute command failed, error: {e!s}")
557+
558+
def ps_execute(self):
559+
self.execute(payload=self.args.ps_execute, shell_type="powershell")
560+
359561
async def screen(self):
360562
try:
361563
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)

nxc/protocols/rdp/proto_args.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,11 @@ def proto_args(parser, parents):
1717
egroup.add_argument("--screentime", type=int, default=10, help="Time to wait for desktop image")
1818
egroup.add_argument("--res", default="1024x768", help="Resolution in WIDTHxHEIGHT format")
1919

20+
cgroup = rdp_parser.add_argument_group("Command Execution", "Options for executing commands")
21+
cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command")
22+
cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
23+
cgroup.add_argument("--cmd-delay", type=int, default=5, help="Sleep time before executing command")
24+
cgroup.add_argument("--clipboard-delay", type=int, default=30, help="Maximum time to wait for clipboard initialization (seconds)")
25+
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
26+
2027
return parser

tests/e2e_commands.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M web_de
261261
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
262262
netexec {DNS} rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
263263
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --nla-screenshot
264+
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --screenshot
265+
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
266+
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
264267
##### SSH - Default test passwords and random key; switch these out if you want correct authentication
265268
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD
266269
netexec ssh TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce

0 commit comments

Comments
 (0)