From 570d173747037b8c74176f93a75d0662ee92f714 Mon Sep 17 00:00:00 2001 From: Coontzy1 Date: Wed, 3 Sep 2025 22:52:26 -0400 Subject: [PATCH 1/2] httpRelayServer: restore POST body handling and add HTTPS/TLS support This commit makes two improvements to httprelayserver.py: 1. Restore POST body draining logic (#913): - Re-applies fix originally merged in 2021 by Rcarnus. - Ensures POST request bodies are consumed before returning 401 Unauthorized. - Fixes WSUS and other web clients that retry authentication on the same TCP stream. 2. Add HTTPS/TLS support with improved logging: - Introduce optional SSL context when --https, --certfile, and --keyfile are provided. - Wrap inbound sockets with TLS, logging negotiated protocol/cipher on success. - On failures, log SSL error details, flagging early EOFs (likely client cert rejection). - Add startup banner showing port, IPv6/HTTPS status for clarity. Together these changes allow ntlmrelayx to: - Correctly handle WSUS POST-based authentication flows. - Relay over HTTPS endpoints (e.g., WSUS 8531) with real certs or self-signed. - Provide more useful debug information for operators. --- examples/ntlmrelayx.py | 11 ++ .../ntlmrelayx/servers/httprelayserver.py | 181 +++++++++++++----- 2 files changed, 140 insertions(+), 52 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 66524aa528..4eabe52b49 100644 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -219,6 +219,11 @@ def start_servers(options, threads): c.setAltName(options.altname) + #https optioons + c.https = options.https + c.certfile = options.certfile + c.keyfile = options.keyfile + #If the redirect option is set, configure the HTTP server to redirect targets to SMB if server is HTTPRelayServer and options.r is not None: c.setMode('REDIRECT') @@ -371,6 +376,12 @@ def stop_servers(threads): httpoptions.add_argument('-domain', action="store", help='Domain FQDN or IP to connect using NETLOGON') httpoptions.add_argument('-remove-target', action='store_true', default=False, help='Try to remove the target in the challenge message (in case CVE-2019-1019 patch is not installed)') + httpoptions.add_argument('--https', action='store_true', + help='Enable TLS (HTTPS) on the HTTP relay server') + httpoptions.add_argument('--certfile', action='store', metavar='FILE', + help='Path to server certificate (PEM format) for HTTPS') + httpoptions.add_argument('--keyfile', action='store', metavar='FILE', + help='Path to private key (PEM format) for HTTPS') #LDAP options ldapoptions = parser.add_argument_group("LDAP client options") diff --git a/impacket/examples/ntlmrelayx/servers/httprelayserver.py b/impacket/examples/ntlmrelayx/servers/httprelayserver.py index 018d935ff6..c9dfb32c6d 100644 --- a/impacket/examples/ntlmrelayx/servers/httprelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/httprelayserver.py @@ -1,6 +1,6 @@ # Impacket - Collection of Python classes for working with network protocols. # -# Copyright Fortra, LLC and its affiliated companies +# Copyright Fortra, LLC and its affiliated companies # # All rights reserved. # @@ -23,6 +23,7 @@ import socket import base64 import random +import ssl import struct import string from threading import Thread @@ -44,10 +45,59 @@ def __init__(self, server_address, RequestHandlerClass, config): self.address_family = socket.AF_INET6 # Tracks the number of times authentication was prompted for WPAD per client self.wpad_counters = {} - socketserver.TCPServer.__init__(self,server_address, RequestHandlerClass) + + socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass) + + # Startup banner with port + HTTPS flag + try: + LOG.info("HTTPD(%s): Listening on %s:%s (IPv6=%s, HTTPS=%s)" % ( + self.server_address[1], + self.server_address[0], + self.server_address[1], + self.config.ipv6, + getattr(self.config, "https", False) + )) + except Exception: + # Fail-safe in case server_address isn't fully populated yet + LOG.info("HTTPD(?): Listening (IPv6=%s, HTTPS=%s)" % ( + self.config.ipv6, + getattr(self.config, "https", False) + )) + + # If HTTPS is enabled, prepare SSL context + if getattr(self.config, "https", False): + self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + self.context.load_cert_chain( + certfile=getattr(self.config, "certfile", None), + keyfile=getattr(self.config, "keyfile", None) + ) + def get_request(self): + sock, addr = socketserver.TCPServer.get_request(self) + if getattr(self.config, "https", False): + try: + ssock = self.context.wrap_socket(sock, server_side=True) + LOG.debug("HTTPD(%s): TLS handshake from %s:%s succeeded (protocol=%s, cipher=%s)", + self.server_address[1], addr[0], addr[1], + ssock.version(), ssock.cipher()) + return ssock, addr + except ssl.SSLError as e: + if "EOF" in str(e): + LOG.warning("HTTPD(%s): TLS handshake from %s:%s aborted early (likely client rejected cert)", + self.server_address[1], addr[0], addr[1]) + else: + LOG.error("HTTPD(%s): TLS handshake from %s:%s failed: %s", + self.server_address[1], addr[0], addr[1], e) + sock.close() + raise + except Exception as e: + LOG.error("HTTPD(%s): TLS handshake from %s:%s failed (generic error: %s)", + self.server_address[1], addr[0], addr[1], e) + sock.close() + raise + return sock, addr class HTTPHandler(http.server.SimpleHTTPRequestHandler): - def __init__(self,request, client_address, server): + def __init__(self, request, client_address, server): self.server = server self.protocol_version = 'HTTP/1.1' self.challengeMessage = None @@ -66,10 +116,10 @@ def __init__(self,request, client_address, server): # Reflection mode, defaults to SMB at the target, for now self.server.config.target = TargetsProcessor(singleTarget='SMB://%s:445/' % client_address[0]) try: - http.server.SimpleHTTPRequestHandler.__init__(self,request, client_address, server) + http.server.SimpleHTTPRequestHandler.__init__(self, request, client_address, server) except Exception as e: - LOG.debug("(HTTP): Exception:", exc_info=True) - LOG.error("(HTTP): %s" % str(e)) + LOG.debug("HTTPD(%s): Exception:", self.server.server_address[1], exc_info=True) + LOG.error("HTTPD(%s): %s" % (self.server.server_address[1], str(e))) def handle_one_request(self): try: @@ -77,16 +127,16 @@ def handle_one_request(self): except KeyboardInterrupt: raise except Exception as e: - LOG.debug("(HTTP): Exception:", exc_info=True) - LOG.error('(HTTP): Exception in HTTP request handler: %s' % e) + LOG.debug("HTTPD(%s): Exception:", self.server.server_address[1], exc_info=True) + LOG.error('HTTPD(%s): Exception in HTTP request handler: %s' % (self.server.server_address[1], e)) def log_message(self, format, *args): return def send_error(self, code, message=None): - if message.find('RPC_OUT') >=0 or message.find('RPC_IN'): + if message and (message.find('RPC_OUT') >= 0 or message.find('RPC_IN') >= 0): return self.do_GET() - return http.server.SimpleHTTPRequestHandler.send_error(self,code,message) + return http.server.SimpleHTTPRequestHandler.send_error(self, code, message) def send_not_found(self): self.send_response(404) @@ -108,7 +158,7 @@ def serve_wpad(self): wpadResponse = self.wpad % (self.server.config.wpad_host, self.server.config.wpad_host) self.send_response(200) self.send_header('Content-type', 'application/x-ns-proxy-autoconfig') - self.send_header('Content-Length',len(wpadResponse)) + self.send_header('Content-Length', len(wpadResponse)) self.end_headers() self.wfile.write(b(wpadResponse)) return @@ -148,7 +198,7 @@ def strip_blob(self, proxy): autorizationHeader = self.headers.get('Authorization') if (proxy and proxyAuthHeader is None) or (not proxy and autorizationHeader is None): - self.do_AUTHHEAD(message = b'NTLM',proxy=proxy) + self.do_AUTHHEAD(message=b'NTLM', proxy=proxy) messageType = 0 token = None else: @@ -160,10 +210,10 @@ def strip_blob(self, proxy): _, blob = typeX.split('NTLM') token = base64.b64decode(blob.strip()) except Exception: - LOG.debug("(HTTP): Exception:", exc_info=True) - self.do_AUTHHEAD(message = b'NTLM', proxy=proxy) + LOG.debug("HTTPD(%s): Exception:", self.server.server_address[1], exc_info=True) + self.do_AUTHHEAD(message=b'NTLM', proxy=proxy) else: - messageType = struct.unpack(' Date: Fri, 5 Sep 2025 23:36:30 -0400 Subject: [PATCH 2/2] Added comment explaining part of code --- impacket/examples/ntlmrelayx/servers/httprelayserver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/impacket/examples/ntlmrelayx/servers/httprelayserver.py b/impacket/examples/ntlmrelayx/servers/httprelayserver.py index c9dfb32c6d..8ac63bb0ee 100644 --- a/impacket/examples/ntlmrelayx/servers/httprelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/httprelayserver.py @@ -186,6 +186,11 @@ def serve_image(self): self.wfile.write(imgFile_data) def strip_blob(self, proxy): + # Get the body of the request if any + # Otherwise, successive requests will not beb handled properly + # Was added in July 29, 2020 branch e59ff69 and removed during + # restructuring March 30, 2022 branch a168273. Needed for + # relaying the request during WSUS relay attacks if PY2: if proxy: proxyAuthHeader = self.headers.getheader('Proxy-Authorization')