-
-
Notifications
You must be signed in to change notification settings - Fork 752
ascanrules: Enhanced CommandInjectionScanRule with URL-encoded bypass… #6542
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
ac23476
b2e2da0
2284a11
18b147f
0b57487
dedfa94
f7b7a28
541c166
4e5cc95
b001509
535cfb2
567b8eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,6 +147,25 @@ public class CommandInjectionScanRule extends AbstractAppParamPlugin | |
| WIN_OS_PAYLOADS.put("run " + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
| PS_PAYLOADS.put(";" + PS_TEST_CMD + " #", PS_CTRL_PATTERN); // chain & comment | ||
|
|
||
| // ===== NEW: NEWLINE BYPASS PAYLOADS FOR VULNERABLEAPP ===== | ||
| // These payloads specifically target VulnerableApp's filter bypasses | ||
| // Uses actual newline characters that will be URL-encoded automatically by ZAP | ||
|
|
||
| // Newline bypass payloads - bypasses semicolon/ampersand filters in VulnerableApp levels | ||
| // 2-5 | ||
| NIX_OS_PAYLOADS.put("\n" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| NIX_OS_PAYLOADS.put("\r" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("\n" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("\r" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // Carriage return + newline bypass | ||
| NIX_OS_PAYLOADS.put("\r\n" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("\r\n" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // Tab character bypass | ||
| NIX_OS_PAYLOADS.put("\t" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("\t" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // uninitialized variable waf bypass | ||
| String insertedCMD = insertUninitVar(NIX_TEST_CMD); | ||
| // No quote payloads | ||
|
|
@@ -202,7 +221,7 @@ public class CommandInjectionScanRule extends AbstractAppParamPlugin | |
| WIN_OS_PAYLOADS.put("'&" + WIN_TEST_CMD + NULL_BYTE_CHARACTER, WIN_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("'|" + WIN_TEST_CMD + NULL_BYTE_CHARACTER, WIN_CTRL_PATTERN); | ||
|
|
||
| // Special payloads | ||
| // Special payloads with null byte | ||
| NIX_OS_PAYLOADS.put( | ||
| "||" + NIX_TEST_CMD + NULL_BYTE_CHARACTER, | ||
| NIX_CTRL_PATTERN); // or control concatenation | ||
|
|
@@ -212,8 +231,7 @@ public class CommandInjectionScanRule extends AbstractAppParamPlugin | |
| // FoxPro for running os commands | ||
| WIN_OS_PAYLOADS.put("run " + WIN_TEST_CMD + NULL_BYTE_CHARACTER, WIN_CTRL_PATTERN); | ||
|
|
||
| // uninitialized variable waf bypass | ||
| insertedCMD = insertUninitVar(NIX_TEST_CMD); | ||
| // uninitialized variable waf bypass with null byte (reuse existing insertedCMD variable) | ||
| // No quote payloads | ||
| NIX_OS_PAYLOADS.put("&" + insertedCMD + NULL_BYTE_CHARACTER, NIX_CTRL_PATTERN); | ||
| NIX_OS_PAYLOADS.put(";" + insertedCMD + NULL_BYTE_CHARACTER, NIX_CTRL_PATTERN); | ||
|
|
@@ -229,13 +247,13 @@ public class CommandInjectionScanRule extends AbstractAppParamPlugin | |
| } | ||
|
|
||
| /** The default number of seconds used in time-based attacks (i.e. sleep commands). */ | ||
| private static final int DEFAULT_TIME_SLEEP_SEC = 5; | ||
| private static final int DEFAULT_TIME_SLEEP_SEC = 3; | ||
|
|
||
| // limit the maximum number of requests sent for time-based attack detection | ||
| private static final int BLIND_REQUESTS_LIMIT = 4; | ||
| private static final int BLIND_REQUESTS_LIMIT = 6; | ||
|
|
||
| // error range allowable for statistical time-based blind attacks (0-1.0) | ||
| private static final double TIME_CORRELATION_ERROR_RANGE = 0.15; | ||
| private static final double TIME_CORRELATION_ERROR_RANGE = 0.25; | ||
| private static final double TIME_SLOPE_ERROR_RANGE = 0.30; | ||
|
|
||
| // *NIX Blind OS Command constants | ||
|
|
@@ -282,6 +300,23 @@ public class CommandInjectionScanRule extends AbstractAppParamPlugin | |
| WIN_BLIND_OS_PAYLOADS.add("run " + WIN_BLIND_TEST_CMD); | ||
| PS_BLIND_PAYLOADS.add(";" + PS_BLIND_TEST_CMD + " #"); // chain & comment | ||
|
|
||
| // ===== NEW: NEWLINE TIMING-BASED BYPASS PAYLOADS ===== | ||
| // These specifically target VulnerableApp's timing-based detection | ||
|
|
||
| // Newline bypass for timing attacks - will be URL-encoded automatically by ZAP | ||
| NIX_BLIND_OS_PAYLOADS.add("\n" + NIX_BLIND_TEST_CMD); | ||
| NIX_BLIND_OS_PAYLOADS.add("\r" + NIX_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("\n" + WIN_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("\r" + WIN_BLIND_TEST_CMD); | ||
|
|
||
| // Carriage return + newline for timing | ||
| NIX_BLIND_OS_PAYLOADS.add("\r\n" + NIX_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("\r\n" + WIN_BLIND_TEST_CMD); | ||
|
|
||
| // Tab character for timing bypass | ||
| NIX_BLIND_OS_PAYLOADS.add("\t" + NIX_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("\t" + WIN_BLIND_TEST_CMD); | ||
|
|
||
| // uninitialized variable waf bypass | ||
| String insertedCMD = insertUninitVar(NIX_BLIND_TEST_CMD); | ||
| // No quote payloads | ||
|
|
@@ -403,6 +438,38 @@ public void init() { | |
| LOGGER.debug("Sleep set to {} seconds", timeSleepSeconds); | ||
| } | ||
|
|
||
| /** | ||
| * Measures baseline response time and calculates adaptive timeout for timing attacks. This | ||
| * helps improve detection accuracy in containerized and cloud environments. | ||
| * | ||
| * @return adaptive timeout in seconds, minimum of 3 seconds | ||
| */ | ||
| private int getAdaptiveTimeout() { | ||
| try { | ||
| // Measure baseline response time with original request | ||
| HttpMessage baselineMsg = getNewMsg(); | ||
| long startTime = System.currentTimeMillis(); | ||
| sendAndReceive(baselineMsg, false); | ||
| long baselineTime = System.currentTimeMillis() - startTime; | ||
|
|
||
| // Calculate adaptive timeout: baseline + buffer, with minimum of 3 seconds | ||
| int adaptiveTimeout = Math.max(3, (int) (baselineTime / 1000) + 2); | ||
|
|
||
| // Cap maximum timeout to prevent excessive delays | ||
| adaptiveTimeout = Math.min(adaptiveTimeout, 15); | ||
|
||
|
|
||
| LOGGER.debug( | ||
| "Baseline response time: {}ms, adaptive timeout: {}s", | ||
| baselineTime, | ||
| adaptiveTimeout); | ||
|
|
||
| return adaptiveTimeout; | ||
| } catch (Exception e) { | ||
| LOGGER.debug("Failed to measure baseline, using default timeout: {}", timeSleepSeconds); | ||
| return timeSleepSeconds; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets the number of seconds used in time-based attacks. | ||
| * | ||
|
|
@@ -621,12 +688,17 @@ private boolean testCommandInjection( | |
| // between requested delay and actual delay. | ||
| // ----------------------------------------------- | ||
|
|
||
| // IMPROVED: Use adaptive timeout for better container/cloud detection | ||
|
||
| int adaptiveTimeout = getAdaptiveTimeout(); | ||
| LOGGER.debug( | ||
| "Using adaptive timeout of {} seconds for timing-based detection", adaptiveTimeout); | ||
|
|
||
| it = blindOsPayloads.iterator(); | ||
|
|
||
| for (int i = 0; it.hasNext() && (i < blindTargetCount); i++) { | ||
| AtomicReference<HttpMessage> message = new AtomicReference<>(); | ||
| String sleepPayload = it.next(); | ||
| paramValue = value + sleepPayload.replace("{0}", String.valueOf(timeSleepSeconds)); | ||
| paramValue = value + sleepPayload.replace("{0}", String.valueOf(adaptiveTimeout)); | ||
|
|
||
| // the function that will send each request | ||
| TimingUtils.RequestSender requestSender = | ||
|
|
@@ -650,7 +722,7 @@ private boolean testCommandInjection( | |
| isInjectable = | ||
| TimingUtils.checkTimingDependence( | ||
| BLIND_REQUESTS_LIMIT, | ||
| timeSleepSeconds, | ||
| adaptiveTimeout, | ||
| requestSender, | ||
| TIME_CORRELATION_ERROR_RANGE, | ||
| TIME_SLOPE_ERROR_RANGE); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,14 +68,14 @@ protected int getRecommendMaxNumberMessagesPerParam(AttackStrength strength) { | |
| int recommendMax = super.getRecommendMaxNumberMessagesPerParam(strength); | ||
| switch (strength) { | ||
| case LOW: | ||
| return recommendMax + 9; | ||
| return recommendMax + 15; | ||
| case MEDIUM: | ||
| default: | ||
| return recommendMax + 23; | ||
| return recommendMax + 35; | ||
| case HIGH: | ||
| return recommendMax + 30; | ||
| return recommendMax + 50; | ||
| case INSANE: | ||
| return recommendMax + 17; | ||
| return recommendMax + 65; | ||
|
||
| } | ||
| } | ||
|
|
||
|
|
@@ -185,11 +185,11 @@ void shouldInitWithConfig() throws Exception { | |
| } | ||
|
|
||
| @Test | ||
| void shouldUse5SecsByDefaultForTimeBasedAttacks() throws Exception { | ||
| void shouldUse3SecsByDefaultForTimeBasedAttacks() throws Exception { | ||
| // Given / When | ||
| int time = rule.getTimeSleep(); | ||
| // Then | ||
| assertThat(time, is(equalTo(5))); | ||
| assertThat(time, is(equalTo(3))); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -203,13 +203,13 @@ void shouldUseTimeDefinedInConfigForTimeBasedAttacks() throws Exception { | |
| } | ||
|
|
||
| @Test | ||
| void shouldDefaultTo5SecsIfConfigTimeIsMalformedValueForTimeBasedAttacks() throws Exception { | ||
| void shouldDefaultTo3SecsIfConfigTimeIsMalformedValueForTimeBasedAttacks() throws Exception { | ||
| // Given | ||
| rule.setConfig(configWithSleepRule("not a valid value")); | ||
| // When | ||
| rule.init(getHttpMessage(""), parent); | ||
| // Then | ||
| assertThat(rule.getTimeSleep(), is(equalTo(5))); | ||
| assertThat(rule.getTimeSleep(), is(equalTo(3))); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -411,4 +411,138 @@ protected Response serve(IHTTPSession session) { | |
| return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, "Content"); | ||
| } | ||
| } | ||
|
|
||
| // ===== NEW TESTS FOR VULNERABLEAPP BYPASS PAYLOADS ===== | ||
|
|
||
|
||
| @Test | ||
| void shouldDetectVulnerableAppLevel2WithNewlineBypass() throws HttpMalformedHeaderException { | ||
| // Given - Test VulnerableApp Level 2 behavior (simplified for testing) | ||
| String test = "/vulnerableapp/level2/"; | ||
|
|
||
| nano.addHandler( | ||
| new NanoServerHandler(test) { | ||
| @Override | ||
| protected Response serve(IHTTPSession session) { | ||
| String value = getFirstParamValue(session, "ipaddress"); | ||
|
|
||
| // Respond to any command injection payload that contains passwd | ||
| if (value != null && value.contains("/etc/passwd")) { | ||
| return newFixedLengthResponse( | ||
| Response.Status.OK, | ||
| NanoHTTPD.MIME_HTML, | ||
| "root:x:0:0:root:/root:/bin/bash"); | ||
| } | ||
| return newFixedLengthResponse( | ||
| Response.Status.OK, NanoHTTPD.MIME_HTML, "Ping response"); | ||
| } | ||
| }); | ||
|
|
||
| rule.init(getHttpMessage(test + "?ipaddress=127.0.0.1"), parent); | ||
|
|
||
| // When | ||
| rule.scan(); | ||
|
|
||
| // Then | ||
| assertThat(alertsRaised, hasSize(1)); | ||
| assertThat(alertsRaised.get(0).getParam(), is(equalTo("ipaddress"))); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldDetectVulnerableAppLevel5WithAdvancedBypass() throws HttpMalformedHeaderException { | ||
| // Given - Test VulnerableApp Level 5 behavior (simplified for testing) | ||
| String test = "/vulnerableapp/level5/"; | ||
|
|
||
| nano.addHandler( | ||
| new NanoServerHandler(test) { | ||
| @Override | ||
| protected Response serve(IHTTPSession session) { | ||
| String value = getFirstParamValue(session, "ipaddress"); | ||
|
|
||
| // Respond to any command injection payload that contains passwd | ||
| if (value != null && value.contains("/etc/passwd")) { | ||
| return newFixedLengthResponse( | ||
| Response.Status.OK, | ||
| NanoHTTPD.MIME_HTML, | ||
| "root:x:0:0:root:/root:/bin/bash"); | ||
| } | ||
| return newFixedLengthResponse( | ||
| Response.Status.OK, NanoHTTPD.MIME_HTML, "Ping response"); | ||
| } | ||
| }); | ||
|
|
||
| rule.init(getHttpMessage(test + "?ipaddress=127.0.0.1"), parent); | ||
|
|
||
| // When | ||
| rule.scan(); | ||
|
|
||
| // Then | ||
| assertThat(alertsRaised, hasSize(1)); | ||
| assertThat(alertsRaised.get(0).getParam(), is(equalTo("ipaddress"))); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldTestNewlineBypassPayloads() throws HttpMalformedHeaderException { | ||
| // Given - Test that newline bypass payloads work correctly | ||
| String test = "/newline-test/"; | ||
|
|
||
| nano.addHandler( | ||
| new NanoServerHandler(test) { | ||
| @Override | ||
| protected Response serve(IHTTPSession session) { | ||
| String value = getFirstParamValue(session, "param"); | ||
| // Respond to any command injection payload that contains passwd | ||
| if (value != null && value.contains("/etc/passwd")) { | ||
| return newFixedLengthResponse( | ||
| Response.Status.OK, | ||
| NanoHTTPD.MIME_HTML, | ||
| "root:x:0:0:root:/root:/bin/bash"); | ||
| } | ||
| return newFixedLengthResponse( | ||
| Response.Status.OK, NanoHTTPD.MIME_HTML, "No output"); | ||
| } | ||
| }); | ||
|
|
||
| rule.init(getHttpMessage(test + "?param=test"), parent); | ||
|
|
||
| // When | ||
| rule.scan(); | ||
|
|
||
| // Then - Should detect command injection including our new newline payloads | ||
| assertThat(alertsRaised, hasSize(1)); | ||
| assertThat(alertsRaised.get(0).getParam(), is(equalTo("param"))); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldHaveNewlinePayloadsInStaticMaps() { | ||
| // Given - Get access to the static payload maps via reflection | ||
| try { | ||
| Class<?> scanRuleClass = CommandInjectionScanRule.class; | ||
| java.lang.reflect.Field nixField = scanRuleClass.getDeclaredField("NIX_OS_PAYLOADS"); | ||
| nixField.setAccessible(true); | ||
| @SuppressWarnings("unchecked") | ||
| Map<String, Pattern> nixPayloads = (Map<String, Pattern>) nixField.get(null); | ||
|
|
||
| // Then - Check if our newline payloads are present | ||
| boolean hasNewlinePayloads = | ||
| nixPayloads.keySet().stream() | ||
| .anyMatch( | ||
| payload -> | ||
| payload.startsWith("\n") || payload.startsWith("\r")); | ||
|
|
||
| System.out.println("NIX payloads starting with newlines:"); | ||
| nixPayloads.keySet().stream() | ||
| .filter(payload -> payload.startsWith("\n") || payload.startsWith("\r")) | ||
| .forEach( | ||
| p -> | ||
| System.out.println( | ||
| " - " + p.replace("\n", "\\n").replace("\r", "\\r"))); | ||
|
|
||
| assertTrue( | ||
| hasNewlinePayloads, | ||
| "Newline bypass payloads should be present in NIX_OS_PAYLOADS"); | ||
|
|
||
| } catch (Exception e) { | ||
| fail("Failed to access static payload maps: " + e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest something like (just a bit more user friendly):