Skip to content

Commit aa5816d

Browse files
committed
ascanrules: Path Traversal add details for dir match Alerts & reduce FPs
- CHANGELOG > Added change note. - Message.properties > Added key/value pair supporting the new Alert details. - PathTraversalScanRule > Updated to include Other Info on Alerts when applicable, and pre-check the original message response to reduce false positives. - PathTraversalScanRuleUnitTest > Updated to assert Other Info or lack thereof where applicable, also assure appropriate skipping due to pre-conditions. Signed-off-by: kingthorin <[email protected]>
1 parent c139f92 commit aa5816d

File tree

4 files changed

+137
-29
lines changed

4 files changed

+137
-29
lines changed

addOns/ascanrules/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
## Unreleased
77
### Changed
88
- The XML External Entity Attack scan rule now include example alert functionality for documentation generation purposes (Issue 6119).
9+
- The Path Traversal scan rule now includes further details when directory matches are made and pre-checks the original message to reduce false positives (Issue 8379).
910

1011
### Fixed
1112
- Added more checks for valid .htaccess files to reduce false positives (Issue 7632).

addOns/ascanrules/src/main/java/org/zaproxy/zap/extension/ascanrules/PathTraversalScanRule.java

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.parosproxy.paros.core.scanner.Category;
4141
import org.parosproxy.paros.network.HttpMessage;
4242
import org.zaproxy.addon.commonlib.CommonAlertTag;
43+
import org.zaproxy.addon.commonlib.ResourceIdentificationUtils;
4344
import org.zaproxy.addon.commonlib.vulnerabilities.Vulnerabilities;
4445
import org.zaproxy.addon.commonlib.vulnerabilities.Vulnerability;
4546
import org.zaproxy.addon.network.common.ZapSocketTimeoutException;
@@ -67,6 +68,7 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
6768
*/
6869
private static final ContentsMatcher WIN_PATTERN =
6970
new PatternContentsMatcher(Pattern.compile("\\[drivers\\]"));
71+
private static final String WIN_DIR_EVIDENCE = "Windows";
7072
private static final String[] WIN_LOCAL_FILE_TARGETS = {
7173
// Absolute Windows file retrieval (we suppose C:\\)
7274
"c:/Windows/system.ini",
@@ -116,6 +118,7 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
116118
// Dot used to match 'x' or '!' (used in AIX)
117119
private static final ContentsMatcher NIX_PATTERN =
118120
new PatternContentsMatcher(Pattern.compile("root:.:0:0"));
121+
private static final String NIX_DIR_EVIDENCE = "etc";
119122
private static final String[] NIX_LOCAL_FILE_TARGETS = {
120123
// Absolute file retrieval
121124
"/etc/passwd",
@@ -177,6 +180,8 @@ public class PathTraversalScanRule extends AbstractAppParamPlugin
177180
*/
178181
private static final String[] LOCAL_FILE_RELATIVE_PREFIXES = {"", "/", "\\"};
179182

183+
private static final List<String> DIR_EVIDENCE_LIST =
184+
List.of(NIX_DIR_EVIDENCE, WIN_DIR_EVIDENCE);
180185
/*
181186
* details of the vulnerability which we are attempting to find
182187
*/
@@ -221,6 +226,10 @@ public String getReference() {
221226
@Override
222227
public void scan(HttpMessage msg, String param, String value) {
223228

229+
if (!isGoodCandidate(getBaseMsg())) {
230+
return;
231+
}
232+
224233
try {
225234
// figure out how aggressively we should test
226235
int nixCount = 0;
@@ -555,6 +564,18 @@ public void scan(HttpMessage msg, String param, String value) {
555564
}
556565
}
557566

567+
private static boolean isGoodCandidate(HttpMessage msg) {
568+
if (ResourceIdentificationUtils.isJavaScript(msg)
569+
|| ResourceIdentificationUtils.isCss(msg)
570+
|| ResourceIdentificationUtils.responseContainsControlChars(msg)) {
571+
572+
return false;
573+
}
574+
return DirNamesContentsMatcher.matchNixDirectories(msg.getResponseBody().toString()) == null
575+
&& DirNamesContentsMatcher.matchWinDirectories(msg.getResponseBody().toString())
576+
== null;
577+
}
578+
558579
private boolean sendAndCheckPayload(
559580
String param, String newValue, ContentsMatcher contentsMatcher, int check)
560581
throws IOException {
@@ -644,12 +665,23 @@ private AlertBuilder createUnmatchedAlert(String param, String attack) {
644665

645666
private AlertBuilder createMatchedAlert(
646667
String param, String attack, String evidence, int check) {
647-
return newAlert()
648-
.setConfidence(Alert.CONFIDENCE_MEDIUM)
649-
.setParam(param)
650-
.setAttack(attack)
651-
.setEvidence(evidence)
652-
.setAlertRef(getId() + "-" + check);
668+
AlertBuilder builder =
669+
newAlert()
670+
.setConfidence(Alert.CONFIDENCE_MEDIUM)
671+
.setParam(param)
672+
.setAttack(attack)
673+
.setEvidence(evidence)
674+
.setAlertRef(getId() + "-" + check);
675+
if (DIR_EVIDENCE_LIST.contains(evidence)) {
676+
builder.setOtherInfo(
677+
Constant.messages.getString(
678+
MESSAGE_PREFIX + "info",
679+
evidence,
680+
evidence.equals(WIN_DIR_EVIDENCE)
681+
? DirNamesContentsMatcher.WIN_MATCHES
682+
: DirNamesContentsMatcher.NIX_MATCHES));
683+
}
684+
return builder;
653685
}
654686

655687
@Override
@@ -691,6 +723,24 @@ public String match(String contents) {
691723

692724
private static class DirNamesContentsMatcher implements ContentsMatcher {
693725

726+
public static final String NIX_MATCHES =
727+
String.join(", ", List.of("proc", NIX_DIR_EVIDENCE, "boot", "tmp", "home"));
728+
private static final Pattern PROC_PATT =
729+
Pattern.compile(
730+
"(?:^|\\W)proc(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
731+
private static final Pattern ETC_PATT =
732+
Pattern.compile(
733+
"(?:^|\\W)etc(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
734+
private static final Pattern BOOT_PATT =
735+
Pattern.compile(
736+
"(?:^|\\W)boot(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
737+
private static final Pattern TMP_PATT =
738+
Pattern.compile(
739+
"(?:^|\\W)tmp(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
740+
private static final Pattern HOME_PATT =
741+
Pattern.compile(
742+
"(?:^|\\W)home(?:\\W|$)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
743+
694744
@Override
695745
public String match(String contents) {
696746
String result = matchNixDirectories(contents);
@@ -701,36 +751,24 @@ public String match(String contents) {
701751
}
702752

703753
private static String matchNixDirectories(String contents) {
704-
Pattern procPattern =
705-
Pattern.compile("(?:^|\\W)proc(?:\\W|$)", Pattern.CASE_INSENSITIVE);
706-
Pattern etcPattern = Pattern.compile("(?:^|\\W)etc(?:\\W|$)", Pattern.CASE_INSENSITIVE);
707-
Pattern bootPattern =
708-
Pattern.compile("(?:^|\\W)boot(?:\\W|$)", Pattern.CASE_INSENSITIVE);
709-
Pattern tmpPattern = Pattern.compile("(?:^|\\W)tmp(?:\\W|$)", Pattern.CASE_INSENSITIVE);
710-
Pattern homePattern =
711-
Pattern.compile("(?:^|\\W)home(?:\\W|$)", Pattern.CASE_INSENSITIVE);
712-
713-
Matcher procMatcher = procPattern.matcher(contents);
714-
Matcher etcMatcher = etcPattern.matcher(contents);
715-
Matcher bootMatcher = bootPattern.matcher(contents);
716-
Matcher tmpMatcher = tmpPattern.matcher(contents);
717-
Matcher homeMatcher = homePattern.matcher(contents);
718-
719-
if (procMatcher.find()
720-
&& etcMatcher.find()
721-
&& bootMatcher.find()
722-
&& tmpMatcher.find()
723-
&& homeMatcher.find()) {
724-
return "etc";
754+
if (PROC_PATT.matcher(contents).find()
755+
&& ETC_PATT.matcher(contents).find()
756+
&& BOOT_PATT.matcher(contents).find()
757+
&& TMP_PATT.matcher(contents).find()
758+
&& HOME_PATT.matcher(contents).find()) {
759+
return NIX_DIR_EVIDENCE;
725760
}
726761

727762
return null;
728763
}
729764

765+
public static final String WIN_MATCHES =
766+
String.join(", ", List.of(WIN_DIR_EVIDENCE, "Program Files"));
767+
730768
private static String matchWinDirectories(String contents) {
731-
if (contents.contains("Windows")
769+
if (contents.contains(WIN_DIR_EVIDENCE)
732770
&& Pattern.compile("Program\\sFiles").matcher(contents).find()) {
733-
return "Windows";
771+
return WIN_DIR_EVIDENCE;
734772
}
735773

736774
return null;

addOns/ascanrules/src/main/resources/org/zaproxy/zap/extension/ascanrules/resources/Messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ ascanrules.parametertamper.desc = Parameter manipulation caused an error page or
114114
ascanrules.parametertamper.name = Parameter Tampering
115115
ascanrules.parametertamper.soln = Identify the cause of the error and fix it. Do not trust client side input and enforce a tight check in the server side. Besides, catch the exception properly. Use a generic 500 error page for internal server error.
116116

117+
ascanrules.pathtraversal.info = While the evidence field indicates {0}, the rule actually checked that the response contains matches for all of the following: {1}.
117118
ascanrules.pathtraversal.name = Path Traversal
118119

119120
ascanrules.payloader.desc = Provides support for custom payloads in scan rules.

addOns/ascanrules/src/test/java/org/zaproxy/zap/extension/ascanrules/PathTraversalScanRuleUnitTest.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse;
2323
import static org.hamcrest.MatcherAssert.assertThat;
24+
import static org.hamcrest.Matchers.emptyString;
2425
import static org.hamcrest.Matchers.equalTo;
2526
import static org.hamcrest.Matchers.greaterThan;
2627
import static org.hamcrest.Matchers.hasSize;
@@ -34,6 +35,7 @@
3435
import org.apache.commons.lang3.ArrayUtils;
3536
import org.junit.jupiter.api.Test;
3637
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.CsvSource;
3739
import org.junit.jupiter.params.provider.EnumSource;
3840
import org.junit.jupiter.params.provider.ValueSource;
3941
import org.parosproxy.paros.core.scanner.Alert;
@@ -42,6 +44,7 @@
4244
import org.parosproxy.paros.core.scanner.Plugin.AttackStrength;
4345
import org.parosproxy.paros.network.HttpMalformedHeaderException;
4446
import org.parosproxy.paros.network.HttpMessage;
47+
import org.parosproxy.paros.network.HttpResponseHeader;
4548
import org.zaproxy.addon.commonlib.CommonAlertTag;
4649
import org.zaproxy.zap.model.Tech;
4750
import org.zaproxy.zap.testutils.NanoServerHandler;
@@ -163,6 +166,13 @@ void shouldAlertIfAttackResponseListsWindowsDirectories() throws Exception {
163166
assertThat(alertsRaised.get(0).getAttack(), is(equalTo("c:/")));
164167
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
165168
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
169+
assertThat(
170+
alertsRaised.get(0).getOtherInfo(),
171+
is(
172+
equalTo(
173+
"While the evidence field indicates Windows, the rule actually "
174+
+ "checked that the response contains matches for all of the "
175+
+ "following: Windows, Program Files.")));
166176
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-3")));
167177
}
168178

@@ -181,6 +191,13 @@ void shouldAlertIfAttackResponseListsLinuxDirectories() throws Exception {
181191
assertThat(alertsRaised.get(0).getAttack(), is(equalTo("/")));
182192
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
183193
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
194+
assertThat(
195+
alertsRaised.get(0).getOtherInfo(),
196+
is(
197+
equalTo(
198+
"While the evidence field indicates etc, the rule actually "
199+
+ "checked that the response contains matches for all of the "
200+
+ "following: proc, etc, boot, tmp, home.")));
184201
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-3")));
185202
}
186203

@@ -199,6 +216,13 @@ void shouldAlertIfAttackResponseListsLinuxDirectoriesInPlainText() throws Except
199216
assertThat(alertsRaised.get(0).getAttack(), is(equalTo("/")));
200217
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
201218
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
219+
assertThat(
220+
alertsRaised.get(0).getOtherInfo(),
221+
is(
222+
equalTo(
223+
"While the evidence field indicates etc, the rule actually "
224+
+ "checked that the response contains matches for all of the "
225+
+ "following: proc, etc, boot, tmp, home.")));
202226
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-3")));
203227
}
204228

@@ -272,6 +296,7 @@ void shouldRaiseAlertIfResponseHasPasswdFileContentAndPayloadIsNullByteBased()
272296
// Then
273297
assertThat(alertsRaised, hasSize(1));
274298
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-2")));
299+
assertThat(alertsRaised.get(0).getOtherInfo(), is(emptyString()));
275300
}
276301

277302
@Test
@@ -288,6 +313,7 @@ void shouldRaiseAlertIfResponseHasSystemINIFileContentAndPayloadIsNullByteBased(
288313
// Then
289314
assertThat(alertsRaised, hasSize(1));
290315
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-1")));
316+
assertThat(alertsRaised.get(0).getOtherInfo(), is(emptyString()));
291317
}
292318

293319
@ParameterizedTest
@@ -308,6 +334,7 @@ void shouldAlertOnCheckFiveBelowHighThresholdUnderValidConditions(AlertThreshold
308334
assertThat(alertsRaised, hasSize(1));
309335
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_LOW)));
310336
assertThat(alertsRaised.get(0).getAlertRef(), is(equalTo("6-5")));
337+
assertThat(alertsRaised.get(0).getOtherInfo(), is(emptyString()));
311338
}
312339

313340
@Test
@@ -356,6 +383,47 @@ void shouldNotAlertOnCheckFiveAtLowThresholdUnderInvalidConditions(String errorT
356383
assertThat(alertsRaised, hasSize(0));
357384
}
358385

386+
@ParameterizedTest
387+
@ValueSource(strings = {"/styles.css", "/code.js"})
388+
void shouldSkipIfReqPathIsCssOrJs(String path) throws Exception {
389+
// Given
390+
rule.init(getHttpMessage(path + "?p=v"), parent);
391+
// When
392+
rule.scan();
393+
// Then
394+
assertThat(httpMessagesSent, hasSize(equalTo(0)));
395+
}
396+
397+
@ParameterizedTest
398+
@CsvSource({"/styles/, text/css", "/code, text/javascript"})
399+
void shouldSkipIfResponseIsCssOfJs(String path, String contentType) throws Exception {
400+
// Given
401+
HttpMessage msg = getHttpMessage(path + "?p=v");
402+
msg.getResponseHeader().setHeader(HttpResponseHeader.CONTENT_TYPE, contentType);
403+
rule.init(msg, parent);
404+
// When
405+
rule.scan();
406+
// Then
407+
assertThat(httpMessagesSent, hasSize(equalTo(0)));
408+
}
409+
410+
@ParameterizedTest
411+
@ValueSource(
412+
strings = {
413+
"etc root tmp bin boot dev home mnt opt proc",
414+
"Program Files Users Windows"
415+
})
416+
void shouldSkipIfResponseHasEvidenceToStartWith(String content) throws Exception {
417+
// Given
418+
HttpMessage msg = getHttpMessage("/?p=v");
419+
msg.setResponseBody(content);
420+
rule.init(msg, parent);
421+
// When
422+
rule.scan();
423+
// Then
424+
assertThat(httpMessagesSent, hasSize(equalTo(0)));
425+
}
426+
359427
private abstract static class ListDirsOnAttack extends NanoServerHandler {
360428

361429
private final String param;

0 commit comments

Comments
 (0)