-
Notifications
You must be signed in to change notification settings - Fork 90
Add NetBIOS name resolution fallback for session_request #296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Z6543
wants to merge
21
commits into
rapid7:master
Choose a base branch
from
Z6543:netbios-name-resolution-fallback
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
d49ec54
Make NetBIOS lookup socket creation pluggable
Z6543 facda14
Widen NetBIOS auto-lookup gate to any rejected called name
Z6543 db69cd8
Drop nmblookup shell-out from NetBIOS name lookup
Z6543 150c57a
Only auto-discover NetBIOS name when the caller used the default
Z6543 9b83361
Add pure-Ruby NBNS node-status helper (nmblookup -A equivalent)
Z6543 3d335fe
Use sendto when the UDP socket provides it
Z6543 ef7e0a3
Use native recv-with-timeout on Rex::Socket::Udp
Z6543 47a1bd2
Bind NBNS socket to local port 137 so Win9x replies are deliverable
Z6543 091893e
Skip 2-arg bind() on Rex::Socket::Udp
Z6543 c1a8ca3
Revert: don't force-bind NBNS socket to local port 137
Z6543 075c157
Re-add local bind to UDP/137 for Win9x NBNS replies
Z6543 ef3824c
Restore NodeStatusRequest/Response BinData structs and specs
Z6543 728a17c
Add raw-socket fallback for Win9x NBNS replies
Z6543 679da4d
Fall back to raw socket in NBNS auto-discovery
Z6543 fef4173
Simplify socket handling by just accepting a socket
smcintyre-r7 0e99467
Drop rex-socket UDP API workarounds
smcintyre-r7 2cabcd7
Let the caller own NetBIOS name-resolution reconnect
smcintyre-r7 18649b3
Merge pull request #3 from smcintyre-r7/pr/collab/296
Z6543 7e81f53
Define NBNS flag fields
smcintyre-r7 1e61448
Merge pull request #4 from smcintyre-r7/pr/collab/296-nbns-bitfields
Z6543 fa57a06
Harden NBNS Node Status query and NBSS error handling
Z6543 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| module RubySMB | ||
| module Nbss | ||
| # NetBIOS Name Service header flags, as defined in RFC 1002 section 4.2.1.1. | ||
| class NameServiceHeaderFlags < BinData::Record | ||
| endian :big | ||
|
|
||
| bit1 :response, label: 'Response', initial_value: 0 | ||
| bit4 :opcode, label: 'Opcode', initial_value: 0 | ||
| bit1 :authoritative_answer, label: 'Authoritative Answer', initial_value: 0 | ||
| bit1 :truncated, label: 'Truncated', initial_value: 0 | ||
| bit1 :recursion_desired, label: 'Recursion Desired', initial_value: 0 | ||
| bit1 :recursion_available, label: 'Recursion Available', initial_value: 0 | ||
| bit2 :reserved, label: 'Reserved', initial_value: 0 | ||
| bit1 :broadcast, label: 'Broadcast', initial_value: 0 | ||
| bit4 :rcode, label: 'Result Code', initial_value: 0 | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| require 'socket' | ||
|
|
||
| module RubySMB | ||
| module Nbss | ||
| # Pure-Ruby implementation of `nmblookup -A <ip>`: sends an NBNS Node | ||
| # Status Request (RFC 1002 4.2.17) over UDP/137 and returns the | ||
| # server's name table. | ||
| # | ||
| # No external binaries are invoked. Compare to Samba's `nmblookup`, | ||
| # which shells out and requires the `samba-common-bin` package to be | ||
| # installed. | ||
| module NodeStatus | ||
| NBNS_PORT = 137 | ||
|
|
||
| # Default per-attempt receive timeout, in seconds. | ||
| DEFAULT_TIMEOUT = 2.0 | ||
|
|
||
| # Default number of attempts before giving up. | ||
| DEFAULT_RETRIES = 3 | ||
|
|
||
| # One entry in the returned name table. | ||
| # | ||
| # @!attribute [r] name [String] the NetBIOS name (trimmed) | ||
| # @!attribute [r] suffix [Integer] 1-byte NetBIOS suffix | ||
| # @!attribute [r] group [Boolean] true for a group name, false for unique | ||
| # @!attribute [r] active [Boolean] true if the name is registered | ||
| Entry = Struct.new(:name, :suffix, :group, :active) do | ||
| def unique? | ||
| !group | ||
| end | ||
|
|
||
| # Human-readable form like `WIN95 <20> UNIQUE ACTIVE`. | ||
| def to_s | ||
| flags = [group ? 'GROUP' : 'UNIQUE', active ? 'ACTIVE' : 'INACTIVE'].join(' ') | ||
| format('%-16s <%02X> %s', name, suffix, flags) | ||
| end | ||
| end | ||
|
|
||
| # Query a host for its NetBIOS name table. | ||
| # | ||
| # @param host [String] target IP address (unicast — no broadcast) | ||
| # @param port [Integer] destination UDP port (default 137) | ||
| # @param timeout [Numeric] per-attempt receive timeout in seconds | ||
| # @param retries [Integer] total number of attempts | ||
| # @param udp_socket [UDPSocket, Rex::Socket::Udp] caller-owned UDP socket. | ||
| # The caller is responsible for binding and closing it. | ||
| # @return [Array<Entry>, nil] the name table, or nil on timeout/parse failure | ||
| def self.query(host, port: NBNS_PORT, timeout: DEFAULT_TIMEOUT, | ||
| retries: DEFAULT_RETRIES, udp_socket:) | ||
| request = NodeStatusRequest.new(transaction_id: rand(0xFFFF)) | ||
| request.question_name.set('*'.ljust(16, "\x00")) | ||
| bytes = request.to_binary_s | ||
|
|
||
| retries.times do | ||
| udp_socket.send(bytes, 0, host, port) | ||
| next unless IO.select([udp_socket], nil, nil, timeout) | ||
|
|
||
| data, addr = udp_socket.recvfrom(4096) | ||
| next if data.nil? || data.empty? | ||
| # Reject replies from an unexpected source — this is a unicast | ||
| # query, so anything not from `host` is spoofed or stray. | ||
| next unless addr.nil? || addr[3].nil? || addr[3] == host | ||
|
|
||
| response = NodeStatusResponse.read(data) | ||
| # Reject replies whose transaction ID doesn't match our request. | ||
| next unless response.transaction_id.to_i == request.transaction_id.to_i | ||
|
|
||
| return entries_from(response) | ||
| end | ||
| nil | ||
| rescue IOError, EOFError, SystemCallError | ||
| nil | ||
| end | ||
|
|
||
| # Return the unique file-server name (suffix 0x20) from a host, or nil | ||
| # if the name table doesn't contain one. Convenience helper for the | ||
| # common case of "give me this host's file-server name." | ||
| # | ||
| # @param host [String] target IP address | ||
| # @param kwargs [Hash] forwarded to {.query} | ||
| # @return [String, nil] | ||
| def self.file_server_name(host, **kwargs) | ||
| entries = query(host, **kwargs) or return nil | ||
| entry = entries.find { |e| e.suffix == 0x20 && e.unique? } | ||
| entry&.name | ||
| end | ||
|
|
||
| # @!visibility private | ||
| def self.entries_from(response) | ||
| response.node_names.map do |n| | ||
| Entry.new( | ||
| n.netbios_name.to_s.rstrip, | ||
| n.suffix.to_i, | ||
| n.group?, | ||
| n.active? | ||
| ) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| require 'ruby_smb/nbss/name_service_header_flags' | ||
|
|
||
| module RubySMB | ||
| module Nbss | ||
| # NetBIOS Name Service (NBNS) Node Status Request packet, as defined in | ||
| # [RFC 1002 4.2.17](https://tools.ietf.org/html/rfc1002#section-4.2.17). | ||
| # Sent over UDP to port 137 to retrieve a host's NetBIOS name table. | ||
| class NodeStatusRequest < BinData::Record | ||
| # NBSTAT question type, RFC 1002 4.2.1.3. | ||
| QUESTION_TYPE_NBSTAT = 0x0021 | ||
| # Internet class. | ||
| QUESTION_CLASS_IN = 0x0001 | ||
|
|
||
| endian :big | ||
|
|
||
| # 12-byte NBNS header (RFC 1002 4.2.1.1 and 4.2.1.2). | ||
| uint16 :transaction_id, label: 'Transaction ID' | ||
| name_service_header_flags :flags, label: 'Flags' | ||
| uint16 :qdcount, label: 'QDCount', initial_value: 1 | ||
| uint16 :ancount, label: 'ANCount', initial_value: 0 | ||
| uint16 :nscount, label: 'NSCount', initial_value: 0 | ||
| uint16 :arcount, label: 'ARCount', initial_value: 0 | ||
|
|
||
| # Question section. For a node status query this is always the wildcard | ||
| # NetBIOS name (16 bytes of 0x2A / 0x00), L1-encoded. | ||
| netbios_name :question_name, label: 'Question Name' | ||
| uint16 :question_type, label: 'Question Type', initial_value: QUESTION_TYPE_NBSTAT | ||
| uint16 :question_class, label: 'Question Class', initial_value: QUESTION_CLASS_IN | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| require 'ruby_smb/nbss/name_service_header_flags' | ||
|
|
||
| module RubySMB | ||
| module Nbss | ||
| # The NAME_FLAGS field for each Node Status Response name entry. | ||
| class NodeStatusNameFlags < BinData::Record | ||
| endian :big | ||
|
|
||
| bit1 :group, label: 'Group Name', initial_value: 0 | ||
| bit2 :owner_node_type, label: 'Owner Node Type', initial_value: 0 | ||
| bit1 :deregister, label: 'Deregister', initial_value: 0 | ||
| bit1 :conflict, label: 'Conflict', initial_value: 0 | ||
| bit1 :active, label: 'Active', initial_value: 0 | ||
| bit1 :permanent, label: 'Permanent', initial_value: 0 | ||
| bit9 :reserved, label: 'Reserved', initial_value: 0 | ||
| end | ||
|
|
||
| # Single entry in the NODE_NAME_ARRAY of a Node Status Response, | ||
| # as defined in [RFC 1002 4.2.18](https://tools.ietf.org/html/rfc1002#section-4.2.18). | ||
| # Fixed 18-byte layout (15-byte name, 1-byte suffix, 16-bit flags). | ||
| class NodeStatusName < BinData::Record | ||
| # NAME_FLAGS bits (RFC 1002 4.2.18). | ||
| GROUP_BIT = 0x8000 # 1 = group name, 0 = unique name | ||
| ACTIVE_BIT = 0x0400 # 1 = name registered | ||
|
|
||
| endian :big | ||
|
|
||
| string :netbios_name, label: 'NetBIOS Name', length: 15 | ||
| uint8 :suffix, label: 'Suffix' | ||
| node_status_name_flags :name_flags, label: 'Name Flags' | ||
|
|
||
| def group? | ||
| (raw_name_flags & GROUP_BIT) != 0 | ||
| end | ||
|
|
||
| def unique? | ||
| !group? | ||
| end | ||
|
|
||
| def active? | ||
| (raw_name_flags & ACTIVE_BIT) != 0 | ||
| end | ||
|
|
||
| private | ||
|
|
||
| # The 16-bit NAME_FLAGS field as a raw integer, for masking against | ||
| # the GROUP_BIT / ACTIVE_BIT constants. | ||
| def raw_name_flags | ||
| name_flags.to_binary_s.unpack1('n') | ||
| end | ||
| end | ||
|
|
||
| # NetBIOS Name Service (NBNS) Node Status Response packet, as defined in | ||
| # [RFC 1002 4.2.18](https://tools.ietf.org/html/rfc1002#section-4.2.18). | ||
| # Received over UDP from port 137 in reply to a {NodeStatusRequest}. | ||
| # Does not decode the trailing STATISTICS field; callers only need the | ||
| # name table. | ||
| class NodeStatusResponse < BinData::Record | ||
| endian :big | ||
|
|
||
| # 12-byte NBNS header. | ||
| uint16 :transaction_id, label: 'Transaction ID' | ||
| name_service_header_flags :flags, label: 'Flags' | ||
| uint16 :qdcount, label: 'QDCount' | ||
| uint16 :ancount, label: 'ANCount' | ||
| uint16 :nscount, label: 'NSCount' | ||
| uint16 :arcount, label: 'ARCount' | ||
|
|
||
| # Answer section. Microsoft's implementation omits the question-echo, | ||
| # so the owner name appears directly after the header. | ||
| netbios_name :owner_name, label: 'Owner Name' | ||
| uint16 :rr_type, label: 'RR Type' | ||
| uint16 :rr_class, label: 'RR Class' | ||
| uint32 :ttl, label: 'TTL' | ||
| uint16 :rdlength, label: 'RDLENGTH' | ||
|
|
||
| # RDATA begins here. NODE_NAME_ARRAY is preceded by an 8-bit count. | ||
| uint8 :num_names, label: 'Number of Names' | ||
| array :node_names, type: :node_status_name, initial_length: :num_names | ||
|
|
||
| # Returns the unique (non-group) file-server name (suffix 0x20) if one | ||
| # is present in the name table, else nil. | ||
| def file_server_name | ||
| entry = node_names.find { |n| n.suffix == 0x20 && n.unique? } | ||
| entry&.netbios_name&.to_s&.rstrip | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also ignore the message if the transaction ID or sender does not match? We're just reading the udp socket hoping it is the response we expect from the source we expect. This could be a security issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, real spoofing risk on an unconnected UDP socket.
querynow drops any datagram whose source address ≠ the queried host or whose transaction ID ≠ the one we sent, and keeps waiting within the timeout window. Added specs for both rejection paths. (fa57a06)