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 0106a9c49..6faa91091 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 @@ -38,6 +38,7 @@ import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester; 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.rabbitmq.RabbitMQCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.jenkins.JenkinsCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mysql.MysqlCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.ncrack.NcrackCredentialTester; @@ -69,6 +70,7 @@ protected void configurePlugin() { credentialTesterBinder.addBinding().to(PostgresCredentialTester.class); credentialTesterBinder.addBinding().to(WordpressCredentialTester.class); credentialTesterBinder.addBinding().to(GrafanaCredentialTester.class); + credentialTesterBinder.addBinding().to(RabbitMQCredentialTester.class); Multibinder credentialProviderBinder = Multibinder.newSetBinder(binder(), CredentialProvider.class); diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/RabbitMQCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/RabbitMQCredentialTester.java new file mode 100644 index 000000000..5351db8e6 --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/RabbitMQCredentialTester.java @@ -0,0 +1,208 @@ +/* + * 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.rabbitmq; + +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 static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.GoogleLogger; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.tsunami.common.data.NetworkEndpointUtils; +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.util.Base64; +import java.util.List; +import javax.inject.Inject; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +/** Credential tester for RabbitMQ Management Portal. */ +public final class RabbitMQCredentialTester extends CredentialTester { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private final HttpClient httpClient; + + private static final String RABBITMQ_SERVICE = "rabbitmq"; + private static final String RABBITMQ_PAGE_TITLE = "RabbitMQ Management"; + private static final String RABBITMQ_SERVER_HEADER = "Cowboy"; + private static final String RABBITMQ_WWW_HEADER = "Basic realm=\"RabbitMQ Management\""; + + @Inject + RabbitMQCredentialTester(HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient); + } + + @Override + public String name() { + return "RabbitMQCredentialTester"; + } + + @Override + public String description() { + return "RabbitMQ credential tester."; + } + + private static String buildTargetUrl(NetworkService networkService, String path) { + StringBuilder targetUrlBuilder = new StringBuilder(); + + if (NetworkServiceUtils.isWebService(networkService)) { + targetUrlBuilder.append(NetworkServiceUtils.buildWebApplicationRootUrl(networkService)); + + } else { + // Default to HTTP protocol when the scanner cannot identify the actual service. + targetUrlBuilder + .append("http://") + .append(NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint())) + .append("/"); + } + targetUrlBuilder.append(path); + return targetUrlBuilder.toString(); + } + + // Checks if the response body contains the title element of rabbitmq management page. + // Custom fingerprint phase. + private static boolean bodyContainsRabbitMQElements(String responseBody) { + Document doc = Jsoup.parse(responseBody); + String title = doc.title(); + + if (title.contains(RABBITMQ_PAGE_TITLE)) { + logger.atInfo().log( + "Found RabbitMQ Management endpoint (RABBITMQ_PAGE_TITLE string present in the page)"); + return true; + } else { + return false; + } + } + + /** + * 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 recognize a + * rabbitmq management instance correctly. + * + * @param networkService the network service passed by tsunami + * @return true if a rabbitmq management instance is recognized + */ + @Override + public boolean canAccept(NetworkService networkService) { + boolean canAcceptByNmapReport = + NetworkServiceUtils.getWebServiceName(networkService).equals(RABBITMQ_SERVICE); + if (canAcceptByNmapReport) { + return true; + } + boolean canAcceptByCustomFingerprint = false; + + String url = buildTargetUrl(networkService, ""); + try { + logger.atInfo().log("Probing RabbitMQ Management Portal - custom fingerprint phase"); + HttpResponse response = httpClient.send(get(url).withEmptyHeaders().build()); + canAcceptByCustomFingerprint = + response.status().isSuccess() + && response.headers().get("server").isPresent() + && response.headers().get("server").get().trim().equals(RABBITMQ_SERVER_HEADER) + && response + .bodyString() + .map(RabbitMQCredentialTester::bodyContainsRabbitMQElements) + .orElse(false); + url = buildTargetUrl(networkService, "api/overview"); + response = httpClient.send(get(url).withEmptyHeaders().build()); + canAcceptByCustomFingerprint = + canAcceptByCustomFingerprint + && response.headers().get("www-authenticate").isPresent() + && response.headers().get("www-authenticate").get().equals(RABBITMQ_WWW_HEADER); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", url); + return false; + } + return canAcceptByCustomFingerprint; + } + + @Override + public ImmutableList testValidCredentials( + NetworkService networkService, List credentials) { + + return credentials.stream() + .filter(cred -> isRabbitMQAccessible(networkService, cred)) + .collect(toImmutableList()); + } + + private boolean isRabbitMQAccessible(NetworkService networkService, TestCredential credential) { + var url = buildTargetUrl(networkService, "api/whoami"); + try { + logger.atInfo().log( + "url: %s, username: %s, password: %s", + url, credential.username(), credential.password().orElse("")); + HttpResponse response = sendRequestWithCredentials(url, credential); + + return response.status().isSuccess() + && response + .bodyString() + .map(RabbitMQCredentialTester::bodyContainsSuccessfulLoginElements) + .orElse(false); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", url); + return false; + } + } + + private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) + throws IOException { + + var headers = + HttpHeaders.builder() + .addHeader( + "Authorization", + "Basic " + + Base64.getEncoder() + .encodeToString( + (credential.username() + ":" + credential.password().orElse("")) + .getBytes(UTF_8))) + .build(); + + return httpClient.send(get(url).setHeaders(headers).build()); + } + + /** + * A successful authenticated request to the /api/whoami endpoint + * returns a JSON with at least the following keys: + * {"name":"username","tags":["roles"]} + */ + private static boolean bodyContainsSuccessfulLoginElements(String responseBody) { + try { + JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); + + if (response.has("name") && response.has("tags")) { + logger.atInfo().log("Successfully logged in to RabbitMQ Management Portal"); + return true; + } else { + return false; + } + } catch (Exception e) { + logger.atWarning().withCause(e).log( + "An error occurred while parsing the json response: %s", responseBody); + 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 9388d3748..35d38fddf 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 @@ -55,3 +55,8 @@ service_default_credentials { default_usernames: "admin" default_passwords: "admin" } +service_default_credentials { + service_name: "rabbitmq" + default_usernames: "guest" + default_passwords: "guest" +} diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/RabbitMQCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/RabbitMQCredentialTesterTest.java new file mode 100644 index 000000000..ca1b9fbe8 --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/RabbitMQCredentialTesterTest.java @@ -0,0 +1,164 @@ +/* + * 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.rabbitmq; + +import static com.google.common.base.Preconditions.checkNotNull; +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.common.io.BaseEncoding; +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.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.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 RabbitMQCredentialTester}. */ +@RunWith(JUnit4.class) +public class RabbitMQCredentialTesterTest { + @Inject private RabbitMQCredentialTester tester; + private MockWebServer mockWebServer; + + private static final TestCredential WEAK_CRED_1 = + TestCredential.create("user", Optional.of("1234")); + private static final TestCredential WEAK_CRED_2 = + TestCredential.create("root", Optional.of("pass")); + private static final TestCredential WRONG_CRED_1 = + TestCredential.create("wrong", Optional.of("pass")); + + private static final String WEAK_CRED_AUTH_1 = "Basic dXNlcjoxMjM0"; + private static final String WEAK_CRED_AUTH_2 = "Basic cm9vdDpwYXNz"; + private static final ServiceContext.Builder RABBITMQ_SERVICE_CONTEXT = + ServiceContext.newBuilder() + .setWebServiceContext( + WebServiceContext.newBuilder() + .setSoftware(Software.newBuilder().setName("rabbitmq"))); + + @Before + public void setup() { + mockWebServer = new MockWebServer(); + Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); + } + + @Test + public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { + startMockWebServer( + "/", + Resources.toString( + Resources.getResource(this.getClass(), "testdata/successfulAuthdResponse.json"), + UTF_8)); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .setServiceContext(RABBITMQ_SERVICE_CONTEXT) + .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( + "/", + Resources.toString( + Resources.getResource(this.getClass(), "testdata/successfulAuthdResponse.json"), + UTF_8)); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .setServiceContext(RABBITMQ_SERVICE_CONTEXT) + .build(); + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1, WEAK_CRED_2); + mockWebServer.shutdown(); + } + + @Test + public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { + startMockWebServer( + "/", + Resources.toString( + Resources.getResource(this.getClass(), "testdata/successfulAuthdResponse.json"), + UTF_8)); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .setServiceContext(RABBITMQ_SERVICE_CONTEXT) + .build(); + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) + .isEmpty(); + mockWebServer.shutdown(); + } + + private void startMockWebServer(String url, String response) throws IOException { + mockWebServer.setDispatcher(new RespondUserInfoResponseDispatcher(response)); + mockWebServer.start(); + mockWebServer.url(url); + } + + static final class RespondUserInfoResponseDispatcher extends Dispatcher { + private final String userInfoResponse; + + RespondUserInfoResponseDispatcher(String authenticatedUserResponse) { + this.userInfoResponse = checkNotNull(authenticatedUserResponse); + } + + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) { + var isUserEndpoint = recordedRequest.getPath().startsWith("/api/whoami"); + var authHeader = recordedRequest.getHeaders().get("Authorization").toString(); + var hasWeakCred1 = authHeader.contains(WEAK_CRED_AUTH_1); + var hasWeakCred2 = authHeader.contains(WEAK_CRED_AUTH_2); + + if (isUserEndpoint && (hasWeakCred1 || hasWeakCred2)) { + String username = BaseEncoding.base64().decode(authHeader).toString().split(":")[0]; + return new MockResponse() + .setResponseCode(HttpStatus.OK.code()) + .setBody(userInfoResponse.replace("", username)); + } + return new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code()); + } + } +} diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/resources/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/testdata/successfulAuthdResponse.json b/google/detectors/credentials/generic_weak_credential_detector/src/test/resources/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/testdata/successfulAuthdResponse.json new file mode 100644 index 000000000..56c74c66e --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/resources/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/rabbitmq/testdata/successfulAuthdResponse.json @@ -0,0 +1 @@ +{"name":"","tags":["administrator"]} \ No newline at end of file