Skip to content

Commit 64fd45e

Browse files
Merge pull request #534 from doyensec:tomcat-default-credentials
PiperOrigin-RevId: 807855571 Change-Id: I5cfddeb8494aa57127a6c13b33d26e11317518c8
2 parents 5307060 + b3e7148 commit 64fd45e

9 files changed

Lines changed: 919 additions & 4 deletions

File tree

google/detectors/credentials/generic_weak_credential_detector/build.gradle

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ repositories {
1515
mavenLocal()
1616
}
1717

18-
19-
2018
protobuf {
2119
protoc {
2220
artifact = "com.google.protobuf:protoc:3.25.5"
@@ -55,6 +53,7 @@ dependencies {
5553
}
5654
implementation "javax.inject:javax.inject:1"
5755
implementation "org.jsoup:jsoup:1.9.2"
56+
implementation "com.doyensec:libajp:1.0.0"
5857
annotationProcessor "com.google.auto.value:auto-value:1.11.0"
5958

6059
testImplementation "com.google.truth:truth:1.4.4"

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
1617
package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector;
1718

1819
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -51,6 +52,8 @@
5152
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.postgres.PostgresCredentialTester;
5253
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.rabbitmq.RabbitMQCredentialTester;
5354
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.rstudio.RStudioCredentialTester;
55+
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.tomcat.TomcatAjpCredentialTester;
56+
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.tomcat.TomcatHttpCredentialTester;
5457
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.wordpress.WordpressCredentialTester;
5558
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.zenml.ZenMlCredentialTester;
5659
import java.io.FileNotFoundException;
@@ -86,6 +89,8 @@ protected void configurePlugin() {
8689
credentialTesterBinder.addBinding().to(PostgresCredentialTester.class);
8790
credentialTesterBinder.addBinding().to(RabbitMQCredentialTester.class);
8891
credentialTesterBinder.addBinding().to(RStudioCredentialTester.class);
92+
credentialTesterBinder.addBinding().to(TomcatAjpCredentialTester.class);
93+
credentialTesterBinder.addBinding().to(TomcatHttpCredentialTester.class);
8994
credentialTesterBinder.addBinding().to(WordpressCredentialTester.class);
9095
credentialTesterBinder.addBinding().to(ZenMlCredentialTester.class);
9196
credentialTesterBinder.addBinding().to(Axis2CredentialTester.class);

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ public final class Top100Passwords extends CredentialProvider {
5959
"vagrant",
6060
"azureuser",
6161
"cisco",
62-
"rstudio");
62+
"rstudio",
63+
"tomcat",
64+
"manager");
6365

6466
private static final ImmutableList<String> TOP_100_PASSWORDS =
6567
ImmutableList.of(
@@ -69,6 +71,8 @@ public final class Top100Passwords extends CredentialProvider {
6971
"123456",
7072
"password",
7173
"Password",
74+
"password1",
75+
"Password1",
7276
"12345678",
7377
"qwerty",
7478
"123456789",
@@ -166,7 +170,10 @@ public final class Top100Passwords extends CredentialProvider {
166170
"austin",
167171
"thunder",
168172
"taylor",
169-
"matrix");
173+
"tomcat",
174+
"matrix",
175+
"s3cret",
176+
"changethis");
170177

171178
private final ImmutableList<TestCredential> credentials;
172179

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Copyright 2023 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+
17+
package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.tomcat;
18+
19+
import static com.google.common.collect.ImmutableList.toImmutableList;
20+
import static java.nio.charset.StandardCharsets.UTF_8;
21+
22+
import com.doyensec.ajp13.AjpMessage;
23+
import com.doyensec.ajp13.AjpReader;
24+
import com.doyensec.ajp13.ForwardRequestMessage;
25+
import com.doyensec.ajp13.Pair;
26+
import com.google.common.collect.ImmutableList;
27+
import com.google.common.flogger.GoogleLogger;
28+
import com.google.tsunami.common.data.NetworkEndpointUtils;
29+
import com.google.tsunami.common.data.NetworkServiceUtils;
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.DataInputStream;
34+
import java.io.DataOutputStream;
35+
import java.io.IOException;
36+
import java.net.Socket;
37+
import java.util.Base64;
38+
import java.util.LinkedList;
39+
import java.util.List;
40+
import javax.inject.Inject;
41+
42+
/** Credential tester for Tomcat using AJP. */
43+
public final class TomcatAjpCredentialTester extends CredentialTester {
44+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
45+
46+
private static final String AJP13_SERVICE = "ajp13";
47+
private static final String TOMCAT_COOKIE_SET = "set-cookie: JSESSIONID";
48+
private static final String TOMCAT_AUTH_HEADER = "Basic realm=\"Tomcat Manager Application\"";
49+
50+
@Inject
51+
TomcatAjpCredentialTester() {}
52+
53+
@Override
54+
public String name() {
55+
return "TomcatAjpCredentialTester";
56+
}
57+
58+
@Override
59+
public boolean batched() {
60+
return true;
61+
}
62+
63+
@Override
64+
public String description() {
65+
return "Tomcat AJP credential tester.";
66+
}
67+
68+
@Override
69+
public boolean canAccept(NetworkService networkService) {
70+
71+
var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint());
72+
73+
boolean canAcceptByNmapReport =
74+
NetworkServiceUtils.getWebServiceName(networkService).equals(AJP13_SERVICE);
75+
76+
if (!canAcceptByNmapReport) {
77+
return false;
78+
}
79+
80+
boolean canAcceptByCustomFingerprint = false;
81+
82+
String[] uriParts = uriAuthority.split(":");
83+
String host = uriParts[0];
84+
int port = Integer.parseInt(uriParts[1]);
85+
86+
// Check if the server response indicates a redirection to /manager/html.
87+
// This typically means that the Tomcat Manager is active and automatically
88+
// redirects users to the management interface when accessing the base manager URL.
89+
try {
90+
logger.atInfo().log("probing Tomcat manager - custom fingerprint phase using AJP");
91+
92+
List<Pair<String, String>> headers = new LinkedList<>();
93+
List<Pair<String, String>> attributes = new LinkedList<>();
94+
AjpMessage request =
95+
new ForwardRequestMessage(
96+
2, "HTTP/1.1", "/manager/html", host, host, host, port, true, headers, attributes);
97+
98+
byte[] response = sendAndReceive(host, port, request.getBytes());
99+
AjpMessage responseMessage = AjpReader.parseMessage(response);
100+
101+
canAcceptByCustomFingerprint =
102+
responseMessage.getDescription().toLowerCase().contains(TOMCAT_AUTH_HEADER.toLowerCase());
103+
104+
} catch (NullPointerException e) {
105+
logger.atWarning().log("Unable to query '%s'.", uriAuthority);
106+
return false;
107+
} catch (IOException e) {
108+
logger.atWarning().log("Unable to query '%s'.", uriAuthority);
109+
return false;
110+
}
111+
112+
return canAcceptByCustomFingerprint;
113+
}
114+
115+
@Override
116+
public ImmutableList<TestCredential> testValidCredentials(
117+
NetworkService networkService, List<TestCredential> credentials) {
118+
119+
return credentials.stream()
120+
.filter(cred -> isTomcatAccessible(networkService, cred))
121+
.collect(toImmutableList());
122+
}
123+
124+
private boolean isTomcatAccessible(NetworkService networkService, TestCredential credential) {
125+
var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint());
126+
String[] uriParts = uriAuthority.split(":");
127+
String host = uriParts[0];
128+
int port = Integer.parseInt(uriParts[1]);
129+
var url = String.format("%s/%s", uriAuthority, "manager/html");
130+
131+
logger.atInfo().log("uriAuthority: %s", uriAuthority);
132+
try {
133+
logger.atInfo().log(
134+
"url: %s, username: %s, password: %s",
135+
url, credential.username(), credential.password().orElse(""));
136+
137+
String authorization =
138+
"Basic "
139+
+ Base64.getEncoder()
140+
.encodeToString(
141+
(credential.username() + ":" + credential.password().orElse(""))
142+
.getBytes(UTF_8));
143+
144+
List<Pair<String, String>> headers = new LinkedList<>();
145+
headers.add(Pair.make("Authorization", authorization));
146+
List<Pair<String, String>> attributes = new LinkedList<>();
147+
148+
AjpMessage request =
149+
new ForwardRequestMessage(
150+
2, "HTTP/1.1", "/manager/html", host, host, host, port, true, headers, attributes);
151+
152+
byte[] response = sendAndReceive(host, port, request.getBytes());
153+
AjpMessage responseMessage = AjpReader.parseMessage(response);
154+
155+
return headersContainsSuccessfulLoginElements(responseMessage);
156+
} catch (IOException e) {
157+
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
158+
return false;
159+
}
160+
}
161+
162+
// This methods send the AjpMessage generated via sockets and return the response from the server
163+
private byte[] sendAndReceive(String host, int port, byte[] data) throws IOException {
164+
try (Socket socket = new Socket(host, port)) {
165+
DataOutputStream os = new DataOutputStream(socket.getOutputStream());
166+
DataInputStream is = new DataInputStream(socket.getInputStream());
167+
168+
os.write(data);
169+
os.flush();
170+
171+
byte[] buffReply = new byte[8192];
172+
int bytesRead = is.read(buffReply);
173+
174+
if (bytesRead > 0) {
175+
byte[] fullReply = new byte[bytesRead];
176+
System.arraycopy(buffReply, 0, fullReply, 0, bytesRead);
177+
178+
return fullReply;
179+
}
180+
return new byte[0];
181+
} catch (IOException e) {
182+
logger.atSevere().withCause(e).log("Error sendind the AjpMessage");
183+
throw e;
184+
}
185+
}
186+
187+
// This method checks if the response headers contain elements indicative of a Tomcat manager
188+
// page. Specifically, it examines the cookies set rather than body elements to improve the
189+
// efficiency and speed of the plugin. By focusing on headers, the plugin can quickly identify
190+
// successful logins without parsing potentially large and variable body content.
191+
private static boolean headersContainsSuccessfulLoginElements(AjpMessage responseMessage) {
192+
try {
193+
String responseHeaders = responseMessage.getDescription().toLowerCase();
194+
if (responseHeaders.contains(TOMCAT_COOKIE_SET.toLowerCase())) {
195+
logger.atInfo().log("Found Tomcat endpoint (TOMCAT_COOKIE_SET string present in the page)");
196+
return true;
197+
} else {
198+
return false;
199+
}
200+
} catch (Exception e) {
201+
logger.atWarning().withCause(e).log(
202+
"An error occurred in headersContainsSuccessfulLoginElements");
203+
return false;
204+
}
205+
}
206+
}

0 commit comments

Comments
 (0)