1414
1515from aardwolf .connection import RDPConnection
1616from aardwolf .commons .queuedata .constants import VIDEO_FORMAT
17+ from aardwolf .commons .queuedata .keyboard import RDP_KEYBOARD_UNICODE
1718from aardwolf .commons .iosettings import RDPIOSettings
1819from aardwolf .commons .target import RDPTarget
20+ from aardwolf .keyboard .layoutmanager import KeyboardLayoutManager
1921from aardwolf .protocol .x224 .constants import SUPP_PROTOCOLS
2022from asyauth .common .credentials .ntlm import NTLMCredential
2123from 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 )
0 commit comments