Add NetBIOS name resolution fallback for session_request#296
Add NetBIOS name resolution fallback for session_request#296Z6543 wants to merge 14 commits intorapid7:masterfrom
Conversation
9a3987d to
8c74913
Compare
|
Since you split this out there's conflicts. It looks like there was some shared commits in this branch and in #294 which has been landed. Can you please rebase this on |
There was a problem hiding this comment.
Pull request overview
Adds NetBIOS Node Status–based name resolution and uses it as a fallback retry for Client#session_request when *SMBSERVER is rejected, while also extending SMB1 support for legacy (Win9x/LANMAN-era) servers (RAP share enumeration, OPEN_ANDX, and more tolerant packet parsing).
Changes:
- Add pure-Ruby NBNS Node Status query support and
session_requestretry logic on NBSSCALLED_NAME_NOT_PRESENT(0x82), with injectable TCP/UDP socket factories. - Add/extend SMB1 legacy compatibility: SMB_INFO_STANDARD directory parsing, OPEN_ANDX support, RAP
NetShareEnumover\PIPE\LANMAN, and tolerant parsing for short/variant SMB1 responses. - Extend client APIs for legacy servers (e.g., share-level password support in SMB1 tree connect; legacy non-extended-security SMB1 auth path).
Impact Analysis:
- Blast radius: High — touches
RubySMB::Clientconnection/session establishment (session_request, negotiation/auth), SMB1 tree operations (list,open_file/open_pipe), and introduces new public RAP/NBNS helpers likely consumed by external callers. - Data and contract effects: Adds new injectable factories and new keyword args (e.g.,
tree_connect(..., password:)), and changes behavior ofsession_requestto reconnect/retry under specific NBSS failures; compatibility risk if callers relied on previous one-shot behavior or on requiringruby_smb/smb1standalone. - Rollback and test focus: Rollback should be straightforward (feature is largely additive) but validate: (1) SMB1 session establishment on port 139 with Win9x, (2) directory listing pagination via FIND_FIRST2/FIND_NEXT2 with SMB_INFO_STANDARD, (3) OPEN_ANDX handling for non-disk resources, (4) RAP NetShareEnum request serialization and truncated-response handling.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/lib/ruby_smb/smb1/tree_spec.rb | Adds specs for SMB_INFO_STANDARD listing behavior and updated max_data_count expectation. |
| spec/lib/ruby_smb/rap/net_share_enum_spec.rb | New specs for RAP NetShareEnum request/response handling and Win9x offset quirks. |
| spec/lib/ruby_smb/nbss/node_status_spec.rb | New specs for Node Status UDP query, retries, parsing failures, and socket API variants. |
| spec/lib/ruby_smb/nbss/node_status_response_spec.rb | New specs for Node Status Response decoding and file-server-name selection. |
| spec/lib/ruby_smb/nbss/node_status_request_spec.rb | New specs verifying Node Status Request encoding. |
| spec/lib/ruby_smb/client_spec.rb | Adds specs for *SMBSERVER rejection retry path, socket factory usage, and UDP lookup wiring. |
| lib/ruby_smb/smb1/tree.rb | Includes RAP module, updates listing logic for SMB_INFO_STANDARD, caps max_data_count, and adds OPEN_ANDX path. |
| lib/ruby_smb/smb1/pipe.rb | Extends Pipe to support RAP NetShareEnum when opened as \PIPE\LANMAN. |
| lib/ruby_smb/smb1/packet/tree_connect_response.rb | Makes parsing tolerant of short Win9x TreeConnect responses (missing fields). |
| lib/ruby_smb/smb1/packet/tree_connect_request.rb | Fixes password field serialization to respect configured length exactly. |
| lib/ruby_smb/smb1/packet/trans2/find_information_level/find_info_standard.rb | Adds BinData structure for SMB_INFO_STANDARD entries. |
| lib/ruby_smb/smb1/packet/trans2/find_information_level.rb | Requires the new SMB_INFO_STANDARD info-level structure. |
| lib/ruby_smb/smb1/packet/trans2/find_first2_response.rb | Adds parsing mode for sequential (no next_offset) info levels with optional pad bytes. |
| lib/ruby_smb/smb1/packet/session_setup_legacy_response.rb | Makes legacy session-setup response parsing tolerant of byte_count=0. |
| lib/ruby_smb/smb1/packet/session_setup_legacy_request.rb | Updates legacy session-setup request strings to null-terminated fields. |
| lib/ruby_smb/smb1/packet/open_andx_response.rb | Adds SMB_COM_OPEN_ANDX response packet structure. |
| lib/ruby_smb/smb1/packet/open_andx_request.rb | Adds SMB_COM_OPEN_ANDX request packet structure. |
| lib/ruby_smb/smb1/packet/negotiate_response.rb | Makes negotiate parsing tolerant of short Win9x response layouts. |
| lib/ruby_smb/smb1/packet.rb | Requires new OPEN_ANDX packet classes. |
| lib/ruby_smb/smb1/commands.rb | Adds SMB_COM_OPEN_ANDX command constant. |
| lib/ruby_smb/rap/net_share_enum.rb | Implements RAP NetShareEnum over SMB_COM_TRANSACTION and Win9x offset-aware parsing. |
| lib/ruby_smb/rap.rb | Introduces RAP namespace entry point. |
| lib/ruby_smb/nbss/node_status.rb | Implements pure-Ruby NBNS Node Status query with retry/timeout and socket-factory injection. |
| lib/ruby_smb/nbss/node_status_response.rb | Defines Node Status Response BinData structures and file-server-name helper. |
| lib/ruby_smb/nbss/node_status_request.rb | Defines Node Status Request BinData structure. |
| lib/ruby_smb/nbss/negative_session_response.rb | Adds NBSS error code constants and uses them in error message mapping. |
| lib/ruby_smb/nbss.rb | Requires new NBNS Node Status request/response/query modules. |
| lib/ruby_smb/error.rb | Extends NetBiosSessionService error to carry numeric NBSS error_code. |
| lib/ruby_smb/client/tree_connect.rb | Adds optional SMB1 share-level password: to tree connect request building. |
| lib/ruby_smb/client/negotiation.rb | Tracks supports_nt_smbs and captures non-extended-security SMB1 challenge bytes. |
| lib/ruby_smb/client/authentication.rb | Adds legacy SMB1 auth path using LM/NTLM challenge-response when non-extended security is negotiated. |
| lib/ruby_smb/client.rb | Adds socket factories, supports_nt_smbs, share-level tree_connect password plumbing, and NBSS retry logic with Node Status lookup. |
| lib/ruby_smb.rb | Requires the new RAP module at top-level. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Exposes #net_share_enum directly on the tree for callers that need | ||
| # RAP against \PIPE\LANMAN without opening the pipe (Win9x servers do | ||
| # not permit OPEN_ANDX on it). | ||
| include RubySMB::Rap::NetShareEnum | ||
|
|
| while eos.zero? && last | ||
| find_next_request = RubySMB::SMB1::Packet::Trans2::FindNext2Request.new | ||
| find_next_request = set_header_fields(find_next_request) | ||
| find_next_request.smb_header.flags2.unicode = 1 if unicode |
| unless response.parameter_block.resource_type == RubySMB::SMB1::ResourceType::DISK | ||
| raise RubySMB::Error::RubySMBError, | ||
| "SMB_COM_OPEN_ANDX resource type 0x#{response.parameter_block.resource_type.to_s(16)} not supported" | ||
| end | ||
| file = RubySMB::SMB1::File.allocate | ||
| file.tree = self | ||
| file.name = filename | ||
| file.fid = response.parameter_block.fid | ||
| file.size = response.parameter_block.file_data_size | ||
| file.size_on_disk = response.parameter_block.file_data_size | ||
| file.attributes = response.parameter_block.file_attributes | ||
| file |
| request = RubySMB::SMB1::Packet::Trans::Request.new | ||
| request.smb_header.tid = rap_tree.id | ||
| request.smb_header.flags2.unicode = 0 | ||
| request.data_block.name = "\\PIPE\\LANMAN\x00".b |
| break [] if offset + ShareInfo1.new.num_bytes > data_bytes.bytesize | ||
| entry = ShareInfo1.read(data_bytes[offset, ShareInfo1.new.num_bytes]) | ||
| { | ||
| name: entry.netname.to_s.delete("\x00"), | ||
| type: entry.share_type | ||
| } | ||
| end.compact |
Address PR rapid7#294 review comments rapid7#16 and rapid7#18. The session-request retry and UDP Node Status query previously hardcoded TCPSocket.new and UDPSocket.new, which breaks Metasploit pivoting because Metasploit needs every socket to come from Rex::Socket. Add tcp_socket_factory and udp_socket_factory attributes on Client. Both default to stdlib socket constructors for standalone use; callers that need custom socket creation (Rex::Socket, test doubles, TLS wrappers) can inject their own callable. Skip setsockopt on sockets that don't respond to it, so Rex-style sockets don't break the retry.
The retry was gated on name == '*SMBSERVER', which missed the common case where a caller passes a specific NetBIOS name that the server rejects with CALLED_NAME_NOT_PRESENT. Drop the name gate and instead guard against retry loops by bailing when the resolved name matches what was just rejected (case-insensitive, whitespace-insensitive). Coerce error_code through to_i so the comparison works regardless of whether the attribute holds a plain Integer or a BinData primitive.
Per PR rapid7#294 review comment, the library should do everything in Ruby. Remove netbios_lookup_nmblookup entirely; netbios_lookup_name now only uses the native NBSS UDP Node Status query.
If the caller supplied an explicit called name (anything other than the wildcard '*SMBSERVER' or an empty string), honor it and propagate the server's rejection instead of silently starting a UDP Node Status query and reconnecting the socket. This lets Metasploit's SMBName option do what a user expects — setting it bypasses auto-discovery entirely. Auto-discovery still kicks in for the default wildcard or an empty name, so no regression for callers that relied on it.
New RubySMB::Nbss::NodeStatus module exposes two public entry points: RubySMB::Nbss::NodeStatus.query(host) # => [Entry, ...] full name table RubySMB::Nbss::NodeStatus.file_server_name(host) # => String (0x20 UNIQUE) No shell-out to Samba's nmblookup. Supports retries, a configurable timeout, and an injectable UDP socket factory so Metasploit callers can route the query through Rex::Socket instead of opening a raw stdlib UDPSocket. Client#netbios_lookup_udp now just delegates to NodeStatus.file_server_name, passing the client's udp_socket_factory.
Rex::Socket::Udp inherits `send(mesg, flags, [sockaddr])` from Socket, not stdlib UDPSocket's 4-arg variant, so calling `send(bytes, 0, host, port)` on it raises "wrong number of arguments". Route through `sendto(mesg, host, port)` when the socket exposes it; otherwise fall back to stdlib's 4-arg `send`.
IO.select([sock]) can miss wakeups on a Rex::Socket::Udp because Rex's own timed_read selects on the underlying fd rather than on self. Call sock.recvfrom(length, timeout) directly when the socket provides the sendto/recvfrom(..., timeout) pair, and only use IO.select for stdlib UDPSocket which has no per-call receive timeout. Fixes NBNS node-status query silently timing out when the socket factory returns a Rex::Socket::Udp (Metasploit module pivot path).
Windows 9x ignores the client source port on NBNS and always sends the Node Status response to destination port 137. On an ephemeral-port socket the kernel drops the reply, so the query appears to time out even though the server is answering (verified via tcpdump: the 229-byte reply goes to :137, not to our ephemeral port). Try to bind(0.0.0.0:137) on the local UDP socket before sending, same technique Samba's nmblookup uses. Silently fall back to the ephemeral bind when we lack the privilege (EACCES) or the port is already held by another listener (EADDRINUSE) — the query will still succeed against well-behaved NBNS servers that honor the request's source port.
Rex::Socket::Udp's bind takes a single sockaddr string, so calling
sock.bind('0.0.0.0', 137) on it raised ArgumentError before we got to
the actual query. Rex sockets bind their local endpoint at create time
via 'LocalHost'/'LocalPort' instead — skip the post-create bind path
entirely for anything that exposes sendto (the Rex-style API marker we
were already using).
Also widen the rescue to swallow ArgumentError in case another socket
type surfaces a similarly incompatible bind signature.
Earlier change assumed Win9x replies are delivered only when the client is bound to local port 137. In practice the response is received fine on an ephemeral-port socket (confirmed by tcpdump + nmblookup on the reporter's host), so the bind trick adds complexity without buying us anything and breaks on Rex::Socket::Udp whose bind signature differs. Keep node_status.rb lean: send, wait with a timeout appropriate to the socket type, parse. Fall back to `set SMBName ...` when a target still doesn't answer.
Reverting the previous revert — Win9x NBNS does reply to destination port 137 regardless of the client source port, so the kernel drops the answer on an ephemeral-port socket. Bind locally to 137 before sending, same technique Samba's nmblookup uses (which works without root when the binary has CAP_NET_BIND_SERVICE or the system has net.ipv4.ip_unprivileged_port_start lowered). Skips the 2-arg bind on Rex::Socket::Udp (its bind signature differs); Rex callers bind via 'LocalPort' at create time. On EACCES/EADDRINUSE the bind silently fails and the caller keeps its ephemeral port — this still works against well-behaved NBNS servers that honor the request's source port.
8c74913 to
075c157
Compare
|
The Copilot comments are no longer valid for this PR after the rebase. There is one important "challenge" with this PR. Metasploit has to be able to listen on port 137. Even though the smb client sends the UDP packet from a random high port to port 137, the smb server responds to port 137 from port 137. But on the other hand, this PR is only needed if the Metasploit user does not provide the SMBSERVER parameter and it is left on default. Here is the output of nmblookup -A 192.168.124.178 |
Win9x always sends the Node Status response to destination port 137, so the reply is dropped when the client socket is on an ephemeral port. Binding to 137 fixes it but fails with EACCES unless the process has CAP_NET_BIND_SERVICE. A raw IPPROTO_UDP socket receives a copy of every incoming IP datagram regardless of destination port, so it works without an exclusive bind. query_via_raw_socket uses this approach when the caller has CAP_NET_RAW; falls back gracefully on EPERM/EACCES.
When nmbd owns UDP/137 on the attacker host it intercepts Win9x NBNS replies, causing netbios_lookup_udp to time out (3 retries × 2 s ≈ 6 s). That 6-second hang pushes NBSS session setup past Rex's ConnectTimeout, so the caller sees Rex::ConnectionTimeout instead of a clean NBSS error. Add netbios_lookup_raw_socket as a fallback: it calls NodeStatus.query_via_raw_socket which uses SOCK_RAW/IPPROTO_UDP and receives a kernel copy of every incoming UDP datagram regardless of which process owns port 137. netbios_lookup_name now chains both paths so the first one that returns a name wins.
Summary
Follow-up to #294 addressing review comments #16, #17, #18, #19 and #20 from @smcintyre-r7. Splits the NetBIOS auto-discovery functionality that was removed from #294 into a dedicated reviewable unit.
On
CALLED_NAME_NOT_PRESENT(NBSS error 0x82) when the caller used the default*SMBSERVERwildcard,Client#session_requestnow performs a pure-Ruby NBNS Node Status query, reconnects the TCP socket, and retries once with the resolved name. A caller-supplied name (e.g. Metasploit'sSMBName) short-circuits auto-discovery.Relationship to #294
This branch contains the commits from #294 plus the NetBIOS-specific additions on top. Review is easiest after #294 merges — at that point the diff here will be only the NetBIOS work. If you'd prefer, I can rebase onto
masterdirectly once #294 lands.What's in the diff
RubySMB::Nbss::NodeStatus.query(host)/.file_server_name(host)— public pure-Rubynmblookup -Aequivalent, built on newNbss::NodeStatusRequestandNbss::NodeStatusResponseBinData structures. No external binaries.Client#tcp_socket_factoryandClient#udp_socket_factory— injectable callables for socket creation. Default to stdlibTCPSocket.new/UDPSocket.new; Metasploit passesRex::Socket::Tcp.create/Rex::Socket::Udp.createfactories so pivoted connections still work.NetBiosSessionService#error_code— typed NBSS error code attribute plus named constants (CALLED_NAME_NOT_PRESENT= 0x82, etc). Retry matches on the number, not a message substring.CAP_NET_BIND_SERVICEornet.ipv4.ip_unprivileged_port_startis 137 or lower.respond_to?(:sendto)branches between the Rex-stylesendto(mesg, host, port)/recvfrom(len, timeout)API and the stdlibsend(mesg, flags, host, port)/IO.select+recvfrom(len)pattern.Addresses PR #294 review comments
TCPSocket.new/UDPSocket.new.netbios_lookup_nmblookupshell-out is gone; everything is Ruby.lib/ruby_smb/nbss/node_status_request.rbandnode_status_response.rbfollow the existing NBSS pattern; the UDP lookup is ~10 lines of BinData glue.Test plan
bundle exec rspec— 12,292 examples, 0 failures (+14 new specs:node_status_request_spec.rb,node_status_response_spec.rb,node_status_spec.rb, plus*SMBSERVERretry coverage inclient_spec.rb).Rex::Socket::Udp.createfactory.