ruby_smb 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.gitignore +21 -0
- data/.rspec +3 -0
- data/.simplecov +42 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +119 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +18 -0
- data/README.md +64 -0
- data/Rakefile +22 -0
- data/examples/authenticate.rb +30 -0
- data/examples/negotiate.rb +25 -0
- data/lib/ruby_smb/client/authentication.rb +236 -0
- data/lib/ruby_smb/client/negotiation.rb +126 -0
- data/lib/ruby_smb/client/signing.rb +48 -0
- data/lib/ruby_smb/client.rb +164 -0
- data/lib/ruby_smb/dispatcher/base.rb +18 -0
- data/lib/ruby_smb/dispatcher/socket.rb +53 -0
- data/lib/ruby_smb/dispatcher.rb +4 -0
- data/lib/ruby_smb/error.rb +17 -0
- data/lib/ruby_smb/field/file_time.rb +62 -0
- data/lib/ruby_smb/field/nt_status.rb +16 -0
- data/lib/ruby_smb/field/stringz16.rb +55 -0
- data/lib/ruby_smb/field.rb +7 -0
- data/lib/ruby_smb/generic_packet.rb +179 -0
- data/lib/ruby_smb/gss.rb +109 -0
- data/lib/ruby_smb/smb1/andx_block.rb +13 -0
- data/lib/ruby_smb/smb1/bit_field/capabilities.rb +39 -0
- data/lib/ruby_smb/smb1/bit_field/header_flags.rb +19 -0
- data/lib/ruby_smb/smb1/bit_field/header_flags2.rb +27 -0
- data/lib/ruby_smb/smb1/bit_field/security_mode.rb +16 -0
- data/lib/ruby_smb/smb1/bit_field.rb +10 -0
- data/lib/ruby_smb/smb1/commands.rb +9 -0
- data/lib/ruby_smb/smb1/data_block.rb +42 -0
- data/lib/ruby_smb/smb1/dialect.rb +11 -0
- data/lib/ruby_smb/smb1/packet/error_packet.rb +14 -0
- data/lib/ruby_smb/smb1/packet/negotiate_request.rb +52 -0
- data/lib/ruby_smb/smb1/packet/negotiate_response.rb +46 -0
- data/lib/ruby_smb/smb1/packet/negotiate_response_extended.rb +47 -0
- data/lib/ruby_smb/smb1/packet/session_setup_request.rb +71 -0
- data/lib/ruby_smb/smb1/packet/session_setup_response.rb +48 -0
- data/lib/ruby_smb/smb1/packet.rb +12 -0
- data/lib/ruby_smb/smb1/parameter_block.rb +42 -0
- data/lib/ruby_smb/smb1/smb_header.rb +21 -0
- data/lib/ruby_smb/smb1.rb +16 -0
- data/lib/ruby_smb/smb2/bit_field/session_flags.rb +17 -0
- data/lib/ruby_smb/smb2/bit_field/smb2_capabailities.rb +23 -0
- data/lib/ruby_smb/smb2/bit_field/smb2_header_flags.rb +23 -0
- data/lib/ruby_smb/smb2/bit_field/smb2_security_mode.rb +15 -0
- data/lib/ruby_smb/smb2/bit_field/smb2_security_mode_single.rb +14 -0
- data/lib/ruby_smb/smb2/bit_field.rb +11 -0
- data/lib/ruby_smb/smb2/commands.rb +25 -0
- data/lib/ruby_smb/smb2/packet/negotiate_request.rb +50 -0
- data/lib/ruby_smb/smb2/packet/negotiate_response.rb +33 -0
- data/lib/ruby_smb/smb2/packet/session_setup_request.rb +53 -0
- data/lib/ruby_smb/smb2/packet/session_setup_response.rb +38 -0
- data/lib/ruby_smb/smb2/packet.rb +10 -0
- data/lib/ruby_smb/smb2/smb2_header.rb +22 -0
- data/lib/ruby_smb/smb2.rb +12 -0
- data/lib/ruby_smb/version.rb +3 -0
- data/lib/ruby_smb.rb +22 -0
- data/ruby_smb.gemspec +38 -0
- data/spec/lib/ruby_smb/client_spec.rb +638 -0
- data/spec/lib/ruby_smb/dispatcher/dispatcher_base_spec.rb +22 -0
- data/spec/lib/ruby_smb/dispatcher/socket_spec.rb +60 -0
- data/spec/lib/ruby_smb/field/file_time_spec.rb +59 -0
- data/spec/lib/ruby_smb/field/nt_status_spec.rb +19 -0
- data/spec/lib/ruby_smb/field/stringz16_spec.rb +50 -0
- data/spec/lib/ruby_smb/generic_packet_spec.rb +58 -0
- data/spec/lib/ruby_smb/smb1/andx_block_spec.rb +41 -0
- data/spec/lib/ruby_smb/smb1/bit_field/capabilities_spec.rb +245 -0
- data/spec/lib/ruby_smb/smb1/bit_field/header_flags2_spec.rb +146 -0
- data/spec/lib/ruby_smb/smb1/bit_field/header_flags_spec.rb +102 -0
- data/spec/lib/ruby_smb/smb1/bit_field/security_mode_spec.rb +44 -0
- data/spec/lib/ruby_smb/smb1/data_block_spec.rb +26 -0
- data/spec/lib/ruby_smb/smb1/dialect_spec.rb +26 -0
- data/spec/lib/ruby_smb/smb1/packet/error_packet_spec.rb +39 -0
- data/spec/lib/ruby_smb/smb1/packet/negotiate_request_spec.rb +77 -0
- data/spec/lib/ruby_smb/smb1/packet/negotiate_response_extended_spec.rb +149 -0
- data/spec/lib/ruby_smb/smb1/packet/negotiate_response_spec.rb +150 -0
- data/spec/lib/ruby_smb/smb1/packet/session_setup_request_spec.rb +100 -0
- data/spec/lib/ruby_smb/smb1/packet/session_setup_response_spec.rb +72 -0
- data/spec/lib/ruby_smb/smb1/parameter_block_spec.rb +26 -0
- data/spec/lib/ruby_smb/smb1/smb_header_spec.rb +96 -0
- data/spec/lib/ruby_smb/smb2/bit_field/header_flags_spec.rb +81 -0
- data/spec/lib/ruby_smb/smb2/bit_field/session_flags_spec.rb +28 -0
- data/spec/lib/ruby_smb/smb2/bit_field/smb2_capabilities_spec.rb +72 -0
- data/spec/lib/ruby_smb/smb2/bit_field/smb_secruity_mode_spec.rb +22 -0
- data/spec/lib/ruby_smb/smb2/packet/negotiate_request_spec.rb +122 -0
- data/spec/lib/ruby_smb/smb2/packet/negotiate_response_spec.rb +147 -0
- data/spec/lib/ruby_smb/smb2/packet/session_setup_request_spec.rb +79 -0
- data/spec/lib/ruby_smb/smb2/packet/session_setup_response_spec.rb +54 -0
- data/spec/lib/ruby_smb/smb2/smb2_header_spec.rb +127 -0
- data/spec/lib/ruby_smb_spec.rb +2 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/support/mock_socket_dispatcher.rb +8 -0
- data/spec/support/shared/examples/bit_field_single_flag.rb +14 -0
- data.tar.gz.sig +0 -0
- metadata +384 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
class Client
|
|
3
|
+
# This module holds all of the methods backing the {RubySMB::Client#negotiate} method
|
|
4
|
+
module Negotiation
|
|
5
|
+
|
|
6
|
+
# Handles the entire SMB Multi-Protocol Negotiation from the
|
|
7
|
+
# Client to the Server. It sets state on the client appropriate
|
|
8
|
+
# to the protocol and capabilites negotiated during the exchange.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
11
|
+
def negotiate
|
|
12
|
+
raw_response = negotiate_request
|
|
13
|
+
response_packet = negotiate_response(raw_response)
|
|
14
|
+
parse_negotiate_response(response_packet)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Creates and dispatches the first Negotiate Request Packet and
|
|
18
|
+
# returns the raw response data.
|
|
19
|
+
#
|
|
20
|
+
# @return [String] the raw binary string containing the response from the server
|
|
21
|
+
def negotiate_request
|
|
22
|
+
if smb1
|
|
23
|
+
request = smb1_negotiate_request
|
|
24
|
+
elsif smb2
|
|
25
|
+
request = smb2_negotiate_request
|
|
26
|
+
end
|
|
27
|
+
send_recv(request)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Takes the raw response data from the server and tries
|
|
31
|
+
# parse it into a valid Response packet object.
|
|
32
|
+
# This method currently assumes that all SMB1 will use Extended Security.
|
|
33
|
+
#
|
|
34
|
+
# @param raw_data [String] the raw binary response from the server
|
|
35
|
+
# @return [RubySMB::SMB1::Packet::NegotiateResponseExtended] when the response is an SMB1 Extended Security Negotiate Response Packet
|
|
36
|
+
# @return [RubySMB::SMB2::Packet::NegotiateResponse] when the response is an SMB2 Negotiate Response Packet
|
|
37
|
+
def negotiate_response(raw_data)
|
|
38
|
+
response = nil
|
|
39
|
+
if smb1
|
|
40
|
+
begin
|
|
41
|
+
packet = RubySMB::SMB1::Packet::NegotiateResponseExtended.read raw_data
|
|
42
|
+
rescue Exception => e
|
|
43
|
+
raise RubySMB::Error::InvalidPacket, "Not a Valid SMB1 Negoitate Response #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
if packet.valid?
|
|
46
|
+
response = packet
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
if smb2 && response.nil?
|
|
50
|
+
begin
|
|
51
|
+
packet = RubySMB::SMB2::Packet::NegotiateResponse.read raw_data
|
|
52
|
+
rescue Exception => e
|
|
53
|
+
raise RubySMB::Error::InvalidPacket, "Not a Valid SMB2 Negoitate Response #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
response = packet
|
|
56
|
+
end
|
|
57
|
+
if response.nil?
|
|
58
|
+
raise RubySMB::Error::InvalidPacket, "No Valid Negotiate Response found"
|
|
59
|
+
end
|
|
60
|
+
response
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Sets the supported SMB Protocol and whether or not
|
|
64
|
+
# Signing is enabled based on the Negotiate Response Packet.
|
|
65
|
+
#
|
|
66
|
+
# @param packet [RubySMB::SMB1::Packet::NegotiateResponseExtended] if SMB1 was negotiated
|
|
67
|
+
# @param packet [RubySMB::SMB2::Packet::NegotiateResponse] if SMB2 was negotiated
|
|
68
|
+
# @return [void] This method sets state and does not return a meaningful value
|
|
69
|
+
def parse_negotiate_response(packet)
|
|
70
|
+
case packet
|
|
71
|
+
when RubySMB::SMB1::Packet::NegotiateResponseExtended
|
|
72
|
+
self.smb1 = true
|
|
73
|
+
self.smb2 = false
|
|
74
|
+
if packet.parameter_block.security_mode.security_signatures_required == 1
|
|
75
|
+
self.signing_required = true
|
|
76
|
+
else
|
|
77
|
+
self.signing_required = false
|
|
78
|
+
end
|
|
79
|
+
'SMB1'
|
|
80
|
+
when RubySMB::SMB2::Packet::NegotiateResponse
|
|
81
|
+
self.smb1 = false
|
|
82
|
+
self.smb2 = true
|
|
83
|
+
if packet.security_mode.signing_required == 1
|
|
84
|
+
self.signing_required = true
|
|
85
|
+
else
|
|
86
|
+
self.signing_required = false
|
|
87
|
+
end
|
|
88
|
+
'SMB2'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Create a {RubySMB::SMB1::Packet::NegotiateRequest} packet with the
|
|
94
|
+
# dialects filled in based on the protocol options set on the Client.
|
|
95
|
+
#
|
|
96
|
+
# @return [RubySMB::SMB1::Packet::NegotiateRequest] a completed SMB1 Negotiate Request packet
|
|
97
|
+
def smb1_negotiate_request
|
|
98
|
+
packet = RubySMB::SMB1::Packet::NegotiateRequest.new
|
|
99
|
+
# Default to always enabling Extended Security. It simplifies the Negotiation process
|
|
100
|
+
# while being gauranteed to work with any modern Windows system. We can get more sophisticated
|
|
101
|
+
# with switching this on and off at a later date if the need arises.
|
|
102
|
+
packet.smb_header.flags2.extended_security = 1
|
|
103
|
+
# There is no real good reason to ever send an SMB1 Negotiate packet
|
|
104
|
+
# to Negotiate strictly SMB2, but the protocol WILL support it
|
|
105
|
+
packet.add_dialect(SMB1_DIALECT_SMB1_DEFAULT) if smb1
|
|
106
|
+
packet.add_dialect(SMB1_DIALECT_SMB2_DEFAULT) if smb2
|
|
107
|
+
packet
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Create a {RubySMB::SMB2::Packet::NegotiateRequest} packet with
|
|
111
|
+
# the default dialect added. This will never be used when we
|
|
112
|
+
# may want to communicate over SMB1
|
|
113
|
+
#
|
|
114
|
+
# @ return [RubySMB::SMB2::Packet::NegotiateRequest] a completed SMB2 Negotiate Request packet
|
|
115
|
+
def smb2_negotiate_request
|
|
116
|
+
packet = RubySMB::SMB2::Packet::NegotiateRequest.new
|
|
117
|
+
packet.smb2_header.message_id = self.smb2_message_id
|
|
118
|
+
# Increment the message id when doing SMB2
|
|
119
|
+
self.smb2_message_id += 1
|
|
120
|
+
packet.security_mode.signing_enabled = 1
|
|
121
|
+
packet.add_dialect(SMB2_DIALECT_DEFAULT)
|
|
122
|
+
packet
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
class Client
|
|
3
|
+
|
|
4
|
+
# Contains the methods for handling packet signing
|
|
5
|
+
module Signing
|
|
6
|
+
|
|
7
|
+
# The NTLM Session Key used for signing
|
|
8
|
+
# @!attribute [rw] session_key
|
|
9
|
+
# @return [String]
|
|
10
|
+
attr_accessor :session_key
|
|
11
|
+
|
|
12
|
+
# Take an SMB1 packet and checks to see if it should be signed.
|
|
13
|
+
# If signing is enabled and we have a session key already, then
|
|
14
|
+
# it will sign the packet appropriately.
|
|
15
|
+
#
|
|
16
|
+
# @param packet [RubySMB::GenericPacket] the packet to sign
|
|
17
|
+
# @return [RubySMB::GenericPacket] the packet, signed if needed
|
|
18
|
+
def smb1_sign(packet)
|
|
19
|
+
if self.signing_required && !self.session_key.empty?
|
|
20
|
+
packet.smb_header.security_features = self.sequence_counter
|
|
21
|
+
signature = OpenSSL::Digest::MD5.digest(self.session_key + packet.to_binary_s)[0,8]
|
|
22
|
+
packet.smb_header.security_features = signature
|
|
23
|
+
self.sequence_counter += 1
|
|
24
|
+
packet
|
|
25
|
+
else
|
|
26
|
+
packet
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Take an SMB2 packet and checks to see if it should be signed.
|
|
31
|
+
# If signing is enabled and we have a session key already, then
|
|
32
|
+
# it will sign the packet appropriately.
|
|
33
|
+
#
|
|
34
|
+
# @param packet [RubySMB::GenericPacket] the packet to sign
|
|
35
|
+
# @return [RubySMB::GenericPacket] the packet, signed if needed
|
|
36
|
+
def smb2_sign(packet)
|
|
37
|
+
if self.signing_required && !self.session_key.empty?
|
|
38
|
+
hmac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, self.session_key, packet.to_binary_s)
|
|
39
|
+
packet.smb2_header.signature = hmac
|
|
40
|
+
packet
|
|
41
|
+
else
|
|
42
|
+
packet
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
|
|
3
|
+
# Represents an SMB client capable of talking to SMB1 or SMB2 servers and handling
|
|
4
|
+
# all end-user client functionality.
|
|
5
|
+
class Client
|
|
6
|
+
require 'ruby_smb/client/negotiation'
|
|
7
|
+
require 'ruby_smb/client/authentication'
|
|
8
|
+
require 'ruby_smb/client/signing'
|
|
9
|
+
|
|
10
|
+
include RubySMB::Client::Negotiation
|
|
11
|
+
include RubySMB::Client::Authentication
|
|
12
|
+
include RubySMB::Client::Signing
|
|
13
|
+
|
|
14
|
+
# The Default SMB1 Dialect string used in an SMB1 Negotiate Request
|
|
15
|
+
SMB1_DIALECT_SMB1_DEFAULT = "NT LM 0.12"
|
|
16
|
+
# The Default SMB2 Dialect string used in an SMB1 Negotiate Request
|
|
17
|
+
SMB1_DIALECT_SMB2_DEFAULT = "SMB 2.002"
|
|
18
|
+
# Dialect value for SMB2 Default (Version 2.02)
|
|
19
|
+
SMB2_DIALECT_DEFAULT = 0x0202
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# The dispatcher responsible for sending packets
|
|
23
|
+
# @!attribute [rw] dispatcher
|
|
24
|
+
# @return [RubySMB::Dispatcher::Socket]
|
|
25
|
+
attr_accessor :dispatcher
|
|
26
|
+
|
|
27
|
+
# The domain you're trying to authenticate to
|
|
28
|
+
# @!attribute [rw] domain
|
|
29
|
+
# @return [String]
|
|
30
|
+
attr_accessor :domain
|
|
31
|
+
|
|
32
|
+
# The local workstation to pretend to be
|
|
33
|
+
# @!attribute [rw] local_workstation
|
|
34
|
+
# @return [String]
|
|
35
|
+
attr_accessor :local_workstation
|
|
36
|
+
|
|
37
|
+
# The NTLM client used for authentication
|
|
38
|
+
# @!attribute [rw] ntlm_client
|
|
39
|
+
# @return [String]
|
|
40
|
+
attr_accessor :ntlm_client
|
|
41
|
+
|
|
42
|
+
# The password to authenticate with
|
|
43
|
+
# @!attribute [rw] password
|
|
44
|
+
# @return [String]
|
|
45
|
+
attr_accessor :password
|
|
46
|
+
|
|
47
|
+
# The Sequence Counter used for SMB1 Signing.
|
|
48
|
+
# It tracks the number of packets both sent and received
|
|
49
|
+
# since the NTLM session was initialized with the Challenge.
|
|
50
|
+
# @!attribute [rw] sequence_counter
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
attr_accessor :sequence_counter
|
|
53
|
+
|
|
54
|
+
# The current Session ID setup by authentication
|
|
55
|
+
# @!attribute [rw] session_id
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
attr_accessor :session_id
|
|
58
|
+
|
|
59
|
+
# Whether or not the Server requires signing
|
|
60
|
+
# @!attribute [rw] signing_enabled
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
attr_accessor :signing_required
|
|
63
|
+
|
|
64
|
+
# Whether or not the Client should support SMB1
|
|
65
|
+
# @!attribute [rw] smb1
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
attr_accessor :smb1
|
|
68
|
+
|
|
69
|
+
# Whether or not the Client should support SMB2
|
|
70
|
+
# @!attribute [rw] smb2
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
attr_accessor :smb2
|
|
73
|
+
|
|
74
|
+
# Tracks the current SMB2 Message ID that keeps communication in sync
|
|
75
|
+
# @!attribute [rw] smb2_message_id
|
|
76
|
+
# @return [Integer]
|
|
77
|
+
attr_accessor :smb2_message_id
|
|
78
|
+
|
|
79
|
+
# The username to authenticate with
|
|
80
|
+
# @!attribute [rw] username
|
|
81
|
+
# @return [String]
|
|
82
|
+
attr_accessor :username
|
|
83
|
+
|
|
84
|
+
# The UID set in SMB1
|
|
85
|
+
# @!attribute [rw] user_id
|
|
86
|
+
# @return [String]
|
|
87
|
+
attr_accessor :user_id
|
|
88
|
+
|
|
89
|
+
# @param dispatcher [RubySMB::Dispacther::Socket] the packet dispatcher to use
|
|
90
|
+
# @param smb1 [Boolean] whether or not to enable SMB1 support
|
|
91
|
+
# @param smb2 [Boolean] whether or not to enable SMB2 support
|
|
92
|
+
def initialize(dispatcher, smb1: true, smb2: true, username:,password:, domain:'.', local_workstation:'WORKSTATION')
|
|
93
|
+
raise ArgumentError, 'No Dispatcher provided' unless dispatcher.kind_of? RubySMB::Dispatcher::Base
|
|
94
|
+
if smb1 == false && smb2 == false
|
|
95
|
+
raise ArgumentError, 'You must enable at least one Protocol'
|
|
96
|
+
end
|
|
97
|
+
@dispatcher = dispatcher
|
|
98
|
+
@domain = domain
|
|
99
|
+
@local_workstation = local_workstation
|
|
100
|
+
@password = password.encode("utf-8")
|
|
101
|
+
@sequence_counter = 0
|
|
102
|
+
@session_id = 0x00
|
|
103
|
+
@session_key = ''
|
|
104
|
+
@signing_required = false
|
|
105
|
+
@smb1 = smb1
|
|
106
|
+
@smb2 = smb2
|
|
107
|
+
@username = username.encode("utf-8")
|
|
108
|
+
|
|
109
|
+
@ntlm_client = Net::NTLM::Client.new(
|
|
110
|
+
@username,
|
|
111
|
+
@password,
|
|
112
|
+
workstation: @local_workstation,
|
|
113
|
+
domain: @domain
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@smb2_message_id = 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def login(username: self.username, password: self.password, domain: self.domain, local_workstation: self.local_workstation )
|
|
120
|
+
@domain = domain
|
|
121
|
+
@local_workstation = local_workstation
|
|
122
|
+
@password = password.encode("utf-8")
|
|
123
|
+
@username = username.encode("utf-8")
|
|
124
|
+
|
|
125
|
+
@ntlm_client = Net::NTLM::Client.new(
|
|
126
|
+
@username,
|
|
127
|
+
@password,
|
|
128
|
+
workstation: @local_workstation,
|
|
129
|
+
domain: @domain
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
negotiate
|
|
133
|
+
authenticate
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Sends a packet and receives the raw response through the Dispatcher.
|
|
137
|
+
# It will also sign the packet if neccessary.
|
|
138
|
+
#
|
|
139
|
+
# @param packet [RubySMB::GenericPacket] the request to be sent
|
|
140
|
+
# @return [String] the raw response data received
|
|
141
|
+
def send_recv(packet)
|
|
142
|
+
case packet.packet_smb_version
|
|
143
|
+
when 'SMB1'
|
|
144
|
+
packet = smb1_sign(packet)
|
|
145
|
+
when 'SMB2'
|
|
146
|
+
packet = smb2_sign(packet)
|
|
147
|
+
else
|
|
148
|
+
packet = packet
|
|
149
|
+
end
|
|
150
|
+
dispatcher.send_packet(packet)
|
|
151
|
+
raw_response = dispatcher.recv_packet
|
|
152
|
+
if self.sequence_counter > 0
|
|
153
|
+
self.sequence_counter += 1
|
|
154
|
+
end
|
|
155
|
+
raw_response
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Provides the base class for the packet dispatcher.
|
|
2
|
+
class RubySMB::Dispatcher::Base
|
|
3
|
+
# @param packet [#length]
|
|
4
|
+
# @return [Fixnum] NBSS header to go in front of `packet`
|
|
5
|
+
def nbss(packet)
|
|
6
|
+
[packet.do_num_bytes].pack('N')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @abstract
|
|
10
|
+
def send_packet(packet)
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @abstract
|
|
15
|
+
def recv_packet
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
|
|
3
|
+
# This class provides a wrapper around a Socket for the packet Dispatcher.
|
|
4
|
+
# It allows for dependency injection of different Socket implementations.
|
|
5
|
+
class RubySMB::Dispatcher::Socket < RubySMB::Dispatcher::Base
|
|
6
|
+
# The underlying socket that we select on
|
|
7
|
+
# @!attribute [rw] tcp_socket
|
|
8
|
+
# @return [IO]
|
|
9
|
+
attr_accessor :tcp_socket
|
|
10
|
+
|
|
11
|
+
# @param tcp_socket [IO]
|
|
12
|
+
def initialize(tcp_socket)
|
|
13
|
+
@tcp_socket = tcp_socket
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param host [String] passed to TCPSocket.new
|
|
17
|
+
# @param port [Fixnum] passed to TCPSocket.new
|
|
18
|
+
def self.connect(host, port: 445, socket: TCPSocket.new(host, port))
|
|
19
|
+
new(socket)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param packet [SMB2::Packet,#to_s]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def send_packet(packet)
|
|
25
|
+
data = nbss(packet) + packet.to_binary_s
|
|
26
|
+
bytes_written = 0
|
|
27
|
+
while bytes_written < data.size
|
|
28
|
+
bytes_written += @tcp_socket.write(data[bytes_written..-1])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Read a packet off the wire and parse it into a string
|
|
35
|
+
# Throw Error::NetBiosSessionService if there's an error reading the first 4 bytes,
|
|
36
|
+
# which are assumed to be the NetBiosSessionService header.
|
|
37
|
+
# @return [String]
|
|
38
|
+
# @todo should return SMB2::Packet
|
|
39
|
+
def recv_packet
|
|
40
|
+
IO.select([@tcp_socket])
|
|
41
|
+
nbss_header = @tcp_socket.read(4) # Length of NBSS header. TODO: remove to a constant
|
|
42
|
+
if nbss_header.nil?
|
|
43
|
+
raise ::RubySMB::Error::NetBiosSessionService, 'NBSS Header is missing'
|
|
44
|
+
else
|
|
45
|
+
length = nbss_header.unpack('N').first
|
|
46
|
+
end
|
|
47
|
+
IO.select([@tcp_socket])
|
|
48
|
+
data = @tcp_socket.read(length)
|
|
49
|
+
data << @tcp_socket.read(length - data.length) while data.length < length
|
|
50
|
+
|
|
51
|
+
data
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module RubySMB::Error
|
|
2
|
+
# Raised when there is a length or formatting issue with an ASN1-encoded string
|
|
3
|
+
# @see https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
|
|
4
|
+
# @todo Find an SMB-specific link for ASN1 above
|
|
5
|
+
class ASN1Encoding < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when there is a problem with communication over NetBios Session Service
|
|
8
|
+
# @see https://wiki.wireshark.org/NetBIOS/NBSS
|
|
9
|
+
class NetBiosSessionService < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Raised when trying to parse raw binary into a Packet and the data
|
|
12
|
+
# is invalid.
|
|
13
|
+
class InvalidPacket < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Raised when a response packet has a NTStatus code that was unexpected.
|
|
16
|
+
class UnexpectedStatusCode < StandardError; end
|
|
17
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module Field
|
|
3
|
+
# Represents a Windows FILETIME structure as defined in
|
|
4
|
+
# [FILETIME structure](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx)
|
|
5
|
+
class FileTime < BinData::Primitive
|
|
6
|
+
# Difference between the Windows and Unix epochs, in 100ns intervals
|
|
7
|
+
EPOCH_DIFF_100NS = 116_444_736_000_000_000
|
|
8
|
+
NS_MULTIPLIER = 10_000_000
|
|
9
|
+
endian :little
|
|
10
|
+
uint64 :val
|
|
11
|
+
|
|
12
|
+
# Gets the value of the field
|
|
13
|
+
#
|
|
14
|
+
# @return [BinData::Bit64] the 64-bit value of the field
|
|
15
|
+
def get
|
|
16
|
+
val
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Sets the value of the field from a DateTime,Time,Fixnum, or object
|
|
20
|
+
# that can be converted to an integer. Datetime and Time objects get
|
|
21
|
+
# converted to account for the Windows/Unix Epoch difference. Any other
|
|
22
|
+
# parameter passed in will be assumed to already be correct.
|
|
23
|
+
#
|
|
24
|
+
# @param value [DateTime,Time,Fixnum,#to_i] the value to set
|
|
25
|
+
# @return
|
|
26
|
+
def set(value)
|
|
27
|
+
case value
|
|
28
|
+
when DateTime
|
|
29
|
+
set(value.to_time)
|
|
30
|
+
when Time
|
|
31
|
+
time_int = value.to_i
|
|
32
|
+
time_int *= NS_MULTIPLIER
|
|
33
|
+
adjusted_epoch = time_int + EPOCH_DIFF_100NS
|
|
34
|
+
set(adjusted_epoch)
|
|
35
|
+
when Fixnum
|
|
36
|
+
self.val = value
|
|
37
|
+
else
|
|
38
|
+
self.val = value.to_i
|
|
39
|
+
end
|
|
40
|
+
val
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the value of the field as a {DateTime}
|
|
44
|
+
#
|
|
45
|
+
# @return [DateTime] the {DateTime} representation of the current value
|
|
46
|
+
def to_datetime
|
|
47
|
+
time = to_time
|
|
48
|
+
time.to_datetime
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns the value of the field as a {Time}
|
|
52
|
+
#
|
|
53
|
+
# @return [Time] the {Time} representation of the current value
|
|
54
|
+
def to_time
|
|
55
|
+
windows_int = val
|
|
56
|
+
adjusted_epoch = windows_int - EPOCH_DIFF_100NS
|
|
57
|
+
unix_int = adjusted_epoch / NS_MULTIPLIER
|
|
58
|
+
Time.at unix_int
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'windows_error/nt_status'
|
|
2
|
+
|
|
3
|
+
module RubySMB
|
|
4
|
+
module Field
|
|
5
|
+
# Represents an NTStatus code as defined in
|
|
6
|
+
# [2.3.1 NTSTATUS values](https://msdn.microsoft.com/en-us/library/cc704588.aspx)
|
|
7
|
+
class NtStatus < BinData::Uint32le
|
|
8
|
+
# Returns a meaningful error code parsed from the numeric value
|
|
9
|
+
#
|
|
10
|
+
# @return [WindowsError::ErrorCode] the ErrorCode object for this code
|
|
11
|
+
def to_nt_status
|
|
12
|
+
WindowsError::NTStatus.find_by_retval(value).first
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module Field
|
|
3
|
+
# Represents a NULL-Terminated String in UTF-16
|
|
4
|
+
class Stringz16 < BinData::Stringz
|
|
5
|
+
|
|
6
|
+
def assign(val)
|
|
7
|
+
super(binary_string(val.encode("utf-16le")))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def snapshot
|
|
11
|
+
# override to always remove trailing zero bytes
|
|
12
|
+
result = _value
|
|
13
|
+
result
|
|
14
|
+
result = trim_and_zero_terminate(result)
|
|
15
|
+
result.chomp("\0\0").force_encoding("utf-16le")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def append_zero_byte_if_needed!(str)
|
|
21
|
+
if str.length == 0 || !(str.end_with?("\0\0"))
|
|
22
|
+
str << "\0\0"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Override parent on {BinData::Stringz} to use
|
|
27
|
+
# a double NULL-byte instead of a single NULL-byte
|
|
28
|
+
# as a terminator
|
|
29
|
+
# @see BinData::Stringz
|
|
30
|
+
def read_and_return_value(io)
|
|
31
|
+
max_length = eval_parameter(:max_length)
|
|
32
|
+
str = ''
|
|
33
|
+
i = 0
|
|
34
|
+
ch = nil
|
|
35
|
+
|
|
36
|
+
# read until double NULL-byte or we have read in the max number of bytes
|
|
37
|
+
while (ch != "\0\0") && (i != max_length)
|
|
38
|
+
ch = io.readbytes(2)
|
|
39
|
+
str << ch
|
|
40
|
+
i += 2
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
trim_and_zero_terminate(str)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Override parent method of #truncate_after_first_zero_byte! on
|
|
47
|
+
# {BinData::Stringz} to use two consecutive NULL-bytes as the terimnator
|
|
48
|
+
# instead of a single NULL-nyte.
|
|
49
|
+
# @see BinData::Stringz
|
|
50
|
+
def truncate_after_first_zero_byte!(str)
|
|
51
|
+
str.sub!(/([^\0]*\0\0\0).*/, '\1')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|