Skip to content

Commit e9d93b2

Browse files
committed
Better Magento Detection
1 parent 8cd6764 commit e9d93b2

File tree

2 files changed

+65
-6
lines changed

2 files changed

+65
-6
lines changed

doyensec/detectors/magento_cosmicsting_xxe/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxe.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.common.collect.ImmutableList;
2525
import com.google.common.flogger.GoogleLogger;
2626
import com.google.common.util.concurrent.Uninterruptibles;
27+
import com.google.gson.JsonElement;
2728
import com.google.gson.JsonObject;
2829
import com.google.protobuf.ByteString;
2930
import com.google.protobuf.util.Timestamps;
@@ -122,6 +123,11 @@ public final class MagentoCosmicStingXxe implements VulnDetector {
122123
static final String VULNERABLE_ENDPOINT_PATH =
123124
"rest/all/V1/guest-carts/test-assetnote/estimate-shipping-methods";
124125

126+
@VisibleForTesting
127+
static final String CURRENCY_ENDPOINT_PATH = "rest/default/V1/directory/currency";
128+
129+
@VisibleForTesting static final String VERSION_ENDPOINT_PATH = "magento_version";
130+
125131
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
126132
private final Clock utcClock;
127133
private final HttpClient httpClient;
@@ -152,15 +158,59 @@ public DetectionReportList detect(
152158
.addAllDetectionReports(
153159
matchedServices.stream()
154160
.filter(NetworkServiceUtils::isWebService)
161+
.filter(this::isMagento)
155162
.filter(this::isServiceVulnerable)
156163
.map(networkService -> buildDetectionReport(targetInfo, networkService))
157164
.collect(toImmutableList()))
158165
.build();
159166
}
160167

168+
/*
169+
Check presence of endpoint with always anonymous access: /rest/default/V1/directory/currency
170+
From: https://developer.adobe.com/commerce/webapi/rest/use-rest/anonymous-api-security/
171+
172+
Typical response:
173+
HTTP/2 200 OK
174+
{
175+
"base_currency_code": "USD",
176+
"base_currency_symbol": "$",
177+
...
178+
}
179+
*/
180+
private boolean isMagento(NetworkService networkService) {
181+
String targetUri =
182+
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + CURRENCY_ENDPOINT_PATH;
183+
184+
HttpRequest req =
185+
HttpRequest.get(targetUri)
186+
.setHeaders(HttpHeaders.builder().addHeader("Accept", "application/json").build())
187+
.build();
188+
189+
HttpResponse response;
190+
try {
191+
response = this.httpClient.send(req, networkService);
192+
} catch (IOException e) {
193+
return false;
194+
}
195+
196+
// Check status code 200
197+
if (response.status() != HttpStatus.OK) return false;
198+
// Check if body is JSON
199+
if (response.bodyJson().isEmpty()) return false;
200+
JsonElement body = response.bodyJson().get();
201+
// Check if JSON body is object
202+
if (!body.isJsonObject()) return false;
203+
// If the body has a known key, e.g. "base_currency_code", it's Magento
204+
return body.getAsJsonObject().has("base_currency_code");
205+
}
206+
207+
/*
208+
Tries to get the Magento version by fetching /magento_version
209+
This endpoint can be manually disabled, so don't stop the plugin if we can't fetch it
210+
*/
161211
private String detectMagentoVersion(NetworkService networkService) {
162212
String targetUri =
163-
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "magento_version";
213+
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + VERSION_ENDPOINT_PATH;
164214
logger.atInfo().log("Trying to detect Magento version at '%s'", targetUri);
165215

166216
HttpRequest req = HttpRequest.get(targetUri).withEmptyHeaders().build();

doyensec/detectors/magento_cosmicsting_xxe/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202434102/MagentoCosmicStingXxeTest.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public final class MagentoCosmicStingXxeTest {
7474
private MockWebServer mockCallbackServer = new MockWebServer();
7575

7676
private static final String MOCK_MAGENTO_VERSION = "Magento/2.4 (Mock)";
77+
private static final String MOCK_CURRENCY_ENDPOINT_RESPONSE =
78+
"{\"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}]}";
7779
private static final String PATCHED_INSTANCE_RESPONSE = "{\"message\":\"Invalid data type\"}";
7880
private static final String VULNERABLE_INSTANCE_RESPONSE =
7981
"{\"message\":\"Internal Error. Details are available in Magento log file. Report ID:"
@@ -120,7 +122,7 @@ public void detect_whenVulnerableAndTcsAvailable_reportsCriticalVulnerability()
120122
DetectionReport expectedDetection =
121123
generateDetectionReportWithCallback(targetInfo, httpServices.get(0));
122124
assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection);
123-
assertThat(mockWebServer.getRequestCount()).isEqualTo(2);
125+
assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
124126
assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1);
125127
}
126128

@@ -140,7 +142,7 @@ public void detect_whenVulnerableAndTcsNotAvailable_reportsHighVulnerability()
140142
DetectionReport expectedDetection =
141143
generateDetectionReportWithResponseMatching(targetInfo, httpServices.get(0));
142144
assertThat(detectionReports.getDetectionReportsList()).containsExactly(expectedDetection);
143-
assertThat(mockWebServer.getRequestCount()).isEqualTo(2);
145+
assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
144146
assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0);
145147
}
146148

@@ -158,7 +160,7 @@ public void detect_whenNotVulnerableAndTcsAvailable_reportsNoVulnerability() thr
158160
DetectionReportList detectionReports = detector.detect(targetInfo, httpServices);
159161

160162
assertThat(detectionReports.getDetectionReportsList()).isEmpty();
161-
assertThat(mockWebServer.getRequestCount()).isEqualTo(2);
163+
assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
162164
assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1);
163165
}
164166

@@ -176,7 +178,7 @@ public void detect_whenNotVulnerableAndTcsNotAvailable_reportsNoVulnerability()
176178
DetectionReportList detectionReports = detector.detect(targetInfo, httpServices);
177179

178180
assertThat(detectionReports.getDetectionReportsList()).isEmpty();
179-
assertThat(mockWebServer.getRequestCount()).isEqualTo(2);
181+
assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
180182
assertThat(mockCallbackServer.getRequestCount()).isEqualTo(0);
181183
}
182184

@@ -254,11 +256,18 @@ static final class EndpointDispatcher extends Dispatcher {
254256
public MockResponse dispatch(RecordedRequest recordedRequest) {
255257

256258
if (recordedRequest.getMethod().equals("GET")
257-
&& recordedRequest.getPath().equals("/magento_version")) {
259+
&& recordedRequest.getPath().equals("/" + VERSION_ENDPOINT_PATH)) {
258260
// Version detection request
259261
return new MockResponse()
260262
.setResponseCode(HttpStatus.OK.code())
261263
.setBody(MOCK_MAGENTO_VERSION);
264+
} else if (recordedRequest.getMethod().equals("GET")
265+
&& recordedRequest.getPath().equals("/" + CURRENCY_ENDPOINT_PATH)) {
266+
// Magento identification request
267+
return new MockResponse()
268+
.setResponseCode(HttpStatus.OK.code())
269+
.setHeader("Content-Type", "application/json; charset=utf-8")
270+
.setBody(MOCK_CURRENCY_ENDPOINT_RESPONSE);
262271
} else if (recordedRequest.getMethod().equals("POST")
263272
&& recordedRequest.getPath().equals("/" + VULNERABLE_ENDPOINT_PATH)) {
264273
// Exploit attempt

0 commit comments

Comments
 (0)