de.kimminich.agile.demos.lecture4.TestSQLInjection.java Source code

Java tutorial

Introduction

Here is the source code for de.kimminich.agile.demos.lecture4.TestSQLInjection.java

Source

/**
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
package de.kimminich.agile.demos.lecture4;

import difflib.Delta;
import difflib.DiffUtils;
import difflib.Patch;
import org.apache.commons.httpclient.URI;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.core.scanner.AbstractAppParamPlugin;
import org.parosproxy.paros.core.scanner.Alert;
import org.parosproxy.paros.core.scanner.Category;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.zap.extension.auth.ExtensionAuth;
import org.zaproxy.zap.model.Context;

import java.net.URLDecoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * TODO: implement stacked query check, since it is actually supported on more RDBMS drivers / frameworks than not (MySQL on PHP/ASP does not by default, but can).
 * PostgreSQL and MSSQL on ASP, ASP.NET, and PHP *do* support it, for instance.  It's better to put the code here and try it for all RDBMSs as a result.
 * Use the following variables: doStackedBased, doStackedMaxRequests, countStackedBasedRequests
 * TODO: implement checks in Header fields (currently does Cookie values, form fields, and url parameters)
 * TODO: change the Alert Titles.
 * TODO: if the argument is reflected back in the HTML output, the boolean based logic will not detect an alert
 * (because the HTML results of argument values "id=1" will not be the same as for "id=1 and 1=1")
 * TODO: add "<param>*2/2" check to the Logic based ones (for integer parameter values).. if the result is the same, it might be a SQL Injection
 * TODO: implement mode checks (Mode.standard, Mode.safe, Mode.protected) for 2.* using "implements SessionChangedListener"
 * <p/>
 * The SQLInjection plugin identifies SQL Injection vulnerabilities
 * note the ordering of checks, for efficiency is :
 * 1) Error based
 * 2) Boolean Based
 * 3) UNION based
 * 4) Stacked (TODO: implement stacked based)
 * 5) Blind/Time Based (RDBMS specific, so not done here right now)
 *
 */
public class TestSQLInjection extends AbstractAppParamPlugin {

    //what do we do at each attack strength?
    //(some SQL Injection vulns would be picked up by multiple types of checks, and we skip out after the first alert for a URL)
    private boolean doErrorBased = false;
    private boolean doBooleanBased = false;
    private boolean doUnionBased = false;
    private boolean doExpressionBased = false;
    private boolean doOrderByBased = false;
    private boolean doStackedBased = false; //TODO: use in the stacked based implementation

    //how many requests can we fire for each method? will be set depending on the attack strength
    private int doErrorMaxRequests = 0;
    private int doBooleanMaxRequests = 0;
    private int doUnionMaxRequests = 0;
    private int doExpressionMaxRequests = 0;
    private int doOrderByMaxRequests = 0;
    private int doStackedMaxRequests = 0; //TODO: use in the stacked based implementation

    /**
     * generic one-line comment.  Various RDBMS Documentation suggests that this syntax works with almost every single RDBMS considered here
     */
    public static final String SQL_ONE_LINE_COMMENT = " -- ";

    /**
     * used to inject to check for SQL errors: some basic SQL metacharacters ordered so as to maximise SQL errors
     * Note that we do separate runs for each family of characters, in case one family are filtered out, the others might still
     * get past
     */
    private static final String[] SQL_CHECK_ERR = { "'", "\"", ")", "(", "NULL", "'\"" };

    /**
     * create a map of SQL related error message fragments, and map them back to the RDBMS that they are associated with
     * keep the ordering the same as the order in which the values are inserted, to allow the more (subjectively judged) common cases to be tested first
     * Note: these should represent actual (driver level) error messages for things like syntax error,
     * otherwise we are simply guessing that the string should/might occur.
     */
    private static final Map<String, String> SQL_ERROR_TO_DBMS = new LinkedHashMap<>();

    static {
        //DONE: we have implemented a MySQL specific scanner. See SQLInjectionMySQL
        SQL_ERROR_TO_DBMS.put("com.mysql.jdbc.exceptions", "MySQL");
        SQL_ERROR_TO_DBMS.put("org.gjt.mm.mysql", "MySQL");

        //TODO: implement a plugin that uses Microsoft SQL specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("com.microsoft.sqlserver.jdbc", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("com.microsoft.jdbc", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("com.inet.tds", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("com.microsoft.sqlserver.jdbc", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("com.ashna.jturbo", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("weblogic.jdbc.mssqlserver", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("[Microsoft]", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("[SQLServer]", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("[SQLServer 2000 Driver for JDBC]", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("net.sourceforge.jtds.jdbc", "Microsoft SQL Server"); //see also be Sybase. could be either!
        SQL_ERROR_TO_DBMS.put("80040e14", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("800a0bcd", "Microsoft SQL Server");
        SQL_ERROR_TO_DBMS.put("80040e57", "Microsoft SQL Server");

        //DONE: we have implemented an Oracle specific scanner. See SQLInjectionOracle
        SQL_ERROR_TO_DBMS.put("oracle.jdbc", "Oracle");
        SQL_ERROR_TO_DBMS.put("SQLSTATE[HY", "Oracle");
        SQL_ERROR_TO_DBMS.put("ORA-00933", "Oracle");
        SQL_ERROR_TO_DBMS.put("ORA-06512", "Oracle"); //indicates the line number of an error
        SQL_ERROR_TO_DBMS.put("SQL command not properly ended", "Oracle");
        SQL_ERROR_TO_DBMS.put("ORA-00942", "Oracle"); //table or view does not exist
        SQL_ERROR_TO_DBMS.put("ORA-29257", "Oracle"); //host unknown
        SQL_ERROR_TO_DBMS.put("ORA-00932", "Oracle"); //inconsistent datatypes

        //TODO: implement a plugin that uses DB2 specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("com.ibm.db2.jcc", "IBM DB2");
        SQL_ERROR_TO_DBMS.put("COM.ibm.db2.jdbc", "IBM DB2");

        //DONE: we have implemented a PostgreSQL specific scanner. See SQLInjectionPostgresql
        SQL_ERROR_TO_DBMS.put("org.postgresql.util.PSQLException", "PostgreSQL");
        SQL_ERROR_TO_DBMS.put("org.postgresql", "PostgreSQL");

        //TODO: implement a plugin that uses Sybase specific functionality to detect SQL Injection vulnerabilities
        //Note: this plugin would also detect Microsoft SQL Server vulnerabilities, due to common syntax.
        SQL_ERROR_TO_DBMS.put("com.sybase.jdbc", "Sybase");
        SQL_ERROR_TO_DBMS.put("com.sybase.jdbc2.jdbc", "Sybase");
        SQL_ERROR_TO_DBMS.put("com.sybase.jdbc3.jdbc", "Sybase");
        SQL_ERROR_TO_DBMS.put("net.sourceforge.jtds.jdbc", "Sybase"); //see also Microsoft SQL Server. could be either!

        //TODO: implement a plugin that uses Informix specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("com.informix.jdbc", "Informix");

        //TODO: implement a plugin that uses Firebird specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("org.firebirdsql.jdbc", "Firebird");

        //TODO: implement a plugin that uses IDS Server specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("ids.sql", "IDS Server");

        //TODO: implement a plugin that uses InstantDB specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("org.enhydra.instantdb.jdbc", "InstantDB");
        SQL_ERROR_TO_DBMS.put("jdbc.idb", "InstantDB");

        //TODO: implement a plugin that uses Interbase specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("interbase.interclient", "Interbase");

        //DONE: we have implemented a Hypersonic specific scanner. See SQLInjectionHypersonic
        SQL_ERROR_TO_DBMS.put("org.hsql", "Hypersonic SQL");
        SQL_ERROR_TO_DBMS.put("hSql.", "Hypersonic SQL");
        SQL_ERROR_TO_DBMS.put("Unexpected token , requires FROM in statement", "Hypersonic SQL");
        SQL_ERROR_TO_DBMS.put("Unexpected end of command in statement", "Hypersonic SQL");
        SQL_ERROR_TO_DBMS.put("Column count does not match in statement", "Hypersonic SQL"); //TODO: too generic to leave in???
        SQL_ERROR_TO_DBMS.put("Table not found in statement", "Hypersonic SQL"); //TODO: too generic to leave in???
        SQL_ERROR_TO_DBMS.put("Unexpected token:", "Hypersonic SQL"); //TODO: too generic to leave in??? Works very nicely in Hypersonic cases, however

        //TODO: implement a plugin that uses Sybase SQL Anywhere specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("sybase.jdbc.sqlanywhere", "Sybase SQL Anywhere");

        //TODO: implement a plugin that uses PointBase specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("com.pointbase.jdbc", "Pointbase");

        //TODO: implement a plugin that uses Cloudbase specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("db2j.", "Cloudscape");
        SQL_ERROR_TO_DBMS.put("COM.cloudscape", "Cloudscape");
        SQL_ERROR_TO_DBMS.put("RmiJdbc.RJDriver", "Cloudscape");

        //TODO: implement a plugin that uses Ingres specific functionality to detect SQL Injection vulnerabilities
        SQL_ERROR_TO_DBMS.put("com.ingres.jdbc", "Ingres");

        //generic error message fragments that do not fingerprint the RDBMS, but that may indicate SQL Injection, nonetheless
        SQL_ERROR_TO_DBMS.put("com.ibatis.common.jdbc", "Generic SQL RDBMS");
        SQL_ERROR_TO_DBMS.put("org.hibernate", "Generic SQL RDBMS");
        SQL_ERROR_TO_DBMS.put("sun.jdbc.odbc", "Generic SQL RDBMS");
        SQL_ERROR_TO_DBMS.put("[ODBC Driver Manager]", "Generic SQL RDBMS");
        SQL_ERROR_TO_DBMS.put("System.Data.OleDb", "Generic SQL RDBMS"); //System.Data.OleDb.OleDbException
        SQL_ERROR_TO_DBMS.put("java.sql.SQLException", "Generic SQL RDBMS"); //in case more specific messages were not detected!
    }

    /**
     * always true statement for comparison in boolean based SQL injection check
     * try the commented versions first, because the law of averages says that the column being queried is more likely *not* in the last where clause in a SQL query
     * so as a result, the rest of the query needs to be closed off with the comment.
     */
    private static final String[] SQL_LOGIC_AND_TRUE = { " AND 1=1" + SQL_ONE_LINE_COMMENT,
            "' AND '1'='1'" + SQL_ONE_LINE_COMMENT, "\" AND \"1\"=\"1\"" + SQL_ONE_LINE_COMMENT, " AND 1=1",
            "' AND '1'='1", "\" AND \"1\"=\"1", "%", //attack for SQL LIKE statements
            "%' " + SQL_ONE_LINE_COMMENT, //attack for SQL LIKE statements
            "%\" " + SQL_ONE_LINE_COMMENT, //attack for SQL LIKE statements
    };

    /**
     * always false statement for comparison in boolean based SQL injection check
     */
    private static final String[] SQL_LOGIC_AND_FALSE = { " AND 1=2" + SQL_ONE_LINE_COMMENT,
            "' AND '1'='2'" + SQL_ONE_LINE_COMMENT, "\" AND \"1\"=\"2\"" + SQL_ONE_LINE_COMMENT, " AND 1=2",
            "' AND '1'='2", "\" AND \"1\"=\"2", "XYZABCDEFGHIJ", //attack for SQL LIKE statements
            "XYZABCDEFGHIJ' " + SQL_ONE_LINE_COMMENT, //attack for SQL LIKE statements
            "XYZABCDEFGHIJ\" " + SQL_ONE_LINE_COMMENT, //attack for SQL LIKE statements
    };

    /**
     * always true statement for comparison if no output is returned from AND in boolean based SQL injection check
     * Note that, if necessary, the code also tries a variant with the one-line comment " -- " appended to the end.
     */
    private static final String[] SQL_LOGIC_OR_TRUE = { " OR 1=1" + SQL_ONE_LINE_COMMENT,
            "' OR '1'='1'" + SQL_ONE_LINE_COMMENT, "\" OR \"1\"=\"1\"" + SQL_ONE_LINE_COMMENT, " OR 1=1",
            "' OR '1'='1", "\" OR \"1\"=\"1", "%", //attack for SQL LIKE statements
            "%' " + SQL_ONE_LINE_COMMENT, //attack for SQL LIKE statements
            "%\" " + SQL_ONE_LINE_COMMENT, //attack for SQL LIKE statements
    };

    /**
     * generic UNION statements. Hoping these will cause a specific error message that we will recognise
     */
    private static String[] SQL_UNION_APPENDAGES = { " UNION ALL select NULL" + SQL_ONE_LINE_COMMENT,
            "' UNION ALL select NULL" + SQL_ONE_LINE_COMMENT, "\" UNION ALL select NULL" + SQL_ONE_LINE_COMMENT,
            ") UNION ALL select NULL" + SQL_ONE_LINE_COMMENT, "') UNION ALL select NULL" + SQL_ONE_LINE_COMMENT,
            "\") UNION ALL select NULL" + SQL_ONE_LINE_COMMENT, };

    /*
    SQL UNION error messages for various RDBMSs. The more, the merrier.
     */
    private static final Map<String, String> SQL_UNION_ERROR_TO_DBMS = new LinkedHashMap<>();

    static {
        SQL_UNION_ERROR_TO_DBMS.put("The used SELECT statements have a different number of columns", "MySQL");
        SQL_UNION_ERROR_TO_DBMS.put("each UNION query must have the same number of columns", "PostgreSQL");
        SQL_UNION_ERROR_TO_DBMS.put(
                "All queries in an SQL statement containing a UNION operator must have an equal number of expressions in their target lists",
                "Microsoft SQL Server");
        SQL_UNION_ERROR_TO_DBMS.put(
                "All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists",
                "Microsoft SQL Server");
        SQL_UNION_ERROR_TO_DBMS.put("query block has incorrect number of result columns", "Oracle");
        SQL_UNION_ERROR_TO_DBMS.put("ORA-01789", "Oracle");
        SQL_UNION_ERROR_TO_DBMS.put("Unexpected end of command in statement", "Hypersonic SQL"); //needs a table name in a UNION query. Like Oracle?
        SQL_UNION_ERROR_TO_DBMS.put("Column count does not match in statement", "Hypersonic SQL");
        //TODO: add other specific UNION based error messages for Union here: PostgreSQL, Sybase, DB2, Informix, etc
    }

    /**
     * plugin dependencies
     */
    private static final String[] dependency = {};

    /**
     * for logging.
     */
    private static Logger log = Logger.getLogger(TestSQLInjection.class);

    /**
     * determines if we should output Debug level logging
     */
    private boolean debugEnabled = log.isDebugEnabled();

    @Override
    public int getId() {
        return 40018;
    }

    @Override
    public String getName() {
        return Constant.messages.getString("ascanrules.sqlinjection.name");
    }

    @Override
    public String[] getDependency() {
        return dependency;
    }

    @Override
    public String getDescription() {
        return Constant.messages.getString("ascanrules.sqlinjection.desc");
    }

    @Override
    public int getCategory() {
        return Category.INJECTION;
    }

    @Override
    public String getSolution() {
        return Constant.messages.getString("ascanrules.sqlinjection.soln");
    }

    @Override
    public String getReference() {
        return Constant.messages.getString("ascanrules.sqlinjection.refs");
    }

    /* initialise
     * Note that this method gets called each time the scanner is called.
     */
    @Override
    public void init() {
        if (this.debugEnabled)
            log.debug("Initialising");
        // AscanUtils.registerI18N();

        //DEBUG only
        //this.debugEnabled=true;
        //this.setAttackStrength(AttackStrength.LOW);

        //set up what we are allowed to do, depending on the attack strength that was set.
        if (this.getAttackStrength() == AttackStrength.LOW) {
            //do error based (if Threshold allows), and some expression based
            doErrorBased = true;
            doErrorMaxRequests = 4;
            doExpressionBased = true;
            doExpressionMaxRequests = 4;
            doBooleanBased = false;
            doBooleanMaxRequests = 0;
            doUnionBased = false;
            doUnionMaxRequests = 0;
            doOrderByBased = false;
            doOrderByMaxRequests = 0;
            doStackedBased = false;
            doStackedMaxRequests = 0;

        } else if (this.getAttackStrength() == AttackStrength.MEDIUM) {
            //do some more error based (if Threshold allows), some more expression based, some boolean based, and some Union based
            doErrorBased = true;
            doErrorMaxRequests = 8;
            doExpressionBased = true;
            doExpressionMaxRequests = 8;
            doBooleanBased = true;
            doBooleanMaxRequests = 6;
            doUnionBased = true;
            doUnionMaxRequests = 5;
            doOrderByBased = false;
            doOrderByMaxRequests = 0;
            doStackedBased = false;
            doStackedMaxRequests = 5;

        } else if (this.getAttackStrength() == AttackStrength.HIGH) {
            //do some more error based (if Threshold allows), some more expression based, some more boolean based, some union based, and some order by based
            doErrorBased = true;
            doErrorMaxRequests = 16;
            doExpressionBased = true;
            doExpressionMaxRequests = 16;
            doBooleanBased = true;
            doBooleanMaxRequests = 20; //will not run all the LIKE attacks.. these are done at insane..
            doUnionBased = true;
            doUnionMaxRequests = 10;
            doOrderByBased = true;
            doOrderByMaxRequests = 5;
            doStackedBased = false;
            doStackedMaxRequests = 10;
        } else if (this.getAttackStrength() == AttackStrength.INSANE) {
            //do some more error based (if Threshold allows), some more expression based, some more boolean based, some more union based, and some more order by based
            doErrorBased = true;
            doErrorMaxRequests = 100;
            doExpressionBased = true;
            doExpressionMaxRequests = 100;
            doBooleanBased = true;
            doBooleanMaxRequests = 100;
            doUnionBased = true;
            doUnionMaxRequests = 100;
            doOrderByBased = true;
            doOrderByMaxRequests = 100;
            doStackedBased = false;
            doStackedMaxRequests = 100;
        }
        //if a high threshold is in place, turn off the error based, which are more prone to false positives
        if (this.getAlertThreshold() == AlertThreshold.HIGH) {
            if (this.debugEnabled)
                log.debug(
                        "Disabling the Error Based checking, since the Alert Threshold is set to High, and this type of check is notably prone to false positives");
            doErrorBased = false;
            doErrorMaxRequests = 0;
        }

    }

    /**
     * scans for SQL Injection vulnerabilities
     */
    @Override
    public void scan(HttpMessage msg, String param, String origParamValue) {
        //Note: the "value" we are passed here is escaped. we need to unescape it before handling it.
        //as soon as we find a single SQL injection on the url, skip out. Do not look for SQL injection on a subsequent parameter on the same URL
        //for performance reasons.
        boolean sqlInjectionFoundForUrl = false;
        String sqlInjectionAttack = null;
        HttpMessage refreshedmessage = null;
        String mResBodyNormalUnstripped = null;
        String mResBodyNormalStripped = null;

        try {

            //reinitialise the count for each type of request, for each parameter.  We will be sticking to limits defined in the attach strength logic
            int countErrorBasedRequests = 0;
            int countExpressionBasedRequests = 0;
            int countBooleanBasedRequests = 0;
            int countUnionBasedRequests = 0;
            int countOrderByBasedRequests = 0;
            int countStackedBasedRequests = 0; //TODO: use in the stacked based queries implementation

            //Check 1: Check for Error Based SQL Injection (actual error messages).
            //for each SQL metacharacter combination to try
            for (int sqlErrorStringIndex = 0; sqlErrorStringIndex < SQL_CHECK_ERR.length && !sqlInjectionFoundForUrl
                    && doErrorBased && countErrorBasedRequests < doErrorMaxRequests; sqlErrorStringIndex++) {

                //work through the attack using each of the following strings as a prefix: the empty string, and the original value
                //Note: this doubles the amount of work done by the scanner, but is necessary in some cases
                String[] prefixStrings;
                if (origParamValue != null) {
                    prefixStrings = new String[] { "", TestSQLInjection.getURLDecode(origParamValue) };
                } else {
                    prefixStrings = new String[] { "" };
                }
                for (int prefixIndex = 0; prefixIndex < prefixStrings.length; prefixIndex++) {

                    //new message for each value we attack with
                    HttpMessage msg1 = getNewMsg();
                    String sqlErrValue = prefixStrings[prefixIndex] + SQL_CHECK_ERR[sqlErrorStringIndex];
                    setParameter(msg1, param, sqlErrValue);

                    //System.out.println("Attacking [" + msg + "], parameter [" + param + "] with value ["+ sqlErrValue + "]");

                    //send the message with the modified parameters
                    sendAndReceive(msg1);
                    countErrorBasedRequests++;

                    //now check the results against each pattern in turn, to try to identify a database, or even better: a specific database.
                    //Note: do NOT check the HTTP error code just yet, as the result could come back with one of various codes.
                    Iterator<String> errorPatternIterator = SQL_ERROR_TO_DBMS.keySet().iterator();

                    while (errorPatternIterator.hasNext() && !sqlInjectionFoundForUrl) {
                        String errorPatternKey = errorPatternIterator.next();
                        String errorPatternRDBMS = SQL_ERROR_TO_DBMS.get(errorPatternKey);

                        //Note: must escape the strings, in case they contain strings like "[Microsoft], which would be interpreted as regular character class regexps"
                        Pattern errorPattern = Pattern.compile("\\Q" + errorPatternKey + "\\E", PATTERN_PARAM);

                        //if the "error message" occurs in the result of sending the modified query, but did NOT occur in the original result of the original query
                        //then we may may have a SQL Injection vulnerability
                        StringBuilder sb = new StringBuilder();
                        if (!matchBodyPattern(getBaseMsg(), errorPattern, null)
                                && matchBodyPattern(msg1, errorPattern, sb)) {
                            //Likely a SQL Injection. Raise it
                            String extraInfo = Constant.messages.getString(
                                    "ascanrules.sqlinjection.alert.errorbased.extrainfo", errorPatternRDBMS,
                                    errorPatternKey);
                            //raise the alert, and save the attack string for the "Authentication Bypass" alert, if necessary
                            sqlInjectionAttack = sqlErrValue;
                            bingo(Alert.RISK_HIGH, Alert.WARNING, getName() + " - " + errorPatternRDBMS,
                                    getDescription(), null, param, sqlInjectionAttack, extraInfo, getSolution(),
                                    msg1);

                            //log it, as the RDBMS may be useful to know later (in subsequent checks, when we need to determine RDBMS specific behaviour, for instance)
                            getKb().add(getBaseMsg().getRequestHeader().getURI(), "sql/" + errorPatternRDBMS,
                                    Boolean.TRUE);

                            sqlInjectionFoundForUrl = true;
                            continue;
                        }
                    } //end of the loop to check for RDBMS specific error messages

                } //for each of the SQL_CHECK_ERR values (SQL metacharacters)
            }

            //###############################
            //Check 4
            //New!  I haven't seen this technique documented anywhere else, but it's dead simple. Let me explain.
            //See if the parameter value can simply be changed to one that *evaluates* to be the same value,
            //if evaluated on a database
            //the simple check is to see if parameter "1" gives the same results as for param "2-1", and different results for param "2-2"
            //for now, we try this for integer values only.
            //###############################
            //Since the previous checks are attempting SQL injection, and may have actually succeeded in modifying the database (ask me how I know?!)
            //then we cannot rely on the database contents being the same as when the original query was last run (could be hours ago)
            //so to work around this, simply re-run the query again now at this point.
            //Note that we are not counting this request in our max number of requests to be issued
            refreshedmessage = getNewMsg();
            sendAndReceive(refreshedmessage);

            //String mResBodyNormal = getBaseMsg().getResponseBody().toString();
            mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString();
            mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue);

            if (!sqlInjectionFoundForUrl && doExpressionBased
                    && countExpressionBasedRequests < doExpressionMaxRequests) {

                //first figure out the type of the parameter..
                try {
                    //is it an integer type?
                    int paramAsInt = new Integer(TestSQLInjection.getURLDecode(origParamValue));
                    if (this.debugEnabled)
                        log.debug("The parameter value [" + TestSQLInjection.getURLDecode(origParamValue)
                                + "] is of type Integer");

                    //get a value 2 sizes bigger
                    int paramPlusTwo = paramAsInt + 2;
                    String modifiedParamValue = String.valueOf(paramPlusTwo) + "-2";

                    //and prepare a request to set the parameter value to a string value like "3-2", if the original parameter value was "1"
                    //those of you still paying attention will note that if handled as expressions (such as by a database), these represent the same value.
                    HttpMessage msg4 = getNewMsg();
                    setParameter(msg4, param, modifiedParamValue);

                    sendAndReceive(msg4);
                    countExpressionBasedRequests++;

                    String modifiedExpressionOutputUnstripped = msg4.getResponseBody().toString();
                    String modifiedExpressionOutputStripped = this.stripOff(modifiedExpressionOutputUnstripped,
                            modifiedParamValue);

                    //set up two little arrays to ease the work of checking the unstripped output, and then the stripped output
                    String normalBodyOutput[] = { mResBodyNormalUnstripped, mResBodyNormalStripped };
                    String expressionBodyOutput[] = { modifiedExpressionOutputUnstripped,
                            modifiedExpressionOutputStripped };
                    boolean strippedOutput[] = { false, true };

                    for (int booleanStrippedUnstrippedIndex = 0; booleanStrippedUnstrippedIndex < 2; booleanStrippedUnstrippedIndex++) {
                        //if the results of the modified request match the original query, we may be onto something.
                        if (expressionBodyOutput[booleanStrippedUnstrippedIndex]
                                .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) == 0) {
                            if (this.debugEnabled)
                                log.debug("Check 4, "
                                        + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED"
                                                : "UNSTRIPPED")
                                        + " html output for modified expression parameter [" + modifiedParamValue
                                        + "] matched (refreshed) original results for "
                                        + refreshedmessage.getRequestHeader().getURI());
                            //confirm that a different parameter value generates different output, to minimise false positives

                            //get a value 3 sizes bigger this time
                            int paramPlusFour = paramAsInt + 3;
                            String modifiedParamValueConfirm = String.valueOf(paramPlusFour) + "-2";

                            //and prepare a request to set the parameter value to a string value like "4-2", if the original parameter value was "1"
                            //Note that the two values are NOT equivalent, and the param value is different to the original
                            HttpMessage msg4Confirm = getNewMsg();
                            setParameter(msg4Confirm, param, modifiedParamValueConfirm);

                            sendAndReceive(msg4Confirm);
                            countExpressionBasedRequests++;

                            String confirmExpressionOutputUnstripped = msg4Confirm.getResponseBody().toString();
                            String confirmExpressionOutputStripped = this
                                    .stripOff(confirmExpressionOutputUnstripped, modifiedParamValueConfirm);

                            //set up two little arrays to ease the work of checking the unstripped output or the stripped output
                            String confirmExpressionBodyOutput[] = { confirmExpressionOutputUnstripped,
                                    confirmExpressionOutputStripped };

                            if (confirmExpressionBodyOutput[booleanStrippedUnstrippedIndex]
                                    .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) != 0) {
                                //the confirm query did not return the same results.  This means that arbitrary queries are not all producing the same page output.
                                //this means the fact we earier reproduced the original page output with a modified parameter was not a coincidence

                                //Likely a SQL Injection. Raise it
                                String extraInfo = null;
                                if (strippedOutput[booleanStrippedUnstrippedIndex])
                                    extraInfo = Constant.messages.getString(
                                            "ascanrules.sqlinjection.alert.expressionbased.extrainfo",
                                            modifiedParamValue, "");
                                else
                                    extraInfo = Constant.messages.getString(
                                            "ascanrules.sqlinjection.alert.expressionbased.extrainfo",
                                            modifiedParamValue, "NOT ");

                                //raise the alert, and save the attack string for the "Authentication Bypass" alert, if necessary
                                sqlInjectionAttack = modifiedParamValue;
                                bingo(Alert.RISK_HIGH, Alert.WARNING, getName(), getDescription(), null, //url
                                        param, sqlInjectionAttack, extraInfo, getSolution(), msg4);

                                sqlInjectionFoundForUrl = true;
                            }
                        }
                    }
                } catch (Exception e) {

                    if (this.debugEnabled)
                        log.debug("The parameter value [" + TestSQLInjection.getURLDecode(origParamValue)
                                + "] is NOT of type Integer");
                    //TODO: implement a similar check for string types?  This probably need to be RDBMS specific (ie, it should not live in this scanner)
                }
            }

            //Check 2: boolean based checks.
            //the check goes like so:
            // append " and 1 = 1" to the param.  Send the query.  Check the results. Hopefully they match the original results from the unmodified query,
            // *suggesting* (but not yet definitely) that we have successfully modified the query, (hopefully not gotten an error message),
            // and have gotten the same results back, which is what you would expect if you added the constraint " and 1 = 1" to most (but not every) SQL query.
            // So was it a fluke that we got the same results back from the modified query? Perhaps the original query returned 0 rows, so adding any number of
            // constraints would change nothing?  It is still a possibility!
            // check to see if we can change the original parameter again to *restrict* the scope of the query using an AND with an always false condition (AND_ERR)
            // (decreasing the results back to nothing), or to *broaden* the scope of the query using an OR with an always true condition (AND_OR)
            // (increasing the results).
            // If we can successfully alter the results to our requirements, by one means or another, we have found a SQL Injection vulnerability.
            //Some additional complications: assume there are 2 HTML parameters: username and password, and the SQL constructed is like so:
            // select * from username where user = "$user" and password = "$password"
            // and lets assume we successfully know the type of the user field, via SQL_OR_TRUE value '" OR "1"="1' (single quotes not part of the value)
            // we still have the problem that the actual SQL executed would look like so:
            // select * from username where user = "" OR "1"="1" and password = "whateveritis"
            // Since the password field is still taken into account (by virtue of the AND condition on the password column), and we only inject one parameter at a time,
            // we are still not in control.
            // the solution is simple: add an end-of-line comment to the field added in (in this example: the user field), so that the SQL becomes:
            // select * from username where user = "" OR "1"="1" -- and password = "whateveritis"
            // the result is that any additional constraints are commented out, and the last condition to have any effect is the one whose
            // HTTP param we are manipulating.
            // Note also that because this comment only needs to be added to the "SQL_OR_TRUE" and not to the equivalent SQL_AND_FALSE, because of the nature of the OR
            // and AND conditions in SQL.
            // Corollary: If a particular RDBMS does not offer the ability to comment out the remainder of a line, we will not attempt to comment out anything in the query
            //            and we will simply hope that the *last* constraint in the SQL query is constructed from a HTTP parameter under our control.

            if (this.debugEnabled)
                log.debug("Doing Check 2, since check 1 did not match for "
                        + getBaseMsg().getRequestHeader().getURI());

            //Since the previous checks are attempting SQL injection, and may have actually succeeded in modifying the database (ask me how I know?!)
            //then we cannot rely on the database contents being the same as when the original query was last run (could be hours ago)
            //so to work around this, simply re-run the query again now at this point.
            //Note that we are not counting this request in our max number of requests to be issued
            refreshedmessage = getNewMsg();
            sendAndReceive(refreshedmessage);

            //String mResBodyNormal = getBaseMsg().getResponseBody().toString();
            mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString();
            mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue);

            //boolean booleanBasedSqlInjectionFoundForParam = false;

            //try each of the AND syntax values in turn.
            //Which one is successful will depend on the column type of the table/view column into which we are injecting the SQL.
            for (int i = 0; i < SQL_LOGIC_AND_TRUE.length && !sqlInjectionFoundForUrl && doBooleanBased
                    && countBooleanBasedRequests < doBooleanMaxRequests; i++) {
                //needs a new message for each type of AND to be issued
                HttpMessage msg2 = getNewMsg();
                String sqlBooleanAndTrueValue = TestSQLInjection.getURLDecode(origParamValue)
                        + SQL_LOGIC_AND_TRUE[i];
                String sqlBooleanAndFalseValue = TestSQLInjection.getURLDecode(origParamValue)
                        + SQL_LOGIC_AND_FALSE[i];

                setParameter(msg2, param, sqlBooleanAndTrueValue);

                //send the AND with an additional TRUE statement tacked onto the end. Hopefully it will return the same results as the original (to find a vulnerability)
                sendAndReceive(msg2);
                countBooleanBasedRequests++;

                //String resBodyAND = msg2.getResponseBody().toString();
                String resBodyANDTrueUnstripped = msg2.getResponseBody().toString();
                String resBodyANDTrueStripped = this.stripOff(resBodyANDTrueUnstripped, sqlBooleanAndTrueValue);

                //set up two little arrays to ease the work of checking the unstripped output, and then the stripped output
                String normalBodyOutput[] = { mResBodyNormalUnstripped, mResBodyNormalStripped };
                String andTrueBodyOutput[] = { resBodyANDTrueUnstripped, resBodyANDTrueStripped };
                boolean strippedOutput[] = { false, true };

                for (int booleanStrippedUnstrippedIndex = 0; booleanStrippedUnstrippedIndex < 2; booleanStrippedUnstrippedIndex++) {
                    //if the results of the "AND 1=1" match the original query (using either the stipped or unstripped versions), we may be onto something.
                    if (andTrueBodyOutput[booleanStrippedUnstrippedIndex]
                            .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) == 0) {
                        if (this.debugEnabled)
                            log.debug("Check 2, "
                                    + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED")
                                    + " html output for AND TRUE condition [" + sqlBooleanAndTrueValue
                                    + "] matched (refreshed) original results for "
                                    + refreshedmessage.getRequestHeader().getURI());
                        //so they match. Was it a fluke? See if we get the same result by tacking on "AND 1 = 2" to the original
                        HttpMessage msg2_and_false = getNewMsg();

                        setParameter(msg2_and_false, param, sqlBooleanAndFalseValue);

                        sendAndReceive(msg2_and_false);
                        countBooleanBasedRequests++;

                        //String resBodyANDFalse = stripOff(msg2_and_false.getResponseBody().toString(), SQL_LOGIC_AND_FALSE[i]);
                        //String resBodyANDFalse = msg2_and_false.getResponseBody().toString();
                        String resBodyANDFalseUnstripped = msg2_and_false.getResponseBody().toString();
                        String resBodyANDFalseStripped = this.stripOff(resBodyANDFalseUnstripped,
                                sqlBooleanAndFalseValue);

                        String andFalseBodyOutput[] = { resBodyANDFalseUnstripped, resBodyANDFalseStripped };

                        //which AND False output should we compare? the stripped or the unstripped version?
                        //depends on which one we used to get to here.. use the same as that..

                        // build an always false AND query.  Result should be different to prove the SQL works.
                        if (andFalseBodyOutput[booleanStrippedUnstrippedIndex]
                                .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) != 0) {
                            if (this.debugEnabled)
                                log.debug("Check 2, "
                                        + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED"
                                                : "UNSTRIPPED")
                                        + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue
                                        + "] differed from (refreshed) original results for "
                                        + refreshedmessage.getRequestHeader().getURI());

                            //it's different (suggesting that the "AND 1 = 2" appended on gave different results because it restricted the data set to nothing
                            //Likely a SQL Injection. Raise it
                            String extraInfo = null;
                            if (strippedOutput[booleanStrippedUnstrippedIndex])
                                extraInfo = Constant.messages.getString(
                                        "ascanrules.sqlinjection.alert.booleanbased.extrainfo",
                                        sqlBooleanAndTrueValue, sqlBooleanAndFalseValue, "");
                            else
                                extraInfo = Constant.messages.getString(
                                        "ascanrules.sqlinjection.alert.booleanbased.extrainfo",
                                        sqlBooleanAndTrueValue, sqlBooleanAndFalseValue, "NOT ");
                            extraInfo = extraInfo + "\n" + Constant.messages
                                    .getString("ascanrules.sqlinjection.alert.booleanbased.extrainfo.dataexists");

                            //raise the alert, and save the attack string for the "Authentication Bypass" alert, if necessary
                            sqlInjectionAttack = sqlBooleanAndTrueValue;
                            bingo(Alert.RISK_HIGH, Alert.WARNING, getName(), getDescription(), null, //url
                                    param, sqlInjectionAttack, extraInfo, getSolution(), msg2);

                            sqlInjectionFoundForUrl = true;

                            continue; //to the next entry in SQL_AND
                        } else {
                            //the results of the always false condition are the same as for the original unmodified parameter
                            //this could be because there was *no* data returned for the original unmodified parameter
                            //so consider the effect of adding comments to both the always true condition, and the always false condition
                            //the first value to try..
                            String orValue = TestSQLInjection.getURLDecode(origParamValue) + SQL_LOGIC_OR_TRUE[i];

                            //this is where that comment comes in handy: if the RDBMS supports one-line comments, add one in to attempt to ensure that the
                            //condition becomes one that is effectively always true, returning ALL data (or as much as possible), allowing us to pinpoint the SQL Injection
                            if (this.debugEnabled)
                                log.debug("Check 2 , "
                                        + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED"
                                                : "UNSTRIPPED")
                                        + " html output for AND FALSE condition [" + sqlBooleanAndFalseValue
                                        + "] SAME as (refreshed) original results for "
                                        + refreshedmessage.getRequestHeader().getURI()
                                        + " ### (forcing OR TRUE check) ");
                            HttpMessage msg2_or_true = getNewMsg();
                            setParameter(msg2_or_true, param, orValue);
                            sendAndReceive(msg2_or_true);
                            countBooleanBasedRequests++;

                            //String resBodyORTrue = stripOff(msg2_or_true.getResponseBody().toString(), orValue);
                            //String resBodyORTrue = msg2_or_true.getResponseBody().toString();
                            String resBodyORTrueUnstripped = msg2_or_true.getResponseBody().toString();
                            String resBodyORTrueStripped = this.stripOff(resBodyORTrueUnstripped, orValue);

                            String orTrueBodyOutput[] = { resBodyORTrueUnstripped, resBodyORTrueStripped };

                            int compareOrToOriginal = orTrueBodyOutput[booleanStrippedUnstrippedIndex]
                                    .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]);
                            if (compareOrToOriginal != 0) {

                                if (this.debugEnabled)
                                    log.debug("Check 2, "
                                            + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED"
                                                    : "UNSTRIPPED")
                                            + " html output for OR TRUE condition [" + orValue
                                            + "] different to (refreshed) original results for "
                                            + refreshedmessage.getRequestHeader().getURI());

                                //it's different (suggesting that the "OR 1 = 1" appended on gave different results because it broadened the data set from nothing to something
                                //Likely a SQL Injection. Raise it
                                String extraInfo = null;
                                if (strippedOutput[booleanStrippedUnstrippedIndex])
                                    extraInfo = Constant.messages.getString(
                                            "ascanrules.sqlinjection.alert.booleanbased.extrainfo",
                                            sqlBooleanAndTrueValue, orValue, "");
                                else
                                    extraInfo = Constant.messages.getString(
                                            "ascanrules.sqlinjection.alert.booleanbased.extrainfo",
                                            sqlBooleanAndTrueValue, orValue, "NOT ");
                                extraInfo = extraInfo + "\n" + Constant.messages.getString(
                                        "ascanrules.sqlinjection.alert.booleanbased.extrainfo.datanotexists");

                                //raise the alert, and save the attack string for the "Authentication Bypass" alert, if necessary
                                sqlInjectionAttack = orValue;
                                bingo(Alert.RISK_HIGH, Alert.WARNING, getName(), getDescription(), null, //url
                                        param, sqlInjectionAttack, extraInfo, getSolution(), msg2);

                                sqlInjectionFoundForUrl = true;
                                //booleanBasedSqlInjectionFoundForParam = true;  //causes us to skip past the other entries in SQL_AND.  Only one will expose a vuln for a given param, since the database column is of only 1 type

                                continue;
                            }
                        }
                    } //if the results of the "AND 1=1" match the original query, we may be onto something.
                    else {
                        //andTrueBodyOutput[booleanStrippedUnstrippedIndex].compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex])
                        //the results of the "AND 1=1" do NOT match the original query, for whatever reason (no sql injection, or the web page is not stable)
                        if (this.debugEnabled) {
                            log.debug("Check 2, "
                                    + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED")
                                    + " html output for AND condition [" + sqlBooleanAndTrueValue
                                    + "] does NOT match the (refreshed) original results for "
                                    + refreshedmessage.getRequestHeader().getURI());
                            Patch diffpatch = DiffUtils.diff(
                                    new LinkedList<String>(Arrays
                                            .asList(normalBodyOutput[booleanStrippedUnstrippedIndex].split("\\n"))),
                                    new LinkedList<String>(Arrays.asList(
                                            andTrueBodyOutput[booleanStrippedUnstrippedIndex].split("\\n"))));

                            //int numberofDifferences = diffpatch.getDeltas().size();

                            //and convert the list of patches to a String, joining using a newline
                            StringBuilder tempDiff = new StringBuilder(250);
                            for (Delta delta : diffpatch.getDeltas()) {
                                String changeType = null;
                                if (delta.getType() == Delta.TYPE.CHANGE)
                                    changeType = "Changed Text";
                                else if (delta.getType() == Delta.TYPE.DELETE)
                                    changeType = "Deleted Text";
                                else if (delta.getType() == Delta.TYPE.INSERT)
                                    changeType = "Inserted text";
                                else
                                    changeType = "Unknown change type [" + delta.getType() + "]";

                                tempDiff.append("\n(" + changeType + ")\n"); //blank line before
                                tempDiff.append("Output for Unmodified parameter: " + delta.getOriginal() + "\n");
                                tempDiff.append("Output for   modified parameter: " + delta.getRevised() + "\n");
                            }
                            log.debug("DIFFS: " + tempDiff);
                        }
                    }

                } //end of boolean logic output index (unstripped + stripped)
            }
            //end of check 2

            //Check 3: UNION based
            //for each SQL UNION combination to try
            for (int sqlUnionStringIndex = 0; sqlUnionStringIndex < SQL_UNION_APPENDAGES.length
                    && !sqlInjectionFoundForUrl && doUnionBased
                    && countUnionBasedRequests < doUnionMaxRequests; sqlUnionStringIndex++) {

                //new message for each value we attack with
                HttpMessage msg3 = getNewMsg();
                String sqlUnionValue = origParamValue + SQL_UNION_APPENDAGES[sqlUnionStringIndex];
                setParameter(msg3, param, sqlUnionValue);
                //send the message with the modified parameters
                sendAndReceive(msg3);
                countUnionBasedRequests++;

                //now check the results.. look first for UNION specific error messages in the output that were not there in the original output
                //and failing that, look for generic RDBMS specific error messages
                //TODO: maybe also try looking at a differentiation based approach?? Prone to false positives though.
                Iterator<String> errorPatternUnionIterator = SQL_UNION_ERROR_TO_DBMS.keySet().iterator();

                while (errorPatternUnionIterator.hasNext() && !sqlInjectionFoundForUrl) {
                    String errorPatternKey = errorPatternUnionIterator.next();
                    String errorPatternRDBMS = SQL_UNION_ERROR_TO_DBMS.get(errorPatternKey);

                    //Note: must escape the strings, in case they contain strings like "[Microsoft], which would be interpreted as regular character class regexps"
                    Pattern errorPattern = Pattern.compile("\\Q" + errorPatternKey + "\\E", PATTERN_PARAM);

                    //if the "error message" occurs in the result of sending the modified query, but did NOT occur in the original result of the original query
                    //then we may may have a SQL Injection vulnerability
                    StringBuilder sb = new StringBuilder();
                    String sqlUnionBodyUnstripped = msg3.getResponseBody().toString();
                    String sqlUnionBodyStripped = this.stripOff(sqlUnionBodyUnstripped, sqlUnionValue);

                    Matcher matcherOrig = errorPattern.matcher(mResBodyNormalStripped);
                    Matcher matcherSQLUnion = errorPattern.matcher(sqlUnionBodyStripped);
                    boolean patternInOrig = matcherOrig.find();
                    boolean patternInSQLUnion = matcherSQLUnion.find();

                    //if (! matchBodyPattern(getBaseMsg(), errorPattern, null) && matchBodyPattern(msg3, errorPattern, sb)) {
                    if (!patternInOrig && patternInSQLUnion) {
                        //Likely a UNION Based SQL Injection (by error message). Raise it
                        String extraInfo = Constant.messages.getString(
                                "ascanrules.sqlinjection.alert.unionbased.extrainfo", errorPatternRDBMS,
                                errorPatternKey);

                        //raise the alert, and save the attack string for the "Authentication Bypass" alert, if necessary
                        sqlInjectionAttack = sqlUnionValue;
                        bingo(Alert.RISK_HIGH, Alert.WARNING, getName() + " - " + errorPatternRDBMS,
                                getDescription(), refreshedmessage.getRequestHeader().getURI().getURI(), //url
                                param, sqlInjectionAttack, extraInfo, getSolution(), msg3);

                        //log it, as the RDBMS may be useful to know later (in subsequent checks, when we need to determine RDBMS specific behaviour, for instance)
                        getKb().add(refreshedmessage.getRequestHeader().getURI(), "sql/" + errorPatternRDBMS,
                                Boolean.TRUE);

                        sqlInjectionFoundForUrl = true;
                        continue;
                    }
                } //end of the loop to check for RDBMS specific UNION error messages
            } ////for each SQL UNION combination to try
              //end of check 3

            //###############################

            //check for columns used in the "order by" clause of a SQL statement. earlier tests will likely not catch these

            //append on " ASC -- " to the end of the original parameter. Grab the results.
            //if the results are different to the original (unmodified parameter) results, then bale
            //if the results are the same as for the original parameter value, then the parameter *might* be influencing the order by
            //   try again for "DESC": append on " DESC -- " to the end of the original parameter. Grab the results.
            //   if the results are the same as the original (unmodified parameter) results, then bale
            //   (the results are not under our control, or there is no difference in the ordering, for some reason: 0 or 1 rows only, or ordering
            //   by the first column alone is not sufficient to change the ordering of the data.)
            //   if the results were different to the original (unmodified parameter) results, then
            //      SQL injection!!

            //Since the previous checks are attempting SQL injection, and may have actually succeeded in modifying the database (ask me how I know?!)
            //then we cannot rely on the database contents being the same as when the original query was last run (could be hours ago)
            //so to work around this, simply re-run the query again now at this point.
            //Note that we are not counting this request in our max number of requests to be issued
            refreshedmessage = getNewMsg();
            sendAndReceive(refreshedmessage);

            //String mResBodyNormal = getBaseMsg().getResponseBody().toString();
            mResBodyNormalUnstripped = refreshedmessage.getResponseBody().toString();
            mResBodyNormalStripped = this.stripOff(mResBodyNormalUnstripped, origParamValue);

            if (!sqlInjectionFoundForUrl && doOrderByBased && countOrderByBasedRequests < doOrderByMaxRequests) {

                String modifiedParamValue = TestSQLInjection.getURLDecode(origParamValue) + " ASC "
                        + SQL_ONE_LINE_COMMENT;

                HttpMessage msg5 = getNewMsg();
                setParameter(msg5, param, modifiedParamValue);

                sendAndReceive(msg5);
                countOrderByBasedRequests++;

                String modifiedAscendingOutputUnstripped = msg5.getResponseBody().toString();
                String modifiedAscendingOutputStripped = this.stripOff(modifiedAscendingOutputUnstripped,
                        modifiedParamValue);

                //set up two little arrays to ease the work of checking the unstripped output, and then the stripped output
                String normalBodyOutput[] = { mResBodyNormalUnstripped, mResBodyNormalStripped };
                String ascendingBodyOutput[] = { modifiedAscendingOutputUnstripped,
                        modifiedAscendingOutputStripped };
                boolean strippedOutput[] = { false, true };

                for (int booleanStrippedUnstrippedIndex = 0; booleanStrippedUnstrippedIndex < 2; booleanStrippedUnstrippedIndex++) {
                    //if the results of the modified request match the original query, we may be onto something.
                    if (ascendingBodyOutput[booleanStrippedUnstrippedIndex]
                            .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) == 0) {
                        if (this.debugEnabled)
                            log.debug("Check X, "
                                    + (strippedOutput[booleanStrippedUnstrippedIndex] ? "STRIPPED" : "UNSTRIPPED")
                                    + " html output for modified Order By parameter [" + modifiedParamValue
                                    + "] matched (refreshed) original results for "
                                    + refreshedmessage.getRequestHeader().getURI());
                        //confirm that a different parameter value generates different output, to minimise false positives

                        //use the descending order this time
                        String modifiedParamValueConfirm = TestSQLInjection.getURLDecode(origParamValue) + " DESC "
                                + SQL_ONE_LINE_COMMENT;

                        HttpMessage msg5Confirm = getNewMsg();
                        setParameter(msg5Confirm, param, modifiedParamValueConfirm);

                        sendAndReceive(msg5Confirm);
                        countOrderByBasedRequests++;

                        String confirmOrderByOutputUnstripped = msg5Confirm.getResponseBody().toString();
                        String confirmOrderByOutputStripped = this.stripOff(confirmOrderByOutputUnstripped,
                                modifiedParamValueConfirm);

                        //set up two little arrays to ease the work of checking the unstripped output or the stripped output
                        String confirmOrderByBodyOutput[] = { confirmOrderByOutputUnstripped,
                                confirmOrderByOutputStripped };

                        if (confirmOrderByBodyOutput[booleanStrippedUnstrippedIndex]
                                .compareTo(normalBodyOutput[booleanStrippedUnstrippedIndex]) != 0) {
                            //the confirm query did not return the same results.  This means that arbitrary queries are not all producing the same page output.
                            //this means the fact we earlier reproduced the original page output with a modified parameter was not a coincidence

                            //Likely a SQL Injection. Raise it
                            String extraInfo = null;
                            if (strippedOutput[booleanStrippedUnstrippedIndex])
                                extraInfo = Constant.messages.getString(
                                        "ascanrules.sqlinjection.alert.orderbybased.extrainfo", modifiedParamValue,
                                        "");
                            else
                                extraInfo = Constant.messages.getString(
                                        "ascanrules.sqlinjection.alert.orderbybased.extrainfo", modifiedParamValue,
                                        "NOT ");

                            //raise the alert, and save the attack string for the "Authentication Bypass" alert, if necessary
                            sqlInjectionAttack = modifiedParamValue;
                            bingo(Alert.RISK_HIGH, Alert.WARNING, getName(), getDescription(), null, //url
                                    param, sqlInjectionAttack, extraInfo, getSolution(), msg5);

                            sqlInjectionFoundForUrl = true;
                        }
                    }
                }

            }

            //###############################

            //if a sql injection was found, we should check if the page is flagged as a login page
            //in any of the contexts.  if it is, raise an "SQL Injection - Authentication Bypass" alert in addition to the alerts already raised
            if (sqlInjectionFoundForUrl) {
                boolean loginUrl = false;
                //log.debug("### A SQL Injection may lead to auth bypass..");

                //are we dealing with a login url in any of the contexts?
                ExtensionAuth extAuth = (ExtensionAuth) Control.getSingleton().getExtensionLoader()
                        .getExtension(ExtensionAuth.NAME);
                URI requestUri = getBaseMsg().getRequestHeader().getURI();

                //using the session, get the list of contexts for the url
                List<Context> contextList = extAuth.getModel().getSession().getContextsForUrl(requestUri.getURI());

                //now loop, and see if the url is a login url in each of the contexts in turn..
                for (Context context : contextList) {
                    HttpMessage loginRequest = extAuth.getApi().getLoginRequest(context.getIndex());
                    if (loginRequest != null) {
                        URI loginUri = loginRequest.getRequestHeader().getURI();
                        if (requestUri.getScheme().equals(loginUri.getScheme())
                                && requestUri.getHost().equals(loginUri.getHost())
                                && requestUri.getPort() == loginUri.getPort()
                                && requestUri.getPath().equals(loginUri.getPath())) {
                            //we got this far.. only the method (GET/POST), user details, query params, fragment, and POST params
                            //are possibly different from the login page.
                            loginUrl = true;
                            //DEBUG only
                            //log.debug("##### The right login page was found");
                            break;
                        } else {
                            //log.debug("#### This is not the login page you're looking for");
                        }
                    } else {
                        //log.debug("### This context has no login page set");
                    }
                }
                if (loginUrl) {
                    //log.debug("##### Raising auth bypass");
                    //raise the alert, using the custom name and description
                    String vulnname = Constant.messages.getString("ascanrules.sqlinjection.authbypass.name");
                    String vulndesc = Constant.messages.getString("ascanrules.sqlinjection.authbypass.desc");

                    //raise the alert, using the attack string stored earlier for this purpose
                    bingo(Alert.RISK_HIGH, Alert.WARNING, vulnname, vulndesc,
                            refreshedmessage.getRequestHeader().getURI().getURI(), //url
                            param, sqlInjectionAttack, "", getSolution(), getBaseMsg());

                } //not a login page
            } //no sql Injection Found For Url

        } catch (Exception e) {
            //Do not try to internationalise this.. we need an error message in any event..
            //if it's in English, it's still better than not having it at all.
            log.error("An error occurred checking a url for SQL Injection vulnerabilities", e);
        }
    }

    @Override
    public int getRisk() {
        return Alert.RISK_HIGH;
    }

    /**
     * Replace body by stripping of pattern string.  The URLencoded
     * pattern will also be stripped off.  The URL decoded pattern will not be stripped off,
     * as this is not necessary of rour purposes, and causes issues when attempting to decode parameter values such as '%' (a single percent character)
     * This is mainly used for stripping off a testing string in HTTP response
     * for comparison against the original response.
     * Reference: TestInjectionSQL
     *
     * @param body
     * @param pattern
     * @return
     */
    protected String stripOff(String body, String pattern) {
        String urlEncodePattern = getURLEncode(pattern);
        String htmlEncodePattern1 = getHTMLEncode(pattern);
        String htmlEncodePattern2 = getHTMLEncode(urlEncodePattern);
        String result = body.replaceAll("\\Q" + pattern + "\\E", "").replaceAll("\\Q" + urlEncodePattern + "\\E",
                "");
        result = result.replaceAll("\\Q" + htmlEncodePattern1 + "\\E", "")
                .replaceAll("\\Q" + htmlEncodePattern2 + "\\E", "");
        return result;
    }

    /**
     * decode method that is aware of %, and will decode it as simply %, if it occurs
     *
     * @param msg
     * @return
     */
    public static String getURLDecode(String msg) {
        String result = "";
        try {
            result = URLDecoder.decode(msg, "UTF8");
        } catch (Exception e) {
            //if it can't decode it, return the original string!
            return msg;
        }
        return result;
    }

}