feat: support parse custom shell className

This commit is contained in:
ReaJason
2025-11-20 00:36:25 +08:00
parent 81926b0a78
commit 245ec99839
7 changed files with 180 additions and 28 deletions
@@ -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<MemShellFormSchema> }>) {
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 (
<Fragment>
<div className="pt-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center gap-2"
>
<Settings className="h-4 w-4" />
{t("classNameOptions")}
{showAdvanced ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
<div className="pt-2 flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-sm">
<Shuffle className="h-4 w-4" />
<span>{t("mainConfig.randomClassName")}</span>
</div>
<Switch
id="randomClassName"
checked={useRandomClassName}
onCheckedChange={handleToggleRandomClass}
/>
</div>
{showAdvanced && (
<FormProvider {...form}>
<FormProvider {...form}>
{!useRandomClassName && (
<FormField
control={form.control}
name="shellClassName"
@@ -53,6 +95,8 @@ export function OptionalClassFormField({
</FormFieldItem>
)}
/>
)}
{!useRandomClassName && (
<FormField
control={form.control}
name="injectorClassName"
@@ -69,8 +113,8 @@ export function OptionalClassFormField({
</FormFieldItem>
)}
/>
</FormProvider>
)}
)}
</FormProvider>
</Fragment>
);
}
@@ -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<string | undefined>(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 (
<FormProvider {...form}>
<TabsContent value="Custom">
+2
View File
@@ -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",
+2
View File
@@ -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 或类文件",