diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index f1f8ce92c..89931c86c 100644 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -220,6 +220,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') @@ -373,6 +378,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 0753eb6d8..1e1061ae9 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 @@ -37,18 +38,73 @@ class HTTPRelayServer(Thread): + class HTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): def __init__(self, server_address, RequestHandlerClass, config): self.config = config self.daemon_threads = True + self.address_family, server_address = get_address(server_address[0], server_address[1], self.config.ipv6) + # Tracks the number of times authentication was prompted for WPAD per client self.wpad_counters = {} + socketserver.TCPServer.allow_reuse_address = True + 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 @@ -67,10 +123,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: @@ -78,16 +134,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) @@ -109,7 +165,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 @@ -137,6 +193,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') @@ -149,7 +210,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: @@ -161,10 +222,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('