ruby_smb 3.3.22 → 3.3.25
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
- data/lib/ruby_smb/client.rb +14 -3
- data/lib/ruby_smb/error.rb +11 -1
- data/lib/ruby_smb/gss/provider/kerberos.rb +155 -0
- data/lib/ruby_smb/gss/provider/multi.rb +129 -0
- data/lib/ruby_smb/gss/provider/ntlm.rb +6 -21
- data/lib/ruby_smb/gss/provider.rb +22 -0
- data/lib/ruby_smb/gss/spnego_neg_token_init.rb +81 -0
- data/lib/ruby_smb/gss/spnego_neg_token_targ.rb +27 -0
- data/lib/ruby_smb/gss.rb +23 -0
- data/lib/ruby_smb/nbss/name_service_header_flags.rb +18 -0
- data/lib/ruby_smb/nbss/negative_session_response.rb +5 -5
- data/lib/ruby_smb/nbss/node_status.rb +137 -0
- data/lib/ruby_smb/nbss/node_status_request.rb +38 -0
- data/lib/ruby_smb/nbss/node_status_response.rb +85 -0
- data/lib/ruby_smb/nbss.rb +11 -0
- data/lib/ruby_smb/peer_info.rb +3 -1
- data/lib/ruby_smb/version.rb +1 -1
- data/ruby_smb.gemspec +3 -0
- data/spec/lib/ruby_smb/client_spec.rb +60 -0
- data/spec/lib/ruby_smb/dcerpc/client_spec.rb +8 -0
- data/spec/lib/ruby_smb/gss/provider/kerberos_spec.rb +206 -0
- data/spec/lib/ruby_smb/gss/provider/multi_spec.rb +174 -0
- data/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb +13 -0
- data/spec/lib/ruby_smb/gss/spnego_spec.rb +80 -0
- data/spec/lib/ruby_smb/nbss/node_status_request_spec.rb +52 -0
- data/spec/lib/ruby_smb/nbss/node_status_response_spec.rb +75 -0
- data/spec/lib/ruby_smb/nbss/node_status_spec.rb +146 -0
- data/spec/lib/ruby_smb/ntlm/client/session_spec.rb +19 -40
- metadata +36 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bda3450e062ff69f5c20600086e7c8686d539b44a339f560764c077c6a1a3cc
|
|
4
|
+
data.tar.gz: 3c2ec535d37e7880e4ba7d023ff8d381f33da0bb47c28424bc4f3828d13f9e2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e57991b0b74090c01a6b6568dce52828e16045255ea2a3debc0afac2fa2f5bbff85e7319b21c36377a5e8a49e327bad518ebf58792e0e3c5a3d338d953064b1a
|
|
7
|
+
data.tar.gz: cb6a8ef6b78ca308df71d89b8c290dcce2ffcc64f155f41be7a66060713aa2c3c990ee4aa8e95d7308f0467e8b58d98ed0891523451086932f5205a26e1df214
|
data/lib/ruby_smb/client.rb
CHANGED
|
@@ -350,7 +350,7 @@ module RubySMB
|
|
|
350
350
|
@server_max_write_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
|
|
351
351
|
@server_max_transact_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
|
|
352
352
|
@server_supports_multi_credit = false
|
|
353
|
-
@server_supports_nt_smbs
|
|
353
|
+
@server_supports_nt_smbs = true
|
|
354
354
|
|
|
355
355
|
# SMB 3.x options
|
|
356
356
|
# this merely initializes the default value for session encryption, it may be changed as necessary when a
|
|
@@ -668,6 +668,14 @@ module RubySMB
|
|
|
668
668
|
|
|
669
669
|
# Requests a NetBIOS Session Service using the provided name.
|
|
670
670
|
#
|
|
671
|
+
# On refusal the raised {RubySMB::Error::NetBiosSessionService} carries the
|
|
672
|
+
# numeric NBSS `error_code`. A {RubySMB::Nbss::CALLED_NAME_NOT_PRESENT} rejection of
|
|
673
|
+
# the default `'*SMBSERVER'` name means the server (e.g. Windows 9x) wants
|
|
674
|
+
# its real name: resolve it with {RubySMB::Nbss::NodeStatus.file_server_name},
|
|
675
|
+
# reconnect (the server drops the connection after a negative response), and
|
|
676
|
+
# retry. Reconnecting is left to the caller so the new socket is routed the
|
|
677
|
+
# same way as the original (e.g. through a Metasploit pivot).
|
|
678
|
+
#
|
|
671
679
|
# @param name [String] the NetBIOS name to request
|
|
672
680
|
# @return [TrueClass] if session request is granted
|
|
673
681
|
# @raise [RubySMB::Error::NetBiosSessionService] if session request is refused
|
|
@@ -679,8 +687,11 @@ module RubySMB
|
|
|
679
687
|
begin
|
|
680
688
|
session_header = RubySMB::Nbss::SessionHeader.read(raw_response)
|
|
681
689
|
if session_header.session_packet_type == RubySMB::Nbss::NEGATIVE_SESSION_RESPONSE
|
|
682
|
-
negative_session_response =
|
|
683
|
-
raise RubySMB::Error::NetBiosSessionService
|
|
690
|
+
negative_session_response = RubySMB::Nbss::NegativeSessionResponse.read(raw_response)
|
|
691
|
+
raise RubySMB::Error::NetBiosSessionService.new(
|
|
692
|
+
"Session Request failed: #{negative_session_response.error_msg}",
|
|
693
|
+
error_code: negative_session_response.error_code.to_i
|
|
694
|
+
)
|
|
684
695
|
end
|
|
685
696
|
rescue IOError
|
|
686
697
|
raise RubySMB::Error::InvalidPacket, 'Not a NBSS packet'
|
data/lib/ruby_smb/error.rb
CHANGED
|
@@ -11,7 +11,17 @@ module RubySMB
|
|
|
11
11
|
|
|
12
12
|
# Raised when there is a problem with communication over NetBios Session Service
|
|
13
13
|
# @see https://wiki.wireshark.org/NetBIOS/NBSS
|
|
14
|
-
class NetBiosSessionService < RubySMBError
|
|
14
|
+
class NetBiosSessionService < RubySMBError
|
|
15
|
+
# The numeric NBSS error code from a NEGATIVE_SESSION_RESPONSE, or nil
|
|
16
|
+
# if the error was raised outside that context.
|
|
17
|
+
# @return [Integer, nil]
|
|
18
|
+
attr_reader :error_code
|
|
19
|
+
|
|
20
|
+
def initialize(msg = nil, error_code: nil)
|
|
21
|
+
@error_code = error_code
|
|
22
|
+
super(msg)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
15
25
|
|
|
16
26
|
# Raised when trying to parse raw binary into a Packet and the data
|
|
17
27
|
# is invalid.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module Gss
|
|
3
|
+
module Provider
|
|
4
|
+
#
|
|
5
|
+
# A GSS provider that advertises Kerberos and surfaces the mechanism token a client sends, without interpreting
|
|
6
|
+
# it.
|
|
7
|
+
#
|
|
8
|
+
# A Kerberos AP-REQ is encrypted to the service the client believes it is talking to, so a server that does not
|
|
9
|
+
# hold that service's key cannot read it. This provider therefore does not attempt to: it hands the token to a
|
|
10
|
+
# handler and lets that decide what to tell the client. That is enough for a server to observe or forward
|
|
11
|
+
# Kerberos authentication, and it keeps Kerberos message parsing out of this library entirely.
|
|
12
|
+
#
|
|
13
|
+
# Accepting Kerberos properly, by decrypting the ticket with a service key and validating the PAC, is a separate
|
|
14
|
+
# concern and is not implemented here.
|
|
15
|
+
#
|
|
16
|
+
# The token handed to the handler is the mechanism token exactly as the client sent it. For Kerberos that is a
|
|
17
|
+
# GSS-API InitialContextToken (RFC 2743 section 3.1), which wraps the mechanism OID and the token identifier
|
|
18
|
+
# around the Kerberos message:
|
|
19
|
+
#
|
|
20
|
+
# 60 82 0c 0e InitialContextToken
|
|
21
|
+
# 06 09 2a 86 48 .. the mechanism OID
|
|
22
|
+
# 01 00 the token id, here KRB_AP_REQ
|
|
23
|
+
# 6e 82 0b fd .. the AP-REQ itself
|
|
24
|
+
#
|
|
25
|
+
# Note that the token id follows the OID rather than starting the token, and that the framing around it is not
|
|
26
|
+
# valid ASN.1, so OpenSSL::ASN1.decode will not parse it. {.token_id} reads it without decoding the payload.
|
|
27
|
+
#
|
|
28
|
+
# @example Capture the token a client sends
|
|
29
|
+
# provider = RubySMB::Gss::Provider::Kerberos.new
|
|
30
|
+
# provider.on_mech_token do |token, authenticator|
|
|
31
|
+
# if RubySMB::Gss::Provider::Kerberos.token_id(token) == RubySMB::Gss::Provider::Kerberos::TOK_ID_KRB_AP_REQ
|
|
32
|
+
# # forward or record the AP-REQ, then decide how to reply
|
|
33
|
+
# end
|
|
34
|
+
# # a handler must return a Result; there is no service key here to validate the ticket, so refuse it
|
|
35
|
+
# RubySMB::Gss::Provider::Result.new(nil, WindowsError::NTStatus::STATUS_LOGON_FAILURE)
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
class Kerberos < Base
|
|
39
|
+
# The GSS token identifiers that may appear in a Kerberos mechanism token, per RFC 4121 section 4.1. They are
|
|
40
|
+
# provided so a handler can tell the messages apart without decoding the payload.
|
|
41
|
+
TOK_ID_KRB_AP_REQ = "\x01\x00".b.freeze
|
|
42
|
+
TOK_ID_KRB_AP_REP = "\x02\x00".b.freeze
|
|
43
|
+
TOK_ID_KRB_ERROR = "\x03\x00".b.freeze
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Read the token identifier out of a GSS-API InitialContextToken, so a handler can tell an AP-REQ from an
|
|
47
|
+
# AP-REP or a KRB-ERROR. The identifier follows the mechanism OID rather than starting the token, and the
|
|
48
|
+
# framing is not valid ASN.1, so it is located by walking the lengths rather than by decoding.
|
|
49
|
+
#
|
|
50
|
+
# @param [String] token the mechanism token as received
|
|
51
|
+
# @return [String, nil] the two byte identifier, or nil if the token is not shaped as expected
|
|
52
|
+
def self.token_id(token)
|
|
53
|
+
return nil if token.nil? || token.bytesize < 4 || token.getbyte(0) != 0x60
|
|
54
|
+
|
|
55
|
+
length_byte = token.getbyte(1)
|
|
56
|
+
# a long form length says how many bytes carry the length, a short form is the length itself
|
|
57
|
+
offset = length_byte > 0x80 ? 2 + (length_byte & 0x7f) : 2
|
|
58
|
+
return nil if token.getbyte(offset) != 0x06 # the mechanism OID must follow
|
|
59
|
+
|
|
60
|
+
offset += 2 + token.getbyte(offset + 1)
|
|
61
|
+
token.byteslice(offset, 2)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param [Proc, nil] block an optional handler for received mechanism tokens, see {#on_mech_token}.
|
|
65
|
+
def initialize(&block)
|
|
66
|
+
@on_mech_token = block
|
|
67
|
+
@allow_anonymous = false
|
|
68
|
+
@allow_guests = false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def new_authenticator(server_client)
|
|
72
|
+
Authenticator.new(self, server_client)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def mech_types
|
|
76
|
+
# both are advertised because Microsoft clients may select either
|
|
77
|
+
[Gss::OID_KERBEROS_5, Gss::OID_MICROSOFT_KERBEROS_5]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Set or invoke the handler called when a client sends a Kerberos mechanism token.
|
|
82
|
+
#
|
|
83
|
+
# The handler receives the opaque token and the authenticator that received it, and returns the {Result} to
|
|
84
|
+
# reply with. When no handler is set the authentication attempt is rejected, since this provider cannot
|
|
85
|
+
# validate a ticket on its own.
|
|
86
|
+
#
|
|
87
|
+
# @param [String] token the mechanism token, as sent by the client
|
|
88
|
+
# @param [Authenticator] authenticator the authenticator that received it
|
|
89
|
+
# @return [Result, nil]
|
|
90
|
+
def on_mech_token(token=nil, authenticator=nil, &block)
|
|
91
|
+
if block.nil?
|
|
92
|
+
return nil if @on_mech_token.nil?
|
|
93
|
+
|
|
94
|
+
@on_mech_token.call(token, authenticator)
|
|
95
|
+
else
|
|
96
|
+
@on_mech_token = block
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class Authenticator < Authenticator::Base
|
|
101
|
+
def reset!
|
|
102
|
+
super
|
|
103
|
+
@mech_token = nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @return [String, nil] the most recent mechanism token received from the client.
|
|
107
|
+
attr_reader :mech_token
|
|
108
|
+
|
|
109
|
+
def process(request_buffer=nil)
|
|
110
|
+
if request_buffer.nil?
|
|
111
|
+
return Result.new(Gss.gss_neg_token_init(@provider.mech_types), WindowsError::NTStatus::STATUS_SUCCESS)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
token = extract_mech_token(request_buffer)
|
|
115
|
+
if token.nil?
|
|
116
|
+
logger.warn('Received a Kerberos request carrying no mechanism token')
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
@mech_token = token
|
|
121
|
+
result = @provider.on_mech_token(token, self)
|
|
122
|
+
# a handler must return a Result: the session setup path calls nt_status on whatever comes back, so
|
|
123
|
+
# anything else (a missing handler, or a handler that returns e.g. a boolean) is refused here rather
|
|
124
|
+
# than handed on to crash the caller
|
|
125
|
+
return result if result.is_a?(Result)
|
|
126
|
+
|
|
127
|
+
Result.new(nil, WindowsError::NTStatus::STATUS_LOGON_FAILURE)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
#
|
|
133
|
+
# Pull the mechanism token out of a SPNEGO NegTokenInit or NegTokenResp. The token is returned exactly as the
|
|
134
|
+
# client sent it, so a caller that forwards it elsewhere does not alter the ticket it contains.
|
|
135
|
+
#
|
|
136
|
+
# @param [String] request_buffer the SPNEGO token as received
|
|
137
|
+
# @return [String, nil]
|
|
138
|
+
def extract_mech_token(request_buffer)
|
|
139
|
+
# the identifier octet tells the two SPNEGO tokens apart: an InitialContextToken carrying a NegTokenInit is
|
|
140
|
+
# tagged [APPLICATION 0], a NegTokenResp continuing an exchange is tagged [CONTEXT 1]
|
|
141
|
+
case request_buffer.b.getbyte(0)
|
|
142
|
+
when 0x60
|
|
143
|
+
SpnegoNegTokenInit.parse(request_buffer).mech_token
|
|
144
|
+
when 0xa1
|
|
145
|
+
SpnegoNegTokenTarg.parse(request_buffer).response_token
|
|
146
|
+
end
|
|
147
|
+
rescue RASN1::ASN1Error => e
|
|
148
|
+
logger.error("Failed to parse the SPNEGO token (#{e.message})")
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module Gss
|
|
3
|
+
module Provider
|
|
4
|
+
#
|
|
5
|
+
# A GSS provider that offers more than one authentication mechanism to the client and routes each request to
|
|
6
|
+
# whichever of its sub-providers understands the mechanism the client selected.
|
|
7
|
+
#
|
|
8
|
+
# SPNEGO exists so that a client and server can agree on a mechanism, but a server that only ever advertises one
|
|
9
|
+
# has nothing to negotiate. This provider advertises the mechanisms of every provider it holds, in the order they
|
|
10
|
+
# were given, so a client can pick the one it prefers.
|
|
11
|
+
#
|
|
12
|
+
# @example Offer Kerberos, falling back to NTLM
|
|
13
|
+
# provider = RubySMB::Gss::Provider::Multi.new([kerberos_provider, ntlm_provider])
|
|
14
|
+
# RubySMB::Server.new(gss_provider: provider)
|
|
15
|
+
#
|
|
16
|
+
class Multi < Base
|
|
17
|
+
#
|
|
18
|
+
# @param [Array<Provider::Base>] providers the providers to offer, in preference order (most preferred first).
|
|
19
|
+
def initialize(providers)
|
|
20
|
+
raise ArgumentError, 'at least one provider is required' if providers.nil? || providers.empty?
|
|
21
|
+
|
|
22
|
+
@providers = providers.dup.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Array<Provider::Base>] the providers this instance will route between.
|
|
26
|
+
attr_reader :providers
|
|
27
|
+
|
|
28
|
+
def new_authenticator(server_client)
|
|
29
|
+
Authenticator.new(self, server_client)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Every mechanism offered by every provider, in provider order, with duplicates removed so a mechanism supported
|
|
34
|
+
# by two providers is only advertised once.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<OpenSSL::ASN1::ObjectId>]
|
|
37
|
+
def mech_types
|
|
38
|
+
@providers.flat_map(&:mech_types).uniq(&:oid)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# The first provider that handles the specified mechanism, or nil if none do.
|
|
43
|
+
#
|
|
44
|
+
# @param [OpenSSL::ASN1::ObjectId] mech_type the mechanism selected by the client
|
|
45
|
+
# @return [Provider::Base, nil]
|
|
46
|
+
def provider_for(mech_type)
|
|
47
|
+
@providers.find { |provider| provider.supports_mech_type?(mech_type) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def allow_anonymous
|
|
51
|
+
@providers.any?(&:allow_anonymous)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def allow_guests
|
|
55
|
+
@providers.any?(&:allow_guests)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Authenticator < Authenticator::Base
|
|
59
|
+
def initialize(provider, server_client)
|
|
60
|
+
# built lazily, so a provider that is advertised but never selected is never instantiated
|
|
61
|
+
@authenticators = {}
|
|
62
|
+
@selected = nil
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reset!
|
|
67
|
+
super
|
|
68
|
+
@authenticators&.each_value(&:reset!)
|
|
69
|
+
@selected = nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def process(request_buffer=nil)
|
|
73
|
+
# the advertisement, listing every mechanism the server is willing to accept
|
|
74
|
+
return Result.new(Gss.gss_neg_token_init(@provider.mech_types), WindowsError::NTStatus::STATUS_SUCCESS) if request_buffer.nil?
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
gss_api = OpenSSL::ASN1.decode(request_buffer)
|
|
78
|
+
rescue OpenSSL::ASN1::ASN1Error => e
|
|
79
|
+
logger.error("Failed to parse the ASN1-encoded authentication request (#{e.message})")
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if negotiation_init?(gss_api)
|
|
84
|
+
# a NegTokenInit names the mechanism the client chose, so this is where routing is decided
|
|
85
|
+
mech_type = Gss.asn1dig(gss_api, 1, 0, 0, 0, 0)
|
|
86
|
+
authenticator = authenticator_for(mech_type)
|
|
87
|
+
if authenticator.nil?
|
|
88
|
+
logger.warn("Client selected an unsupported GSS mechanism (#{mech_type&.oid || 'unknown'})")
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@selected = authenticator
|
|
93
|
+
elsif @selected.nil?
|
|
94
|
+
# a NegTokenResp carries no mechanism OID, so it can only be interpreted as a continuation of a
|
|
95
|
+
# negotiation that has already selected one
|
|
96
|
+
logger.warn('Received a GSS continuation token before any mechanism was selected')
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@selected.process(request_buffer)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The session key belongs to whichever mechanism actually authenticated the client.
|
|
104
|
+
def session_key
|
|
105
|
+
@selected&.session_key
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def session_key=(value)
|
|
109
|
+
@selected&.session_key = value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Whether the token is a NegTokenInit, which is the only token that names a mechanism.
|
|
115
|
+
def negotiation_init?(gss_api)
|
|
116
|
+
gss_api&.tag == 0 && gss_api&.tag_class == :APPLICATION
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def authenticator_for(mech_type)
|
|
120
|
+
provider = @provider.provider_for(mech_type)
|
|
121
|
+
return nil if provider.nil?
|
|
122
|
+
|
|
123
|
+
@authenticators[provider] ||= provider.new_authenticator(@server_client)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -26,26 +26,7 @@ module RubySMB
|
|
|
26
26
|
|
|
27
27
|
def process(request_buffer=nil)
|
|
28
28
|
if request_buffer.nil?
|
|
29
|
-
|
|
30
|
-
buffer = OpenSSL::ASN1::ASN1Data.new([
|
|
31
|
-
Gss::OID_SPNEGO,
|
|
32
|
-
OpenSSL::ASN1::ASN1Data.new([
|
|
33
|
-
OpenSSL::ASN1::Sequence.new([
|
|
34
|
-
OpenSSL::ASN1::ASN1Data.new([
|
|
35
|
-
OpenSSL::ASN1::Sequence.new([
|
|
36
|
-
Gss::OID_NTLMSSP
|
|
37
|
-
])
|
|
38
|
-
], 0, :CONTEXT_SPECIFIC),
|
|
39
|
-
OpenSSL::ASN1::ASN1Data.new([
|
|
40
|
-
OpenSSL::ASN1::ASN1Data.new([
|
|
41
|
-
OpenSSL::ASN1::ASN1Data.new([
|
|
42
|
-
OpenSSL::ASN1::GeneralString.new('not_defined_in_RFC4178@please_ignore')
|
|
43
|
-
], 0, :CONTEXT_SPECIFIC)
|
|
44
|
-
], 16, :UNIVERSAL)
|
|
45
|
-
], 3, :CONTEXT_SPECIFIC)
|
|
46
|
-
])
|
|
47
|
-
], 0, :CONTEXT_SPECIFIC)
|
|
48
|
-
], 0, :APPLICATION).to_der
|
|
29
|
+
buffer = Gss.gss_neg_token_init(@provider.mech_types)
|
|
49
30
|
return Result.new(buffer, WindowsError::NTStatus::STATUS_SUCCESS)
|
|
50
31
|
end
|
|
51
32
|
|
|
@@ -85,7 +66,7 @@ module RubySMB
|
|
|
85
66
|
|
|
86
67
|
@server_challenge = @provider.generate_server_challenge
|
|
87
68
|
msg.challenge = @server_challenge.unpack1('Q<') # 64-bit unsigned, little endian (uint64_t)
|
|
88
|
-
target_info = Net::NTLM::TargetInfo.new(
|
|
69
|
+
target_info = Net::NTLM::TargetInfo.new(nil)
|
|
89
70
|
target_info.av_pairs.merge!({
|
|
90
71
|
Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => @provider.netbios_domain.encode('UTF-16LE').b,
|
|
91
72
|
Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => @provider.netbios_hostname.encode('UTF-16LE').b,
|
|
@@ -293,6 +274,10 @@ module RubySMB
|
|
|
293
274
|
Authenticator.new(self, server_client)
|
|
294
275
|
end
|
|
295
276
|
|
|
277
|
+
def mech_types
|
|
278
|
+
[Gss::OID_NTLMSSP]
|
|
279
|
+
end
|
|
280
|
+
|
|
296
281
|
#
|
|
297
282
|
# Lookup and return an account based on the username and optionally, the domain. If no domain is specified or
|
|
298
283
|
# or it is the special value '.', the default domain will be used. The username and domain values are case
|
|
@@ -26,6 +26,26 @@ module RubySMB
|
|
|
26
26
|
raise NotImplementedError
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
#
|
|
30
|
+
# The GSS mechanisms this provider can handle, in preference order. These are advertised to the client in the
|
|
31
|
+
# SPNEGO NegTokenInit, and are used to route an incoming token to the provider that understands it.
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<OpenSSL::ASN1::ObjectId>]
|
|
34
|
+
def mech_types
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Whether this provider can handle a token for the specified mechanism.
|
|
40
|
+
#
|
|
41
|
+
# @param [OpenSSL::ASN1::ObjectId] mech_type the mechanism selected by the client
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def supports_mech_type?(mech_type)
|
|
44
|
+
return false if mech_type.nil?
|
|
45
|
+
|
|
46
|
+
mech_types.any? { |oid| oid.oid == mech_type.oid }
|
|
47
|
+
end
|
|
48
|
+
|
|
29
49
|
#
|
|
30
50
|
# Whether or not anonymous authentication attempts should be permitted.
|
|
31
51
|
#
|
|
@@ -42,3 +62,5 @@ end
|
|
|
42
62
|
|
|
43
63
|
require 'ruby_smb/gss/provider/authenticator'
|
|
44
64
|
require 'ruby_smb/gss/provider/ntlm'
|
|
65
|
+
require 'ruby_smb/gss/provider/kerberos'
|
|
66
|
+
require 'ruby_smb/gss/provider/multi'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'rasn1'
|
|
2
|
+
|
|
3
|
+
module RubySMB
|
|
4
|
+
module Gss
|
|
5
|
+
# The SPNEGO negotiation token an initiator sends first, and that a server sends to advertise the mechanisms it
|
|
6
|
+
# supports. Modelled with RASN1 so the fields can be read and built by name rather than by walking a decoded
|
|
7
|
+
# structure by hand.
|
|
8
|
+
#
|
|
9
|
+
# https://datatracker.ietf.org/doc/html/rfc4178#section-4.2.1
|
|
10
|
+
class MechType < RASN1::Types::ObjectId
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class MechTypeList < RASN1::Model
|
|
14
|
+
sequence_of(:mech_type, MechType)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class ContextFlags < RASN1::Types::BitString
|
|
18
|
+
def initialize(options = {})
|
|
19
|
+
options[:bit_length] = 32
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# RASN1 does not define a GeneralString type, and a SPNEGO negHints carries its hintName as one, so it is defined
|
|
25
|
+
# here as an octet string tagged UNIVERSAL 27.
|
|
26
|
+
class GeneralString < RASN1::Types::OctetString
|
|
27
|
+
ID = 27
|
|
28
|
+
|
|
29
|
+
def self.type
|
|
30
|
+
'GeneralString'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# NegHints, the optional field Microsoft servers place at [3] of a NegTokenInit2 in lieu of a mechListMIC. Windows
|
|
35
|
+
# servers send a fixed placeholder hintName, so a client that expects the field still finds one.
|
|
36
|
+
#
|
|
37
|
+
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/8e71cf53-e867-4b79-9df6-cc9edb7f8829
|
|
38
|
+
class NegHints < RASN1::Model
|
|
39
|
+
define_type_accel('general_string', GeneralString)
|
|
40
|
+
|
|
41
|
+
sequence :neg_hints,
|
|
42
|
+
content: [general_string(:hint_name, explicit: 0, class: :context, constructed: true, optional: true),
|
|
43
|
+
octet_string(:hint_address, explicit: 1, class: :context, constructed: true, optional: true)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class NegTokenInit < RASN1::Model
|
|
47
|
+
sequence :neg_token_init, explicit: 0, class: :context, constructed: true,
|
|
48
|
+
content: [wrapper(model(:mech_type_list, MechTypeList), explicit: 0, constructed: true),
|
|
49
|
+
wrapper(model(:context_flags, ContextFlags), explicit: 1, constructed: true, optional: true),
|
|
50
|
+
octet_string(:mech_token, explicit: 2, constructed: true, optional: true),
|
|
51
|
+
wrapper(model(:neg_hints, NegHints), explicit: 3, constructed: true, optional: true)]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class SpnegoNegTokenInit < RASN1::Model
|
|
55
|
+
# The placeholder hintName a Windows server sends, reproduced so the advertisement matches what a client expects.
|
|
56
|
+
NEG_HINTS_NAME = 'not_defined_in_RFC4178@please_ignore'.freeze
|
|
57
|
+
|
|
58
|
+
sequence :gssapi, implicit: 0, class: :application, constructed: true,
|
|
59
|
+
content: [objectid(:oid),
|
|
60
|
+
model(:neg_token_init, NegTokenInit)]
|
|
61
|
+
|
|
62
|
+
# Build the NegTokenInit a server sends to advertise the mechanisms it supports, including the Microsoft negHints
|
|
63
|
+
# placeholder so the token is shaped as a Windows server's is.
|
|
64
|
+
#
|
|
65
|
+
# @param [Array<OpenSSL::ASN1::ObjectId>] mech_types the mechanisms to advertise, in preference order.
|
|
66
|
+
# @return [String] the DER encoded token.
|
|
67
|
+
def self.build(mech_types)
|
|
68
|
+
token = new
|
|
69
|
+
token[:gssapi][:oid].value = Gss::OID_SPNEGO.oid
|
|
70
|
+
token[:gssapi][:neg_token_init][:mech_type_list][:mech_type] = mech_types.map { |mech| MechType.new(value: mech.oid) }
|
|
71
|
+
token[:gssapi][:neg_token_init][:neg_hints][:hint_name] = NEG_HINTS_NAME
|
|
72
|
+
token.to_der
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [String, nil] the mechanism token the initiator carried, or nil if it carried none.
|
|
76
|
+
def mech_token
|
|
77
|
+
self[:gssapi][:neg_token_init][:mech_token].value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'rasn1'
|
|
2
|
+
|
|
3
|
+
module RubySMB
|
|
4
|
+
module Gss
|
|
5
|
+
# The SPNEGO negotiation token exchanged after the first, carrying a continuation of the selected mechanism.
|
|
6
|
+
# A client sends one to continue an exchange, so it is where a mechanism token arrives on any leg past the first.
|
|
7
|
+
#
|
|
8
|
+
# https://www.rfc-editor.org/rfc/rfc2478
|
|
9
|
+
class SpnegoNegTokenTarg < RASN1::Model
|
|
10
|
+
NEG_RESULTS = { 'accept-completed' => 0,
|
|
11
|
+
'accept-incomplete' => 1,
|
|
12
|
+
'reject' => 2,
|
|
13
|
+
'request-mic' => 3 }.freeze
|
|
14
|
+
|
|
15
|
+
sequence :token, explicit: 1, class: :context, constructed: true,
|
|
16
|
+
content: [enumerated(:neg_result, enum: NEG_RESULTS, explicit: 0, class: :context, constructed: true, optional: true),
|
|
17
|
+
objectid(:supported_mech, explicit: 1, class: :context, constructed: true, optional: true),
|
|
18
|
+
octet_string(:response_token, explicit: 2, class: :context, constructed: true, optional: true),
|
|
19
|
+
octet_string(:mech_list_mic, explicit: 3, class: :context, constructed: true, optional: true)]
|
|
20
|
+
|
|
21
|
+
# @return [String, nil] the mechanism token the continuation carried, or nil if it carried none.
|
|
22
|
+
def response_token
|
|
23
|
+
self[:response_token].value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/ruby_smb/gss.rb
CHANGED
|
@@ -2,11 +2,18 @@ module RubySMB
|
|
|
2
2
|
# module containing methods required for using the [GSS-API](http://www.rfc-editor.org/rfc/rfc2743.txt)
|
|
3
3
|
# for Secure Protected Negotiation(SPNEGO) in SMB Authentication.
|
|
4
4
|
module Gss
|
|
5
|
+
require 'ruby_smb/gss/spnego_neg_token_init'
|
|
6
|
+
require 'ruby_smb/gss/spnego_neg_token_targ'
|
|
5
7
|
require 'ruby_smb/gss/provider'
|
|
6
8
|
|
|
7
9
|
OID_SPNEGO = OpenSSL::ASN1::ObjectId.new('1.3.6.1.5.5.2')
|
|
8
10
|
OID_NEGOEX = OpenSSL::ASN1::ObjectId.new('1.3.6.1.4.1.311.2.2.30')
|
|
9
11
|
OID_NTLMSSP = OpenSSL::ASN1::ObjectId.new('1.3.6.1.4.1.311.2.2.10')
|
|
12
|
+
# The Kerberos v5 GSS-API mechanism (RFC 4121). Microsoft's SPNEGO
|
|
13
|
+
# implementation also uses a legacy OID that differs by a single arc, and
|
|
14
|
+
# clients may offer or select either, so both are defined here.
|
|
15
|
+
OID_KERBEROS_5 = OpenSSL::ASN1::ObjectId.new('1.2.840.113554.1.2.2')
|
|
16
|
+
OID_MICROSOFT_KERBEROS_5 = OpenSSL::ASN1::ObjectId.new('1.2.840.48018.1.2.2')
|
|
10
17
|
|
|
11
18
|
# Allow safe navigation of a decoded ASN.1 data structure. Similar to Ruby's
|
|
12
19
|
# builtin Hash#dig method but using the #value attribute of each ASN object.
|
|
@@ -46,6 +53,22 @@ module RubySMB
|
|
|
46
53
|
encoded_string
|
|
47
54
|
end
|
|
48
55
|
|
|
56
|
+
# Build the SPNEGO NegTokenInit that a server sends to advertise the
|
|
57
|
+
# authentication mechanisms it supports, per RFC 4178 section 4.2.1.
|
|
58
|
+
#
|
|
59
|
+
# The mechTypes list is supplied by the caller so that it reflects every
|
|
60
|
+
# mechanism the server actually offers, rather than being fixed to a single
|
|
61
|
+
# mechanism by whichever provider happens to build the token.
|
|
62
|
+
#
|
|
63
|
+
# @param [Array<OpenSSL::ASN1::ObjectId>] mech_types the mechanisms to
|
|
64
|
+
# advertise, in preference order (most preferred first).
|
|
65
|
+
# @return [String] the DER encoded NegTokenInit.
|
|
66
|
+
def self.gss_neg_token_init(mech_types)
|
|
67
|
+
raise ArgumentError, 'at least one mechanism must be advertised' if mech_types.nil? || mech_types.empty?
|
|
68
|
+
|
|
69
|
+
SpnegoNegTokenInit.build(mech_types)
|
|
70
|
+
end
|
|
71
|
+
|
|
49
72
|
# Create a GSS Security Blob of an NTLM Type 1 Message.
|
|
50
73
|
def self.gss_type1(type1)
|
|
51
74
|
OpenSSL::ASN1::ASN1Data.new([
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module Nbss
|
|
3
|
+
# The NM_FLAGS field of the NetBIOS Name Service header, as defined in
|
|
4
|
+
# RFC 1002 section 4.2.1.1. The surrounding R/OPCODE and RCODE bits are
|
|
5
|
+
# declared inline in the header records that embed this field
|
|
6
|
+
# ({NodeStatusRequest} and {NodeStatusResponse}).
|
|
7
|
+
class NameServiceHeaderFlags < BinData::Record
|
|
8
|
+
endian :big
|
|
9
|
+
|
|
10
|
+
bit1 :authoritative_answer, label: 'Authoritative Answer', initial_value: 0
|
|
11
|
+
bit1 :truncated, label: 'Truncated', initial_value: 0
|
|
12
|
+
bit1 :recursion_desired, label: 'Recursion Desired', initial_value: 0
|
|
13
|
+
bit1 :recursion_available, label: 'Recursion Available', initial_value: 0
|
|
14
|
+
bit2 :reserved, label: 'Reserved', initial_value: 0
|
|
15
|
+
bit1 :broadcast, label: 'Broadcast', initial_value: 0
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -12,15 +12,15 @@ module RubySMB
|
|
|
12
12
|
|
|
13
13
|
def error_msg
|
|
14
14
|
case error_code
|
|
15
|
-
when
|
|
15
|
+
when NOT_LISTENING_ON_CALLED_NAME
|
|
16
16
|
'Not listening on called name'
|
|
17
|
-
when
|
|
17
|
+
when NOT_LISTENING_FOR_CALLING_NAME
|
|
18
18
|
'Not listening for calling name'
|
|
19
|
-
when
|
|
19
|
+
when CALLED_NAME_NOT_PRESENT
|
|
20
20
|
'Called name not present'
|
|
21
|
-
when
|
|
21
|
+
when CALLED_NAME_INSUFFICIENT_RESOURCES
|
|
22
22
|
'Called name present, but insufficient resources'
|
|
23
|
-
when
|
|
23
|
+
when UNSPECIFIED_ERROR
|
|
24
24
|
'Unspecified error'
|
|
25
25
|
end
|
|
26
26
|
end
|