Skip to content

Commit

Permalink
Added Tomcat HTTP and AJP Weak Credentials Testers
Browse files Browse the repository at this point in the history
  • Loading branch information
RaulDoyensec committed Sep 6, 2024
1 parent e3aefd1 commit 2471230
Show file tree
Hide file tree
Showing 9 changed files with 916 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ ext {
guiceVersion = '4.2.3'
javaxInjectVersion = '1'
jsoupVersion = '1.9.2'
ajpVersion = '1.0.0'
okhttpVersion = '3.12.0'
protobufVersion = '3.25.2'
tsunamiVersion = 'latest.release'
Expand Down Expand Up @@ -91,6 +92,7 @@ dependencies {
implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}"
implementation "javax.inject:javax.inject:${javaxInjectVersion}"
implementation "org.jsoup:jsoup:${jsoupVersion}"
implementation "com.doyensec:libajp:${ajpVersion}"
annotationProcessor "com.google.auto.value:auto-value:${autoValueVersion}"

testImplementation "com.google.truth:truth:${truthVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector;

import static java.nio.charset.StandardCharsets.UTF_8;
Expand Down Expand Up @@ -44,8 +45,10 @@
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.tomcat.TomcatAjpCredentialTester;
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 java.io.FileNotFoundException;
Expand Down Expand Up @@ -77,6 +80,8 @@ protected void configurePlugin() {
credentialTesterBinder.addBinding().to(GrafanaCredentialTester.class);
credentialTesterBinder.addBinding().to(RStudioCredentialTester.class);
credentialTesterBinder.addBinding().to(RabbitMQCredentialTester.class);
credentialTesterBinder.addBinding().to(TomcatHttpCredentialTester.class);
credentialTesterBinder.addBinding().to(TomcatAjpCredentialTester.class);
credentialTesterBinder.addBinding().to(ZenMlCredentialTester.class);

Multibinder<CredentialProvider> credentialProviderBinder =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public final class Top100Passwords extends CredentialProvider {
"vagrant",
"azureuser",
"cisco",
"rstudio");
"rstudio",
"tomcat",
"manager");

private static final ImmutableList<String> TOP_100_PASSWORDS =
ImmutableList.of(
Expand All @@ -68,6 +70,8 @@ public final class Top100Passwords extends CredentialProvider {
"123456",
"password",
"Password",
"password1",
"Password1",
"12345678",
"qwerty",
"123456789",
Expand Down Expand Up @@ -165,7 +169,10 @@ public final class Top100Passwords extends CredentialProvider {
"austin",
"thunder",
"taylor",
"matrix");
"tomcat",
"matrix",
"s3cret",
"changethis");

private final ImmutableList<TestCredential> credentials;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* 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.tomcat;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.doyensec.ajp13.AjpMessage;
import com.doyensec.ajp13.AjpReader;
import com.doyensec.ajp13.ForwardRequestMessage;
import com.doyensec.ajp13.Pair;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.tsunami.common.data.NetworkEndpointUtils;
import com.google.tsunami.common.data.NetworkServiceUtils;
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.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.sql.Timestamp;
import java.util.Base64;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;

/** Credential tester for Tomcat using AJP. */
public final class TomcatAjpCredentialTester extends CredentialTester {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

private static final String AJP13_SERVICE = "ajp13";
private static final String TOMCAT_COOKIE_SET = "set-cookie: JSESSIONID";
private static final String TOMCAT_AUTH_HEADER = "Basic realm=\"Tomcat Manager Application\"";

@Inject
TomcatAjpCredentialTester() {
}

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

@Override
public boolean batched() {
return true;
}

@Override
public String description() {
return "Tomcat AJP credential tester.";
}

@Override
public boolean canAccept(NetworkService networkService) {

var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint());

boolean canAcceptByNmapReport =
NetworkServiceUtils.getWebServiceName(networkService).equals(AJP13_SERVICE);

if (canAcceptByNmapReport) {
return true;
}

boolean canAcceptByCustomFingerprint = false;

String[] uriParts = uriAuthority.split(":");
String host = uriParts[0];
int port = Integer.parseInt(uriParts[1]);

// Check if the server response indicates a redirection to /manager/html.
// This typically means that the Tomcat Manager is active and automatically
// redirects users to the management interface when accessing the base manager URL.
try {
logger.atInfo().log("probing Tomcat manager - custom fingerprint phase using AJP");

List<Pair<String, String>> headers = new LinkedList<>();
List<Pair<String, String>> attributes = new LinkedList<>();
AjpMessage request = new ForwardRequestMessage(
2, "HTTP/1.1", "/manager/html", host, host, host, port, true, headers, attributes);

byte[] response = sendAndReceive(host, port, request.getBytes());
AjpMessage responseMessage = AjpReader.parseMessage(response);

canAcceptByCustomFingerprint = responseMessage.getDescription()
.toLowerCase().contains(TOMCAT_AUTH_HEADER.toLowerCase());

} catch (Exception e) {
// This catch block will catch both IOException and NullPointerException
logger.atWarning().withCause(e).log("Unable to query '%s'.", uriAuthority);
return false;
}

return canAcceptByCustomFingerprint;
}

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

return credentials.stream()
.filter(cred -> isTomcatAccessible(networkService, cred))
.collect(toImmutableList());
}

private boolean isTomcatAccessible(NetworkService networkService, TestCredential credential) {
var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint());
String[] uriParts = uriAuthority.split(":");
String host = uriParts[0];
int port = Integer.parseInt(uriParts[1]);
var url = String.format("http://%s/%s", uriAuthority, "manager/html");

logger.atInfo().log("uriAuthority: %s", uriAuthority);
try {
logger.atInfo().log(
"url: %s, username: %s, password: %s",
url, credential.username(), credential.password().orElse(""));

String authorization = "Basic " + Base64.getEncoder()
.encodeToString((credential.username() + ":" + credential.password().orElse(""))
.getBytes(UTF_8));

List<Pair<String, String>> headers = new LinkedList<>();
headers.add(Pair.make("Authorization", authorization));
List<Pair<String, String>> attributes = new LinkedList<>();

AjpMessage request = new ForwardRequestMessage(
2, "HTTP/1.1", "/manager/html", host, host, host, port, true, headers, attributes);

byte[] response = sendAndReceive(host, port, request.getBytes());
AjpMessage responseMessage = AjpReader.parseMessage(response);

return headersContainsSuccessfulLoginElements(responseMessage);
} catch (IOException e) {
logger.atWarning().withCause(e).log("Unable to query '%s'.", url);
return false;
}
}


// This methods send the AjpMessage generated via sockets and return the response from the server
private byte[] sendAndReceive(String host, int port, byte[] data) throws IOException {
try (Socket socket = new Socket(host, port)) {
DataOutputStream os = new DataOutputStream(socket.getOutputStream());
DataInputStream is = new DataInputStream(socket.getInputStream());

os.write(data);
os.flush();

byte[] buffReply = new byte[8192];
int bytesRead = is.read(buffReply);

if (bytesRead > 0) {
byte[] fullReply = new byte[bytesRead];
System.arraycopy(buffReply, 0, fullReply, 0, bytesRead);

return fullReply;
}
return new byte[0];
} catch (IOException e) {
logger.atSevere().withCause(e).log("Error sendind the AjpMessage");
throw e;
}
}

// This method checks if the response headers contain elements indicative of a Tomcat manager
// page. Specifically, it examines the cookies set rather than body elements to improve the
// efficiency and speed of the plugin. By focusing on headers, the plugin can quickly identify
// successful logins without parsing potentially large and variable body content.
private static boolean headersContainsSuccessfulLoginElements(AjpMessage responseMessage) {
try {
String responseHeaders = responseMessage.getDescription().toLowerCase();
if (responseHeaders.contains(TOMCAT_COOKIE_SET.toLowerCase())) {
logger.atInfo().log(
"Found Tomcat endpoint (TOMCAT_COOKIE_SET string present in the page)");
return true;
} else {
return false;
}
} catch (Exception e) {
logger.atWarning().withCause(e).log(
"An error occurred in headersContainsSuccessfulLoginElements");
return false;
}
}
}
Loading

0 comments on commit 2471230

Please sign in to comment.