diff --git a/lib/ruby_smb.rb b/lib/ruby_smb.rb index 2a18e8147..173f5bda4 100644 --- a/lib/ruby_smb.rb +++ b/lib/ruby_smb.rb @@ -6,13 +6,11 @@ require 'openssl/cmac' require 'windows_error' require 'windows_error/nt_status' -require 'ruby_smb/ntlm/custom/string_encoder' # A packet parsing and manipulation library for the SMB1 and SMB2 protocols # # [[MS-SMB] Server Message Block (SMB) Protocol Version 1](https://msdn.microsoft.com/en-us/library/cc246482.aspx) # [[MS-SMB2] Server Message Block (SMB) Protocol Versions 2 and 3](https://msdn.microsoft.com/en-us/library/cc246482.aspx) module RubySMB - require 'ruby_smb/utils' require 'ruby_smb/error' require 'ruby_smb/create_actions' require 'ruby_smb/dispositions' diff --git a/lib/ruby_smb/client.rb b/lib/ruby_smb/client.rb index 51d3d0934..473293091 100644 --- a/lib/ruby_smb/client.rb +++ b/lib/ruby_smb/client.rb @@ -4,7 +4,6 @@ module RubySMB class Client require 'ruby_smb/ntlm' require 'ruby_smb/signing' - require 'ruby_smb/utils' require 'ruby_smb/client/negotiation' require 'ruby_smb/client/authentication' require 'ruby_smb/client/tree_connect' @@ -320,11 +319,12 @@ def initialize(dispatcher, smb1: true, smb2: true, smb3: true, username:, passwo if smb1 == false && smb2 == false && smb3 == false raise ArgumentError, 'You must enable at least one Protocol' end + @dispatcher = dispatcher @pid = rand(0xFFFF) @domain = domain @local_workstation = local_workstation - @password = RubySMB::Utils.safe_encode((password||''), 'utf-8') + @password = (password || '') @sequence_counter = 0 @session_id = 0x00 @session_key = '' @@ -334,7 +334,7 @@ def initialize(dispatcher, smb1: true, smb2: true, smb3: true, username:, passwo @smb1 = smb1 @smb2 = smb2 @smb3 = smb3 - @username = RubySMB::Utils.safe_encode((username||''), 'utf-8') + @username = (username || '') @max_buffer_size = MAX_BUFFER_SIZE # These sizes will be modified during negotiation @server_max_buffer_size = SERVER_MAX_BUFFER_SIZE @@ -417,8 +417,8 @@ def session_setup(user, pass, domain, do_recv=true, local_workstation: self.local_workstation, ntlm_flags: NTLM::DEFAULT_CLIENT_FLAGS) @domain = domain @local_workstation = local_workstation - @password = RubySMB::Utils.safe_encode((pass||''), 'utf-8') - @username = RubySMB::Utils.safe_encode((user||''), 'utf-8') + @password = (pass || '') + @username = (user || '') @ntlm_client = RubySMB::NTLM::Client.new( @username, diff --git a/lib/ruby_smb/dcerpc/client.rb b/lib/ruby_smb/dcerpc/client.rb index 34477796a..526832318 100644 --- a/lib/ruby_smb/dcerpc/client.rb +++ b/lib/ruby_smb/dcerpc/client.rb @@ -9,12 +9,10 @@ class Client require 'ruby_smb/dcerpc' require 'ruby_smb/gss' require 'ruby_smb/peer_info' - require 'ruby_smb/utils' include Dcerpc include Epm include PeerInfo - include Utils # The default maximum size of a RPC message that the Client accepts (in bytes) MAX_BUFFER_SIZE = 64512 @@ -120,8 +118,8 @@ def initialize(host, @read_timeout = read_timeout @domain = domain @local_workstation = local_workstation - @username = RubySMB::Utils.safe_encode(username, 'utf-8') - @password = RubySMB::Utils.safe_encode(password, 'utf-8') + @username = username + @password = password @max_buffer_size = MAX_BUFFER_SIZE @call_id = 1 @ctx_id = 0 diff --git a/lib/ruby_smb/dcerpc/icpr.rb b/lib/ruby_smb/dcerpc/icpr.rb index 6a843f169..aa08245d2 100644 --- a/lib/ruby_smb/dcerpc/icpr.rb +++ b/lib/ruby_smb/dcerpc/icpr.rb @@ -35,7 +35,7 @@ def buffer def cert_server_request(attributes:, authority:, csr:) cert_server_request_request = CertServerRequestRequest.new( pwsz_authority: authority, - pctb_attribs: { pb: (RubySMB::Utils.safe_encode(attributes.map { |k,v| "#{k}:#{v}" }.join("\n"), 'UTF-16le').force_encoding('ASCII-8bit') + "\x00\x00".b) }, + pctb_attribs: { pb: (attributes.map { |k,v| "#{k}:#{v}" }.join("\n").encode('UTF-16LE').force_encoding('ASCII-8BIT') + "\x00\x00".b) }, pctb_request: { pb: csr.to_der } ) @@ -53,7 +53,7 @@ def cert_server_request(attributes:, authority:, csr:) ret = { certificate: nil, disposition: cert_server_request_response.pdw_disposition.value, - disposition_message: cert_server_request_response.pctb_disposition_message.buffer.chomp("\x00\x00").force_encoding('utf-16le').encode, + disposition_message: cert_server_request_response.pctb_disposition_message.buffer.chomp("\x00\x00").force_encoding('UTF-16LE').encode, status: { CR_DISP_ISSUED => :issued, CR_DISP_UNDER_SUBMISSION => :submitted, diff --git a/lib/ruby_smb/dcerpc/samr.rb b/lib/ruby_smb/dcerpc/samr.rb index b7e899a34..7c1a7d4fc 100644 --- a/lib/ruby_smb/dcerpc/samr.rb +++ b/lib/ruby_smb/dcerpc/samr.rb @@ -337,7 +337,7 @@ class SamprEncryptedUserPasswordNew < BinData::Record def self.encrypt_password(password, key) # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/5fe3c4c4-e71b-440d-b2fd-8448bfaf6e04 - password = RubySMB::Utils.safe_encode(password, 'UTF-16LE').force_encoding('ASCII-8bit') + password = password.encode('UTF-16LE').force_encoding('ASCII-8BIT') buffer = password.rjust(512, "\x00") + [ password.length ].pack('V') salt = SecureRandom.random_bytes(16) key = OpenSSL::Digest::MD5.new(salt + key).digest diff --git a/lib/ruby_smb/gss/provider/ntlm.rb b/lib/ruby_smb/gss/provider/ntlm.rb index b3ace35e1..5f774f253 100644 --- a/lib/ruby_smb/gss/provider/ntlm.rb +++ b/lib/ruby_smb/gss/provider/ntlm.rb @@ -142,10 +142,7 @@ def process_ntlm_type3(type3_msg) case type3_msg.ntlm_version when :ntlmv1 my_ntlm_response = Net::NTLM::ntlm_response( - ntlm_hash: Net::NTLM::ntlm_hash( - RubySMB::Utils.safe_encode(account.password, 'UTF-16LE'), - unicode: true - ), + ntlm_hash: Net::NTLM::ntlm_hash(account.password), challenge: @server_challenge ) matches = my_ntlm_response == type3_msg.ntlm_response @@ -157,7 +154,7 @@ def process_ntlm_type3(type3_msg) ntlmv2_hash = Net::NTLM.ntlmv2_hash( Net::NTLM::EncodeUtil.encode_utf16le(account.username), Net::NTLM::EncodeUtil.encode_utf16le(account.password), - type3_msg.domain.force_encoding('ASCII-8BIT'), # don't use the account domain because of the special '.' value + type3_msg.domain.dup.force_encoding('ASCII-8BIT'), # don't use the account domain because of the special '.' value {client_challenge: their_blob[16...24], unicode: true} ) @@ -310,8 +307,7 @@ def get_account(username, domain: nil) domain = @default_domain if domain.nil? || domain == '.'.encode(domain.encoding) domain = domain.downcase @accounts.find do |account| - RubySMB::Utils.safe_encode(account.username, username.encoding).downcase == username && - RubySMB::Utils.safe_encode(account.domain, domain.encoding).downcase == domain + account.username.encode(username.encoding).downcase == username && account.domain.encode(domain.encoding).downcase == domain end end diff --git a/lib/ruby_smb/ntlm.rb b/lib/ruby_smb/ntlm.rb index c642de5aa..2f60dece1 100644 --- a/lib/ruby_smb/ntlm.rb +++ b/lib/ruby_smb/ntlm.rb @@ -1,5 +1,3 @@ -require 'ruby_smb/ntlm/custom/string_encoder' - module RubySMB module NTLM # [[MS-NLMP] 2.2.2.5](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/99d90ff4-957f-4c8a-80e4-5bfe5a9a9832) @@ -58,41 +56,6 @@ def to_s "Version #{major}.#{minor} (Build #{build}); NTLM Current Revision #{ntlm_revision}" end end - - class << self - - # Generate a NTLMv2 Hash - # @param [String] user The username - # @param [String] password The password - # @param [String] target The domain or workstation to authenticate to - # @option opt :unicode (false) Unicode encode the domain - def ntlmv2_hash(user, password, target, opt={}) - if Net::NTLM.is_ntlm_hash? password - decoded_password = Net::NTLM::EncodeUtil.decode_utf16le(password) - ntlmhash = [decoded_password.upcase[33,65]].pack('H32') - else - ntlmhash = Net::NTLM.ntlm_hash(password, opt) - end - - if opt[:unicode] - # Uppercase operation on username containing non-ASCII characters - # after being unicode encoded with `EncodeUtil.encode_utf16le` - # doesn't play well. Upcase should be done before encoding. - user_upcase = Net::NTLM::EncodeUtil.decode_utf16le(user).upcase - user_upcase = Net::NTLM::EncodeUtil.encode_utf16le(user_upcase) - else - user_upcase = user.upcase - end - userdomain = user_upcase + target - - unless opt[:unicode] - userdomain = Net::NTLM::EncodeUtil.encode_utf16le(userdomain) - end - OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain) - end - - end - end end diff --git a/lib/ruby_smb/ntlm/client.rb b/lib/ruby_smb/ntlm/client.rb index 9210459a9..6e9865fe2 100644 --- a/lib/ruby_smb/ntlm/client.rb +++ b/lib/ruby_smb/ntlm/client.rb @@ -1,78 +1,7 @@ module RubySMB::NTLM - module Message - def deflag - security_buffers.inject(head_size) do |cur, a| - a[1].offset = cur - cur += a[1].data_size - has_flag?(:UNICODE) ? cur + cur % 2 : cur - end - end - - def serialize - deflag - @alist.map { |n, f| f.serialize }.join + security_buffers.map { |n, f| f.value + (has_flag?(:UNICODE) ? "\x00".b * (f.value.length % 2) : '') }.join - end - end - class Client < Net::NTLM::Client - class Session < Net::NTLM::Client::Session - def authenticate! - calculate_user_session_key! - type3_opts = { - :lm_response => is_anonymous? ? "\x00".b : lmv2_resp, - :ntlm_response => is_anonymous? ? '' : ntlmv2_resp, - :domain => domain, - :user => username, - :workstation => workstation, - :flag => (challenge_message.flag & client.flags) - } - t3 = Net::NTLM::Message::Type3.create type3_opts - t3.extend(Message) - if negotiate_key_exchange? - t3.enable(:session_key) - rc4 = OpenSSL::Cipher.new("rc4") - rc4.encrypt - rc4.key = user_session_key - sk = rc4.update exported_session_key - sk << rc4.final - t3.session_key = sk - end - t3 - end - - def is_anonymous? - username == '' && password == '' - end - - private - - def use_oem_strings? - # @see https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/99d90ff4-957f-4c8a-80e4-5bfe5a9a9832 - !challenge_message.has_flag?(:UNICODE) && challenge_message.has_flag?(:OEM) - end - - def ntlmv2_hash - @ntlmv2_hash ||= RubySMB::NTLM.ntlmv2_hash(username, password, domain, {:client_challenge => client_challenge, :unicode => !use_oem_strings?}) - end - - def calculate_user_session_key! - if is_anonymous? - # see MS-NLMP section 3.4 - @user_session_key = "\x00".b * 16 - else - @user_session_key = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_hash, nt_proof_str) - end - end - end - - def init_context(resp = nil, channel_binding = nil) - if resp.nil? - @session = nil - type1_message - else - @session = Client::Session.new(self, Net::NTLM::Message.decode64(resp), channel_binding) - @session.authenticate! - end - end + # There was a bunch of code in here that was necessary in versions up to and including rubyntlm version 0.6.3. + # The class is kept because there are references to it that should be kept in place in case future alterations to + # rubyntlm are required. end end diff --git a/lib/ruby_smb/ntlm/custom/string_encoder.rb b/lib/ruby_smb/ntlm/custom/string_encoder.rb deleted file mode 100644 index 45e1ddce5..000000000 --- a/lib/ruby_smb/ntlm/custom/string_encoder.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'net/ntlm' - -module RubySMB - module NTLM - module Custom - module StringEncoder - - def self.prepended(base) - base.singleton_class.send(:prepend, ClassMethods) - end - - module ClassMethods - def encode_utf16le(str) - str.dup.force_encoding('UTF-8').encode(Encoding::UTF_16LE, Encoding::UTF_8).force_encoding('ASCII-8BIT') - end - end - end - end - end -end - -Net::NTLM::EncodeUtil.send(:prepend, RubySMB::NTLM::Custom::StringEncoder) diff --git a/lib/ruby_smb/server/server_client/tree_connect.rb b/lib/ruby_smb/server/server_client/tree_connect.rb index 193cc9014..b766df679 100644 --- a/lib/ruby_smb/server/server_client/tree_connect.rb +++ b/lib/ruby_smb/server/server_client/tree_connect.rb @@ -7,7 +7,7 @@ def do_tree_connect_smb1(request, session) # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/b062f3e3-1b65-4a9a-854a-0ee432499d8f response = RubySMB::SMB1::Packet::TreeConnectResponse.new - share_name = RubySMB::Utils.safe_encode(request.data_block.path, 'UTF-8').split('\\', 4).last + share_name = request.data_block.path.split('\\', 4).last share_provider = @server.shares.transform_keys(&:downcase)[share_name.downcase] if share_provider.nil? logger.warn("Received TREE_CONNECT request for non-existent share: #{share_name}") @@ -51,7 +51,7 @@ def do_tree_connect_smb2(request, session) return response end - share_name = RubySMB::Utils.safe_encode(request.path, 'UTF-8').split('\\', 4).last + share_name = request.path.encode.split('\\', 4).last share_provider = @server.shares.transform_keys(&:downcase)[share_name.downcase] if share_provider.nil? diff --git a/lib/ruby_smb/utils.rb b/lib/ruby_smb/utils.rb deleted file mode 100644 index cffc900e9..000000000 --- a/lib/ruby_smb/utils.rb +++ /dev/null @@ -1,15 +0,0 @@ -module RubySMB - module Utils - - def self.safe_encode(str, encoding) - str.encode(encoding) - rescue EncodingError - if str.encoding == ::Encoding::ASCII_8BIT - str.dup.force_encoding(encoding) - else - raise - end - end - - end -end diff --git a/ruby_smb.gemspec b/ruby_smb.gemspec index ea91464eb..3a990f075 100644 --- a/ruby_smb.gemspec +++ b/ruby_smb.gemspec @@ -6,7 +6,14 @@ require 'ruby_smb/version' Gem::Specification.new do |spec| spec.name = 'ruby_smb' spec.version = RubySMB::VERSION - spec.authors = ['Metasploit Hackers', 'David Maloney', 'James Lee', 'Dev Mohanty', 'Christophe De La Fuente'] + spec.authors = [ + 'Metasploit Hackers', + 'David Maloney', + 'James Lee', + 'Dev Mohanty', + 'Christophe De La Fuente', + 'Spencer McIntyre' + ] spec.email = ['msfdev@metasploit.com'] spec.summary = 'A pure Ruby implementation of the SMB Protocol Family' spec.description = '' @@ -33,7 +40,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rake' spec.add_development_dependency 'yard' - spec.add_runtime_dependency 'rubyntlm' + spec.add_runtime_dependency 'rubyntlm', '>= 0.6.5' spec.add_runtime_dependency 'windows_error', '>= 0.1.4' spec.add_runtime_dependency 'bindata', '2.4.15' spec.add_runtime_dependency 'openssl-ccm' diff --git a/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb b/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb index 90275a860..8fafc3339 100644 --- a/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb +++ b/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb @@ -9,8 +9,11 @@ msg.domain = domain end end + let(:type2_msg) do + Net::NTLM::Message::Type2.new + end let(:type3_msg) do - Net::NTLM::Message::Type2.new.response(user: username, password: '', domain: domain) + type2_msg.response({user: username, password: password, domain: domain}, {ntlmv2: true}) end before(:each) do @@ -65,9 +68,121 @@ end describe '#process_ntlm_type3' do - it 'should process a NTLM type 3 message and return an error code' do - expect(authenticator.process_ntlm_type3(type3_msg)).to be_a WindowsError::ErrorCode - expect(authenticator.process_ntlm_type3(type3_msg)).to eq WindowsError::NTStatus::STATUS_LOGON_FAILURE + context 'when the message is anonymous' do + let(:type3_msg) do + type2_msg.response({user: '', password: ''}, {ntlmv2: true}) + end + + context 'when anonymous access is disabled' do + before(:each) do + expect(provider).to_not receive(:allow_guests) + expect(provider).to receive(:allow_anonymous).and_return(false) + end + + it 'should process a NTLM type 3 message and return STATUS_LOGON_FAILURE' do + status = authenticator.process_ntlm_type3(type3_msg) + expect(status).to be_a WindowsError::ErrorCode + expect(status).to eq WindowsError::NTStatus::STATUS_LOGON_FAILURE + end + + after(:each) do + expect(authenticator.session_key).to be_nil + end + end + + context 'when anonymous access is enabled' do + before(:each) do + expect(provider).to_not receive(:allow_guests) + expect(provider).to receive(:allow_anonymous).and_return(true) + end + + it 'should process a NTLM type 3 message and return STATUS_SUCCESS' do + status = authenticator.process_ntlm_type3(type3_msg) + expect(status).to be_a WindowsError::ErrorCode + expect(status).to eq WindowsError::NTStatus::STATUS_SUCCESS + end + + after(:each) do + expect(authenticator.session_key).to eq "\x00".b * 16 + end + end + end + + context 'when the message is a guest' do + let(:type3_msg) do + type2_msg.response({user: 'Spencer', password: password}, {ntlmv2: true}) + end + + context 'when guest access is disabled' do + before(:each) do + expect(provider).to_not receive(:allow_anonymous) + expect(provider).to receive(:allow_guests).and_return(false) + end + + it 'should process a NTLM type 3 message and return STATUS_LOGON_FAILURE' do + status = authenticator.process_ntlm_type3(type3_msg) + expect(status).to be_a WindowsError::ErrorCode + expect(status).to eq WindowsError::NTStatus::STATUS_LOGON_FAILURE + end + + after(:each) do + expect(authenticator.session_key).to be_nil + end + end + + context 'when guest access is enabled' do + before(:each) do + expect(provider).to_not receive(:allow_anonymous) + expect(provider).to receive(:allow_guests).and_return(true) + end + + it 'should process a NTLM type 3 message and return STATUS_SUCCESS' do + status = authenticator.process_ntlm_type3(type3_msg) + expect(status).to be_a WindowsError::ErrorCode + expect(status).to eq WindowsError::NTStatus::STATUS_SUCCESS + end + + after(:each) do + expect(authenticator.session_key).to eq "\x00".b * 16 + end + end + end + + context 'when the message is a known user' do + before(:each) do + authenticator.instance_variable_set(:@server_challenge, type2_msg[:challenge].serialize) + end + + context 'when the password is correct' do + it 'should process a NTLM type 3 message and return STATUS_SUCCESS' do + type3_msg.user.force_encoding('UTF-16LE') + type3_msg.domain.force_encoding('UTF-16LE') + status = authenticator.process_ntlm_type3(type3_msg) + expect(status).to be_a WindowsError::ErrorCode + expect(status).to eq WindowsError::NTStatus::STATUS_SUCCESS + end + + after(:each) do + expect(authenticator.session_key).to be_a String + expect(authenticator.session_key.length).to eq 16 + end + end + + context 'when the password is wrong' do + let(:type3_msg) do + type2_msg.response({user: username, password: 'Wrong' + password, domain: domain}, {ntlmv2: true}) + end + + it 'should process a NTLM type 3 message and return STATUS_LOGON_FAILURE' do + status = authenticator.process_ntlm_type3(type3_msg) + expect(status).to be_a WindowsError::ErrorCode + expect(status).to eq WindowsError::NTStatus::STATUS_LOGON_FAILURE + end + + after(:each) do + expect(authenticator.session_key).to be nil + end + end end end diff --git a/spec/lib/ruby_smb/ntlm/client/session_spec.rb b/spec/lib/ruby_smb/ntlm/client/session_spec.rb index c675a10d1..53a80423c 100644 --- a/spec/lib/ruby_smb/ntlm/client/session_spec.rb +++ b/spec/lib/ruby_smb/ntlm/client/session_spec.rb @@ -24,7 +24,7 @@ it 'returns a Type3 message' do expect(session.authenticate!).to be_a Net::NTLM::Message::Type3 - expect(session.authenticate!).to be_a RubySMB::NTLM::Message + expect(session.authenticate!).to be_a Net::NTLM::Message end context 'when it is anonymous' do