# -*- coding: binary -*-
###
#
# This mixn 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>
#
###

require 'msf/core'

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.exists? 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.install_root, "data", "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.exists? 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.class.to_s != "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.exists? 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
			# Sames 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.exists? 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

end
end
