|
19 | 19 | */ |
20 | 20 | package org.zaproxy.zap.extension.ascanrules; |
21 | 21 |
|
| 22 | +import java.io.IOException; |
22 | 23 | import java.util.Collections; |
23 | 24 | import java.util.HashMap; |
24 | 25 | import java.util.Map; |
| 26 | +import java.util.concurrent.TimeUnit; |
| 27 | +import java.util.concurrent.atomic.AtomicReference; |
| 28 | +import org.apache.commons.configuration.ConversionException; |
25 | 29 | import org.apache.logging.log4j.LogManager; |
26 | 30 | import org.apache.logging.log4j.Logger; |
27 | 31 | import org.parosproxy.paros.Constant; |
|
31 | 35 | import org.parosproxy.paros.network.HttpMessage; |
32 | 36 | import org.zaproxy.addon.commonlib.CommonAlertTag; |
33 | 37 | import org.zaproxy.addon.commonlib.PolicyTag; |
| 38 | +import org.zaproxy.addon.commonlib.timing.TimingUtils; |
| 39 | +import org.zaproxy.zap.extension.ruleconfig.RuleConfigParam; |
34 | 40 | import org.zaproxy.zap.model.Tech; |
35 | 41 | import org.zaproxy.zap.model.TechSet; |
36 | 42 |
|
|
51 | 57 | * requires a table name in normal select statements (like Hypersonic: cannot just say "select 1" or |
52 | 58 | * "select 2" like in most RDBMSs - requires a table name in "union select" statements (like |
53 | 59 | * 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 |
59 | 62 | * |
60 | 63 | * @author 70pointer |
61 | 64 | */ |
62 | 65 | public class SqlInjectionOracleTimingScanRule extends AbstractAppParamPlugin |
63 | 66 | implements CommonActiveScanRuleInfo { |
64 | 67 |
|
65 | | - private int expectedDelayInMs = 5000; |
| 68 | + private int sleepInSeconds; |
66 | 69 |
|
67 | 70 | private int doTimeMaxRequests = 0; |
68 | 71 |
|
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 |
87 | 92 | + ")" |
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 |
91 | 97 | + ")" |
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 |
95 | 102 | + ")" |
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 |
99 | 107 | + ")" |
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 |
103 | 112 | + ")" |
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 |
107 | 117 | + ")" |
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 |
111 | 122 | + ")" |
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 |
115 | 127 | + ")" |
116 | | - + SQL_ONE_LINE_COMMENT, // Param in WHERE clause somewhere |
| 128 | + + ONE_LINE_COMMENT, // Param in WHERE clause somewhere |
117 | 129 | }; |
118 | 130 |
|
119 | 131 | private static final Map<String, String> ALERT_TAGS; |
@@ -187,117 +199,85 @@ public void init() { |
187 | 199 | } else if (this.getAttackStrength() == AttackStrength.INSANE) { |
188 | 200 | doTimeMaxRequests = 100; |
189 | 201 | } |
| 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); |
190 | 214 | } |
191 | 215 |
|
192 | 216 | @Override |
193 | 217 | 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; |
199 | 238 | 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 |
208 | 250 | } |
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(); |
244 | 251 |
|
| 252 | + if (isInjectable) { |
| 253 | + String finalPayloadValue = |
| 254 | + payloadValue.replace(SLEEP_TOKEN, String.valueOf(sleepInSeconds)); |
245 | 255 | 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 [{}]", |
248 | 257 | 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; |
291 | 275 | } |
292 | | - |
293 | | - } catch (Exception e) { |
294 | | - LOGGER.warn( |
295 | | - "An error occurred checking a URL for Oracle SQL Injection vulnerabilities", e); |
296 | 276 | } |
297 | 277 | } |
298 | 278 |
|
299 | | - public void setExpectedDelayInMs(int delay) { |
300 | | - expectedDelayInMs = delay; |
| 279 | + public void setSleepInSeconds(int sleep) { |
| 280 | + this.sleepInSeconds = sleep; |
301 | 281 | } |
302 | 282 |
|
303 | 283 | @Override |
|
0 commit comments