ruby_smb 2.0.7 → 2.0.11
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/verify.yml +57 -0
- data/README.md +0 -1
- data/examples/auth_capture.rb +71 -0
- data/lib/ruby_smb/client/negotiation.rb +11 -13
- data/lib/ruby_smb/client.rb +32 -27
- data/lib/ruby_smb/compression/lznt1.rb +164 -0
- data/lib/ruby_smb/compression.rb +7 -0
- data/lib/ruby_smb/dialect.rb +45 -0
- data/lib/ruby_smb/dispatcher/base.rb +1 -1
- data/lib/ruby_smb/dispatcher/socket.rb +1 -1
- data/lib/ruby_smb/gss/provider/authenticator.rb +42 -0
- data/lib/ruby_smb/gss/provider/ntlm.rb +303 -0
- data/lib/ruby_smb/gss/provider.rb +35 -0
- data/lib/ruby_smb/gss.rb +56 -63
- data/lib/ruby_smb/ntlm.rb +45 -0
- data/lib/ruby_smb/server/server_client/negotiation.rb +155 -0
- data/lib/ruby_smb/server/server_client/session_setup.rb +82 -0
- data/lib/ruby_smb/server/server_client.rb +163 -0
- data/lib/ruby_smb/server.rb +54 -0
- data/lib/ruby_smb/signing.rb +59 -0
- data/lib/ruby_smb/smb1/packet/negotiate_response.rb +11 -11
- data/lib/ruby_smb/smb1/packet/negotiate_response_extended.rb +1 -1
- data/lib/ruby_smb/smb1/packet/session_setup_request.rb +1 -1
- data/lib/ruby_smb/smb1/tree.rb +1 -1
- data/lib/ruby_smb/smb2/negotiate_context.rb +18 -2
- data/lib/ruby_smb/smb2/packet/compression_transform_header.rb +4 -0
- data/lib/ruby_smb/smb2/packet/negotiate_request.rb +9 -0
- data/lib/ruby_smb/smb2/packet/negotiate_response.rb +0 -1
- data/lib/ruby_smb/smb2/packet/session_setup_response.rb +2 -2
- data/lib/ruby_smb/smb2/packet/tree_connect_request.rb +1 -1
- data/lib/ruby_smb/smb2/tree.rb +1 -1
- data/lib/ruby_smb/smb2.rb +3 -1
- data/lib/ruby_smb/version.rb +1 -1
- data/lib/ruby_smb.rb +5 -3
- data/spec/lib/ruby_smb/client_spec.rb +24 -16
- data/spec/lib/ruby_smb/compression/lznt1_spec.rb +32 -0
- data/spec/lib/ruby_smb/gss/provider/ntlm/account_spec.rb +32 -0
- data/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb +101 -0
- data/spec/lib/ruby_smb/gss/provider/ntlm/os_version_spec.rb +32 -0
- data/spec/lib/ruby_smb/gss/provider/ntlm_spec.rb +113 -0
- data/spec/lib/ruby_smb/server/server_client_spec.rb +156 -0
- data/spec/lib/ruby_smb/server_spec.rb +32 -0
- data/spec/lib/ruby_smb/smb1/tree_spec.rb +4 -4
- data/spec/lib/ruby_smb/smb2/negotiate_context_spec.rb +2 -2
- data/spec/lib/ruby_smb/smb2/tree_spec.rb +5 -5
- data/spec/spec_helper.rb +1 -1
- data.tar.gz.sig +3 -1
- metadata +30 -4
- metadata.gz.sig +0 -0
- data/.travis.yml +0 -6
- data/lib/ruby_smb/client/signing.rb +0 -64
@@ -0,0 +1,45 @@
|
|
1
|
+
module RubySMB
|
2
|
+
# Definitions that define metadata around a particular SMB dialect. This is useful for grouping dialects into a
|
3
|
+
# hierarchy as well as printing them as human readable strings with varying degrees of specificity.
|
4
|
+
module Dialect
|
5
|
+
# the order (taxonomic ranking) of the family, 2 and 3 are intentionally combined
|
6
|
+
ORDER_SMB1 = 'SMB1'.freeze
|
7
|
+
ORDER_SMB2 = 'SMB2'.freeze
|
8
|
+
|
9
|
+
# the family of the dialect
|
10
|
+
FAMILY_SMB1 = 'SMB 1'.freeze
|
11
|
+
FAMILY_SMB2 = 'SMB 2.x'.freeze
|
12
|
+
FAMILY_SMB3 = 'SMB 3.x'.freeze
|
13
|
+
|
14
|
+
# the major version of the dialect
|
15
|
+
VERSION_SMB1 = 'SMB v1'.freeze
|
16
|
+
VERSION_SMB2 = 'SMB v2'.freeze
|
17
|
+
VERSION_SMB3 = 'SMB v3'.freeze
|
18
|
+
|
19
|
+
# the names are meant to be human readable and may change in the future, use the #dialect, #order and #family
|
20
|
+
# attributes for any programmatic comparisons
|
21
|
+
Definition = Struct.new(:dialect, :order, :family, :version_name, :full_name) do
|
22
|
+
alias_method :short_name, :version_name
|
23
|
+
end
|
24
|
+
|
25
|
+
ALL = [
|
26
|
+
Definition.new('NT LM 0.12', ORDER_SMB1, FAMILY_SMB1, VERSION_SMB1, 'SMB v1 (NT LM 0.12)'.freeze),
|
27
|
+
Definition.new('0x0202', ORDER_SMB2, FAMILY_SMB2, VERSION_SMB2, 'SMB v2.0.2'.freeze),
|
28
|
+
Definition.new('0x0210', ORDER_SMB2, FAMILY_SMB2, VERSION_SMB2, 'SMB v2.1'.freeze),
|
29
|
+
Definition.new('0x02ff', ORDER_SMB2, FAMILY_SMB2, VERSION_SMB2, 'SMB 2.???'.freeze), # wildcard revision
|
30
|
+
Definition.new('0x0300', ORDER_SMB2, FAMILY_SMB3, VERSION_SMB3, 'SMB v3.0'.freeze),
|
31
|
+
Definition.new('0x0302', ORDER_SMB2, FAMILY_SMB3, VERSION_SMB3, 'SMB v3.0.2'.freeze),
|
32
|
+
Definition.new('0x0311', ORDER_SMB2, FAMILY_SMB3, VERSION_SMB3, 'SMB v3.1.1'.freeze)
|
33
|
+
].map { |definition| [definition.dialect, definition] }.to_h
|
34
|
+
|
35
|
+
#
|
36
|
+
# Retrieve a dialect definition. The definition contains metadata describing the particular dialect.
|
37
|
+
#
|
38
|
+
# @param [Integer, String] dialect the dialect to retrieve the definition for
|
39
|
+
# @return [Definition, nil] the definition if it was found
|
40
|
+
def self.[](dialect)
|
41
|
+
dialect = '0x%04x' % dialect if dialect.is_a? Integer
|
42
|
+
ALL[dialect]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -9,7 +9,7 @@ module RubySMB
|
|
9
9
|
def nbss(packet)
|
10
10
|
nbss = RubySMB::Nbss::SessionHeader.new
|
11
11
|
nbss.session_packet_type = RubySMB::Nbss::SESSION_MESSAGE
|
12
|
-
nbss.stream_protocol_length = packet.do_num_bytes
|
12
|
+
nbss.stream_protocol_length = packet.do_num_bytes.to_i
|
13
13
|
nbss.to_binary_s
|
14
14
|
end
|
15
15
|
|
@@ -55,7 +55,7 @@ module RubySMB
|
|
55
55
|
|
56
56
|
# Read a packet off the wire and parse it into a string
|
57
57
|
#
|
58
|
-
# @param full_response [Boolean] whether to include the NetBios Session Service header in the
|
58
|
+
# @param full_response [Boolean] whether to include the NetBios Session Service header in the response
|
59
59
|
# @return [String] the raw response (including the NetBios Session Service header if full_response is true)
|
60
60
|
# @raise [RubySMB::Error::NetBiosSessionService] if there's an error reading the first 4 bytes,
|
61
61
|
# which are assumed to be the NetBiosSessionService header.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RubySMB
|
2
|
+
module Gss
|
3
|
+
module Provider
|
4
|
+
module Authenticator
|
5
|
+
#
|
6
|
+
# The base class for a GSS provider's unique authenticator. This provides a common interface and is not usable
|
7
|
+
# on it's own. The provider-specific authentication logic is defined within this authenticator class which
|
8
|
+
# actually runs the authentication routine.
|
9
|
+
#
|
10
|
+
class Base
|
11
|
+
# @param [Provider::Base] provider the GSS provider that this instance is an authenticator for
|
12
|
+
# @param server_client the client instance that this will be an authenticator for
|
13
|
+
def initialize(provider, server_client)
|
14
|
+
@provider = provider
|
15
|
+
@server_client = server_client
|
16
|
+
@session_key = nil
|
17
|
+
reset!
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Process a GSS authentication buffer. If no buffer is specified, the request is assumed to be the first in
|
22
|
+
# the negotiation sequence.
|
23
|
+
#
|
24
|
+
# @param [String, nil] buffer the request GSS request buffer that should be processed
|
25
|
+
# @return [Gss::Provider::Result] the result of the processed GSS request
|
26
|
+
def process(request_buffer=nil)
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Reset the authenticator's state, wiping anything related to a partial or complete authentication process.
|
32
|
+
#
|
33
|
+
def reset!
|
34
|
+
@session_key = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :session_key
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
require 'ruby_smb/ntlm'
|
2
|
+
|
3
|
+
module RubySMB
|
4
|
+
module Gss
|
5
|
+
module Provider
|
6
|
+
#
|
7
|
+
# A GSS provider that authenticates clients via the NT LAN Manager (NTLM) Security Support Provider (NTLMSSP)
|
8
|
+
# protocol.
|
9
|
+
#
|
10
|
+
class NTLM < Base
|
11
|
+
include RubySMB::NTLM
|
12
|
+
|
13
|
+
# An account representing an identity for which this provider will accept authentication attempts.
|
14
|
+
Account = Struct.new(:username, :password, :domain) do
|
15
|
+
def to_s
|
16
|
+
"#{domain}\\#{username}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Authenticator < Authenticator::Base
|
21
|
+
def reset!
|
22
|
+
super
|
23
|
+
@server_challenge = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def process(request_buffer=nil)
|
27
|
+
if request_buffer.nil?
|
28
|
+
# this is only NTLMSSP (as opposed to SPNEGO + NTLMSSP)
|
29
|
+
buffer = OpenSSL::ASN1::ASN1Data.new([
|
30
|
+
Gss::OID_SPNEGO,
|
31
|
+
OpenSSL::ASN1::ASN1Data.new([
|
32
|
+
OpenSSL::ASN1::Sequence.new([
|
33
|
+
OpenSSL::ASN1::ASN1Data.new([
|
34
|
+
OpenSSL::ASN1::Sequence.new([
|
35
|
+
Gss::OID_NTLMSSP
|
36
|
+
])
|
37
|
+
], 0, :CONTEXT_SPECIFIC),
|
38
|
+
OpenSSL::ASN1::ASN1Data.new([
|
39
|
+
OpenSSL::ASN1::ASN1Data.new([
|
40
|
+
OpenSSL::ASN1::ASN1Data.new([
|
41
|
+
OpenSSL::ASN1::GeneralString.new('not_defined_in_RFC4178@please_ignore')
|
42
|
+
], 0, :CONTEXT_SPECIFIC)
|
43
|
+
], 16, :UNIVERSAL)
|
44
|
+
], 3, :CONTEXT_SPECIFIC)
|
45
|
+
])
|
46
|
+
], 0, :CONTEXT_SPECIFIC)
|
47
|
+
], 0, :APPLICATION).to_der
|
48
|
+
return Result.new(buffer, WindowsError::NTStatus::STATUS_SUCCESS)
|
49
|
+
end
|
50
|
+
|
51
|
+
begin
|
52
|
+
gss_api = OpenSSL::ASN1.decode(request_buffer)
|
53
|
+
rescue OpenSSL::ASN1::ASN1Error
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
if gss_api&.tag == 0 && gss_api&.tag_class == :APPLICATION
|
58
|
+
result = process_gss_type1(gss_api)
|
59
|
+
elsif gss_api&.tag == 1 && gss_api&.tag_class == :CONTEXT_SPECIFIC
|
60
|
+
result = process_gss_type3(gss_api)
|
61
|
+
end
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Process the NTLM type 1 message and build a type 2 response message.
|
68
|
+
#
|
69
|
+
# @param [Net::NTLM::Message::Type1] type1_msg the NTLM type 1 message received by the client that should be
|
70
|
+
# processed
|
71
|
+
# @return [Net::NTLM::Message::Type2] the NTLM type 2 response message with which to reply to the client
|
72
|
+
def process_ntlm_type1(type1_msg)
|
73
|
+
type2_msg = Net::NTLM::Message::Type2.new.tap do |msg|
|
74
|
+
msg.target_name = 'LOCALHOST'.encode('UTF-16LE').b
|
75
|
+
msg.flag = 0
|
76
|
+
%i{ KEY56 KEY128 KEY_EXCHANGE UNICODE TARGET_INFO VERSION_INFO }.each do |flag|
|
77
|
+
msg.flag |= NTLM::NEGOTIATE_FLAGS.fetch(flag)
|
78
|
+
end
|
79
|
+
|
80
|
+
@server_challenge = @provider.generate_server_challenge
|
81
|
+
msg.challenge = @server_challenge.unpack1('Q<') # 64-bit unsigned, little endian (uint64_t)
|
82
|
+
target_info = Net::NTLM::TargetInfo.new('')
|
83
|
+
target_info.av_pairs.merge!({
|
84
|
+
Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => @provider.netbios_domain.encode('UTF-16LE').b,
|
85
|
+
Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => @provider.netbios_hostname.encode('UTF-16LE').b,
|
86
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_DOMAIN_NAME => @provider.dns_domain.encode('UTF-16LE').b,
|
87
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME => @provider.dns_hostname.encode('UTF-16LE').b,
|
88
|
+
Net::NTLM::TargetInfo::MSV_AV_TIMESTAMP => [(Time.now.to_i + Net::NTLM::TIME_OFFSET) * Field::FileTime::NS_MULTIPLIER].pack('Q')
|
89
|
+
})
|
90
|
+
msg.target_info = target_info.to_s
|
91
|
+
msg.enable(:target_info)
|
92
|
+
msg.context = 0
|
93
|
+
msg.enable(:context)
|
94
|
+
msg.os_version = NTLM::OSVersion.new(major: 6, minor: 3).to_binary_s
|
95
|
+
msg.enable(:os_version)
|
96
|
+
end
|
97
|
+
|
98
|
+
type2_msg
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Process the NTLM type 3 message and either accept or reject the authentication attempt.
|
103
|
+
#
|
104
|
+
# @param [Net::NTLM::Message::Type3] type3_msg the NTLM type 3 message received by the client that should be
|
105
|
+
# processed
|
106
|
+
# @return [WindowsError::ErrorCode] an NT Status error code representing the operations outcome where
|
107
|
+
# STATUS_SUCCESS is a successful authentication attempt and anything else is a failure
|
108
|
+
def process_ntlm_type3(type3_msg)
|
109
|
+
if type3_msg.user == '' && type3_msg.domain == ''
|
110
|
+
if @provider.allow_anonymous
|
111
|
+
return WindowsError::NTStatus::STATUS_SUCCESS
|
112
|
+
end
|
113
|
+
|
114
|
+
return WindowsError::NTStatus::STATUS_LOGON_FAILURE
|
115
|
+
end
|
116
|
+
|
117
|
+
account = @provider.get_account(
|
118
|
+
type3_msg.user,
|
119
|
+
domain: type3_msg.domain
|
120
|
+
)
|
121
|
+
return WindowsError::NTStatus::STATUS_LOGON_FAILURE if account.nil?
|
122
|
+
|
123
|
+
matches = false
|
124
|
+
case type3_msg.ntlm_version
|
125
|
+
when :ntlmv1
|
126
|
+
my_ntlm_response = Net::NTLM::ntlm_response(
|
127
|
+
ntlm_hash: Net::NTLM::ntlm_hash(account.password.encode('UTF-16LE'), unicode: true),
|
128
|
+
challenge: @server_challenge
|
129
|
+
)
|
130
|
+
matches = my_ntlm_response == type3_msg.ntlm_response
|
131
|
+
when :ntlmv2
|
132
|
+
digest = OpenSSL::Digest::MD5.new
|
133
|
+
their_nt_proof_str = type3_msg.ntlm_response[0...digest.digest_length]
|
134
|
+
their_blob = type3_msg.ntlm_response[digest.digest_length..-1]
|
135
|
+
|
136
|
+
ntlmv2_hash = Net::NTLM.ntlmv2_hash(
|
137
|
+
account.username.encode('UTF-16LE'),
|
138
|
+
account.password.encode('UTF-16LE'),
|
139
|
+
type3_msg.domain.encode('UTF-16LE'), # don't use the account domain because of the special '.' value
|
140
|
+
{client_challenge: their_blob[16...24], unicode: true}
|
141
|
+
)
|
142
|
+
|
143
|
+
my_nt_proof_str = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_hash, @server_challenge + their_blob)
|
144
|
+
matches = my_nt_proof_str == their_nt_proof_str
|
145
|
+
if matches
|
146
|
+
user_session_key = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_hash, my_nt_proof_str)
|
147
|
+
if type3_msg.flag & NTLM::NEGOTIATE_FLAGS[:KEY_EXCHANGE] == NTLM::NEGOTIATE_FLAGS[:KEY_EXCHANGE] && type3_msg.session_key.length == 16
|
148
|
+
rc4 = OpenSSL::Cipher.new('rc4')
|
149
|
+
rc4.decrypt
|
150
|
+
rc4.key = user_session_key
|
151
|
+
@session_key = rc4.update type3_msg.session_key
|
152
|
+
@session_key << rc4.final
|
153
|
+
else
|
154
|
+
@session_key = user_session_key
|
155
|
+
end
|
156
|
+
end
|
157
|
+
else
|
158
|
+
# the only other value Net::NTLM will return for this is ntlm_session
|
159
|
+
raise NotImplementedError, "authentication via ntlm version #{type3_msg.ntlm_version} is not supported"
|
160
|
+
end
|
161
|
+
|
162
|
+
return WindowsError::NTStatus::STATUS_LOGON_FAILURE unless matches
|
163
|
+
|
164
|
+
WindowsError::NTStatus::STATUS_SUCCESS
|
165
|
+
end
|
166
|
+
|
167
|
+
attr_accessor :server_challenge
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
# take the GSS blob, extract the NTLM type 1 message and pass it to the process method to build the response
|
172
|
+
# which is then put back into a new GSS reply-blob
|
173
|
+
def process_gss_type1(gss_api)
|
174
|
+
unless Gss.asn1dig(gss_api, 1, 0, 0, 0, 0)&.value == Gss::OID_NTLMSSP.value
|
175
|
+
return
|
176
|
+
end
|
177
|
+
|
178
|
+
raw_type1_msg = Gss.asn1dig(gss_api, 1, 0, 1, 0)&.value
|
179
|
+
return unless raw_type1_msg
|
180
|
+
|
181
|
+
type1_msg = Net::NTLM::Message.parse(raw_type1_msg)
|
182
|
+
if type1_msg.flag & NTLM::NEGOTIATE_FLAGS[:UNICODE] == NTLM::NEGOTIATE_FLAGS[:UNICODE]
|
183
|
+
type1_msg.domain.force_encoding('UTF-16LE')
|
184
|
+
type1_msg.workstation.force_encoding('UTF-16LE')
|
185
|
+
end
|
186
|
+
type2_msg = process_ntlm_type1(type1_msg)
|
187
|
+
|
188
|
+
Result.new(Gss.gss_type2(type2_msg.serialize), WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED)
|
189
|
+
end
|
190
|
+
|
191
|
+
# take the GSS blob, extract the NTLM type 3 message and pass it to the process method to build the response
|
192
|
+
# which is then put back into a new GSS reply-blob
|
193
|
+
def process_gss_type3(gss_api)
|
194
|
+
neg_token_init = Hash[RubySMB::Gss.asn1dig(gss_api, 0).value.map { |obj| [obj.tag, obj.value[0].value] }]
|
195
|
+
raw_type3_msg = neg_token_init[2]
|
196
|
+
|
197
|
+
type3_msg = Net::NTLM::Message.parse(raw_type3_msg)
|
198
|
+
if type3_msg.flag & NTLM::NEGOTIATE_FLAGS[:UNICODE] == NTLM::NEGOTIATE_FLAGS[:UNICODE]
|
199
|
+
type3_msg.domain.force_encoding('UTF-16LE')
|
200
|
+
type3_msg.user.force_encoding('UTF-16LE')
|
201
|
+
type3_msg.workstation.force_encoding('UTF-16LE')
|
202
|
+
end
|
203
|
+
|
204
|
+
nt_status = process_ntlm_type3(type3_msg)
|
205
|
+
buffer = identity = nil
|
206
|
+
|
207
|
+
case nt_status
|
208
|
+
when WindowsError::NTStatus::STATUS_SUCCESS
|
209
|
+
buffer = OpenSSL::ASN1::ASN1Data.new([
|
210
|
+
OpenSSL::ASN1::Sequence.new([
|
211
|
+
OpenSSL::ASN1::ASN1Data.new([
|
212
|
+
OpenSSL::ASN1::Enumerated.new(OpenSSL::BN.new(0)),
|
213
|
+
], 0, :CONTEXT_SPECIFIC)
|
214
|
+
])
|
215
|
+
], 1, :CONTEXT_SPECIFIC).to_der
|
216
|
+
|
217
|
+
account = @provider.get_account(
|
218
|
+
type3_msg.user,
|
219
|
+
domain: type3_msg.domain
|
220
|
+
)
|
221
|
+
if account.nil?
|
222
|
+
if @provider.allow_anonymous
|
223
|
+
identity = IDENTITY_ANONYMOUS
|
224
|
+
end
|
225
|
+
else
|
226
|
+
identity = account.to_s
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
Result.new(buffer, nt_status, identity)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# @param [Boolean] allow_anonymous whether or not to allow anonymous authentication attempts
|
235
|
+
# @param [String] default_domain the default domain to use for authentication, unless specified 'WORKGROUP' will
|
236
|
+
# be used
|
237
|
+
def initialize(allow_anonymous: false, default_domain: 'WORKGROUP')
|
238
|
+
raise ArgumentError, 'Must specify a default domain' unless default_domain
|
239
|
+
|
240
|
+
@allow_anonymous = allow_anonymous
|
241
|
+
@default_domain = default_domain
|
242
|
+
@accounts = []
|
243
|
+
@generate_server_challenge = -> { SecureRandom.bytes(8) }
|
244
|
+
|
245
|
+
@dns_domain = @netbios_domain = 'LOCALDOMAIN'
|
246
|
+
@dns_hostname = @netbios_hostname = 'LOCALHOST'
|
247
|
+
end
|
248
|
+
|
249
|
+
#
|
250
|
+
# Generate the 8-byte server challenge. If a block is specified, it's used as the challenge generation routine
|
251
|
+
# and should return an 8-byte value.
|
252
|
+
#
|
253
|
+
# @return [String] an 8-byte challenge value
|
254
|
+
def generate_server_challenge(&block)
|
255
|
+
if block.nil?
|
256
|
+
@generate_server_challenge.call
|
257
|
+
else
|
258
|
+
@generate_server_challenge = block
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def new_authenticator(server_client)
|
263
|
+
# build and return an instance that can process and track stateful information for a particular connection but
|
264
|
+
# that's backed by this particular provider
|
265
|
+
Authenticator.new(self, server_client)
|
266
|
+
end
|
267
|
+
|
268
|
+
#
|
269
|
+
# Lookup and return an account based on the username and optionally, the domain. If no domain is specified or
|
270
|
+
# or it is the special value '.', the default domain will be used. The username and domain values are case
|
271
|
+
# insensitive.
|
272
|
+
#
|
273
|
+
# @param [String] username the username of the account to fetch.
|
274
|
+
# @param [String, nil] domain the domain in which the account to fetch exists.
|
275
|
+
# @return [Account, nil] the account if it was found
|
276
|
+
def get_account(username, domain: nil)
|
277
|
+
# the username and password values should use the native encoding for the comparison in the #find operation
|
278
|
+
username = username.downcase
|
279
|
+
domain = @default_domain if domain.nil? || domain == '.'.encode(domain.encoding)
|
280
|
+
domain = domain.downcase
|
281
|
+
@accounts.find { |account| account.username.encode(username.encoding).downcase == username && account.domain.encode(domain.encoding).downcase == domain }
|
282
|
+
end
|
283
|
+
|
284
|
+
#
|
285
|
+
# Add an account to the database.
|
286
|
+
#
|
287
|
+
# @param [String] username the username of the account to add
|
288
|
+
# @param [String] password either the plaintext password or the NTLM hash of the account to add
|
289
|
+
# @param [String] domain the domain of the account to add, if not specified, the @default_domain will be used
|
290
|
+
def put_account(username, password, domain: nil)
|
291
|
+
domain = @default_domain if domain.nil? || domain == '.'.encode(domain.encoding)
|
292
|
+
@accounts << Account.new(username, password, domain)
|
293
|
+
end
|
294
|
+
|
295
|
+
#
|
296
|
+
# The default domain value to use for accounts which do not have one specified or use the special '.' value.
|
297
|
+
attr_reader :default_domain
|
298
|
+
|
299
|
+
attr_accessor :dns_domain, :dns_hostname, :netbios_domain, :netbios_hostname
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module RubySMB
|
2
|
+
module Gss
|
3
|
+
#
|
4
|
+
# This module provides GSS based authentication.
|
5
|
+
#
|
6
|
+
module Provider
|
7
|
+
# A special constant implying that the authenticated user is anonymous.
|
8
|
+
IDENTITY_ANONYMOUS = :anonymous
|
9
|
+
# The result of a processed GSS request.
|
10
|
+
Result = Struct.new(:buffer, :nt_status, :identity)
|
11
|
+
|
12
|
+
#
|
13
|
+
# The base class for a GSS authentication provider. This class defines a common interface and is not usable as a
|
14
|
+
# provider on its own.
|
15
|
+
#
|
16
|
+
class Base
|
17
|
+
# Create a new, client-specific authenticator instance. This new instance is then able to track the unique state
|
18
|
+
# of a particular client / connection.
|
19
|
+
#
|
20
|
+
# @param [Server::ServerClient] server_client the client instance that this the authenticator will be for
|
21
|
+
def new_authenticator(server_client)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Whether or not anonymous authentication attempts should be permitted.
|
27
|
+
#
|
28
|
+
attr_accessor :allow_anonymous
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'ruby_smb/gss/provider/authenticator'
|
35
|
+
require 'ruby_smb/gss/provider/ntlm'
|