diff --git a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/probe/padding/PaddingOracleAttacker.java b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/probe/padding/PaddingOracleAttacker.java index 55d9db075..cf5129459 100644 --- a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/probe/padding/PaddingOracleAttacker.java +++ b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/probe/padding/PaddingOracleAttacker.java @@ -54,6 +54,8 @@ public class PaddingOracleAttacker { private long additionalTimeout = 1000; private long additionalTcpTimeout = 5000; private List fullResponseMap; + private static final int MAX_CONSECUTIVE_FAILURES = 20; + private static final double MAX_FAILURE_RATE = 0.75; public PaddingOracleAttacker( Config baseConfig, @@ -135,11 +137,49 @@ private List createVectorResponseList() { } List tempResponseVectorList = new LinkedList<>(); executor.bulkExecuteTasks(taskList); + + int consecutiveFailures = 0; + int totalFailures = 0; + int totalProcessed = 0; + for (FingerprintTaskVectorPair pair : stateVectorPairList) { + totalProcessed++; ResponseFingerprint fingerprint = null; if (pair.getFingerPrintTask().isHasError()) { LOGGER.warn("Could not extract fingerprint for " + pair.toString()); + consecutiveFailures++; + totalFailures++; + + // Check for early termination conditions + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + LOGGER.warn( + "Stopping padding oracle test due to {} consecutive failures. This may indicate connectivity issues or an unresponsive server.", + consecutiveFailures); + throw new AttackFailedException( + "Too many consecutive failures (" + + consecutiveFailures + + "). Aborting to prevent endless execution."); + } + + // Check overall failure rate + if (totalProcessed >= 10 + && (double) totalFailures / totalProcessed > MAX_FAILURE_RATE) { + LOGGER.warn( + "Stopping padding oracle test due to high failure rate: {}/{} ({}%)", + totalFailures, + totalProcessed, + String.format("%.2f", (double) totalFailures / totalProcessed * 100)); + throw new AttackFailedException( + "High failure rate detected (" + + totalFailures + + "/" + + totalProcessed + + "). Aborting to prevent endless execution."); + } } else { + // Reset consecutive failures on success + consecutiveFailures = 0; + testedSuite = pair.getFingerPrintTask() .getState() @@ -158,6 +198,18 @@ private List createVectorResponseList() { tempResponseVectorList.add(new VectorResponse(pair.getVector(), fingerprint)); } } + + // Log final statistics + if (totalFailures > 0) { + LOGGER.info( + "Padding oracle test completed with {}/{} failures ({}% success rate)", + totalFailures, + totalProcessed, + String.format( + "%.2f", + (double) (totalProcessed - totalFailures) / totalProcessed * 100)); + } + return tempResponseVectorList; } diff --git a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/PaddingOracleProbe.java b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/PaddingOracleProbe.java index a8c6acd99..83db76468 100644 --- a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/PaddingOracleProbe.java +++ b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/PaddingOracleProbe.java @@ -49,6 +49,8 @@ public class PaddingOracleProbe extends TlsServerProbe { private List serverSupportedSuites; private List> resultList = new LinkedList<>(); + private static final long MAX_PROBE_RUNTIME_MS = 1200000; // 20 minutes max per probe + private long probeStartTime; public PaddingOracleProbe(ConfigSelector configSelector, ParallelExecutor parallelExecutor) { super(parallelExecutor, TlsProbeType.PADDING_ORACLE, configSelector); @@ -69,6 +71,7 @@ public PaddingOracleProbe(ConfigSelector configSelector, ParallelExecutor parall @Override protected void executeTest() { LOGGER.debug("Starting evaluation"); + probeStartTime = System.currentTimeMillis(); List vectorTypeList = createVectorTypeList(); resultList = new LinkedList<>(); for (PaddingVectorGeneratorType vectorGeneratorType : vectorTypeList) { @@ -78,17 +81,35 @@ protected void executeTest() { if (!suite.isPsk() && suite.isCBC() && CipherSuite.getImplemented().contains(suite)) { + // Check if we've exceeded the maximum runtime + long currentRuntime = System.currentTimeMillis() - probeStartTime; + if (currentRuntime > MAX_PROBE_RUNTIME_MS) { + LOGGER.warn( + "Padding oracle probe exceeded maximum runtime of {} minutes. Skipping remaining tests.", + MAX_PROBE_RUNTIME_MS / 60000); + return; + } + PaddingRecordGeneratorType recordGeneratorType = scanDetail.isGreaterEqualTo(ScannerDetail.NORMAL) ? PaddingRecordGeneratorType.SHORT : PaddingRecordGeneratorType.VERY_SHORT; - resultList.add( - getPaddingOracleInformationLeakTest( - vectorGeneratorType, - recordGeneratorType, - numberOfIterations, - pair.getVersion(), - suite)); + try { + resultList.add( + getPaddingOracleInformationLeakTest( + vectorGeneratorType, + recordGeneratorType, + numberOfIterations, + pair.getVersion(), + suite)); + } catch (Exception e) { + LOGGER.warn( + "Failed to test padding oracle for {} with {}: {}", + suite, + pair.getVersion(), + e.getMessage()); + // Continue with next suite instead of failing completely + } } } } @@ -101,7 +122,24 @@ protected void executeTest() { for (InformationLeakTest fingerprint : resultList) { if (fingerprint.isDistinctAnswers() || scanDetail.isGreaterEqualTo(ScannerDetail.DETAILED)) { - extendFingerPrint(fingerprint, numberOfAddtionalIterations); + // Check runtime before extended evaluation + long currentRuntime = System.currentTimeMillis() - probeStartTime; + if (currentRuntime > MAX_PROBE_RUNTIME_MS) { + LOGGER.warn( + "Padding oracle probe exceeded maximum runtime during extended evaluation. Skipping remaining tests."); + break; + } + + try { + extendFingerPrint(fingerprint, numberOfAddtionalIterations); + } catch (Exception e) { + LOGGER.warn( + "Failed to extend fingerprint for {} with {}: {}", + fingerprint.getTestInfo().getCipherSuite(), + fingerprint.getTestInfo().getVersion(), + e.getMessage()); + // Continue with next fingerprint + } } } LOGGER.debug("Finished extended evaluation"); diff --git a/TLS-Server-Scanner/src/test/java/de/rub/nds/tlsscanner/serverscanner/probe/PaddingOracleProbeTest.java b/TLS-Server-Scanner/src/test/java/de/rub/nds/tlsscanner/serverscanner/probe/PaddingOracleProbeTest.java new file mode 100644 index 000000000..d81b31809 --- /dev/null +++ b/TLS-Server-Scanner/src/test/java/de/rub/nds/tlsscanner/serverscanner/probe/PaddingOracleProbeTest.java @@ -0,0 +1,176 @@ +/* + * 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.*; + +import de.rub.nds.scanner.core.config.ScannerDetail; +import de.rub.nds.scanner.core.probe.result.TestResults; +import de.rub.nds.tlsattacker.core.config.Config; +import de.rub.nds.tlsattacker.core.constants.CipherSuite; +import de.rub.nds.tlsattacker.core.constants.ProtocolVersion; +import de.rub.nds.tlsattacker.core.workflow.ParallelExecutor; +import de.rub.nds.tlsscanner.core.constants.TlsAnalyzedProperty; +import de.rub.nds.tlsscanner.core.leak.PaddingOracleTestInfo; +import de.rub.nds.tlsscanner.core.probe.result.VersionSuiteListPair; +import de.rub.nds.tlsscanner.core.vector.statistics.InformationLeakTest; +import de.rub.nds.tlsscanner.serverscanner.config.ServerScannerConfig; +import de.rub.nds.tlsscanner.serverscanner.report.ServerReport; +import de.rub.nds.tlsscanner.serverscanner.selector.ConfigSelector; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class PaddingOracleProbeTest { + + private PaddingOracleProbe probe; + private ConfigSelector configSelector; + private ParallelExecutor executor; + private ServerReport report; + private ServerScannerConfig scannerConfig; + + @BeforeEach + public void setUp() { + executor = Mockito.mock(ParallelExecutor.class); + Mockito.when(executor.getReexecutions()).thenReturn(1); + + scannerConfig = new ServerScannerConfig(); + scannerConfig.getExecutorConfig().setScanDetail(ScannerDetail.QUICK); + + configSelector = Mockito.mock(ConfigSelector.class); + Mockito.when(configSelector.getScannerConfig()).thenReturn(scannerConfig); + Mockito.when(configSelector.getBaseConfig()).thenReturn(Config.createConfig()); + + report = new ServerReport(); + + probe = new PaddingOracleProbe(configSelector, executor); + } + + @Test + public void testProbeCreation() { + assertNotNull(probe); + } + + @Test + public void testProbeRequiresBlockCiphers() { + ServerReport testReport = new ServerReport(); + testReport.putResult(TlsAnalyzedProperty.SUPPORTS_BLOCK_CIPHERS, TestResults.FALSE); + + assertFalse(probe.getRequirements().evaluate(testReport)); + + testReport.putResult(TlsAnalyzedProperty.SUPPORTS_BLOCK_CIPHERS, TestResults.TRUE); + assertTrue(probe.getRequirements().evaluate(testReport)); + } + + @Test + public void testProbeSkipsNonCbcCiphers() { + // Prepare report with non-CBC cipher suites + VersionSuiteListPair versionPair = + new VersionSuiteListPair( + ProtocolVersion.TLS12, + Arrays.asList( + CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, // GCM, not CBC + CipherSuite + .TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 // ChaCha20, not + // CBC + )); + + report.setVersionSuitePairs(Arrays.asList(versionPair)); + probe.adjustConfig(report); + probe.executeTest(); + + List> results = + (List>) probe.getCouldNotExecuteReason(); + + // Should not test any cipher suite as none are CBC + assertEquals(0, results != null ? results.size() : 0); + } + + @Test + public void testProbeTestsCbcCiphers() { + // Prepare report with CBC cipher suites + VersionSuiteListPair versionPair = + new VersionSuiteListPair( + ProtocolVersion.TLS12, + Arrays.asList( + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA256)); + + report.setVersionSuitePairs(Arrays.asList(versionPair)); + probe.adjustConfig(report); + + // Mock the executor to avoid actual network calls + Mockito.doNothing().when(executor).bulkExecuteTasks(Mockito.anyList()); + + // Execute with proper mocking would test CBC ciphers + // This test verifies the setup and filtering logic + assertDoesNotThrow(() -> probe.executeTest()); + } + + @Test + public void testProbeRespectsMaxRuntime() throws Exception { + // Use reflection to verify MAX_PROBE_RUNTIME_MS constant + Field maxRuntimeField = PaddingOracleProbe.class.getDeclaredField("MAX_PROBE_RUNTIME_MS"); + maxRuntimeField.setAccessible(true); + long maxRuntime = (long) maxRuntimeField.get(null); + + // 20 minutes = 1200000ms + assertEquals(1200000L, maxRuntime); + } + + @Test + public void testProbeHandlesEmptySuiteList() { + report.setVersionSuitePairs(Arrays.asList()); + probe.adjustConfig(report); + probe.executeTest(); + + // Should complete without errors + assertNotNull(probe.getCouldNotExecuteReason()); + } + + @Test + public void testProbeSkipsTls13() { + // TLS 1.3 doesn't use padding oracle vulnerable CBC mode + VersionSuiteListPair versionPair = + new VersionSuiteListPair( + ProtocolVersion.TLS13, Arrays.asList(CipherSuite.TLS_AES_128_GCM_SHA256)); + + report.setVersionSuitePairs(Arrays.asList(versionPair)); + probe.adjustConfig(report); + probe.executeTest(); + + List> results = + (List>) probe.getCouldNotExecuteReason(); + + // Should not test TLS 1.3 + assertEquals(0, results != null ? results.size() : 0); + } + + @Test + public void testProbeSkipsSsl() { + // SSL versions are skipped + VersionSuiteListPair versionPair = + new VersionSuiteListPair( + ProtocolVersion.SSL3, + Arrays.asList(CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA)); + + report.setVersionSuitePairs(Arrays.asList(versionPair)); + probe.adjustConfig(report); + probe.executeTest(); + + List> results = + (List>) probe.getCouldNotExecuteReason(); + + // Should not test SSL versions + assertEquals(0, results != null ? results.size() : 0); + } +}