4874943e7f
Per the discussion with @schierlm on GitHub (mihi), the most direct way to deliver and instantiate our Java payload in the target is via remote code loading of the JAR using HTTP. This requires a bootstrap class, a Factory, which instantiates our Payload.class by calling its main() function on-load from the HTTP endpoint serving the remote-code-loaded JAR. Implement a basic PayloadFactory class and include and its sources in the Metasploit tree. Using @schierlm's own code from ~10y ago, implement injection of the PayloadFactory class into our JAR-encoded payloads. Then, using more of his code from the same module (2011-3544), implement a secondary service within the exploit module (Rex::ServiceManager services don't stack well in Msf namespace as they all get assigned to self.service - faux pas on our end) to serve HTTP requests with the injected JAR. Finally, generate an appropriate URL target for the remote code loaded JAR for injection into the LDAP response and leveraging a final piece of @schierlm's hackery, generate a valid URI path (updating the datastore is ill advised @ runtime, but its needed here for the correct service cleanup routines to fire). Note: during development, i figured out a way to use Rjb for native Java object serialization to buffers which we can use in Ruby, so i stashed that away in the Exploit::Java mixin for posterity and left a reference to it in the module for future endeavors. Testing: Verified that the generated jar is served at the generated URL Verified that the generated JAR can be executed at the CLI for both metasploit.Payload and metasploit.PayloadFactory Currently not triggering the remote code load (per wireshark and our own HTTP service) when delivering the LDAP response, so tuning that is the next leg of this effort.
215 lines
7.3 KiB
Ruby
215 lines
7.3 KiB
Ruby
# -*- 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
|