fortra/impacket

HTTP relay server fails to negotiate NTLM authentication for keep-alive requests

itm4n opened this issue · 0 comments

Configuration

impacket version: 0.12.0.dev1
Python version: 3.11
Target OS: Debian 12

Debug Output With Command String

$ ntlmrelayx -t 'ldap://10.10.10.10' -i --http-port 8000 --no-smb-server
Impacket v0.12.0.dev1 - Copyright 2023 Fortra
[...]
[*] Servers started, waiting for connections
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123
[*] HTTPD(8000): Client requested path: /foo123

PCAP

impacket-http-server.zip

Additional context

My objective was to relay the NTLM authentication of a WSUS client to a DC over LDAP, but it didn't work. I inspected the traffic with Wireshark, and realized that the WSUS client did respond to the NTLM authentication request, but the HTTP relay server seemed to ignore it.

The following code can be used to reproduce the issue.

import requests

with requests.Session() as s:
    url =  "http://localhost:8000/foo123"
    response = s.post(url, data="SOME DATA")
    print(str(response.status_code))

    if response.status_code == 401 and response.headers.get('WWW-Authenticate') is not None:
        s.headers.update({'Authorization': 'NTLM plop'})
        response = s.post(url, data=None)
        print(str(response.status_code))

After adding some debug messages, I observed the following behavior. When the first request is received, the handler do_POST is called (as expected), but then the handler do_GET seems to be called in a loop on the first request, instead of being called only once.

$ ntlmrelayx -t 'ldap://10.10.10.10' -i --http-port 8000 --no-smb-server -debug
Impacket v0.12.0.dev1 - Copyright 2023 Fortra
[...]
[+] do_POST
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_GET

I compared the traffic with another relay that worked with the same setup, and found that the above behavior occurs when the client reuses the same connection ("keep-alive" request), instead of initiating a new TCP session to negotiate the authentication.

Although I wasn't able to pinpoint the exact issue, I came up with the following workaround. When sending the first response with the header WWW-Authenticate: NTLM, I set the header Connection: close to coerce the client to create a new TCP session.

Note that this solution might not be suited for the "Proxy" mode, where the connection probably needs to be maintained.

def do_AUTHHEAD(self, message = b'', proxy=False):
    if proxy:
        self.send_response(407)
        self.send_header('Proxy-Authenticate', message.decode('utf-8'))
    else:
        self.send_response(401)
        self.send_header('WWW-Authenticate', message.decode('utf-8'))
    self.send_header('Content-type', 'text/html')
    self.send_header('Content-Length','0')
    # === Workaround ===
    if message.decode('utf-8') == 'NTLM':
       self.send_header('Connection', 'close')
    else:
       self.send_header('Connection', 'keep-alive')
    # === Workaround ===
    self.end_headers()

After applying this change, the HTTP relay server works as intended, and actually tries to negotiate the NTLM authentication.

[*] Servers started, waiting for connections
[+] do_POST
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] do_POST
[+] do_GET
[*] HTTPD(8000): Client requested path: /foo123
[+] HTTPD(8000): Exception:
[...] <- Exception stripped (because of invalid NTLM blob)
[-] HTTPD(8000): Exception in HTTP request handler: unpack requires a buffer of 4 bytes