0/ TL;DR

WinRM is protected against NTLMRrelay as communications are encrypted. However WinRMS (the one communicating over HTTPS) is not. That said, WinRMS is not configured on a default server installation (while WinRM is). That means that if someone tried to harden their servers’ configurations (removing HTTP endpoint), they would open a new possible target that can be used to relay HTTP/SMB and LDAP NTLM authentications to WinRMS and thus gain remote code execution.

I/ Stupidest idea becoming an actual thing

At the end of my talk at Insomni’hack 2025 about Browser Cache Smuggling, I said that “most of the time, huge innovations come from the stupidest and/or smallest idea”. What you are going to read is a perfect example of these words. Six months ago I was doing an internal assessment in which the client had a super strong security baseline. No known CVE’s exposed, no relay possibilities… Nothing that could be used. Except a missconfigured server that was administrator of another one (that’s a classic SCCM configuration). Thing is, even if you can trigger an authentication from a server to another one, it won’t be useful if:

  • SMB and LDAP are signed ;
  • LDAP channel binding is set ;
  • ESC8 is patched.

Turns out, all of these protections were set… Is it game over ? Well yes but no. A client may be secured against known exploits but what about those even us, hackerz, are not yet aware of ? That in my mind, I started looking for new potential exploits. Especially there are two things we deeply lake for internal AD assessments:

  • Ways of coercing a HTTP authentication (since these ones do not support signing/channel binding) ;
  • Endpoints to which we can coerce HTTP authentication since, most of the time they are not protected either!

And now let me introduce our new target:

WinRM!

II/ What’s WinRM

WinRM (Windows Remoting) is a specific protocole used to administrate Windows computer and servers through via an API exposed on a web server. WinRM comes with two versions:

  • Classic WinRM exposed on port 5985;
  • TLS encapsulated WinRM exposed on port 5986.

From our hacker point of view, WinRM looks like a promising target as it is exposed unencrypted. As such, it should be possible to relay SMB/LDAP/HTTP NTLM authentication to that endpoint. And it worked… Until realize that even if WinRM does expose itself on HTTP, its communications are actually encrypted:

Sucks right ? Sucks even more when I realized that encryption is done via a key derived from the user’s password… Naively I thought that WinRMS would be protected itself but as it turns, data are not encrypted inside the TLS tunnel. I mean, why would they ? TLS is already encrypted stuff. Last thing I needed to know is whether channel binding is enabled on WinRMS. People said yes but ultimately I asked the question on Twitter and @filip_dragovic linked that screenshot:

That’s actually quite interesting because the paper says that relay to WinRMS should not work, even thought it also says that Channel Binding (CBT) is set to “Relaxed” in the WinRM configuration. However, Channel Binding, which is used to protect a NTLM authentication binding it to a TLS session, can be configured following 3 values:

  • None: Channel Binding is not supported (that’s the default configuration for ADCS HTTPS web enrolment) ;
  • Strict: Channel Binding is required. If not sent by the client, the server must NOT process the request ;
  • Relaxed: Channel Binding is optionnal. If the client sends it then the server protects the communication otherwise it does not.

Sounds interesting right ? :D

III/ Weaponizing the relay

Having that information got me into thinking that there is something we can do here but I forgot about it until @EAGAIIN asked for any new information. Which ultimately led me into getting back to that subject. All I had to do was to create a new relay server/client for WinRM on NTLMRelayX which I started doing… Until I found foofus’s hidden github repo which already provided most of the things I needed! So I’m not going deep dive into the internals of NTLMRelayX because most of it is really all about catching the NTLM authentication and forwarding it to another target. However, let’s just check how the WinRM protocol looks like.

First of all, WinRM is a protocol that relies on, mostly, four XML files. The first one is used to initiate a “ShellId” that will be used in all our futur calls:

<?xml version="1.0" encoding="utf-8"?>
<env:Envelope
    xmlns:env="http://www.w3.org/2003/05/soap-envelope"
    xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
    xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
    xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd"
    xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell">

    <env:Header>
        <a:To>http://windows-host:5985/wsman</a:To>
        <a:ReplyTo>
            <a:Address mustUnderstand="true">
                http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
            </a:Address>
        </a:ReplyTo>
        <a:MessageID>uuid:2a8ac24f-00f0-4a87-860c-bf58d33a1e0a</a:MessageID>
        <a:Action mustUnderstand="true">
            http://schemas.xmlsoap.org/ws/2004/09/transfer/Create
        </a:Action>
        <w:ResourceURI mustUnderstand="true">
            http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd
        </w:ResourceURI>
        <w:OperationTimeout>PT20S</w:OperationTimeout>
        <w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize>
        <w:OptionSet>
            <w:Option Name="WINRS_NOPROFILE">FALSE</w:Option>
            <w:Option Name="WINRS_CODEPAGE">437</w:Option>
        </w:OptionSet>
        <w:Locale xml:lang="en-US"/>
        <p:DataLocale xml:lang="en-US"/>
    </env:Header>

    <env:Body>
        <rsp:Shell>
            <rsp:InputStreams>stdin</rsp:InputStreams>
            <rsp:OutputStreams>stdout stderr</rsp:OutputStreams>
        </rsp:Shell>
    </env:Body>
</env:Envelope>

Nothing interesting in that packet to me. The only thing to remember is that sending that XML content will end up in the server sending you back a ShellID (an UUID). Now that you have that ID, you can send another XML file in which you will provide the command you want to launch:

<?xml version="1.0" encoding="utf-8"?>
<env:Envelope
    xmlns:env="http://www.w3.org/2003/05/soap-envelope"
    xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
    xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
    xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell">

    <env:Header>
        <a:To>http://windows-host:5985/wsman</a:To>
        <a:ReplyTo>
            <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
        </a:ReplyTo>
        <a:Action mustUnderstand="true">
            http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command
        </a:Action>
        <a:MessageID>uuid:10000000-0000-0000-0000-000000000002</a:MessageID>
        <w:ResourceURI mustUnderstand="true">
            http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd
        </w:ResourceURI>
        <w:SelectorSet>
            <w:Selector Name="ShellId">SHELLID</w:Selector>
        </w:SelectorSet>
    </env:Header>

    <env:Body>
        <rsp:CommandLine>
            <rsp:Command>whoami</rsp:Command>
        </rsp:CommandLine>
    </env:Body>
    </env:Envelope>

The server will send a command id that will then allow you asking for where you want to get the result of that commando with the following XML request:

<?xml version="1.0" encoding="utf-8"?>
<env:Envelope
    xmlns:env="http://www.w3.org/2003/05/soap-envelope"
    xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
    xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
    xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell">

    <env:Header>
        <a:To>http://windows-host:5985/wsman</a:To>
        <a:ReplyTo>
            <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
        </a:ReplyTo>
        <a:Action mustUnderstand="true">
            http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive
        </a:Action>
        <a:MessageID>uuid:2a8ac24f-00f0-4a87-860c-bf58d33a1e0a</a:MessageID>
        <w:ResourceURI mustUnderstand="true">
            http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd
        </w:ResourceURI>
        <w:SelectorSet>
            <w:Selector Name="ShellId">SHELLID</w:Selector>
        </w:SelectorSet>
    </env:Header>

    <env:Body>
        <rsp:Receive>
            <rsp:DesiredStream CommandId="COMMANDID">stdout stderr</rsp:DesiredStream>
        </rsp:Receive>
    </env:Body>
</env:Envelope>

And then you’ll get the result of the command:

At this point we are able to relay a NTLM authentication and un commands on the remote target via the XML files. Thing is, when relaying authentications, you are not always sure of who’s you are going to be able to relay. As such, it was not possible to create a default action when relaying a target to WinRM. As such, I decided the default behaviour is to create an interactive shell which we will allow you running commands remotely depending of what you’d want to do. Below is the demo of the final tool:

IV/ Caveat and blueteaming

Altought this relay technique is interesting, it’s important to mention that WinRMS is not running by default, even when enabling the remote desktop functionnalities. To enable the WinRMS service, the following commands will have to be launched:

# Generates a self signed TLS certificate (you can also use your certificate service)
New-SelfSignedCertificate -Subject 'CN=server_name.domain.com' -TextExtension '2.5.29.37={text}1.3.6.1.5.5.7.3.1'

# Creates a new HTTPS listener that uses the certificate created before
winrm create winrm/config/Listener?Address=*+Transport=HTTPS '@{Hostname="ServerB.domain.com"; CertificateThumbprint="<cert thumbprint here>"}'

# Opening port 5986 on the internal firewall
$FirewallParam = @{
    DisplayName = 'WinRM HTTPS'
    Direction = 'Inbound'
    LocalPort = 5986
    Protocol = 'TCP'
    Action = 'Allow'
    Program = 'System'
}
New-NetFirewallRule @FirewallParam

But as I mentionned before, Channel Binding is not enforced by default on the HTTPS listener as the following command shows:

winrm get winrm/config/service/auth

Which is quite ironic considering that a sysadmin trying to harden its domain, removing HTTP endpoints, will actually activate a protocol that is not secured by default and will allows us relaying NTLM authentication to it… So yeah more securite actually bring less security (which kind of reminds me of the “restrictedAdmin” thing)…

Hopefully, fixing that is quite easy, all you’ll have to do is to enable Channel Binding over the HTTP listener using the following PowerShell command:

winrm set winrm/config/service/auth '@{CbtHardeningLevel="Strict"}'

And relaying won’t be doable anymore:

Before closing that blogpost, I wanted to thank Joe, Foufos, who developed the basement for that NTLMRelayx’s plugin, Github is really full of very interesting researches, tools and peoples :P! The PR is already published on the impacket repo :)