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

Added Tomcat HTTP and AJP Weak Credentials Testers #534

Open
wants to merge 5 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 @@ -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,204 @@
/*
* 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.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.util.Base64;
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 false;
}

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 (NullPointerException e) {
logger.atWarning().log("Unable to query '%s'.", uriAuthority);
return false;
} catch (IOException e){
logger.atWarning().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("%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
Loading