Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apache Axis2 weak credential tester #578

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,19 @@
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.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.mlflow.MlFlowCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mysql.MysqlCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.hive.HiveCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.ncrack.NcrackCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.postgres.PostgresCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.rabbitmq.RabbitMQCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.wordpress.WordpressCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.rstudio.RStudioCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.wordpress.WordpressCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.zenml.ZenMlCredentialTester;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -80,6 +80,7 @@ protected void configurePlugin() {
credentialTesterBinder.addBinding().to(RStudioCredentialTester.class);
credentialTesterBinder.addBinding().to(RabbitMQCredentialTester.class);
credentialTesterBinder.addBinding().to(ZenMlCredentialTester.class);
credentialTesterBinder.addBinding().to(Axis2CredentialTester.class);

Multibinder<CredentialProvider> credentialProviderBinder =
Multibinder.newSetBinder(binder(), CredentialProvider.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* 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.axis2;

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.base.Ascii;
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.util.List;
import java.util.Optional;
import javax.inject.Inject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

/** Credential tester specifically for Apache Axis2 Administration Panel. */
public final class Axis2CredentialTester extends CredentialTester {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final HttpClient httpClient;

private static final String AXIS_PAGE_TITLE = "axis 2 - home";
private static final String AXIS_LOGIN_TITLE = "<title>axis2 :: administration page</title>";
private static final String AXIS_USERNAME = "admin";
private static final String AXIS_PASSWORD = "axis2";
Comment on lines +47 to +48
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized I didn't include this in the review while transposing my notes. Is there a specific reason why the default credentials aren’t in the appropriate proto file? If possible, please move them to:

google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto.

Thanks!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @giacomo-doyensec thanks for your feedback, I'll resolve them as suggested!

Default credentials of Axis2 have been inserted within the class since nmap identifies the service as "http", so in order to not test such credentials against each http service we decided to add the credentials inside the detector.

Following I've inserted a screenshot of nmap discovery in order to show the issue:
screenshot_2025-02-11_11-43

Do you prefer to add the credential in the proto file or we can leave it here and add a comment in order to explain why the default credentials are inserted inside the detector instead of the proto file?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, I think a comment will suffice. Thank you!


@Inject
Axis2CredentialTester(HttpClient httpClient) {
this.httpClient = checkNotNull(httpClient);
}

@Override
public String name() {
return "Axis2CredentialTester";
}

@Override
public String description() {
return "Apache Axis2 Administration Panel credential tester.";
}

@Override
public boolean batched() {
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
* Axis2 instance correctly.
*
* @param networkService the network service passed by tsunami
* @return true if a axis2 instance is recognized
*/
@Override
public boolean canAccept(NetworkService networkService) {
boolean isWebService = NetworkServiceUtils.isWebService(networkService);

if (!isWebService) {
return false;
}
Comment on lines +80 to +84
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function can be simplified maintaining the same logic and a good readability

Suggested change
boolean isWebService = NetworkServiceUtils.isWebService(networkService);
if (!isWebService) {
return false;
}
if (!NetworkServiceUtils.isWebService(networkService)) {
return false;
}

boolean canAcceptByCustomFingerprint = false;

String url = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "axis2/";

try {
logger.atInfo().log("probing Axis2 Home Page - custom fingerprint phase");
HttpResponse response = httpClient.send(get(url).withEmptyHeaders().build());

canAcceptByCustomFingerprint =
response.status().isSuccess()
&& response
.bodyString()
.map(Axis2CredentialTester::bodyContainsAxis2Elements)
.orElse(false);
} catch (Exception e) {
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
return false;
}
return canAcceptByCustomFingerprint;
Comment on lines +85 to +103
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
boolean canAcceptByCustomFingerprint = false;
String url = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "axis2/";
try {
logger.atInfo().log("probing Axis2 Home Page - custom fingerprint phase");
HttpResponse response = httpClient.send(get(url).withEmptyHeaders().build());
canAcceptByCustomFingerprint =
response.status().isSuccess()
&& response
.bodyString()
.map(Axis2CredentialTester::bodyContainsAxis2Elements)
.orElse(false);
} catch (Exception e) {
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
return false;
}
return canAcceptByCustomFingerprint;
String url = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "axis2/";
try {
logger.atInfo().log("probing Axis2 Home Page - custom fingerprint phase");
HttpResponse response = httpClient.send(get(url).withEmptyHeaders().build());
return response.status().isSuccess()
&& response
.bodyString()
.map(Axis2CredentialTester::bodyContainsAxis2Elements)
.orElse(false);
} catch (Exception e) {
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
return false;
}

}

/**
* Checks if the response body contains elements of a axis2 home page - custom fingerprinting
* phase
*/
private static boolean bodyContainsAxis2Elements(String responseBody) {
Document doc = Jsoup.parse(responseBody);
String title = doc.title();

if (Ascii.toLowerCase(title).contains(AXIS_PAGE_TITLE)) {
logger.atInfo().log("Found Axis2 Home Page (AXIS_PAGE_TITLE string present in the page)");
return true;
} else {
return false;
}
Comment on lines +114 to +119
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else clause can be removed, and a single return statement is sufficient if the result of the check is stored in a variable.

Suggested change
if (Ascii.toLowerCase(title).contains(AXIS_PAGE_TITLE)) {
logger.atInfo().log("Found Axis2 Home Page (AXIS_PAGE_TITLE string present in the page)");
return true;
} else {
return false;
}
boolean containsAxis2Elements = Ascii.toLowerCase(title).contains(AXIS_PAGE_TITLE);
if (containsAxis2Elements) {
logger.atInfo().log("Found Axis2 Home Page (AXIS_PAGE_TITLE string present in the page)");
}
return containsAxis2Elements;

}

private static boolean bodyContainsAxis2AdminElements(String responseBody) {
// Checks if the response body contains title for successful authentication
return Ascii.toLowerCase(responseBody).contains(AXIS_LOGIN_TITLE);
}

@Override
public ImmutableList<TestCredential> testValidCredentials(
NetworkService networkService, List<TestCredential> credentials) {

/**
* Added default credentials for Axis2 as reported within the documentation
* https://axis.apache.org/axis2/java/core/docs/webadminguide.html#login
*/
TestCredential defaultUser = TestCredential.create(AXIS_USERNAME, Optional.of(AXIS_PASSWORD));
if (isAxis2Accessible(networkService, defaultUser)) return ImmutableList.of(defaultUser);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per Google Java Style Guide - 4.1.1:

Braces are used with if [...] statements, even when the body is empty or contains only a single statement.

Suggested change
if (isAxis2Accessible(networkService, defaultUser)) return ImmutableList.of(defaultUser);
if (isAxis2Accessible(networkService, defaultUser)) {
return ImmutableList.of(defaultUser);
}


// Returning only first match since Axis2 supports a single user
return credentials.stream()
.filter(cred -> isAxis2Accessible(networkService, cred))
.findFirst()
.map(ImmutableList::of)
.orElseGet(ImmutableList::of);
}

private boolean isAxis2Accessible(NetworkService networkService, TestCredential credential) {
var url =
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "axis2/axis2-admin/login";
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(Axis2CredentialTester::bodyContainsAxis2AdminElements)
.orElse(false);
} catch (IOException e) {
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
return false;
}
}

/*
* setFollowRedirects(true) in order to manage different behaviors of Axis2
* Axis2 1.7.3 to 1.8.2 (latest) returns 302 to index when credentials are ok, to welcome otherwise
* Axis2 before 1.7.3 returns 200 in both cases
* All versions contain the same title after the redirect
*/
private HttpResponse sendRequestWithCredentials(String url, TestCredential credential)
throws IOException {
return httpClient
.modify()
.setFollowRedirects(true)
.build()
.send(
post(url)
.setHeaders(
HttpHeaders.builder()
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.build())
.setRequestBody(
ByteString.copyFromUtf8(
String.format(
"userName=%s&password=%s",
credential.username(), credential.password().orElse(""))))
.build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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.axis2;

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 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.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;

/** Tests for {@link Axis2CredentialTester}. */
public class Axis2CredentialTesterTest {
private static final TestCredential WEAK_CRED_1 =
TestCredential.create("properUsername", Optional.of("properPassword"));
private static final TestCredential WRONG_CRED_1 =
TestCredential.create("wrong", Optional.of("pass"));

@Inject private Axis2CredentialTester tester;
private MockWebServer mockWebServer;

private static final ServiceContext.Builder axis2ServiceContext =
ServiceContext.newBuilder()
.setWebServiceContext(
WebServiceContext.newBuilder().setSoftware(Software.newBuilder().setName("axis2")));

@Before
public void setup() {
mockWebServer = new MockWebServer();
Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this);
}

/**
* No need for detect_weakCredentialsExist_returnsAllWeakCredentials since Axis2 only supports a
* single administrator user
*/
Comment on lines +62 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment can be confusing for the reader, please restructure the sentence in a more clearer way.

Suggested change
/**
* No need for detect_weakCredentialsExist_returnsAllWeakCredentials since Axis2 only supports a
* single administrator user
*/
/**
* A separate test for detecting multiple weak credentials is unnecessary because
* Axis2 only supports a single administrator user. Therefore, this test
* (`detect_weakCredentialsExists_returnsWeakCredentials`) effectively covers the
* intended behavior.
*/

@Test
public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception {
startMockWebServer("/", 200, "<title>axis2 :: administration page</title>");
NetworkService targetNetworkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
.setServiceName("http")
.setServiceContext(axis2ServiceContext)
.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_noWeakCredentials_returnsNoCredentials() throws Exception {
startMockWebServer("/", 200, "<title>Login to Axis2 :: Administration page</title>");
NetworkService targetNetworkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
.setServiceName("http")
.setServiceContext(axis2ServiceContext)
.setSoftware(Software.newBuilder().setName("http"))
.build();

assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1)))
.isEmpty();
}

private void startMockWebServer(String url, int responseCode, String response)
throws IOException {
mockWebServer.enqueue(new MockResponse().setResponseCode(responseCode).setBody(response));
mockWebServer.setDispatcher(new RespondUserInfoResponseDispatcher(response));
mockWebServer.start();
mockWebServer.url(url);
}

static final class RespondUserInfoResponseDispatcher extends Dispatcher {
private final String loginPageResponse;

RespondUserInfoResponseDispatcher(String loginPageResponse) {
this.loginPageResponse = checkNotNull(loginPageResponse);
}

@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
var isLoginEndpoint = recordedRequest.getPath().startsWith("/axis2/axis2-admin/login");
var hasWeakCred1 =
recordedRequest
.getBody()
.readUtf8()
.toString()
.contains(
"userName="
+ WEAK_CRED_1.username()
+ "&password="
+ WEAK_CRED_1.password().get());

if (isLoginEndpoint && hasWeakCred1) {
return new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(loginPageResponse);
}
return new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code());
}
}
}
Loading