ruby_smb 3.0.2 → 3.0.5
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/anonymous_auth.rb +29 -6
- data/examples/auth_capture.rb +3 -3
- data/examples/file_server.rb +3 -3
- data/examples/read_file.rb +51 -10
- data/examples/tree_connect.rb +49 -8
- data/lib/ruby_smb/client/authentication.rb +11 -3
- data/lib/ruby_smb/client/negotiation.rb +1 -1
- data/lib/ruby_smb/client.rb +34 -4
- data/lib/ruby_smb/server/session.rb +6 -0
- data/lib/ruby_smb/smb1/tree.rb +87 -79
- data/lib/ruby_smb/smb2/packet/session_setup_request.rb +11 -0
- data/lib/ruby_smb/smb2/tree.rb +80 -70
- data/lib/ruby_smb/version.rb +1 -1
- data/spec/lib/ruby_smb/client_spec.rb +10 -0
- data/spec/lib/ruby_smb/smb1/tree_spec.rb +8 -3
- data/spec/lib/ruby_smb/smb2/tree_spec.rb +9 -4
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a4881649381520e25f5f014f66baac4130dd8a58d4eadd3476198fec3fc9e8d
|
4
|
+
data.tar.gz: 809b581025c19ab7ca1162a7b73d342d63b24edd77835744191a0d7c930425fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e340189b0ae87ec98e7ca6bde9d4c30c1eb3a0d3e1f69383869e4803bf3d3d9bbc502cef143a83bad9f91dc1de10ca3bcd4132a8d518a1bb2121a7d4490309d
|
7
|
+
data.tar.gz: 0c7198d84416da8b995e50c17f7826abba21affa03cc1c2404e2e2ecaac1322178f76c356d8d026c4a75b7e9d356bd134f1a0df12fe327eca1240b87afaeee0d
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/examples/anonymous_auth.rb
CHANGED
@@ -4,14 +4,15 @@
|
|
4
4
|
# including protocol negotiation and authentication.
|
5
5
|
|
6
6
|
require 'bundler/setup'
|
7
|
+
require 'optparse'
|
7
8
|
require 'ruby_smb'
|
8
9
|
|
9
|
-
def run_authentication(address, smb1, smb2, smb3
|
10
|
+
def run_authentication(address, smb1, smb2, smb3)
|
10
11
|
# Create our socket and add it to the dispatcher
|
11
12
|
sock = TCPSocket.new address, 445
|
12
13
|
dispatcher = RubySMB::Dispatcher::Socket.new(sock)
|
13
14
|
|
14
|
-
client = RubySMB::Client.new(dispatcher, smb1: smb1, smb2: smb2, smb3: smb3, username:
|
15
|
+
client = RubySMB::Client.new(dispatcher, smb1: smb1, smb2: smb2, smb3: smb3, username: '', password: '')
|
15
16
|
protocol = client.negotiate
|
16
17
|
status = client.authenticate
|
17
18
|
puts "#{protocol} : #{status}"
|
@@ -22,9 +23,31 @@ def run_authentication(address, smb1, smb2, smb3, username, password)
|
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
args = ARGV.dup
|
27
|
+
options = {
|
28
|
+
smbv1: true,
|
29
|
+
smbv2: true,
|
30
|
+
smbv3: true,
|
31
|
+
target: nil
|
32
|
+
}
|
33
|
+
options[:target ] = args.pop
|
34
|
+
optparser = OptionParser.new do |opts|
|
35
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options] target"
|
36
|
+
opts.on("--[no-]smbv1", "Enable or disable SMBv1 (default: #{options[:smbv1] ? 'Enabled' : 'Disabled'})") do |smbv1|
|
37
|
+
options[:smbv1] = smbv1
|
38
|
+
end
|
39
|
+
opts.on("--[no-]smbv2", "Enable or disable SMBv2 (default: #{options[:smbv2] ? 'Enabled' : 'Disabled'})") do |smbv2|
|
40
|
+
options[:smbv2] = smbv2
|
41
|
+
end
|
42
|
+
opts.on("--[no-]smbv3", "Enable or disable SMBv3 (default: #{options[:smbv3] ? 'Enabled' : 'Disabled'})") do |smbv3|
|
43
|
+
options[:smbv3] = smbv3
|
44
|
+
end
|
45
|
+
end
|
46
|
+
optparser.parse!(args)
|
47
|
+
|
48
|
+
if options[:target].nil?
|
49
|
+
abort(optparser.help)
|
50
|
+
end
|
28
51
|
|
29
52
|
# Negotiate with only SMB1 enabled
|
30
|
-
run_authentication(
|
53
|
+
run_authentication(options[:target], options[:smbv1], options[:smbv2], options[:smbv3])
|
data/examples/auth_capture.rb
CHANGED
@@ -15,13 +15,13 @@ options = {
|
|
15
15
|
}
|
16
16
|
OptionParser.new do |opts|
|
17
17
|
opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
|
18
|
-
opts.on("--[no-]smbv1", "
|
18
|
+
opts.on("--[no-]smbv1", "Enable or disable SMBv1 (default: #{options[:smbv1] ? 'Enabled' : 'Disabled'})") do |smbv1|
|
19
19
|
options[:smbv1] = smbv1
|
20
20
|
end
|
21
|
-
opts.on("--[no-]smbv2", "
|
21
|
+
opts.on("--[no-]smbv2", "Enable or disable SMBv2 (default: #{options[:smbv2] ? 'Enabled' : 'Disabled'})") do |smbv2|
|
22
22
|
options[:smbv2] = smbv2
|
23
23
|
end
|
24
|
-
opts.on("--[no-]smbv3", "
|
24
|
+
opts.on("--[no-]smbv3", "Enable or disable SMBv3 (default: #{options[:smbv3] ? 'Enabled' : 'Disabled'})") do |smbv3|
|
25
25
|
options[:smbv3] = smbv3
|
26
26
|
end
|
27
27
|
end.parse!
|
data/examples/file_server.rb
CHANGED
@@ -30,13 +30,13 @@ OptionParser.new do |opts|
|
|
30
30
|
opts.on("--[no-]anonymous", "Allow anonymous access (default: #{options[:allow_anonymous]})") do |allow_anonymous|
|
31
31
|
options[:allow_anonymous] = allow_anonymous
|
32
32
|
end
|
33
|
-
opts.on("--[no-]smbv1", "
|
33
|
+
opts.on("--[no-]smbv1", "Enable or disable SMBv1 (default: #{options[:smbv1] ? 'Enabled' : 'Disabled'})") do |smbv1|
|
34
34
|
options[:smbv1] = smbv1
|
35
35
|
end
|
36
|
-
opts.on("--[no-]smbv2", "
|
36
|
+
opts.on("--[no-]smbv2", "Enable or disable SMBv2 (default: #{options[:smbv2] ? 'Enabled' : 'Disabled'})") do |smbv2|
|
37
37
|
options[:smbv2] = smbv2
|
38
38
|
end
|
39
|
-
opts.on("--[no-]smbv3", "
|
39
|
+
opts.on("--[no-]smbv3", "Enable or disable SMBv3 (default: #{options[:smbv3] ? 'Enabled' : 'Disabled'})") do |smbv3|
|
40
40
|
options[:smbv3] = smbv3
|
41
41
|
end
|
42
42
|
opts.on("--username USERNAME", "The account's username (default: #{options[:username]})") do |username|
|
data/examples/read_file.rb
CHANGED
@@ -7,34 +7,75 @@
|
|
7
7
|
# and read the file short.txt
|
8
8
|
|
9
9
|
require 'bundler/setup'
|
10
|
+
require 'optparse'
|
10
11
|
require 'ruby_smb'
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
args = ARGV.dup
|
14
|
+
options = {
|
15
|
+
domain: '.',
|
16
|
+
username: '',
|
17
|
+
password: '',
|
18
|
+
share: nil,
|
19
|
+
smbv1: true,
|
20
|
+
smbv2: true,
|
21
|
+
smbv3: true,
|
22
|
+
target: nil
|
23
|
+
}
|
24
|
+
options[:file] = args.pop
|
25
|
+
options[:share] = args.pop
|
26
|
+
options[:target ] = args.pop
|
27
|
+
optparser = OptionParser.new do |opts|
|
28
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options] target share file"
|
29
|
+
opts.on("--[no-]smbv1", "Enable or disable SMBv1 (default: #{options[:smbv1] ? 'Enabled' : 'Disabled'})") do |smbv1|
|
30
|
+
options[:smbv1] = smbv1
|
31
|
+
end
|
32
|
+
opts.on("--[no-]smbv2", "Enable or disable SMBv2 (default: #{options[:smbv2] ? 'Enabled' : 'Disabled'})") do |smbv2|
|
33
|
+
options[:smbv2] = smbv2
|
34
|
+
end
|
35
|
+
opts.on("--[no-]smbv3", "Enable or disable SMBv3 (default: #{options[:smbv3] ? 'Enabled' : 'Disabled'})") do |smbv3|
|
36
|
+
options[:smbv3] = smbv3
|
37
|
+
end
|
38
|
+
opts.on("--username USERNAME", "The account's username (default: #{options[:username]})") do |username|
|
39
|
+
if username.include?('\\')
|
40
|
+
options[:domain], options[:username] = username.split('\\', 2)
|
41
|
+
else
|
42
|
+
options[:username] = username
|
43
|
+
end
|
44
|
+
end
|
45
|
+
opts.on("--password PASSWORD", "The account's password (default: #{options[:password]})") do |password|
|
46
|
+
options[:password] = password
|
47
|
+
end
|
48
|
+
end
|
49
|
+
optparser.parse!(args)
|
50
|
+
|
51
|
+
if options[:target].nil? || options[:share].nil? || options[:file].nil?
|
52
|
+
abort(optparser.help)
|
53
|
+
end
|
18
54
|
|
19
|
-
path = "\\\\#{
|
55
|
+
path = "\\\\#{options[:target]}\\#{options[:share]}"
|
20
56
|
|
21
|
-
sock = TCPSocket.new
|
57
|
+
sock = TCPSocket.new options[:target], 445
|
22
58
|
dispatcher = RubySMB::Dispatcher::Socket.new(sock)
|
23
59
|
|
24
|
-
client = RubySMB::Client.new(dispatcher, smb1:
|
60
|
+
client = RubySMB::Client.new(dispatcher, smb1: options[:smbv1], smb2: options[:smbv2], smb3: options[:smbv3], username: options[:username], password: options[:password], domain: options[:domain])
|
25
61
|
protocol = client.negotiate
|
26
62
|
status = client.authenticate
|
27
63
|
|
28
64
|
puts "#{protocol} : #{status}"
|
65
|
+
unless status == WindowsError::NTStatus::STATUS_SUCCESS
|
66
|
+
puts 'Authentication failed!'
|
67
|
+
exit(1)
|
68
|
+
end
|
29
69
|
|
30
70
|
begin
|
31
71
|
tree = client.tree_connect(path)
|
32
72
|
puts "Connected to #{path} successfully!"
|
33
73
|
rescue StandardError => e
|
34
74
|
puts "Failed to connect to #{path}: #{e.message}"
|
75
|
+
exit(1)
|
35
76
|
end
|
36
77
|
|
37
|
-
file = tree.open_file(filename: file)
|
78
|
+
file = tree.open_file(filename: options[:file])
|
38
79
|
|
39
80
|
data = file.read
|
40
81
|
puts data
|
data/examples/tree_connect.rb
CHANGED
@@ -6,24 +6,64 @@
|
|
6
6
|
# This will try to connect to \\192.168.172.138\TEST_SHARE with the msfadmin:msfadmin credentials
|
7
7
|
|
8
8
|
require 'bundler/setup'
|
9
|
+
require 'optparse'
|
9
10
|
require 'ruby_smb'
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
args = ARGV.dup
|
13
|
+
options = {
|
14
|
+
domain: '.',
|
15
|
+
username: '',
|
16
|
+
password: '',
|
17
|
+
share: nil,
|
18
|
+
smbv1: true,
|
19
|
+
smbv2: true,
|
20
|
+
smbv3: true,
|
21
|
+
target: nil
|
22
|
+
}
|
23
|
+
options[:share] = args.pop
|
24
|
+
options[:target ] = args.pop
|
25
|
+
optparser = OptionParser.new do |opts|
|
26
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options] target share"
|
27
|
+
opts.on("--[no-]smbv1", "Enable or disable SMBv1 (default: #{options[:smbv1] ? 'Enabled' : 'Disabled'})") do |smbv1|
|
28
|
+
options[:smbv1] = smbv1
|
29
|
+
end
|
30
|
+
opts.on("--[no-]smbv2", "Enable or disable SMBv2 (default: #{options[:smbv2] ? 'Enabled' : 'Disabled'})") do |smbv2|
|
31
|
+
options[:smbv2] = smbv2
|
32
|
+
end
|
33
|
+
opts.on("--[no-]smbv3", "Enable or disable SMBv3 (default: #{options[:smbv3] ? 'Enabled' : 'Disabled'})") do |smbv3|
|
34
|
+
options[:smbv3] = smbv3
|
35
|
+
end
|
36
|
+
opts.on("--username USERNAME", "The account's username (default: #{options[:username]})") do |username|
|
37
|
+
if username.include?('\\')
|
38
|
+
options[:domain], options[:username] = username.split('\\', 2)
|
39
|
+
else
|
40
|
+
options[:username] = username
|
41
|
+
end
|
42
|
+
end
|
43
|
+
opts.on("--password PASSWORD", "The account's password (default: #{options[:password]})") do |password|
|
44
|
+
options[:password] = password
|
45
|
+
end
|
46
|
+
end
|
47
|
+
optparser.parse!(args)
|
48
|
+
|
49
|
+
if options[:target].nil? || options[:share].nil?
|
50
|
+
abort(optparser.help)
|
51
|
+
end
|
16
52
|
|
17
|
-
path
|
53
|
+
path = "\\\\#{options[:target]}\\#{options[:share]}"
|
18
54
|
|
19
|
-
sock = TCPSocket.new
|
55
|
+
sock = TCPSocket.new options[:target], 445
|
20
56
|
dispatcher = RubySMB::Dispatcher::Socket.new(sock)
|
21
57
|
|
22
|
-
client = RubySMB::Client.new(dispatcher, smb1:
|
58
|
+
client = RubySMB::Client.new(dispatcher, smb1: options[:smbv1], smb2: options[:smbv2], smb3: options[:smbv3], username: options[:username], password: options[:password], domain: options[:domain])
|
23
59
|
protocol = client.negotiate
|
24
60
|
status = client.authenticate
|
25
61
|
|
26
62
|
puts "#{protocol} : #{status}"
|
63
|
+
unless status == WindowsError::NTStatus::STATUS_SUCCESS
|
64
|
+
puts 'Authentication failed!'
|
65
|
+
exit(1)
|
66
|
+
end
|
27
67
|
|
28
68
|
begin
|
29
69
|
tree = client.tree_connect(path)
|
@@ -31,4 +71,5 @@ begin
|
|
31
71
|
tree.disconnect!
|
32
72
|
rescue StandardError => e
|
33
73
|
puts "Failed to connect to #{path}: #{e.message}"
|
74
|
+
exit(1)
|
34
75
|
end
|
@@ -212,9 +212,17 @@ module RubySMB
|
|
212
212
|
|
213
213
|
raw = smb2_ntlmssp_authenticate(type3_message, @session_id)
|
214
214
|
response = smb2_ntlmssp_final_packet(raw)
|
215
|
-
|
216
|
-
|
217
|
-
|
215
|
+
@session_is_guest = response.session_flags.guest == 1
|
216
|
+
|
217
|
+
if @smb3
|
218
|
+
if response.session_flags.encrypt_data == 1
|
219
|
+
# if the server indicates that encryption is required, enable it
|
220
|
+
@session_encrypt_data = true
|
221
|
+
elsif (@session_is_guest && password != '') || (username == '' && password == '')
|
222
|
+
# disable encryption when necessary
|
223
|
+
@session_encrypt_data = false
|
224
|
+
end
|
225
|
+
# otherwise, leave encryption to the default value that it was initialized to
|
218
226
|
end
|
219
227
|
######
|
220
228
|
# DEBUG
|
@@ -251,7 +251,7 @@ module RubySMB
|
|
251
251
|
raise ArgumentError, 'Must be an array of strings' unless dialect.is_a? String
|
252
252
|
packet.add_dialect(dialect.to_i(16))
|
253
253
|
end
|
254
|
-
packet.capabilities.encryption = 1
|
254
|
+
packet.capabilities.encryption = @session_encrypt_data ? 1 : 0
|
255
255
|
|
256
256
|
if packet.dialects.include?(0x0311)
|
257
257
|
nc = RubySMB::SMB2::NegotiateContext.new(
|
data/lib/ruby_smb/client.rb
CHANGED
@@ -29,10 +29,26 @@ module RubySMB
|
|
29
29
|
# It indicates that the server implements SMB 2.1 or future dialect revisions
|
30
30
|
# Note that this must be used for SMB3
|
31
31
|
SMB1_DIALECT_SMB2_WILDCARD = 'SMB 2.???'.freeze
|
32
|
+
|
33
|
+
SMB2_DIALECT_0202 = '0x0202'.freeze
|
34
|
+
SMB2_DIALECT_0210 = '0x0210'.freeze
|
35
|
+
SMB2_DIALECT_0300 = '0x0300'.freeze
|
36
|
+
SMB2_DIALECT_0302 = '0x0302'.freeze
|
37
|
+
SMB2_DIALECT_0311 = '0x0311'.freeze
|
38
|
+
|
32
39
|
# Dialect values for SMB2
|
33
|
-
SMB2_DIALECT_DEFAULT = [
|
40
|
+
SMB2_DIALECT_DEFAULT = [
|
41
|
+
SMB2_DIALECT_0202,
|
42
|
+
SMB2_DIALECT_0210,
|
43
|
+
].freeze
|
44
|
+
|
34
45
|
# Dialect values for SMB3
|
35
|
-
SMB3_DIALECT_DEFAULT = [
|
46
|
+
SMB3_DIALECT_DEFAULT = [
|
47
|
+
SMB2_DIALECT_0300,
|
48
|
+
SMB2_DIALECT_0302,
|
49
|
+
SMB2_DIALECT_0311
|
50
|
+
].freeze
|
51
|
+
|
36
52
|
# The default maximum size of a SMB message that the Client accepts (in bytes)
|
37
53
|
MAX_BUFFER_SIZE = 64512
|
38
54
|
# The default maximum size of a SMB message that the Server accepts (in bytes)
|
@@ -129,6 +145,11 @@ module RubySMB
|
|
129
145
|
# @return [Integer]
|
130
146
|
attr_accessor :session_id
|
131
147
|
|
148
|
+
# Whether or not the current session has the guest flag set
|
149
|
+
# @!attribute [rw] session_is_guest
|
150
|
+
# @return [Boolean]
|
151
|
+
attr_accessor :session_is_guest
|
152
|
+
|
132
153
|
# Whether or not the Server requires signing
|
133
154
|
# @!attribute [rw] signing_enabled
|
134
155
|
# @return [Boolean]
|
@@ -292,6 +313,7 @@ module RubySMB
|
|
292
313
|
@sequence_counter = 0
|
293
314
|
@session_id = 0x00
|
294
315
|
@session_key = ''
|
316
|
+
@session_is_guest = false
|
295
317
|
@signing_required = false
|
296
318
|
@smb1 = smb1
|
297
319
|
@smb2 = smb2
|
@@ -306,6 +328,8 @@ module RubySMB
|
|
306
328
|
@server_supports_multi_credit = false
|
307
329
|
|
308
330
|
# SMB 3.x options
|
331
|
+
# this merely initializes the default value for session encryption, it may be changed as necessary when a
|
332
|
+
# session setup response is received
|
309
333
|
@session_encrypt_data = always_encrypt
|
310
334
|
|
311
335
|
@ntlm_client = Net::NTLM::Client.new(
|
@@ -444,8 +468,13 @@ module RubySMB
|
|
444
468
|
unless packet.is_a?(RubySMB::SMB2::Packet::SessionSetupRequest) || session_key.empty?
|
445
469
|
if self.smb2 && signing_required
|
446
470
|
packet = smb2_sign(packet)
|
447
|
-
elsif self.smb3
|
448
|
-
|
471
|
+
elsif self.smb3
|
472
|
+
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/652e0c14-5014-4470-999d-b174d7b2da87
|
473
|
+
if @dialect == '0x0311' && (signing_required || (!@session_is_guest && packet.is_a?(RubySMB::SMB2::Packet::TreeConnectRequest)))
|
474
|
+
packet = smb3_sign(packet)
|
475
|
+
elsif signing_required
|
476
|
+
packet = smb3_sign(packet)
|
477
|
+
end
|
449
478
|
end
|
450
479
|
end
|
451
480
|
else
|
@@ -589,6 +618,7 @@ module RubySMB
|
|
589
618
|
self.session_id = 0x00
|
590
619
|
self.user_id = 0x00
|
591
620
|
self.session_key = ''
|
621
|
+
self.session_is_guest = false
|
592
622
|
self.sequence_counter = 0
|
593
623
|
self.smb2_message_id = 0
|
594
624
|
self.client_encryption_key = nil
|
@@ -13,6 +13,7 @@ module RubySMB
|
|
13
13
|
@user_id = user_id
|
14
14
|
@state = state
|
15
15
|
@signing_required = false
|
16
|
+
@metadata = {}
|
16
17
|
# tree id => provider processor instance
|
17
18
|
@tree_connect_table = {}
|
18
19
|
@creation_time = Time.now
|
@@ -62,6 +63,11 @@ module RubySMB
|
|
62
63
|
# @return [Hash]
|
63
64
|
attr_accessor :tree_connect_table
|
64
65
|
|
66
|
+
# Untyped hash for storing additional arbitrary metadata about the current session
|
67
|
+
# @!attribute [rw] metadaa
|
68
|
+
# @return [Hash]
|
69
|
+
attr_accessor :metadata
|
70
|
+
|
65
71
|
# The time at which this session was created.
|
66
72
|
# @!attribute [r] creation_time
|
67
73
|
# @return [Time]
|
data/lib/ruby_smb/smb1/tree.rb
CHANGED
@@ -59,8 +59,8 @@ module RubySMB
|
|
59
59
|
# Make sure we don't modify the caller's hash options
|
60
60
|
opts = opts.dup
|
61
61
|
opts[:filename] = opts[:filename].dup
|
62
|
-
opts[:filename].prepend('\\') unless opts[:filename].start_with?('\\')
|
63
|
-
|
62
|
+
opts[:filename].prepend('\\') unless opts[:filename].start_with?('\\'.encode(opts[:filename].encoding))
|
63
|
+
_open(**opts)
|
64
64
|
end
|
65
65
|
|
66
66
|
# Open a file on the remote share.
|
@@ -80,83 +80,12 @@ module RubySMB
|
|
80
80
|
# @return [RubySMB::SMB1::File] handle to the created file
|
81
81
|
# @raise [RubySMB::Error::InvalidPacket] if the response command is not SMB_COM_NT_CREATE_ANDX
|
82
82
|
# @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
|
83
|
-
def open_file(
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
if flags
|
91
|
-
nt_create_andx_request.parameter_block.flags = flags
|
92
|
-
else
|
93
|
-
nt_create_andx_request.parameter_block.flags.request_extended_response = 1
|
94
|
-
end
|
95
|
-
|
96
|
-
if options
|
97
|
-
nt_create_andx_request.parameter_block.create_options = options
|
98
|
-
else
|
99
|
-
nt_create_andx_request.parameter_block.create_options.directory_file = 0
|
100
|
-
nt_create_andx_request.parameter_block.create_options.non_directory_file = 1
|
101
|
-
end
|
102
|
-
|
103
|
-
if read
|
104
|
-
nt_create_andx_request.parameter_block.share_access.share_read = 1
|
105
|
-
nt_create_andx_request.parameter_block.desired_access.read_data = 1
|
106
|
-
nt_create_andx_request.parameter_block.desired_access.read_ea = 1
|
107
|
-
nt_create_andx_request.parameter_block.desired_access.read_attr = 1
|
108
|
-
nt_create_andx_request.parameter_block.desired_access.read_control = 1
|
109
|
-
end
|
110
|
-
|
111
|
-
if write
|
112
|
-
nt_create_andx_request.parameter_block.share_access.share_write = 1
|
113
|
-
nt_create_andx_request.parameter_block.desired_access.write_data = 1
|
114
|
-
nt_create_andx_request.parameter_block.desired_access.append_data = 1
|
115
|
-
nt_create_andx_request.parameter_block.desired_access.write_ea = 1
|
116
|
-
nt_create_andx_request.parameter_block.desired_access.write_attr = 1
|
117
|
-
end
|
118
|
-
|
119
|
-
if delete
|
120
|
-
nt_create_andx_request.parameter_block.share_access.share_delete = 1
|
121
|
-
nt_create_andx_request.parameter_block.desired_access.delete_access = 1
|
122
|
-
end
|
123
|
-
|
124
|
-
nt_create_andx_request.parameter_block.impersonation_level = impersonation
|
125
|
-
nt_create_andx_request.parameter_block.create_disposition = disposition
|
126
|
-
|
127
|
-
unicode_enabled = nt_create_andx_request.smb_header.flags2.unicode == 1
|
128
|
-
nt_create_andx_request.data_block.file_name = add_null_termination(str: filename, unicode: unicode_enabled)
|
129
|
-
|
130
|
-
raw_response = @client.send_recv(nt_create_andx_request)
|
131
|
-
response = RubySMB::SMB1::Packet::NtCreateAndxResponse.read(raw_response)
|
132
|
-
unless response.valid?
|
133
|
-
if response.is_a?(RubySMB::SMB1::Packet::EmptyPacket) &&
|
134
|
-
response.smb_header.protocol == RubySMB::SMB1::SMB_PROTOCOL_ID &&
|
135
|
-
response.smb_header.command == response.original_command
|
136
|
-
raise RubySMB::Error::InvalidPacket.new(
|
137
|
-
'The response seems to be an SMB1 NtCreateAndxResponse but an '\
|
138
|
-
'error occurs while parsing it. It is probably missing the '\
|
139
|
-
'required extended information.'
|
140
|
-
)
|
141
|
-
end
|
142
|
-
raise RubySMB::Error::InvalidPacket.new(
|
143
|
-
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
|
144
|
-
expected_cmd: RubySMB::SMB1::Packet::NtCreateAndxResponse::COMMAND,
|
145
|
-
packet: response
|
146
|
-
)
|
147
|
-
end
|
148
|
-
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
|
149
|
-
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
150
|
-
end
|
151
|
-
|
152
|
-
case response.parameter_block.resource_type
|
153
|
-
when RubySMB::SMB1::ResourceType::BYTE_MODE_PIPE, RubySMB::SMB1::ResourceType::MESSAGE_MODE_PIPE
|
154
|
-
RubySMB::SMB1::Pipe.new(name: filename, tree: self, response: response)
|
155
|
-
when RubySMB::SMB1::ResourceType::DISK
|
156
|
-
RubySMB::SMB1::File.new(name: filename, tree: self, response: response)
|
157
|
-
else
|
158
|
-
raise RubySMB::Error::RubySMBError
|
159
|
-
end
|
83
|
+
def open_file(opts)
|
84
|
+
# Make sure we don't modify the caller's hash options
|
85
|
+
opts = opts.dup
|
86
|
+
opts[:filename] = opts[:filename].dup
|
87
|
+
opts[:filename] = opts[:filename][1..-1] if opts[:filename].start_with?('\\'.encode(opts[:filename].encoding))
|
88
|
+
_open(**opts)
|
160
89
|
end
|
161
90
|
|
162
91
|
# List `directory` on the remote share.
|
@@ -264,6 +193,85 @@ module RubySMB
|
|
264
193
|
|
265
194
|
private
|
266
195
|
|
196
|
+
def _open(filename:, flags: nil, options: nil, disposition: RubySMB::Dispositions::FILE_OPEN,
|
197
|
+
impersonation: RubySMB::ImpersonationLevels::SEC_IMPERSONATE, read: true, write: false, delete: false)
|
198
|
+
nt_create_andx_request = RubySMB::SMB1::Packet::NtCreateAndxRequest.new
|
199
|
+
nt_create_andx_request = set_header_fields(nt_create_andx_request)
|
200
|
+
|
201
|
+
nt_create_andx_request.parameter_block.ext_file_attributes.normal = 1
|
202
|
+
|
203
|
+
if flags
|
204
|
+
nt_create_andx_request.parameter_block.flags = flags
|
205
|
+
else
|
206
|
+
nt_create_andx_request.parameter_block.flags.request_extended_response = 1
|
207
|
+
end
|
208
|
+
|
209
|
+
if options
|
210
|
+
nt_create_andx_request.parameter_block.create_options = options
|
211
|
+
else
|
212
|
+
nt_create_andx_request.parameter_block.create_options.directory_file = 0
|
213
|
+
nt_create_andx_request.parameter_block.create_options.non_directory_file = 1
|
214
|
+
end
|
215
|
+
|
216
|
+
if read
|
217
|
+
nt_create_andx_request.parameter_block.share_access.share_read = 1
|
218
|
+
nt_create_andx_request.parameter_block.desired_access.read_data = 1
|
219
|
+
nt_create_andx_request.parameter_block.desired_access.read_ea = 1
|
220
|
+
nt_create_andx_request.parameter_block.desired_access.read_attr = 1
|
221
|
+
nt_create_andx_request.parameter_block.desired_access.read_control = 1
|
222
|
+
end
|
223
|
+
|
224
|
+
if write
|
225
|
+
nt_create_andx_request.parameter_block.share_access.share_write = 1
|
226
|
+
nt_create_andx_request.parameter_block.desired_access.write_data = 1
|
227
|
+
nt_create_andx_request.parameter_block.desired_access.append_data = 1
|
228
|
+
nt_create_andx_request.parameter_block.desired_access.write_ea = 1
|
229
|
+
nt_create_andx_request.parameter_block.desired_access.write_attr = 1
|
230
|
+
end
|
231
|
+
|
232
|
+
if delete
|
233
|
+
nt_create_andx_request.parameter_block.share_access.share_delete = 1
|
234
|
+
nt_create_andx_request.parameter_block.desired_access.delete_access = 1
|
235
|
+
end
|
236
|
+
|
237
|
+
nt_create_andx_request.parameter_block.impersonation_level = impersonation
|
238
|
+
nt_create_andx_request.parameter_block.create_disposition = disposition
|
239
|
+
|
240
|
+
unicode_enabled = nt_create_andx_request.smb_header.flags2.unicode == 1
|
241
|
+
nt_create_andx_request.data_block.file_name = add_null_termination(str: filename, unicode: unicode_enabled)
|
242
|
+
|
243
|
+
raw_response = @client.send_recv(nt_create_andx_request)
|
244
|
+
response = RubySMB::SMB1::Packet::NtCreateAndxResponse.read(raw_response)
|
245
|
+
unless response.valid?
|
246
|
+
if response.is_a?(RubySMB::SMB1::Packet::EmptyPacket) &&
|
247
|
+
response.smb_header.protocol == RubySMB::SMB1::SMB_PROTOCOL_ID &&
|
248
|
+
response.smb_header.command == response.original_command
|
249
|
+
raise RubySMB::Error::InvalidPacket.new(
|
250
|
+
'The response seems to be an SMB1 NtCreateAndxResponse but an '\
|
251
|
+
'error occurs while parsing it. It is probably missing the '\
|
252
|
+
'required extended information.'
|
253
|
+
)
|
254
|
+
end
|
255
|
+
raise RubySMB::Error::InvalidPacket.new(
|
256
|
+
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
|
257
|
+
expected_cmd: RubySMB::SMB1::Packet::NtCreateAndxResponse::COMMAND,
|
258
|
+
packet: response
|
259
|
+
)
|
260
|
+
end
|
261
|
+
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
|
262
|
+
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
263
|
+
end
|
264
|
+
|
265
|
+
case response.parameter_block.resource_type
|
266
|
+
when RubySMB::SMB1::ResourceType::BYTE_MODE_PIPE, RubySMB::SMB1::ResourceType::MESSAGE_MODE_PIPE
|
267
|
+
RubySMB::SMB1::Pipe.new(name: filename, tree: self, response: response)
|
268
|
+
when RubySMB::SMB1::ResourceType::DISK
|
269
|
+
RubySMB::SMB1::File.new(name: filename, tree: self, response: response)
|
270
|
+
else
|
271
|
+
raise RubySMB::Error::RubySMBError
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
267
275
|
# Sets ParameterBlock options for FIND_FIRST2 and
|
268
276
|
# FIND_NEXT2 requests. In particular we need to do this
|
269
277
|
# to tell the server to ignore the Trans2DataBlock as we are
|
@@ -18,6 +18,17 @@ module RubySMB
|
|
18
18
|
uint64 :previous_session_id, label: 'Previous Session ID'
|
19
19
|
string :buffer, label: 'Security Buffer', length: -> { security_buffer_length }
|
20
20
|
|
21
|
+
# Takes the specified security buffer string and inserts it into the {RubySMB::SMB2::Packet::SessionSetupRequest#buffer}
|
22
|
+
# as well as updating the {RubySMB::SMB2::Packet::SessionSetupRequest#security_buffer_length}
|
23
|
+
# This method DOES NOT wrap the security buffer in any way.
|
24
|
+
#
|
25
|
+
# @param buffer [String] the security buffer
|
26
|
+
# @return [void]
|
27
|
+
def set_security_buffer(buffer)
|
28
|
+
self.security_buffer_length = buffer.length
|
29
|
+
self.buffer = buffer
|
30
|
+
end
|
31
|
+
|
21
32
|
# Takes a serialized NTLM Type 1 message and wraps it in the GSS ASN1 encoding
|
22
33
|
# and inserts it into the {RubySMB::SMB2::Packet::SessionSetupRequest#buffer}
|
23
34
|
# as well as updating the {RubySMB::SMB2::Packet::SessionSetupRequest#security_buffer_length}
|
data/lib/ruby_smb/smb2/tree.rb
CHANGED
@@ -60,78 +60,16 @@ module RubySMB
|
|
60
60
|
# Make sure we don't modify the caller's hash options
|
61
61
|
opts = opts.dup
|
62
62
|
opts[:filename] = opts[:filename].dup
|
63
|
-
opts[:filename] = opts[:filename][1..-1] if opts[:filename].start_with?
|
64
|
-
|
63
|
+
opts[:filename] = opts[:filename][1..-1] if opts[:filename].start_with?('\\'.encode(opts[:filename].encoding))
|
64
|
+
_open(**opts)
|
65
65
|
end
|
66
66
|
|
67
|
-
def open_file(
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
# If the user supplied file attributes, use those, otherwise set some
|
74
|
-
# sane defaults.
|
75
|
-
if attributes
|
76
|
-
create_request.file_attributes = attributes
|
77
|
-
else
|
78
|
-
create_request.file_attributes.directory = 0
|
79
|
-
create_request.file_attributes.normal = 1
|
80
|
-
end
|
81
|
-
|
82
|
-
# If the user supplied Create Options, use those, otherwise set some
|
83
|
-
# sane defaults.
|
84
|
-
if options
|
85
|
-
create_request.create_options = options
|
86
|
-
else
|
87
|
-
create_request.create_options.directory_file = 0
|
88
|
-
create_request.create_options.non_directory_file = 1
|
89
|
-
end
|
90
|
-
|
91
|
-
if read
|
92
|
-
create_request.share_access.read_access = 1
|
93
|
-
create_request.desired_access.read_data = 1
|
94
|
-
end
|
95
|
-
|
96
|
-
if write
|
97
|
-
create_request.share_access.write_access = 1
|
98
|
-
create_request.desired_access.write_data = 1
|
99
|
-
create_request.desired_access.append_data = 1
|
100
|
-
end
|
101
|
-
|
102
|
-
if delete
|
103
|
-
create_request.share_access.delete_access = 1
|
104
|
-
create_request.desired_access.delete_access = 1
|
105
|
-
end
|
106
|
-
|
107
|
-
create_request.requested_oplock = 0xff
|
108
|
-
create_request.impersonation_level = impersonation
|
109
|
-
create_request.create_disposition = disposition
|
110
|
-
create_request.name = filename
|
111
|
-
|
112
|
-
raw_response = client.send_recv(create_request, encrypt: @tree_connect_encrypt_data)
|
113
|
-
response = RubySMB::SMB2::Packet::CreateResponse.read(raw_response)
|
114
|
-
unless response.valid?
|
115
|
-
raise RubySMB::Error::InvalidPacket.new(
|
116
|
-
expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
|
117
|
-
expected_cmd: RubySMB::SMB2::Packet::CreateResponse::COMMAND,
|
118
|
-
packet: response
|
119
|
-
)
|
120
|
-
end
|
121
|
-
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
|
122
|
-
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
123
|
-
end
|
124
|
-
|
125
|
-
case @share_type
|
126
|
-
when RubySMB::SMB2::Packet::TreeConnectResponse::SMB2_SHARE_TYPE_DISK
|
127
|
-
RubySMB::SMB2::File.new(name: filename, tree: self, response: response, encrypt: @tree_connect_encrypt_data)
|
128
|
-
when RubySMB::SMB2::Packet::TreeConnectResponse::SMB2_SHARE_TYPE_PIPE
|
129
|
-
RubySMB::SMB2::Pipe.new(name: filename, tree: self, response: response)
|
130
|
-
# when RubySMB::SMB2::TreeConnectResponse::SMB2_SHARE_TYPE_PRINT
|
131
|
-
# it's a printer!
|
132
|
-
else
|
133
|
-
raise RubySMB::Error::RubySMBError, 'Unsupported share type'
|
134
|
-
end
|
67
|
+
def open_file(opts)
|
68
|
+
# Make sure we don't modify the caller's hash options
|
69
|
+
opts = opts.dup
|
70
|
+
opts[:filename] = opts[:filename].dup
|
71
|
+
opts[:filename] = opts[:filename][1..-1] if opts[:filename].start_with?('\\'.encode(opts[:filename].encoding))
|
72
|
+
_open(**opts)
|
135
73
|
end
|
136
74
|
|
137
75
|
# List `directory` on the remote share.
|
@@ -270,6 +208,78 @@ module RubySMB
|
|
270
208
|
request.smb2_header.credits = 256
|
271
209
|
request
|
272
210
|
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def _open(filename:, attributes: nil, options: nil, disposition: RubySMB::Dispositions::FILE_OPEN,
|
215
|
+
impersonation: RubySMB::ImpersonationLevels::SEC_IMPERSONATE, read: true, write: false, delete: false)
|
216
|
+
|
217
|
+
create_request = RubySMB::SMB2::Packet::CreateRequest.new
|
218
|
+
create_request = set_header_fields(create_request)
|
219
|
+
|
220
|
+
# If the user supplied file attributes, use those, otherwise set some
|
221
|
+
# sane defaults.
|
222
|
+
if attributes
|
223
|
+
create_request.file_attributes = attributes
|
224
|
+
else
|
225
|
+
create_request.file_attributes.directory = 0
|
226
|
+
create_request.file_attributes.normal = 1
|
227
|
+
end
|
228
|
+
|
229
|
+
# If the user supplied Create Options, use those, otherwise set some
|
230
|
+
# sane defaults.
|
231
|
+
if options
|
232
|
+
create_request.create_options = options
|
233
|
+
else
|
234
|
+
create_request.create_options.directory_file = 0
|
235
|
+
create_request.create_options.non_directory_file = 1
|
236
|
+
end
|
237
|
+
|
238
|
+
if read
|
239
|
+
create_request.share_access.read_access = 1
|
240
|
+
create_request.desired_access.read_data = 1
|
241
|
+
end
|
242
|
+
|
243
|
+
if write
|
244
|
+
create_request.share_access.write_access = 1
|
245
|
+
create_request.desired_access.write_data = 1
|
246
|
+
create_request.desired_access.append_data = 1
|
247
|
+
end
|
248
|
+
|
249
|
+
if delete
|
250
|
+
create_request.share_access.delete_access = 1
|
251
|
+
create_request.desired_access.delete_access = 1
|
252
|
+
end
|
253
|
+
|
254
|
+
create_request.requested_oplock = 0xff
|
255
|
+
create_request.impersonation_level = impersonation
|
256
|
+
create_request.create_disposition = disposition
|
257
|
+
create_request.name = filename
|
258
|
+
|
259
|
+
raw_response = client.send_recv(create_request, encrypt: @tree_connect_encrypt_data)
|
260
|
+
response = RubySMB::SMB2::Packet::CreateResponse.read(raw_response)
|
261
|
+
unless response.valid?
|
262
|
+
raise RubySMB::Error::InvalidPacket.new(
|
263
|
+
expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
|
264
|
+
expected_cmd: RubySMB::SMB2::Packet::CreateResponse::COMMAND,
|
265
|
+
packet: response
|
266
|
+
)
|
267
|
+
end
|
268
|
+
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
|
269
|
+
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
270
|
+
end
|
271
|
+
|
272
|
+
case @share_type
|
273
|
+
when RubySMB::SMB2::Packet::TreeConnectResponse::SMB2_SHARE_TYPE_DISK
|
274
|
+
RubySMB::SMB2::File.new(name: filename, tree: self, response: response, encrypt: @tree_connect_encrypt_data)
|
275
|
+
when RubySMB::SMB2::Packet::TreeConnectResponse::SMB2_SHARE_TYPE_PIPE
|
276
|
+
RubySMB::SMB2::Pipe.new(name: filename, tree: self, response: response)
|
277
|
+
# when RubySMB::SMB2::TreeConnectResponse::SMB2_SHARE_TYPE_PRINT
|
278
|
+
# it's a printer!
|
279
|
+
else
|
280
|
+
raise RubySMB::Error::RubySMBError, 'Unsupported share type'
|
281
|
+
end
|
282
|
+
end
|
273
283
|
end
|
274
284
|
end
|
275
285
|
end
|
data/lib/ruby_smb/version.rb
CHANGED
@@ -557,6 +557,16 @@ RSpec.describe RubySMB::Client do
|
|
557
557
|
end
|
558
558
|
end
|
559
559
|
|
560
|
+
describe '#wipe_state!' do
|
561
|
+
context 'with SMB1' do
|
562
|
+
it { expect { smb1_client.wipe_state! }.to_not raise_error }
|
563
|
+
end
|
564
|
+
|
565
|
+
context 'with SMB2' do
|
566
|
+
it { expect { smb2_client.wipe_state! }.to_not raise_error }
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
560
570
|
describe '#logoff!' do
|
561
571
|
context 'with SMB1' do
|
562
572
|
let(:raw_response) { double('Raw response') }
|
@@ -118,6 +118,11 @@ RSpec.describe RubySMB::SMB1::Tree do
|
|
118
118
|
end
|
119
119
|
tree.open_file(filename: unicode_filename.chop)
|
120
120
|
end
|
121
|
+
|
122
|
+
it 'removes the leading \\ from the filename if needed' do
|
123
|
+
expect(tree).to receive(:_open).with(filename: filename)
|
124
|
+
tree.open_file(filename: '\\' + filename)
|
125
|
+
end
|
121
126
|
end
|
122
127
|
|
123
128
|
describe 'flags' do
|
@@ -495,17 +500,17 @@ RSpec.describe RubySMB::SMB1::Tree do
|
|
495
500
|
describe '#open_pipe' do
|
496
501
|
let(:opts) { { filename: 'test', write: true } }
|
497
502
|
before :example do
|
498
|
-
allow(tree).to receive(:
|
503
|
+
allow(tree).to receive(:_open)
|
499
504
|
end
|
500
505
|
|
501
506
|
it 'calls #open_file with the provided options' do
|
502
507
|
opts[:filename] ='\\test'
|
503
|
-
expect(tree).to receive(:
|
508
|
+
expect(tree).to receive(:_open).with(opts)
|
504
509
|
tree.open_pipe(**opts)
|
505
510
|
end
|
506
511
|
|
507
512
|
it 'prepends the filename with \\ if needed' do
|
508
|
-
expect(tree).to receive(:
|
513
|
+
expect(tree).to receive(:_open).with(filename: '\\test', write: true)
|
509
514
|
tree.open_pipe(**opts)
|
510
515
|
end
|
511
516
|
|
@@ -348,6 +348,11 @@ RSpec.describe RubySMB::SMB2::Tree do
|
|
348
348
|
end
|
349
349
|
tree.open_file(filename: filename)
|
350
350
|
end
|
351
|
+
|
352
|
+
it 'removes the leading \\ from the filename if needed' do
|
353
|
+
expect(tree).to receive(:_open).with(filename: filename)
|
354
|
+
tree.open_file(filename: "\\".encode('UTF-16LE') + filename)
|
355
|
+
end
|
351
356
|
end
|
352
357
|
|
353
358
|
describe 'attributes' do
|
@@ -535,17 +540,17 @@ RSpec.describe RubySMB::SMB2::Tree do
|
|
535
540
|
describe '#open_pipe' do
|
536
541
|
let(:opts) { { filename: '\\test', write: true } }
|
537
542
|
before :example do
|
538
|
-
allow(tree).to receive(:
|
543
|
+
allow(tree).to receive(:_open)
|
539
544
|
end
|
540
545
|
|
541
546
|
it 'calls #open_file with the provided options' do
|
542
547
|
opts[:filename] ='test'
|
543
|
-
expect(tree).to receive(:
|
548
|
+
expect(tree).to receive(:_open).with(**opts)
|
544
549
|
tree.open_pipe(**opts)
|
545
550
|
end
|
546
551
|
|
547
|
-
it '
|
548
|
-
expect(tree).to receive(:
|
552
|
+
it 'removes the leading \\ from the filename if needed' do
|
553
|
+
expect(tree).to receive(:_open).with(filename: 'test', write: true)
|
549
554
|
tree.open_pipe(**opts)
|
550
555
|
end
|
551
556
|
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_smb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Metasploit Hackers
|
@@ -97,7 +97,7 @@ cert_chain:
|
|
97
97
|
EknWpNgVhohbot1lfVAMmIhdtOVaRVcQQixWPwprDj/ydB8ryDMDosIMcw+fkoXU
|
98
98
|
9GJsSaSRRYQ9UUkVL27b64okU8D48m8=
|
99
99
|
-----END CERTIFICATE-----
|
100
|
-
date: 2022-
|
100
|
+
date: 2022-03-01 00:00:00.000000000 Z
|
101
101
|
dependencies:
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: redcarpet
|
metadata.gz.sig
CHANGED
Binary file
|