[
  {
    "path": "README.md",
    "content": "Damn Small SQLi Scanner [![Python 3.x](https://img.shields.io/badge/python-3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-Public_domain-red.svg)](https://wiki.creativecommons.org/wiki/Public_domain)\n=========\n\n**Damn Small SQLi Scanner** (DSSS) is a fully functional [SQL injection](https://en.wikipedia.org/wiki/SQL_injection) vulnerability scanner (supporting GET and POST parameters) written in under 100 lines of code.\n\n![Vulnerable](http://i.imgur.com/7mXeXjF.png)\n\nAs of optional settings it supports HTTP proxy together with HTTP header values `User-Agent`, `Referer` and `Cookie`.\n\nSample runs\n----\n\n```\n$ python3 dsss.py -h\nDamn Small SQLi Scanner (DSSS) < 100 LoC (Lines of Code) #v0.3a\n by: Miroslav Stampar (@stamparm)\n\nUsage: dsss.py [options]\n\nOptions:\n  --version          show program's version number and exit\n  -h, --help         show this help message and exit\n  -u URL, --url=URL  Target URL (e.g. \"http://www.target.com/page.php?id=1\")\n  --data=DATA        POST data (e.g. \"query=test\")\n  --cookie=COOKIE    HTTP Cookie header value\n  --user-agent=UA    HTTP User-Agent header value\n  --referer=REFERER  HTTP Referer header value\n  --proxy=PROXY      HTTP proxy address (e.g. \"http://127.0.0.1:8080\")\n```\n\n```\n$ python3 dsss.py -u \"http://testphp.vulnweb.com/artists.php?artist=1\"\nDamn Small SQLi Scanner (DSSS) < 100 LoC (Lines of Code) #v0.3a\n by: Miroslav Stampar (@stamparm)\n\n* scanning GET parameter 'artist'\n (i) GET parameter 'artist' could be error SQLi vulnerable (MySQL)\n (i) GET parameter 'artist' appears to be blind SQLi vulnerable (e.g.: 'http://t\nestphp.vulnweb.com/artists.php?artist=1%20AND%2061%3E60')\n\nscan results: possible vulnerabilities found\n```\n\nRequirements\n----\n\n[Python](http://www.python.org/download/) version **3.x** is required for running this program.\n"
  },
  {
    "path": "dsss.py",
    "content": "#!/usr/bin/python3\nimport difflib, http.client, itertools, optparse, random, re, urllib, urllib.parse, urllib.request  # Python 3 required\n\nNAME, VERSION, AUTHOR, LICENSE = \"Damn Small SQLi Scanner (DSSS) < 100 LoC (Lines of Code)\", \"0.3b\", \"Miroslav Stampar (@stamparm)\", \"Public domain (FREE)\"\n\nPREFIXES, SUFFIXES = (\" \", \") \", \"' \", \"') \"), (\"\", \"-- -\", \"#\", \"%%16\")            # prefix/suffix values used for building testing blind payloads\nTAMPER_SQL_CHAR_POOL = ('(', ')', '\\'', '\"')                                        # characters used for SQL tampering/poisoning of parameter values\nBOOLEAN_TESTS = (\"AND %d=%d\", \"OR NOT (%d>%d)\")                                     # boolean tests used for building testing blind payloads\nCOOKIE, UA, REFERER = \"Cookie\", \"User-Agent\", \"Referer\"                             # optional HTTP header names\nGET, POST = \"GET\", \"POST\"                                                           # enumerator-like values used for marking current phase\nTEXT, HTTPCODE, TITLE, HTML = range(4)                                             # enumerator-like values used for marking content type\nFUZZY_THRESHOLD = 0.95                                                              # ratio value in range (0,1) used for distinguishing True from False responses\nTIMEOUT = 30                                                                        # connection timeout in seconds\nRANDINT = random.randint(1, 255)                                                    # random integer value used across all tests\nBLOCKED_IP_REGEX = r\"(?i)(\\A|\\b)IP\\b.*\\b(banned|blocked|bl(a|o)ck\\s?list|firewall)\" # regular expression used for recognition of generic firewall blocking messages\n\nDBMS_ERRORS = {                                                                     # regular expressions used for DBMS recognition based on error message response\n    \"MySQL\": (r\"SQL syntax.*MySQL\", r\"Warning.*mysql_.*\", r\"valid MySQL result\", r\"MySqlClient\\.\"),\n    \"PostgreSQL\": (r\"PostgreSQL.*ERROR\", r\"Warning.*\\Wpg_.*\", r\"valid PostgreSQL result\", r\"Npgsql\\.\"),\n    \"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\"(?s)Exception.*\\WSystem\\.Data\\.SqlClient\\.\", r\"(?s)Exception.*\\WRoadhouse\\.Cms\\.\"),\n    \"Microsoft Access\": (r\"Microsoft Access Driver\", r\"JET Database Engine\", r\"Access Database Engine\"),\n    \"Oracle\": (r\"\\bORA-[0-9][0-9][0-9][0-9]\", r\"Oracle error\", r\"Oracle.*Driver\", r\"Warning.*\\Woci_.*\", r\"Warning.*\\Wora_.*\"),\n    \"IBM DB2\": (r\"CLI Driver.*DB2\", r\"DB2 SQL error\", r\"\\bdb2_\\w+\\(\"),\n    \"SQLite\": (r\"SQLite/JDBCDriver\", r\"SQLite.Exception\", r\"System.Data.SQLite.SQLiteException\", r\"Warning.*sqlite_.*\", r\"Warning.*SQLite3::\", r\"\\[SQLITE_ERROR\\]\"),\n    \"Sybase\": (r\"(?i)Warning.*sybase.*\", r\"Sybase message\", r\"Sybase.*Server message.*\"),\n}\n\ndef _retrieve_content(url, data=None):\n    retval = {HTTPCODE: http.client.OK}\n    try:\n        req = urllib.request.Request(\"\".join(url[_].replace(' ', \"%20\") if _ > url.find('?') else url[_] for _ in range(len(url))), data.encode(\"utf8\", \"ignore\") if data else None, globals().get(\"_headers\", {}))\n        retval[HTML] = urllib.request.urlopen(req, timeout=TIMEOUT).read()\n    except Exception as ex:\n        retval[HTTPCODE] = getattr(ex, \"code\", None)\n        retval[HTML] = ex.read() if hasattr(ex, \"read\") else str(ex.args[-1])\n    retval[HTML] = (retval[HTML].decode(\"utf8\", \"ignore\") if hasattr(retval[HTML], \"decode\") else \"\") or \"\"\n    retval[HTML] = \"\" if re.search(BLOCKED_IP_REGEX, retval[HTML]) else retval[HTML]\n    retval[HTML] = re.sub(r\"(?i)[^>]*(AND|OR)[^<]*%d[^<]*\" % RANDINT, \"__REFLECTED__\", retval[HTML])\n    match = re.search(r\"<title>(?P<result>[^<]+)</title>\", retval[HTML], re.I)\n    retval[TITLE] = match.group(\"result\") if match and \"result\" in match.groupdict() else None\n    retval[TEXT] = re.sub(r\"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>|<[^>]+>|\\s+\", \" \", retval[HTML])\n    return retval\n\ndef scan_page(url, data=None):\n    retval, usable = False, False\n    url, data = re.sub(r\"=(&|\\Z)\", r\"=1\\g<1>\", url) if url else url, re.sub(r\"=(&|\\Z)\", r\"=1\\g<1>\", data) if data else data\n    try:\n        for phase in (GET, POST):\n            original, current = None, url if phase is GET else (data or \"\")\n            for match in re.finditer(r\"((\\A|[?&])(?P<parameter>[^_]\\w*)=)(?P<value>[^&#]+)\", current):\n                vulnerable, usable = False, True\n                print(\"* scanning %s parameter '%s'\" % (phase, match.group(\"parameter\")))\n                original = original or (_retrieve_content(current, data) if phase is GET else _retrieve_content(url, current))\n                tampered = current.replace(match.group(0), \"%s%s\" % (match.group(0), urllib.parse.quote(\"\".join(random.sample(TAMPER_SQL_CHAR_POOL, len(TAMPER_SQL_CHAR_POOL))))))\n                content = _retrieve_content(tampered, data) if phase is GET else _retrieve_content(url, tampered)\n                for (dbms, regex) in ((dbms, regex) for dbms in DBMS_ERRORS for regex in DBMS_ERRORS[dbms]):\n                    if not vulnerable and re.search(regex, content[HTML], re.I) and not re.search(regex, original[HTML], re.I):\n                        print(\" (i) %s parameter '%s' appears to be error SQLi vulnerable (%s)\" % (phase, match.group(\"parameter\"), dbms))\n                        retval = vulnerable = True\n                vulnerable = False\n                for prefix, boolean, suffix, inline_comment in itertools.product(PREFIXES, BOOLEAN_TESTS, SUFFIXES, (False, True)):\n                    if not vulnerable:\n                        template = (\"%s%s%s\" % (prefix, boolean, suffix)).replace(\" \" if inline_comment else \"/**/\", \"/**/\")\n                        payloads = dict((_, current.replace(match.group(0), \"%s%s\" % (match.group(0), urllib.parse.quote(template % (RANDINT if _ else RANDINT + 1, RANDINT), safe='%')))) for _ in (True, False))\n                        contents = dict((_, _retrieve_content(payloads[_], data) if phase is GET else _retrieve_content(url, payloads[_])) for _ in (False, True))\n                        if all(_[HTTPCODE] and _[HTTPCODE] < http.client.INTERNAL_SERVER_ERROR for _ in (original, contents[True], contents[False])):\n                            if any(original[_] == contents[True][_] != contents[False][_] for _ in (HTTPCODE, TITLE)):\n                                vulnerable = True\n                            else:\n                                ratios = dict((_, difflib.SequenceMatcher(None, original[TEXT], contents[_][TEXT]).quick_ratio()) for _ in (False, True))\n                                vulnerable = all(ratios.values()) and min(ratios.values()) < FUZZY_THRESHOLD < max(ratios.values()) and abs(ratios[True] - ratios[False]) > FUZZY_THRESHOLD / 10\n                        if vulnerable:\n                            print(\" (i) %s parameter '%s' appears to be blind SQLi vulnerable (e.g.: '%s')\" % (phase, match.group(\"parameter\"), payloads[True]))\n                            retval = True\n        if not usable:\n            print(\" (x) no usable GET/POST parameters found\")\n    except KeyboardInterrupt:\n        print(\"\\r (x) Ctrl-C pressed\")\n    return retval\n\ndef init_options(proxy=None, cookie=None, ua=None, referer=None):\n    globals()[\"_headers\"] = dict(filter(lambda _: _[1], ((COOKIE, cookie), (UA, ua or NAME), (REFERER, referer))))\n    urllib.request.install_opener(urllib.request.build_opener(urllib.request.ProxyHandler({'http': proxy})) if proxy else None)\n\nif __name__ == \"__main__\":\n    print(\"%s #v%s\\n by: %s\\n\" % (NAME, VERSION, AUTHOR))\n    parser = optparse.OptionParser(version=VERSION)\n    parser.add_option(\"-u\", \"--url\", dest=\"url\", help=\"Target URL (e.g. \\\"http://www.target.com/page.php?id=1\\\")\")\n    parser.add_option(\"--data\", dest=\"data\", help=\"POST data (e.g. \\\"query=test\\\")\")\n    parser.add_option(\"--cookie\", dest=\"cookie\", help=\"HTTP Cookie header value\")\n    parser.add_option(\"--user-agent\", dest=\"ua\", help=\"HTTP User-Agent header value\")\n    parser.add_option(\"--referer\", dest=\"referer\", help=\"HTTP Referer header value\")\n    parser.add_option(\"--proxy\", dest=\"proxy\", help=\"HTTP proxy address (e.g. \\\"http://127.0.0.1:8080\\\")\")\n    options, _ = parser.parse_args()\n    if options.url:\n        init_options(options.proxy, options.cookie, options.ua, options.referer)\n        result = scan_page(options.url if options.url.startswith(\"http\") else \"http://%s\" % options.url, options.data)\n        print(\"\\nscan results: %s vulnerabilities found\" % (\"possible\" if result else \"no\"))\n    else:\n        parser.print_help()\n"
  }
]