0x01 背景

vSphereVMware推出的虚拟化平台套件,包含ESXivCenter Server等一系列的软件。其中vCenter Server ESXi的控制中心,可从单一控制点统一管理数据中心的所有vSphere主机和虚拟机,使得IT管理员能够提高控制能力,简化入场任务,并降低IT环境的管理复杂性与成本。

vSphere Client(HTML5)vCenter Server插件中存在一个远程执行代码漏洞。未授权的攻击者可以通过开放443端口的服务器向vCenter Server发送精心构造的请求,从而在服务器上写入webshell,最终造成远程任意代码执行。

0x02 代码分析

vCenter ServervROPS插件的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;

try {
if (code != 200) {
response.sendError(code, "Arguments Missing");
return;
}

wr = response.getWriter();
} catch (IOException var14) {
var14.printStackTrace();
logger.info("upload Ova Controller Ended With Error");
}

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();

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();
}

OutputStream out = new FileOutputStream(curfile);
IOUtils.copy(in, out);
out.close();
result.add(entry.getName());
entry = in.getNextTarEntry();
}
}

in.close();
logger.info("Successfully deployed File at Location :/tmp/unicorn_ova_dir");
} catch (Exception var15) {
logger.error("Unable to upload OVA file :" + var15);
returnStatus = "FAILED";
}
}

wr.write(returnStatus);
wr.flush();
wr.close();
}

代码逻辑是将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 拼接并写入文件。如果文件名内存在 ../ 即可实现目录遍历。

0x03 影响版本

  • vcenter_server 7.0 U1c 之前的 7.0 版本
  • vcenter_server 6.7 U3l 之前的 6.7 版本
  • vcenter_server 6.5 U3n 之前的 6.5 版本

0x04 复现

VMware-VCSA-all-6.7.0-8217866为例,web的资源目录为/usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/{REPLACE_RANDOM_ID_HERE}/0/h5ngc.war/resources/

https://VCIP/ui/vropspluginui/rest/services/uploadova发送POST请求,tar包内容为带有目录穿越的shell

image-20210227234148006

上传成功会返回SUCCESS

访问上传的shell

image-20210227234950695

0x05 VMware的骚操作

vSphere vCenter6.7 U2的某个分支里,VM官方搞了个骚操作导致6.7U2+无法通过上传webshell的形式去获取vCenter权限,以下为启动前端UI的命令

/usr/java/jre-vmware/bin/vsphere-ui.launcher
-Xmx597m
-XX:CompressedClassSpaceSize=256m
-Xss320k
-XX:ParallelGCThreads=1
-Djava.io.tmpdir=/usr/lib/vmware-vsphere-ui/server/work/tmp
-Dorg.eclipse.virgo.kernel.home=/usr/lib/vmware-vsphere-ui/server
-DPS_BASEDIR=/storage/vsphere-ui/
-Declipse.ignoreApp=true
-Dcatalina.base=/usr/lib/vmware-vsphere-ui/server
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/vmware/vsphere-ui/
-XX:ErrorFile=/var/log/vmware/vsphere-ui/java_error%p.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintReferenceGC
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=1024K
-XX:-OmitStackTraceInFastThrow
-Xloggc:/var/log/vmware/vsphere-ui/vsphere-ui-gc.log
-Djava.security.properties=/etc/vmware/java/vmware-override-java.security
-Djava.ext.dirs=/usr/java/jre-vmware/lib/ext:/usr/java/packages/lib/ext:/opt/vmware/jre_ext/
-Dorg.osgi.framework.system.packages.extra=sun.misc
-Dsun.zip.disableMemoryMapping=true
-Dui.component.name=vsphere-ui
-Dvlsi.client.vecs.certstore=false
-DisFling=false
-Dorg.apache.tomcat.websocket.DISABLE_BUILTIN_EXTENSIONS=true
-Dlogback.configurationFile=/usr/lib/vmware-vsphere-ui/server/conf/serviceability.xml
-Dlogs.dir=/var/log/vmware/vsphere-ui/logs/
-Dhttps.port=5443
-Dhttp.port=5090
-Dshutdown.port=-1
-Dcom.sun.org.apache.xml.internal.security.ignoreLineBreaks=true
-Dorg.apache.xml.security.ignoreLineBreaks=true
-classpath
/usr/lib/vmware-vsphere-ui/server/bootstrap/server-launcher.jar:/usr/lib/vmware-vsphere-ui/server/bin/bootstrap.jar:/usr/lib/vmware-vsphere-ui/server/bin/tomcat-juli.jar
com.vmware.vise.launcher.tomcat.TomcatLauncher
start

注意server-launcher.jar这个包调用了com.vmware.vise.launcher.tomcat.TomcatLauncher类,反编译后部分代码如下

public final class TomcatLauncher
{
private static final String VECS_KEYSTORE_TYPE = "VKS";
private static final String MACHINE_SSL_CERT = "MACHINE_SSL_CERT";
private static final String VECS_LOAD_STORE_PARAM_TYPE = "com.vmware.provider.VecsLoadStoreParameter";
private static final String FILE_SEPARATOR;
private static final String SYSPROP_COMPONENT_NAME = "ui.component.name";
private static final String DEFAULT_COMPONENT_NAME = "vsphere-ui";
private static final String CLIENT_APP_DATA_FOLDER;
private static final String DEST_KEYSTORE_NAME = "keystore.jks";
private static final String PASSWORD;
private static final String TOMCAT_DIR = "catalina.base";
private static final String CONFIG_DIR;

public static void main(final String[] args) {
loadKeystore();
deleteWorkDirectory();
createJavaTempDir();
Bootstrap.main(args);
}

其中有个函数deleteWorkDirectory

private static void deleteWorkDirectory() {
final File workDir = new File(System.getProperty("catalina.base") + "/work");
if (!workDir.exists()) {
return;
}
try {
deleteDirectoryRecur(workDir);
}
catch (IOException e) {
throw new RuntimeException("Cannot clean work dir", e);
}
}

这个函数主要作用就是删除web目录,先从命令行中的catalina.base获取设置的Tomcat工作目录/usr/lib/vmware-vsphere-ui/server,然后拼接/work路径并判断是否存在,不存在则返回,存在的调用deleteDirectoryRecur函数进行删除

private static void deleteDirectoryRecur(final File directory) throws IOException {
final File[] contents = directory.listFiles();
if (contents != null) {
for (final File f : contents) {
if (isSymlink(f)) {
f.delete();
}
else {
deleteDirectoryRecur(f);
}
}
}
directory.delete();
}

private static boolean isSymlink(final File file) throws IOException {
if (file == null) {
throw new NullPointerException("File must not be null");
}
File canon;
if (file.getParent() == null) {
canon = file;
}
else {
final File canonDir = file.getParentFile().getCanonicalFile();
canon = new File(canonDir, file.getName());
}
return !canon.getCanonicalFile().equals(canon.getAbsoluteFile());
}

这也是为什么6.7U2+、7.0+的版本无法进行常规写shell的操作原因。

但是问题来了,实际发现tomcat并没有启动

image-20210228110129479

0x06 柳暗花明

经过分析启动进程rhttpproxy,发现该进程将443请求转发到5090端口

image-20210228110251923

这和启动进程的参数吻合

经过后续的分析,发现了一种有条件写shell的操作,vCenter在重启时会启动服务,但是在启动服务的过程中会调用war包重新生成web目录,这就可以给我们一个可乘之机

查看war包权限

image-20210228000158942

770权限,那么我们可以先把该文件下载回本地进行加料,再利用CVE-2021-21792漏洞回传回vCener服务器进行复写操作,等待vCenter重启时就会在内存中驻留我们的webshell

成功如图

image-20210228000400112

0x07 EXP

https://github.com/NS-Sp4ce/CVE-2021-21972