-
-
Notifications
You must be signed in to change notification settings - Fork 748
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 6 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,39 @@ 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: URL-ENCODED BYPASS PAYLOADS FOR VULNERABLEAPP ===== | ||
| // These payloads specifically target VulnerableApp's filter bypasses | ||
|
|
||
|
||
| // Newline bypass payloads (%0A = newline) - bypasses semicolon/ampersand filters | ||
| NIX_OS_PAYLOADS.put("%0A" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| NIX_OS_PAYLOADS.put("%0a" + NIX_TEST_CMD, NIX_CTRL_PATTERN); // lowercase | ||
| WIN_OS_PAYLOADS.put("%0A" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("%0a" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // Carriage return + newline bypass (%0D%0A) | ||
| NIX_OS_PAYLOADS.put("%0D%0A" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| NIX_OS_PAYLOADS.put("%0d%0a" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("%0D%0A" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("%0d%0a" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // Tab character bypass (%09) | ||
| NIX_OS_PAYLOADS.put("%09" + NIX_TEST_CMD, NIX_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("%09" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // Double URL encoding bypasses (for Level 4+ filters) | ||
| NIX_OS_PAYLOADS.put("%250A" + NIX_TEST_CMD, NIX_CTRL_PATTERN); // double-encoded newline | ||
| NIX_OS_PAYLOADS.put("%2526" + NIX_TEST_CMD, NIX_CTRL_PATTERN); // double-encoded & | ||
| NIX_OS_PAYLOADS.put("%253B" + NIX_TEST_CMD, NIX_CTRL_PATTERN); // double-encoded ; | ||
| WIN_OS_PAYLOADS.put("%250A" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("%2526" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("%253B" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // Unicode bypass attempts | ||
| NIX_OS_PAYLOADS.put("%u000A" + NIX_TEST_CMD, NIX_CTRL_PATTERN); // Unicode newline | ||
| NIX_OS_PAYLOADS.put("%u0026" + NIX_TEST_CMD, NIX_CTRL_PATTERN); // Unicode & | ||
| WIN_OS_PAYLOADS.put("%u000A" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
| WIN_OS_PAYLOADS.put("%u0026" + WIN_TEST_CMD, WIN_CTRL_PATTERN); | ||
|
|
||
| // uninitialized variable waf bypass | ||
| String insertedCMD = insertUninitVar(NIX_TEST_CMD); | ||
| // No quote payloads | ||
|
|
@@ -202,7 +235,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 +245,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 +261,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 +314,27 @@ 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: URL-ENCODED TIMING-BASED BYPASS PAYLOADS ===== | ||
| // These specifically target VulnerableApp's timing-based detection with URL encoding | ||
|
|
||
|
||
| // Newline bypass for timing attacks | ||
| NIX_BLIND_OS_PAYLOADS.add("%0A" + NIX_BLIND_TEST_CMD); | ||
| NIX_BLIND_OS_PAYLOADS.add("%0a" + NIX_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("%0A" + WIN_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("%0a" + WIN_BLIND_TEST_CMD); | ||
|
|
||
| // Carriage return + newline for timing | ||
| NIX_BLIND_OS_PAYLOADS.add("%0D%0A" + NIX_BLIND_TEST_CMD); | ||
| NIX_BLIND_OS_PAYLOADS.add("%0d%0a" + NIX_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("%0D%0A" + WIN_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("%0d%0a" + WIN_BLIND_TEST_CMD); | ||
|
|
||
| // Double URL encoding for advanced filter bypass | ||
| NIX_BLIND_OS_PAYLOADS.add("%250A" + NIX_BLIND_TEST_CMD); | ||
| NIX_BLIND_OS_PAYLOADS.add("%2526" + NIX_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("%250A" + WIN_BLIND_TEST_CMD); | ||
| WIN_BLIND_OS_PAYLOADS.add("%2526" + WIN_BLIND_TEST_CMD); | ||
|
|
||
| // uninitialized variable waf bypass | ||
| String insertedCMD = insertUninitVar(NIX_BLIND_TEST_CMD); | ||
| // No quote payloads | ||
|
|
@@ -403,6 +456,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 +706,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 +740,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,124 @@ 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 (blocks semicolon, ampersand, space) | ||
| String test = "/vulnerableapp/level2/"; | ||
|
|
||
| nano.addHandler( | ||
| new NanoServerHandler(test) { | ||
| @Override | ||
| protected Response serve(IHTTPSession session) { | ||
| String value = getFirstParamValue(session, "ipaddress"); | ||
| String url = session.getUri() + "?" + session.getQueryParameterString(); | ||
|
|
||
| // Simulate VulnerableApp Level 2 filtering | ||
| Pattern blockPattern = Pattern.compile("[;& ]"); | ||
| if (value != null && !blockPattern.matcher(url).find()) { | ||
| // Allow newline-based command injection | ||
| if (value.contains("%0A") || value.contains("%0a")) { | ||
| 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)); | ||
| String attack = alertsRaised.get(0).getAttack(); | ||
| assertTrue(attack.contains("%0A") || attack.contains("%0a")); | ||
| assertThat(alertsRaised.get(0).getParam(), is(equalTo("ipaddress"))); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldDetectVulnerableAppLevel5WithAdvancedBypass() throws HttpMalformedHeaderException { | ||
| // Given - Test VulnerableApp Level 5 (also blocks %7C - pipe) | ||
| String test = "/vulnerableapp/level5/"; | ||
|
|
||
| nano.addHandler( | ||
| new NanoServerHandler(test) { | ||
| @Override | ||
| protected Response serve(IHTTPSession session) { | ||
| String value = getFirstParamValue(session, "ipaddress"); | ||
| String url = session.getUri() + "?" + session.getQueryParameterString(); | ||
|
|
||
| // Simulate VulnerableApp Level 5 filtering | ||
| Pattern blockPattern = Pattern.compile("[;& ]"); | ||
| if (value != null | ||
| && !blockPattern.matcher(url).find() | ||
| && !url.toUpperCase().contains("%26") | ||
| && !url.toUpperCase().contains("%3B") | ||
| && !url.toUpperCase().contains("%7C")) { | ||
| // Allow newline-based command injection (the key bypass!) | ||
| if (value.contains("%0A") || value.contains("%0a")) { | ||
| 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)); | ||
| String attack = alertsRaised.get(0).getAttack(); | ||
| assertTrue(attack.contains("%0A") || attack.contains("%0a")); | ||
| assertThat(alertsRaised.get(0).getParam(), is(equalTo("ipaddress"))); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldTestUrlEncodedNewlinePayloads() throws HttpMalformedHeaderException { | ||
| // Given - Test that our new URL-encoded payloads are actually being used | ||
| String test = "/newline-test/"; | ||
|
|
||
| nano.addHandler( | ||
| new NanoServerHandler(test) { | ||
| @Override | ||
| protected Response serve(IHTTPSession session) { | ||
| String value = getFirstParamValue(session, "param"); | ||
| // Only respond to URL-encoded newline attacks | ||
| if (value != null && (value.startsWith("%0A") || value.startsWith("%0a"))) { | ||
| 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 | ||
| assertThat(alertsRaised, hasSize(1)); | ||
| String attack = alertsRaised.get(0).getAttack(); | ||
| assertTrue(attack.startsWith("%0A") || attack.startsWith("%0a")); | ||
| assertThat(alertsRaised.get(0).getParam(), is(equalTo("param"))); | ||
| } | ||
| } | ||
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):