ruby_smb 0.0.20 → 0.0.21

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -3
  4. data/examples/pipes.rb +45 -0
  5. data/lib/ruby_smb/client.rb +25 -3
  6. data/lib/ruby_smb/client/negotiation.rb +10 -3
  7. data/lib/ruby_smb/nbss/session_header.rb +3 -3
  8. data/lib/ruby_smb/smb1.rb +1 -0
  9. data/lib/ruby_smb/smb1/bit_field.rb +1 -0
  10. data/lib/ruby_smb/smb1/bit_field/trans_flags.rb +15 -0
  11. data/lib/ruby_smb/smb1/commands.rb +1 -0
  12. data/lib/ruby_smb/smb1/packet.rb +1 -0
  13. data/lib/ruby_smb/smb1/packet/trans.rb +16 -0
  14. data/lib/ruby_smb/smb1/packet/trans/data_block.rb +49 -0
  15. data/lib/ruby_smb/smb1/packet/trans/peek_nmpipe_request.rb +24 -0
  16. data/lib/ruby_smb/smb1/packet/trans/peek_nmpipe_response.rb +59 -0
  17. data/lib/ruby_smb/smb1/packet/trans/request.rb +50 -0
  18. data/lib/ruby_smb/smb1/packet/trans/response.rb +46 -0
  19. data/lib/ruby_smb/smb1/packet/trans/subcommands.rb +11 -0
  20. data/lib/ruby_smb/smb1/packet/trans2/find_next2_response.rb +1 -1
  21. data/lib/ruby_smb/smb1/pipe.rb +65 -0
  22. data/lib/ruby_smb/smb1/tree.rb +8 -1
  23. data/lib/ruby_smb/smb2.rb +1 -0
  24. data/lib/ruby_smb/smb2/file.rb +6 -6
  25. data/lib/ruby_smb/smb2/packet/tree_disconnect_request.rb +1 -0
  26. data/lib/ruby_smb/smb2/pipe.rb +69 -0
  27. data/lib/ruby_smb/smb2/tree.rb +11 -1
  28. data/lib/ruby_smb/version.rb +1 -1
  29. data/spec/lib/ruby_smb/nbss/session_header_spec.rb +4 -4
  30. data/spec/lib/ruby_smb/smb1/bit_field/trans_flags_spec.rb +26 -0
  31. data/spec/lib/ruby_smb/smb1/packet/trans/peek_nmpipe_request_spec.rb +47 -0
  32. data/spec/lib/ruby_smb/smb1/packet/trans/peek_nmpipe_response_spec.rb +31 -0
  33. data/spec/lib/ruby_smb/smb1/packet/trans/request_spec.rb +94 -0
  34. data/spec/lib/ruby_smb/smb1/packet/trans/response_spec.rb +85 -0
  35. data/spec/lib/ruby_smb/smb1/packet/trans2/open2_response_spec.rb +1 -1
  36. data/spec/lib/ruby_smb/smb1/pipe_spec.rb +65 -0
  37. data/spec/lib/ruby_smb/smb2/pipe_spec.rb +64 -0
  38. metadata +27 -2
  39. metadata.gz.sig +0 -0
@@ -0,0 +1,11 @@
1
+ module RubySMB
2
+ module SMB1
3
+ module Packet
4
+ module Trans
5
+ module Subcommands
6
+ PEEK_NMPIPE = 0x0023
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -24,7 +24,7 @@ module RubySMB
24
24
  end
25
25
  end
26
26
 
27
- # The Trans2 Data Blcok for this particular Subcommand
27
+ # The Trans2 Data Block for this particular Subcommand
28
28
  class Trans2Data < BinData::Record
29
29
  rest :buffer, label: 'Results Buffer'
30
30
 
@@ -0,0 +1,65 @@
1
+ module RubySMB
2
+ module SMB1
3
+ # Represents a pipe on the Remote server that we can perform
4
+ # various I/O operations on.
5
+ class Pipe < File
6
+
7
+ # Reference: https://msdn.microsoft.com/en-us/library/ee441883.aspx
8
+ STATUS_DISCONNECTED = 0x0001
9
+ STATUS_LISTENING = 0x0002
10
+ STATUS_OK = 0x0003
11
+ STATUS_CLOSED = 0x0004
12
+
13
+ # Performs a peek operation on the named pipe
14
+ #
15
+ # @param peek_size [Integer] Amount of data to peek
16
+ # @return [RubySMB::SMB1::Packet::Trans::PeekNmpipeResponse]
17
+ # @raise [RubySMB::Error::InvalidPacket] If not a valid PeekNmpipeResponse
18
+ # @raise [RubySMB::Error::UnexpectedStatusCode] If status is not STATUS_BUFFER_OVERFLOW or STATUS_SUCCESS
19
+ def peek(peek_size: 0)
20
+ packet = RubySMB::SMB1::Packet::Trans::PeekNmpipeRequest.new
21
+ packet.fid = @fid
22
+ packet.parameter_block.max_data_count = peek_size
23
+ packet = @tree.set_header_fields(packet)
24
+ raw_response = @tree.client.send_recv(packet)
25
+ response = RubySMB::SMB1::Packet::Trans::PeekNmpipeResponse.read(raw_response)
26
+
27
+ unless response.status_code == WindowsError::NTStatus::STATUS_BUFFER_OVERFLOW or response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
28
+ raise RubySMB::Error::UnexpectedStatusCode, response.status_code.name
29
+ end
30
+
31
+ unless response.smb_header.command == RubySMB::SMB1::Commands::SMB_COM_TRANSACTION
32
+ raise RubySMB::Error::InvalidPacket, 'Not a TransResponse packet'
33
+ end
34
+ response
35
+ end
36
+
37
+ # @return [Integer] The number of bytes available to be read from the pipe
38
+ def peek_available
39
+ packet = peek
40
+ # Only 1 of these should be non-zero
41
+ packet.data_block.trans_parameters.read_data_available or packet.data_block.trans_parameters.message_bytes_length
42
+ end
43
+
44
+ # @return [Integer] Pipe status
45
+ def peek_state
46
+ packet = peek
47
+ packet.data_block.trans_parameters.pipe_state
48
+ end
49
+
50
+ # @return [Boolean] True if pipe is connected, false otherwise
51
+ def is_connected?
52
+ begin
53
+ state = peek_state
54
+ rescue RubySMB::Error::UnexpectedStatusCode => e
55
+ if e.message == 'STATUS_INVALID_HANDLE'
56
+ return false
57
+ end
58
+ raise e
59
+ end
60
+ state == STATUS_OK
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -120,7 +120,14 @@ module RubySMB
120
120
  raise RubySMB::Error::UnexpectedStatusCode, response.status_code.name
121
121
  end
122
122
 
123
- RubySMB::SMB1::File.new(name: filename, tree: self, response: response)
123
+ case response.parameter_block.resource_type
124
+ when RubySMB::SMB1::ResourceType::BYTE_MODE_PIPE, RubySMB::SMB1::ResourceType::MESSAGE_MODE_PIPE
125
+ RubySMB::SMB1::Pipe.new(name: filename, tree: self, response: response)
126
+ when RubySMB::SMB1::ResourceType::DISK
127
+ RubySMB::SMB1::File.new(name: filename, tree: self, response: response)
128
+ else
129
+ raise RubySMB::Error::RubySMBError
130
+ end
124
131
  end
125
132
 
126
133
  # List `directory` on the remote share.
@@ -14,5 +14,6 @@ module RubySMB
14
14
  require 'ruby_smb/smb2/packet'
15
15
  require 'ruby_smb/smb2/tree'
16
16
  require 'ruby_smb/smb2/file'
17
+ require 'ruby_smb/smb2/pipe'
17
18
  end
18
19
  end
@@ -94,8 +94,8 @@ module RubySMB
94
94
  # @param offset [Integer] the byte offset in the file to start reading from
95
95
  # @return [String] the data read from the file
96
96
  def read(bytes: size, offset: 0)
97
- atomic_read_size = if bytes > MAX_PACKET_SIZE
98
- MAX_PACKET_SIZE
97
+ atomic_read_size = if bytes > tree.client.server_max_read_size
98
+ tree.client.server_max_read_size
99
99
  else
100
100
  bytes
101
101
  end
@@ -110,7 +110,7 @@ module RubySMB
110
110
 
111
111
  while remaining_bytes > 0
112
112
  offset += atomic_read_size
113
- atomic_read_size = remaining_bytes if remaining_bytes < MAX_PACKET_SIZE
113
+ atomic_read_size = remaining_bytes if remaining_bytes < tree.client.server_max_read_size
114
114
 
115
115
  read_request = read_packet(read_length: atomic_read_size, offset: offset)
116
116
  raw_response = tree.client.send_recv(read_request)
@@ -184,9 +184,9 @@ module RubySMB
184
184
  def write(data:'', offset: 0)
185
185
  buffer = data.dup
186
186
  bytes = data.length
187
- atomic_write_size = if bytes > MAX_PACKET_SIZE
188
- MAX_PACKET_SIZE
189
- else
187
+ atomic_write_size = if bytes > tree.client.server_max_write_size
188
+ tree.client.server_max_write_size
189
+ else
190
190
  bytes
191
191
  end
192
192
 
@@ -7,6 +7,7 @@ module RubySMB
7
7
  endian :little
8
8
  smb2_header :smb2_header
9
9
  uint16 :structure_size, label: 'Structure Size', initial_value: 4
10
+ uint16 :reserved, label: 'Reserved', initial_value: 0
10
11
 
11
12
  def initialize_instance
12
13
  super
@@ -0,0 +1,69 @@
1
+ module RubySMB
2
+ module SMB2
3
+ # Represents a pipe on the Remote server that we can perform
4
+ # various I/O operations on.
5
+ class Pipe < File
6
+
7
+ STATUS_CONNECTED = 0x00000003
8
+ STATUS_CLOSING = 0x00000004
9
+
10
+ # Performs a peek operation on the named pipe
11
+ #
12
+ # @param peek_size [Integer] Amount of data to peek
13
+ # @return [RubySMB::SMB2::Packet::IoctlResponse]
14
+ # @raise [RubySMB::Error::InvalidPacket] if not a valid FSCTL_PIPE_PEEK response
15
+ # @raise [RubySMB::Error::UnexpectedStatusCode] If status is not STATUS_BUFFER_OVERFLOW or STATUS_SUCCESS
16
+ def peek(peek_size: 0)
17
+ packet = RubySMB::SMB2::Packet::IoctlRequest.new
18
+ packet.ctl_code = RubySMB::Fscc::ControlCodes::FSCTL_PIPE_PEEK
19
+ packet.flags.is_fsctl = true
20
+ # read at least 16 bytes for state, avail, msg_count, first_msg_len
21
+ packet.max_output_response = 16 + peek_size
22
+ packet = set_header_fields(packet)
23
+ raw_response = @tree.client.send_recv(packet)
24
+ begin
25
+ response = RubySMB::SMB2::Packet::IoctlResponse.read(raw_response)
26
+ rescue IOError
27
+ response = RubySMB::SMB2::Packet::ErrorPacket.read(raw_response)
28
+ end
29
+
30
+ unless response.status_code == WindowsError::NTStatus::STATUS_BUFFER_OVERFLOW or response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
31
+ raise RubySMB::Error::UnexpectedStatusCode, response.status_code.name
32
+ end
33
+
34
+ unless response.smb2_header.command == RubySMB::SMB2::Commands::IOCTL
35
+ raise RubySMB::Error::InvalidPacket, 'Not an IoctlResponse packet'
36
+ end
37
+ response
38
+ end
39
+
40
+ # @return [Integer] The number of bytes available to be read from the pipe
41
+ def peek_available
42
+ packet = peek
43
+ state, avail, msg_count, first_msg_len = packet.buffer.unpack('VVVV')
44
+ # Only 1 of these should be non-zero
45
+ avail or first_msg_len
46
+ end
47
+
48
+ # @return [Integer] Pipe status
49
+ def peek_state
50
+ packet = peek
51
+ packet.buffer.unpack('V')[0]
52
+ end
53
+
54
+ # @return [Boolean] True if pipe is connected, false otherwise
55
+ def is_connected?
56
+ begin
57
+ state = peek_state
58
+ rescue RubySMB::Error::UnexpectedStatusCode => e
59
+ if e.message == 'STATUS_FILE_CLOSED'
60
+ return false
61
+ end
62
+ raise e
63
+ end
64
+ state == STATUS_CONNECTED
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -28,6 +28,7 @@ module RubySMB
28
28
  @share = share
29
29
  @id = response.smb2_header.tree_id
30
30
  @permissions = response.maximal_access
31
+ @share_type = response.share_type
31
32
  end
32
33
 
33
34
  # Disconnects this Tree from the current session
@@ -89,7 +90,16 @@ module RubySMB
89
90
  raw_response = client.send_recv(create_request)
90
91
  response = RubySMB::SMB2::Packet::CreateResponse.read(raw_response)
91
92
 
92
- RubySMB::SMB2::File.new(name: filename, tree: self, response: response)
93
+ case @share_type
94
+ when 0x01
95
+ RubySMB::SMB2::File.new(name: filename, tree: self, response: response)
96
+ when 0x02
97
+ RubySMB::SMB2::Pipe.new(name: filename, tree: self, response: response)
98
+ # when 0x03
99
+ # it's a printer!
100
+ else
101
+ raise RubySMB::Error::RubySMBError
102
+ end
93
103
  end
94
104
 
95
105
  # List `directory` on the remote share.
@@ -1,3 +1,3 @@
1
1
  module RubySMB
2
- VERSION = '0.0.20'.freeze
2
+ VERSION = '0.0.21'.freeze
3
3
  end
@@ -16,14 +16,14 @@ RSpec.describe RubySMB::Nbss::SessionHeader do
16
16
  end
17
17
 
18
18
  describe '#flags' do
19
- it 'is a 8-bit Unsigned Integer' do
20
- expect(session_header.flags).to be_a BinData::Uint8
19
+ it 'is a 7-bit Unsigned Integer' do
20
+ expect(session_header.flags).to be_a BinData::Bit7
21
21
  end
22
22
  end
23
23
 
24
24
  describe '#packet_length' do
25
- it 'is a 16-bit Unsigned Integer' do
26
- expect(session_header.packet_length).to be_a BinData::Uint16be
25
+ it 'is a 17-bit Unsigned Integer' do
26
+ expect(session_header.packet_length).to be_a BinData::Bit17
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,26 @@
1
+ RSpec.describe RubySMB::SMB1::BitField::TransFlags do
2
+ subject(:flags) { described_class.new }
3
+
4
+ it { is_expected.to respond_to :no_response }
5
+ it { is_expected.to respond_to :disconnect }
6
+
7
+ it 'is little endian' do
8
+ expect(described_class.fields.instance_variable_get(:@hints)[:endian]).to eq :little
9
+ end
10
+
11
+ describe '#no_response' do
12
+ it 'should be a 1-bit field per the SMB spec' do
13
+ expect(flags.no_response).to be_a BinData::Bit1
14
+ end
15
+
16
+ it_behaves_like 'bit field with one flag set', :no_response, 'C', 0x02
17
+ end
18
+
19
+ describe '#disconnect' do
20
+ it 'should be a 1-bit field per the SMB spec' do
21
+ expect(flags.disconnect).to be_a BinData::Bit1
22
+ end
23
+
24
+ it_behaves_like 'bit field with one flag set', :disconnect, 'C', 0x01
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::SMB1::Packet::Trans::PeekNmpipeRequest do
4
+
5
+ subject(:packet) { described_class.new }
6
+
7
+ describe '#smb_header' do
8
+ subject(:header) { packet.smb_header }
9
+
10
+ it 'is a standard SMB Header' do
11
+ expect(header).to be_a RubySMB::SMB1::SMBHeader
12
+ end
13
+
14
+ it 'should have the command set to SMB_COM_TRANSACTION' do
15
+ expect(header.command).to eq RubySMB::SMB1::Commands::SMB_COM_TRANSACTION
16
+ end
17
+
18
+ it 'should not have the response flag set' do
19
+ expect(header.flags.reply).to eq 0
20
+ end
21
+ end
22
+
23
+ describe '#parameter_block' do
24
+ subject(:parameter_block) { packet.parameter_block }
25
+
26
+ it 'is a standard Trans ParameterBlock' do
27
+ expect(parameter_block).to be_a RubySMB::SMB1::Packet::Trans::Request::ParameterBlock
28
+ end
29
+
30
+ it 'should have a setup_count of 2' do
31
+ expect(parameter_block.setup_count).to eq 2
32
+ end
33
+
34
+ it 'should have subcommand PEEK_NMPIPE' do
35
+ expect(parameter_block.setup[0]).to eq RubySMB::SMB1::Packet::Trans::Subcommands::PEEK_NMPIPE
36
+ end
37
+ end
38
+
39
+ describe '#data_block' do
40
+ subject(:data_block) { packet.data_block }
41
+
42
+ it 'should have a name of \\PIPE\\' do
43
+ expect(data_block.name).to eq "\\PIPE\\"
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::SMB1::Packet::Trans::PeekNmpipeResponse do
4
+
5
+ subject(:packet) { described_class.new }
6
+
7
+ describe '#smb_header' do
8
+ subject(:header) { packet.smb_header }
9
+
10
+ it 'is a standard SMB Header' do
11
+ expect(header).to be_a RubySMB::SMB1::SMBHeader
12
+ end
13
+
14
+ it 'should have the command set to SMB_COM_TRANSACTION' do
15
+ expect(header.command).to eq RubySMB::SMB1::Commands::SMB_COM_TRANSACTION
16
+ end
17
+
18
+ it 'should have the response flag set' do
19
+ expect(header.flags.reply).to eq 1
20
+ end
21
+ end
22
+
23
+ describe '#parameter_block' do
24
+ subject(:parameter_block) { packet.parameter_block }
25
+
26
+ it 'is a standard Trans ParameterBlock' do
27
+ expect(parameter_block).to be_a RubySMB::SMB1::Packet::Trans::Response::ParameterBlock
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::SMB1::Packet::Trans::Request do
4
+ subject(:packet) { described_class.new }
5
+
6
+ describe '#smb_header' do
7
+ subject(:header) { packet.smb_header }
8
+
9
+ it 'is a standard SMB Header' do
10
+ expect(header).to be_a RubySMB::SMB1::SMBHeader
11
+ end
12
+
13
+ it 'should have the command set to SMB_COM_NEGOTIATE' do
14
+ expect(header.command).to eq RubySMB::SMB1::Commands::SMB_COM_TRANSACTION
15
+ end
16
+
17
+ it 'should not have the response flag set' do
18
+ expect(header.flags.reply).to eq 0
19
+ end
20
+ end
21
+
22
+ describe '#parameter_block' do
23
+ subject(:parameter_block) { packet.parameter_block }
24
+
25
+ it 'is a standard ParameterBlock' do
26
+ expect(parameter_block).to be_a RubySMB::SMB1::ParameterBlock
27
+ end
28
+
29
+ it { is_expected.to respond_to :total_parameter_count }
30
+ it { is_expected.to respond_to :total_data_count }
31
+ it { is_expected.to respond_to :max_parameter_count }
32
+ it { is_expected.to respond_to :max_data_count }
33
+ it { is_expected.to respond_to :max_setup_count }
34
+ it { is_expected.to respond_to :timeout }
35
+ it { is_expected.to respond_to :parameter_count }
36
+ it { is_expected.to respond_to :parameter_offset }
37
+ it { is_expected.to respond_to :data_count }
38
+ it { is_expected.to respond_to :data_offset }
39
+ it { is_expected.to respond_to :setup_count }
40
+
41
+ describe 'flags' do
42
+ it 'is a trans_flags BitField' do
43
+ expect(parameter_block.flags).to be_a RubySMB::SMB1::BitField::TransFlags
44
+ end
45
+ end
46
+
47
+ describe 'parameter_count' do
48
+ it 'is a count of bytes in the data_block trans_parameters field' do
49
+ packet.data_block.trans_parameters = "\x00\x01\x02\x03"
50
+ expect(parameter_block.parameter_count).to eq 4
51
+ end
52
+ end
53
+
54
+ describe 'parameter_offset' do
55
+ it ' contains the absolute_offset to the data_block trans_parameters field' do
56
+ expect(parameter_block.parameter_offset).to eq packet.data_block.trans_parameters.abs_offset
57
+ end
58
+ end
59
+
60
+ describe 'data_count' do
61
+ it 'is a count of bytes in the data_block trans_data field' do
62
+ packet.data_block.trans_data = "\x00\x01\x02\x03"
63
+ expect(parameter_block.data_count).to eq 4
64
+ end
65
+ end
66
+
67
+ describe 'data_offset' do
68
+ it 'contains the absolute_offset to the data_block trans_data field' do
69
+ expect(parameter_block.data_offset).to eq packet.data_block.trans_data.abs_offset
70
+ end
71
+ end
72
+ end
73
+
74
+ describe '#data_block' do
75
+ subject(:data_block) { packet.data_block }
76
+
77
+ it 'is a standard DataBlock' do
78
+ expect(data_block).to be_a RubySMB::SMB1::DataBlock
79
+ end
80
+
81
+ it { is_expected.to respond_to :name }
82
+ it { is_expected.to respond_to :trans_parameters }
83
+ it { is_expected.to respond_to :trans_data }
84
+
85
+ it 'should keep #trans_parameters 4-byte aligned' do
86
+ expect(data_block.trans_parameters.abs_offset % 4).to eq 0
87
+ end
88
+
89
+ it 'should keep #trans_data 4-byte aligned' do
90
+ data_block.trans_parameters = 'a'
91
+ expect(data_block.trans_data.abs_offset % 4).to eq 0
92
+ end
93
+ end
94
+ end