#!/usr/bin/env ruby

msfbase = __FILE__
while File.symlink?(msfbase)
  msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
end
$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))
$:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']

require 'nokogiri'

module CVE
  class XRefTable

    attr_reader :module_full_name_ref
    attr_reader :edb_ref
    attr_reader :bid_ref
    attr_reader :osvdb_ref
    attr_reader :msb_ref
    attr_reader :zdi_ref
    attr_reader :url_refs

    def initialize(refs)
      @module_full_name_ref = refs['fullname']
      @edb_ref               = refs['EDB']
      @bid_ref               = refs['BID']
      @osvdb_ref             = refs['OSVDB']
      @msb_ref               = refs['MSB']
      @zdi_ref               = refs['ZDI']
      @url_refs              = refs['URL']
    end

    def has_match?(ref_match)
      if (
           (module_full_name_ref && ref_match.match(/#{module_full_name_ref}/)) ||
           (edb_ref               && ref_match.match(/EXPLOIT\-DB:#{edb_ref}$/)) ||
           (osvdb_ref             && ref_match.match(/OSVDB:#{osvdb_ref}$/)) ||
           (bid_ref               && ref_match.match(/BID:#{bid_ref}$/)) ||
           (msb_ref               && ref_match.match(/http:\/\/technet\.microsoft\.com\/security\/bulletin\/#{msb_ref}$/)) ||
           (zdi_ref               && ref_match.match(/zerodayinitiative\.com\/advisories\/ZDI\-#{zdi_ref}/)) ||
           (url_refs_match?(ref_match))
        )
        return true
      end

      false
    end

    private

    def url_refs_match?(ref_match)
      return false unless url_refs
      return false unless ref_match.match(/^http/)

      url_refs.each do |url|
        return true if url == ref_match
      end

      false
    end
  end

  class Database
    attr_reader :database

    def initialize(db_path)
      @database = normalize(db_path)
    end

    def cross_reference(reference_matches)
      return nil if reference_matches.empty?
      xref_table = XRefTable.new(reference_matches)

      database.each_pair do |cve_name, references|
        references.each do |cve_ref|
          if xref_table.has_match?(cve_ref)
            return cve_name
          end
        end
      end

      nil
    end

    private

    def normalize(db_path)
      html = load_cve_html_file(db_path)
      normalize_html_to_hash(html)
    end

    def load_cve_html_file(db_path)
      puts "[*] Loading database..."
      raw_data = File.read(db_path)
      Nokogiri::HTML(raw_data)
    end

    def normalize_html_to_hash(html)
      puts "[*] Normalizing database..."

      db = {}
      current_cve = nil
      metasploit_refs = []
      html.traverse do |element|
        if current_cve
          if element.text =~ /(https*:\/\/.*metasploit.+)/
            metasploit_refs << $1
          elsif element.text =~ /(http:\/\/www\.exploit\-db\.com\/.+)/
            metasploit_refs << $1
          elsif element.text =~ /(BID:\d+)/
            metasploit_refs << $1
          elsif element.text =~ /(OSVDB:\d+)/
            metasploit_refs << $1
          elsif element.text =~ /http:\/\/technet\.microsoft\.com\/security\/bulletin\/(MS\d+\-\d+)$/
            metasploit_refs << $1
          elsif element.text =~ /zerodayinitiative\.com\/advisories\/(ZDI\-\d+\-\d+)/
            metasploit_refs << $1
          elsif element.text =~ /URL:(http.+)/
            metasploit_refs << $1
          end
        end

        if element.text =~ /^Name: (CVE\-\d+\-\d+)$/
          current_cve = $1
        elsif element.text =~ /^Votes:/
          unless metasploit_refs.empty?
            db[current_cve] = metasploit_refs
          end
          current_cve = nil
          metasploit_refs = []
        end
      end

      db
    end
  end

end

class Utility
  def self.ignore_module?(module_full_name)
    [
      'exploit/multi/handler'
    ].include?(module_full_name)
  end

  def self.collect_references_from_module!(module_references, ref_ids, mod)
    if ref_ids.include?('EDB')
      edb_ref = mod.references.select { |r| r.ctx_id == 'EDB' }.first.ctx_val
      module_references['EDB'] = edb_ref
    end

    if ref_ids.include?('BID')
      bid_ref = mod.references.select { |r| r.ctx_id == 'BID' }.first.ctx_val
      module_references['BID'] = bid_ref
    end

    if ref_ids.include?('OSVDB')
      osvdb_ref = mod.references.select { |r| r.ctx_id == 'OSVDB' }.first.ctx_val
      module_references['OSVDB'] = osvdb_ref
    end

    if ref_ids.include?('MSB')
      msb_ref = mod.references.select { |r| r.ctx_id == 'MSB' }.first.ctx_val
      module_references['MSB'] = msb_ref
    end

    if ref_ids.include?('ZDI')
      zdi_ref = mod.references.select { |r| r.ctx_id == 'ZDI' }.first.ctx_val
      module_references['ZDI'] = zdi_ref
    end

    if ref_ids.include?('URL')
      url_refs = mod.references.select { |r| r.ctx_id == 'URL' }.collect { |r| r.ctx_val if r }
      module_references['URL'] = url_refs
    end
  end

end

require 'msfenv'

def main
  filter  = 'All'
  filters = ['all','exploit','payload','post','nop','encoder','auxiliary']
  type    = 'CVE'
  db_path = nil

  opts = Rex::Parser::Arguments.new(
    "-h" => [ false, 'Help menu.' ],
    "-f" => [ true,  'Filter based on Module Type [All,Exploit,Payload,Post,NOP,Encoder,Auxiliary] (Default = ALL).'],
    "-d" => [ true,  'Source of CVE database in HTML (allitems.html)'],
  )

  opts.parse(ARGV) { |opt, idx, val|
    case opt
    when "-h"
      puts "\nMetasploit script for finding CVEs from other references."
      puts "=========================================================="
      puts opts.usage
      exit
    when "-f"
      unless filters.include?(val.downcase)
        puts "Invalid Filter Supplied: #{val}"
        puts "Please use one of these: #{filters.map{|f|f.capitalize}.join(", ")}"
        exit
      end
      filter = val
    when "-d"
      unless File.exist?(val.to_s)
        raise RuntimeError, "#{val} not found"
      end

      db_path = val
    end
  }

  framework_opts = { 'DisableDatabase' => true }
  framework_opts[:module_types] = [ filter.downcase ] if filter.downcase != 'all'
  $framework = Msf::Simple::Framework.create(framework_opts)
  cve_database = CVE::Database.new(db_path)

  puts "[*] Going through Metasploit modules for missing references..."
  $framework.modules.each { |name, mod|
    if mod.nil?
      elog("Unable to load #{name}")
      next
    end

    elog "Loading #{name}"
    m = mod.new
    next if Utility.ignore_module?(m.fullname)

    ref_ids = m.references.collect { |r| r.ctx_id }
    next if ref_ids.include?(type)

    elog "Checking references for #{m.fullname}"
    module_references = {}
    module_references['fullname'] = m.fullname
    Utility.collect_references_from_module!(module_references, ref_ids, m)
    cve_match = cve_database.cross_reference(module_references)
    if cve_match
      puts "[*] #{m.fullname}: Found #{cve_match}"
    end
  }
end

if __FILE__ == $PROGRAM_NAME
  main
end

