diff --git a/google/detectors/exposedui/apache_flink/README.md b/google/detectors/exposedui/apache_flink/README.md deleted file mode 100644 index c0032dfe7..000000000 --- a/google/detectors/exposedui/apache_flink/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Exposed Apache Flink UI Detector - -This detector scans for exposed Apache Flink UI that unauthenticated -attackers can use as a target to submit malicious codes. - -## 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/exposedui/apache_flink/build.gradle b/google/detectors/exposedui/apache_flink/build.gradle deleted file mode 100644 index c68dbe53a..000000000 --- a/google/detectors/exposedui/apache_flink/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -plugins { - id 'java-library' -} - -description = 'Tsunami VulnDetector plugin for exposed Apache Flink UI.' -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.flogger:flogger:0.9" - implementation "com.google.flogger:google-extensions:0.9" - implementation "com.google.flogger:flogger-system-backend:0.9" - implementation "com.google.guava:guava:33.0.0-jre" - implementation "com.google.protobuf:protobuf-java:3.25.5" - implementation "com.google.protobuf:protobuf-javalite:3.25.5" - implementation "com.google.protobuf:protobuf-java-util:3.25.5" - 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}" } - } - implementation "javax.inject:javax.inject:1" - implementation "org.jsoup:jsoup:1.9.2" - - 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" - testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" - testImplementation "junit:junit:4.13.2" - testImplementation "org.mockito:mockito-core:5.18.0" -} diff --git a/google/detectors/exposedui/apache_flink/settings.gradle b/google/detectors/exposedui/apache_flink/settings.gradle deleted file mode 100644 index ea074c236..000000000 --- a/google/detectors/exposedui/apache_flink/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'apache_flink_exposed_ui_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/exposedui/apache_flink/src/main/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetector.java b/google/detectors/exposedui/apache_flink/src/main/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetector.java deleted file mode 100644 index 455df2a82..000000000 --- a/google/detectors/exposedui/apache_flink/src/main/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetector.java +++ /dev/null @@ -1,154 +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.exposedui.apacheflink; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.tsunami.common.net.http.HttpRequest.get; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.flogger.GoogleLogger; -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.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.ForWebService; -import com.google.tsunami.plugin.annotations.PluginInfo; -import com.google.tsunami.proto.DetectionReport; -import com.google.tsunami.proto.DetectionReportList; -import com.google.tsunami.proto.DetectionStatus; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.Severity; -import com.google.tsunami.proto.TargetInfo; -import com.google.tsunami.proto.Vulnerability; -import com.google.tsunami.proto.VulnerabilityId; -import java.io.IOException; -import java.time.Clock; -import java.time.Instant; -import javax.inject.Inject; -import org.jsoup.Jsoup; -import org.jsoup.select.Elements; - -/** A {@link VulnDetector} that detects unauthenticated Apache Flink. */ -@PluginInfo( - type = PluginType.VULN_DETECTION, - name = "ApacheFlinkExposedUiDetector", - version = "0.1", - description = - "This detector checks whether an unauthenticated Apache Flink UI instance is exposed to" - + " anonymous users from the job submission, /#/submit, endpoint. If exposed, any user" - + " can submit arbitrary jobs, which could result in remote code execution (RCE)", - author = "Tsunami Team (tsunami-dev@google.com)", - bootstrapModule = ApacheFlinkExposedUiDetectorBootstrapModule.class) -@ForWebService -public final class ApacheFlinkExposedUiDetector implements VulnDetector { - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - @VisibleForTesting - static final String FINDING_RECOMMENDATION_TEXT = - "Apache Flink Web UI does not support access control out of the box. Even if the feature to" - + " upload jobs in the Apached Flink Web UI is disabled, the session cluster could still" - + " accept job submission requests via REST calls. Per Apache Flink documentation, we" - + " recommend adding authentication using a REST proxy. See" - + " https://nightlies.apache.org/flink/flink-docs-master/docs/deployment/security/security-ssl/#external--rest-connectivity" - + " and https://github.com/ing-bank/flink-deployer#authentication."; - - private final Clock utcClock; - private final HttpClient httpClient; - - @Inject - ApacheFlinkExposedUiDetector(@UtcClock Clock utcClock, HttpClient httpClient) { - this.utcClock = checkNotNull(utcClock); - this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(false).build(); - } - - @Override - public ImmutableList getAdvisories() { - return ImmutableList.of( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher("GOOGLE") - .setValue("APACHE_FLINK_EXPOSED_UI")) - .setSeverity(Severity.CRITICAL) - .setTitle("Apache Flink Exposed Ui") - .setDescription("Apache Flink is not password or token protected") - .setRecommendation(FINDING_RECOMMENDATION_TEXT) - .build()); - } - - @Override - public DetectionReportList detect( - TargetInfo targetInfo, ImmutableList matchedServices) { - logger.atInfo().log("ApacheFlinkExposedUiDetector starts detecting."); - - return DetectionReportList.newBuilder() - .addAllDetectionReports( - matchedServices.stream() - .filter(NetworkServiceUtils::isWebService) - .filter(this::isServiceVulnerable) - .map(networkService -> buildDetectionReport(targetInfo, networkService)) - .collect(toImmutableList())) - .build(); - } - - private boolean isServiceVulnerable(NetworkService networkService) { - String targetUri = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "#/submit"; - try { - HttpResponse response = - httpClient.send(get(targetUri).withEmptyHeaders().build(), networkService); - return response.status().isSuccess() - && response - .bodyString() - .map(body -> body.contains("Apache Flink Web") && bodyContainsFlinkSubmit(body)) - // orElse fix operand types error as bodyString() type is Optional - .orElse(false); - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", targetUri); - return false; - } - } - - private static boolean bodyContainsFlinkSubmit(String responseBody) { - // An unauthenticated Apache Flink UI instance will display a flink-submit custom element - // enabling anonymous users to submit malicious codes from the /#/submit endpoint. - Elements flinkSubmit = Jsoup.parse(responseBody).select("flink-submit"); - if (flinkSubmit.isEmpty()) { - logger.atInfo().log("Apache Flink UI does not allow creating new jobs as anonymous user."); - return false; - } else { - logger.atInfo().log( - "Apache Flink UI allows creating new jobs as anonymous user, enabling" - + " remote code execution!"); - return true; - } - } - - private DetectionReport buildDetectionReport( - TargetInfo scannedTarget, NetworkService vulnerableNetworkService) { - return DetectionReport.newBuilder() - .setTargetInfo(scannedTarget) - .setNetworkService(vulnerableNetworkService) - .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability(this.getAdvisories().get(0)) - .build(); - } -} diff --git a/google/detectors/exposedui/apache_flink/src/main/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetectorBootstrapModule.java b/google/detectors/exposedui/apache_flink/src/main/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetectorBootstrapModule.java deleted file mode 100644 index bd7edacd1..000000000 --- a/google/detectors/exposedui/apache_flink/src/main/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetectorBootstrapModule.java +++ /dev/null @@ -1,27 +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.exposedui.apacheflink; - -import com.google.tsunami.plugin.PluginBootstrapModule; - -/** A {@link PluginBootstrapModule} for {@link ApacheFlinkExposedUiDetector}. */ -public final class ApacheFlinkExposedUiDetectorBootstrapModule extends PluginBootstrapModule { - - @Override - protected void configurePlugin() { - registerPlugin(ApacheFlinkExposedUiDetector.class); - } -} diff --git a/google/detectors/exposedui/apache_flink/src/test/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetectorTest.java b/google/detectors/exposedui/apache_flink/src/test/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetectorTest.java deleted file mode 100644 index 302364ca6..000000000 --- a/google/detectors/exposedui/apache_flink/src/test/java/com/google/tsunami/plugins/detectors/exposedui/apacheflink/ApacheFlinkExposedUiDetectorTest.java +++ /dev/null @@ -1,175 +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.exposedui.apacheflink; - -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; - -import com.google.common.collect.ImmutableList; -import com.google.inject.Guice; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.net.http.HttpClientModule; -import com.google.tsunami.common.net.http.HttpStatus; -import com.google.tsunami.common.time.testing.FakeUtcClock; -import com.google.tsunami.common.time.testing.FakeUtcClockModule; -import com.google.tsunami.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.Software; -import com.google.tsunami.proto.TargetInfo; -import com.google.tsunami.proto.TransportProtocol; -import java.io.IOException; -import java.time.Instant; -import javax.inject.Inject; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -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 ApacheFlinkExposedUiDetector}. */ -@RunWith(JUnit4.class) -public final class ApacheFlinkExposedUiDetectorTest { - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); - - private MockWebServer mockWebServer; - - @Inject private ApacheFlinkExposedUiDetector detector; - - @Before - public void setUp() { - mockWebServer = new MockWebServer(); - - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - new ApacheFlinkExposedUiDetectorBootstrapModule()) - .injectMembers(this); - } - - @After - public void tearDown() throws IOException { - mockWebServer.shutdown(); - } - - @Test - public void detect_whenApacheFlinkDoesNotRequireAuthentication_reportsVuln() throws IOException { - startMockWebServer("/#/submit", HttpStatus.OK.code(), fakeApacheFlinkPage()); - ImmutableList httpServices = createService("Apache Flink"); - - assertThat( - detector - .detect(buildTargetInfo(forHostname(mockWebServer.getHostName())), httpServices) - .getDetectionReportsList()) - .containsExactly( - DetectionReport.newBuilder() - .setTargetInfo(buildTargetInfo(forHostname(mockWebServer.getHostName()))) - .setNetworkService(httpServices.get(0)) - .setDetectionTimestamp( - Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability(detector.getAdvisories().get(0)) - .build()); - } - - @Test - public void detect_whenApacheFlinkRedirectsToLoginPage_doesNotReportVuln() throws IOException { - startMockWebServer("/#/submit", HttpStatus.FOUND.code(), "Fake Apache Flink login page."); - ImmutableList httpServices = createService("Apache Flink"); - assertThat( - detector - .detect(buildTargetInfo(forHostname(mockWebServer.getHostName())), httpServices) - .getDetectionReportsList()) - .isEmpty(); - } - - @Test - public void detect_whenApacheFlinkDisablesSubmitFeature_doesNotReportVuln() throws IOException { - startMockWebServer("/#/submit", HttpStatus.OK.code(), "Apache Flink Web"); - ImmutableList httpServices = createService("Apache Flink"); - assertThat( - detector - .detect(buildTargetInfo(forHostname(mockWebServer.getHostName())), httpServices) - .getDetectionReportsList()) - .isEmpty(); - } - - @Test - public void detect_whenNotApacheFlink_ignoresServices() throws IOException { - startMockWebServer("/#/submit", HttpStatus.OK.code(), "I am Jenkins"); - ImmutableList httpServices = createService("Jenkins"); - assertThat( - detector - .detect(buildTargetInfo(forHostname(mockWebServer.getHostName())), httpServices) - .getDetectionReportsList()) - .isEmpty(); - } - - @Test - public void detect_whenNonHttpNetworkService_ignoresServices() { - ImmutableList nonHttpServices = - ImmutableList.of( - NetworkService.newBuilder().setServiceName("ssh").build(), - NetworkService.newBuilder().setServiceName("rdp").build()); - assertThat( - detector - .detect(buildTargetInfo(forHostname(mockWebServer.getHostName())), nonHttpServices) - .getDetectionReportsList()) - .isEmpty(); - } - - @Test - public void detect_whenEmptyNetworkService_generatesEmptyDetectionReports() { - assertThat( - detector - .detect( - buildTargetInfo(forHostname(mockWebServer.getHostName())), ImmutableList.of()) - .getDetectionReportsList()) - .isEmpty(); - } - - private ImmutableList createService(String serviceName) { - return ImmutableList.of( - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setTransportProtocol(TransportProtocol.TCP) - .setSoftware(Software.newBuilder().setName(serviceName)) - .setServiceName("http") - .build()); - } - - private void startMockWebServer(String url, int responseCode, String response) - throws IOException { - mockWebServer.enqueue(new MockResponse().setResponseCode(responseCode).setBody(response)); - mockWebServer.start(); - mockWebServer.url(url); - } - - private String fakeApacheFlinkPage() { - return "Apache Flink Web Dashboard" - + ""; - } - - private static TargetInfo buildTargetInfo(NetworkEndpoint networkEndpoint) { - return TargetInfo.newBuilder().addNetworkEndpoints(networkEndpoint).build(); - } -} diff --git a/templated/templateddetector/plugins/exposedui/ApacheFlink_ExposedUI.textproto b/templated/templateddetector/plugins/exposedui/ApacheFlink_ExposedUI.textproto new file mode 100644 index 000000000..78bf3670c --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/ApacheFlink_ExposedUI.textproto @@ -0,0 +1,118 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "ApacheFlink_ExposedUI" + author: "Robert Dick (robert@doyensec.com) templated version " + "(containing some modifications from the original Java version), " + "Tsunami Team (tsunami-dev@google.com) for original Java version. " + + version: "0.2" +} + +finding: { + main_id: { + publisher: "GOOGLE" + value: "APACHE_FLINK_EXPOSED_UI" + } + title: "Exposed Apache Flink instance" + description: "Apache Flink is not password or token protected." + recommendation: + "Apache Flink Web UI does not support access control out of the box. Even if the feature to" + " upload jobs in the Apached Flink Web UI is disabled, the session cluster could still" + " accept job submission requests via REST calls. Per Apache Flink documentation, we" + " recommend adding authentication using a REST proxy. See" + " https://nightlies.apache.org/flink/flink-docs-master/docs/deployment/security/security-ssl/#external--rest-connectivity" + " and https://github.com/ing-bank/flink-deployer#authentication." + severity: CRITICAL +} + +########### +# ACTIONS # +########### + +# Performs fingerprinting for Apache Flink before attempting the JAR upload + +actions: { + name: "fingerprint_flink" + http_request: { + method: GET + uri: "/" + response: { + http_status: 200 + expect_all: { + conditions: [ + { body: {} contains: "Apache Flink Web" }, + { body: {} contains: "" + }, + { + uri: "/jars/upload" + status: 200 + body_content: "{\"filename\":" + "\"/tmp/flink-web-73493f68-69aa-49ca-a462-378fde7e3bc4/flink-web-upload/" + "ecb336e5-66d6-440b-9d5b-ea50946c3811_myfile.jar\"," + "\"status\":\"success\"}" + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "{}" + } + ] + } +} + +tests: { + name: "whenNotApacheFlink_returnsNoVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "not flink" + } + ] + } +} + +tests: { + name: "whenFailedUpload_returnsNoVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: "... Apache Flink Web ... " + }, + { + uri: "/jars/upload" + status: 401 + body_content: "Requires Authentication" + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "Anything here..." + } + ] + } +}