Skip to content

Fix issue #113: Prevent PaddingOracleAttacker from running endlessly #136

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

Open
wants to merge 1 commit into
base: main
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 @@ -54,6 +54,8 @@ public class PaddingOracleAttacker {
private long additionalTimeout = 1000;
private long additionalTcpTimeout = 5000;
private List<VectorResponse> fullResponseMap;
private static final int MAX_CONSECUTIVE_FAILURES = 20;
private static final double MAX_FAILURE_RATE = 0.75;

public PaddingOracleAttacker(
Config baseConfig,
Expand Down Expand Up @@ -135,11 +137,49 @@ private List<VectorResponse> createVectorResponseList() {
}
List<VectorResponse> 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()
Expand All @@ -158,6 +198,18 @@ private List<VectorResponse> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class PaddingOracleProbe extends TlsServerProbe {

private List<VersionSuiteListPair> serverSupportedSuites;
private List<InformationLeakTest<PaddingOracleTestInfo>> 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);
Expand All @@ -69,6 +71,7 @@ public PaddingOracleProbe(ConfigSelector configSelector, ParallelExecutor parall
@Override
protected void executeTest() {
LOGGER.debug("Starting evaluation");
probeStartTime = System.currentTimeMillis();
List<PaddingVectorGeneratorType> vectorTypeList = createVectorTypeList();
resultList = new LinkedList<>();
for (PaddingVectorGeneratorType vectorGeneratorType : vectorTypeList) {
Expand All @@ -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
}
}
}
}
Expand All @@ -101,7 +122,24 @@ protected void executeTest() {
for (InformationLeakTest<PaddingOracleTestInfo> 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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<InformationLeakTest<PaddingOracleTestInfo>> results =
(List<InformationLeakTest<PaddingOracleTestInfo>>) 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<InformationLeakTest<PaddingOracleTestInfo>> results =
(List<InformationLeakTest<PaddingOracleTestInfo>>) 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<InformationLeakTest<PaddingOracleTestInfo>> results =
(List<InformationLeakTest<PaddingOracleTestInfo>>) probe.getCouldNotExecuteReason();

// Should not test SSL versions
assertEquals(0, results != null ? results.size() : 0);
}
}