Skip to content

Commit c285b4a

Browse files
committed
ascanrules: Oracle SQLi use DBMS_SESSION.SLEEP
Signed-off-by: kingthorin <[email protected]>
1 parent 44c26ef commit c285b4a

File tree

4 files changed

+178
-158
lines changed

4 files changed

+178
-158
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
- Update alert references to latest locations to fix 404s and resolve redirections.
9+
- The SQL Injection - Oracle (Time Based) rule now uses DBMS_SESSION.SLEEP instead of an "expensive" query.
910

1011
### Fixed
1112
- Hidden Files rule raising false positives if server returning 200 for files that don't exist (Issue 8434).

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

Lines changed: 136 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919
*/
2020
package org.zaproxy.zap.extension.ascanrules;
2121

22+
import java.io.IOException;
23+
import java.net.SocketException;
2224
import java.util.Collections;
2325
import java.util.HashMap;
2426
import java.util.Map;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
import org.apache.commons.configuration.ConversionException;
2529
import org.apache.logging.log4j.LogManager;
2630
import org.apache.logging.log4j.Logger;
2731
import org.parosproxy.paros.Constant;
@@ -31,6 +35,8 @@
3135
import org.parosproxy.paros.network.HttpMessage;
3236
import org.zaproxy.addon.commonlib.CommonAlertTag;
3337
import org.zaproxy.addon.commonlib.PolicyTag;
38+
import org.zaproxy.addon.commonlib.timing.TimingUtils;
39+
import org.zaproxy.zap.extension.ruleconfig.RuleConfigParam;
3440
import org.zaproxy.zap.model.Tech;
3541
import org.zaproxy.zap.model.TechSet;
3642

@@ -62,58 +68,69 @@
6268
public class SqlInjectionOracleTimingScanRule extends AbstractAppParamPlugin
6369
implements CommonActiveScanRuleInfo {
6470

65-
private int expectedDelayInMs = 5000;
71+
private int sleepInSeconds;
6672

6773
private int doTimeMaxRequests = 0;
6874

69-
/** Oracle one-line comment */
70-
public static final String SQL_ONE_LINE_COMMENT = " -- ";
71-
72-
/** the 5 second sleep function in Oracle SQL */
73-
private static String SQL_ORACLE_TIME_SELECT =
74-
"SELECT UTL_INADDR.get_host_name('10.0.0.1') from dual union SELECT UTL_INADDR.get_host_name('10.0.0.2') from dual union SELECT UTL_INADDR.get_host_name('10.0.0.3') from dual union SELECT UTL_INADDR.get_host_name('10.0.0.4') from dual union SELECT UTL_INADDR.get_host_name('10.0.0.5') from dual";
75-
76-
/** Oracle specific time based injection strings. each for 5 seconds */
77-
// Note: <<<<ORIGINALVALUE>>>> is replaced with the original parameter value at runtime in these
78-
// examples below (see * comment)
79-
// TODO: maybe add support for ')' after the original value, before the sleeps
80-
private static String[] SQL_ORACLE_TIME_REPLACEMENTS = {
81-
"(" + SQL_ORACLE_TIME_SELECT + ")",
82-
"<<<<ORIGINALVALUE>>>> / (" + SQL_ORACLE_TIME_SELECT + ") ",
83-
"<<<<ORIGINALVALUE>>>>' / (" + SQL_ORACLE_TIME_SELECT + ") / '",
84-
"<<<<ORIGINALVALUE>>>>\" / (" + SQL_ORACLE_TIME_SELECT + ") / \"",
85-
"<<<<ORIGINALVALUE>>>> and exists ("
86-
+ SQL_ORACLE_TIME_SELECT
75+
private static final String ORIG_VALUE_TOKEN = "<<<<ORIGINALVALUE>>>>";
76+
private static final String SLEEP_TOKEN = "<<<<SLEEP>>>>";
77+
private static final int DEFAULT_TIME_SLEEP_SEC = 5;
78+
private static final int BLIND_REQUEST_LIMIT = 4;
79+
// Error range allowable for statistical time-based blind attacks (0-1.0)
80+
private static final double TIME_CORRELATION_ERROR_RANGE = 0.15;
81+
private static final double TIME_SLOPE_ERROR_RANGE = 0.30;
82+
83+
private static String ORACLE_SLEEP_FUNCTION = "DBMS_SESSION.SLEEP(" + SLEEP_TOKEN + ")";
84+
85+
public static final String ORACLE_ONE_LINE_COMMENT = " -- ";
86+
87+
/** Oracle specific sleep injection strings. */
88+
private static String[] ORACLE_TIME_PAYLOADS = {
89+
"(" + ORACLE_SLEEP_FUNCTION + ")",
90+
ORIG_VALUE_TOKEN + " AND " + ORACLE_SLEEP_FUNCTION,
91+
ORIG_VALUE_TOKEN + " / (" + ORACLE_SLEEP_FUNCTION + ") ",
92+
ORIG_VALUE_TOKEN + "' / (" + ORACLE_SLEEP_FUNCTION + ") / '",
93+
ORIG_VALUE_TOKEN + "\" / (" + ORACLE_SLEEP_FUNCTION + ") / \"",
94+
ORIG_VALUE_TOKEN
95+
+ " and exists ("
96+
+ ORACLE_SLEEP_FUNCTION
8797
+ ")"
88-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
89-
"<<<<ORIGINALVALUE>>>>' and exists ("
90-
+ SQL_ORACLE_TIME_SELECT
98+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
99+
ORIG_VALUE_TOKEN
100+
+ "' and exists ("
101+
+ ORACLE_SLEEP_FUNCTION
91102
+ ")"
92-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
93-
"<<<<ORIGINALVALUE>>>>\" and exists ("
94-
+ SQL_ORACLE_TIME_SELECT
103+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
104+
ORIG_VALUE_TOKEN
105+
+ "\" and exists ("
106+
+ ORACLE_SLEEP_FUNCTION
95107
+ ")"
96-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
97-
"<<<<ORIGINALVALUE>>>>) and exists ("
98-
+ SQL_ORACLE_TIME_SELECT
108+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
109+
ORIG_VALUE_TOKEN
110+
+ ") and exists ("
111+
+ ORACLE_SLEEP_FUNCTION
99112
+ ")"
100-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
101-
"<<<<ORIGINALVALUE>>>> or exists ("
102-
+ SQL_ORACLE_TIME_SELECT
113+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
114+
ORIG_VALUE_TOKEN
115+
+ " or exists ("
116+
+ ORACLE_SLEEP_FUNCTION
103117
+ ")"
104-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
105-
"<<<<ORIGINALVALUE>>>>' or exists ("
106-
+ SQL_ORACLE_TIME_SELECT
118+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
119+
ORIG_VALUE_TOKEN
120+
+ "' or exists ("
121+
+ ORACLE_SLEEP_FUNCTION
107122
+ ")"
108-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
109-
"<<<<ORIGINALVALUE>>>>\" or exists ("
110-
+ SQL_ORACLE_TIME_SELECT
123+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
124+
ORIG_VALUE_TOKEN
125+
+ "\" or exists ("
126+
+ ORACLE_SLEEP_FUNCTION
111127
+ ")"
112-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
113-
"<<<<ORIGINALVALUE>>>>) or exists ("
114-
+ SQL_ORACLE_TIME_SELECT
128+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
129+
ORIG_VALUE_TOKEN
130+
+ ") or exists ("
131+
+ ORACLE_SLEEP_FUNCTION
115132
+ ")"
116-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
133+
+ ORACLE_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
117134
};
118135

119136
private static final Map<String, String> ALERT_TAGS;
@@ -187,106 +204,94 @@ public void init() {
187204
} else if (this.getAttackStrength() == AttackStrength.INSANE) {
188205
doTimeMaxRequests = 100;
189206
}
207+
208+
// Read the sleep value from the configs
209+
try {
210+
sleepInSeconds =
211+
this.getConfig()
212+
.getInt(RuleConfigParam.RULE_COMMON_SLEEP_TIME, DEFAULT_TIME_SLEEP_SEC);
213+
} catch (ConversionException e) {
214+
LOGGER.debug(
215+
"Invalid value for 'rules.common.sleep': {}",
216+
this.getConfig().getString(RuleConfigParam.RULE_COMMON_SLEEP_TIME));
217+
}
218+
LOGGER.debug("Sleep set to {} seconds", sleepInSeconds);
190219
}
191220

192221
@Override
193222
public void scan(HttpMessage originalMessage, String paramName, String paramValue) {
194-
195223
try {
196224
// Timing Baseline check: we need to get the time that it took the original query, to
197225
// know if the time based check is working correctly..
198-
HttpMessage msgTimeBaseline = getNewMsg();
199-
try {
200-
sendAndReceive(msgTimeBaseline, false); // do not follow redirects
201-
} catch (java.net.SocketTimeoutException e) {
202-
// to be expected occasionally, if the base query was one that contains some
203-
// parameters exploiting time based SQL injection?
204-
LOGGER.debug(
205-
"The Base Time Check timed out on [{}] URL [{}]",
206-
msgTimeBaseline.getRequestHeader().getMethod(),
207-
msgTimeBaseline.getRequestHeader().getURI());
208-
}
209-
long originalTimeUsed = msgTimeBaseline.getTimeElapsedMillis();
210-
211-
int countTimeBasedRequests = 0;
212-
213-
LOGGER.debug(
214-
"Scanning URL [{}] [{}], field [{}] with value [{}] for Oracle SQL Injection",
215-
getBaseMsg().getRequestHeader().getMethod(),
216-
getBaseMsg().getRequestHeader().getURI(),
217-
paramName,
218-
paramValue);
219-
220-
// Check for time based SQL Injection, using Oracle specific syntax
221-
for (int timeBasedSQLindex = 0;
222-
timeBasedSQLindex < SQL_ORACLE_TIME_REPLACEMENTS.length
226+
for (int timeBasedSQLindex = 0, countTimeBasedRequests = 0;
227+
timeBasedSQLindex < ORACLE_TIME_PAYLOADS.length
223228
&& countTimeBasedRequests < doTimeMaxRequests;
224-
timeBasedSQLindex++) {
225-
HttpMessage msgAttack = getNewMsg();
226-
String newTimeBasedInjectionValue =
227-
SQL_ORACLE_TIME_REPLACEMENTS[timeBasedSQLindex].replace(
228-
"<<<<ORIGINALVALUE>>>>", paramValue);
229-
setParameter(msgAttack, paramName, newTimeBasedInjectionValue);
230-
229+
timeBasedSQLindex++, countTimeBasedRequests++) {
230+
AtomicReference<HttpMessage> message = new AtomicReference<>();
231+
String payloadValue =
232+
ORACLE_TIME_PAYLOADS[timeBasedSQLindex].replace(
233+
ORIG_VALUE_TOKEN, paramValue);
234+
TimingUtils.RequestSender requestSender =
235+
x -> {
236+
HttpMessage timedMsg = getNewMsg();
237+
message.compareAndSet(null, timedMsg);
238+
String finalPayload =
239+
payloadValue.replace(SLEEP_TOKEN, String.valueOf((int) x));
240+
setParameter(timedMsg, paramName, finalPayload);
241+
sendAndReceive(timedMsg, false); // do not follow redirects
242+
return timedMsg.getTimeElapsedMillis() / 1000.0;
243+
};
244+
boolean isInjectable;
231245
try {
232-
sendAndReceive(msgAttack, false); // do not follow redirects
233-
countTimeBasedRequests++;
234-
} catch (java.net.SocketTimeoutException e) {
235-
// this is to be expected, if we start sending slow queries to the database.
236-
// ignore it in this case.. and just get the time.
237-
LOGGER.debug(
238-
"The time check query timed out on [{}] URL [{}] on field: [{}]",
239-
msgTimeBaseline.getRequestHeader().getMethod(),
240-
msgTimeBaseline.getRequestHeader().getURI(),
241-
paramName);
242-
}
243-
long modifiedTimeUsed = msgAttack.getTimeElapsedMillis();
244-
245-
LOGGER.debug(
246-
"Time Based SQL Injection test: [{}] on field: [{}] with value [{}] took {}ms, where the original took {}ms",
247-
newTimeBasedInjectionValue,
248-
paramName,
249-
newTimeBasedInjectionValue,
250-
modifiedTimeUsed,
251-
originalTimeUsed);
252-
253-
if (modifiedTimeUsed >= (originalTimeUsed + expectedDelayInMs)) {
254-
// takes more than 5 extra seconds => likely time based SQL injection.
255-
256-
// But first double check
257-
HttpMessage msgc = getNewMsg();
258246
try {
259-
sendAndReceive(msgc, false); // do not follow redirects
260-
} catch (Exception e) {
261-
// Ignore all exceptions
262-
}
263-
long checkTimeUsed = msgc.getTimeElapsedMillis();
264-
if (checkTimeUsed >= (originalTimeUsed + this.expectedDelayInMs - 200)) {
265-
// Looks like the server is overloaded, very unlikely this is a real issue
266-
continue;
247+
// Use TimingUtils to detect a response to sleep payloads
248+
isInjectable =
249+
TimingUtils.checkTimingDependence(
250+
BLIND_REQUEST_LIMIT,
251+
sleepInSeconds,
252+
requestSender,
253+
TIME_CORRELATION_ERROR_RANGE,
254+
TIME_SLOPE_ERROR_RANGE);
255+
} catch (SocketException ex) {
256+
LOGGER.debug(
257+
"Caught {} {} when accessing: {}.\n The target may have replied with a poorly formed redirect due to our input.",
258+
ex.getClass().getName(),
259+
ex.getMessage(),
260+
message.get().getRequestHeader().getURI());
261+
continue; // Something went wrong, move to next blind iteration
267262
}
268263

269-
newAlert()
270-
.setConfidence(Alert.CONFIDENCE_MEDIUM)
271-
.setUri(getBaseMsg().getRequestHeader().getURI().toString())
272-
.setParam(paramName)
273-
.setAttack(newTimeBasedInjectionValue)
274-
.setOtherInfo(
275-
Constant.messages.getString(
276-
"ascanrules.sqlinjection.alert.timebased.extrainfo",
277-
newTimeBasedInjectionValue,
278-
modifiedTimeUsed,
279-
paramValue,
280-
originalTimeUsed))
281-
.setMessage(msgAttack)
282-
.raise();
283-
284-
LOGGER.debug(
285-
"A likely Time Based SQL Injection Vulnerability has been found with [{}] URL [{}] on field: [{}]",
286-
msgAttack.getRequestHeader().getMethod(),
287-
msgAttack.getRequestHeader().getURI(),
288-
paramName);
289-
return;
264+
if (isInjectable) {
265+
String finalPayloadValue =
266+
payloadValue.replace(SLEEP_TOKEN, String.valueOf(sleepInSeconds));
267+
LOGGER.debug(
268+
"[Time Based Oracle SQL Injection - Found] on parameter [{}] with value [{}]",
269+
paramName,
270+
paramValue);
271+
272+
newAlert()
273+
.setConfidence(Alert.CONFIDENCE_MEDIUM)
274+
.setName(getName() + " - Time Based")
275+
.setUri(getBaseMsg().getRequestHeader().getURI().toString())
276+
.setParam(paramName)
277+
.setAttack(finalPayloadValue)
278+
.setMessage(message.get())
279+
.setOtherInfo(
280+
Constant.messages.getString(
281+
"ascanrules.sqlinjection.alert.timebased.extrainfo",
282+
finalPayloadValue,
283+
message.get().getTimeElapsedMillis(),
284+
paramValue,
285+
getBaseMsg().getTimeElapsedMillis()))
286+
.raise();
287+
return;
288+
}
289+
} catch (IOException ex) {
290+
LOGGER.warn(
291+
"Time based Oracle SQL Injection vulnerability check failed for parameter [{}] and payload [{}] due to an I/O error",
292+
paramName,
293+
paramValue,
294+
ex);
290295
}
291296
}
292297

@@ -296,8 +301,8 @@ public void scan(HttpMessage originalMessage, String paramName, String paramValu
296301
}
297302
}
298303

299-
public void setExpectedDelayInMs(int delay) {
300-
expectedDelayInMs = delay;
304+
public void setSleepInSeconds(int sleep) {
305+
this.sleepInSeconds = sleep;
301306
}
302307

303308
@Override

addOns/ascanrules/src/main/javahelp/org/zaproxy/zap/extension/ascanrules/resources/help/contents/ascanrules.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,9 @@ <H2 id="id-40021">SQL Injection - Oracle (Time Based)</H2>
413413
load delays rather than by SQL injection delays. <br>
414414
The scan rule tests only for time-based SQL injection vulnerabilities.<br>
415415
<br>
416-
Note that this rule does not currently allow you to change the length of time used for the timing attacks due to the way the delay is caused.
416+
The scan rule tests only for time-based SQL injection vulnerabilities.<br>
417+
<p>
418+
Post 2.5.0 you can change the length of time used for the attack by changing the <code>rules.common.sleep</code> parameter via the Options 'Rule configuration' panel.
417419
<p>
418420
Latest code: <a href="https://github.com/zaproxy/zap-extensions/blob/main/addOns/ascanrules/src/main/java/org/zaproxy/zap/extension/ascanrules/SqlInjectionOracleTimingScanRule.java">SqlInjectionOracleTimingScanRule.java</a>
419421
<br>

0 commit comments

Comments
 (0)