ruby_smb 0.0.20 → 0.0.21

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