From 9ba051ea420310f57ae7255875b39f00efab51ef Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Tue, 26 Mar 2024 11:03:42 +0100 Subject: [PATCH] Add GetKeySecurity and SetKeySecurity MS-RRP structures, specs and example script (DCERPC) --- examples/registry_key_security_descriptor.rb | 109 +++++++ lib/ruby_smb/client/winreg.rb | 12 + lib/ruby_smb/dcerpc/ndr.rb | 14 +- lib/ruby_smb/dcerpc/request.rb | 34 ++- lib/ruby_smb/dcerpc/rrp_rpc_unicode_string.rb | 2 +- lib/ruby_smb/dcerpc/winreg.rb | 129 +++++++- .../dcerpc/winreg/get_key_security_request.rb | 26 ++ .../winreg/get_key_security_response.rb | 26 ++ .../dcerpc/winreg/query_value_response.rb | 2 +- .../dcerpc/winreg/set_key_security_request.rb | 26 ++ .../winreg/set_key_security_response.rb | 25 ++ lib/ruby_smb/field/security_descriptor.rb | 17 ++ spec/lib/ruby_smb/dcerpc/ndr_spec.rb | 80 +++++ spec/lib/ruby_smb/dcerpc/winreg_spec.rb | 275 +++++++++++++++++- 14 files changed, 738 insertions(+), 39 deletions(-) create mode 100644 examples/registry_key_security_descriptor.rb create mode 100644 lib/ruby_smb/dcerpc/winreg/get_key_security_request.rb create mode 100644 lib/ruby_smb/dcerpc/winreg/get_key_security_response.rb create mode 100644 lib/ruby_smb/dcerpc/winreg/set_key_security_request.rb create mode 100644 lib/ruby_smb/dcerpc/winreg/set_key_security_response.rb diff --git a/examples/registry_key_security_descriptor.rb b/examples/registry_key_security_descriptor.rb new file mode 100644 index 000000000..02867f4fc --- /dev/null +++ b/examples/registry_key_security_descriptor.rb @@ -0,0 +1,109 @@ +#!/usr/bin/ruby + +# This example script is used for testing the Winreg registry key security descriptor functionalities. +# It will attempt to connect to a host and reads (or writes) the security descriptor of a specified registry key. +# +# Example usage: +# - read: +# ruby examples/read_registry_key_security.rb --username msfadmin --password msfadmin -i 7 -o r 192.168.172.138 'HKLM\SECURITY\Policy\PolEKList' +# This will try to connect to \\192.168.172.138 with the msfadmin:msfadmin +# credentialas and read the security descriptor of the +# `HKLM\SECURITY\Policy\PolEKList` registry key with the security information 7 +# (OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | +# DACL_SECURITY_INFORMATION). +# +# - write: +# ruby examples/read_registry_key_security.rb --username msfadmin --password msfadmin -i 4 --sd 01000480000000000000000000000000140000000200340002000000000214003f000f00010100000000000512000000000218000000060001020000000000052000000020020000 -o w 192.168.172.138 'HKLM\SECURITY\Policy\PolEKList' +# This will try to connect to \\192.168.172.138 with the msfadmin:msfadmin +# credentialas and write the given security descriptor to the +# `HKLM\SECURITY\Policy\PolEKList` registry key with the security information 4 +# (DACL_SECURITY_INFORMATION). + +require 'bundler/setup' +require 'optparse' +require 'ruby_smb' + +OPERATIONS = %w{read write} +OPERATION_ALIASES = { "r" => "read", "w" => "write" } + +args = ARGV.dup +options = { + domain: '.', + username: '', + password: '', + smbv1: true, + smbv2: true, + smbv3: true, + target: nil, + key: nil, + operation: 'read', + info: RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION | RubySMB::Field::SecurityDescriptor::GROUP_SECURITY_INFORMATION | RubySMB::Field::SecurityDescriptor::DACL_SECURITY_INFORMATION, + sd: nil +} +options[:key] = args.pop +options[:target ] = args.pop +optparser = OptionParser.new do |opts| + opts.banner = "Usage: #{File.basename(__FILE__)} [options] target reg_key" + opts.on('--[no-]smbv1', "Enable or disable SMBv1 (default: #{options[:smbv1] ? 'Enabled' : 'Disabled'})") do |smbv1| + options[:smbv1] = smbv1 + end + opts.on('--[no-]smbv2', "Enable or disable SMBv2 (default: #{options[:smbv2] ? 'Enabled' : 'Disabled'})") do |smbv2| + options[:smbv2] = smbv2 + end + opts.on('--[no-]smbv3', "Enable or disable SMBv3 (default: #{options[:smbv3] ? 'Enabled' : 'Disabled'})") do |smbv3| + options[:smbv3] = smbv3 + end + opts.on('-u', '--username [USERNAME]', "The account's username (default: #{options[:username]})") do |username| + if username.include?('\\') + options[:domain], options[:username] = username.split('\\', 2) + else + options[:username] = username + end + end + opts.on('-p', '--password [PASSWORD]', "The account's password (default: #{options[:password]})") do |password| + options[:password] = password + end + operation_list = (OPERATION_ALIASES.keys + OPERATIONS).join(', ') + opts.on('-o', '--operation OPERATION', OPERATIONS, OPERATION_ALIASES, "The operation to perform on the registry key (default: #{options[:operation]})", "(#{operation_list})") do |operation| + options[:operation] = operation + end + opts.on('-i', '--info [SECURITY INFORMATION]', Integer, "The security information value (default: #{options[:info]})") do |password| + options[:info] = password + end + opts.on('-s', '--sd [SECURITY DESCRIPTOR]', "The security descriptor to write as an hex string") do |sd| + options[:sd] = sd + end +end +optparser.parse!(args) + +if options[:target].nil? || options[:key].nil? + abort(optparser.help) +end + +sock = TCPSocket.new options[:target], 445 +dispatcher = RubySMB::Dispatcher::Socket.new(sock) + +client = RubySMB::Client.new(dispatcher, smb1: options[:smbv1], smb2: options[:smbv2], smb3: options[:smbv3], username: options[:username], password: options[:password], domain: options[:domain]) +protocol = client.negotiate +status = client.authenticate + +puts "#{protocol}: #{status}" + +case options[:operation] +when 'read', 'r' + puts "Read registry key #{options[:key]} security descriptor with security information #{options[:info]}" + security_descriptor = client.get_key_security_descriptor(options[:target], options[:key], options[:info]) + puts "Security descriptor: #{security_descriptor.b.bytes.map {|c| "%02x" % c.ord}.join}" +when 'write', 'w' + unless options[:sd] && !options[:sd].empty? + puts "Security descriptor missing" + abort(optparser.help) + end + puts "Write security descriptor #{options[:sd]} to registry key #{options[:key]} with security information #{options[:info]}" + sd = options[:sd].chars.each_slice(2).map {|c| c.join.to_i(16).chr}.join + status = client.set_key_security_descriptor(options[:target], options[:key], sd, options[:info]) + puts "Success!" +end + +client.disconnect! + diff --git a/lib/ruby_smb/client/winreg.rb b/lib/ruby_smb/client/winreg.rb index 8ac108ba2..9a4d4f23b 100644 --- a/lib/ruby_smb/client/winreg.rb +++ b/lib/ruby_smb/client/winreg.rb @@ -40,6 +40,18 @@ def enum_registry_values(host, key) end end + def get_key_security_descriptor(host, key, security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION) + connect_to_winreg(host) do |named_pipe| + named_pipe.get_key_security_descriptor(key, security_information) + end + end + + def set_key_security_descriptor(host, key, security_descriptor, security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION) + connect_to_winreg(host) do |named_pipe| + named_pipe.set_key_security_descriptor(key, security_descriptor, security_information) + end + end + end end end diff --git a/lib/ruby_smb/dcerpc/ndr.rb b/lib/ruby_smb/dcerpc/ndr.rb index f9bbe17c6..9a3443d06 100644 --- a/lib/ruby_smb/dcerpc/ndr.rb +++ b/lib/ruby_smb/dcerpc/ndr.rb @@ -563,8 +563,11 @@ def do_num_bytes def get_max_count(val) if is_a?(BinData::Stringz) max_count = val.to_s.strip.length - # Only count the terminating NULL byte if the string is not empty - max_count += 1 if max_count > 0 + # Add one to count the terminator. According to + # https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_04_02, + # the NDR String must contain at least one element, the terminator. So, + # add one even if it is an empty string. + max_count += 1 return max_count else return val.to_s.length @@ -618,8 +621,11 @@ def do_num_bytes def update_actual_count(val) if is_a?(BinData::Stringz) @actual_count = val.to_s.strip.length - # Only count the terminating NULL byte if the string is not empty - @actual_count += 1 if @actual_count > 0 + # Add one to count the terminator. According to + # https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_04, + # the NDR String must contain at least one element, the terminator. So, + # add one even if it is an empty string. + @actual_count += 1 else @actual_count = val.to_s.length end diff --git a/lib/ruby_smb/dcerpc/request.rb b/lib/ruby_smb/dcerpc/request.rb index ac32a5bf8..2a1817212 100644 --- a/lib/ruby_smb/dcerpc/request.rb +++ b/lib/ruby_smb/dcerpc/request.rb @@ -18,22 +18,24 @@ class Request < BinData::Record choice :stub, label: 'Stub', selection: -> { @obj.parent.get_parameter(:endpoint) || '' } do string 'Encrypted' choice 'Winreg', selection: -> { opnum } do - open_root_key_request Winreg::OPEN_HKCR, opnum: Winreg::OPEN_HKCR - open_root_key_request Winreg::OPEN_HKCU, opnum: Winreg::OPEN_HKCU - open_root_key_request Winreg::OPEN_HKLM, opnum: Winreg::OPEN_HKLM - open_root_key_request Winreg::OPEN_HKPD, opnum: Winreg::OPEN_HKPD - open_root_key_request Winreg::OPEN_HKU, opnum: Winreg::OPEN_HKU - open_root_key_request Winreg::OPEN_HKCC, opnum: Winreg::OPEN_HKCC - open_root_key_request Winreg::OPEN_HKPT, opnum: Winreg::OPEN_HKPT - open_root_key_request Winreg::OPEN_HKPN, opnum: Winreg::OPEN_HKPN - close_key_request Winreg::REG_CLOSE_KEY - enum_key_request Winreg::REG_ENUM_KEY - enum_value_request Winreg::REG_ENUM_VALUE - open_key_request Winreg::REG_OPEN_KEY - query_info_key_request Winreg::REG_QUERY_INFO_KEY - query_value_request Winreg::REG_QUERY_VALUE - create_key_request Winreg::REG_CREATE_KEY - save_key_request Winreg::REG_SAVE_KEY + open_root_key_request Winreg::OPEN_HKCR, opnum: Winreg::OPEN_HKCR + open_root_key_request Winreg::OPEN_HKCU, opnum: Winreg::OPEN_HKCU + open_root_key_request Winreg::OPEN_HKLM, opnum: Winreg::OPEN_HKLM + open_root_key_request Winreg::OPEN_HKPD, opnum: Winreg::OPEN_HKPD + open_root_key_request Winreg::OPEN_HKU, opnum: Winreg::OPEN_HKU + open_root_key_request Winreg::OPEN_HKCC, opnum: Winreg::OPEN_HKCC + open_root_key_request Winreg::OPEN_HKPT, opnum: Winreg::OPEN_HKPT + open_root_key_request Winreg::OPEN_HKPN, opnum: Winreg::OPEN_HKPN + close_key_request Winreg::REG_CLOSE_KEY + enum_key_request Winreg::REG_ENUM_KEY + enum_value_request Winreg::REG_ENUM_VALUE + open_key_request Winreg::REG_OPEN_KEY + query_info_key_request Winreg::REG_QUERY_INFO_KEY + query_value_request Winreg::REG_QUERY_VALUE + create_key_request Winreg::REG_CREATE_KEY + save_key_request Winreg::REG_SAVE_KEY + get_key_security_request Winreg::REG_GET_KEY_SECURITY + set_key_security_request Winreg::REG_SET_KEY_SECURITY string :default end choice 'Netlogon', selection: -> { opnum } do diff --git a/lib/ruby_smb/dcerpc/rrp_rpc_unicode_string.rb b/lib/ruby_smb/dcerpc/rrp_rpc_unicode_string.rb index 9931d7091..1670ca658 100644 --- a/lib/ruby_smb/dcerpc/rrp_rpc_unicode_string.rb +++ b/lib/ruby_smb/dcerpc/rrp_rpc_unicode_string.rb @@ -20,7 +20,7 @@ def assign(val) when BinData::Stringz, BinData::String, String self.buffer = val.to_s val_length = val.strip.length - val_length += 1 unless val == '' + val_length += 1 self.buffer_length = val_length * 2 self.maximum_length = val_length * 2 else diff --git a/lib/ruby_smb/dcerpc/winreg.rb b/lib/ruby_smb/dcerpc/winreg.rb index bacb7a123..ef1b1e0cc 100644 --- a/lib/ruby_smb/dcerpc/winreg.rb +++ b/lib/ruby_smb/dcerpc/winreg.rb @@ -16,10 +16,12 @@ module Winreg REG_CREATE_KEY = 0x06 REG_ENUM_KEY = 0x09 REG_ENUM_VALUE = 0x0a + REG_GET_KEY_SECURITY = 0x0c REG_OPEN_KEY = 0x0f REG_QUERY_INFO_KEY = 0x10 REG_QUERY_VALUE = 0x11 REG_SAVE_KEY = 0x14 + REG_SET_KEY_SECURITY = 0x15 OPEN_HKCC = 0x1b OPEN_HKPT = 0x20 OPEN_HKPN = 0x21 @@ -43,6 +45,10 @@ module Winreg require 'ruby_smb/dcerpc/winreg/create_key_response' require 'ruby_smb/dcerpc/winreg/save_key_request' require 'ruby_smb/dcerpc/winreg/save_key_response' + require 'ruby_smb/dcerpc/winreg/get_key_security_request' + require 'ruby_smb/dcerpc/winreg/get_key_security_response' + require 'ruby_smb/dcerpc/winreg/set_key_security_request' + require 'ruby_smb/dcerpc/winreg/set_key_security_response' ROOT_KEY_MAP = { "HKEY_CLASSES_ROOT" => OPEN_HKCR, @@ -105,10 +111,7 @@ def open_root_key(root_key) # @raise [RubySMB::Dcerpc::Error::WinregError] if the response error status is not ERROR_SUCCESS def open_key(handle, sub_key) openkey_request_packet = RubySMB::Dcerpc::Winreg::OpenKeyRequest.new(hkey: handle, lp_sub_key: sub_key) - openkey_request_packet.sam_desired.read_control = 1 - openkey_request_packet.sam_desired.key_query_value = 1 - openkey_request_packet.sam_desired.key_enumerate_sub_keys = 1 - openkey_request_packet.sam_desired.key_notify = 1 + openkey_request_packet.sam_desired.maximum_allowed = 1 response = dcerpc_request(openkey_request_packet) begin open_key_response = RubySMB::Dcerpc::Winreg::OpenKeyResponse.read(response) @@ -124,14 +127,16 @@ def open_key(handle, sub_key) end # Retrieve the data associated with the named value of a specified - # registry open key. + # registry open key. This will also return the type if required. # # @param handle [Ndr::NdrContextHandle] the handle for the key # @param value_name [String] the name of the value + # @param value_name [Boolean] also return the data type if set to true # @return [String] the data of the value entry + # @return [Array] if `type` is true, an array containing the data type and the actual data of the value entry # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a QueryValueResponse packet # @raise [RubySMB::Dcerpc::Error::WinregError] if the response error status is not ERROR_SUCCESS - def query_value(handle, value_name) + def query_value(handle, value_name, type: false) query_value_request_packet = RubySMB::Dcerpc::Winreg::QueryValueRequest.new(hkey: handle, lp_value_name: value_name) query_value_request_packet.lp_type = 0 query_value_request_packet.lpcb_data = 0 @@ -161,7 +166,11 @@ def query_value(handle, value_name) "#{WindowsError::Win32.find_by_retval(query_value_response.error_status.value).join(',')}" end - query_value_response.data + if type + [query_value_response.lp_type, query_value_response.data] + else + query_value_response.data + end end # Close the handle to the registry key. @@ -323,6 +332,7 @@ def save_key(handle, file_name, opts = {}) # exists, false otherwise. # # @param key [String] the registry key to check + # @param bind [Boolean] Bind to the winreg endpoint if true (default) # @return [Boolean] def has_registry_key?(key, bind: true) bind(endpoint: RubySMB::Dcerpc::Winreg) if bind @@ -345,6 +355,7 @@ def has_registry_key?(key, bind: true) # # @param key [String] the registry key # @param value_name [String] the name of the value to read + # @param bind [Boolean] Bind to the winreg endpoint if true (default) # @return [String] the data of the value entry def read_registry_key_value(key, value_name, bind: true) bind(endpoint: RubySMB::Dcerpc::Winreg) if bind @@ -363,6 +374,7 @@ def read_registry_key_value(key, value_name, bind: true) # is provided, it enumerates its subkeys. # # @param key [String] the registry key + # @param bind [Boolean] Bind to the winreg endpoint if true (default) # @return [Array] the subkeys def enum_registry_key(key, bind: true) bind(endpoint: RubySMB::Dcerpc::Winreg) if bind @@ -389,6 +401,7 @@ def enum_registry_key(key, bind: true) # Enumerate the values for the specified registry key. # # @param key [String] the registry key + # @param bind [Boolean] Bind to the winreg endpoint if true (default) # @return [Array] the values def enum_registry_values(key, bind: true) bind(endpoint: RubySMB::Dcerpc::Winreg) if bind @@ -412,6 +425,108 @@ def enum_registry_values(key, bind: true) close_key(root_key_handle) if root_key_handle && root_key_handle != subkey_handle end + + # Retrieve the security descriptor for the given registry key handle. + # + # @param handle [String] the handle to the registry key + # @param security_information [] the security information to query (see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/23e75ca3-98fd-4396-84e5-86cd9d40d343). These constants are defined in the `RubySMB::Field::SecurityDescriptor` class + # @return [String] The security descriptor as a byte stream + # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a GetKeySecurityResponse packet + # @raise [RubySMB::Dcerpc::Error::WinregError] if the response error status is not ERROR_SUCCESS + def get_key_security(handle, security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION) + get_key_security_request = RubySMB::Dcerpc::Winreg::GetKeySecurityRequest.new( + hkey: handle, + security_information: security_information, + prpc_security_descriptor_in: { cb_in_security_descriptor: 4096 } + ) + response = dcerpc_request(get_key_security_request) + begin + get_key_security_response = RubySMB::Dcerpc::Winreg::GetKeySecurityResponse.read(response) + rescue IOError + raise RubySMB::Dcerpc::Error::InvalidPacket, "Error reading the GetKeySecurity response" + end + unless get_key_security_response.error_status == WindowsError::Win32::ERROR_SUCCESS + raise RubySMB::Dcerpc::Error::WinregError, "Error returned when querying information: "\ + "#{WindowsError::Win32.find_by_retval(get_key_security_response.error_status.value).join(',')}" + end + + get_key_security_response.prpc_security_descriptor_out.lp_security_descriptor.to_a.pack('C*') + end + + # Retrieve the security descriptor for the given key. + # + # @param key [String] the registry key + # @param security_information [] the security information to query (see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/23e75ca3-98fd-4396-84e5-86cd9d40d343). These constants are defined in the `RubySMB::Field::SecurityDescriptor` class + # @param bind [Boolean] Bind to the winreg endpoint if true (default) + # @return [String] The security descriptor as a byte stream + # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a GetKeySecurityResponse packet + # @raise [RubySMB::Dcerpc::Error::WinregError] if the response error status is not ERROR_SUCCESS + def get_key_security_descriptor(key, security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION, bind: true) + bind(endpoint: RubySMB::Dcerpc::Winreg) if bind + + root_key, sub_key = key.gsub(/\//, '\\').split('\\', 2) + root_key_handle = open_root_key(root_key) + subkey_handle = open_key(root_key_handle, sub_key) + get_key_security(subkey_handle, security_information) + ensure + close_key(subkey_handle) if subkey_handle + close_key(root_key_handle) if root_key_handle + end + + # Set the security descriptor for the given registry key handle. + # + # @param handle [String] the handle to the registry key + # @param security_descriptor [String] the new security descriptor to set as a byte stream + # @param security_information [] the security information to query (see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/23e75ca3-98fd-4396-84e5-86cd9d40d343). These constants are defined in the `RubySMB::Field::SecurityDescriptor` class + # @param bind [Boolean] Bind to the winreg endpoint if true (default) + # @return [Integer] The error status returned by the DCERPC call + # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a SetKeySecurityResponse packet + # @raise [RubySMB::Dcerpc::Error::WinregError] if the response error status is not ERROR_SUCCESS + def set_key_security(handle, security_descriptor, security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION) + set_key_security_request = RubySMB::Dcerpc::Winreg::SetKeySecurityRequest.new( + hkey: handle, + security_information: security_information, + prpc_security_descriptor: { + lp_security_descriptor: security_descriptor.bytes, + cb_in_security_descriptor: security_descriptor.b.size, + cb_out_security_descriptor: security_descriptor.b.size + } + ) + response = dcerpc_request(set_key_security_request) + begin + set_key_security_response = RubySMB::Dcerpc::Winreg::SetKeySecurityResponse.read(response) + rescue IOError + raise RubySMB::Dcerpc::Error::InvalidPacket, "Error reading the SetKeySecurity response" + end + unless set_key_security_response.error_status == WindowsError::Win32::ERROR_SUCCESS + raise RubySMB::Dcerpc::Error::WinregError, "Error returned when setting the registry key: "\ + "#{WindowsError::Win32.find_by_retval(set_key_security_response.error_status.value).join(',')}" + end + + set_key_security_response.error_status + end + + # Set the security descriptor for the given key. + # + # @param key [String] the registry key + # @param security_descriptor [String] the new security descriptor to set as a byte stream + # @param security_information [] the security information to query (see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/23e75ca3-98fd-4396-84e5-86cd9d40d343). These constants are defined in the `RubySMB::Field::SecurityDescriptor` class + # @param bind [Boolean] Bind to the winreg endpoint if true (default) + # @return [Integer] The error status returned by the DCERPC call + # @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a SetKeySecurityResponse packet + # @raise [RubySMB::Dcerpc::Error::WinregError] if the response error status is not ERROR_SUCCESS + def set_key_security_descriptor(key, security_descriptor, security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION, bind: true) + bind(endpoint: RubySMB::Dcerpc::Winreg) if bind + + root_key, sub_key = key.gsub(/\//, '\\').split('\\', 2) + root_key_handle = open_root_key(root_key) + subkey_handle = open_key(root_key_handle, sub_key) + set_key_security(subkey_handle, security_descriptor, security_information) + ensure + close_key(subkey_handle) if subkey_handle + close_key(root_key_handle) if root_key_handle + end + end end end diff --git a/lib/ruby_smb/dcerpc/winreg/get_key_security_request.rb b/lib/ruby_smb/dcerpc/winreg/get_key_security_request.rb new file mode 100644 index 000000000..37273a924 --- /dev/null +++ b/lib/ruby_smb/dcerpc/winreg/get_key_security_request.rb @@ -0,0 +1,26 @@ +module RubySMB + module Dcerpc + module Winreg + + # This class represents a GetKeySecurity Request Packet as defined in + # [3.1.5.13 BaseRegGetKeySecurity (Opnum 12)](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rrp/b0e1868c-f4fd-4b43-959f-c0f0cac3ee26) + class GetKeySecurityRequest < BinData::Record + attr_reader :opnum + + endian :little + + rpc_hkey :hkey + uint32 :security_information + rpc_security_descriptor :prpc_security_descriptor_in + + def initialize_instance + super + @opnum = REG_GET_KEY_SECURITY + end + end + + end + end +end + + diff --git a/lib/ruby_smb/dcerpc/winreg/get_key_security_response.rb b/lib/ruby_smb/dcerpc/winreg/get_key_security_response.rb new file mode 100644 index 000000000..ce5c4389f --- /dev/null +++ b/lib/ruby_smb/dcerpc/winreg/get_key_security_response.rb @@ -0,0 +1,26 @@ +module RubySMB + module Dcerpc + module Winreg + + # This class represents a GetKeySecurity Response Packet as defined in + # [3.1.5.13 BaseRegGetKeySecurity (Opnum 12)](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rrp/b0e1868c-f4fd-4b43-959f-c0f0cac3ee26) + class GetKeySecurityResponse < BinData::Record + attr_reader :opnum + + endian :little + + rpc_security_descriptor :prpc_security_descriptor_out + ndr_uint32 :error_status + + def initialize_instance + super + @opnum = REG_GET_KEY_SECURITY + end + end + + end + end +end + + + diff --git a/lib/ruby_smb/dcerpc/winreg/query_value_response.rb b/lib/ruby_smb/dcerpc/winreg/query_value_response.rb index cf41741b7..c079261a9 100644 --- a/lib/ruby_smb/dcerpc/winreg/query_value_response.rb +++ b/lib/ruby_smb/dcerpc/winreg/query_value_response.rb @@ -27,7 +27,7 @@ def data case lp_type when 1,2 bytes.force_encoding('utf-16le').strip - when 3 + when 0,3 # 0 is undefined type, let's consider an array of bytes bytes when 4 bytes.unpack('V').first diff --git a/lib/ruby_smb/dcerpc/winreg/set_key_security_request.rb b/lib/ruby_smb/dcerpc/winreg/set_key_security_request.rb new file mode 100644 index 000000000..21540c3e4 --- /dev/null +++ b/lib/ruby_smb/dcerpc/winreg/set_key_security_request.rb @@ -0,0 +1,26 @@ +module RubySMB + module Dcerpc + module Winreg + + # This class represents a SetKeySecurity Request Packet as defined in + # [3.1.5.21 BaseRegSetKeySecurity (Opnum 21)](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rrp/da18856c-8a6d-4217-8e93-3625865e562c) + class SetKeySecurityRequest < BinData::Record + attr_reader :opnum + + endian :little + + rpc_hkey :hkey + uint32 :security_information + rpc_security_descriptor :prpc_security_descriptor + + def initialize_instance + super + @opnum = REG_SET_KEY_SECURITY + end + end + + end + end +end + + diff --git a/lib/ruby_smb/dcerpc/winreg/set_key_security_response.rb b/lib/ruby_smb/dcerpc/winreg/set_key_security_response.rb new file mode 100644 index 000000000..3116d8a33 --- /dev/null +++ b/lib/ruby_smb/dcerpc/winreg/set_key_security_response.rb @@ -0,0 +1,25 @@ +module RubySMB + module Dcerpc + module Winreg + + # This class represents a SetKeySecurity Response Packet as defined in + # [3.1.5.21 BaseRegSetKeySecurity (Opnum 21)](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rrp/da18856c-8a6d-4217-8e93-3625865e562c) + class SetKeySecurityResponse < BinData::Record + attr_reader :opnum + + endian :little + + ndr_uint32 :error_status + + def initialize_instance + super + @opnum = REG_SET_KEY_SECURITY + end + end + + end + end +end + + + diff --git a/lib/ruby_smb/field/security_descriptor.rb b/lib/ruby_smb/field/security_descriptor.rb index c4cb96f31..cd7f8eac1 100644 --- a/lib/ruby_smb/field/security_descriptor.rb +++ b/lib/ruby_smb/field/security_descriptor.rb @@ -3,6 +3,23 @@ module Field # Class representing a SECURITY_DESCRIPTOR as defined in # [2.4.6 SECURITY_DESCRIPTOR](https://msdn.microsoft.com/en-us/library/cc230366.aspx) class SecurityDescriptor < BinData::Record + + # Security Information as defined in + # [2.4.7 SECURITY_INFORMATION](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/23e75ca3-98fd-4396-84e5-86cd9d40d343) + OWNER_SECURITY_INFORMATION = 0x00000001 + GROUP_SECURITY_INFORMATION = 0x00000002 + DACL_SECURITY_INFORMATION = 0x00000004 + SACL_SECURITY_INFORMATION = 0x00000008 + LABEL_SECURITY_INFORMATION = 0x00000010 + UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000 + UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000 + PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000 + PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000 + ATTRIBUTE_SECURITY_INFORMATION = 0x00000020 + SCOPE_SECURITY_INFORMATION = 0x00000040 + PROCESS_TRUST_LABEL_SECURITY_INFORMATION = 0x00000080 + BACKUP_SECURITY_INFORMATION = 0x00010000 + endian :little uint8 :revision, label: 'Revision', initial_value: 0x01 uint8 :sbz1, label: 'Resource Manager Control Bits' diff --git a/spec/lib/ruby_smb/dcerpc/ndr_spec.rb b/spec/lib/ruby_smb/dcerpc/ndr_spec.rb index a7bca55a4..9e9d9ac00 100644 --- a/spec/lib/ruby_smb/dcerpc/ndr_spec.rb +++ b/spec/lib/ruby_smb/dcerpc/ndr_spec.rb @@ -1352,6 +1352,15 @@ } let(:value) { 'ABCD' } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: false, char_size: 1, null_terminated: false do + let(:binary_stream) { + "\x00\x00\x00\x00"\ + "\x00\x00\x00\x00".b + } + let(:value) { '' } + end + end end RSpec.describe RubySMB::Dcerpc::Ndr::NdrVarStringz do @@ -1368,6 +1377,16 @@ } let(:value) { 'ABCD' } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: false, char_size: 1, null_terminated: true do + let(:binary_stream) { + "\x00\x00\x00\x00"\ + "\x01\x00\x00\x00"\ + "\x00".b + } + let(:value) { '' } + end + end end RSpec.describe RubySMB::Dcerpc::Ndr::NdrVarWideString do @@ -1383,6 +1402,15 @@ } let(:value) { 'ABCD'.encode('utf-16le') } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: false, char_size: 2, null_terminated: false do + let(:binary_stream) { + "\x00\x00\x00\x00"\ + "\x00\x00\x00\x00".b + } + let(:value) { '' } + end + end end RSpec.describe RubySMB::Dcerpc::Ndr::NdrVarWideStringz do @@ -1398,6 +1426,16 @@ } let(:value) { 'ABCD'.encode('utf-16le') } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: false, char_size: 2, null_terminated: true do + let(:binary_stream) { + "\x00\x00\x00\x00"\ + "\x01\x00\x00\x00"\ + "\x00\x00".b + } + let(:value) { '' } + end + end end RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarString do @@ -1415,6 +1453,16 @@ } let(:value) { 'ABCD' } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: false do + let(:binary_stream) { + "\x00\x00\x00\x00"\ + "\x00\x00\x00\x00"\ + "\x00\x00\x00\x00".b + } + let(:value) { '' } + end + end end RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarStringz do @@ -1432,6 +1480,17 @@ } let(:value) { 'ABCD' } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: true do + let(:binary_stream) { + "\x01\x00\x00\x00"\ + "\x00\x00\x00\x00"\ + "\x01\x00\x00\x00"\ + "\x00".b + } + let(:value) { '' } + end + end end RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarWideString do @@ -1448,6 +1507,16 @@ } let(:value) { 'ABCD'.encode('utf-16le') } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: true, char_size: 2, null_terminated: false do + let(:binary_stream) { + "\x00\x00\x00\x00"\ + "\x00\x00\x00\x00"\ + "\x00\x00\x00\x00".b + } + let(:value) { '' } + end + end end RSpec.describe RubySMB::Dcerpc::Ndr::NdrConfVarWideStringz do @@ -1464,6 +1533,17 @@ } let(:value) { 'ABCD'.encode('utf-16le') } end + context 'with an empty string' do + it_behaves_like 'a NDR String', conformant: true, char_size: 1, null_terminated: true do + let(:binary_stream) { + "\x01\x00\x00\x00"\ + "\x00\x00\x00\x00"\ + "\x01\x00\x00\x00"\ + "\x00\x00".b + } + let(:value) { '' } + end + end end diff --git a/spec/lib/ruby_smb/dcerpc/winreg_spec.rb b/spec/lib/ruby_smb/dcerpc/winreg_spec.rb index 61ed9100c..cd8f42374 100644 --- a/spec/lib/ruby_smb/dcerpc/winreg_spec.rb +++ b/spec/lib/ruby_smb/dcerpc/winreg_spec.rb @@ -73,12 +73,7 @@ before :example do allow(described_class::OpenKeyRequest).to receive(:new).and_return(openkey_request_packet) allow(openkey_request_packet).to receive(:sam_desired).and_return(regsam) - allow(regsam).to receive_messages( - :read_control= => nil, - :key_query_value= => nil, - :key_enumerate_sub_keys= => nil, - :key_notify= => nil, - ) + allow(regsam).to receive(:maximum_allowed=) allow(winreg).to receive(:dcerpc_request).and_return(response) allow(described_class::OpenKeyResponse).to receive(:read).and_return(open_key_response) allow(open_key_response).to receive_messages( @@ -94,10 +89,7 @@ it 'sets the expected user rights on the request packet' do winreg.open_key(handle, sub_key) - expect(regsam).to have_received(:read_control=).with(1) - expect(regsam).to have_received(:key_query_value=).with(1) - expect(regsam).to have_received(:key_enumerate_sub_keys=).with(1) - expect(regsam).to have_received(:key_notify=).with(1) + expect(regsam).to have_received(:maximum_allowed=).with(1) end it 'sends the expected dcerpc request' do @@ -236,6 +228,16 @@ it 'returns the expected response data' do expect(winreg.query_value(handle, value_name)).to eq(data) end + + context 'when the data type is also required' do + let(:lp_type) { double('Type') } + before :example do + allow(query_value_response2).to receive(:lp_type).and_return(lp_type) + end + it 'returns the expected response type and data' do + expect(winreg.query_value(handle, value_name, type: true)).to eq([lp_type, data]) + end + end end describe '#close_key' do @@ -864,4 +866,257 @@ end end end + + describe '#get_key_security_descriptor' do + let(:root_key) { 'HKLM' } + let(:sub_key) { 'my\\sub\\key\\path' } + let(:key) { "#{root_key}\\#{sub_key}" } + let(:root_key_handle) { double('Root Key Handle') } + let(:subkey_handle) { double('Subkey Handle') } + before :example do + allow(winreg).to receive_messages( + :bind => nil, + :open_root_key => root_key_handle, + :open_key => subkey_handle, + :get_key_security => nil, + :close_key => nil + ) + end + + it 'binds a DCERPC connection to the expected remote endpoint' do + winreg.get_key_security_descriptor(key) + expect(winreg).to have_received(:bind).with(endpoint: RubySMB::Dcerpc::Winreg) + end + + it 'does not bind a DCERPC connection if #bind argument is false' do + winreg.get_key_security_descriptor(key, bind: false) + expect(winreg).to_not have_received(:bind) + end + + it 'opens the expected root key' do + winreg.get_key_security_descriptor(key) + expect(winreg).to have_received(:open_root_key).with(root_key) + end + + it 'opens the expected registry key' do + winreg.get_key_security_descriptor(key) + expect(winreg).to have_received(:open_key).with(root_key_handle, sub_key) + end + + it 'calls #get_key_security with the expected arguments' do + winreg.get_key_security_descriptor(key) + security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION + expect(winreg).to have_received(:get_key_security).with(subkey_handle, security_information) + end + + context 'with a non-default security informaiton' do + it 'calls #get_key_security with the expected arguments' do + security_information = RubySMB::Field::SecurityDescriptor::GROUP_SECURITY_INFORMATION + winreg.get_key_security_descriptor(key, security_information) + expect(winreg).to have_received(:get_key_security).with(subkey_handle, security_information) + end + end + end + + describe '#get_key_security' do + let(:handle) { double('Handle') } + let(:get_key_security_request) { double('GetKeySecurity Request') } + let(:response) { + '0000020000100000940000000010000000000000940000000100048078000000880000'\ + '0000000000140000000200640004000000000214003f000f0001010000000000051200'\ + '0000000218000000060001020000000000052000000020020000000218000900060001'\ + '0200000000000520000000200200000002180009000600010200000000000520000000'\ + '2002000001020000000000052000000020020000010100000000000512000000000000'\ + '00'.unhexlify + } + let(:security_descriptor) { + '01000480780000008800000000000000140000000200640004000000000214003f000f'\ + '0001010000000000051200000000021800000006000102000000000005200000002002'\ + '0000000218000900060001020000000000052000000020020000000218000900060001'\ + '0200000000000520000000200200000102000000000005200000002002000001010000'\ + '0000000512000000'.unhexlify + } + before :example do + allow(described_class::GetKeySecurityRequest).to receive(:new).and_return(get_key_security_request) + allow(winreg).to receive(:dcerpc_request).and_return(response) + end + + it 'create the expected GetKeySecurityRequest packet with the default options' do + opts = { + hkey: handle, + security_information: RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION, + prpc_security_descriptor_in: { cb_in_security_descriptor: 4096 } + } + winreg.get_key_security(handle) + expect(described_class::GetKeySecurityRequest).to have_received(:new).with(opts) + end + + it 'create the expected SaveKeyRequest packet with custom options' do + security_information = RubySMB::Field::SecurityDescriptor::GROUP_SECURITY_INFORMATION + opts = { + hkey: handle, + security_information: security_information, + prpc_security_descriptor_in: { cb_in_security_descriptor: 4096 } + } + winreg.get_key_security(handle, security_information) + expect(described_class::GetKeySecurityRequest).to have_received(:new).with(opts) + end + + it 'sends the expected dcerpc request' do + winreg.get_key_security(handle) + expect(winreg).to have_received(:dcerpc_request).with(get_key_security_request) + end + + it 'creates a GetKeySecurityResponse structure from the expected dcerpc response' do + expect(described_class::GetKeySecurityResponse).to receive(:read).with(response).and_call_original + winreg.get_key_security(handle) + end + + context 'when an IOError occurs while parsing the response' do + it 'raises a RubySMB::Dcerpc::Error::InvalidPacket' do + allow(described_class::GetKeySecurityResponse).to receive(:read).and_raise(IOError) + expect { winreg.get_key_security(handle) }.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 + response[-4..-1] = [WindowsError::Win32::ERROR_INVALID_DATA.value].pack('V') + expect { winreg.get_key_security(handle) }.to raise_error(RubySMB::Dcerpc::Error::WinregError) + end + end + + it 'returns the expected security descriptor' do + expect(winreg.get_key_security(handle)).to eq(security_descriptor) + end + end + + describe '#set_key_security_descriptor' do + let(:root_key) { 'HKLM' } + let(:sub_key) { 'my\\sub\\key\\path' } + let(:key) { "#{root_key}\\#{sub_key}" } + let(:security_descriptor) { 'Security Descriptor' } + let(:root_key_handle) { double('Root Key Handle') } + let(:subkey_handle) { double('Subkey Handle') } + before :example do + allow(winreg).to receive_messages( + :bind => nil, + :open_root_key => root_key_handle, + :open_key => subkey_handle, + :set_key_security => nil, + :close_key => nil + ) + end + + it 'binds a DCERPC connection to the expected remote endpoint' do + winreg.set_key_security_descriptor(key, security_descriptor) + expect(winreg).to have_received(:bind).with(endpoint: RubySMB::Dcerpc::Winreg) + end + + it 'does not bind a DCERPC connection if #bind argument is false' do + winreg.set_key_security_descriptor(key, security_descriptor, bind: false) + expect(winreg).to_not have_received(:bind) + end + + it 'opens the expected root key' do + winreg.set_key_security_descriptor(key, security_descriptor) + expect(winreg).to have_received(:open_root_key).with(root_key) + end + + it 'opens the expected registry key' do + winreg.set_key_security_descriptor(key, security_descriptor) + expect(winreg).to have_received(:open_key).with(root_key_handle, sub_key) + end + + it 'calls #set_key_security with the expected arguments' do + winreg.set_key_security_descriptor(key, security_descriptor) + security_information = RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION + expect(winreg).to have_received(:set_key_security).with(subkey_handle, security_descriptor, security_information) + end + + context 'with a non-default security informaiton' do + it 'calls #get_key_security with the expected arguments' do + security_information = RubySMB::Field::SecurityDescriptor::GROUP_SECURITY_INFORMATION + winreg.set_key_security_descriptor(key, security_descriptor, security_information) + expect(winreg).to have_received(:set_key_security).with(subkey_handle, security_descriptor, security_information) + end + end + end + + describe '#set_key_security' do + let(:handle) { double('Handle') } + let(:set_key_security_request) { double('GetKeySecurity Request') } + let(:response) { '00000000'.unhexlify } + let(:security_descriptor) { + '0100048014000000240000000000000030000000010200000000000520000000200200'\ + '0001010000000000051200000002007c0005000000000214003f000f00010100000000'\ + '0005120000000002180000000600010200000000000520000000200200000002180009'\ + '0006000102000000000005200000002002000000021800090006000102000000000005'\ + '2000000020020000000218000900060001020000000000052000000020020000'.unhexlify + } + before :example do + allow(described_class::SetKeySecurityRequest).to receive(:new).and_return(set_key_security_request) + allow(winreg).to receive(:dcerpc_request).and_return(response) + end + + it 'create the expected SetKeySecurityRequest packet with the default options' do + opts = { + hkey: handle, + security_information: RubySMB::Field::SecurityDescriptor::OWNER_SECURITY_INFORMATION, + prpc_security_descriptor: { + lp_security_descriptor: security_descriptor.bytes, + cb_in_security_descriptor: security_descriptor.size, + cb_out_security_descriptor: security_descriptor.size + } + } + winreg.set_key_security(handle, security_descriptor) + expect(described_class::SetKeySecurityRequest).to have_received(:new).with(opts) + end + + it 'create the expected SaveKeyRequest packet with custom options' do + security_information = RubySMB::Field::SecurityDescriptor::GROUP_SECURITY_INFORMATION + opts = { + hkey: handle, + security_information: security_information, + prpc_security_descriptor: { + lp_security_descriptor: security_descriptor.bytes, + cb_in_security_descriptor: security_descriptor.size, + cb_out_security_descriptor: security_descriptor.size + } + } + winreg.set_key_security(handle, security_descriptor, security_information) + expect(described_class::SetKeySecurityRequest).to have_received(:new).with(opts) + end + + it 'sends the expected dcerpc request' do + winreg.set_key_security(handle, security_descriptor) + expect(winreg).to have_received(:dcerpc_request).with(set_key_security_request) + end + + it 'creates a SetKeySecurityResponse structure from the expected dcerpc response' do + expect(described_class::SetKeySecurityResponse).to receive(:read).with(response).and_call_original + winreg.set_key_security(handle, security_descriptor) + end + + context 'when an IOError occurs while parsing the response' do + it 'raises a RubySMB::Dcerpc::Error::InvalidPacket' do + allow(described_class::SetKeySecurityResponse).to receive(:read).and_raise(IOError) + expect { winreg.set_key_security(handle, security_descriptor) }.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 + response[-4..-1] = [WindowsError::Win32::ERROR_INVALID_DATA.value].pack('V') + expect { winreg.set_key_security(handle, security_descriptor) }.to raise_error(RubySMB::Dcerpc::Error::WinregError) + end + end + + it 'returns the expected error status' do + expect(winreg.set_key_security(handle, security_descriptor)).to eq(WindowsError::Win32::ERROR_SUCCESS) + end + end + + + end