Reproducing The ProxyShell Pwn2Own Exploit

Peterjson
7 min readAug 6, 2021

INTRO

I and Jang recently successfully reproduced the ProxyShell Pwn2Own Exploit of Orange Tsai 🍊. Firstly, I just want to tell that I respect your hard work and the contribution of you to cybersecurity which inspired me many years ago. Now I want to summary the progress when we reproduce this Exploit chain as a write-up for our-self. When ZDI release the advisories about these bug, I decided to analysis this chain for learning purpose. We can almost finished the chain before the BlackHat US talk’s of Orange and then we found an missing piece when Orange’s talk finished. So this write-up as an progress of 1-day analysis, we diff the patch, and solved the puzzle step by step. For those who never seen Orange’s talk, you can check it here.

https://www.zerodayinitiative.com/advisories/published/

With these advisories, we can imagine that this chain maybe similar to ProxyLogon chain

  1. pre-auth SSRF
  2. somehow we can SSRF to /powershell endpoint
  3. finally calling cmdlets for post-auth RCE

ProxyLogon revisited

For each front-end endpoint like /ecp/, /owa/, /autodiscover/ , /powershell/ and so on, there’s a class implement Microsoft.Exchange.HttpProxy.ProxyRequestHandler . We already know that from ProxyLogon analysis

ProxyLogon entry

From ProxyLogon, we know that we can set AnchoredRoutingTarget variable from “X-BEResource”, then Exchange when calculate the target backend URL to request internal we can reach internal endpoint we overwrite it and we have SSRF

ProxyRequestHandler.GetTargetBackEndServerUrl()

Autodiscover Pre-auth SSRF

With the information from ZDI, pre-auth SSRF come from autodiscover service so we find some class which implement “Microsoft.Exchange.HttpProxy.ProxyRequestHandler” and allow for unauthenticated

Microsoft.Exchange.HttpProxy.AutodiscoverProxyRequestHandler
AutodiscoverProxyRequestHandler 
=> implement EwsAutodiscoverProxyRequestHandler
=> implement BEServerCookieProxyRequestHandler
=> implement ProxyRequestHandler

And “/autodiscover” also allow for unauthenticated

ProxyModule.SelectHandlerForUnauthenticatedRequest()

With these information, we sure that this is what need need to focus on. Look back at the point we SSRF came from.

ProxyRequestHandler.GetTargetBackEndServerUrl()

After ProxyLogon patch, there’s a check for AnchoredRoutingTarget variable, so we somehow can successfully change it again like ProxyLogon, we will got 503 , don’t know why? check here

ProxyRequestHandler.GetTargetBackEndServerUrl() will return the URI after finish calculate, we cannot abuse AnchoredRoutingTarget anymore, how about GetClientUrlForProxy() ? Then control our URI and send into backend, sound interesting

checking FQDN after ProxyLogon

This is what we need to looking for, if our request IsAutodiscoverV2Request(), it will remove the “explicitLogonAddress” from URI and rebuild the URI.

EwsAutodiscoverProxyRequestHandler.GetClientUrlForProxy()

How can we set explicitLogonAddress variable from our request?

Notice that, Params variable contains parameters from query string, form parameter, cookies, …

We need to pass some conditions

  1. We want to reach the if statement so IsAutodiscoverV2Request() must return False and IsAutodiscoverV2PreviewRequest() return False also
Focus that this is AbsoluteUri not AbsolutePath

Because IsAutodiscoverV2PreviewRequest() check EndsWith(“/autodiscover.json”) and the path variable is AbsoluteUri we can make it return False like

So it is /autodiscover/autodiscover.json + dummy string

2. explicitLogonAddress must contains valid email address

So it is “/autodiscover/autodiscover.json?a=dummy@dummy.pw” (in order to help us can reach the if statement which will return False and remove explicitLogon ) and then we set this value into Email Cookie with the same value

3. When preparing request to send to backend internal, Exchange will generate Kerberos auth header and attach into Authorization header. This is why we can reach some other endpoint without any authentication

PrepareServerRequest()

Chaining into together we have an pre-auth SSRF

SSRF with system privilege

Pre-auth SSRF into /powershell

The next bug we need to looking for is SSRF into “/powershell endpoint”

We don’t have permission on this endpoint :(

Always remember one think, you should understand on what you are looking while doing 1-day anlysis. IIS has some modules on each web application, they are excuted before the actual handler executed. You can imagine they’re like “filter” mechanism on Java web apps.

Powershell-Proxy IIS modules

We need to look at each module to see what we have missed. On BackendRehydrationModule when process the request, this module cannot get CommonAccessToken (from Exchange SSRF) there will be an exception and we cannot go through.

BackendRehydrationModule.OnAuthenticateRequest()
BackendRehydrationModule.ProcessRequest()

So how can we set the header “X-CommonAccessToken” because we cannot make Exchange copy it to SSRF request and send to “/powershell”

some blacklist cookies Exchange won’t copy to internal

Before BackendRehydrationModule executed, there’s RemotePowershellBackendCmdletProxyModule

fetch CommonAccessToken from “X-Rps-CAT”

Basically, when the SSRF doesn’t contain Header “X-CommonAccessToken” Exchange try to fetch the value of param “X-Rps-CAT” and then deserialize our controlled data to create an valid CommonAccessToken. Then at BackendRehydrationModule we will survive from the Exception. But how can we create a valid CommonAccessToken or maybe high privilege CommonAccessToken? We need to reverse the structure of CommonAccessToken

deserialize “X-Rps-CAT” into CommonAccessToken
V + version + T + type + C + compress + data
if compress => decompress then if type is Windows

This is pseudocode I make for CommonAccessToken, if the token type is “Windows”, Exchange continue deserialise our data

WindowsAccessToken
A + authenType + L + logonName + U + user SUID
read group SUIDs
G + groupLength + SUIDs of group

At this point, I setup socat, change internal Exchange port from 444 to 4443, using socat listening on port 444, then redirect to BurpSuite port (8080) and finally foward into 4443. With this setup, we can capture a “sample” CommonAccessToken format and then crafting a suitable one.

How can you get a valid user SUID without exist user on Exchange? When deploying Exchange, there are some “always exist” mailbox such as

https://docs.microsoft.com/en-us/exchange/architecture/mailbox-servers/recreate-arbitration-mailboxes?view=exchserver-2019

Like ProxyLogon we can easily got SUID of this user “SystemMailbox{bb558c35–97f1–4cb9–8ff7-d53741dc928c}” with this flow

  1. SSRF to “/autodiscover/autodiscover.xml”
  2. Leaking user SUID via “/mapi/emsmdb”

Now we got SUID, but how about group SUIDs? Check out this cmdlet

Get-Group | Format-List Identity,Sid

Now, we can craft an admin privilege CommonAccessToken via “X-Rps-CAT” parameter.

New-MailboxExportRequest arbitrary file write

From MS docs, this cmdlet can export the mailbox into arbitrary location. This mean we can write our shell into web root of Exchange and archive RCE?

https://docs.microsoft.com/en-us/powershell/module/exchange/new-mailboxexportrequest?view=exchange-ps

We can confirm it again, because the patch only allow some specific extension

But how can we control the data in the mailbox and make it into shell after the file was exported? This is what we got stuck for a long time until Orange’s talk appear.

https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-ProxyLogon-Is-Just-The-Tip-Of-The-Iceberg-A-New-Attack-Surface-On-Microsoft-Exchange-Server.pdf?fbclid=IwAR2V0-4k2yb8dmPP5Mksd8iHYTOfE6sBwygMt4wjq3M9be8Tw6TlH0andhA

Looking back into MS documents, Jang found this one which help us successfully write shell

https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-pst/5faf4800-645d-49d1-9457-2ac40eb467bd

But how can we put our shell into the mailbox and then export it as our shell?

EWS will save us, EWS (/ews/exchange.asmx) is a service based on SOAP which help us can create mail, event, meeting, …

We can create an email saved in “drafts” for any user via SOAP header “SerializedSecurityContext”- this called EWS Impersonation . Then injecting our “encoded” shell as an attachment.

Channing all together

Now, we have every thing for this chain, the only thing we need to do is implement an WinRM protocol for the Pre-auth SSRF to comunicate with “/powershell” endpoint. I leave this as an lesson for reader and hopefully you should reproduce this bug by yourself because it help you learn many things.

For myself, I use pypsrp then collect the data while it processing and plug it into our SSRF. To understand more about WinRM you can check this awesome blog

Or you can do the same with Orange’s way, implement his own proxy to communicate with WinRM

Our demonstration:

https://www.youtube.com/watch?v=LbIYPFrltdA

Thanks everyone for reading to the end. Hopefully everyone stay safe during the Covid-19 pandemic recently at VietNam . Have a nice weekend !

--

--