diff --git a/lib/ruby_smb/dcerpc/request.rb b/lib/ruby_smb/dcerpc/request.rb index 1267c33ee..7931ff397 100644 --- a/lib/ruby_smb/dcerpc/request.rb +++ b/lib/ruby_smb/dcerpc/request.rb @@ -80,7 +80,8 @@ class Request < BinData::Record string :default end choice 'Wkssvc', selection: -> { opnum } do - netr_wksta_get_info_request Wkssvc::NETR_WKSTA_GET_INFO + netr_wksta_get_info_request Wkssvc::NETR_WKSTA_GET_INFO + netr_wksta_user_enum_request Wkssvc::NETR_WKSTA_USER_ENUM string :default end choice 'Epm', selection: -> { opnum } do diff --git a/lib/ruby_smb/dcerpc/wkssvc.rb b/lib/ruby_smb/dcerpc/wkssvc.rb index 278a4820b..0291e2324 100644 --- a/lib/ruby_smb/dcerpc/wkssvc.rb +++ b/lib/ruby_smb/dcerpc/wkssvc.rb @@ -7,7 +7,8 @@ module Wkssvc VER_MINOR = 0 # Operation numbers - NETR_WKSTA_GET_INFO = 0x0000 + NETR_WKSTA_GET_INFO = 0x0000 + NETR_WKSTA_USER_ENUM = 0x0002 PLATFORM_ID = { 0x0000012C => "DOS", @@ -23,9 +24,85 @@ module Wkssvc WKSTA_INFO_102 = 0x00000066 #TODO: WKSTA_INFO_502 = 0x000001F6 + # User Enum Information Level + WKSTA_USER_INFO_0 = 0x00000000 + WKSTA_USER_INFO_1 = 0x00000001 + + # [2.2.2.1 WKSSVC_IDENTIFY_HANDLE](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/9ef94a11-0e5c-49d7-9ac7-68d6f03565de) + class WkssvcIdentifyHandle < Ndr::NdrWideStringzPtr; end + + # [2.2.5.9 WKSTA_USER_INFO_0](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/b7c53c6f-8b92-4e5d-9a2e-6462cb4ef1ac) + class WkstaUserInfo0 < Ndr::NdrStruct + default_parameter byte_align: 4 + endian :little + + ndr_wide_stringz_ptr :wkui0_username + end + + class WkstaUserInfo0ArrayPtr < Ndr::NdrConfArray + default_parameter type: :wksta_user_info0 + extend Ndr::PointerClassPlugin + end + + # [2.2.5.12 WKSTA_USER_INFO_0_CONTAINER](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/0b0cff8f-09bc-43a8-b0d3-88f0bf7e3664) + class WkstaUserInfo0Container < Ndr::NdrStruct + default_parameter byte_align: 4 + endian :little + + ndr_uint32 :wkui0_entries_read + wksta_user_info0_array_ptr :wkui0_buffer + end + + class PwkstaUserInfo0Container < WkstaUserInfo0Container + extend Ndr::PointerClassPlugin + end + + # [2.2.5.10 WKSTA_USER_INFO_1](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/c37b9606-866f-40ac-9490-57b8334968e2) + class WkstaUserInfo1 < Ndr::NdrStruct + default_parameter byte_align: 4 + endian :little + + ndr_wide_stringz_ptr :wkui1_username + ndr_wide_stringz_ptr :wkui1_logon_domain + ndr_wide_stringz_ptr :wkui1_oth_domains + ndr_wide_stringz_ptr :wkui1_logon_server + end + + class WkstaUserInfo1ArrayPtr < Ndr::NdrConfArray + default_parameter type: :wksta_user_info1 + extend Ndr::PointerClassPlugin + end + + # [2.2.5.13 WKSTA_USER_INFO_1_CONTAINER](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/22a813e4-fc7d-4fe3-a6d6-78debfd2c0c9) + class WkstaUserInfo1Container < Ndr::NdrStruct + default_parameter byte_align: 4 + endian :little + + ndr_uint32 :wkui1_entries_read + wksta_user_info1_array_ptr :wkui1_buffer + end + + class PwkstaUserInfo1Container < WkstaUserInfo1Container + extend Ndr::PointerClassPlugin + end + + # [2.2.5.14 WKSTA_USER_ENUM_STRUCT](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/4041455a-52be-4389-a4fc-82fea3cb3160) + class WkstaUserEnumStructure < Ndr::NdrStruct + default_parameter byte_align: 4 + endian :little + + ndr_uint32 :level + ndr_uint32 :tag, value: -> { self.level } + choice :info, selection: :level, byte_align: 4 do + pwksta_user_info0_container WKSTA_USER_INFO_0 + pwksta_user_info1_container WKSTA_USER_INFO_1 + end + end require 'ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request' require 'ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_response' + require 'ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_request' + require 'ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_response' # Returns details about a computer environment, including # platform-specific information, the names of the domain and local @@ -33,14 +110,14 @@ module Wkssvc # # @param server_name [optional, String] String that identifies the server (optional # since it is ignored by the server) - # @param server_name [optional, Integer] The information level of the data (default: WKSTA_INFO_100) + # @param level [optional, Integer] The information level of the data (default: WKSTA_INFO_100) # @return [RubySMB::Dcerpc::Wkssvc::WkstaInfo100, RubySMB::Dcerpc::Wkssvc::WkstaInfo101, # RubySMB::Dcerpc::Wkssvc::WkstaInfo102] The structure containing the requested information # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a # NetrWkstaGetInfoResponse packet # @raise [RubySMB::Dcerpc::Error::WkssvcError] if the response error status # is not STATUS_SUCCESS - def netr_wksta_get_info(server_name: "\x00", level: WKSTA_INFO_100) + def netr_wksta_get_info(server_name: '', level: WKSTA_INFO_100) wkst_netr_wksta_get_info_request = NetrWkstaGetInfoRequest.new( server_name: server_name, level: level @@ -59,6 +136,44 @@ def netr_wksta_get_info(server_name: "\x00", level: WKSTA_INFO_100) wkst_netr_wksta_get_info_response.wksta_info.info end + # Returns details about users who are currently active on a remote computer. + # + # @param server_name [optional, String] String that identifies the server (optional + # since it is ignored by the server) + # @param level [optional, Integer] The information level of the data (default: WKSTA_USER_INFO_0) + # @return [RubySMB::Dcerpc::Wkssvc::WkstaUserInfo0, RubySMB::Dcerpc::Wkssvc::WkstaUserInfo1] + # The structure containing the requested information + # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a + # NetrWkstaGetInfoResponse packet + # @raise [RubySMB::Dcerpc::Error::WkssvcError] if the response error status + # is not STATUS_SUCCESS + def netr_wksta_user_enum(server_name: '', level: WKSTA_USER_INFO_0) + wkst_netr_wksta_enum_user_request = NetrWkstaUserEnumRequest.new( + server_name: server_name, + user_info: { + level: level, + tag: level, + info: { + wkui0_entries_read: 0, + }, + }, + preferred_max_length: 0xFFFFFFFF, + result_handle: 0 + ) + response = dcerpc_request(wkst_netr_wksta_enum_user_request) + begin + wkst_netr_wksta_enum_user_response = NetrWkstaUserEnumResponse.read(response) + rescue IOError + raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading WkstNetrWkstaUserEnumResponse' + end + unless wkst_netr_wksta_enum_user_response.error_status == WindowsError::NTStatus::STATUS_SUCCESS + raise RubySMB::Dcerpc::Error::WkssvcError, + "Error returned with netr_wksta_enum_user: #{wkst_netr_wksta_enum_user_response.error_status.value} - "\ + "#{WindowsError::NTStatus.find_by_retval(wkst_netr_wksta_enum_user_response.error_status.value).join(',')}" + end + wkst_netr_wksta_enum_user_response.user_info.info + end + end end end diff --git a/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request.rb b/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request.rb index b9b1eb5fe..9de032b9c 100644 --- a/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request.rb +++ b/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request.rb @@ -2,9 +2,6 @@ module RubySMB module Dcerpc module Wkssvc - # [2.2.2.1 WKSSVC_IDENTIFY_HANDLE](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/9ef94a11-0e5c-49d7-9ac7-68d6f03565de) - class WkssvcIdentifyHandle < Ndr::NdrWideStringPtr; end - # [3.2.4.1 NetrWkstaGetInfo (Opnum 0)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/4af41d6f-b800-4de1-af5b-0b15a85f8e04) class NetrWkstaGetInfoRequest < BinData::Record attr_reader :opnum diff --git a/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_request.rb b/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_request.rb new file mode 100644 index 000000000..9412bce9a --- /dev/null +++ b/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_request.rb @@ -0,0 +1,25 @@ +module RubySMB + module Dcerpc + module Wkssvc + + # [3.2.4.3 NetrWkstaUserEnum (Opnum 2)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/4af41d6f-b800-4de1-af5b-0b15a85f8e04) + class NetrWkstaUserEnumRequest < BinData::Record + attr_reader :opnum + + endian :little + + wkssvc_identify_handle :server_name + wksta_user_enum_structure :user_info + ndr_uint32 :preferred_max_length, initial_value: 0xFFFFFFFF + ndr_uint32_ptr :result_handle, initial_value: 0 + + def initialize_instance + super + @opnum = NETR_WKSTA_USER_ENUM + end + end + + end + end +end + diff --git a/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_response.rb b/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_response.rb new file mode 100644 index 000000000..49cb256a0 --- /dev/null +++ b/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_response.rb @@ -0,0 +1,25 @@ +module RubySMB + module Dcerpc + module Wkssvc + + # [3.2.4.3 NetrWkstaUserEnum (Opnum 2)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wkst/4af41d6f-b800-4de1-af5b-0b15a85f8e04) + class NetrWkstaUserEnumResponse < BinData::Record + attr_reader :opnum + + endian :little + + wksta_user_enum_structure :user_info + ndr_uint32_ptr :total_entries + ndr_uint32_ptr :result_handle + ndr_uint32 :error_status + + def initialize_instance + super + @opnum = NETR_WKSTA_USER_ENUM + end + end + + end + end +end + diff --git a/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request_spec.rb b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request_spec.rb index 3cfd38010..90ec26b0f 100644 --- a/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request_spec.rb +++ b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_request_spec.rb @@ -1,11 +1,3 @@ -RSpec.describe RubySMB::Dcerpc::Wkssvc::WkssvcIdentifyHandle do - subject(:packet) { described_class.new } - - it 'is a Ndr::NdrWideStringPtr' do - expect(packet).to be_a(RubySMB::Dcerpc::Ndr::NdrWideStringPtr) - end -end - RSpec.describe RubySMB::Dcerpc::Wkssvc::NetrWkstaGetInfoRequest do subject(:packet) { described_class.new } diff --git a/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_response_spec.rb b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_response_spec.rb index 4e8e251cb..c29c67e25 100644 --- a/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_response_spec.rb +++ b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_get_info_response_spec.rb @@ -305,7 +305,7 @@ it 'is little endian' do expect(described_class.fields.instance_variable_get(:@hints)[:endian]).to eq :little end - it 'it is a Ndr::NdrStruct' do + it 'is a Ndr::NdrStruct' do expect(described_class).to be < RubySMB::Dcerpc::Ndr::NdrStruct end describe '#level' do diff --git a/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_identity_handle.rb b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_identity_handle.rb new file mode 100644 index 000000000..6fa4c8feb --- /dev/null +++ b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_identity_handle.rb @@ -0,0 +1,7 @@ +RSpec.describe RubySMB::Dcerpc::Wkssvc::WkssvcIdentifyHandle do + subject(:packet) { described_class.new } + + it 'is a Ndr::NdrWideStringPtr' do + expect(packet).to be_a(RubySMB::Dcerpc::Ndr::NdrWideStringPtr) + end +end diff --git a/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_request_spec.rb b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_request_spec.rb new file mode 100644 index 000000000..54a18258f --- /dev/null +++ b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_request_spec.rb @@ -0,0 +1,71 @@ +RSpec.describe RubySMB::Dcerpc::Wkssvc::NetrWkstaUserEnumRequest do + subject(:packet) { described_class.new } + + def random_str(nb = 8) + nb.times.map { rand('a'.ord..'z'.ord).chr }.join + end + + it { is_expected.to respond_to :server_name } + it { is_expected.to respond_to :user_info } + it { is_expected.to respond_to :preferred_max_length } + it { is_expected.to respond_to :result_handle } + it { is_expected.to respond_to :opnum } + + it 'is little endian' do + expect(described_class.fields.instance_variable_get(:@hints)[:endian]).to eq :little + end + it 'is a BinData::Record' do + expect(packet).to be_a(BinData::Record) + end + describe '#server_name' do + it 'is a WkssvcIdentifyHandle structure' do + expect(packet.server_name).to be_a RubySMB::Dcerpc::Wkssvc::WkssvcIdentifyHandle + end + end + describe '#user_info' do + it 'is a WkstaUserEnumStructure structure' do + expect(packet.user_info).to be_a RubySMB::Dcerpc::Wkssvc::WkstaUserEnumStructure + end + end + describe '#preferred_max_length' do + it 'is a NdrUint32 structure' do + expect(packet.preferred_max_length).to be_a RubySMB::Dcerpc::Ndr::NdrUint32 + end + + it 'has a default value of 0xFFFFFFFF' do + expect(packet.preferred_max_length).to eq(0xFFFFFFFF) + end + end + describe '#result_handle' do + it 'is a NdrUint32Ptr structure' do + expect(packet.result_handle).to be_a RubySMB::Dcerpc::Ndr::NdrUint32Ptr + end + + it 'has a default value of 0' do + expect(packet.result_handle).to eq(0) + end + end + describe '#initialize_instance' do + it 'sets #opnum to NETR_WKSTA_USER_ENUM constant' do + expect(packet.opnum).to eq(RubySMB::Dcerpc::Wkssvc::NETR_WKSTA_USER_ENUM) + end + end + it 'reads itself' do + packet = described_class.new( + server_name: 'TestServer', + user_info: { + level: RubySMB::Dcerpc::Wkssvc::WKSTA_USER_INFO_0, + info: { + wkui0_entries_read: 1, + wkui0_buffer: [{ + wkui0_username: random_str + }], + }, + }, + preferred_max_length: 0xFFFFFFFF, + result_handle: 0 + ) + binary = packet.to_binary_s + expect(described_class.read(binary)).to eq(packet) + end +end diff --git a/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_response_spec.rb b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_response_spec.rb new file mode 100644 index 000000000..db09dcd2f --- /dev/null +++ b/spec/lib/ruby_smb/dcerpc/wkssvc/netr_wksta_user_enum_response_spec.rb @@ -0,0 +1,65 @@ +RSpec.describe RubySMB::Dcerpc::Wkssvc::NetrWkstaUserEnumResponse do + subject(:packet) { described_class.new } + + def random_str(nb = 8) + nb.times.map { rand('a'.ord..'z'.ord).chr }.join + end + + it { is_expected.to respond_to :user_info } + it { is_expected.to respond_to :total_entries } + it { is_expected.to respond_to :result_handle } + it { is_expected.to respond_to :error_status } + + it 'is little endian' do + expect(described_class.fields.instance_variable_get(:@hints)[:endian]).to eq :little + end + it 'is a BinData::Record' do + expect(packet).to be_a(BinData::Record) + end + describe '#user_info' do + it 'is a WkstaUserEnumStructure structure' do + expect(packet.user_info).to be_a RubySMB::Dcerpc::Wkssvc::WkstaUserEnumStructure + end + end + describe '#total_entries' do + it 'is a NdrUint32Ptr structure' do + expect(packet.total_entries).to be_a RubySMB::Dcerpc::Ndr::NdrUint32Ptr + end + end + describe '#result_handle' do + it 'is a NdrUint32Ptr structure' do + expect(packet.result_handle).to be_a RubySMB::Dcerpc::Ndr::NdrUint32Ptr + end + end + describe '#error_status' do + it 'is a NdrUint32 structure' do + expect(packet.error_status).to be_a RubySMB::Dcerpc::Ndr::NdrUint32 + end + end + describe '#initialize_instance' do + it 'sets #opnum to NETR_WKSTA_USER_ENUM constant' do + expect(packet.opnum).to eq(RubySMB::Dcerpc::Wkssvc::NETR_WKSTA_USER_ENUM) + end + end + it 'reads itself' do + packet = described_class.new( + user_info: { + level: RubySMB::Dcerpc::Wkssvc::WKSTA_USER_INFO_1, + info: { + wkui1_entries_read: 1, + wkui1_buffer: [{ + wkui1_username: random_str, + wkui1_logon_domain: random_str, + wkui1_oth_domains: random_str, + wkui1_logon_server: random_str + }], + }, + }, + total_entries: 1, + result_handle: 0, + error_status: 0 + ) + binary = packet.to_binary_s + expect(described_class.read(binary)).to eq(packet) + end +end diff --git a/spec/lib/ruby_smb/dcerpc/wkssvc_spec.rb b/spec/lib/ruby_smb/dcerpc/wkssvc_spec.rb index fb5020d8e..e97152041 100644 --- a/spec/lib/ruby_smb/dcerpc/wkssvc_spec.rb +++ b/spec/lib/ruby_smb/dcerpc/wkssvc_spec.rb @@ -1,3 +1,11 @@ +RSpec.describe RubySMB::Dcerpc::Wkssvc::WkssvcIdentifyHandle do + subject(:packet) { described_class.new } + + it 'is a Ndr::NdrWideStringzPtr' do + expect(packet).to be_a(RubySMB::Dcerpc::Ndr::NdrWideStringzPtr) + end +end + RSpec.describe RubySMB::Dcerpc::Wkssvc do let(:wkssvc) do RubySMB::SMB1::Pipe.new( @@ -23,7 +31,7 @@ it 'sets the request with the expected values' do wkssvc.netr_wksta_get_info expect(described_class::NetrWkstaGetInfoRequest).to have_received(:new).with( - server_name: "\x00", + server_name: '', level: described_class::WKSTA_INFO_100 ) end @@ -67,4 +75,53 @@ end end end + + describe '#netr_wksta_user_enum' do + let(:wkst_netr_wksta_user_enum_request) { double('NetrWkstaUserEnumRequest') } + let(:response) { double('Response') } + let(:wkst_netr_wksta_user_enum_response) { double('NetrWkstaUserEnumResponse') } + let(:info) { double('info') } + before :example do + allow(described_class::NetrWkstaUserEnumRequest).to receive(:new).and_return(wkst_netr_wksta_user_enum_request) + allow(wkssvc).to receive(:dcerpc_request).and_return(response) + allow(described_class::NetrWkstaUserEnumResponse).to receive(:read).and_return(wkst_netr_wksta_user_enum_response) + allow(wkst_netr_wksta_user_enum_response).to receive(:error_status).and_return(WindowsError::Win32::ERROR_SUCCESS) + allow(wkst_netr_wksta_user_enum_response).to receive_message_chain(:user_info, :info => info) + end + + it 'sets the request with the expected values' do + wkssvc.netr_wksta_user_enum + expect(described_class::NetrWkstaUserEnumRequest).to have_received(:new).with( + server_name: '', + user_info: { + level: described_class::WKSTA_USER_INFO_0, + tag: described_class::WKSTA_USER_INFO_0, + info: { + wkui0_entries_read: 0, + }, + }, + preferred_max_length: 0xFFFFFFFF, + result_handle: 0 + ) + end + it 'send the expected request structure' do + wkssvc.netr_wksta_user_enum + expect(wkssvc).to have_received(:dcerpc_request).with(wkst_netr_wksta_user_enum_request) + end + context 'when an IOError occurs while parsing the response' do + it 'raises a RubySMB::Dcerpc::Error::InvalidPacket' do + allow(described_class::NetrWkstaUserEnumResponse).to receive(:read).and_raise(IOError) + expect { wkssvc.netr_wksta_user_enum }.to raise_error(RubySMB::Dcerpc::Error::InvalidPacket) + end + end + context 'when the response error status is not WindowsError::Win32::ERROR_SUCCESS' do + it 'raises a RubySMB::Dcerpc::Error::WinregError' do + allow(wkst_netr_wksta_user_enum_response).to receive(:error_status).and_return(WindowsError::Win32::ERROR_INVALID_DATA) + expect { wkssvc.netr_wksta_user_enum }.to raise_error(RubySMB::Dcerpc::Error::WkssvcError) + end + end + it 'returns the expected handler' do + expect(wkssvc.netr_wksta_user_enum).to eq(info) + end + end end