# -*- coding: binary -*-
###
#
# This mixin provides methods for interacting with a JDK installation to perform
# functions such as dynamic compilation and jar signing.
#
# Dependencies:
#       - JDK6
#       - rjb (rjb.rubyforge.org)
#		- the $JAVA_HOME variable must point to the JDK
#
# Nathan Keltner <natron@metasploit.com>
#
###

module Msf
module Exploit::Java

  def initialize(info = {})
    super

    register_advanced_options(
      [
        OptString.new( 'JavaCache', 	[true, 'Java cache location',
          File.join(Msf::Config.config_directory, "javacache")]),
        OptString.new( 'AddClassPath', 	[false, 'Additional java classpath', nil]),
      ], self.class)

    begin
      require 'rjb'
      @rjb_loaded = true
      init_jvm
    rescue ::Exception => e
      @rjb_loaded = false
      @jvm_init   = false
      @java_error  = e
    end
  end

  def init_jvm(jvmoptions = nil)
    if (not ENV['JAVA_HOME'])
      raise RuntimeError, 'JAVA_HOME is not set'
    end

    toolsjar = File.join(ENV['JAVA_HOME'], "lib", "tools.jar")
    if (not File.exist? toolsjar)
      raise RuntimeError, 'JAVA_HOME does not point to a valid JDK installation.'
    end

    # Instantiate the JVM with a classpath pointing to the JDK tools.jar
    # and our javatoolkit jar.
    classpath  = File.join(Msf::Config.data_directory, "exploits", "msfJavaToolkit.jar")
    classpath += ":" + toolsjar
    classpath += ":" + datastore['ADDCLASSPATH'] if datastore['ADDCLASSPATH']

    Rjb::load(classpath, jvmargs=[])

    @jvm_init = true
  end

  def query_jvm
    return @jvmInit
  end

  def save_to_file(classnames, codez, location)
    path = File.join( Msf::Config.install_root, "external", "source", location )

    if not File.exist? path
      Dir.mkdir(path)
    end

    i = 0
    classnames.each { |fil|
      file = File.join( path, fil + ".java")
      fp   = File.open( file, "wb" )
      print_status "Writing #{fil} to " + file
      fp.puts codez[i]
      i += 1
      fp.close
    }
  end

  def compile(classnames, codez, compile_options=nil)
    if !@rjb_loaded or !@jvm_init
      raise RuntimeError, "Could not load rjb and/or the JVM: " + @java_error.to_s
    end

    if !compile_options.is_a?(Array) && compile_options
      raise RuntimeError, "Compiler options must be of type Array."
    end

    compile_options = [] if compile_options.nil?

    # Create the directory if it doesn't exist
    Dir.mkdir(datastore['JavaCache']) if !File.exist? datastore['JavaCache']

    # For compatibility, some exploits need to have the target and source version
    # set to a previous JRE version.
    std_compiler_opts = [ "-target", "1.3", "-source", "1.3", "-d", datastore['JavaCache'] ]

    compile_options += std_compiler_opts

    java_compiler_klass = Rjb::import('javaCompile.CompileSourceInMemory')

    # If we were passed arrays
    if classnames.class == [].class && codez.class == [].class
      # default compile class
      begin
      # Same as java_compiler_klass.CompileFromMemory(	String[] classnames,
      #						String[] codez, String[] compilerOptions)
        success = java_compiler_klass._invoke('CompileFromMemory',
          # Signature explained: [ means array, Lpath.to.object; means object
          # Thus, this reads as call the method with 3 String[] args.
          '[Ljava.lang.String;[Ljava.lang.String;[Ljava.lang.String;',
          classnames, codez, compile_options)
      rescue Exception => e
        print_error "Received unknown error: " + e
      end
    else
      raise RuntimeError, "The Java mixin received unknown argument-type combinations and cannot continue."
    end
    if !success
      raise RuntimeError, "Compile failed."
    end
  end

  def build_jar(output_jar, in_files)
    if output_jar.class != "".class || in_files.class != [].class
      raise RuntimeError, "Building a jar requires an output_jar and an Array of in_files."
    end

    # Add paths
    in_files	= in_files.map { |file| File.join(datastore['JavaCache'], file) }

    create_jar_klass = Rjb::import('javaCompile.CreateJarFile')
    file_class	 = Rjb::import('java.io.File')

    file_out_jar	= file_class.new_with_sig('Ljava.lang.String;', File.join(datastore['JavaCache'], output_jar) )
    files_in	= Array.new

    in_files.each { |file| files_in << file_class.new_with_sig('Ljava.lang.String;', file) }
    create_jar_klass._invoke('createJarArchive', 'Ljava.io.File;[Ljava.io.File;', file_out_jar, files_in)
  end

  #
  # http://www.defcon.org/images/defcon-17/dc-17-presentations/defcon-17-valsmith-metaphish.pdf
  #
  def sign_jar(cert_cn, unsiged_jar, signed_jar, cert_alias="signFiles", msf_keystore="msfkeystore",
      msf_store_pass="msfstorepass", msf_key_pass="msfkeypass")

    # Dependent on $JAVA_HOME/lib/tools.jar that comes with the JDK.
    signer_klass 	= Rjb::import('javaCompile.SignJar')

    # Check if the keystore exists from previous run.  If it does, delete it.
    msf_keystore	= File.join(datastore['JavaCache'], msf_keystore)
    File.delete msf_keystore if File.exist? msf_keystore

    # Rjb pukes on a CN with a comma in it so bad that it crashes to shell
    # and turns input echoing off.  Simple fix for this ugly bug is
    # just to get rid of commas which kinda sucks but whatever.  See #1543.
    keytool_opts	= [
      "-genkey", "-alias", cert_alias, "-keystore", msf_keystore,
      "-storepass", msf_store_pass, "-dname", "CN=#{cert_cn.gsub(",",'')}",
      "-keypass", "msfkeypass"
    ]

    # Build the cert keystore
    signer_klass._invoke('KeyToolMSF','[Ljava.lang.String;',keytool_opts)

    jarsigner_opts	= [
      "-keystore", msf_keystore, "-storepass", msf_store_pass,
      "-keypass", msf_key_pass, "-signedJar",
      File.join(datastore['JavaCache'], signed_jar), 	 # Signed Jar
      File.join(datastore['JavaCache'], unsiged_jar),  # Input Jar we're signing
      cert_alias  # The cert we're using
    ]
    signer_klass._invoke('JarSignerMSF','[Ljava.lang.String;',jarsigner_opts)

    # There are warnings in the source for KeyTool/JarSigner warning that security providers
    # are not released, and if you are calling .main(foo) from another app, you need to release
    # them manually.  This is not done here, and should Rjb be used for anything in the future,
    # this may need to be cleaned up.
  end

  #
  # Create a Java-natively-serialized object for use in Ruby
  #
  # @param jar [String] Buffer containing JAR data from which to extract the class
  # @param ser_class [String] The class name to be serialized
  #
  # @return [String] Marshalled serialized byteArray
  def serialized_class_from_jar(jar, ser_class)
    file_name      = Rex::Text.rand_text_alpha_lower(8)
    file_path      = datastore['JavaCache'] + "/#{file_name}.jar"
    ::File.open(file_path, 'wb+') {|f| f.write(jar)}
    ::Rjb::add_jar(file_path)
    ::File.unlink(file_path)
    payClass       = ::Rjb::import(ser_class)
    byteArrayClass = ::Rjb::import("java.io.ByteArrayOutputStream")
    outputClass    = ::Rjb::import("java.io.ObjectOutputStream")
    payInst        = payClass.new()
    byteArrayInst  = byteArrayClass.new()
    outputInst     = outputClass.new(byteArrayInst)
    begin
      serResult      = outputInst.writeObject(payInst)
    rescue => e
      # Rjb exceptions are pretty broken - try to inform the user of where we keeled
      print_error("Failed to Rjb-serialize the #{ser_class} class due to #{e}")
      raise e
    end
    byteArrayInst.toByteArray()
  end
end
end
