From a9209a079cf4719fbe97f66fbf11edee2cd0ddaf Mon Sep 17 00:00:00 2001 From: TLS Scanner Developer Date: Fri, 27 Jun 2025 11:25:18 +0000 Subject: [PATCH] Add ApplicationLayerProbe for HTTP detection on TLS ports - Add new SPEAKS_HTTP property to TlsAnalyzedProperty enum - Add APPLICATION_LAYER probe type to TlsProbeType enum - Implement ApplicationLayerProbe that sends HTTP GET request after TLS handshake - Register new probe in TlsServerScanner probe list - Add SPEAKS_HTTP property to rating influencers XML - Add unit tests for ApplicationLayerProbe This addresses issue #38 to detect when a server speaks HTTP on a TLS port, which is useful for identifying HTTPS services. --- .../core/constants/TlsAnalyzedProperty.java | 1 + .../core/constants/TlsProbeType.java | 1 + .../execution/TlsServerScanner.java | 2 + .../probe/ApplicationLayerProbe.java | 163 ++++++++++++++++++ .../src/main/resources/rating/influencers.xml | 3 + .../probe/ApplicationLayerProbeTest.java | 88 ++++++++++ 6 files changed, 258 insertions(+) create mode 100644 TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbe.java create mode 100644 TLS-Server-Scanner/src/test/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbeTest.java diff --git a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsAnalyzedProperty.java b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsAnalyzedProperty.java index 72c739fea..db1f8d97b 100644 --- a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsAnalyzedProperty.java +++ b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsAnalyzedProperty.java @@ -22,6 +22,7 @@ public enum TlsAnalyzedProperty implements AnalyzedProperty { CLOSED_AFTER_APP_DATA_DELTA(TlsAnalyzedPropertyCategory.CONNECTION), KNOWN_PADDING_ORACLE_VULNERABILITY(TlsAnalyzedPropertyCategory.ATTACKS), SUPPORTED_APPLICATIONS(TlsAnalyzedPropertyCategory.APPLICATION_LAYER), + SPEAKS_HTTP(TlsAnalyzedPropertyCategory.APPLICATION_LAYER), BLEICHENBACHER_TEST_RESULT(TlsAnalyzedPropertyCategory.ATTACKS), PADDING_ORACLE_TEST_RESULT(TlsAnalyzedPropertyCategory.ATTACKS), DIRECT_RACCOON_TEST_RESULT(TlsAnalyzedPropertyCategory.ATTACKS), diff --git a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsProbeType.java b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsProbeType.java index 02b38c849..1592848d0 100644 --- a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsProbeType.java +++ b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/constants/TlsProbeType.java @@ -63,6 +63,7 @@ public enum TlsProbeType implements ProbeType { DTLS_MESSAGE_SEQUENCE_NUMBER("DTLS message sequence number"), DTLS_RETRANSMISSIONS("DTLS retransmissions"), DTLS_APPLICATION_FINGERPRINT("DTLS application fingerprint"), + APPLICATION_LAYER("Application layer detection"), HTTP_FALSE_START("HTTP false start"), HELLO_RETRY("Hello retry"), CROSS_PROTOCOL_ALPACA("Alpaca attack"), diff --git a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/execution/TlsServerScanner.java b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/execution/TlsServerScanner.java index 04a09bbbb..a854be016 100644 --- a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/execution/TlsServerScanner.java +++ b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/execution/TlsServerScanner.java @@ -49,6 +49,7 @@ import de.rub.nds.tlsscanner.serverscanner.passive.SessionTicketExtractor; import de.rub.nds.tlsscanner.serverscanner.probe.AlpacaProbe; import de.rub.nds.tlsscanner.serverscanner.probe.AlpnProbe; +import de.rub.nds.tlsscanner.serverscanner.probe.ApplicationLayerProbe; import de.rub.nds.tlsscanner.serverscanner.probe.BleichenbacherProbe; import de.rub.nds.tlsscanner.serverscanner.probe.CcaRequiredProbe; import de.rub.nds.tlsscanner.serverscanner.probe.CcaSupportProbe; @@ -261,6 +262,7 @@ protected void fillProbeLists() { registerProbeForExecution(new EsniProbe(configSelector, parallelExecutor)); registerProbeForExecution(new TokenbindingProbe(configSelector, parallelExecutor)); registerProbeForExecution(new HttpHeaderProbe(configSelector, parallelExecutor)); + registerProbeForExecution(new ApplicationLayerProbe(configSelector, parallelExecutor)); registerProbeForExecution(new HttpFalseStartProbe(configSelector, parallelExecutor)); registerProbeForExecution(new DrownProbe(configSelector, parallelExecutor)); registerProbeForExecution( diff --git a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbe.java b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbe.java new file mode 100644 index 000000000..41d66eb14 --- /dev/null +++ b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbe.java @@ -0,0 +1,163 @@ +/* + * TLS-Scanner - A TLS configuration and analysis tool based on TLS-Attacker + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.tlsscanner.serverscanner.probe; + +import de.rub.nds.scanner.core.probe.requirements.Requirement; +import de.rub.nds.scanner.core.probe.result.TestResult; +import de.rub.nds.scanner.core.probe.result.TestResults; +import de.rub.nds.tlsattacker.core.config.Config; +import de.rub.nds.tlsattacker.core.constants.RunningModeType; +import de.rub.nds.tlsattacker.core.protocol.ProtocolMessage; +import de.rub.nds.tlsattacker.core.protocol.message.ApplicationMessage; +import de.rub.nds.tlsattacker.core.record.Record; +import de.rub.nds.tlsattacker.core.state.State; +import de.rub.nds.tlsattacker.core.workflow.ParallelExecutor; +import de.rub.nds.tlsattacker.core.workflow.WorkflowTrace; +import de.rub.nds.tlsattacker.core.workflow.WorkflowTraceResultUtil; +import de.rub.nds.tlsattacker.core.workflow.action.ReceiveAction; +import de.rub.nds.tlsattacker.core.workflow.action.SendAction; +import de.rub.nds.tlsattacker.core.workflow.factory.WorkflowConfigurationFactory; +import de.rub.nds.tlsattacker.core.workflow.factory.WorkflowTraceType; +import de.rub.nds.tlsscanner.core.constants.TlsAnalyzedProperty; +import de.rub.nds.tlsscanner.core.constants.TlsProbeType; +import de.rub.nds.tlsscanner.serverscanner.constants.ApplicationProtocol; +import de.rub.nds.tlsscanner.serverscanner.probe.requirements.ServerOptionsRequirement; +import de.rub.nds.tlsscanner.serverscanner.report.ServerReport; +import de.rub.nds.tlsscanner.serverscanner.selector.ConfigSelector; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ApplicationLayerProbe extends TlsServerProbe { + + private static final Logger LOGGER = LogManager.getLogger(); + + private List supportedApplications; + private TestResult speaksHttp = TestResults.COULD_NOT_TEST; + + public ApplicationLayerProbe(ConfigSelector configSelector, ParallelExecutor parallelExecutor) { + super(parallelExecutor, TlsProbeType.APPLICATION_LAYER, configSelector); + register(TlsAnalyzedProperty.SUPPORTED_APPLICATIONS, TlsAnalyzedProperty.SPEAKS_HTTP); + } + + @Override + protected void executeTest() { + supportedApplications = new ArrayList<>(); + speaksHttp = TestResults.FALSE; + + // Test for HTTP + if (isHttpSupported()) { + supportedApplications.add(ApplicationProtocol.HTTP); + speaksHttp = TestResults.TRUE; + } + } + + private boolean isHttpSupported() { + // Send an HTTP GET request + String httpRequest = + "GET / HTTP/1.1\r\nHost: " + + configSelector.getScannerConfig().getClientDelegate().getHost() + + "\r\nConnection: close\r\n\r\n"; + byte[] requestData = httpRequest.getBytes(StandardCharsets.UTF_8); + + byte[] responseData = sendApplicationData(requestData); + + if (responseData.length > 0) { + String response = new String(responseData, StandardCharsets.UTF_8); + // Check for HTTP response pattern + if (response.contains("HTTP/") + && (response.contains("200") + || response.contains("301") + || response.contains("302") + || response.contains("403") + || response.contains("404") + || response.contains("500") + || response.contains("503"))) { + LOGGER.debug( + "HTTP response detected: " + + response.substring(0, Math.min(response.length(), 100))); + return true; + } + } + + return false; + } + + private byte[] sendApplicationData(byte[] data) { + Config config = configSelector.getAnyWorkingBaseConfig(); + WorkflowTrace trace = + new WorkflowConfigurationFactory(config) + .createWorkflowTrace( + WorkflowTraceType.DYNAMIC_HANDSHAKE, RunningModeType.CLIENT); + trace.addTlsAction(new SendAction(new ApplicationMessage(data))); + ReceiveAction receiveAction = new ReceiveAction(new ApplicationMessage()); + trace.addTlsAction(receiveAction); + + State state = new State(config, trace); + executeState(state); + + if (receiveAction.getReceivedRecords() != null + && !receiveAction.getReceivedRecords().isEmpty()) { + ByteArrayOutputStream receivedData = new ByteArrayOutputStream(); + try { + for (Record record : receiveAction.getReceivedRecords()) { + receivedData.write(record.getCleanProtocolMessageBytes().getValue()); + } + } catch (IOException ex) { + LOGGER.error("Could not write cleanProtocolMessageBytes to receivedData", ex); + } + return receivedData.toByteArray(); + } else { + ProtocolMessage receivedMessage = + WorkflowTraceResultUtil.getLastReceivedMessage(state.getWorkflowTrace()); + if (receivedMessage instanceof ApplicationMessage) { + ApplicationMessage appMessage = (ApplicationMessage) receivedMessage; + if (appMessage.getData() != null && appMessage.getData().getValue() != null) { + return appMessage.getData().getValue(); + } + } + } + + return new byte[0]; + } + + @Override + public void adjustConfig(ServerReport report) {} + + @Override + protected void mergeData(ServerReport report) { + if (!supportedApplications.isEmpty()) { + @SuppressWarnings("unchecked") + List existingApplications = + (List) + report.getResult(TlsAnalyzedProperty.SUPPORTED_APPLICATIONS); + if (existingApplications != null) { + // Merge with existing applications (e.g., from DTLS probe) + for (ApplicationProtocol app : supportedApplications) { + if (!existingApplications.contains(app)) { + existingApplications.add(app); + } + } + put(TlsAnalyzedProperty.SUPPORTED_APPLICATIONS, existingApplications); + } else { + put(TlsAnalyzedProperty.SUPPORTED_APPLICATIONS, supportedApplications); + } + } + put(TlsAnalyzedProperty.SPEAKS_HTTP, speaksHttp); + } + + @Override + public Requirement getRequirements() { + return new ServerOptionsRequirement(configSelector.getScannerConfig(), getType()); + } +} diff --git a/TLS-Server-Scanner/src/main/resources/rating/influencers.xml b/TLS-Server-Scanner/src/main/resources/rating/influencers.xml index c74c5112d..9de056677 100644 --- a/TLS-Server-Scanner/src/main/resources/rating/influencers.xml +++ b/TLS-Server-Scanner/src/main/resources/rating/influencers.xml @@ -2167,6 +2167,9 @@ SUPPORTED_APPLICATIONS + + SPEAKS_HTTP + SUPPORTED_CIPHERSUITES diff --git a/TLS-Server-Scanner/src/test/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbeTest.java b/TLS-Server-Scanner/src/test/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbeTest.java new file mode 100644 index 000000000..473eb5a12 --- /dev/null +++ b/TLS-Server-Scanner/src/test/java/de/rub/nds/tlsscanner/serverscanner/probe/ApplicationLayerProbeTest.java @@ -0,0 +1,88 @@ +/* + * TLS-Scanner - A TLS configuration and analysis tool based on TLS-Attacker + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.tlsscanner.serverscanner.probe; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import de.rub.nds.tlsattacker.core.config.Config; +import de.rub.nds.tlsattacker.core.workflow.ParallelExecutor; +import de.rub.nds.tlsattacker.util.tests.TestCategories; +import de.rub.nds.tlsscanner.core.constants.TlsAnalyzedProperty; +import de.rub.nds.tlsscanner.serverscanner.config.ServerScannerConfig; +import de.rub.nds.tlsscanner.serverscanner.constants.ApplicationProtocol; +import de.rub.nds.tlsscanner.serverscanner.report.ServerReport; +import de.rub.nds.tlsscanner.serverscanner.selector.ConfigSelector; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag(TestCategories.SLOW_TEST) +public class ApplicationLayerProbeTest { + + private ApplicationLayerProbe probe; + private ConfigSelector configSelector; + private ParallelExecutor parallelExecutor; + private ServerScannerConfig scannerConfig; + private ServerReport report; + + @BeforeEach + public void setUp() { + scannerConfig = mock(ServerScannerConfig.class); + // No need to mock hostname - handled by probe using clientDelegate + + configSelector = mock(ConfigSelector.class); + when(configSelector.getScannerConfig()).thenReturn(scannerConfig); + + Config config = Config.createConfig(); + when(configSelector.getAnyWorkingBaseConfig()).thenReturn(config); + + parallelExecutor = mock(ParallelExecutor.class); + + report = new ServerReport("example.com", "example.com", 443); + + probe = new ApplicationLayerProbe(configSelector, parallelExecutor); + } + + @Test + public void testProbeRegistersCorrectProperties() { + // Since we can't easily mock the network interaction, + // just test that the probe structure is correct + + // Execute without actually running network code + probe.merge(report); + + // Check that properties exist (even if with default values) + assertNotNull(report.getResult(TlsAnalyzedProperty.SPEAKS_HTTP)); + assertNotNull(report.getResult(TlsAnalyzedProperty.SUPPORTED_APPLICATIONS)); + } + + @Test + public void testMergeWithExistingApplications() { + // Test that the probe correctly merges with existing applications + List existingApps = new ArrayList<>(); + existingApps.add(ApplicationProtocol.ECHO); + report.putResult(TlsAnalyzedProperty.SUPPORTED_APPLICATIONS, existingApps); + + // Run merge without actual network test + probe.merge(report); + + @SuppressWarnings("unchecked") + List mergedApps = + (List) + report.getResult(TlsAnalyzedProperty.SUPPORTED_APPLICATIONS); + assertNotNull(mergedApps); + // Should still contain the existing app even without running the test + assertTrue(mergedApps.contains(ApplicationProtocol.ECHO)); + } +}