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 269df1022..1b76b8323 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 @@ -37,15 +37,17 @@ 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.axis2.Axis2CredentialTester; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.actifio.ActifioCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airbyte.AirbyteCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airflow.AirflowCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.argocd.ArgoCdCredentialTester; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.axis2.Axis2CredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.grafana.GrafanaCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.hive.HiveCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.hydra.HydraCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.jenkins.JenkinsCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.kubeflow.KubeflowCredentialTester; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.litmus.LitmusCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow.MlFlowCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mysql.MysqlCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.ncrack.NcrackCredentialTester; @@ -56,7 +58,6 @@ import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.tomcat.TomcatHttpCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.wordpress.WordpressCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.zenml.ZenMlCredentialTester; -import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.actifio.ActifioCredentialTester; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; @@ -95,6 +96,7 @@ protected void configurePlugin() { credentialTesterBinder.addBinding().to(WordpressCredentialTester.class); credentialTesterBinder.addBinding().to(ZenMlCredentialTester.class); credentialTesterBinder.addBinding().to(Axis2CredentialTester.class); + credentialTesterBinder.addBinding().to(LitmusCredentialTester.class); credentialTesterBinder.addBinding().to(ActifioCredentialTester.class); Multibinder credentialProviderBinder = diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/litmus/LitmusCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/litmus/LitmusCredentialTester.java new file mode 100644 index 000000000..0de5b30f6 --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/litmus/LitmusCredentialTester.java @@ -0,0 +1,212 @@ +/* + * 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.litmus; + +import static com.google.common.base.Preconditions.checkNotNull; +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; +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.common.net.http.HttpStatus; +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.util.List; +import java.util.Optional; +import javax.inject.Inject; + +/** Credential tester specifically for Litmus Chaos Center. */ +public final class LitmusCredentialTester extends CredentialTester { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private static final String LITMUS_SERVICE = "litmus"; + + private final HttpClient httpClient; + private boolean addCredentials = false; + + @Inject + LitmusCredentialTester(HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient); + } + + @Override + public String name() { + return "LitmusCredentialTester"; + } + + @Override + public String description() { + return "Litmus Chaos Center credential tester."; + } + + /** + * Determines if this tester can accept the {@link NetworkService} based on the name of the + * service or a custom fingerprint. The fingerprint is necessary since nmap doesn't always + * recognize a Litmus Chaos Center instance correctly. + * + * @param networkService the network service passed by tsunami + * @return true if a Litmus Chaos Center instance is recognized + */ + @Override + public boolean canAccept(NetworkService networkService) { + boolean canAcceptByNmapReport = + NetworkServiceUtils.getWebServiceName(networkService).equals(LITMUS_SERVICE); + + if (canAcceptByNmapReport) { + return true; + } + + if (!NetworkServiceUtils.isWebService(networkService)) { + return false; + } + + return isLitmusService(networkService); + } + + /** Custom fingerprinting to detect Litmus Chaos Center by checking for well-known endpoints. */ + private boolean isLitmusService(NetworkService networkService) { + String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); + + // Check for Litmus Chaos Center's login endpoint + String[] fingerprintPaths = {"auth/login", "login"}; + + for (String path : fingerprintPaths) { + try { + HttpResponse response = + httpClient.send(get(rootUrl + path).withEmptyHeaders().build(), networkService); + + if (response.status().isSuccess() && response.bodyString().isPresent()) { + String body = response.bodyString().get(); + // Check for Litmus-specific markers in the page + if (body.contains("litmus") || body.contains("Litmus") || body.contains("chaos")) { + logger.atInfo().log( + "Detected Litmus Chaos Center instance via custom fingerprinting at %s", rootUrl); + addCredentials = true; + return true; + } + } + } catch (IOException e) { + logger.atWarning().withCause(e).log( + "Unable to fingerprint Litmus at '%s'.", rootUrl + path); + } + } + + return false; + } + + @Override + public boolean batched() { + return false; + } + + @Override + public ImmutableList testValidCredentials( + NetworkService networkService, List credentials) { + ImmutableList allCredentials; + + if (addCredentials) { + // Custom fingerprinting detected Litmus (not Nmap), so credentials were fetched for + // generic service type (e.g., "http"). Add Litmus-specific default credentials. + // TODO: remove this once fingerprinting is updated and made more robust + + allCredentials = + ImmutableList.builder() + .add(TestCredential.create("admin", Optional.of("litmus"))) + .addAll(credentials) + .build(); + + logger.atInfo().log( + "Custom fingerprinting detected Litmus Chaos Center - testing %d credentials (1 defaults" + + " + %d provided)", + allCredentials.size(), credentials.size()); + } else { + // Nmap detected Litmus, use credentials from providers only + allCredentials = ImmutableList.copyOf(credentials); + } + + // Always return 1st weak credential to gracefully handle no auth configured case + return allCredentials.stream() + .filter(cred -> isLitmusAccessible(networkService, cred)) + .findFirst() + .map(ImmutableList::of) + .orElseGet(ImmutableList::of); + } + + private boolean isLitmusAccessible(NetworkService networkService, TestCredential credential) { + String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); + + // Litmus Chaos Center auth endpoint + String loginPath = "auth/login"; + String loginUrl = rootUrl + loginPath; + + try { + // Litmus uses JSON authentication with username and password + String jsonBody = + String.format( + "{\"username\":\"%s\",\"password\":\"%s\"}", + credential.username(), credential.password().orElse("")); + + logger.atInfo().log( + "Testing Litmus Chaos Center credentials - URL: %s, Username: %s", + loginUrl, credential.username()); + + HttpResponse response = + httpClient.send( + post(loginUrl) + .setHeaders( + HttpHeaders.builder() + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", "Bearer null") + .build()) + .setRequestBody(ByteString.copyFromUtf8(jsonBody)) + .build(), + networkService); + + // Successful authentication returns 200 with JSON containing accessToken + if (response.status() == HttpStatus.OK) { + String body = response.bodyString().orElse(""); + if (body.contains("accessToken")) { + logger.atInfo().log( + "Successfully authenticated to Litmus Chaos Center with credentials: %s:%s", + credential.username(), credential.password().orElse("")); + return true; + } + } + + // Check for invalid credentials error + String body = response.bodyString().orElse(""); + if (body.contains("invalid_credentials")) { + logger.atInfo().log( + "Authentication failed - Invalid credentials for user: %s", credential.username()); + } else { + logger.atInfo().log( + "Authentication failed - Status: %s, Response: %s", + response.status().code(), body.isEmpty() ? "(empty response)" : body); + } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", loginUrl); + } + + return false; + } +} 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 4eebab55d..94642ae14 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 @@ -114,8 +114,14 @@ service_default_credentials { default_passwords: "your_new_password_here" } +service_default_credentials { + service_name: "litmus" + default_usernames: "admin" + default_passwords: "litmus" +} + service_default_credentials { service_name: "actifio" default_usernames: "admin" default_passwords: "password" -} \ No newline at end of file +} diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/litmus/LitmusCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/litmus/LitmusCredentialTesterTest.java new file mode 100644 index 000000000..c3e3d6640 --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/litmus/LitmusCredentialTesterTest.java @@ -0,0 +1,163 @@ +/* + * 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.credentials.genericweakcredentialdetector.testers.litmus; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Guice; +import com.google.tsunami.common.net.http.HttpClientModule; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; +import com.google.tsunami.proto.NetworkService; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +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.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link LitmusCredentialTester}. */ +@RunWith(JUnit4.class) +public class LitmusCredentialTesterTest { + @Rule public MockitoRule rule = MockitoJUnit.rule(); + @Inject private LitmusCredentialTester tester; + private MockWebServer mockWebServer; + private static final TestCredential WEAK_CRED_1 = + TestCredential.create("admin", Optional.of("litmus")); + private static final TestCredential WRONG_CRED_1 = + TestCredential.create("wrong", Optional.of("wrong")); + + // The default username and password for Litmus Chaos Center + private static final String DEFAULT_USERNAME = "admin"; + private static final String DEFAULT_PASSWORD = "litmus"; + + @Before + public void setup() { + mockWebServer = new MockWebServer(); + Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); + } + + @Test + public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("litmus") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("litmus") + .build(); + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) + .isEmpty(); + } + + @Test + public void detect_litmusService_canAccept() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("litmus") + .build(); + + assertThat(tester.canAccept(targetNetworkService)).isTrue(); + } + + private void startMockWebServer() throws IOException { + final Dispatcher dispatcher = + new Dispatcher() { + final MockResponse invalidCredentialsResponse = + new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody( + "{\"error\":\"invalid_credentials\"," + + "\"errorDescription\":\"Invalid Credentials\"}"); + + @Override + public MockResponse dispatch(RecordedRequest request) { + // Handle Litmus Chaos Center login endpoint + if (request.getPath().startsWith("/auth/login") + && Objects.equals(request.getMethod(), "POST")) { + String body = request.getBody().readString(StandardCharsets.UTF_8); + + // Check if credentials match admin:litmus + if (body.contains("\"username\":\"" + DEFAULT_USERNAME + "\"") + && body.contains("\"password\":\"" + DEFAULT_PASSWORD + "\"")) { + // Return success response with access token + return new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody( + "{\"accessToken\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjUzODQzNTMsInJvbGUiOiJhZG1pbiIsInVpZCI6ImFmYjFjOThmLWQwODctNDBmZC04ZDgwLWM3YTE1NTA2Mjk5MCIsInVzZXJuYW1lIjoiYWRtaW4ifQ.LtRyawFb3aCozqJoy0WDvMIOAlfZXdjiSnmuHKQCcbKC9b-7I70sBaQRJlCab47Wm-2wa_HWdtxpJHviSxk3NA\"," + + "\"expiresIn\":86400,\"projectID\":\"\",\"projectRole\":\"Owner\"," + + "\"type\":\"Bearer\"}"); + } else { + // Return invalid credentials error + return invalidCredentialsResponse; + } + } + + // Handle login page GET request for fingerprinting + if (request.getPath().startsWith("/login") + && Objects.equals(request.getMethod(), "GET")) { + return new MockResponse() + .setResponseCode(200) + .setBody( + "\n" + + "\n" + + " Litmus Chaos Center\n" + + " \n" + + "

Welcome to Litmus Chaos Center

\n" + + " \n" + + ""); + } + + // Default response for unmatched requests + return new MockResponse().setResponseCode(404).setBody("Not Found"); + } + }; + mockWebServer.setDispatcher(dispatcher); + mockWebServer.start(); + mockWebServer.url("/"); + } +}