Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions addOns/ascanrules/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Maintenance changes.
- Depends on an updated version of the Common Library add-on.
- ascanrules: Enhanced CommandInjectionScanRule with URL-encoded bypass payloads and adaptive timing detection for improved container/cloud environment compatibility.
Copy link
Member

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):

  • Enhanced the Remote OS Command Injection scan rule with URL-encoded bypass payloads and adaptive timing detection for improved container/cloud environment compatibility.


### Added
- Rules (as applicable) have been tagged in relation to HIPAA and PCI DSS.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the sectional comments are enough. These two and the space could be removed.

// 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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here re; comments and blank

// 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
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be combined and just do the assignment once.


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.
*
Expand Down Expand Up @@ -621,12 +706,17 @@ private boolean testCommandInjection(
// between requested delay and actual delay.
// -----------------------------------------------

// IMPROVED: Use adaptive timeout for better container/cloud detection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate you being specific to simplify the review, but these don't need to live on in the code base 😉

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 =
Expand All @@ -650,7 +740,7 @@ private boolean testCommandInjection(
isInjectable =
TimingUtils.checkTimingDependence(
BLIND_REQUESTS_LIMIT,
timeSleepSeconds,
adaptiveTimeout,
requestSender,
TIME_CORRELATION_ERROR_RANGE,
TIME_SLOPE_ERROR_RANGE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are significant increases to a rule which is already sending "too many" requests 😦

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, agree, i just understood this function too late, what is your suggestion here?

Copy link
Member

@psiinon psiinon Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cutting it down to just the best payloads which give us the widest coverage against real world apps 😁
Also, I think WAF bypass techniques could potentially apply to all rules, so it would be better to look at that as a separate enhancement, rather than adding them to each rule..

}
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -411,4 +411,124 @@ protected Response serve(IHTTPSession session) {
return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, "Content");
}
}

// ===== NEW TESTS FOR VULNERABLEAPP BYPASS PAYLOADS =====

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment and blank could go too I think

@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")));
}
}
Loading