-
Notifications
You must be signed in to change notification settings - Fork 84
Add Windows 95/98/ME SMB1 support #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d0d2607
10ed8be
541321c
b5269e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -603,13 +603,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 | ||
|
|
@@ -625,6 +627,61 @@ def net_share_enum_all(host) | |
| named_pipe.net_share_enum_all(host) | ||
| end | ||
|
|
||
| # Enumerates shares using the RAP (Remote Administration Protocol). | ||
| # This is the only share-enumeration method supported by Windows | ||
| # 95/98/ME and old Samba builds that lack DCERPC/srvsvc. | ||
| # | ||
| # @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::UnexpectedStatusCode] on transport errors | ||
| def net_share_enum_rap(host, password: nil) | ||
| tree = tree_connect("\\\\#{host}\\IPC$", password: password) | ||
| begin | ||
| request = RubySMB::SMB1::Packet::Trans::Request.new | ||
| request.smb_header.tid = tree.id | ||
| request.smb_header.flags2.unicode = 0 | ||
|
|
||
| rap_params = [0].pack('v') # Function: NetShareEnum (0) | ||
| rap_params << "WrLeh\x00" # Param descriptor | ||
| rap_params << "B13BWz\x00" # Return descriptor | ||
| rap_params << [1].pack('v') # Info level 1 | ||
| rap_params << [0x1000].pack('v') # Receive buffer size | ||
|
|
||
| request.data_block.name = "\\PIPE\\LANMAN\x00" | ||
| request.data_block.trans_parameters = rap_params | ||
| request.parameter_block.max_data_count = 0x1000 | ||
| request.parameter_block.max_parameter_count = 8 | ||
|
|
||
| raw_response = send_recv(request) | ||
| 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 | ||
|
Comment on lines
+657
to
+679
|
||
| ensure | ||
| tree.disconnect! | ||
| end | ||
| end | ||
|
|
||
| # Resets all of the session state on the client, setting it | ||
| # back to scratch. Should only be called when a session is no longer | ||
| # valid. | ||
|
|
@@ -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. | ||
|
Comment on lines
+704
to
+707
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| # | ||
| # @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') | ||
|
|
||
|
Comment on lines
+714
to
+717
|
||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 | ||
|
Comment on lines
638
to
+770
|
||
|
|
||
| # Resolves a NetBIOS name using the system nmblookup command. | ||
| # | ||
| # @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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| return nil unless $?.success? | ||
|
|
||
| output.each_line do |line| | ||
| if line =~ /\A\s+(\S+)\s+<20>\s/ | ||
| return $1.strip | ||
| end | ||
| end | ||
| nil | ||
| rescue Errno::ENOENT | ||
| nil | ||
| end | ||
|
|
||
| # Resolves a NetBIOS name via a raw UDP Node Status request | ||
| # (RFC 1002, port 137). | ||
| # | ||
| # @param host [String] the IP address to query | ||
| # @return [String, nil] the file server NetBIOS name | ||
| def netbios_lookup_udp(host) | ||
| 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') | ||
|
Comment on lines
+796
to
+806
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| sock = UDPSocket.new | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| sock.send(request, 0, host, 137) | ||
|
|
||
| return nil unless IO.select([sock], nil, nil, 3) | ||
|
|
||
| data, = sock.recvfrom(4096) | ||
| return nil if data.nil? || data.length < 57 | ||
|
|
||
| offset = 12 | ||
| while offset < data.length | ||
| len = data[offset].ord | ||
| break if len == 0 | ||
| offset += len + 1 | ||
| end | ||
| offset += 1 # label terminator | ||
| offset += 10 # Type(2) + Class(2) + TTL(4) + RDLENGTH(2) | ||
| return nil if offset >= data.length | ||
|
|
||
| num_names = data[offset].ord | ||
| offset += 1 | ||
|
|
||
| num_names.times do | ||
| break if offset + 18 > data.length | ||
| nb_name = data[offset, 15].rstrip | ||
| suffix = data[offset + 15].ord | ||
| flags = data[offset + 16, 2].unpack1('n') | ||
| offset += 18 | ||
| return nb_name if suffix == 0x20 && (flags & 0x8000) == 0 | ||
| end | ||
|
|
||
| nil | ||
| ensure | ||
| sock&.close | ||
| end | ||
|
|
||
| public | ||
|
|
||
| # Crafts the NetBIOS SessionRequest packet to be sent for session request operations. | ||
| # | ||
| # @param name [String] the NetBIOS name to request | ||
| # @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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,6 +120,20 @@ def parse_negotiate_response(packet) | |
| self.session_encrypt_data = false | ||
| self.negotiation_security_buffer = packet.data_block.security_blob | ||
| 'SMB1' | ||
| when RubySMB::SMB1::Packet::NegotiateResponse | ||
| # Non-extended security (e.g. Windows 95/98/ME, old Samba). The server provides | ||
|
Comment on lines
+123
to
+124
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' |
||
| # a raw 8-byte challenge in the negotiate response instead of a SPNEGO blob. | ||
| 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 | ||
| self.server_max_buffer_size = packet.parameter_block.max_buffer_size - 260 | ||
| self.negotiated_smb_version = 1 | ||
| self.session_encrypt_data = false | ||
| # Store the 8-byte challenge so authentication can compute LM/NTLM responses. | ||
| @smb1_negotiate_challenge = packet.data_block.challenge.to_s | ||
| 'SMB1' | ||
| when RubySMB::SMB2::Packet::NegotiateResponse | ||
| self.smb1 = false | ||
| unless packet.dialect_revision.to_i == RubySMB::SMB2::SMB2_WILDCARD_REVISION | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NULL bytes need the
Suggested change
|
||||||
| 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) | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is going to break when SMB 2 or 3 was negotiated. The entire method though probably shouldn't exist. The current pattern is that if this is a DCERPC method, then it should have corresponding DCERPC definitions for NetShareEnum below. It looks like you'd create a RAP folder with the definitions to mirror the MS-RAP spec where this is defined.
The pipe then needs to be updated for both SMB1 and SMB2 https://github.com/rapid7/ruby_smb/blob/master/lib/ruby_smb/smb1/pipe.rb#L16 which will declare your
net_share_enummethod.