Skip to content

Commit 89c1c33

Browse files
committed
Add checking for location header
In case of redirection after authentication, the body may be empty and content comparison won't work. By adding new checking for location header, we may catch potential vulnerability especially in case of OR true injection. Signed-off-by: koneko096 <[email protected]>
1 parent 7abc822 commit 89c1c33

File tree

3 files changed

+148
-10
lines changed

3 files changed

+148
-10
lines changed

addOns/ascanrules/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1010
### Fixed
1111
- The Remote File Inclusion scan rule no longer follows redirects before checking the response for content indicating a vulnerability (Issue 5887).
1212
- False positive where Cross Site Scripting payloads are safely rendered in a textarea tag.
13+
- SQL Injection will now recognize authentication bypass if redirected to different page (Issue 6883).
1314

1415
## [46] - 2022-03-21
1516
### Changed

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

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@
3636
import java.util.regex.Pattern;
3737
import org.apache.commons.httpclient.InvalidRedirectLocationException;
3838
import org.apache.commons.httpclient.URI;
39+
import org.apache.commons.lang3.StringUtils;
3940
import org.apache.logging.log4j.LogManager;
4041
import org.apache.logging.log4j.Logger;
4142
import org.parosproxy.paros.Constant;
4243
import org.parosproxy.paros.control.Control;
4344
import org.parosproxy.paros.core.scanner.AbstractAppParamPlugin;
4445
import org.parosproxy.paros.core.scanner.Alert;
4546
import org.parosproxy.paros.core.scanner.Category;
47+
import org.parosproxy.paros.network.HttpHeader;
4648
import org.parosproxy.paros.network.HttpMessage;
4749
import org.zaproxy.addon.commonlib.CommonAlertTag;
4850
import org.zaproxy.zap.extension.authentication.ExtensionAuthentication;
@@ -937,6 +939,9 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
937939
mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString();
938940
mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue);
939941

942+
String locationHeaderNormal =
943+
refreshedmessage.getResponseHeader().getHeader(HttpHeader.LOCATION);
944+
940945
// boolean booleanBasedSqlInjectionFoundForParam = false;
941946

942947
// try each of the AND syntax values in turn.
@@ -980,6 +985,8 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
980985
String resBodyANDTrueStripped =
981986
stripOffOriginalAndAttackParam(
982987
resBodyANDTrueUnstripped, origParamValue, sqlBooleanAndTrueValue);
988+
String headerANDTrue =
989+
msg2.getResponseHeader().getHeader(HttpHeader.LOCATION);
983990

984991
// set up two little arrays to ease the work of checking the unstripped output, and
985992
// then the stripped output
@@ -1037,6 +1044,8 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
10371044
resBodyANDFalseUnstripped,
10381045
origParamValue,
10391046
sqlBooleanAndFalseValue);
1047+
String headerANDFalse =
1048+
msg2_and_false.getResponseHeader().getHeader(HttpHeader.LOCATION);
10401049

10411050
String andFalseBodyOutput[] = {
10421051
resBodyANDFalseUnstripped, resBodyANDFalseStripped
@@ -1098,8 +1107,22 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
10981107

10991108
sqlInjectionFoundForUrl = true;
11001109

1101-
break; // No further need to loop through SQL_AND
1110+
break; // No further need to loop through header check
11021111

1112+
} else if (StringUtils.compare(locationHeaderNormal, headerANDTrue) == 0 &&
1113+
StringUtils.compare(headerANDTrue, headerANDFalse) != 0) {
1114+
// or we get redirected, likely an SQL Injection too
1115+
sqlInjectionAttack = sqlBooleanAndTrueValue;
1116+
newAlert()
1117+
.setConfidence(Alert.CONFIDENCE_MEDIUM)
1118+
.setParam(param)
1119+
.setAttack(sqlInjectionAttack)
1120+
.setMessage(msg2)
1121+
.raise();
1122+
1123+
sqlInjectionFoundForUrl = true;
1124+
1125+
break; // No further need to loop through SQL_AND
11031126
} else {
11041127
// the results of the always false condition are the same as for the
11051128
// original unmodified parameter
@@ -1146,14 +1169,17 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
11461169
String resBodyORTrueStripped =
11471170
stripOffOriginalAndAttackParam(
11481171
resBodyORTrueUnstripped, origParamValue, orValue);
1172+
String headerORTrue =
1173+
msg2_or_true.getResponseHeader().getHeader(HttpHeader.LOCATION);
11491174

11501175
String orTrueBodyOutput[] = {
11511176
resBodyORTrueUnstripped, resBodyORTrueStripped
11521177
};
11531178

11541179
int compareOrToOriginal =
11551180
orTrueBodyOutput[booleanStrippedUnstrippedIndex].compareTo(
1156-
normalBodyOutput[booleanStrippedUnstrippedIndex]);
1181+
normalBodyOutput[booleanStrippedUnstrippedIndex]) |
1182+
StringUtils.compare(locationHeaderNormal, headerORTrue);
11571183
if (compareOrToOriginal != 0) {
11581184
log.debug(
11591185
"Check 2, {} html output for OR TRUE condition [{}] different to (refreshed) original results for {}",
@@ -1306,12 +1332,14 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
13061332
countBooleanBasedRequests++;
13071333

13081334
String resBodyORTrueUnstripped = msg2.getResponseBody().toString();
1335+
String locationHeaderORTrue =
1336+
msg2.getResponseHeader().getHeader(HttpHeader.LOCATION);
13091337

13101338
// if the results of the "OR 1=1" exceed the original query (unstripped, by more
1311-
// than a 20% size difference, say), we may be onto something.
1339+
// than a 20% size difference, say) or got redirected, we may be onto something.
13121340
// TODO: change the percentage difference threshold based on the alert threshold
1313-
if ((resBodyORTrueUnstripped.length()
1314-
> (mResBodyNormalUnstripped.length() * 1.2))) {
1341+
if ((resBodyORTrueUnstripped.length() > (mResBodyNormalUnstripped.length() * 1.2))
1342+
|| (StringUtils.compare(locationHeaderNormal, locationHeaderORTrue) != 0)) {
13151343
log.debug(
13161344
"Check 2a, unstripped html output for OR TRUE condition [{}] produced sufficiently larger results than the original message",
13171345
sqlBooleanOrTrueValue);
@@ -1337,14 +1365,21 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
13371365
resBodyANDFalseUnstripped,
13381366
origParamValue,
13391367
sqlBooleanAndFalseValue);
1368+
String headerANDFalse =
1369+
msg2_and_false.getResponseHeader().getHeader(HttpHeader.LOCATION);
13401370

13411371
// does the "AND 1=2" version produce the same as the original (for
13421372
// stripped/unstripped versions)
13431373
boolean verificationUsingUnstripped =
13441374
resBodyANDFalseUnstripped.compareTo(mResBodyNormalUnstripped) == 0;
13451375
boolean verificationUsingStripped =
13461376
resBodyANDFalseStripped.compareTo(mResBodyNormalStripped) == 0;
1347-
if (verificationUsingUnstripped || verificationUsingStripped) {
1377+
boolean verificationUsingLocationHeader =
1378+
StringUtils.compare(headerANDFalse, locationHeaderORTrue) != 0;
1379+
1380+
if (verificationUsingUnstripped
1381+
|| verificationUsingStripped
1382+
|| verificationUsingLocationHeader) {
13481383
log.debug(
13491384
"Check 2, {} html output for AND FALSE condition [{}] matches the (refreshed) original results",
13501385
(verificationUsingStripped ? "STRIPPED" : "UNSTRIPPED"),
@@ -1358,7 +1393,7 @@ && matchBodyPattern(msg1, errorPattern, sb)) {
13581393
sqlBooleanOrTrueValue,
13591394
sqlBooleanAndFalseValue,
13601395
"");
1361-
} else {
1396+
} else if (verificationUsingUnstripped) {
13621397
extraInfo =
13631398
Constant.messages.getString(
13641399
MESSAGE_PREFIX + "alert.booleanbased.extrainfo",

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

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Zed Attack Proxy (ZAP) and its related class files.
2+
* Zed Attack Proxy (ZAP) `and `its related class files.
33
*
44
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
55
*
@@ -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.containsString;
2425
import static org.hamcrest.Matchers.equalTo;
2526
import static org.hamcrest.Matchers.greaterThan;
2627
import static org.hamcrest.Matchers.hasSize;
@@ -29,10 +30,13 @@
2930
import fi.iki.elonen.NanoHTTPD;
3031
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
3132
import fi.iki.elonen.NanoHTTPD.Response;
33+
34+
import java.util.Collections;
3235
import java.util.Map;
3336
import org.junit.jupiter.api.Test;
3437
import org.parosproxy.paros.core.scanner.Alert;
3538
import org.parosproxy.paros.core.scanner.Plugin.AttackStrength;
39+
import org.parosproxy.paros.network.HttpHeader;
3640
import org.zaproxy.addon.commonlib.CommonAlertTag;
3741
import org.zaproxy.zap.model.Tech;
3842
import org.zaproxy.zap.model.TechSet;
@@ -132,6 +136,60 @@ void shouldNotTargetNonDbTechs() {
132136
assertThat(targets, is(equalTo(false)));
133137
}
134138

139+
@Test
140+
void shouldAlertIfTrueExpressionsAreSuccessful() throws Exception {
141+
// Given
142+
String param = "id";
143+
nano.addHandler(new ConditionBasedHandler(
144+
"/",
145+
param,
146+
ConditionBasedHandler.Condition.AND_FALSE.expression));
147+
148+
rule.init(
149+
getHttpMessage("GET", NanoHTTPD.MIME_HTML, "/?" + param + "=admin",
150+
"Some content"),
151+
parent);
152+
// When
153+
rule.scan();
154+
// Then
155+
assertThat(httpMessagesSent, hasSize(greaterThan(1)));
156+
assertThat(alertsRaised, hasSize(1));
157+
assertThat(alertsRaised.get(0).getEvidence(), is(equalTo("")));
158+
assertThat(alertsRaised.get(0).getParam(), is(equalTo(param)));
159+
assertThat(
160+
alertsRaised.get(0).getAttack(),
161+
is(containsString(ConditionBasedHandler.Condition.AND_TRUE.expression)));
162+
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
163+
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
164+
}
165+
166+
@Test
167+
void shouldAlertIfFalseExpressionsAreSuccessful() throws Exception {
168+
// Given
169+
String param = "id";
170+
nano.addHandler(new ConditionBasedHandler(
171+
"/",
172+
param,
173+
ConditionBasedHandler.Condition.OR_TRUE.expression));
174+
175+
rule.init(
176+
getHttpMessage("GET", NanoHTTPD.MIME_HTML, "/?" + param + "=admin",
177+
"Some content"),
178+
parent);
179+
// When
180+
rule.scan();
181+
// Then
182+
assertThat(httpMessagesSent, hasSize(greaterThan(1)));
183+
assertThat(alertsRaised, hasSize(1));
184+
assertThat(alertsRaised.get(0).getEvidence(), is(equalTo("")));
185+
assertThat(alertsRaised.get(0).getParam(), is(equalTo(param)));
186+
assertThat(
187+
alertsRaised.get(0).getAttack(),
188+
is(containsString(ConditionBasedHandler.Condition.OR_TRUE.expression)));
189+
assertThat(alertsRaised.get(0).getRisk(), is(equalTo(Alert.RISK_HIGH)));
190+
assertThat(alertsRaised.get(0).getConfidence(), is(equalTo(Alert.CONFIDENCE_MEDIUM)));
191+
}
192+
135193
@Test
136194
void shouldAlertIfSumExpressionsAreSuccessful() throws Exception {
137195
// Given
@@ -350,12 +408,12 @@ public ExpressionBasedHandler(
350408
}
351409

352410
public ExpressionBasedHandler(
353-
String parth,
411+
String path,
354412
String param,
355413
Expression expression,
356414
boolean confirmationFails,
357415
String contentAddition) {
358-
this(parth, param, expression, confirmationFails);
416+
this(path, param, expression, confirmationFails);
359417
this.contentAddition = contentAddition;
360418
}
361419

@@ -381,4 +439,48 @@ protected String getContent(String value) {
381439
return "Some Content " + contentAddition;
382440
}
383441
}
442+
443+
private static class ConditionBasedHandler extends NanoServerHandler {
444+
445+
public enum Condition {
446+
AND_TRUE( " AND 1=1 --"),
447+
AND_FALSE( " AND 1=2 --"),
448+
OR_TRUE(" OR 1=1 --");
449+
450+
private final String expression;
451+
452+
Condition(String expression) {
453+
this.expression = expression;
454+
}
455+
}
456+
457+
private final String param;
458+
459+
private final String confirmationExpression;
460+
461+
public ConditionBasedHandler(String name,
462+
String param,
463+
String confirmationExpression) {
464+
super(name);
465+
this.param = param;
466+
this.confirmationExpression = confirmationExpression;
467+
}
468+
469+
@Override
470+
protected Response serve(IHTTPSession session) {
471+
String value = getFirstParamValue(session, param);
472+
if (value.contains(confirmationExpression)) {
473+
Response response =
474+
newFixedLengthResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML, getContent());
475+
response.addHeader("Location", "http://somewhere");
476+
return response;
477+
}
478+
return newFixedLengthResponse(
479+
Response.Status.OK, NanoHTTPD.MIME_HTML, getContent());
480+
}
481+
482+
protected String getContent() {
483+
return "Some content";
484+
}
485+
}
384486
}

0 commit comments

Comments
 (0)