rapid7/metasploit-framework

View on GitHub
lib/msf/core/modules/external/bridge.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# -*- coding: binary -*-
require 'open3'
require 'json'

module Msf::Modules
  class External
    class Bridge

      attr_reader :path, :running, :messages, :exit_status

      def self.applies?(module_name)
        File::executable? module_name
      end

      def exec(req)
        unless self.running
          self.running = true
          send(req)
          self.read_thread = threadme do
            begin
              while self.running && m = next_message
                self.messages.push m
              end
            ensure
              cleanup
            end
          end

          self
        end
      end

      def close
        self.running = false
        self.read_thread.join

        self
      end

      def success?
        self.exit_status && self.exit_status.success?
      end

      def initialize(module_path, framework: nil)
        self.env = {}
        self.running = false
        self.path = module_path
        self.cmd = [[self.path, self.path]]
        self.messages = Queue.new
        self.buf = ''
        self.framework = framework
      end

      protected

      attr_writer :path, :running, :messages, :exit_status
      attr_accessor :cmd, :env, :ios, :buf, :read_thread, :wait_thread, :framework

      # XXX TODO non-blocking writes, check write lengths

      def send(message)
        input, output, err, status = ::Open3.popen3(self.env, *self.cmd)
        self.ios = [input, output, err]
        self.wait_thread = status
        # We would call Rex::Threadsafe directly, but that would require rex for standalone use
        case select(nil, [input], nil, 0.1)
        when nil
          raise "Cannot run module #{self.path}"
        when [[], [input], []]
          m = message.to_json
          write_message(input, m)
        else
          raise "Error running module #{self.path}"
        end
      rescue => e
        raise handle_exception(e)
      end

      def handle_exception(e)
        e
      end

      def write_message(fd, json)
        fd.write(json)
      end

      def next_message(timeout=600)
        _, out, err = self.ios
        message = ''

        # Multiple messages can come over the wire all at once, and since yajl
        # doesn't play nice with windows, we have to emulate a state machine to
        # read just enough off the wire to get one request at a time. Since
        # Windows cannot do a nonblocking read on a pipe, we are forced to do a
        # whole lot of `select` syscalls and keep a buffer ourselves :(
        begin
          loop do
            # This is so we don't end up calling JSON.parse on every char and
            # catch an exception. Windows can't do nonblock on pipes, so we
            # still have to do the select if we are not at the end of object
            # and don't have any buffer left
            parts = self.buf.split '}', 2
            if parts.length == 2 # [part, rest]
              message << parts[0] << '}'
              self.buf = parts[1]
              break
            elsif parts.length == 1 # [part]
              message << parts[0]
              self.buf = ''
            end

            # We would call Rex::Threadsafe directly, but that would require Rex for standalone use
            res = select([out, err], nil, nil, timeout)
            if res == nil
              # This is what we would have gotten without Rex and what `readpartial` can also raise
              raise EOFError.new
            else
              fds = res[0]
              # Preferentially drain and log stderr, EOF counts as activity, but
              # stdout might have some buffered data left, so carry on
              if fds.include?(err) && !err.eof?
                errbuf = err.readpartial(4096)
                if self.framework
                  elog "Unexpected output running #{self.path}:\n#{errbuf}"
                else
                  $stderr.puts errbuf
                end
              end
              if fds.include? out
                self.buf << out.readpartial(4096)
              end
            end
          end

          Message.from_module(JSON.parse(message))
        rescue JSON::ParserError
          # Probably an incomplete response, but no way to really tell. Keep trying
          # until EOF
          retry
        rescue EOFError => e
          self.running = false
        end
      end

      def harvest_process
        if self.wait_thread.join(10)
          self.exit_status = self.wait_thread.value
        elsif Process.kill('TERM', self.wait_thread.pid) && self.wait_thread.join(10)
          self.exit_status = self.wait_thread.value
        else
          Process.kill('KILL', self.wait_thread.pid)
          self.exit_status = self.wait_thread.value
        end
      end

      def cleanup
        self.running = false
        self.messages.close
        harvest_process
        self.ios.each {|fd| fd.close rescue nil} # Yeah, yeah. I know.
      end

      def threadme(&block)
        if self.framework
          # Leak as few connections as possible
          self.framework.threads.spawn("External Module #{self.path}", false, &block)
        else
          ::Thread.new &block
        end
      end
    end
  end
end

class Msf::Modules::External::PyBridge < Msf::Modules::External::Bridge
  def self.applies?(module_name)
    module_name.match? /\.py$/
  end

  def initialize(module_path, framework: nil)
    super
    pythonpath = ENV['PYTHONPATH'] || ''
    self.env = self.env.merge({ 'PYTHONPATH' => File.expand_path('../python', __FILE__) + File::PATH_SEPARATOR + pythonpath})
  end

  def handle_exception(error)
    case error
    when Errno::ENOENT
      LoadError.new('Failed to execute external Python module. Please ensure you have Python3 installed on your environment.')
    else
      super
    end
  end
end

class Msf::Modules::External::RbBridge < Msf::Modules::External::Bridge
  def self.applies?(module_name)
    module_name.match? /\.rb$/
  end

  def initialize(module_path, framework: nil)
    super
    ruby_path = File.expand_path('../ruby', __FILE__)
    self.cmd = [[Gem.ruby, 'ruby'], "-I#{ruby_path}", self.path]
  end
end

class Msf::Modules::External::GoBridge < Msf::Modules::External::Bridge
  def self.applies?(module_name)
    module_name.match? /\.go$/
  end

  def initialize(module_path, framework: nil)
    super
    default_go_path = ENV['GOPATH'] || ''
    shared_module_lib_path = File.dirname(module_path) + "/shared"
    go_path = File.expand_path('../go', __FILE__)

    if File.exist?(default_go_path)
      go_path = go_path + File::PATH_SEPARATOR + default_go_path
    end

    if File.exist?(shared_module_lib_path)
      go_path = go_path + File::PATH_SEPARATOR + shared_module_lib_path
    end

    self.env = self.env.merge(
      {
        'GOPATH' => go_path,
        'GO111MODULE' => 'auto'
      }
    )
    self.cmd = ['go', 'run', self.path]
  end

  def handle_exception(error)
    case error
    when Errno::ENOENT
      LoadError.new('Failed to execute external Go module. Please ensure you have Go installed on your environment.')
    else
      super
    end
  end
end

class Msf::Modules::External::Bridge

  LOADERS = [
    Msf::Modules::External::PyBridge,
    Msf::Modules::External::RbBridge,
    Msf::Modules::External::GoBridge,
    Msf::Modules::External::Bridge
  ]

  def self.open(module_path, framework: nil)
    LOADERS.each do |klass|
      return klass.new module_path, framework: framework if klass.applies? module_path
    end

    nil
  end
end