Files
metasploit-gs/lib/msf/core/exploit/java.rb
T
RageLtMan 4874943e7f Implement infrastructure for payload delivery
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.
2021-12-29 09:10:07 -05:00

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