View Javadoc
1   package com.jsql.model.suspendable;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.model.bean.database.AbstractElementDatabase;
5   import com.jsql.model.bean.database.Table;
6   import com.jsql.model.bean.util.Interaction;
7   import com.jsql.model.bean.util.Request;
8   import com.jsql.model.exception.AbstractSlidingException;
9   import com.jsql.model.exception.InjectionFailureException;
10  import com.jsql.model.exception.LoopDetectedSlidingException;
11  import com.jsql.model.exception.StoppedByUserSlidingException;
12  import com.jsql.model.injection.strategy.AbstractStrategy;
13  import com.jsql.util.LogLevelUtil;
14  import com.jsql.util.StringUtil;
15  import org.apache.commons.lang3.StringUtils;
16  import org.apache.commons.text.StringEscapeUtils;
17  import org.apache.logging.log4j.LogManager;
18  import org.apache.logging.log4j.Logger;
19  
20  import java.net.URLDecoder;
21  import java.nio.charset.StandardCharsets;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
26  import java.util.regex.PatternSyntaxException;
27  
28  import static com.jsql.model.accessible.DataAccess.*;
29  import static com.jsql.model.injection.vendor.model.VendorYaml.LIMIT;
30  
31  /**
32   * Get data as chunks by performance query from SQL request.
33   * 
34   * <pre>
35   * Single row format: \4[0-9A-F]*\5[0-9A-F]*c?\4
36   * Row separator: \6
37   * Tape example: \4xxRow#Xxx\5x\4\6\4xxRow#X+1xx\5x\4\6...\4\1\3\3\7</pre>
38   * 
39   * MID and LIMIT move two sliding windows in a 2D array tape in that order.
40   * MID skips characters when collected, then LIMIT skips lines when collected.
41   * The process can be interrupted by the user (stop/pause).
42   */
43  public class SuspendableGetRows extends AbstractSuspendable {
44  
45      /**
46       * Log4j logger sent to view.
47       */
48      private static final Logger LOGGER = LogManager.getRootLogger();
49  
50      public SuspendableGetRows(InjectionModel injectionModel) {
51          super(injectionModel);
52      }
53  
54      @Override
55      public String run(Object... args) throws AbstractSlidingException {
56          // TODO Map class
57          String initialSqlQuery = (String) args[0];
58          String[] sourcePage = (String[]) args[1];
59          boolean isMultipleRows = (Boolean) args[2];
60          int countRowsToFind = (Integer) args[3];
61          AbstractElementDatabase elementDatabase = (AbstractElementDatabase) args[4];
62          String metadataInjectionProcess = (String) args[5];
63          
64          this.injectionModel.getMediatorUtils().getThreadUtil().put(elementDatabase, this);
65  
66          AbstractStrategy strategy = this.injectionModel.getMediatorStrategy().getStrategy();
67          
68          // Fix #14417
69          if (strategy == null) {
70              return StringUtils.EMPTY;
71          }
72          
73          // Stop injection if all rows are found, skip rows and characters collected
74          var slidingWindowAllRows = new StringBuilder();
75          var slidingWindowCurrentRow = new StringBuilder();
76          
77          String previousChunk = StringUtils.EMPTY;
78          var countAllRows = 0;
79          var charPositionInCurrentRow = 1;
80          var countInfiniteLoop = 0;
81          
82          String queryGetRows = this.getQuery(initialSqlQuery, countAllRows);
83          
84          while (true) {
85              this.checkSuspend(strategy, slidingWindowAllRows, slidingWindowCurrentRow);
86              
87              sourcePage[0] = strategy.inject(queryGetRows, Integer.toString(charPositionInCurrentRow), this, metadataInjectionProcess);
88              // Parse all the data we have retrieved
89              Matcher regexLeadFound = this.parseLeadFound(sourcePage[0], strategy.getPerformanceLength());
90              Matcher regexTrailOnlyFound = this.parseTrailOnlyFound(sourcePage[0]);
91              
92              if (
93                  (!regexLeadFound.find() || regexTrailOnlyFound.find())
94                  && isMultipleRows
95                  && StringUtils.isNotEmpty(slidingWindowAllRows.toString())
96              ) {
97                  this.sendProgress(countRowsToFind, countRowsToFind, elementDatabase);
98                  break;
99              }
100 
101             // Add the result to the data already found.
102             // Fix #40947: OutOfMemoryError on append()
103             // Fix #95382: IllegalArgumentException on URLDecoder.decode()
104             try {
105                 String currentChunk = regexLeadFound.group(1);
106                 currentChunk = this.decodeUnicode(currentChunk, initialSqlQuery);
107                 currentChunk = this.decodeUrl(currentChunk);
108 
109                 countInfiniteLoop = this.checkInfinite(countInfiniteLoop, previousChunk, currentChunk, slidingWindowCurrentRow, slidingWindowAllRows);
110                 
111                 previousChunk = currentChunk;
112                 slidingWindowCurrentRow.append(currentChunk);
113                 this.sendChunk(currentChunk);
114             } catch (IllegalArgumentException | IllegalStateException | OutOfMemoryError e) {
115                 this.endInjection(elementDatabase, e);
116             }
117 
118             // Check how many rows we have collected from the beginning of that chunk
119             int countChunkRows = this.getCountRows(slidingWindowCurrentRow);
120             this.sendProgress(countRowsToFind, countAllRows + countChunkRows, elementDatabase);
121 
122             // End of rows detected: \1\3\3\7
123             // => \4xxxxxxxx\500\4\6\4...\4\1\3\3\7
124             if (
125                 countChunkRows > 0
126                 || slidingWindowCurrentRow.toString().matches("(?s).*"+ TRAIL_RGX +".*")
127             ) {
128                 this.scrapeTrailJunk(slidingWindowCurrentRow);
129                 slidingWindowAllRows.append(slidingWindowCurrentRow);
130                 
131                 if (isMultipleRows) {
132                     this.scrap(slidingWindowAllRows);
133                     this.scrap(slidingWindowCurrentRow);
134                     this.appendRowFixed(slidingWindowAllRows, slidingWindowCurrentRow);
135 
136                     countAllRows = this.getCountRows(slidingWindowAllRows);
137                     this.sendProgress(countRowsToFind, countAllRows, elementDatabase);
138 
139                     // Ending condition: every expected rows have been retrieved.
140                     if (countAllRows == countRowsToFind) {
141                         break;
142                     }
143                     // Add the LIMIT statement to the next SQL query and reset variables.
144                     // Put the character cursor to the beginning of the line, and reset the result of the current query
145                     queryGetRows = this.getQuery(initialSqlQuery, countAllRows);
146                     slidingWindowCurrentRow.setLength(0);
147                 } else {
148                     this.sendProgress(countRowsToFind, countRowsToFind, elementDatabase);
149                     break;
150                 }
151             }
152             charPositionInCurrentRow = slidingWindowCurrentRow.length() + 1;
153         }
154         this.injectionModel.getMediatorUtils().getThreadUtil().remove(elementDatabase);
155         return slidingWindowAllRows.toString();
156     }
157 
158     private String decodeUrl(String currentChunk) {
159         if (!this.injectionModel.getMediatorUtils().getPreferencesUtil().isUrlDecodeDisabled()) {
160             try {
161                 return URLDecoder.decode(currentChunk, StandardCharsets.UTF_8);  // Transform %00 entities to text
162             } catch (IllegalArgumentException e) {
163                 LOGGER.log(LogLevelUtil.CONSOLE_JAVA, "Decoding fails on UT8, keeping raw result");
164             }
165         }
166         return currentChunk;
167     }
168 
169     private String decodeUnicode(String currentChunk, String initialSqlQuery) {
170         if (
171             !this.injectionModel.getMediatorUtils().getPreferencesUtil().isUnicodeDecodeDisabled()
172             && !"select@@plugin_dir".equals(initialSqlQuery)  // can give C:\path\
173             && initialSqlQuery != null && !initialSqlQuery.matches("(?si).*select.*sys_eval\\('.*'\\).*")
174         ) {
175             return StringEscapeUtils.unescapeJava(  // transform \u0000 entities to text
176                 currentChunk.replaceAll("\\\\u.{0,3}$", StringUtils.EMPTY)  // remove incorrect entities
177             );
178         }
179         return currentChunk;
180     }
181 
182     private String getQuery(String initialSqlQuery, int countAllRows) {
183         return initialSqlQuery.replace(LIMIT, this.injectionModel.getMediatorVendor().getVendor().instance().sqlLimit(countAllRows));
184     }
185 
186     private void appendRowFixed(StringBuilder slidingWindowAllRows, StringBuilder slidingWindowCurrentRow) {
187         // Check either if there is more than 1 row and if there is less than 1 complete row
188         var regexAtLeastOneRow = Pattern.compile(
189             String.format(
190                 "%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]%s%s%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$",
191                 MODE,
192                 ENCLOSE_VALUE_RGX,
193                 SEPARATOR_CELL_RGX,
194                 ENCLOSE_VALUE_RGX
195             )
196         )
197         .matcher(slidingWindowCurrentRow);
198         
199         var regexRowIncomplete = Pattern.compile(
200             MODE
201             + ENCLOSE_VALUE_RGX
202             + "[^\\x01-\\x03\\x05-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$"
203         )
204         .matcher(slidingWindowCurrentRow);
205 
206         // If there is more than 1 row, delete the last incomplete one in order to restart properly from it at the next loop,
207         // else if there is 1 row but incomplete, mark it as cut with the letter c
208         if (regexAtLeastOneRow.find()) {
209             var allLine = slidingWindowAllRows.toString();
210             slidingWindowAllRows.setLength(0);
211             slidingWindowAllRows.append(
212                 Pattern.compile(
213                     MODE
214                     + ENCLOSE_VALUE_RGX
215                     + "[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$"
216                 )
217                 .matcher(allLine)
218                 .replaceAll(StringUtils.EMPTY)
219             );
220             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Chunk unreliable, reloading row part...");
221         } else if (regexRowIncomplete.find()) {
222             slidingWindowAllRows.append(StringUtil.hexstr("05")).append("1").append(StringUtil.hexstr("0804"));
223             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Chunk unreliable, keeping row parts only");
224         }
225     }
226 
227     private void scrapeTrailJunk(StringBuilder slidingWindowCurrentRow) {
228         // Remove everything after chunk
229         // => \4xxxxxxxx\500\4\6\4...\4 => \1\3\3\7junk
230         var currentRow = slidingWindowCurrentRow.toString();
231         slidingWindowCurrentRow.setLength(0);
232         slidingWindowCurrentRow.append(
233             Pattern.compile(MODE + TRAIL_RGX +".*")
234             .matcher(currentRow)
235             .replaceAll(StringUtils.EMPTY)
236         );
237     }
238 
239     private int getCountRows(StringBuilder slidingWindowCurrentRow) {
240         var regexAtLeastOneRow = Pattern.compile(
241             String.format(
242                 "%s(%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?\\x08?%s)",
243                 MODE,
244                 ENCLOSE_VALUE_RGX,
245                 SEPARATOR_QTE_RGX,
246                 ENCLOSE_VALUE_RGX
247             )
248         )
249         .matcher(slidingWindowCurrentRow);
250         var nbCompleteLine = 0;
251         while (regexAtLeastOneRow.find()) {
252             nbCompleteLine++;
253         }
254         return nbCompleteLine;
255     }
256 
257     private void endInjection(AbstractElementDatabase searchName, Throwable e) throws InjectionFailureException {
258         // Premature end of results
259         // if it's not the root (empty tree)
260         if (searchName != null) {
261             var request = new Request();
262             request.setMessage(Interaction.END_PROGRESS);
263             request.setParameters(searchName);
264             this.injectionModel.sendToViews(request);
265         }
266         var messageError = new StringBuilder("Fetching fails: no data to parse");
267         if (searchName != null) {
268             messageError.append(" for ").append(StringUtil.detectUtf8(searchName.toString()));
269         }
270         if (searchName instanceof Table && searchName.getChildCount() > 0) {
271             messageError.append(", check Network tab for logs");
272         }
273         throw new InjectionFailureException(messageError.toString(), e);
274     }
275 
276     private void sendChunk(String currentChunk) {
277         var request = new Request();
278         request.setMessage(Interaction.MESSAGE_CHUNK);
279         request.setParameters(
280             Pattern.compile(MODE + TRAIL_RGX +".*")
281             .matcher(currentChunk)
282             .replaceAll(StringUtils.EMPTY)
283             .replace("\\n", "\\\\\\n")
284             .replace("\\r", "\\\\\\r")
285             .replace("\\t", "\\\\\\t")
286         );
287         this.injectionModel.sendToViews(request);
288     }
289 
290     // TODO pb for same char string like aaaaaaaaaaaaa...aaaaaaaaaaaaa
291     // detected as infinite
292     private int checkInfinite(
293         int loop,
294         String previousChunk,
295         String currentChunk,
296         StringBuilder slidingWindowCurrentRow,
297         StringBuilder slidingWindowAllRows
298     ) throws LoopDetectedSlidingException {
299         int infiniteLoop = loop;
300         if (previousChunk.equals(currentChunk)) {
301             infiniteLoop++;
302             if (infiniteLoop >= 20) {
303                 this.stop();
304                 throw new LoopDetectedSlidingException(
305                     slidingWindowAllRows.toString(),
306                     slidingWindowCurrentRow.toString()
307                 );
308             }
309         }
310         return infiniteLoop;
311     }
312 
313     private Matcher parseTrailOnlyFound(String sourcePage) {
314         String sourcePageUnicodeDecoded = this.decodeUnicode(sourcePage, null);
315         // TODO: prevent to find the last line directly: MODE + LEAD + .* + TRAIL_RGX
316         // It creates extra query which can be endless if not nullified
317         return Pattern.compile(
318             String.format("(?s)%s(?i)%s", LEAD, TRAIL_RGX)
319         )
320         .matcher(sourcePageUnicodeDecoded);
321     }
322 
323     /**
324      * After ${lead} tag, gets characters between 1 and maxPerf
325      * performanceQuery() gets 65536 characters or fewer
326      * ${lead}blahblah1337      ] : end or limit+1
327      * ${lead}blahblah      blah] : continue substr()
328      */
329     private Matcher parseLeadFound(String sourcePage, String performanceLength) throws InjectionFailureException {
330         Matcher regexAtLeastOneRow;
331         try {
332             regexAtLeastOneRow = Pattern.compile(
333                 String.format("(?s)%s(?i)(.{1,%s})", LEAD, performanceLength)
334             )
335             .matcher(sourcePage);
336         } catch (PatternSyntaxException e) {
337             // Fix #35382 : PatternSyntaxException null on SQLi(.{1,null})
338             throw new InjectionFailureException("Row parsing failed using capacity", e);
339         }
340         return regexAtLeastOneRow;
341     }
342 
343     private void checkSuspend(
344         AbstractStrategy strategy,
345         StringBuilder slidingWindowAllRows,
346         StringBuilder slidingWindowCurrentRow
347     ) throws StoppedByUserSlidingException, InjectionFailureException {
348         if (this.isSuspended()) {
349             throw new StoppedByUserSlidingException(
350                 slidingWindowAllRows.toString(),
351                 slidingWindowCurrentRow.toString()
352             );
353         } else if (strategy == null) {
354             // Fix #1905 : NullPointerException on injectionStrategy.inject()
355             throw new InjectionFailureException("Undefined strategy");
356         }
357     }
358 
359     private void scrap(StringBuilder slidingWindowAllRows) {
360         // Remove everything not properly attached to the last row:
361         // 1. very start of a new row: XXXXX\4[\6\4]$
362         // 2. very end of the last row: XXXXX[\500]$
363         var allRowsLimit = slidingWindowAllRows.toString();
364         slidingWindowAllRows.setLength(0);
365         slidingWindowAllRows.append(
366             Pattern.compile(
367                 String.format(
368                     "%s(%s%s|%s\\d*)$",
369                     MODE,
370                     SEPARATOR_CELL_RGX,
371                     ENCLOSE_VALUE_RGX,
372                     SEPARATOR_QTE_RGX
373                 )
374             )
375             .matcher(allRowsLimit)
376             .replaceAll(StringUtils.EMPTY)
377         );
378     }
379 
380     private void sendProgress(int numberToFind, int countProgress, AbstractElementDatabase searchName) {
381         if (numberToFind > 0 && searchName != null) {
382             var request = new Request();
383             request.setMessage(Interaction.UPDATE_PROGRESS);
384             request.setParameters(searchName, countProgress);
385             this.injectionModel.sendToViews(request);
386         }
387     }
388     
389     public static List<List<String>> parse(String rows) throws InjectionFailureException {
390         // Parse all the data we have retrieved
391         var regexSearch = Pattern.compile(
392                 String.format(
393                     "%s%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)(\\x08)?%s",
394                     MODE,
395                     ENCLOSE_VALUE_RGX,
396                     SEPARATOR_QTE_RGX,
397                     ENCLOSE_VALUE_RGX
398                 )
399             )
400             .matcher(rows);
401         if (!regexSearch.find()) {
402             throw new InjectionFailureException();
403         }
404         regexSearch.reset();
405         var rowsFound = 0;
406         List<List<String>> listValues = new ArrayList<>();
407 
408         // Build a 2D array of strings from the data we have parsed
409         // => row number, occurrence, value1, value2...
410         while (regexSearch.find()) {
411             String values = regexSearch.group(1);
412             var instances = Integer.parseInt(regexSearch.group(2));
413 
414             listValues.add(new ArrayList<>());
415             listValues.get(rowsFound).add(Integer.toString(rowsFound + 1));
416             listValues.get(rowsFound).add("x"+ instances);
417             for (String cellValue: values.split("\\x7F", -1)) {
418                 listValues.get(rowsFound).add(cellValue);
419             }
420             rowsFound++;
421         }
422         return listValues;
423     }
424 }