Skip to content

Add Windows 95/98/ME SMB1 support#294

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

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

Conversation

@Z6543
Copy link

@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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant