From 00674f5b8f750f382cfa429a261480b03b401b00 Mon Sep 17 00:00:00 2001 From: joernNNN Date: Wed, 14 Aug 2024 07:45:55 +0200 Subject: [PATCH 1/3] add new weak credential tester module for apache airflow --- ...WeakCredentialDetectorBootstrapModule.java | 2 + .../airflow/AirflowCredentialTester.java | 170 ++++++++++++++++++ .../service_default_credentials.textproto | 6 + .../airflow/AirflowCredentialTesterTest.java | 153 ++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java create mode 100644 google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java index db865a24a..dcbf4e0cd 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java @@ -36,6 +36,7 @@ import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.DefaultCredentials; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.Top100Passwords; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airflow.AirflowCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.grafana.GrafanaCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.hydra.HydraCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.jenkins.JenkinsCredentialTester; @@ -67,6 +68,7 @@ protected void configurePlugin() { Multibinder credentialTesterBinder = Multibinder.newSetBinder(binder(), CredentialTester.class); + credentialTesterBinder.addBinding().to(AirflowCredentialTester.class); credentialTesterBinder.addBinding().to(JenkinsCredentialTester.class); credentialTesterBinder.addBinding().to(MlFlowCredentialTester.class); credentialTesterBinder.addBinding().to(MysqlCredentialTester.class); diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java new file mode 100644 index 000000000..4da8d3284 --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java @@ -0,0 +1,170 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airflow; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.tsunami.common.net.http.HttpRequest.post; +import static com.google.tsunami.common.net.http.HttpRequest.get; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.GoogleLogger; +import com.google.protobuf.ByteString; +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.HttpResponse; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester; +import com.google.tsunami.proto.NetworkService; +import java.io.IOException; +import java.net.HttpCookie; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; + +/** Credential tester specifically for airflow. */ +public final class AirflowCredentialTester extends CredentialTester { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private final HttpClient httpClient; + + private static final String AIRFLOW_SERVICE = "airflow"; + private static final Pattern CSRF_PATTERN = + Pattern.compile("(var CSRF = |var csrfToken = )[\"']([\\w-.]+)[\"']"); + + @Inject + AirflowCredentialTester(HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(false).build(); + } + + @Override + public String name() { + return "AirflowCredentialTester"; + } + + @Override + public String description() { + return "Airflow credential tester."; + } + + @Override + public boolean canAccept(NetworkService networkService) { + return NetworkServiceUtils.getWebServiceName(networkService).equals(AIRFLOW_SERVICE); + } + + @Override + public boolean batched() { + return true; + } + + @Override + public ImmutableList testValidCredentials( + NetworkService networkService, List credentials) { + // Always return 1st weak credential to gracefully handle no auth configured case, where we + // return empty credential instead of all the weak credentials + return credentials.stream() + .filter(cred -> isAirflowAccessible(networkService, cred)) + .findFirst() + .map(ImmutableList::of) + .orElseGet(ImmutableList::of); + } + + private boolean isAirflowAccessible(NetworkService networkService, TestCredential credential) { + // sending the first request to retrieve a valid CSRF token and a valid session cookie + Map results = getFreshCsrfTokenAndSessionCookie(networkService); + if (results == null) { + return false; + } + String freshSessionCookieValue = results.get("freshSessionCookieValue"); + String freshCsrfToken = results.get("freshCsrfToken"); + + String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); + String loginUrl = rootUrl + "login/"; + try { + logger.atInfo().log( + "url: %s, username: %s, password: %s", + loginUrl, credential.username(), credential.password().orElse("")); + HttpResponse response = + this.httpClient.send( + post(loginUrl) + .setHeaders( + HttpHeaders.builder() + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .addHeader("Cookie", String.format("session=%s", freshSessionCookieValue)) + .build()) + .setRequestBody( + ByteString.copyFrom( + String.format( + "csrf_token=%s&username=%s&password=%s", + freshCsrfToken, + credential.username(), + credential.password().orElse("")), + StandardCharsets.UTF_8)) + .build(), + networkService); + return response.status().isRedirect() + && response.headers().get("Location").isPresent() + && response.headers().get("Location").get().equals("/home") + && response.headers().get("Set-Cookie").isPresent() + && response.headers().get("Set-Cookie").get().contains("session="); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", loginUrl); + return false; + } + } + + private Map getFreshCsrfTokenAndSessionCookie(NetworkService networkService) { + String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); + Map results = new HashMap<>(); + + HttpResponse firstResponse; + try { + firstResponse = + this.httpClient.send(get(rootUrl + "login/").withEmptyHeaders().build(), networkService); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Failed to send request."); + return null; + } + + if (firstResponse.bodyString().isEmpty() + || firstResponse.headers().get("Set-Cookie").isEmpty()) { + return null; + } + List parsedCookies = + HttpCookie.parse(firstResponse.headers().get("Set-Cookie").get()); + String freshSessionCookieValue = null; + for (HttpCookie cookie : parsedCookies) { + if (cookie.getName().equals("session")) { + freshSessionCookieValue = cookie.getValue(); + } + } + if (freshSessionCookieValue == null) { + return null; + } + results.put("freshSessionCookieValue", freshSessionCookieValue); + + Matcher m = CSRF_PATTERN.matcher(firstResponse.bodyString().get()); + if (!m.find()) { + return null; + } + String freshCsrfToken = m.group(2); + results.put("freshCsrfToken", freshCsrfToken); + return results; + } +} diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto b/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto index 653b4e5ae..d1e786c73 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto @@ -83,3 +83,9 @@ service_default_credentials { default_usernames: "default" default_passwords: "" } + +service_default_credentials { + service_name: "airflow" + default_usernames: "airflow" + default_passwords: "airflow" +} diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java new file mode 100644 index 000000000..297236f81 --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airflow; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; +import static java.nio.charset.StandardCharsets.UTF_8; + +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.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; +import com.google.tsunami.proto.NetworkService; +import com.google.tsunami.proto.ServiceContext; +import com.google.tsunami.proto.Software; +import com.google.tsunami.proto.WebServiceContext; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Optional; +import javax.inject.Inject; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AirflowCredentialTester}. */ +@RunWith(JUnit4.class) +public class AirflowCredentialTesterTest { + @Inject private AirflowCredentialTester tester; + private MockWebServer mockWebServer; + private static final TestCredential WEAK_CRED_1 = + TestCredential.create("airflow", Optional.of("airflow")); + private static final TestCredential WRONG_CRED_1 = + TestCredential.create("admin", Optional.of("admin")); + private static final ServiceContext.Builder airflowServiceContext = + ServiceContext.newBuilder() + .setWebServiceContext( + WebServiceContext.newBuilder().setSoftware(Software.newBuilder().setName("airflow"))); + + @Before + public void setup() { + mockWebServer = new MockWebServer(); + Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); + } + + @Test + public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .setServiceContext(airflowServiceContext) + .setSoftware(Software.newBuilder().setName("http")) + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .containsExactly(WEAK_CRED_1); + mockWebServer.shutdown(); + } + + @Test + public void detect_weakCredentialsExist_returnsAllWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .setServiceContext(airflowServiceContext) + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .containsExactly(WEAK_CRED_1); + + mockWebServer.shutdown(); + } + + @Test + public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .setServiceContext(airflowServiceContext) + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) + .isEmpty(); + + mockWebServer.shutdown(); + } + + private void startMockWebServer() throws IOException { + mockWebServer.setDispatcher(new AirflowDispatcher()); + mockWebServer.start(); + mockWebServer.url("/"); + } + + static final class AirflowDispatcher extends Dispatcher { + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) { + + if (recordedRequest.getPath().startsWith("/login/") + && recordedRequest.getMethod().equals("POST") + && recordedRequest.getHeader("Content-Type").equals("application/x-www-form-urlencoded") + && recordedRequest.getHeader("Cookie").equals("session=aCookie")) { + ByteArrayOutputStream body = new ByteArrayOutputStream(); + try { + recordedRequest.getBody().writeTo(body); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (body.toString(UTF_8).contains("csrf_token=a.CSRF.Token") + && body.toString(UTF_8).contains("username=airflow") + && body.toString(UTF_8).contains("password=airflow")) { + return new MockResponse() + .setResponseCode(302) + .setHeader("Location", "/home") + .setHeader("Set-Cookie", "session=someCookies"); + } + } else if (recordedRequest.getPath().startsWith("/login/") + && recordedRequest.getMethod().equals("GET")) { + return new MockResponse() + .setHeader("Set-Cookie", "session=aCookie") + .setHeader("Content-Type", "application/x-www-form-urlencoded") + .setBody("var csrfToken = 'a.CSRF.Token';"); + } + return new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code()); + } + } +} From 0d65bc87bc22d242e0dd7bf85106e7a184b4ebb2 Mon Sep 17 00:00:00 2001 From: joernNNN Date: Wed, 14 Aug 2024 07:48:59 +0200 Subject: [PATCH 2/3] remove a logging line --- .../testers/airflow/AirflowCredentialTester.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java index 4da8d3284..f1ba5bc50 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java @@ -97,9 +97,6 @@ private boolean isAirflowAccessible(NetworkService networkService, TestCredentia String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); String loginUrl = rootUrl + "login/"; try { - logger.atInfo().log( - "url: %s, username: %s, password: %s", - loginUrl, credential.username(), credential.password().orElse("")); HttpResponse response = this.httpClient.send( post(loginUrl) From 7559e6bb66f184145ca439ac2b4e93fb3b241e9f Mon Sep 17 00:00:00 2001 From: joernNNN Date: Tue, 3 Dec 2024 15:40:26 +0100 Subject: [PATCH 3/3] change order of imports, add a space before package statements --- .../testers/airflow/AirflowCredentialTester.java | 3 ++- .../testers/airflow/AirflowCredentialTesterTest.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java index f1ba5bc50..22f681ae0 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTester.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airflow; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.tsunami.common.net.http.HttpRequest.post; import static com.google.tsunami.common.net.http.HttpRequest.get; +import static com.google.tsunami.common.net.http.HttpRequest.post; import com.google.common.collect.ImmutableList; import com.google.common.flogger.GoogleLogger; diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java index 297236f81..aa996fbd9 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/airflow/AirflowCredentialTesterTest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airflow; import static com.google.common.truth.Truth.assertThat;