Skip to content

Add Windows 95/98/ME SMB1 support#294

Open
Z6543 wants to merge 11 commits intorapid7:masterfrom
Z6543:win9x-smb1-support
Open

Add Windows 95/98/ME SMB1 support#294
Z6543 wants to merge 11 commits intorapid7:masterfrom
Z6543:win9x-smb1-support

Conversation

@Z6543
Copy link
Copy Markdown

@Z6543 Z6543 commented Mar 14, 2026

Fixes #293

Summary

  • Add SMB1 protocol support for Windows 95/98/ME and other pre-NT legacy hosts that use port 139, non-extended security, share-level authentication, and RAP-based share enumeration
  • Fix TreeConnectRequest password field serialization bug that corrupted packets when callers set password_length without a trailing null
  • Add NetBIOS name resolution fallback when the *SMBSERVER wildcard is rejected

Details

Windows 95/98/ME SMB1 support (d0d2607)

Windows 9x hosts differ from NT-based systems in several ways that RubySMB did not handle:

  • Non-extended security negotiate: parse the legacy negotiate response (no extended_security flag, no NTLMSSP)
  • Legacy LM/NTLM session setup: use SessionSetupLegacyRequest/SessionSetupLegacyResponse instead of NTLMSSP blobs
  • Minimal TreeConnectResponse: Win95 may omit the native_file_system field; the response DataBlock now handles short reads
  • FindInfoStandard: add the SMB_FIND_FILE_STANDARD information level for TRANS2 directory listings
  • SMB1::Tree fallbacks: handle ERRDOS/ERRnomem from TRANS2 by falling back to core protocol commands

SMB_COM_OPEN_ANDX, share-level auth, and RAP (10ed8be)

  • net_share_enum_rap: enumerate shares via RAP (Remote Administration Protocol) over \PIPE\LANMAN, the only method Windows 9x supports (DCERPC/SRVSVC fails with DBG_CONTINUE)
  • smb1_tree_connect password support: accept a password: keyword for share-level authentication where passwords are per-share, not per-user
  • SMB_COM_OPEN_ANDX: add request/response packets and Tree#open_file/Tree#read_file/Tree#write_file for Win95 hosts that lack NT_CREATE_ANDX

Fix TreeConnectRequest password field (541321c)

The password field was declared as BinData::Stringz, which always appends a null terminator when serializing, regardless of the length parameter. With password_length=N, the field wrote N+1 bytes, shifting the share path and corrupting the packet.
Changed to BinData::String which respects the length exactly. Existing callers are unaffected since smb1_tree_connect already appends "\x00" explicitly.

NetBIOS name resolution fallback (b5269e0)

Windows 95 rejects NetBIOS session requests using the *SMBSERVER wildcard. When the server responds with "Called name not present", the client now resolves the server's actual NetBIOS name via nmblookup or a raw UDP Node Status query (RFC 1002),
reconnects, and retries.

Test plan

  • bundle exec rspec — all existing specs pass (verified: 407 examples, 0 failures)
  • Connect to Windows 95 on port 139 with direct: false, versions: [1]
  • smb_login with empty credentials (share-level auth)
  • net_share_enum_rap returns share list
  • tree_connect with password: keyword to a password-protected share
  • TreeConnectRequest with password_length=1 serializes exactly 1 password byte (no extra null)
  • Session request to a host that rejects *SMBSERVER resolves the name and retries

Z6543 and others added 4 commits March 14, 2026 12:44
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SMB1 compatibility for legacy (pre-NT) SMB servers (notably Windows 95/98/ME) by supporting non-extended security negotiation/auth, RAP-based share enumeration, and additional SMB1 command/response handling.

Changes:

  • Add SMB1 legacy protocol features: non-extended security negotiate + legacy SessionSetup, RAP share enumeration, and SMB_COM_OPEN_ANDX support.
  • Improve SMB1 directory listing compatibility (SMB_INFO_STANDARD path and buffer sizing adjustments).
  • Fix SMB1 TreeConnect password field serialization and add NetBIOS name-resolution retry when *SMBSERVER is rejected.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
spec/lib/ruby_smb/smb1/tree_spec.rb Updates expectation for max_data_count behavior to reflect negotiated server buffer size.
spec/lib/ruby_smb/client_spec.rb Adjusts NetBIOS session request expectations for calling name formatting.
lib/ruby_smb/smb1/tree.rb Adds SMB_COM_OPEN_ANDX helper and legacy SMB_INFO_STANDARD parsing/behavior for directory listing.
lib/ruby_smb/smb1/packet/tree_connect_response.rb Makes TreeConnectResponse parsing tolerant of minimal Win9x responses.
lib/ruby_smb/smb1/packet/tree_connect_request.rb Fixes TreeConnect password field serialization length behavior.
lib/ruby_smb/smb1/packet/trans2/find_information_level/find_info_standard.rb Introduces SMB_INFO_STANDARD find info structure for legacy servers.
lib/ruby_smb/smb1/packet/trans2/find_information_level.rb Loads the new SMB_INFO_STANDARD info level.
lib/ruby_smb/smb1/packet/session_setup_legacy_response.rb Makes legacy SessionSetup response parsing tolerant of empty string blocks.
lib/ruby_smb/smb1/packet/session_setup_legacy_request.rb Adjusts legacy SessionSetup request strings to be null-terminated string fields.
lib/ruby_smb/smb1/packet/open_andx_request.rb Adds SMB_COM_OPEN_ANDX request packet definition.
lib/ruby_smb/smb1/packet/open_andx_response.rb Adds SMB_COM_OPEN_ANDX response packet definition.
lib/ruby_smb/smb1/packet/negotiate_response.rb Makes SMB1 negotiate parsing tolerant of challenge-only Win9x responses.
lib/ruby_smb/smb1/packet.rb Requires the new OpenAndx packet classes.
lib/ruby_smb/smb1/commands.rb Adds SMB_COM_OPEN_ANDX command constant.
lib/ruby_smb/client/tree_connect.rb Adds SMB1 share-level password support for TreeConnect.
lib/ruby_smb/client/negotiation.rb Supports SMB1 non-extended security negotiate responses and stores the legacy challenge.
lib/ruby_smb/client/authentication.rb Adds SMB1 legacy (LM/NTLM challenge-response) authentication path.
lib/ruby_smb/client.rb Adds RAP share enumeration and NetBIOS *SMBSERVER rejection fallback with name lookup + reconnect.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/ruby_smb/smb1/tree.rb Outdated
Comment on lines +384 to +395
break if offset + 23 > blob.length
name_len = blob[offset + 22].ord
if name_len == 0
offset += 23
next
end
break if offset + 23 + name_len > blob.length
entry = type.read(blob[offset, 23 + name_len])
results << entry
entry_end = offset + 23 + name_len
if entry_end < blob.length && blob[entry_end].ord == 0
offset = entry_end + 1
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blob[offset + 22].ord (and later blob[entry_end].ord) will raise NoMethodError because String#[] with an integer index returns an Integer in Ruby, not a 1-byte String. Use getbyte (e.g., blob.getbyte(offset + 22)) or slice a 1-byte substring before calling .ord to safely read these length/terminator bytes.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1845dac by switching to getbyte. (Minor nit — str[i].ord actually does work in Ruby 1.9+ since String#[] returns a 1-byte String there, not an Integer, but getbyte is clearer anyway.) The whole parse_info_standard_blob path was subsequently removed in the same commit when FindInfoStandard became a proper BinData type parsed through FindFirst2Response#results — see the reply to #14.

Comment thread lib/ruby_smb/client.rb Outdated
Comment on lines +657 to +679
response = RubySMB::SMB1::Packet::Trans::Response.read(
raw_response
)

rap_resp_params = response.data_block.trans_parameters.to_s
if rap_resp_params.length < 8
raise RubySMB::Error::InvalidPacket,
'Invalid RAP response parameters'
end

_status, _converter, entry_count, _available =
rap_resp_params.unpack('vvvv')

rap_data = response.data_block.trans_data.to_s
shares = []
entry_count.times do |i|
offset = i * 20
break if offset + 20 > rap_data.length
name = rap_data[offset, 13].delete("\x00")
type_val = rap_data[offset + 14, 2].unpack1('v')
shares << { name: name, type: type_val & 0x0FFFFFFF }
end
shares
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

net_share_enum_rap parses the transaction response without validating the SMB packet. Please verify response.valid? and that response.status_code indicates success before unpacking trans_parameters/trans_data, and consider failing fast if the RAP _status field is non-zero (it is currently ignored). Without these checks, malformed/failed responses can be silently misinterpreted as an empty/partial share list.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1845dacnet_share_enum_rap now validates response.valid? and response.status_code and raises on a non-zero RAP _status word. 97bd0ab then moved the call site into RubySMB::Rap::NetShareEnum, see #15.

Comment thread lib/ruby_smb/client.rb Outdated
Comment on lines +714 to +717
send_session_request(name)
rescue RubySMB::Error::NetBiosSessionService => e
raise unless name == '*SMBSERVER' && e.message.include?('Called name not present')

Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The session-request retry logic keys off e.message.include?('Called name not present'), which is brittle (message text can change) and loses access to the underlying NBSS error_code (0x82). Since NegativeSessionResponse already exposes error_code, consider propagating the code in the raised exception (or returning it) so the retry condition can be based on the numeric code instead of substring matching.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code path was split out to a separate branch (netbios-name-resolution-fallback) per @smcintyre-r7's recommendation in #20 below. In that branch the retry keys on e.error_code (new typed attribute on NetBiosSessionService) rather than a message-substring match.

Comment thread lib/ruby_smb/client.rb Outdated
Comment on lines +638 to +770
@@ -644,35 +701,154 @@ def wipe_state!
end

# Requests a NetBIOS Session Service using the provided name.
# When the name is '*SMBSERVER' and the server rejects it with
# "Called name not present", this method automatically looks up
# the server's actual NetBIOS name via a Node Status query,
# reconnects the TCP socket, and retries.
#
# @param name [String] the NetBIOS name to request
# @return [TrueClass] if session request is granted
# @raise [RubySMB::Error::NetBiosSessionService] if session request is refused
# @raise [RubySMB::Error::InvalidPacket] if the response packet is not a NBSS packet
def session_request(name = '*SMBSERVER')
send_session_request(name)
rescue RubySMB::Error::NetBiosSessionService => e
raise unless name == '*SMBSERVER' && e.message.include?('Called name not present')

sock = dispatcher.tcp_socket
if sock.respond_to?(:peerhost)
host = sock.peerhost
port = sock.peerport
else
addr = sock.remote_address
host = addr.ip_address
port = addr.ip_port
end

resolved = netbios_lookup_name(host)
raise unless resolved

dispatcher.tcp_socket.close rescue nil
new_sock = TCPSocket.new(host, port)
new_sock.setsockopt(
::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true
)
dispatcher.tcp_socket = new_sock
send_session_request(resolved)
end

private

# Sends a single NetBIOS Session Request and reads the response.
#
# @param name [String] the NetBIOS name to request
# @return [TrueClass] if session request is granted
def send_session_request(name)
session_request = session_request_packet(name)
dispatcher.send_packet(session_request, nbss_header: false)
raw_response = dispatcher.recv_packet(full_response: true)
begin
session_header = RubySMB::Nbss::SessionHeader.read(raw_response)
if session_header.session_packet_type == RubySMB::Nbss::NEGATIVE_SESSION_RESPONSE
negative_session_response = RubySMB::Nbss::NegativeSessionResponse.read(raw_response)
negative_session_response = RubySMB::Nbss::NegativeSessionResponse.read(raw_response)
raise RubySMB::Error::NetBiosSessionService, "Session Request failed: #{negative_session_response.error_msg}"
end
rescue IOError
raise RubySMB::Error::InvalidPacket, 'Not a NBSS packet'
end

return true
true
end

# Resolves a host's NetBIOS name. Tries nmblookup first (if
# available), then falls back to a raw UDP Node Status query.
#
# @param host [String] the IP address to query
# @return [String, nil] the NetBIOS name, or nil if lookup fails
def netbios_lookup_name(host)
netbios_lookup_nmblookup(host) || netbios_lookup_udp(host)
end
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior paths introduced here (RAP share enumeration, NetBIOS name-resolution fallback/retry, and SMB_INFO_STANDARD parsing) are not covered by specs. Since this file already has comprehensive client/session tests, please add unit tests that exercise (1) RAP parsing including non-zero RAP status, and (2) the *SMBSERVER retry path when the NBSS error code is 0x82.

Copilot generated this review using guidance from organization custom instructions.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 1845dac. spec/lib/ruby_smb/rap/net_share_enum_spec.rb covers RAP parsing (success, non-zero RAP status, truncated params, SMB-level failure). spec/lib/ruby_smb/smb1/tree_spec.rb covers the SMB_INFO_STANDARD path in six examples. The *SMBSERVER retry specs live on the split netbios-name-resolution-fallback branch.

Comment thread lib/ruby_smb/smb1/tree.rb Outdated
Comment on lines +169 to +200
@@ -120,11 +188,16 @@ def list(directory: '\\', pattern: '*', unicode: true,
t2_params.flags.resume_keys = 0
t2_params.information_level = type::CLASS_LEVEL
t2_params.filename = search_path
t2_params.search_count = 10
t2_params.search_count = info_standard ? 255 : 10

find_first_request = set_find_params(find_first_request)

raw_response = client.send_recv(find_first_request)

if info_standard
return parse_find_first2_info_standard(raw_response, type)
end

Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FindInfoStandard/info_standard branch adds a separate parsing path that bypasses the existing FindFirst2Response parsing and changes request behavior (unicode flag off, search_count 255). spec/lib/ruby_smb/smb1/tree_spec.rb has good coverage for #list, but there are no examples for this new path; adding a spec that feeds a raw FIND_FIRST2 response with SMB_INFO_STANDARD entries would help prevent regressions (especially around edge cases like trailing null padding).

Copilot generated this review using guidance from organization custom instructions.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1845dac — new #list context in tree_spec.rb with six examples: sequential entries separated by the trailing-null pad Win9x inserts, single entry without padding, zero file_name_length sentinel, SMB-level failure, unicode-off + search_count=255 assertion, empty blob.

Comment thread lib/ruby_smb/smb1/tree.rb Outdated
Comment on lines +373 to +374
data_offset = raw_response[pb_offset + 14, 2].unpack1('v')
data_count = raw_response[pb_offset + 12, 2].unpack1('v')
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_find_first2_info_standard is extracting data_offset/data_count from the wrong positions in the TRANS2 response parameter block. In the TRANS2 Response parameter block layout (see lib/ruby_smb/smb1/packet/trans2/response.rb), data_count is at offset +14 and data_offset at +16 from the start of the parameter words; the current code reads +12/+14 (parameter_displacement/data_count), which will slice the wrong portion of the packet and can lead to incorrect parsing or exceptions.

Suggested change
data_offset = raw_response[pb_offset + 14, 2].unpack1('v')
data_count = raw_response[pb_offset + 12, 2].unpack1('v')
data_count = raw_response[pb_offset + 14, 2].unpack1('v')
data_offset = raw_response[pb_offset + 16, 2].unpack1('v')

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified incorrect. Trans2::Response::ParameterBlock lays out total_parameter_count(2) + total_data_count(2) + reserved(2) + parameter_count(2) + parameter_offset(2) + parameter_displacement(2) = 12 bytes before data_count, so the original +12 / +14 reads were right. Moot regardless — the whole byte-slicing path was removed in 1845dac (see #14).

Copy link
Copy Markdown
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs some work. There are still opaque DCERPC and NetBIOS structures and the BinData classes that are present are missing references to the structs that they represent and at least one is completely bypassed in favor of a bespoke parser.

I haven't gotten to testing it because this needs a bit more work but once things are in a better shape, I'll give it a whirl.

Comment thread lib/ruby_smb/client/negotiation.rb Outdated
Comment on lines +123 to +124
when RubySMB::SMB1::Packet::NegotiateResponse
# Non-extended security (e.g. Windows 95/98/ME, old Samba). The server provides
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can de-duplicate a bunch of code by collapsing this and just extracting the challenge you need depending on the type:

when RubySMB::SMB1::Packet::NegotiateResponse,
     RubySMB::SMB1::Packet::NegotiateResponseExtended
  self.smb1 = true
  self.smb2 = false
  self.smb3 = false
  self.signing_required = packet.parameter_block.security_mode.security_signatures_required == 1
  self.dialect = packet.negotiated_dialect.to_s
  # MaxBufferSize is largest message server will receive, measured from start of the SMB header.
  # Subtract 260 for protocol overhead so this value can be used directly as max read/write size.
  self.server_max_buffer_size = packet.parameter_block.max_buffer_size - 260
  self.negotiated_smb_version = 1
  self.session_encrypt_data = false
  if packet.is_a?(RubySMB::SMB1::Packet::NegotiateResponseExtended)
    self.negotiation_security_buffer = packet.data_block.security_blob
  else
    # Non-extended security (e.g. Windows 95/98/ME, old Samba). Server provides a raw
    # 8-byte challenge instead of a SPNEGO blob; store it so auth can compute LM/NTLM responses.
    @smb1_negotiate_challenge = packet.data_block.challenge.to_s
  end
  'SMB1'

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1845dac, applying your suggested structure verbatim.

Comment thread lib/ruby_smb/client/tree_connect.rb Outdated
request = RubySMB::SMB1::Packet::TreeConnectRequest.new
request.smb_header.tid = 65_535
if password
pass_bytes = password + "\x00"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL bytes need the #b method in Ruby to always be treated correctly.

Suggested change
pass_bytes = password + "\x00"
pass_bytes = password + "\x00".b

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1845dac.

module Packet
module Trans2
module FindInformationLevel
# SMB_INFO_STANDARD find result entry (LANMAN 2.0).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields here don't match the definition of SMB_INFO_STANDARD as provided by Microsoft. The first 6 fields look consistent, (overlooking the lack of SMB_DATE and SMB_TIME data types) but start to diverge after that.

It's possible they're correct, I haven't tested things yet but do you have a link you used to write this definition?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc you linked (3e6f3a13) is the SET_PATH/FILE_INFORMATION variant of SMB_INFO_STANDARD, which is a different structure from the FIND variant used by TRANS2_FIND_FIRST2/FIND_NEXT2. The FIND variant is at MS-CIFS 2.2.8.1.1. Its fields — CreationDate/Time, LastAccessDate/Time, LastWriteDate/Time, FileDataSize, AllocationSize, Attributes, FileNameLength, FileName — match our struct exactly. Link in the file updated to the correct doc in 1845dac.

module SMB1
module Packet
# A SMB1 SMB_COM_OPEN_ANDX Request Packet as defined in
# [2.2.4.41.1 Request](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb/0ab8c5c5-a8d1-4460-bab1-cdae4e18dab7)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link is 404'ed do you have another one to the resource you used?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 1845dac to MS-CIFS 2.2.4.41.1. WordCount 0x0F, 15 words: AndXCommand/Reserved/Offset, Flags, AccessMode, SearchAttrs, FileAttrs, CreationTime, OpenMode, AllocationSize, Timeout, Reserved[2] — matches our layout.

module SMB1
module Packet
# A SMB1 SMB_COM_OPEN_ANDX Response Packet as defined in
# [2.2.4.41.2 Response](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb/0ab8c5c5-a8d1-4460-bab1-cdae4e18dab7)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link is also a 404. Do you have the correct one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 1845dac to MS-CIFS 2.2.4.41.2. Field layout matches. Our server_fid(4) + reserved(2) occupies the spec's Reserved[3] = 6 bytes; could be renamed in a follow-up but the wire layout is correct.

Comment thread lib/ruby_smb/client.rb Outdated
raise unless resolved

dispatcher.tcp_socket.close rescue nil
new_sock = TCPSocket.new(host, port)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't going to work for Metasploit. Metasploit needs to handle the socket creation so it's always Rex::Socket otherwise things will break when pivoting. That complicates this entire method. Unless it's required right now, I'd recommend raising an exception and forcing the caller to sort it out themselves. That would be a good thing to separate into a different PR as well and just require the name be correct right now.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the separate branch netbios-name-resolution-fallback per #20. In that branch socket creation is injectable via Client#tcp_socket_factory — a callable (host, port) -> socket defaulting to stdlib TCPSocket.new for standalone use. Metasploit can inject a Rex::Socket::Tcp factory so pivoting keeps working; setsockopt is only called if the returned socket responds to it (Rex sockets don't always). Will open as a stacked PR once this one lands.

Comment thread lib/ruby_smb/client.rb Outdated
# @param host [String] the IP address to query
# @return [String, nil] the file server NetBIOS name
def netbios_lookup_nmblookup(host)
output = IO.popen(['nmblookup', '-A', host], err: :close, &:read)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, definitely not going to work. We need to be doing everything in Ruby.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully pure-Ruby in the split branch. The IO.popen(['nmblookup', …]) path (netbios_lookup_nmblookup) is gone. Replaced with RubySMB::Nbss::NodeStatus — a public helper that implements nmblookup -A equivalence via new Nbss::NodeStatusRequest / Nbss::NodeStatusResponse BinData structures. No external binaries, ever.

Comment thread lib/ruby_smb/client.rb Outdated
request << [0x00].pack('C')
request << [0x0021, 0x0001].pack('nn')

sock = UDPSocket.new
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here, the socket needs to come from Rex::Socket for Metasploit. I would highly recommend you avoid anything that requires you to create new sockets if it is remotely possible.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same injection pattern as #16Client#udp_socket_factory in the split branch. Defaults to stdlib UDPSocket.new; MSF callers pass a Rex::Socket::Udp.create factory so UDP probes respect pivots and routes. Verified end-to-end against a live Win95 target via the matching MSF module.

Comment thread lib/ruby_smb/client.rb Outdated
Comment on lines +796 to +806
raw_name = "*" + "\x00" * 15
encoded = raw_name.bytes.map { |b|
((b >> 4) + 0x41).chr + ((b & 0x0F) + 0x41).chr
}.join

request = [rand(0xFFFF)].pack('n')
request << [0x0000, 1, 0, 0, 0].pack('nnnnn')
request << [0x20].pack('C')
request << encoded
request << [0x00].pack('C')
request << [0x0021, 0x0001].pack('nn')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all need to use the existing definitions we have in lib/ruby_smb/nbss or add new ones.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the split branch. New lib/ruby_smb/nbss/node_status_request.rb and node_status_response.rb follow the existing NBSS pattern (session_request.rb, negative_session_response.rb). The UDP lookup is now ~10 lines of BinData glue; all the hand-crafted pack('n*') calls are gone.

Comment thread lib/ruby_smb/client.rb Outdated
Comment on lines +704 to +707
# When the name is '*SMBSERVER' and the server rejects it with
# "Called name not present", this method automatically looks up
# the server's actual NetBIOS name via a Node Status query,
# reconnects the TCP socket, and retries.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I highly recommend removing this automatic lookup functionality. It's going to significantly complicate this PR and should be a dedicated unit of work. If it's not absolutely essential to "Add(ing) Windows 95/98/ME SMB1 support" as the PR title states, it should be removed and moved into a separate PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 09c848c on this PR — all NetBIOS auto-lookup code is gone, branch is now strictly Win9x SMB1 support. The functionality is preserved on netbios-name-resolution-fallback, which will be opened as a stacked PR. It addresses your concerns #16/#18 (injectable Rex::Socket factories), #17 (pure Ruby, no nmblookup shell-out), and #19 (NBSS BinData structs) in one reviewable unit.

@smcintyre-r7 smcintyre-r7 self-assigned this Apr 15, 2026
@smcintyre-r7 smcintyre-r7 moved this to Waiting on Contributor in Metasploit Kanban Apr 15, 2026
Z6543 added 4 commits April 23, 2026 05:49
- 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
Per PR rapid7#294 review, the automatic NetBIOS name lookup and session-request
retry do not belong with the Win9x SMB1 support work — they open raw
sockets which breaks Metasploit's Rex::Socket pivoting, and need their
own review cycle. The functionality (including the NBNS BinData structs
and typed NetBiosSessionService error_code) moves to a dedicated branch.
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.
Copy link
Copy Markdown
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are looking pretty good now. I left a couple of comments and I can start testing this tomorrow. The larger outstanding concern is around https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/dbce00e7-68a1-41c6-982d-9483c902ad9b and the definition of fields we have as uint16 (e.g. SMB_FILE_ATTRIBUTES).

Comment thread lib/ruby_smb/smb1/packet/open_andx_request.rb Outdated
Comment thread lib/ruby_smb/smb1/packet/open_andx_response.rb Outdated
module SMB1
module Packet
# A SMB1 SMB_COM_OPEN_ANDX Response Packet as defined in
# [MS-CIFS 2.2.4.41.2 Response](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/dbce00e7-68a1-41c6-982d-9483c902ad9b)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields in here aren't aligned with the docs you're referencing here. For example :device_state is where I'd expect nmpipe_status to be. Also there's an SMB_NMPIPE_STATUS type defined that should be used instead of a uint if that's the correct field.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 650cf3a. The ParameterBlock is now aligned with MS-CIFS 2.2.4.41.2:

  • Renamed device_statenmpipe_status and typed it as smb_nmpipe_status (same bit_field the NT_CREATE_ANDX and TRANS2_OPEN2 responses use).
  • Renamed actionopen_results, data_sizefile_data_size, granted_accessaccess_rights, file_typeresource_type.
  • Typed last_write_time as utime.
  • Collapsed the misplaced server_fid(4B) + reserved(2B) pair into a single array :reserved, type: :uint16, initial_length: 3 matching the spec's Reserved[3].
  • Tree#build_open_andx_handle updated for the renames.

Kept :file_attributes as the name for the smb_file_attributes field to stay consistent with the suggestion you already accepted on the request side (cfddc48).

Comment thread lib/ruby_smb/client.rb Outdated
Comment on lines +639 to +649
# Enumerates shares using the RAP NetShareEnum call over `\PIPE\LANMAN`.
# This is the only share-enumeration method supported by Windows
# 95/98/ME and other LAN Manager-era servers that predate DCERPC/srvsvc.
# RAP is an SMB1 feature; callers negotiating SMB2/3 should use
# {#net_share_enum_all} instead.
#
# @param host [String] the server hostname or IP
# @param password [String, nil] share-level password for IPC$
# @return [Array<Hash>] each entry has :name (String) and :type (Integer)
# @raise [RubySMB::Error::RubySMBError] if SMB1 was not negotiated
def net_share_enum_rap(host, password: nil)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's hold off on adding this to the top level. It's unlikely to get a lot of usage given that 95/98/ME targets are only going to decline in popularity. While I know we already have a #net_share_enum_all method, most of the other methods are namespace'ed to the tree and not the client to keep the client itself on the thinner side.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 650cf3aClient#net_share_enum_rap is gone. Callers now tree-connect to IPC$ and use the tree-scoped Tree#net_share_enum (from the RubySMB::Rap::NetShareEnum mixin added earlier in this PR).

The Metasploit module that uses this (scanner/smb/cve_2000_0979) has been updated accordingly — it now does:

tree = simple.client.tree_connect("\\\\#{rhost}\\IPC$")
tree.net_share_enum.each { |s| ... }

Z6543 and others added 3 commits April 24, 2026 08:38
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.
Z6543 added a commit to Z6543/ruby_smb that referenced this pull request Apr 24, 2026
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.
Z6543 added a commit to Z6543/ruby_smb that referenced this pull request Apr 24, 2026
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.
Z6543 added a commit to Z6543/metasploit-framework that referenced this pull request Apr 24, 2026
RubySMB removed Client#net_share_enum_rap (rapid7/ruby_smb#294 review
comment r3134040652). Tree-connect to IPC\$ here and call the tree-scoped
helper directly, mirroring the pattern used for other SMB1 tree ops.
@smcintyre-r7
Copy link
Copy Markdown
Contributor

Alright things are looking pretty good. I spent this afternoon testing these cases with the following harness:

  • Windows ME SMB 1
  • Windows Server 2019 SMB 1
  • Windows Server 2019 SMB 2/3

I sent a PR to your branch to fix a couple of bugs. If you wouldn't mind testing that and landing it (assuming you find no issues with it) then we can advance this. At this point, with the bugs I found fixed, and assuming the CI tests pass, I'd be comfortable merging things.

Let me know if you have any questions, thanks for your work on this!

Test Harness
#!/usr/bin/ruby

# Demo script for PR #294 (Windows 95/98/ME SMB1 support).
#
# Exercises the new code paths added by the branch when run against a Win9x
# target, and verifies existing SMB1 + SMB2/3 behavior is unchanged when run
# against a modern host (e.g. Windows Server 2019 with SMB1 still enabled).
#
# New paths exercised:
#   1. SMB1 negotiate tolerates non-extended-security responses
#      (NegotiateResponse::DataBlock#do_read, byte_count == 8 case).
#   2. Client#parse_negotiate_response stashes the legacy 8-byte challenge
#      into @smb1_negotiate_challenge when extended security is absent, and
#      populates supports_nt_smbs from the capabilities word.
#   3. Client::Authentication#authenticate dispatches to
#      #smb1_legacy_authenticate (LM + NTLM response, no NTLMSSP) whenever a
#      legacy challenge was captured above.
#   4. Client::TreeConnect#smb1_tree_connect honors the `password:` keyword
#      for share-level auth, writing exactly password_length bytes (no trailing
#      null from the old BinData::Stringz).
#   5. TreeConnectResponse + SessionSetupLegacyResponse both tolerate short /
#      empty DataBlocks emitted by Win95/98.
#   6. Tree#open_file auto-dispatches SMB_COM_OPEN_ANDX (OpenAndxRequest /
#      OpenAndxResponse) when the server did not advertise NT SMBs.
#   7. Tree#list handles SMB_INFO_STANDARD results (FindInfoStandard) via the
#      sequential-entry parser in FindFirst2Response#results.
#   8. Tree#net_share_enum (RubySMB::Rap::NetShareEnum) issues the RAP
#      NetShareEnum transaction on \PIPE\LANMAN directly off the IPC$ tree.
#
# Regression coverage against Server 2019 (SMB1 + SMB2/3):
#   - SMB1 with extended security still negotiates and authenticates via
#     NTLMSSP; Tree#open_file still uses NT_CREATE_ANDX because
#     supports_nt_smbs is true.
#   - RAP share enumeration still works over SMB1 against a modern server.
#   - Directory listing with the default FindFileFullDirectoryInfo info level
#     still works.
#   - SMB2/3 path is untouched; same demo runs on port 445 with --no-smbv1.
#
# Example invocations:
#
#   # Windows ME over NetBIOS (139/tcp). Share-level auth, password "secret":
#   ruby examples/win9x_demo.rb \
#     --target 192.168.50.10 --port 139 \
#     --no-smbv2 --no-smbv3 \
#     --share-password secret \
#     --share SHARED --file README.TXT
#
#   # Windows Server 2019 with SMB1 still enabled, 445/tcp:
#   ruby examples/win9x_demo.rb \
#     --target 192.168.50.20 --port 445 \
#     --username Administrator --password 'P@ssw0rd!' \
#     --no-smbv2 --no-smbv3 \
#     --share C$ --file Windows\\System32\\drivers\\etc\\hosts
#
#   # Windows Server 2019 over SMB2/3 (regression check):
#   ruby examples/win9x_demo.rb \
#     --target 192.168.50.20 --port 445 \
#     --username Administrator --password 'P@ssw0rd!' \
#     --no-smbv1 \
#     --share C$ --file Windows\\System32\\drivers\\etc\\hosts

require 'bundler/setup'
require 'optparse'
require 'ruby_smb'

options = {
  target:            nil,
  port:              445,
  smbv1:             true,
  smbv2:             true,
  smbv3:             true,
  username:          '',
  password:          '',
  domain:            '.',
  share:             nil,
  share_password:    nil,
  file:              nil,
  session_request:   false,
  netbios_name:      '*SMBSERVER',
  read_bytes:        256
}

parser = OptionParser.new do |opts|
  opts.banner = "Usage: #{File.basename(__FILE__)} [options]"

  opts.on('--target HOST',          'Target IP or hostname (required)')        { |v| options[:target]    = v }
  opts.on('--port PORT', Integer,   "TCP port (default #{options[:port]}, use 139 for Win9x)") { |v| options[:port] = v }
  opts.on('--username USER',        'Account username (default: empty)')        { |v| options[:username] = v }
  opts.on('--password PASS',        'Account password (default: empty)')        { |v| options[:password] = v }
  opts.on('--domain DOMAIN',        'Account domain  (default: .)')             { |v| options[:domain]   = v }
  opts.on('--share NAME',           'Share name, e.g. SHARED or C$')            { |v| options[:share]    = v }
  opts.on('--share-password PASS',  'Share-level password (Win9x only)')        { |v| options[:share_password] = v }
  opts.on('--file PATH',            'File to open/read on the share')           { |v| options[:file]     = v }
  opts.on('--read-bytes N', Integer, "Max bytes to read from --file (default #{options[:read_bytes]})") { |v| options[:read_bytes] = v }
  opts.on('--[no-]smbv1',           "Enable SMBv1 (default: #{options[:smbv1]})")  { |v| options[:smbv1] = v }
  opts.on('--[no-]smbv2',           "Enable SMBv2 (default: #{options[:smbv2]})")  { |v| options[:smbv2] = v }
  opts.on('--[no-]smbv3',           "Enable SMBv3 (default: #{options[:smbv3]})")  { |v| options[:smbv3] = v }
  opts.on('--session-request',      'Send a NetBIOS session request before SMB negotiate (usually needed on 139)') { options[:session_request] = true }
  opts.on('--netbios-name NAME',    "Called NetBIOS name (default #{options[:netbios_name]})") { |v| options[:netbios_name] = v }
end
parser.parse!

abort(parser.help) if options[:target].nil?
options[:session_request] = true if options[:port] == 139 && !options[:session_request]

def banner(text)
  line = '=' * text.length
  puts "\n#{line}\n#{text}\n#{line}"
end

def info(label, value)
  printf "  %-28s %s\n", label + ':', value
end

def safely(label)
  yield
rescue => e
  puts "  [!] #{label} failed: #{e.class}: #{e.message}"
  nil
end

# Pretty-print a binary blob as offset / hex / ASCII rows. Used to dump the
# raw FIND_FIRST2 buffer when debugging Win9x's SMB_INFO_STANDARD layout.
def hexdump(bytes, indent: '  ')
  bytes = bytes.b
  bytes.bytes.each_slice(16).with_index do |row, i|
    hex   = row.map { |b| format('%02x', b) }.each_slice(8).map { |g| g.join(' ') }.join('  ')
    ascii = row.map { |b| (32..126).cover?(b) ? b.chr : '.' }.join
    printf "%s%08x  %-49s  |%s|\n", indent, i * 16, hex, ascii
  end
end

# Wrap client.send_recv so the next call's raw response is captured. Returns
# a [thunk_to_install, accessor_to_get_captured_bytes] pair. Caller is
# responsible for restoring the original method afterwards.
def capture_next_response(client)
  captured = nil
  original = client.method(:send_recv)
  client.define_singleton_method(:send_recv) do |*args, **kwargs|
    raw = original.call(*args, **kwargs)
    captured ||= raw.dup
    raw
  end
  [-> { client.singleton_class.send(:remove_method, :send_recv) }, -> { captured }]
end

# Same idea but also captures the request packet that was sent.
def capture_next_exchange(client)
  req = nil
  resp = nil
  original = client.method(:send_recv)
  client.define_singleton_method(:send_recv) do |packet, *args, **kwargs|
    req  ||= packet
    raw = original.call(packet, *args, **kwargs)
    resp ||= raw.dup
    raw
  end
  [-> { client.singleton_class.send(:remove_method, :send_recv) }, -> { [req, resp] }]
end

# Decode an SMB1 NT-status word as either an NT status name or DOS-style
# class.code, whichever fits.
def decode_smb1_status(raw)
  return '(no response)' if raw.nil? || raw.bytesize < 9
  status_dword = raw.byteslice(5, 4).unpack1('V')
  klass = status_dword & 0xff
  code  = (status_dword >> 16) & 0xffff
  nt    = WindowsError::NTStatus.find_by_retval(status_dword).first
  if nt
    "0x#{status_dword.to_s(16).rjust(8, '0')} (#{nt.name})"
  else
    "0x#{status_dword.to_s(16).rjust(8, '0')}  DOS-style: ErrorClass=0x#{klass.to_s(16)}  ErrorCode=0x#{code.to_s(16).rjust(4, '0')}"
  end
end

banner "Connecting to #{options[:target]}:#{options[:port]}"
sock = TCPSocket.new(options[:target], options[:port])
dispatcher = RubySMB::Dispatcher::Socket.new(sock, read_timeout: 30)

client = RubySMB::Client.new(
  dispatcher,
  smb1:     options[:smbv1],
  smb2:     options[:smbv2],
  smb3:     options[:smbv3],
  username: options[:username],
  password: options[:password],
  domain:   options[:domain]
)

if options[:session_request]
  banner "NetBIOS session request (called name: #{options[:netbios_name]})"
  client.session_request(options[:netbios_name])
  puts "  [+] NBSS session granted"
end

banner 'Negotiate'
protocol = client.negotiate
info 'Negotiated protocol', protocol
info 'Dialect',             client.dialect
info 'Max buffer size',     client.server_max_buffer_size
# #4 supports_nt_smbs: true on SMB2/3 and extended-security SMB1; false on Win9x SMB1.
info 'Supports NT SMBs',    client.supports_nt_smbs
legacy = client.instance_variable_get(:@smb1_negotiate_challenge)
if legacy
  info 'Legacy 8-byte challenge', legacy.unpack1('H*')
  puts '  [+] Non-extended-security path taken (Win9x/LANMAN style)'
else
  puts '  [+] Extended-security path taken (NTLMSSP available)'
end

banner 'Authenticate'
status = client.authenticate
info 'NT status', status
unless status == WindowsError::NTStatus::STATUS_SUCCESS
  abort("  [!] Authentication failed, aborting")
end
info 'Peer native OS',  client.peer_native_os
info 'Peer native LM',  client.peer_native_lm
info 'Primary domain',  client.primary_domain

# --- RAP share enumeration off the IPC$ tree -------------------------------
# Win9x does not support DCERPC/SRVSVC, so Tree#net_share_enum (mixed in from
# RubySMB::Rap::NetShareEnum) is the only path that works there. The same
# call should still succeed against Server 2019 with SMB1 enabled.
ipc_path = "\\\\#{options[:target]}\\IPC$"
banner "RAP NetShareEnum via #{ipc_path}"
if client.negotiated_smb_version == 1
  ipc_tree = safely('tree_connect IPC$') { client.tree_connect(ipc_path) }
  if ipc_tree
    shares = safely('tree.net_share_enum (RAP)') { ipc_tree.net_share_enum }
    if shares
      if shares.empty?
        puts '  (no shares reported)'
      else
        shares.each { |s| printf "  %-20s type=%s\n", s[:name], s[:type] }
      end
    end

    # Also exercise the DCERPC SRVSVC path. Expected to fail on Win9x and
    # succeed on modern Windows. Only the RAP path is new, but running both
    # side-by-side here makes regressions obvious.
    puts
    puts '  SRVSVC NetShareEnumAll (DCERPC, expected to fail on Win9x):'
    srv_shares = safely('net_share_enum_all (SRVSVC)') { client.net_share_enum_all(options[:target]) }
    if srv_shares
      srv_shares.each { |s| puts "    #{s.inspect}" }
    end

    safely('IPC$ tree disconnect') { ipc_tree.disconnect! }
  end
else
  puts "  [skip] Tree#net_share_enum is an SMB1 mixin (RAP over \\PIPE\\LANMAN);"
  puts "         negotiated #{client.dialect}. Using SRVSVC instead."
  srv_shares = safely('net_share_enum_all (SRVSVC)') { client.net_share_enum_all(options[:target]) }
  (srv_shares || []).each { |s| puts "    #{s.inspect}" }
end

# --- Tree-connect with share-level password --------------------------------
if options[:share]
  share_path = "\\\\#{options[:target]}\\#{options[:share]}"
  label = "tree_connect #{share_path}"
  label += " (password-protected)" if options[:share_password]
  banner label
  tree = safely('tree_connect') do
    client.tree_connect(share_path, password: options[:share_password])
  end

  if tree
    # --- list with the default (FindFileFullDirectoryInfo) info level ----
    # Skip on Win9x: the default info level (FindFileFullDirectoryInfo) is
    # an NT-only class. Win9x rejects it with STATUS_OS2_INVALID_LEVEL
    # (0x00060001), which is expected, not a regression.
    if client.supports_nt_smbs
      banner "list #{share_path} (default info level)"
      # SMB1::Tree#list wants directory: '\\' for the share root; SMB2::Tree#list
      # wants nil (it treats '\\' as a sub-directory name → STATUS_INVALID_PARAMETER).
      root_dir = client.negotiated_smb_version == 1 ? '\\' : nil
      default_listing = safely('tree.list(default)') { tree.list(directory: root_dir) }
      (default_listing || []).first(10).each do |e|
        name = e.file_name.to_s
        name = name.force_encoding('UTF-16LE').encode('UTF-8', invalid: :replace, undef: :replace) rescue name
        size = e.respond_to?(:end_of_file) ? e.end_of_file : '?'
        puts "  #{name.ljust(40)}  size=#{size}"
      end
      puts "  ... (#{default_listing.length} total)" if default_listing && default_listing.length > 10
    else
      banner "list #{share_path} (default info level)"
      puts '  [skip] server does not support NT SMBs; default info level (FindFileFullDirectoryInfo)'
      puts '         would be rejected with STATUS_OS2_INVALID_LEVEL. Falling through to'
      puts '         SMB_INFO_STANDARD below, which is the LANMAN-era equivalent.'
    end

    # --- list with SMB_INFO_STANDARD (the new FindInfoStandard class) ----
    # SMB1-only: FindInfoStandard is an SMB1 Trans2 FIND_INFO_LEVEL class,
    # and SMB1::Tree#list's `unicode:`/`type:` signature doesn't exist on
    # SMB2::Tree.
    banner "list #{share_path} (SMB_INFO_STANDARD)"
    info_std = RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindInfoStandard
    if client.negotiated_smb_version != 1
      puts "  [skip] SMB_INFO_STANDARD is an SMB1 info level; negotiated #{client.dialect}."
      raw = nil
      std_listing = nil
    else
      restore, captured = capture_next_response(client)
      std_listing = safely('tree.list(FindInfoStandard)') do
        tree.list(directory: '\\', type: info_std, unicode: false)
      end
      restore.call
      raw = captured.call
    end

    if std_listing && !std_listing.empty?
      std_listing.first(10).each do |e|
        name = e.file_name.to_s.force_encoding('ASCII-8BIT')
        puts "  #{name.ljust(40)}  size=#{e.data_size.to_i}  attrs=0x#{e.file_attributes.to_i.to_s(16).rjust(4, '0')}"
      end
      puts "  ... (#{std_listing.length} total)" if std_listing.length > 10
    elsif std_listing
      puts '  (parsed 0 entries; raw FIND_FIRST2 response below)'
    end

    if raw
      begin
        resp = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
        pb   = resp.parameter_block
        t2p  = resp.data_block.trans2_parameters
        puts
        puts '  --- FIND_FIRST2 response (parsed) ---'
        info 'raw response bytes',  raw.bytesize
        info 'word_count',          pb.word_count
        info 'NT status',           resp.status_code
        info 'parameter_count',     pb.parameter_count
        info 'parameter_offset',    pb.parameter_offset
        info 'data_count',          pb.data_count
        info 'data_offset',         pb.data_offset
        info 'byte_count abs off',  resp.data_block.byte_count.abs_offset
        info 'trans2_params bd off',resp.data_block.trans2_parameters.abs_offset
        info 'trans2_data bd off',  resp.data_block.trans2_data.abs_offset
        info 'BinData buffer size', resp.data_block.trans2_data.buffer.to_binary_s.bytesize
        info 'sid',                 format('0x%04x', t2p.sid)
        info 'search_count',        t2p.search_count
        info 'eos',                 t2p.eos
        info 'last_name_offset',    t2p.last_name_offset

        puts
        puts '  --- raw response, full hex dump ---'
        hexdump(raw)

        if pb.data_count.to_i > 0 && raw.bytesize >= pb.data_offset.to_i + pb.data_count.to_i
          buf = raw.byteslice(pb.data_offset.to_i, pb.data_count.to_i)
          puts
          puts "  --- trans2_data, sliced at server-reported offset #{pb.data_offset} (#{buf.bytesize} bytes) ---"
          hexdump(buf)

          # Try parsing the entries straight from the server-reported buffer,
          # bypassing BinData's data_block layout (which assumes 4-byte
          # alignment Win9x doesn't honor).
          puts
          puts '  --- entries parsed manually from the server-reported buffer ---'
          work = buf.dup
          idx = 0
          loop do
            break if work.empty?
            fi = info_std.new
            fi.read(work)
            consumed = fi.num_bytes
            break if consumed.zero? || consumed > work.bytesize
            work.slice!(0, consumed)
            break if fi.file_name_length.zero?
            puts "    [#{idx}] #{fi.file_name.to_s.inspect}  size=#{fi.data_size.to_i}  attrs=0x#{fi.file_attributes.to_i.to_s(16).rjust(4, '0')}"
            work.slice!(0, 1) if work.bytesize > 0 && work.getbyte(0) == 0
            idx += 1
            break if idx > 50
          end
          puts "    (#{idx} entries from #{buf.bytesize}-byte server buffer)"
        else
          puts '  (server returned an empty trans2_data buffer)'
        end
      rescue => e
        puts "  [!] failed to re-parse captured response: #{e.class}: #{e.message}"
        puts '  --- raw response bytes ---'
        hexdump(raw)
      end
    end

    # --- open + read a file. Auto-dispatches OPEN_ANDX on Win9x and -------
    #     NT_CREATE_ANDX on everything else (both end up returning an
    #     RubySMB::SMB1::File with a usable FID).
    if options[:file]
      banner "open_file + read #{options[:file]}"
      restore, captured = capture_next_exchange(client)
      file = safely('tree.open_file') do
        tree.open_file(filename: options[:file], read: true)
      end
      restore.call
      open_req, open_raw = captured.call

      if file
        cmd = case client.negotiated_smb_version
              when 1 then client.supports_nt_smbs ? 'NT_CREATE_ANDX' : 'OPEN_ANDX'
              else        'SMB2 CREATE'
              end
        info 'Wire command', cmd
        if file.respond_to?(:fid)
          info 'FID', format('0x%04x', file.fid)
        elsif file.respond_to?(:guid)
          info 'File GUID', file.guid.to_s.unpack1('H*')
        end
        info 'File size', file.size
        data = safely('file.read') { file.read(bytes: [options[:read_bytes], file.size.to_i].min) }
        if data
          preview = data.to_s[0, 120]
          puts "  preview: #{preview.inspect}"
        end
        safely('file.close') { file.close }
      else
        # Surface what we sent vs. the raw error so the failure is diagnosable.
        cmd = client.supports_nt_smbs ? 'NT_CREATE_ANDX' : 'OPEN_ANDX'
        puts
        puts "  --- #{cmd} request that was rejected ---"
        if open_req
          info 'request class',  open_req.class.name
          if open_req.respond_to?(:parameter_block)
            pb = open_req.parameter_block
            info 'access_mode',        format('0x%04x', pb.access_mode)        if pb.respond_to?(:access_mode)
            info 'search_attributes',  format('0x%04x', pb.search_attributes.to_binary_s.unpack1('v')) if pb.respond_to?(:search_attributes)
            info 'file_attributes',    format('0x%04x', pb.file_attributes.to_binary_s.unpack1('v'))   if pb.respond_to?(:file_attributes)
            info 'open_mode',          format('0x%04x', pb.open_mode)          if pb.respond_to?(:open_mode)
          end
          info 'request bytes',  open_req.to_binary_s.bytesize
          puts '  --- request hex ---'
          hexdump(open_req.to_binary_s)
        end
        if open_raw
          puts "  --- raw response (#{open_raw.bytesize} bytes), status #{decode_smb1_status(open_raw)} ---"
          hexdump(open_raw)
        end
      end
    end

    safely('tree.disconnect!') { tree.disconnect! }
  end
end

banner 'Disconnect'
client.disconnect!
puts '  [+] done'
Test Output
smcintyre@fedora:~/Repositories/ruby_smb.pr$ ruby examples/win9x_demo.rb --target 192.168.159.137 --port 139 --netbios-name ME --no-smbv2 --no-smbv3 --share SHARED --share-password secret --file HELLO.TXT
Source locally installed gems is ignoring #<Bundler::StubSpecification name=ed25519 version=1.3.0 platform=ruby> because it is missing extensions
Source locally installed gems is ignoring #<Bundler::StubSpecification name=debase version=3.0.12 platform=ruby> because it is missing extensions
Source locally installed gems is ignoring #<Bundler::StubSpecification name=bson version=5.0.2 platform=ruby> because it is missing extensions

=================================
Connecting to 192.168.159.137:139
=================================

=========================================
NetBIOS session request (called name: ME)
=========================================
  [+] NBSS session granted

=========
Negotiate
=========
  Negotiated protocol:         SMB1
  Dialect:                     NT LM 0.12
  Max buffer size:             2660
  Supports NT SMBs:            false
  Legacy 8-byte challenge:     0c000180b3d2eb69
  [+] Non-extended-security path taken (Win9x/LANMAN style)

============
Authenticate
============
  NT status:                   (0x00000000) STATUS_SUCCESS: The operation completed successfully.
  Peer native OS:              
  Peer native LM:              
  Primary domain:              

===========================================
RAP NetShareEnum via \\192.168.159.137\IPC$
===========================================
  SHARED               type=DISK
  IPC$                 type=IPC

  SRVSVC NetShareEnumAll (DCERPC, expected to fail on Win9x):
  [!] net_share_enum_all (SRVSVC) failed: RubySMB::Error::UnexpectedStatusCode: The server responded with an unexpected status code: 0x40002

==========================================================
tree_connect \\192.168.159.137\SHARED (password-protected)
==========================================================

==================================================
list \\192.168.159.137\SHARED (default info level)
==================================================
  [skip] server does not support NT SMBs; default info level (FindFileFullDirectoryInfo)
         would be rejected with STATUS_OS2_INVALID_LEVEL. Falling through to
         SMB_INFO_STANDARD below, which is the LANMAN-era equivalent.

=================================================
list \\192.168.159.137\SHARED (SMB_INFO_STANDARD)
=================================================
  .                                         size=0  attrs=0x0010
  ..                                        size=0  attrs=0x0010
  HELLO.TXT                                 size=22  attrs=0x0020

  --- FIND_FIRST2 response (parsed) ---
  raw response bytes:          150
  word_count:                  10
  NT status:                   (0x00000000) STATUS_SUCCESS: The operation completed successfully.
  parameter_count:             10
  parameter_offset:            55
  data_count:                  84
  data_offset:                 66
  byte_count abs off:          53
  trans2_params bd off:        56
  trans2_data bd off:          68
  BinData buffer size:         82
  sid:                         0x0300
  search_count:                256
  eos:                         0
  last_name_offset:            0

  --- raw response, full hex dump ---
  00000000  ff 53 4d 42 32 00 00 00  00 98 03 60 00 00 00 00   |.SMB2......`....|
  00000010  00 00 00 00 00 00 00 00  00 c8 bc a0 00 00 00 00   |................|
  00000020  0a 0a 00 54 00 00 00 0a  00 37 00 00 00 54 00 42   |...T.....7...T.B|
  00000030  00 00 00 00 00 5f 00 00  00 03 00 01 00 00 00 4a   |....._.........J|
  00000040  00 00 98 5c 38 70 98 5c  00 00 98 5c 39 70 00 00   |...\8p.\...\9p..|
  00000050  00 00 00 00 00 00 10 00  01 2e 00 98 5c 38 70 98   |............\8p.|
  00000060  5c 00 00 98 5c 39 70 00  00 00 00 00 00 00 00 10   |\...\9p.........|
  00000070  00 02 2e 2e 00 98 5c 40  70 98 5c 00 00 98 5c 4c   |......\@p.\...\L|
  00000080  70 16 00 00 00 16 00 00  00 20 00 09 48 45 4c 4c   |p........ ..HELL|
  00000090  4f 2e 54 58 54 00                                  |O.TXT.|

  --- trans2_data, sliced at server-reported offset 66 (84 bytes) ---
  00000000  98 5c 38 70 98 5c 00 00  98 5c 39 70 00 00 00 00   |.\8p.\...\9p....|
  00000010  00 00 00 00 10 00 01 2e  00 98 5c 38 70 98 5c 00   |..........\8p.\.|
  00000020  00 98 5c 39 70 00 00 00  00 00 00 00 00 10 00 02   |..\9p...........|
  00000030  2e 2e 00 98 5c 40 70 98  5c 00 00 98 5c 4c 70 16   |....\@p.\...\Lp.|
  00000040  00 00 00 16 00 00 00 20  00 09 48 45 4c 4c 4f 2e   |....... ..HELLO.|
  00000050  54 58 54 00                                        |TXT.|

  --- entries parsed manually from the server-reported buffer ---
    [0] "."  size=0  attrs=0x0010
    [1] ".."  size=0  attrs=0x0010
    [2] "HELLO.TXT"  size=22  attrs=0x0020
    (3 entries from 84-byte server buffer)

==========================
open_file + read HELLO.TXT
==========================
  Wire command:                OPEN_ANDX
  FID:                         0x0014
  File size:                   22
  preview: "flag{me-is-really-old}"

==========
Disconnect
==========
  [+] done
smcintyre@fedora:~/Repositories/ruby_smb.pr$ ruby examples/win9x_demo.rb --target 192.168.159.10 --port 445 --username smcintyre --password 'Password1!' --no-smbv2 --no-smbv3 --share C$ --file 'Windows\\System32\\drivers\\etc\\hosts'
Source locally installed gems is ignoring #<Bundler::StubSpecification name=ed25519 version=1.3.0 platform=ruby> because it is missing extensions
Source locally installed gems is ignoring #<Bundler::StubSpecification name=debase version=3.0.12 platform=ruby> because it is missing extensions
Source locally installed gems is ignoring #<Bundler::StubSpecification name=bson version=5.0.2 platform=ruby> because it is missing extensions

================================
Connecting to 192.168.159.10:445
================================

=========
Negotiate
=========
  Negotiated protocol:         SMB1
  Dialect:                     NT LM 0.12
  Max buffer size:             16384
  Supports NT SMBs:            true
  [+] Extended-security path taken (NTLMSSP available)

============
Authenticate
============
  NT status:                   (0x00000000) STATUS_SUCCESS: The operation completed successfully.
  Peer native OS:              Windows Server 2019 Standard 17763
  Peer native LM:              Windows Server 2019 Standard 6.3
  Primary domain:              

==========================================
RAP NetShareEnum via \\192.168.159.10\IPC$
==========================================
  [!] tree.net_share_enum (RAP) failed: RubySMB::Error::UnexpectedStatusCode: The server responded with an unexpected status code: STATUS_NOT_SUPPORTED

  SRVSVC NetShareEnumAll (DCERPC, expected to fail on Win9x):
    {:name=>"ADMIN$", :type=>"DISK|SPECIAL", :comment=>"Remote Admin"}
    {:name=>"C$", :type=>"DISK|SPECIAL", :comment=>"Default share"}
    {:name=>"CertEnroll", :type=>"DISK", :comment=>"Active Directory Certificate Services share"}
    {:name=>"IPC$", :type=>"IPC|SPECIAL", :comment=>"Remote IPC"}
    {:name=>"NETLOGON", :type=>"DISK", :comment=>"Logon server share "}
    {:name=>"Public", :type=>"DISK", :comment=>""}
    {:name=>"SYSVOL", :type=>"DISK", :comment=>"Logon server share "}
    {:name=>"Users", :type=>"DISK", :comment=>""}

================================
tree_connect \\192.168.159.10\C$
================================

=============================================
list \\192.168.159.10\C$ (default info level)
=============================================
  $Recycle.Bin                              size=0
  Documents and Settings                    size=0
  inetpub                                   size=0
  ofbiz                                     size=0
  pagefile.sys                              size=738197504
  PerfLogs                                  size=0
  Program Files                             size=0
  Program Files (x86)                       size=0
  ProgramData                               size=0
  Python27                                  size=0
  ... (15 total)

============================================
list \\192.168.159.10\C$ (SMB_INFO_STANDARD)
============================================
  $Recycle.Bin                              size=0  attrs=0x0016
  Documents and Settings                    size=0  attrs=0x0016
  inetpub                                   size=0  attrs=0x0010
  ofbiz                                     size=0  attrs=0x0010
  pagefile.sys                              size=738197504  attrs=0x0026
  PerfLogs                                  size=0  attrs=0x0010
  Program Files                             size=0  attrs=0x0011
  Program Files (x86)                       size=0  attrs=0x0010
  ProgramData                               size=0  attrs=0x0012
  Python27                                  size=0  attrs=0x0010
  ... (15 total)

  --- FIND_FIRST2 response (parsed) ---
  raw response bytes:          600
  word_count:                  10
  NT status:                   (0x00000000) STATUS_SUCCESS: The operation completed successfully.
  parameter_count:             10
  parameter_offset:            56
  data_count:                  532
  data_offset:                 68
  byte_count abs off:          53
  trans2_params bd off:        56
  trans2_data bd off:          68
  BinData buffer size:         532
  sid:                         0x0801
  search_count:                15
  eos:                         1
  last_name_offset:            501

  --- raw response, full hex dump ---
  00000000  ff 53 4d 42 32 00 00 00  00 98 03 60 00 00 00 00   |.SMB2......`....|
  00000010  00 00 00 00 00 00 00 00  02 08 f1 da 00 08 00 00   |................|
  00000020  0a 0a 00 14 02 00 00 0a  00 38 00 00 00 14 02 44   |.........8.....D|
  00000030  00 00 00 00 00 21 02 00  01 08 0f 00 01 00 00 00   |.....!..........|
  00000040  f5 01 00 00 2f 4d 61 1a  2f 4d 61 1a 2f 4d 61 1a   |..../Ma./Ma./Ma.|
  00000050  00 00 00 00 00 00 00 00  16 00 0c 24 52 65 63 79   |...........$Recy|
  00000060  63 6c 65 2e 42 69 6e 00  ad 58 9c 5a ad 58 9c 5a   |cle.Bin..X.Z.X.Z|
  00000070  ad 58 9c 5a 00 00 00 00  00 00 00 00 16 00 16 44   |.X.Z...........D|
  00000080  6f 63 75 6d 65 6e 74 73  20 61 6e 64 20 53 65 74   |ocuments and Set|
  00000090  74 69 6e 67 73 00 16 5b  96 75 16 5b 96 75 16 5b   |tings..[.u.[.u.[|
  000000a0  96 75 00 00 00 00 00 00  00 00 10 00 07 69 6e 65   |.u...........ine|
  000000b0  74 70 75 62 00 cb 58 ad  88 cb 58 5c 89 cb 58 5c   |tpub..X...X\..X\|
  000000c0  89 00 00 00 00 00 00 00  00 10 00 05 6f 66 62 69   |............ofbi|
  000000d0  7a 00 ad 58 55 72 98 5c  33 7a 98 5c 33 7a 00 00   |z..XUr.\3z.\3z..|
  000000e0  00 2c 00 00 00 2c 26 00  0c 70 61 67 65 66 69 6c   |.,...,&..pagefil|
  000000f0  65 2e 73 79 73 00 2f 4d  61 1a 05 53 6d 7c 05 53   |e.sys./Ma..Sm|.S|
  00000100  6d 7c 00 00 00 00 00 00  00 00 10 00 08 50 65 72   |m|...........Per|
  00000110  66 4c 6f 67 73 00 2f 4d  61 1a 6d 5b dd 82 6d 5b   |fLogs./Ma.m[..m[|
  00000120  dd 82 00 00 00 00 00 00  00 00 11 00 0d 50 72 6f   |.............Pro|
  00000130  67 72 61 6d 20 46 69 6c  65 73 00 2f 4d 61 1a 1c   |gram Files./Ma..|
  00000140  5b 81 7e 1c 5b 81 7e 00  00 00 00 00 00 00 00 10   |[.~.[.~.........|
  00000150  00 13 50 72 6f 67 72 61  6d 20 46 69 6c 65 73 20   |..Program Files |
  00000160  28 78 38 36 29 00 2f 4d  61 1a 96 5c 00 6f 96 5c   |(x86)./Ma..\.o.\|
  00000170  00 6f 00 00 00 00 00 00  00 00 12 00 0b 50 72 6f   |.o...........Pro|
  00000180  67 72 61 6d 44 61 74 61  00 1c 5b 8a 7e 1c 5b 8d   |gramData..[.~.[.|
  00000190  7e 1c 5b 8d 7e 00 00 00  00 00 00 00 00 10 00 08   |~.[.~...........|
  000001a0  50 79 74 68 6f 6e 32 37  00 ad 58 60 72 ad 58 9c   |Python27..X`r.X.|
  000001b0  5a ad 58 9c 5a 00 00 00  00 00 00 00 00 16 00 08   |Z.X.Z...........|
  000001c0  52 65 63 6f 76 65 72 79  00 7e 5c f4 70 7e 5c 29   |Recovery.~\.p~\)|
  000001d0  71 7e 5c 29 71 00 00 00  00 00 00 00 00 10 00 0a   |q~\)q...........|
  000001e0  52 75 62 79 33 34 2d 78  36 34 00 ad 58 55 72 ad   |Ruby34-x64..XUr.|
  000001f0  58 2b 60 ad 58 2b 60 00  00 00 00 00 00 00 00 16   |X+`.X+`.........|
  00000200  00 19 53 79 73 74 65 6d  20 56 6f 6c 75 6d 65 20   |..System Volume |
  00000210  49 6e 66 6f 72 6d 61 74  69 6f 6e 00 2f 4d 2e 11   |Information./M..|
  00000220  16 5b a2 75 16 5b a2 75  00 00 00 00 00 00 00 00   |.[.u.[.u........|
  00000230  11 00 05 55 73 65 72 73  00 2f 4d 2e 11 8f 5c 49   |...Users./M...\I|
  00000240  5b 8f 5c 49 5b 00 00 00  00 00 00 00 00 10 00 07   |[.\I[...........|
  00000250  57 69 6e 64 6f 77 73 00                            |Windows.|

  --- trans2_data, sliced at server-reported offset 68 (532 bytes) ---
  00000000  2f 4d 61 1a 2f 4d 61 1a  2f 4d 61 1a 00 00 00 00   |/Ma./Ma./Ma.....|
  00000010  00 00 00 00 16 00 0c 24  52 65 63 79 63 6c 65 2e   |.......$Recycle.|
  00000020  42 69 6e 00 ad 58 9c 5a  ad 58 9c 5a ad 58 9c 5a   |Bin..X.Z.X.Z.X.Z|
  00000030  00 00 00 00 00 00 00 00  16 00 16 44 6f 63 75 6d   |...........Docum|
  00000040  65 6e 74 73 20 61 6e 64  20 53 65 74 74 69 6e 67   |ents and Setting|
  00000050  73 00 16 5b 96 75 16 5b  96 75 16 5b 96 75 00 00   |s..[.u.[.u.[.u..|
  00000060  00 00 00 00 00 00 10 00  07 69 6e 65 74 70 75 62   |.........inetpub|
  00000070  00 cb 58 ad 88 cb 58 5c  89 cb 58 5c 89 00 00 00   |..X...X\..X\....|
  00000080  00 00 00 00 00 10 00 05  6f 66 62 69 7a 00 ad 58   |........ofbiz..X|
  00000090  55 72 98 5c 33 7a 98 5c  33 7a 00 00 00 2c 00 00   |Ur.\3z.\3z...,..|
  000000a0  00 2c 26 00 0c 70 61 67  65 66 69 6c 65 2e 73 79   |.,&..pagefile.sy|
  000000b0  73 00 2f 4d 61 1a 05 53  6d 7c 05 53 6d 7c 00 00   |s./Ma..Sm|.Sm|..|
  000000c0  00 00 00 00 00 00 10 00  08 50 65 72 66 4c 6f 67   |.........PerfLog|
  000000d0  73 00 2f 4d 61 1a 6d 5b  dd 82 6d 5b dd 82 00 00   |s./Ma.m[..m[....|
  000000e0  00 00 00 00 00 00 11 00  0d 50 72 6f 67 72 61 6d   |.........Program|
  000000f0  20 46 69 6c 65 73 00 2f  4d 61 1a 1c 5b 81 7e 1c   | Files./Ma..[.~.|
  00000100  5b 81 7e 00 00 00 00 00  00 00 00 10 00 13 50 72   |[.~...........Pr|
  00000110  6f 67 72 61 6d 20 46 69  6c 65 73 20 28 78 38 36   |ogram Files (x86|
  00000120  29 00 2f 4d 61 1a 96 5c  00 6f 96 5c 00 6f 00 00   |)./Ma..\.o.\.o..|
  00000130  00 00 00 00 00 00 12 00  0b 50 72 6f 67 72 61 6d   |.........Program|
  00000140  44 61 74 61 00 1c 5b 8a  7e 1c 5b 8d 7e 1c 5b 8d   |Data..[.~.[.~.[.|
  00000150  7e 00 00 00 00 00 00 00  00 10 00 08 50 79 74 68   |~...........Pyth|
  00000160  6f 6e 32 37 00 ad 58 60  72 ad 58 9c 5a ad 58 9c   |on27..X`r.X.Z.X.|
  00000170  5a 00 00 00 00 00 00 00  00 16 00 08 52 65 63 6f   |Z...........Reco|
  00000180  76 65 72 79 00 7e 5c f4  70 7e 5c 29 71 7e 5c 29   |very.~\.p~\)q~\)|
  00000190  71 00 00 00 00 00 00 00  00 10 00 0a 52 75 62 79   |q...........Ruby|
  000001a0  33 34 2d 78 36 34 00 ad  58 55 72 ad 58 2b 60 ad   |34-x64..XUr.X+`.|
  000001b0  58 2b 60 00 00 00 00 00  00 00 00 16 00 19 53 79   |X+`...........Sy|
  000001c0  73 74 65 6d 20 56 6f 6c  75 6d 65 20 49 6e 66 6f   |stem Volume Info|
  000001d0  72 6d 61 74 69 6f 6e 00  2f 4d 2e 11 16 5b a2 75   |rmation./M...[.u|
  000001e0  16 5b a2 75 00 00 00 00  00 00 00 00 11 00 05 55   |.[.u...........U|
  000001f0  73 65 72 73 00 2f 4d 2e  11 8f 5c 49 5b 8f 5c 49   |sers./M...\I[.\I|
  00000200  5b 00 00 00 00 00 00 00  00 10 00 07 57 69 6e 64   |[...........Wind|
  00000210  6f 77 73 00                                        |ows.|

  --- entries parsed manually from the server-reported buffer ---
    [0] "$Recycle.Bin"  size=0  attrs=0x0016
    [1] "Documents and Settings"  size=0  attrs=0x0016
    [2] "inetpub"  size=0  attrs=0x0010
    [3] "ofbiz"  size=0  attrs=0x0010
    [4] "pagefile.sys"  size=738197504  attrs=0x0026
    [5] "PerfLogs"  size=0  attrs=0x0010
    [6] "Program Files"  size=0  attrs=0x0011
    [7] "Program Files (x86)"  size=0  attrs=0x0010
    [8] "ProgramData"  size=0  attrs=0x0012
    [9] "Python27"  size=0  attrs=0x0010
    [10] "Recovery"  size=0  attrs=0x0016
    [11] "Ruby34-x64"  size=0  attrs=0x0010
    [12] "System Volume Information"  size=0  attrs=0x0016
    [13] "Users"  size=0  attrs=0x0011
    [14] "Windows"  size=0  attrs=0x0010
    (15 entries from 532-byte server buffer)

=======================================================
open_file + read Windows\\System32\\drivers\\etc\\hosts
=======================================================
  Wire command:                NT_CREATE_ANDX
  FID:                         0x4001
  File size:                   824
  preview: "# Copyright (c) 1993-2009 Microsoft Corp.\r\n#\r\n# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.\r\n#\r\n# "

==========
Disconnect
==========
  [+] done
smcintyre@fedora:~/Repositories/ruby_smb.pr$ ruby examples/win9x_demo.rb --target 192.168.159.10 --port 445 --username smcintyre --password 'Password1!' --no-smbv1 --share C$ --file 'Windows\\System32\\drivers\\etc\\hosts'
Source locally installed gems is ignoring #<Bundler::StubSpecification name=ed25519 version=1.3.0 platform=ruby> because it is missing extensions
Source locally installed gems is ignoring #<Bundler::StubSpecification name=debase version=3.0.12 platform=ruby> because it is missing extensions
Source locally installed gems is ignoring #<Bundler::StubSpecification name=bson version=5.0.2 platform=ruby> because it is missing extensions

================================
Connecting to 192.168.159.10:445
================================

=========
Negotiate
=========
  Negotiated protocol:         SMB3
  Dialect:                     0x0311
  Max buffer size:             8388608
  Supports NT SMBs:            true
  [+] Extended-security path taken (NTLMSSP available)

============
Authenticate
============
  NT status:                   (0x00000000) STATUS_SUCCESS: The operation completed successfully.
  Peer native OS:              
  Peer native LM:              
  Primary domain:              

==========================================
RAP NetShareEnum via \\192.168.159.10\IPC$
==========================================
  [skip] Tree#net_share_enum is an SMB1 mixin (RAP over \PIPE\LANMAN);
         negotiated 0x0311. Using SRVSVC instead.
    {:name=>"ADMIN$", :type=>"DISK|SPECIAL", :comment=>"Remote Admin"}
    {:name=>"C$", :type=>"DISK|SPECIAL", :comment=>"Default share"}
    {:name=>"CertEnroll", :type=>"DISK", :comment=>"Active Directory Certificate Services share"}
    {:name=>"IPC$", :type=>"IPC|SPECIAL", :comment=>"Remote IPC"}
    {:name=>"NETLOGON", :type=>"DISK", :comment=>"Logon server share "}
    {:name=>"Public", :type=>"DISK", :comment=>""}
    {:name=>"SYSVOL", :type=>"DISK", :comment=>"Logon server share "}
    {:name=>"Users", :type=>"DISK", :comment=>""}

================================
tree_connect \\192.168.159.10\C$
================================

=============================================
list \\192.168.159.10\C$ (default info level)
=============================================
  $Recycle.Bin                              size=0
  Documents and Settings                    size=0
  inetpub                                   size=0
  ofbiz                                     size=0
  pagefile.sys                              size=738197504
  PerfLogs                                  size=0
  Program Files                             size=0
  Program Files (x86)                       size=0
  ProgramData                               size=0
  Python27                                  size=0
  ... (15 total)

============================================
list \\192.168.159.10\C$ (SMB_INFO_STANDARD)
============================================
  [skip] SMB_INFO_STANDARD is an SMB1 info level; negotiated 0x0311.

=======================================================
open_file + read Windows\\System32\\drivers\\etc\\hosts
=======================================================
  Wire command:                SMB2 CREATE
  File GUID:                   7b3a70657273697374656e743d3e3438353333313330343439392c203a766f6c6174696c653d3e3438353333313330343435377d
  File size:                   824
  preview: "# Copyright (c) 1993-2009 Microsoft Corp.\r\n#\r\n# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.\r\n#\r\n# "

==========
Disconnect
==========
  [+] done
smcintyre@fedora:~/Repositories/ruby_smb.pr$ 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting on Contributor

Development

Successfully merging this pull request may close these issues.

SMB1 incompatible with Windows 95/98/ME share-level auth and legacy protocols

3 participants