How To Detect Remote Desktop Protocol tunneling over SSH

10 min readAug 3, 2022

During some testing on my Windows VPS, I was checking out the OpenSSH server feature in Windows. This reminded me about the times our Red Team tunneled Remote Desktop Protocol (RDP) network traffic through an encrypted Secure Shell (SSH)-session, when pivoting in our customer’s network. Hiding your RDP traffic in SSH sessions can be beneficial when trying to evade the Blue Team. Especially if the RDP session was initiated from a Linux host, since in most networks it’s not standard practice to use RDP on Linux.

Because of this flashback, I thought of a Use Case to see if I could detect this behavior. How I setup the OpenSSH server on Windows can be found later in this blog, for those interested. First, I’ll show what I did to detect the Use Case. Please be careful if you want to use the queries in this blog; they give the results I need quick and dirty, but they’re hardly fine-tuned or efficient.

Anyway, let’s get started, shall we!

Detecting RDP over SSH on a network level — There was an attempt…

To see if we can detect this Use Case, we’ll turn to Host-Based logging. Why, do you ask? Well, let’s look at the following snippet from two pcaps:

The left part of the picture is SSH traffic and right is RDP traffic. Both streams are encrypted traffic, with very little discernable traits. Of course, most (corporate) firewalls and routers will recognize both types of traffic, but the point is that we can’t see what is going on due to the encryption. Therefore, instead of putting a lot of effort into analyzing network traffic to see if we can make chocolate from it (Dutch expression), we’re turning to our trusty Windows Event Logs.

Detecting RDP over SSH on a host level

So, what happens on the remote host when we tunnel RDP over SSH:

• Successful logon over SSH on, in my case, port 31337 (port 22 in the standard config);

• Traffic being forwarded from localhost to port 3389 (Default RDP port);

• A “normal” successful windows logon attempt.

Important: for this blog the Use Case is a successful logon on both SSH and RDP. Meaning a successful SSH logon followed by a failed logon is something I haven’t checked yet, same goes for logons with SSH keys (probably the same, but with some extra logs about accepting a SSH key). Maybe I’ll make this a two-parter and test those Use Cases later.

What I did was log in with the user user for SSH and Administrator for Windows and check the logs for what happened around that time. I used the following Sentinel query:

| where TimeGenerated > ago(2h)
| where (EventID == “4624” and AccountType == “User” and TargetDomainName != “VIRTUAL USERS” and Process == “sshd.exe”) or (EventID ==”4624" and IpAddress == “”)
| project TimeGenerated, TargetUserName, Process, LogonTypeName, IpAddress,Activity

For those of you unknown with the Kusto Query Language (KQL) used by Sentinel, here’s a breakdown per line:

• Look for events in the SecurityEvent table;

• Set the search period between now and 2 hours ago to display more events;

• Check for EventID 4624 (successful Logon) from a User (No machine account), filtering out Virtual Users (Managed Local Accounts) and where the process is sshd.exe;

• Or we check for EventID 4624 coming from localhost;

• We then project (display) a couple of columns.

If you see this chain of events in a small timeframe, for example 1 minute, it might indicate someone tunneling RDP over SSH. Again, this query is for testing purposes, I have not yet used it in a production environment. So, if you use it, use it as a basis and finetune it further. When we execute the query, it looks like this:

Here, you see three distinct sessions with four events each, where the events happen in a couple of seconds from each other. if you want to verify that the Administrator logons are indeed from RDP, you can check the RDP logs:

The image shows the Administrator account logging in to RDP from localhost, during the timeframe we saw in the previous search. Will this 100% be malicious? Probably not. Is this reason for concern? I would say yes, since this behavior is uncommon in a network.

Logging requirements and setup

For this, at the minimum you need the Windows Security Logs, but for confirmation and investigation you can also enable OpenSSH/Operational under the Application and Services and the Microsoft-Windows-TerminalServices-LocalSessionsManager/Operational logs, which should be under Server Role/Remote Desktop Services. In my setup I’ve enabled basically every log and I’m also using SwiftOnSecurity’s Sysmon config with some minor tweaks. I have added my Server to Azure Arc for easy management and am using the Microsoft Azure Monitor-Agent to send my logs to Sentinel for analysis.

Further investigating the Use Case

So, what if you find this in your logs? I would definitely check the logs for other suspicious behaviour on this host. Same goes for the originating machine(s). The source address should be in the Defender Firewall logs or in your network logs. Investigate if you see more aberrant network connections and check your (sys)logs for signs of compromise. For example, unusual cron jobs or processes. Try avoiding doing this on the machine itself, unless you have no other option, as to not alert a possible attacker.

Caveats and ideas

These are just the first results after a couple of tests. As I said, there are several things I haven’t researched yet:

• Failed logons. A failed SSH logon won’t result in a RDP session, so you can’t correlate it the same way as I did with a successful logon;

• SSH Key Authentication;

• Successful SSH logon followed by a failed Windows logon. Although the only thing that would probably change is EventID 4624 to 4625 in the second part of the query.

There are also some other ideas I have. For example, what if the target machine has MFA enabled (like DUO Security)? Since the logon via SSH is basically still a Windows login, you should get prompted for MFA. But does it?

Also, can we leverage remote Powershell or WMI to remotely enable and configure the SSH Server? And remember, just about everything we do here needs valid (elevated) credentials and if this is possible, you’re probably already compromised.

For the Red Team this might be a nice thing to have in the toolbox though, SSH tunnels are a staple already anyway. It’s a niche Use Case that many Blue Teams probably don’t hunt for and SSH coming from your hacked linux machine to your target will probably stand out less than RDP traffic. There’s still some work to be done, but I think at the very least we have a nice starting point to create some detection rules and hunting queries based on this Use Case.

Have any remarks or ideas? Find me on Twitter or LinkedIn (DM that you’ve found me through this blog, I’m pretty picky with accepting requests).

Installing SSH on Windows

This and more can also be found on the Microsoft website. Start by opening an elevated Powershell and use these commands to install the client and server:

# Install the OpenSSH Client
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~
# Install the OpenSSH Server
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~
After installing, start the service with:# Start the sshd service
Start-Service sshd
# OPTIONAL but recommended:
Set-Service -Name sshd -StartupType ‘Automatic’

By default, after starting the sshd service, a firewall rule should be added to allow SSH traffic. You can check this with the following command (this will also create the rule if it doesn’t exist):

# Confirm the Firewall rule is configured. It should be created automatically by setup. Run the following to verifyif (!(Get-NetFirewallRule -Name “OpenSSH-Server-In-TCP” -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
Write-Output “Firewall Rule ‘OpenSSH-Server-In-TCP’ does not exist, creating it…”
New-NetFirewallRule -Name ‘OpenSSH-Server-In-TCP’ -DisplayName ‘OpenSSH Server (sshd)’ -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
} else {
Write-Output “Firewall rule ‘OpenSSH-Server-In-TCP’ has been created and exists.”

Alternatively, you can check this by opening the advanced settings from the Windows Firewall Control Panel and then looking for the rule in the Inbound Rules.

If everything works, you should now be able to SSH into your machine using your preferred method, like: ssh user@targetmachine.

Changing the default SSH port

If your machine is directly connected to the internet, you absolutely should change the default port. To do this we need to edit the sshd_config in %programdata%\ssh\. If this directory is empty, start the sshd, because sshd generates the needed configs and files upon first run (troubleshooting this cost me 30 minutes of my life I’ll never get back). I’m not getting into details about the entire config, because there’s excellent documentation on GitHub. For the purpose of this test, I’m not doing anything with SSH keys, I’ll focus on just password authentication for now.

All we need to do, is open sshd_config with for example Notepad++ and look for this:

Port 22

Change this to a different and higher port like 31337. Make sure you restart sshd in Powershell with Restart-Service -Name sshd. You can check if it worked by typing netstat -ano | findstr “31337” and see if sshd is listening on this port. It should look something like this:

PS C:\Users\Administrator> netstat -ano | findstr “31337”
TCP [::]:31337 [::]:0 LISTENING 4844

Adding a Rule to Windows Defender Firewall

Since we changed the port, we also need to change the accompanying Firewall rule. In the Windows menubar search for and open Firewall and then open advanced settings on the left. This will open the rulebase, where you need to look for the existing OpenSSH rule. When you open it, it should look something like this:

This is where we run into a small problem, because as you can see, we’re mostly not allowed to change this rule and that includes the default port. This means we need to add our own rule and disable the original. We can do this via the GUI or use part of the Powershell command we already used earlier. I’m going to show the latter:

New-NetFirewallRule -Name “OpenSSH-Server-In-TCP (port 31337)” -DisplayName “OpenSSH Server (sshd — port 31337)” -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 31337

Make sure to change the LocalPort, Name and DisplayName variables to something appropriate. Don’t forget to disable the default rule! Now, use your favorite program and open a SSH session: ssh -p 31337 user@yourhost. If you can’t connect, make sure that sshd is listening on the correct socket. In case it still doesn’t work, turn the firewall off and on, effectively reloading the rules. Be mindful when and where you do this because it might lock you out of the machine.

Tunneling RDP over SSH.

If everything is working, we can create our tunnel. This is a simple command. On your host, start an SSH session like so:

ssh -L 33389: -p 31337 user@remote_windows_host

Let’s break down the command:

• ssh -L: use the ssh client and setup a local port forward.

• 33389: The listening port on our own machine.

• The IP address of our own machine, a.k.a. Localhost. Be careful when binding to a different address, as others might use it.

• 3389: The port to forward to on our target machine.

• -p 31337: Connect to SSH on this port.

• user@remote_windows_host: The user and host to logon to.

If everything is working you should get a command prompt (CMD), or a Powershell prompt (you can change this on the Windows machine, just read the docs):

donald$ ssh -L 33389: -p 11337 user@thehost
user@thehost’s password:
Windows PowerShell
Copyright © Microsoft Corporation. All rights reserved.
PS C:\Users\user>

Now use your desired program to start a RDP session, for instance Microsoft Remote Desktop on the Mac like me. When creating the session, instead of connecting to the remote host enter: localhost:33389. Here’s how it looks on my Mac:

Start the session and logon with your credentials, and Bob’s your uncle! We’re now tunneling our RDP session over SSH! Now you can disable remote RDP logons or block RDP on the firewall all together.

Added Benefit: Getting Rid Of Automated Brute Force Attacks On The Default Port

What was fun to see, is that changing the default port of course got rid of automated brute force attempts. This was before the change:

This is the result in 24 hours after I made the changes:

As you can see, there were only 6 failed logons and they were all mine. Sidenote: NOUSER is the username when someone tries to logon through SSH with a nonexistent user.

Now, of course this is not real security measure by any means, although changing the default port on internet facing machines will help with automated scanning as you can see. This won’t stop an actual person from finding and scanning the actual open port, but the increased scanning may help detecting recon attempts.