AbstractInjectionBit.java

package com.jsql.model.injection.strategy.blind;

import com.jsql.model.InjectionModel;
import com.jsql.model.accessible.DataAccess;
import com.jsql.model.bean.util.Interaction;
import com.jsql.model.bean.util.Request;
import com.jsql.model.exception.InjectionFailureException;
import com.jsql.model.exception.StoppedByUserSlidingException;
import com.jsql.model.injection.strategy.blind.callable.AbstractCallableBit;
import com.jsql.model.suspendable.AbstractSuspendable;
import com.jsql.util.LogLevelUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;

public abstract class AbstractInjectionBit<T extends AbstractCallableBit<T>> {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();

    // Every FALSE SQL statements will be checked,
    // more statements means a more robust application
    protected List<String> falsyBit;
    protected List<String> falsyBin;

    // Every TRUE SQL statements will be checked,
    // more statements means a more robust application
    protected List<String> truthyBit;
    protected List<String> truthyBin;

    public enum BlindOperator {
        AND, OR, STACK, NO_MODE
    }

    protected final InjectionModel injectionModel;
    protected final BlindOperator blindOperator;
    
    protected AbstractInjectionBit(InjectionModel injectionModel, BlindOperator blindOperator) {
        this.injectionModel = injectionModel;
        this.blindOperator = blindOperator;
        this.falsyBit = this.injectionModel.getMediatorVendor().getVendor().instance().getFalsyBit();
        this.truthyBit = this.injectionModel.getMediatorVendor().getVendor().instance().getTruthyBit();
        this.falsyBin = this.injectionModel.getMediatorVendor().getVendor().instance().getFalsyBin();
        this.truthyBin = this.injectionModel.getMediatorVendor().getVendor().instance().getTruthyBin();
    }

    /**
     * Start one test to verify if boolean works.
     * @return true if boolean method is confirmed
     */
    public abstract boolean isInjectable() throws StoppedByUserSlidingException;

    public abstract void initNextChar(
        String sqlQuery,
        List<char[]> bytes,
        AtomicInteger indexChar,
        CompletionService<T> taskCompletionService,
        AtomicInteger countTasksSubmitted,
        T currentCallable  // required by sequential calls like binary search
    );

    public abstract char[] initMaskAsciiChar(List<char[]> bytes, T currentCallable);

    /**
     * Display a message to explain how is blind/time working.
     */
    public abstract String getInfoMessage();

    /**
     * Process the whole boolean injection, character by character, bit by bit.
     * @param sqlQuery SQL query
     * @param suspendable Action a user can stop
     * @return Final string: SQLiABCDEF...
     */
    public String inject(String sqlQuery, AbstractSuspendable suspendable) throws StoppedByUserSlidingException {
        // List of the characters, each one represented by an array of 8 bits
        // e.g. SQLi: bytes[0] => 01010011:S, bytes[1] => 01010001:Q ...
        List<char[]> bytes = new ArrayList<>();
        var indexChar = new AtomicInteger(0);  // current char position

        // Concurrent URL requests
        ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().getThreadUtil().getExecutor("CallableAbstractBoolean");
        CompletionService<T> taskCompletionService = new ExecutorCompletionService<>(taskExecutor);

        var countTasksSubmitted = new AtomicInteger(0);
        var countBadAsciiCode = new AtomicInteger(0);

        this.initNextChar(sqlQuery, bytes, indexChar, taskCompletionService, countTasksSubmitted, null);

        // Process the job until there is no more active task,
        // in other word until all HTTP requests are done
        while (countTasksSubmitted.get() > 0) {
            if (suspendable.isSuspended()) {
                String result = this.stop(bytes, taskExecutor);
                throw new StoppedByUserSlidingException(result);
            }
            
            try {
                var currentCallable = taskCompletionService.take().get();  // URL call done
                countTasksSubmitted.decrementAndGet();  // one task just ended
                
                // If SQL result is not empty, then add a new unknown character and define a new array of 7 undefined bit.
                // Then add 7 bits requests for that new character.
                var isComplete = this.injectCharacter(bytes, countTasksSubmitted, countBadAsciiCode, currentCallable);
                if (isComplete || currentCallable.isBinary()) {  // prevents bitwise overload new char init on each bit
                    this.initNextChar(sqlQuery, bytes, indexChar, taskCompletionService, countTasksSubmitted, currentCallable);
                }

                String result = AbstractInjectionBit.convert(bytes);
                if (result.matches("(?s).*"+ DataAccess.TRAIL_RGX +".*")) {
                    countTasksSubmitted.set(0);
                    break;
                }
            } catch (InterruptedException e) {
                LOGGER.log(LogLevelUtil.IGNORE, e, e);
                Thread.currentThread().interrupt();
            } catch (ExecutionException e) {
                LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
            } catch (InjectionFailureException e) {
                LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage());
                break;
            }
        }
        return this.stop(bytes, taskExecutor);
    }

    private static String convert(List<char[]> bytes) {
        var result = new StringBuilder();
        for (char[] c: bytes) {
            try {
                var charCode = Integer.parseInt(new String(c), 2);
                var str = Character.toString((char) charCode);
                result.append(str);
            } catch (NumberFormatException err) {
                // Ignore, byte string not fully constructed (0x1x010x)
            }
        }
        return result.toString();
    }

    protected boolean injectCharacter(List<char[]> bytes, AtomicInteger countTasksSubmitted, AtomicInteger countBadAsciiCode, T currentCallable) throws InjectionFailureException {
        // Process url that has just checked one bit, convert bits to chars,
        // and change current bit from undefined to 0 or 1
        char[] maskAsciiChar = this.initMaskAsciiChar(bytes, currentCallable);
        var asciiCodeBit = new String(maskAsciiChar);
        var isComplete = false;

        // Inform the View if bits array is complete, else nothing #Need fix
        if (asciiCodeBit.matches("^[01]{8}$")) {
            var asciiCode = Integer.parseInt(asciiCodeBit, 2);
            if (asciiCode == 127 || asciiCode == 0) {  // Stop if many 11111111, 01111111 or 00000000
                if (countTasksSubmitted.get() != 0 && countBadAsciiCode.get() > 15) {
                    throw new InjectionFailureException("Boolean false positive, stopping...");
                }
                countBadAsciiCode.incrementAndGet();
            }

            currentCallable.setCharText(Character.toString((char) asciiCode));
            
            var interaction = new Request();
            interaction.setMessage(Interaction.MESSAGE_BINARY);
            interaction.setParameters(
                asciiCodeBit
                + "="
                + currentCallable.getCharText()
                .replace("\\n", "\\\\\\n")
                .replace("\\r", "\\\\\\r")
                .replace("\\t", "\\\\\\t")
            );
            this.injectionModel.sendToViews(interaction);
            isComplete = true;
        }
        return isComplete;
    }

    private String stop(List<char[]> bytes, ExecutorService taskExecutor) {
        this.injectionModel.getMediatorUtils().getThreadUtil().shutdown(taskExecutor);

        // Get current progress and display
        var result = new StringBuilder();
        
        for (char[] c: bytes) {
            try {
                var charCode = Integer.parseInt(new String(c), 2);
                var str = Character.toString((char) charCode);
                result.append(str);
            } catch (NumberFormatException err) {
                // Byte string not fully constructed : 0x1x010x
                // Ignore
            }
        }
        return result.toString();
    }

    /**
     * Run a HTTP call via the model.
     * @param urlString URL to inject
     * @return Source code
     */
    public String callUrl(String urlString, String metadataInjectionProcess) {
        return this.injectionModel.injectWithoutIndex(urlString, metadataInjectionProcess);
    }

    public String callUrl(String urlString, String metadataInjectionProcess, AbstractCallableBit<?> callableBoolean) {
        return this.injectionModel.injectWithoutIndex(urlString, metadataInjectionProcess, callableBoolean);
    }

    public BlindOperator getBooleanMode() {
        return this.blindOperator;
    }

    protected static char[] getBitsUnset() {
        return new char[]{ '0', 'x', 'x', 'x', 'x', 'x', 'x', 'x' };
    }
}