Skip to content

Commit 6c2463a

Browse files
Merge pull request #578 from mindedsecurity:axis2
PiperOrigin-RevId: 795587462 Change-Id: Ib83b49af31632b4532ec35c68def5e4912a1ea45
2 parents 38fc813 + 09704a1 commit 6c2463a

3 files changed

Lines changed: 329 additions & 0 deletions

File tree

google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.DefaultCredentials;
3737
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.Top100Passwords;
3838
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester;
39+
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.axis2.Axis2CredentialTester;
3940
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airbyte.AirbyteCredentialTester;
4041
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.airflow.AirflowCredentialTester;
4142
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.argocd.ArgoCdCredentialTester;
@@ -87,6 +88,7 @@ protected void configurePlugin() {
8788
credentialTesterBinder.addBinding().to(RStudioCredentialTester.class);
8889
credentialTesterBinder.addBinding().to(WordpressCredentialTester.class);
8990
credentialTesterBinder.addBinding().to(ZenMlCredentialTester.class);
91+
credentialTesterBinder.addBinding().to(Axis2CredentialTester.class);
9092

9193
Multibinder<CredentialProvider> credentialProviderBinder =
9294
Multibinder.newSetBinder(binder(), CredentialProvider.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.axis2;
17+
18+
import static com.google.common.base.Preconditions.checkNotNull;
19+
import static com.google.tsunami.common.net.http.HttpRequest.get;
20+
import static com.google.tsunami.common.net.http.HttpRequest.post;
21+
22+
import com.google.common.base.Ascii;
23+
import com.google.common.collect.ImmutableList;
24+
import com.google.common.flogger.GoogleLogger;
25+
import com.google.protobuf.ByteString;
26+
import com.google.tsunami.common.data.NetworkServiceUtils;
27+
import com.google.tsunami.common.net.http.HttpClient;
28+
import com.google.tsunami.common.net.http.HttpHeaders;
29+
import com.google.tsunami.common.net.http.HttpResponse;
30+
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential;
31+
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester;
32+
import com.google.tsunami.proto.NetworkService;
33+
import java.io.IOException;
34+
import java.util.List;
35+
import java.util.Optional;
36+
import javax.inject.Inject;
37+
import org.jsoup.Jsoup;
38+
import org.jsoup.nodes.Document;
39+
40+
/** Credential tester specifically for Apache Axis2 Administration Panel. */
41+
public final class Axis2CredentialTester extends CredentialTester {
42+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
43+
private final HttpClient httpClient;
44+
45+
private static final String AXIS_PAGE_TITLE = "axis 2 - home";
46+
private static final String AXIS_LOGIN_TITLE = "<title>axis2 :: administration page</title>";
47+
48+
/**
49+
* Default credentials are inserted here instead of in the appropriate proto file since nmap
50+
* identifies the service name as "http". Due to this behavior, credentials have been inserted
51+
* here to not test such credentials against each "http" service.
52+
*/
53+
private static final String AXIS_USERNAME = "admin";
54+
55+
private static final String AXIS_PASSWORD = "axis2";
56+
57+
@Inject
58+
Axis2CredentialTester(HttpClient httpClient) {
59+
this.httpClient = checkNotNull(httpClient);
60+
}
61+
62+
@Override
63+
public String name() {
64+
return "Axis2CredentialTester";
65+
}
66+
67+
@Override
68+
public String description() {
69+
return "Apache Axis2 Administration Panel credential tester.";
70+
}
71+
72+
@Override
73+
public boolean batched() {
74+
return false;
75+
}
76+
77+
/**
78+
* Determines if this tester can accept the {@link NetworkService} based on the name of the
79+
* service or a custom fingerprint. The fingerprint is necessary since nmap doesn't recognize a
80+
* Axis2 instance correctly.
81+
*
82+
* @param networkService the network service passed by tsunami
83+
* @return true if a axis2 instance is recognized
84+
*/
85+
@Override
86+
public boolean canAccept(NetworkService networkService) {
87+
if (!NetworkServiceUtils.isWebService(networkService)) {
88+
return false;
89+
}
90+
String url = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "axis2/";
91+
92+
try {
93+
logger.atInfo().log("probing Axis2 Home Page - custom fingerprint phase");
94+
HttpResponse response = httpClient.send(get(url).withEmptyHeaders().build());
95+
96+
return response.status().isSuccess()
97+
&& response
98+
.bodyString()
99+
.map(Axis2CredentialTester::bodyContainsAxis2Elements)
100+
.orElse(false);
101+
} catch (Exception e) {
102+
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
103+
return false;
104+
}
105+
}
106+
107+
/**
108+
* Checks if the response body contains elements of a axis2 home page - custom fingerprinting
109+
* phase
110+
*/
111+
private static boolean bodyContainsAxis2Elements(String responseBody) {
112+
Document doc = Jsoup.parse(responseBody);
113+
String title = doc.title();
114+
115+
return Ascii.toLowerCase(title).contains(AXIS_PAGE_TITLE);
116+
}
117+
118+
private static boolean bodyContainsAxis2AdminElements(String responseBody) {
119+
// Checks if the response body contains title for successful authentication
120+
return Ascii.toLowerCase(responseBody).contains(AXIS_LOGIN_TITLE);
121+
}
122+
123+
@Override
124+
public ImmutableList<TestCredential> testValidCredentials(
125+
NetworkService networkService, List<TestCredential> credentials) {
126+
127+
// Added default credentials for Axis2 as reported within the documentation
128+
// https://axis.apache.org/axis2/java/core/docs/webadminguide.html#login
129+
TestCredential defaultUser = TestCredential.create(AXIS_USERNAME, Optional.of(AXIS_PASSWORD));
130+
if (isAxis2Accessible(networkService, defaultUser)) {
131+
return ImmutableList.of(defaultUser);
132+
}
133+
134+
// Returning only first match since Axis2 supports a single user
135+
return credentials.stream()
136+
.filter(cred -> isAxis2Accessible(networkService, cred))
137+
.findFirst()
138+
.map(ImmutableList::of)
139+
.orElseGet(ImmutableList::of);
140+
}
141+
142+
private boolean isAxis2Accessible(NetworkService networkService, TestCredential credential) {
143+
var url =
144+
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "axis2/axis2-admin/login";
145+
try {
146+
logger.atInfo().log(
147+
"url: %s, username: %s, password: %s",
148+
url, credential.username(), credential.password().orElse(""));
149+
150+
HttpResponse response = sendRequestWithCredentials(url, credential);
151+
152+
return response.status().isSuccess()
153+
&& response
154+
.bodyString()
155+
.map(Axis2CredentialTester::bodyContainsAxis2AdminElements)
156+
.orElse(false);
157+
} catch (IOException e) {
158+
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
159+
return false;
160+
}
161+
}
162+
163+
/*
164+
* setFollowRedirects(true) in order to manage different behaviors of Axis2
165+
* Axis2 1.7.3 to 1.8.2 (latest) returns 302 to index when credentials are ok, to welcome otherwise
166+
* Axis2 before 1.7.3 returns 200 in both cases
167+
* All versions contain the same title after the redirect
168+
*/
169+
private HttpResponse sendRequestWithCredentials(String url, TestCredential credential)
170+
throws IOException {
171+
return httpClient
172+
.modify()
173+
.setFollowRedirects(true)
174+
.build()
175+
.send(
176+
post(url)
177+
.setHeaders(
178+
HttpHeaders.builder()
179+
.addHeader("Content-Type", "application/x-www-form-urlencoded")
180+
.build())
181+
.setRequestBody(
182+
ByteString.copyFromUtf8(
183+
String.format(
184+
"userName=%s&password=%s",
185+
credential.username(), credential.password().orElse(""))))
186+
.build());
187+
}
188+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.axis2;
17+
18+
import static com.google.common.base.Preconditions.checkNotNull;
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort;
21+
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.inject.Guice;
24+
import com.google.tsunami.common.net.http.HttpClientModule;
25+
import com.google.tsunami.common.net.http.HttpStatus;
26+
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential;
27+
import com.google.tsunami.proto.NetworkService;
28+
import com.google.tsunami.proto.ServiceContext;
29+
import com.google.tsunami.proto.Software;
30+
import com.google.tsunami.proto.WebServiceContext;
31+
import java.io.IOException;
32+
import java.util.Optional;
33+
import javax.inject.Inject;
34+
import okhttp3.mockwebserver.Dispatcher;
35+
import okhttp3.mockwebserver.MockResponse;
36+
import okhttp3.mockwebserver.MockWebServer;
37+
import okhttp3.mockwebserver.RecordedRequest;
38+
import org.junit.Before;
39+
import org.junit.Test;
40+
import org.junit.runner.RunWith;
41+
import org.junit.runners.JUnit4;
42+
43+
/** Tests for {@link Axis2CredentialTester}. */
44+
@RunWith(JUnit4.class)
45+
public class Axis2CredentialTesterTest {
46+
private static final TestCredential WEAK_CRED_1 =
47+
TestCredential.create("properUsername", Optional.of("properPassword"));
48+
private static final TestCredential WRONG_CRED_1 =
49+
TestCredential.create("wrong", Optional.of("pass"));
50+
51+
@Inject private Axis2CredentialTester tester;
52+
private MockWebServer mockWebServer;
53+
54+
private static final ServiceContext.Builder axis2ServiceContext =
55+
ServiceContext.newBuilder()
56+
.setWebServiceContext(
57+
WebServiceContext.newBuilder().setSoftware(Software.newBuilder().setName("axis2")));
58+
59+
@Before
60+
public void setup() {
61+
mockWebServer = new MockWebServer();
62+
Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this);
63+
}
64+
65+
/**
66+
* A separate test for detecting multiple weak credentials is unnecessary because Axis2 only
67+
* supports a single administrator user. Therefore, this test
68+
* (`detect_weakCredentialsExists_returnsWeakCredentials`) effectively covers the intended
69+
* behavior.
70+
*/
71+
@Test
72+
public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception {
73+
startMockWebServer("/", 200, "<title>axis2 :: administration page</title>");
74+
NetworkService targetNetworkService =
75+
NetworkService.newBuilder()
76+
.setNetworkEndpoint(
77+
forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
78+
.setServiceName("http")
79+
.setServiceContext(axis2ServiceContext)
80+
.setSoftware(Software.newBuilder().setName("http"))
81+
.build();
82+
83+
assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1)))
84+
.containsExactly(WEAK_CRED_1);
85+
mockWebServer.shutdown();
86+
}
87+
88+
@Test
89+
public void detect_noWeakCredentials_returnsNoCredentials() throws Exception {
90+
startMockWebServer("/", 200, "<title>Login to Axis2 :: Administration page</title>");
91+
NetworkService targetNetworkService =
92+
NetworkService.newBuilder()
93+
.setNetworkEndpoint(
94+
forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
95+
.setServiceName("http")
96+
.setServiceContext(axis2ServiceContext)
97+
.setSoftware(Software.newBuilder().setName("http"))
98+
.build();
99+
100+
assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1)))
101+
.isEmpty();
102+
}
103+
104+
private void startMockWebServer(String url, int responseCode, String response)
105+
throws IOException {
106+
mockWebServer.enqueue(new MockResponse().setResponseCode(responseCode).setBody(response));
107+
mockWebServer.setDispatcher(new RespondUserInfoResponseDispatcher(response));
108+
mockWebServer.start();
109+
mockWebServer.url(url);
110+
}
111+
112+
static final class RespondUserInfoResponseDispatcher extends Dispatcher {
113+
private final String loginPageResponse;
114+
115+
RespondUserInfoResponseDispatcher(String loginPageResponse) {
116+
this.loginPageResponse = checkNotNull(loginPageResponse);
117+
}
118+
119+
@Override
120+
public MockResponse dispatch(RecordedRequest recordedRequest) {
121+
var isLoginEndpoint = recordedRequest.getPath().startsWith("/axis2/axis2-admin/login");
122+
var hasWeakCred1 =
123+
recordedRequest
124+
.getBody()
125+
.readUtf8()
126+
.toString()
127+
.contains(
128+
"userName="
129+
+ WEAK_CRED_1.username()
130+
+ "&password="
131+
+ WEAK_CRED_1.password().get());
132+
133+
if (isLoginEndpoint && hasWeakCred1) {
134+
return new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(loginPageResponse);
135+
}
136+
return new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code());
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)