ruby_smb 3.0.2 → 3.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|