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


require 'msf/core'
require 'rex/proto/http'

class MetasploitModule < Msf::Auxiliary

  include Msf::Auxiliary::Report


  def initialize(info={})
    super(update_info(info,
      'Name'        => 'ZoomEye Search',
      'Description' => %q{
        The module use the ZoomEye API to search ZoomEye. ZoomEye is a search
        engine for cyberspace that lets the user find specific network
        components(ip, services, etc.).
      },
      'Author'      => [ 'Nixawk' ],
      'References'  => [
        ['URL', 'https://github.com/zoomeye/SDK'],
        ['URL', 'https://www.zoomeye.org/api/doc'],
        ['URL', 'https://www.zoomeye.org/help/manual']
      ],
      'License'     => MSF_LICENSE
      ))

      deregister_options('RHOST', 'DOMAIN', 'DigestAuthIIS', 'NTLM::SendLM',
            'NTLM::SendNTLM', 'VHOST', 'RPORT', 'NTLM::SendSPN', 'NTLM::UseLMKey',
            'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2', 'SSL')

      register_options(
        [
          OptString.new('USERNAME', [true, 'The ZoomEye username']),
          OptString.new('PASSWORD', [true, 'The ZoomEye password']),
          OptString.new('ZOOMEYE_DORK', [true, 'The ZoomEye Dock']),
          OptEnum.new('RESOURCE', [true, 'ZoomEye Resource Type', 'host', ['host', 'web']]),
          OptInt.new('MAXPAGE', [true, 'Max amount of pages to collect', 1])
        ], self.class)
  end

  # Check to see if api.zoomeye.org resolves properly
  def zoomeye_resolvable?
    begin
      Rex::Socket.resolv_to_dotted("api.zoomeye.org")
    rescue RuntimeError, SocketError
      return false
    end

    true
  end

  def login(username, password)
    # See more: https://www.zoomeye.org/api/doc#login

    access_token = ''
    @cli = Rex::Proto::Http::Client.new('api.zoomeye.org', 443, {}, true)
    @cli.connect

    data = {'username' => username, 'password' => password}
    req = @cli.request_cgi({
      'uri'    => '/user/login',
      'method' => 'POST',
      'data'   => data.to_json
    })

    res = @cli.send_recv(req)

    unless res
      print_error('server_response_error')
      return
    end

    records = ActiveSupport::JSON.decode(res.body)
    access_token = records['access_token'] if records && records.key?('access_token')
    access_token
  end

  def dork_search(dork, resource, page)
    # param: dork
    #        ex: country:cn
    #        access https://www.zoomeye.org/search/dorks for more details.
    # param: page
    #        total page(s) number
    # param: resource
    #        set a search resource type, ex: [web, host]
    # param: facet
    #        ex: [app, device]
    #         A comma-separated list of properties to get summary information

    begin
      req = @cli.request_cgi({
        'uri'      => "/#{resource}/search",
        'method'   => 'GET',
        'headers'  => { 'Authorization' => "JWT #{@zoomeye_token}" },
        'vars_get' => {
          'query'  => dork,
          'page'   => page,
          'facet'  => 'ip'
        }
      })

      res = @cli.send_recv(req)

    rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
      print_error("HTTP Connection Failed")
    end

    unless res
      print_error('server_response_error')
      return
    end

    # Invalid Token, Not enough segments
    # Invalid Token, Signature has expired
    if res.body =~ /Invalid Token, /
      fail_with(Failure::BadConfig, '401 Unauthorized. Your ZOOMEYE_APIKEY is invalid')
    end

    ActiveSupport::JSON.decode(res.body)
  end

  def match_records?(records)
    records && records.key?('matches')
  end

  def parse_host_records(records)
    records.each do |match|
      host = match['ip']
      port = match['portinfo']['port']

      report_service(:host => host, :port => port)
      print_good("Host: #{host} ,PORT: #{port}")
    end
  end

  def parse_web_records(records)
    records.each do |match|
      host = match['ip'][0]
      domains = match['domains']

      report_host(:host => host)
      print_good("Host: #{host}, Domains: #{domains}")
    end
  end

  def run
    # check to ensure api.zoomeye.org is resolvable
    unless zoomeye_resolvable?
      print_error("Unable to resolve api.zoomeye.org")
      return
    end

    @zoomeye_token = login(datastore['USERNAME'], datastore['PASSWORD'])
    if @zoomeye_token.blank?
      print_error("Unable to login api.zoomeye.org")
      return
    end

    # create ZoomEye request parameters
    dork = datastore['ZOOMEYE_DORK']
    resource = datastore['RESOURCE']
    page = 1
    maxpage = datastore['MAXPAGE']

    # scroll max pages from ZoomEye
    while page <= maxpage
      print_status("ZoomEye #{resource} Search: #{dork} - page: #{page}")
      results = dork_search(dork, resource, page) if dork
      break unless match_records?(results)

      matches = results['matches']
      if resource.include?('web')
        parse_web_records(matches)
      else
        parse_host_records(matches)
      end
      page += 1
    end
  end
end
