Add NetBIOS name resolution fallback for session_request#296
Open
Z6543 wants to merge 21 commits intorapid7:masterfrom
Open
Add NetBIOS name resolution fallback for session_request#296Z6543 wants to merge 21 commits intorapid7:masterfrom
Z6543 wants to merge 21 commits intorapid7:masterfrom
Conversation
Win95 uses a pre-NTLMSSP dialect of SMB1 that differs from modern Windows in several ways. This commit adds the minimum changes needed to handle those differences across the negotiate, auth, tree connect, and file listing phases. Negotiate (negotiation.rb, negotiate_response.rb): - Handle NegotiateResponse (non-extended security) in addition to NegotiateResponseExtended. Win95 returns a raw 8-byte challenge instead of a SPNEGO security blob. - Override DataBlock#do_read in NegotiateResponse to tolerate byte_count=8 responses that omit domain_name and server_name (Win95 only sends the challenge). Authentication (authentication.rb, session_setup_legacy_request.rb, session_setup_legacy_response.rb): - Add smb1_legacy_authenticate path triggered when the negotiate phase stored an 8-byte challenge (@smb1_negotiate_challenge). Computes LM and NTLM challenge-response hashes via Net::NTLM and sends them in SessionSetupLegacyRequest. - Fix SessionSetupLegacyRequest DataBlock: account_name and primary_domain were fixed-length `string` (2 bytes) instead of null-terminated `stringz`, truncating the username and domain. - Override DataBlock#do_read in SessionSetupLegacyResponse to handle byte_count=0 responses (Win95 returns no string fields). NetBIOS session (client.rb, client_spec.rb): - Use @local_workstation as the calling name in NetBIOS session requests. Win95 rejects sessions with an empty calling name. Tree connect (tree_connect_response.rb): - Make optional_support conditional on word_count >= 3. Win95 returns a smaller ParameterBlock without this field. - Override DataBlock#do_read to handle responses that omit native_file_system when byte_count only covers the service field. File listing (tree.rb, find_information_level.rb, find_info_standard.rb): - Add FindInfoStandard struct for SMB_INFO_STANDARD (level 1), the LANMAN 2.0 information level used by Win95. - When type is FindInfoStandard, disable unicode in the request, request up to 255 entries, and return early after FIND_FIRST2 using raw byte parsing (parse_find_first2_info_standard) that bypasses BinData alignment issues with Win95 responses. - Clamp max_data_count to server_max_buffer_size in set_find_params so requests don't exceed Win95's small buffer limit. - Add defensive guards in the FIND_NEXT2 pagination loop: use safe navigation on results.last, check for empty batches, and require `last` to be non-nil before continuing. These prevent infinite loops when the server returns zero results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move three protocol features from the smb_browser application into the
ruby_smb library so they are reusable:
SMB_COM_OPEN_ANDX (0x2D) packet classes and Tree#open_andx:
New OpenAndxRequest and OpenAndxResponse BinData packet classes
implement the LANMAN 1.0 file-open command. Tree#open_andx uses
these to open files on servers that lack NT_CREATE_ANDX, such as
Windows 95/98/ME. Returns a standard SMB1::File handle that
supports read/write/close via the existing ReadAndx/WriteAndx
infrastructure.
Share-level password on tree_connect:
Client#tree_connect and smb1_tree_connect now accept a password:
keyword argument. When provided, the password is placed in the
TreeConnectRequest data block with the null terminator and correct
password_length, enabling connection to shares protected by
share-level authentication (Windows 95/98/ME).
RAP share enumeration via Client#net_share_enum_rap:
New method sends a NetShareEnum RAP request (function 0) over
\PIPE\LANMAN to enumerate shares on servers that do not support
DCERPC/srvsvc. Returns an array of {name:, type:} hashes.
Automatically connects to and disconnects from IPC$.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The password field in TreeConnectRequest::DataBlock was declared as BinData::Stringz, which always appends a null terminator when serializing regardless of the length parameter. With password_length set to N, the field wrote N+1 bytes (the password data plus a null), corrupting the share path that follows in the packet. This broke any use case requiring exact control over the password byte count, such as CVE-2000-0979 exploitation. Windows 95 uses password_length to decide how many bytes to validate. With the extra null, the server read the correct password bytes but then parsed the null as the start of the share path, causing every tree connect to fail with a path error rather than a password result. Change the field from stringz to string. BinData::String respects the length parameter exactly, writing precisely password_length bytes with no trailing null. The initial_value changes from '' to "\x00" to preserve the default behavior: when no password is set, the field writes one null byte (matching the default password_length of 1). Existing callers are unaffected because smb1_tree_connect already appends the null terminator explicitly (password + "\x00") and sets password_length to include it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Windows 95 rejects NetBIOS session requests using the wildcard name '*SMBSERVER'. When the server responds with "Called name not present", look up the server's actual NetBIOS name via nmblookup or a raw UDP Node Status query (RFC 1002, port 137), reconnect the TCP socket, and retry the session request with the resolved name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix blob.getbyte, validate RAP response, use numeric NBSS error_code - Add specs for RAP + *SMBSERVER retry + SMB_INFO_STANDARD parsing - Collapse SMB1 negotiate response branches; use "\x00".b in tree_connect - Fix 404 doc links on OPEN_ANDX request/response and FindInfoStandard - Make FindInfoStandard round-trip via BinData; drop byte-slicing path - Auto-select OPEN_ANDX inside open_file via supports_nt_smbs capability - Move RAP NetShareEnum into lib/ruby_smb/rap/ + SMB1::Pipe mixin - Replace hand-crafted UDP Node Status bytes with NBNS BinData structs
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.
The RAP refactor moved net_share_enum onto SMB1::Pipe, but Metasploit modules that predate this change still call Client#net_share_enum_rap. Restore that entry point as a thin wrapper; guard it so it raises cleanly when SMB2/3 was negotiated (addresses smcintyre-r7's original concern about the method breaking in the SMB2/3 path). Also let the RAP NetShareEnum module be mixed into SMB1::Tree directly, via a rap_tree helper that resolves self-or-tree. Win9x servers do not allow OPEN_ANDX on \PIPE\LANMAN, so callers cannot always instantiate a Pipe first; Tree-level access handles that.
Windows 95 packs Trans response parameters immediately after byte_count (parameter_offset = 55), with no 4-byte-alignment padding. BinData's Trans::Response::DataBlock always inserts a 1-byte pad1 before reading trans_parameters, so the parser reads bytes 56-63 instead of 55-62 and mis-decodes status as 0x9f00 instead of the real 0x0000. Slice the parameter and data sections from the raw response using the parameter_offset and data_offset fields the server set, which are authoritative. BinData-generated responses (including our own test fixtures) put the same offsets, so existing specs are unaffected. Add a regression spec that constructs a Win9x-style response with parameter_offset=55.
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.
Co-authored-by: Spencer McIntyre <58950994+smcintyre-r7@users.noreply.github.com>
Co-authored-by: Spencer McIntyre <58950994+smcintyre-r7@users.noreply.github.com>
OpenAndxResponse (comment r3133976375): - Align field names with MS-CIFS 2.2.4.41.2: granted_access->access_rights, file_type->resource_type, device_state->nmpipe_status, action->open_results, data_size->file_data_size, file_attributes->file_attrs. - Use SMB_NMPIPE_STATUS bit_field for the nmpipe_status field instead of a plain uint16 (same type the NT_CREATE_ANDX / TRANS2_OPEN2 responses use). - Use SMB_FILE_ATTRIBUTES bit_field for file_attrs and UTIME for last_write_time — consistent with the other SMB1 open responses. - Collapse the misplaced 'server_fid'(4) + 'reserved'(2) pair into a single Reserved[3] array of uint16s, as the spec defines. - Update Tree#build_open_andx_handle callers for the renamed fields. Client#net_share_enum_rap (comment r3134040652): - Drop from the Client namespace. Callers use tree.net_share_enum on an IPC$ tree instead, mirroring the rest of the tree-scoped helpers.
9a3987d to
8c74913
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
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.