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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef6a12241de5bf3b7f37028f5dd927f1bc71249e37c77f2bb8d1575836bc9889
4
- data.tar.gz: 624c0708b392f448c17d163b71ef821902d83d17f5f632d7416f6a691041bb79
3
+ metadata.gz: 2bda3450e062ff69f5c20600086e7c8686d539b44a339f560764c077c6a1a3cc
4
+ data.tar.gz: 3c2ec535d37e7880e4ba7d023ff8d381f33da0bb47c28424bc4f3828d13f9e2b
5
5
  SHA512:
6
- metadata.gz: 5b518493f293005ce5b805221625a03cb937d7b2218029824f5cabb577f70cfdf8a1cd6ed6b08b6fc75a57c490200c20b469b8e347a3625d4e633f716d05929c
7
- data.tar.gz: 92e4ce7a72b4d7c8b9f140df9bb94e8798810f9ec0491bff81c295e68b0ebc07b17bd1f0c72e38cc7f8a2fb86ae14b08065296d83969adfe99b22038a1c24f33
6
+ metadata.gz: e57991b0b74090c01a6b6568dce52828e16045255ea2a3debc0afac2fa2f5bbff85e7319b21c36377a5e8a49e327bad518ebf58792e0e3c5a3d338d953064b1a
7
+ data.tar.gz: cb6a8ef6b78ca308df71d89b8c290dcce2ffcc64f155f41be7a66060713aa2c3c990ee4aa8e95d7308f0467e8b58d98ed0891523451086932f5205a26e1df214
@@ -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 = true
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 = RubySMB::Nbss::NegativeSessionResponse.read(raw_response)
683
- raise RubySMB::Error::NetBiosSessionService, "Session Request failed: #{negative_session_response.error_msg}"
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'
@@ -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; end
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
- # this is only NTLMSSP (as opposed to SPNEGO + NTLMSSP)
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 0x80
15
+ when NOT_LISTENING_ON_CALLED_NAME
16
16
  'Not listening on called name'
17
- when 0x81
17
+ when NOT_LISTENING_FOR_CALLING_NAME
18
18
  'Not listening for calling name'
19
- when 0x82
19
+ when CALLED_NAME_NOT_PRESENT
20
20
  'Called name not present'
21
- when 0x83
21
+ when CALLED_NAME_INSUFFICIENT_RESOURCES
22
22
  'Called name present, but insufficient resources'
23
- when 0x8F
23
+ when UNSPECIFIED_ERROR
24
24
  'Unspecified error'
25
25
  end
26
26
  end