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
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