Skip to content

Commit 7cff8f3

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

File tree

4 files changed

+167
-170
lines changed

4 files changed

+167
-170
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: 124 additions & 144 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;
2223
import java.util.Collections;
2324
import java.util.HashMap;
2425
import java.util.Map;
26+
import java.util.concurrent.TimeUnit;
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

@@ -51,69 +57,75 @@
5157
* requires a table name in normal select statements (like Hypersonic: cannot just say "select 1" or
5258
* "select 2" like in most RDBMSs - requires a table name in "union select" statements (like
5359
* Hypersonic). - does NOT allow stacked queries via JDBC driver or in PHP. - Constants in select
54-
* must be in single quotes, not doubles (like Hypersonic). - supports UDFs (very interesting!!) - 5
55-
* second delay select statement: SELECT UTL_INADDR.get_host_name('10.0.0.1') from dual union SELECT
56-
* UTL_INADDR.get_host_name('10.0.0.2') from dual union SELECT UTL_INADDR.get_host_name('10.0.0.3')
57-
* from dual union SELECT UTL_INADDR.get_host_name('10.0.0.4') from dual union SELECT
58-
* UTL_INADDR.get_host_name('10.0.0.5') from dual - metadata select statement: TODO
60+
* must be in single quotes, not doubles (like Hypersonic). - supports UDFs (very interesting!!) -
61+
* metadata select statement: TODO
5962
*
6063
* @author 70pointer
6164
*/
6265
public class SqlInjectionOracleTimingScanRule extends AbstractAppParamPlugin
6366
implements CommonActiveScanRuleInfo {
6467

65-
private int expectedDelayInMs = 5000;
68+
private int sleepInSeconds;
6669

6770
private int doTimeMaxRequests = 0;
6871

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
72+
private static final String ORIG_VALUE_TOKEN = "<<<<ORIGINALVALUE>>>>";
73+
private static final String SLEEP_TOKEN = "<<<<SLEEP>>>>";
74+
private static final int DEFAULT_TIME_SLEEP_SEC = 5;
75+
private static final int BLIND_REQUEST_LIMIT = 4;
76+
// Error range allowable for statistical time-based blind attacks (0-1.0)
77+
private static final double TIME_CORRELATION_ERROR_RANGE = 0.15;
78+
private static final double TIME_SLOPE_ERROR_RANGE = 0.30;
79+
80+
private static String SLEEP_FUNCTION = "DBMS_SESSION.SLEEP(" + SLEEP_TOKEN + ")";
81+
82+
public static final String ONE_LINE_COMMENT = " -- ";
83+
84+
private static String[] PAYLOADS = {
85+
"(" + SLEEP_FUNCTION + ")",
86+
ORIG_VALUE_TOKEN + " / (" + SLEEP_FUNCTION + ") ",
87+
ORIG_VALUE_TOKEN + "' / (" + SLEEP_FUNCTION + ") / '",
88+
ORIG_VALUE_TOKEN + "\" / (" + SLEEP_FUNCTION + ") / \"",
89+
ORIG_VALUE_TOKEN
90+
+ " and exists ("
91+
+ SLEEP_FUNCTION
8792
+ ")"
88-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
89-
"<<<<ORIGINALVALUE>>>>' and exists ("
90-
+ SQL_ORACLE_TIME_SELECT
93+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
94+
ORIG_VALUE_TOKEN
95+
+ "' and exists ("
96+
+ SLEEP_FUNCTION
9197
+ ")"
92-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
93-
"<<<<ORIGINALVALUE>>>>\" and exists ("
94-
+ SQL_ORACLE_TIME_SELECT
98+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
99+
ORIG_VALUE_TOKEN
100+
+ "\" and exists ("
101+
+ SLEEP_FUNCTION
95102
+ ")"
96-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
97-
"<<<<ORIGINALVALUE>>>>) and exists ("
98-
+ SQL_ORACLE_TIME_SELECT
103+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
104+
ORIG_VALUE_TOKEN
105+
+ ") and exists ("
106+
+ SLEEP_FUNCTION
99107
+ ")"
100-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
101-
"<<<<ORIGINALVALUE>>>> or exists ("
102-
+ SQL_ORACLE_TIME_SELECT
108+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
109+
ORIG_VALUE_TOKEN
110+
+ " or exists ("
111+
+ SLEEP_FUNCTION
103112
+ ")"
104-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
105-
"<<<<ORIGINALVALUE>>>>' or exists ("
106-
+ SQL_ORACLE_TIME_SELECT
113+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
114+
ORIG_VALUE_TOKEN
115+
+ "' or exists ("
116+
+ SLEEP_FUNCTION
107117
+ ")"
108-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
109-
"<<<<ORIGINALVALUE>>>>\" or exists ("
110-
+ SQL_ORACLE_TIME_SELECT
118+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
119+
ORIG_VALUE_TOKEN
120+
+ "\" or exists ("
121+
+ SLEEP_FUNCTION
111122
+ ")"
112-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
113-
"<<<<ORIGINALVALUE>>>>) or exists ("
114-
+ SQL_ORACLE_TIME_SELECT
123+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
124+
ORIG_VALUE_TOKEN
125+
+ ") or exists ("
126+
+ SLEEP_FUNCTION
115127
+ ")"
116-
+ SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere
128+
+ ONE_LINE_COMMENT, // Param in WHERE clause somewhere
117129
};
118130

119131
private static final Map<String, String> ALERT_TAGS;
@@ -187,117 +199,85 @@ public void init() {
187199
} else if (this.getAttackStrength() == AttackStrength.INSANE) {
188200
doTimeMaxRequests = 100;
189201
}
202+
203+
// Read the sleep value from the configs
204+
try {
205+
sleepInSeconds =
206+
this.getConfig()
207+
.getInt(RuleConfigParam.RULE_COMMON_SLEEP_TIME, DEFAULT_TIME_SLEEP_SEC);
208+
} catch (ConversionException e) {
209+
LOGGER.debug(
210+
"Invalid value for 'rules.common.sleep': {}",
211+
this.getConfig().getString(RuleConfigParam.RULE_COMMON_SLEEP_TIME));
212+
}
213+
LOGGER.debug("Sleep set to {} seconds", sleepInSeconds);
190214
}
191215

192216
@Override
193217
public void scan(HttpMessage originalMessage, String paramName, String paramValue) {
194-
195-
try {
196-
// Timing Baseline check: we need to get the time that it took the original query, to
197-
// know if the time based check is working correctly..
198-
HttpMessage msgTimeBaseline = getNewMsg();
218+
for (int payloadIndex = 0, countTimeBasedRequests = 0;
219+
payloadIndex < PAYLOADS.length && countTimeBasedRequests < doTimeMaxRequests;
220+
payloadIndex++, countTimeBasedRequests++) {
221+
if (isStop()) {
222+
LOGGER.debug("Stopping the scan due to a user request.");
223+
return;
224+
}
225+
AtomicReference<HttpMessage> message = new AtomicReference<>();
226+
String payloadValue = PAYLOADS[payloadIndex].replace(ORIG_VALUE_TOKEN, paramValue);
227+
TimingUtils.RequestSender requestSender =
228+
x -> {
229+
HttpMessage timedMsg = getNewMsg();
230+
message.compareAndSet(null, timedMsg);
231+
String finalPayload =
232+
payloadValue.replace(SLEEP_TOKEN, String.valueOf((int) x));
233+
setParameter(timedMsg, paramName, finalPayload);
234+
sendAndReceive(timedMsg, false); // do not follow redirects
235+
return TimeUnit.MILLISECONDS.toSeconds(timedMsg.getTimeElapsedMillis());
236+
};
237+
boolean isInjectable;
199238
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());
239+
// Use TimingUtils to detect a response to sleep payloads
240+
isInjectable =
241+
TimingUtils.checkTimingDependence(
242+
BLIND_REQUEST_LIMIT,
243+
sleepInSeconds,
244+
requestSender,
245+
TIME_CORRELATION_ERROR_RANGE,
246+
TIME_SLOPE_ERROR_RANGE);
247+
} catch (IOException ex) {
248+
LOGGER.debug("An error occurred checking a URL for Oracle SQLi");
249+
continue; // Something went wrong, move to next blind iteration
208250
}
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
223-
&& 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-
231-
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();
244251

252+
if (isInjectable) {
253+
String finalPayloadValue =
254+
payloadValue.replace(SLEEP_TOKEN, String.valueOf(sleepInSeconds));
245255
LOGGER.debug(
246-
"Time Based SQL Injection test: [{}] on field: [{}] with value [{}] took {}ms, where the original took {}ms",
247-
newTimeBasedInjectionValue,
256+
"Time Based Oracle SQL Injection - Found on parameter [{}] with value [{}]",
248257
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();
258-
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;
267-
}
268-
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;
290-
}
258+
paramValue);
259+
260+
newAlert()
261+
.setConfidence(Alert.CONFIDENCE_MEDIUM)
262+
.setUri(getBaseMsg().getRequestHeader().getURI().toString())
263+
.setParam(paramName)
264+
.setAttack(finalPayloadValue)
265+
.setMessage(message.get())
266+
.setOtherInfo(
267+
Constant.messages.getString(
268+
"ascanrules.sqlinjection.alert.timebased.extrainfo",
269+
finalPayloadValue,
270+
message.get().getTimeElapsedMillis(),
271+
paramValue,
272+
getBaseMsg().getTimeElapsedMillis()))
273+
.raise();
274+
return;
291275
}
292-
293-
} catch (Exception e) {
294-
LOGGER.warn(
295-
"An error occurred checking a URL for Oracle SQL Injection vulnerabilities", e);
296276
}
297277
}
298278

299-
public void setExpectedDelayInMs(int delay) {
300-
expectedDelayInMs = delay;
279+
public void setSleepInSeconds(int sleep) {
280+
this.sleepInSeconds = sleep;
301281
}
302282

303283
@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)