1919 */
2020package org .zaproxy .zap .extension .ascanrules ;
2121
22+ import java .io .IOException ;
23+ import java .net .SocketException ;
2224import java .util .Collections ;
2325import java .util .HashMap ;
2426import java .util .Map ;
27+ import java .util .concurrent .atomic .AtomicReference ;
28+ import org .apache .commons .configuration .ConversionException ;
2529import org .apache .logging .log4j .LogManager ;
2630import org .apache .logging .log4j .Logger ;
2731import org .parosproxy .paros .Constant ;
3135import org .parosproxy .paros .network .HttpMessage ;
3236import org .zaproxy .addon .commonlib .CommonAlertTag ;
3337import org .zaproxy .addon .commonlib .PolicyTag ;
38+ import org .zaproxy .addon .commonlib .timing .TimingUtils ;
39+ import org .zaproxy .zap .extension .ruleconfig .RuleConfigParam ;
3440import org .zaproxy .zap .model .Tech ;
3541import org .zaproxy .zap .model .TechSet ;
3642
6268public 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
0 commit comments