Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ruby_smb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module RubySMB
require 'ruby_smb/dispatcher'
require 'ruby_smb/version'
require 'ruby_smb/smb2'
require 'ruby_smb/rap'
require 'ruby_smb/smb1'
require 'ruby_smb/client'
require 'ruby_smb/crypto'
Expand Down
21 changes: 16 additions & 5 deletions lib/ruby_smb/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,14 @@ class Client
# @return [String] The raw security buffer bytes
attr_accessor :negotiation_security_buffer

# Whether the negotiated SMB1 server supports the "NT SMBs" capability
# (i.e. SMB_COM_NT_CREATE_ANDX). Set false for Windows 95/98/ME and other
# LAN Manager-era servers, which must use SMB_COM_OPEN_ANDX instead.
# Always true for SMB2/3.
# @!attribute [rw] supports_nt_smbs
# @return [Boolean]
attr_accessor :server_supports_nt_smbs

# @param dispatcher [RubySMB::Dispatcher::Socket] the packet dispatcher to use
# @param smb1 [Boolean] whether or not to enable SMB1 support
# @param smb2 [Boolean] whether or not to enable SMB2 support
Expand Down Expand Up @@ -338,10 +346,11 @@ def initialize(dispatcher, smb1: true, smb2: true, smb3: true, username:, passwo
@max_buffer_size = MAX_BUFFER_SIZE
# These sizes will be modified during negotiation
@server_max_buffer_size = SERVER_MAX_BUFFER_SIZE
@server_max_read_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_max_write_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_max_read_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_max_write_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_max_transact_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_supports_multi_credit = false
@server_supports_nt_smbs = true

# SMB 3.x options
# this merely initializes the default value for session encryption, it may be changed as necessary when a
Expand Down Expand Up @@ -603,13 +612,15 @@ def recv_packet(encrypt: false)
# Connects to the supplied share
#
# @param share [String] the path to the share in `\\server\share_name` format
# @param password [String, nil] share-level password (SMB1 only, for
# servers using share-level auth such as Windows 95/98/ME)
# @return [RubySMB::SMB1::Tree] if talking over SMB1
# @return [RubySMB::SMB2::Tree] if talking over SMB2
def tree_connect(share)
def tree_connect(share, password: nil)
connected_tree = if smb2 || smb3
smb2_tree_connect(share)
else
smb1_tree_connect(share)
smb1_tree_connect(share, password: password)
end
@tree_connects << connected_tree
connected_tree
Expand Down Expand Up @@ -672,7 +683,7 @@ def session_request(name = '*SMBSERVER')
# @return [RubySMB::Nbss::SessionRequest] the SessionRequest packet
def session_request_packet(name = '*SMBSERVER')
called_name = "#{name.upcase.ljust(15)}\x20"
calling_name = "#{''.ljust(15)}\x00"
calling_name = "#{@local_workstation.upcase.ljust(15)}\x00"

session_request = RubySMB::Nbss::SessionRequest.new
session_request.session_header.session_packet_type = RubySMB::Nbss::SESSION_REQUEST
Expand Down
53 changes: 53 additions & 0 deletions lib/ruby_smb/client/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def authenticate
if smb1
if username.empty? && password.empty?
smb1_anonymous_auth
elsif @smb1_negotiate_challenge
# Non-extended security negotiated (e.g. Windows 95/98). Use legacy
# LM/NTLM challenge-response rather than NTLMSSP.
smb1_legacy_authenticate
else
smb1_authenticate
end
Expand Down Expand Up @@ -198,6 +202,55 @@ def smb1_type2_message(response_packet)
[type2_blob].pack('m')
end

# Handles SMB1 authentication against servers that negotiated non-extended
# (legacy) security — Windows 95/98/ME and old Samba builds. These hosts
# provide a raw 8-byte challenge in the Negotiate response and expect
# LM + NTLM hash responses in SessionSetupLegacyRequest.
def smb1_legacy_authenticate
challenge = @smb1_negotiate_challenge
lm_hash = Net::NTLM.lm_hash(@password)
ntlm_hash = Net::NTLM.ntlm_hash(@password)
lm_resp = Net::NTLM.lm_response(lm_hash: lm_hash, challenge: challenge)
ntlm_resp = Net::NTLM.ntlm_response(ntlm_hash: ntlm_hash, challenge: challenge)

packet = smb1_legacy_auth_request(lm_resp, ntlm_resp)
raw_response = send_recv(packet)
response = smb1_legacy_auth_response(raw_response)
response_code = response.status_code

if response_code == WindowsError::NTStatus::STATUS_SUCCESS
self.user_id = response.smb_header.uid
self.peer_native_os = response.data_block.native_os.to_s
self.peer_native_lm = response.data_block.native_lan_man.to_s
self.primary_domain = response.data_block.primary_domain.to_s
end

response_code
end

def smb1_legacy_auth_request(lm_response, ntlm_response)
packet = RubySMB::SMB1::Packet::SessionSetupLegacyRequest.new
packet.parameter_block.max_buffer_size = self.max_buffer_size
packet.parameter_block.max_mpx_count = 50
packet.data_block.oem_password = lm_response
packet.data_block.unicode_password = ntlm_response
packet.data_block.account_name = @username.encode('ASCII', invalid: :replace, undef: :replace)
packet.data_block.primary_domain = @domain.encode('ASCII', invalid: :replace, undef: :replace)
packet
end

def smb1_legacy_auth_response(raw_response)
packet = RubySMB::SMB1::Packet::SessionSetupLegacyResponse.read(raw_response)
unless packet.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::SessionSetupLegacyResponse::COMMAND,
packet: packet
)
end
packet
end

#
# SMB 2 Methods
#
Expand Down
12 changes: 10 additions & 2 deletions lib/ruby_smb/client/negotiation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ def negotiate_response(raw_data)
# @return [String] The SMB version as a string ('SMB1', 'SMB2')
def parse_negotiate_response(packet)
case packet
when RubySMB::SMB1::Packet::NegotiateResponseExtended
when RubySMB::SMB1::Packet::NegotiateResponse,
RubySMB::SMB1::Packet::NegotiateResponseExtended
self.smb1 = true
self.smb2 = false
self.smb3 = false
Expand All @@ -118,7 +119,14 @@ def parse_negotiate_response(packet)
self.server_max_buffer_size = packet.parameter_block.max_buffer_size - 260
self.negotiated_smb_version = 1
self.session_encrypt_data = false
self.negotiation_security_buffer = packet.data_block.security_blob
self.server_supports_nt_smbs = packet.parameter_block.capabilities.nt_smbs != 0
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'
when RubySMB::SMB2::Packet::NegotiateResponse
self.smb1 = false
Expand Down
9 changes: 8 additions & 1 deletion lib/ruby_smb/client/tree_connect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ module TreeConnect
# {RubySMB::SMB1::Tree}
#
# @param share [String] the share path to connect to
# @param password [String, nil] share-level password for servers using
# share-level authentication (e.g. Windows 95/98/ME)
# @return [RubySMB::SMB1::Tree] the connected Tree
def smb1_tree_connect(share)
def smb1_tree_connect(share, password: nil)
request = RubySMB::SMB1::Packet::TreeConnectRequest.new
request.smb_header.tid = 65_535
if password
pass_bytes = password + "\x00".b
request.parameter_block.password_length = pass_bytes.length
request.data_block.password = pass_bytes
end
request.data_block.path = share
raw_response = send_recv(request)
response = RubySMB::SMB1::Packet::TreeConnectResponse.read(raw_response)
Expand Down
10 changes: 10 additions & 0 deletions lib/ruby_smb/rap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module RubySMB
# Remote Administration Protocol (RAP), as defined in [MS-RAP]
# (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rap/).
# RAP is the LAN Manager remote-administration API, carried over the
# `\PIPE\LANMAN` named pipe using SMB1 SMB_COM_TRANSACTION. It is the only
# share-enumeration path supported by pre-NT servers (e.g. Windows 95/98/ME).
module Rap
require 'ruby_smb/rap/net_share_enum'
end
end
166 changes: 166 additions & 0 deletions lib/ruby_smb/rap/net_share_enum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
module RubySMB
module Rap
# NetShareEnum (RAP opcode 0), as defined in [MS-RAP 3.3.4.1](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rap/48dd86d2-4092-49a4-9024-308f0ed77520).
# Carried over `\PIPE\LANMAN` using SMB_COM_TRANSACTION. Request parameters
# describe the shape of the data the server returns; response parameters
# carry the RAP status, entry count, and buffer sizing hint, and the
# response data block is an array of `share_info_1` records.
module NetShareEnum
OPCODE = 0

# Parameter descriptor for the RAP call itself: (W)ord info level,
# (r)eturn buffer pointer, (L)ength hint, (e)ntry count, (h)andle.
PARAM_DESCRIPTOR = 'WrLeh'.freeze

# Data descriptor for `share_info_1`: (B)13 name, (B)yte pad, (W)ord type,
# (z) pointer to remark. See MS-RAP 3.2.4 for descriptor syntax.
DATA_DESCRIPTOR_LEVEL_1 = 'B13BWz'.freeze

# Default server receive-buffer size.
DEFAULT_RECEIVE_BUFFER_SIZE = 0x1000

# Share type codes carried in the low bits of `share_info_1.shi1_type`
# per MS-RAP 2.5.14. The RAP field is 16 bits wide, unlike the 32-bit
# SRVSVC variant in {RubySMB::Dcerpc::Srvsvc::SHARE_TYPES}.
SHARE_TYPES = {
0x0000 => 'DISK',
0x0001 => 'PRINTER',
0x0002 => 'DEVICE',
0x0003 => 'IPC'
}.freeze
STYPE_SPECIAL = 0x8000
STYPE_TEMPORARY = 0x4000

# Single share entry (`share_info_1`) as it appears on the wire.
# MS-RAP 2.5.21. Fixed 20-byte layout.
class ShareInfo1 < BinData::Record
endian :little

string :netname, length: 13, trim_padding: true
uint8 :pad1
uint16 :share_type
uint32 :remark_offset
end

# Parameters block of the RAP request (sent in SMB trans_parameters).
# Variable-length because of the null-terminated descriptor strings.
class Request < BinData::Record
endian :little

uint16 :opcode, asserted_value: OPCODE
stringz :param_descriptor, initial_value: PARAM_DESCRIPTOR
stringz :data_descriptor, initial_value: DATA_DESCRIPTOR_LEVEL_1
uint16 :info_level, initial_value: 1
uint16 :receive_buffer_size, initial_value: DEFAULT_RECEIVE_BUFFER_SIZE
end

# Parameters block of the RAP response.
# MS-RAP 3.3.5.1 NetShareEnum Response.
class Response < BinData::Record
endian :little

uint16 :status
uint16 :converter
uint16 :entry_count
uint16 :available
end

# Sends a RAP NetShareEnum over `\PIPE\LANMAN` using the tree's
# existing SMB1 connection. Does not rely on having an opened pipe FID
# because Win9x does not permit OPEN_ANDX on `\PIPE\LANMAN`; RAP trans
# is accepted directly against the IPC$ tree.
#
# @return [Array<Hash>] each entry has :name (String) and :type (Integer).
# @raise [RubySMB::Error::InvalidPacket] on a malformed SMB response.
# @raise [RubySMB::Error::UnexpectedStatusCode] on a non-success SMB status.
# @raise [RubySMB::Error::RubySMBError] on a non-zero RAP status.
def net_share_enum
request = build_net_share_enum_request
raw_response = rap_client.send_recv(request)
response = RubySMB::SMB1::Packet::Trans::Response.read(raw_response)
validate_trans_response!(response)
parse_net_share_enum_response(response, raw_response)
end

private

# The SMB1 tree used to carry RAP traffic. `self` when this module is
# mixed into {RubySMB::SMB1::Tree}; the pipe's `tree` when mixed into
# {RubySMB::SMB1::Pipe}.
def rap_tree
is_a?(RubySMB::SMB1::Tree) ? self : tree
end

def rap_client
rap_tree.client
end

def build_net_share_enum_request
request = RubySMB::SMB1::Packet::Trans::Request.new
request.smb_header.tid = rap_tree.id
request.smb_header.flags2.unicode = 0
request.data_block.name = "\\PIPE\\LANMAN\x00".b
request.data_block.trans_parameters = Request.new.to_binary_s
request.parameter_block.max_parameter_count = 8
request.parameter_block.max_data_count = DEFAULT_RECEIVE_BUFFER_SIZE
request
end

def validate_trans_response!(response)
unless response.valid?
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::Trans::Response::COMMAND,
packet: response
)
end
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
end
end

def parse_net_share_enum_response(response, raw_response)
# Slice the parameter and data sections using the offsets the server
# reported, not the ones BinData computed. Win9x packs trans_parameters
# right after byte_count with no 4-byte-alignment padding, which
# confuses Trans::Response::DataBlock#pad1_length and shifts the
# trans_parameters window by 1 byte.
params_bytes = raw_response[response.parameter_block.parameter_offset,
response.parameter_block.parameter_count].to_s
data_bytes = raw_response[response.parameter_block.data_offset,
response.parameter_block.data_count].to_s

if params_bytes.bytesize < Response.new.num_bytes
raise RubySMB::Error::InvalidPacket,
"Truncated RAP NetShareEnum response parameters: #{params_bytes.unpack1('H*')}"
end
params = Response.read(params_bytes)
unless params.status.zero?
raise RubySMB::Error::RubySMBError,
"RAP NetShareEnum failed with status 0x#{params.status.to_i.to_s(16)}"
end

params.entry_count.times.map do |i|
offset = i * ShareInfo1.new.num_bytes
break [] if offset + ShareInfo1.new.num_bytes > data_bytes.bytesize
entry = ShareInfo1.read(data_bytes[offset, ShareInfo1.new.num_bytes])
{
name: entry.netname.to_s.delete("\x00"),
type: format_share_type(entry.share_type.to_i)
}
end.compact
end

# Format a RAP `share_info_1.shi1_type` value as a pipe-joined string in
# the same style as {RubySMB::Dcerpc::Srvsvc#net_share_enum_all}, so
# callers can consume both APIs uniformly.
def format_share_type(share_type)
base_bits = share_type & ~(STYPE_SPECIAL | STYPE_TEMPORARY)
parts = [SHARE_TYPES[base_bits] || format('UNKNOWN(0x%04x)', base_bits)]
parts << 'SPECIAL' unless (share_type & STYPE_SPECIAL).zero?
parts << 'TEMPORARY' unless (share_type & STYPE_TEMPORARY).zero?
parts.join('|')
end
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_smb/smb1/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Commands
SMB_COM_CLOSE = 0x04
SMB_COM_TRANSACTION = 0x25
SMB_COM_ECHO = 0x2B
SMB_COM_OPEN_ANDX = 0x2D
SMB_COM_READ_ANDX = 0x2E
SMB_COM_WRITE_ANDX = 0x2F
SMB_COM_TRANSACTION2 = 0x32
Expand Down
2 changes: 2 additions & 0 deletions lib/ruby_smb/smb1/packet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ module Packet
require 'ruby_smb/smb1/packet/trans'
require 'ruby_smb/smb1/packet/trans2'
require 'ruby_smb/smb1/packet/nt_trans'
require 'ruby_smb/smb1/packet/open_andx_request'
require 'ruby_smb/smb1/packet/open_andx_response'
require 'ruby_smb/smb1/packet/nt_create_andx_request'
require 'ruby_smb/smb1/packet/nt_create_andx_response'
require 'ruby_smb/smb1/packet/read_andx_request'
Expand Down
11 changes: 11 additions & 0 deletions lib/ruby_smb/smb1/packet/negotiate_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,21 @@ class ParameterBlock < RubySMB::SMB1::ParameterBlock
end

# An SMB_Data Block as defined by the {NegotiateResponse}
# Windows 95/98/ME may only return the challenge with no domain/server names.
class DataBlock < RubySMB::SMB1::DataBlock
string :challenge, label: 'Auth Challenge', length: 8
stringz16 :domain_name, label: 'Primary Domain'
stringz16 :server_name, label: 'Server Name'

# Override to handle Win95 responses that only contain the challenge
# (byte_count=8) without domain_name or server_name fields.
def do_read(io)
byte_count.do_read(io)
challenge.do_read(io)
return unless byte_count > 8
domain_name.do_read(io)
server_name.do_read(io)
end
end

smb_header :smb_header
Expand Down
Loading
Loading