1#!/usr/bin/env python
2
3"""
4Copyright (c) 2019 Miroslav Stampar (@stamparm), MIT
5See the file 'LICENSE' for copying permission
6
7The above copyright notice and this permission notice shall be included in
8all copies or substantial portions of the Software.
9"""
10
11from __future__ import print_function
12
13import base64
14import codecs
15import difflib
16import json
17import locale
18import optparse
19import os
20import random
21import re
22import ssl
23import socket
24import string
25import struct
26import sys
27import time
28import zlib
29
30PY3 = sys.version_info >= (3, 0)
31
32if PY3:
33    import http.cookiejar
34    import http.client as httplib
35    import urllib.request
36
37    build_opener = urllib.request.build_opener
38    install_opener = urllib.request.install_opener
39    quote = urllib.parse.quote
40    urlopen = urllib.request.urlopen
41    CookieJar = http.cookiejar.CookieJar
42    ProxyHandler = urllib.request.ProxyHandler
43    Request = urllib.request.Request
44    HTTPCookieProcessor = urllib.request.HTTPCookieProcessor
45
46    xrange = range
47else:
48    import cookielib
49    import httplib
50    import urllib
51    import urllib2
52
53    build_opener = urllib2.build_opener
54    install_opener = urllib2.install_opener
55    quote = urllib.quote
56    urlopen = urllib2.urlopen
57    CookieJar = cookielib.CookieJar
58    ProxyHandler = urllib2.ProxyHandler
59    Request = urllib2.Request
60    HTTPCookieProcessor = urllib2.HTTPCookieProcessor
61
62NAME = "identYwaf"
63VERSION = "1.0.122"
64BANNER = r"""
65                                   ` __ __ `
66 ____  ___      ___  ____   ______ `|  T  T` __    __   ____  _____
67l    j|   \    /  _]|    \ |      T`|  |  |`|  T__T  T /    T|   __|
68 |  T |    \  /  [_ |  _  Yl_j  l_j`|  ~  |`|  |  |  |Y  o  ||  l_
69 |  | |  D  YY    _]|  |  |  |  |  `|___  |`|  |  |  ||     ||   _|
70 j  l |     ||   [_ |  |  |  |  |  `|     !` \      / |  |  ||  ]
71|____jl_____jl_____jl__j__j  l__j  `l____/ `  \_/\_/  l__j__jl__j  (%s)%s""".strip("\n") % (VERSION, "\n")
72
73RAW, TEXT, HTTPCODE, SERVER, TITLE, HTML, URL = xrange(7)
74COOKIE, UA, REFERER = "Cookie", "User-Agent", "Referer"
75GET, POST = "GET", "POST"
76GENERIC_PROTECTION_KEYWORDS = ("rejected", "forbidden", "suspicious", "malicious", "captcha", "invalid", "your ip", "please contact", "terminated", "protected", "unauthorized", "blocked", "protection", "incident", "denied", "detected", "dangerous", "firewall", "fw_block", "unusual activity", "bad request", "request id", "injection", "permission", "not acceptable", "security policy", "security reasons")
77GENERIC_PROTECTION_REGEX = r"(?i)\b(%s)\b"
78GENERIC_ERROR_MESSAGE_REGEX = r"\b[A-Z][\w, '-]*(protected by|security|unauthorized|detected|attack|error|rejected|allowed|suspicious|automated|blocked|invalid|denied|permission)[\w, '!-]*"
79WAF_RECOGNITION_REGEX = None
80HEURISTIC_PAYLOAD = "1 AND 1=1 UNION ALL SELECT 1,NULL,'<script>alert(\"XSS\")</script>',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../etc/passwd')#"  # Reference: https://github.com/sqlmapproject/sqlmap/blob/master/lib/core/settings.py
81PAYLOADS = []
82SIGNATURES = {}
83DATA_JSON = {}
84DATA_JSON_FILE = os.path.join(os.path.dirname(__file__), "data.json")
85MAX_HELP_OPTION_LENGTH = 18
86IS_TTY = sys.stdout.isatty()
87IS_WIN = os.name == "nt"
88COLORIZE = not IS_WIN and IS_TTY
89LEVEL_COLORS = {"o": "\033[00;94m", "x": "\033[00;91m", "!": "\033[00;93m", "i": "\033[00;95m", "=": "\033[00;93m", "+": "\033[00;92m", "-": "\033[00;91m"}
90VERIFY_OK_INTERVAL = 5
91VERIFY_RETRY_TIMES = 3
92MIN_MATCH_PARTIAL = 5
93DEFAULTS = {"timeout": 10}
94MAX_MATCHES = 5
95QUICK_RATIO_THRESHOLD = 0.2
96MAX_JS_CHALLENGE_SNAPLEN = 120
97ENCODING_TRANSLATIONS = {"windows-874": "iso-8859-11", "utf-8859-1": "utf8", "en_us": "utf8", "macintosh": "iso-8859-1", "euc_tw": "big5_tw", "th": "tis-620", "unicode": "utf8", "utc8": "utf8", "ebcdic": "ebcdic-cp-be", "iso-8859": "iso8859-1", "iso-8859-0": "iso8859-1", "ansi": "ascii", "gbk2312": "gbk", "windows-31j": "cp932", "en": "us"}  # Reference: https://github.com/sqlmapproject/sqlmap/blob/master/lib/request/basic.py
98PROXY_TESTING_PAGE = "https://myexternalip.com/raw"
99
100if COLORIZE:
101    for _ in re.findall(r"`.+?`", BANNER):
102        BANNER = BANNER.replace(_, "\033[01;92m%s\033[00;49m" % _.strip('`'))
103    for _ in re.findall(r" [Do] ", BANNER):
104        BANNER = BANNER.replace(_, "\033[01;93m%s\033[00;49m" % _.strip('`'))
105    BANNER = re.sub(VERSION, r"\033[01;91m%s\033[00;49m" % VERSION, BANNER)
106else:
107    BANNER = BANNER.replace('`', "")
108
109_ = random.randint(20, 64)
110DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; %s; rv:%d.0) Gecko/20100101 Firefox/%d.0" % (NAME, _, _)
111HEADERS = {"User-Agent": DEFAULT_USER_AGENT, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "identity", "Cache-Control": "max-age=0"}
112
113original = None
114options = None
115intrusive = None
116heuristic = None
117chained = False
118locked_code = None
119locked_regex = None
120non_blind = set()
121seen = set()
122blocked = []
123servers = set()
124codes = set()
125proxies = list()
126proxies_index = 0
127
128_exit = exit
129
130def exit(message=None):
131    if message:
132        print("%s%s" % (message, ' ' * 20))
133    _exit(1)
134
135def retrieve(url, data=None):
136    global proxies_index
137
138    retval = {}
139
140    if proxies:
141        while True:
142            try:
143                opener = build_opener(ProxyHandler({"http": proxies[proxies_index], "https": proxies[proxies_index]}))
144                install_opener(opener)
145                proxies_index = (proxies_index + 1) % len(proxies)
146                urlopen(PROXY_TESTING_PAGE).read()
147            except KeyboardInterrupt:
148                raise
149            except:
150                pass
151            else:
152                break
153
154    try:
155        req = Request("".join(url[_].replace(' ', "%20") if _ > url.find('?') else url[_] for _ in xrange(len(url))), data, HEADERS)
156        resp = urlopen(req, timeout=options.timeout)
157        retval[URL] = resp.url
158        retval[HTML] = resp.read()
159        retval[HTTPCODE] = resp.code
160        retval[RAW] = "%s %d %s\n%s\n%s" % (httplib.HTTPConnection._http_vsn_str, retval[HTTPCODE], resp.msg, str(resp.headers), retval[HTML])
161    except Exception as ex:
162        retval[URL] = getattr(ex, "url", url)
163        retval[HTTPCODE] = getattr(ex, "code", None)
164        try:
165            retval[HTML] = ex.read() if hasattr(ex, "read") else getattr(ex, "msg", str(ex))
166        except:
167            retval[HTML] = ""
168        retval[RAW] = "%s %s %s\n%s\n%s" % (httplib.HTTPConnection._http_vsn_str, retval[HTTPCODE] or "", getattr(ex, "msg", ""), str(ex.headers) if hasattr(ex, "headers") else "", retval[HTML])
169
170    for encoding in re.findall(r"charset=[\s\"']?([\w-]+)", retval[RAW])[::-1] + ["utf8"]:
171        encoding = ENCODING_TRANSLATIONS.get(encoding, encoding)
172        try:
173            retval[HTML] = retval[HTML].decode(encoding, errors="replace")
174            break
175        except:
176            pass
177
178    match = re.search(r"<title>\s*(?P<result>[^<]+?)\s*</title>", retval[HTML], re.I)
179    retval[TITLE] = match.group("result") if match and "result" in match.groupdict() else None
180    retval[TEXT] = re.sub(r"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>|<[^>]+>|\s+", " ", retval[HTML])
181    match = re.search(r"(?im)^Server: (.+)", retval[RAW])
182    retval[SERVER] = match.group(1).strip() if match else ""
183    return retval
184
185def calc_hash(value, binary=True):
186    value = value.encode("utf8") if not isinstance(value, bytes) else value
187    result = zlib.crc32(value) & 0xffff
188    if binary:
189        result = struct.pack(">H", result)
190    return result
191
192def single_print(message):
193    if message not in seen:
194        print(message)
195        seen.add(message)
196
197def check_payload(payload, protection_regex=GENERIC_PROTECTION_REGEX % '|'.join(GENERIC_PROTECTION_KEYWORDS)):
198    global chained
199    global heuristic
200    global intrusive
201    global locked_code
202    global locked_regex
203
204    time.sleep(options.delay or 0)
205    if options.post:
206        _ = "%s=%s" % ("".join(random.sample(string.ascii_letters, 3)), quote(payload))
207        intrusive = retrieve(options.url, _)
208    else:
209        _ = "%s%s%s=%s" % (options.url, '?' if '?' not in options.url else '&', "".join(random.sample(string.ascii_letters, 3)), quote(payload))
210        intrusive = retrieve(_)
211
212    if options.lock and not payload.isdigit():
213        if payload == HEURISTIC_PAYLOAD:
214            match = re.search(re.sub(r"Server:|Protected by", "".join(random.sample(string.ascii_letters, 6)), WAF_RECOGNITION_REGEX, flags=re.I), intrusive[RAW] or "")
215            if match:
216                result = True
217
218                for _ in match.groupdict():
219                    if match.group(_):
220                        waf = re.sub(r"\Awaf_", "", _)
221                        locked_regex = DATA_JSON["wafs"][waf]["regex"]
222                        locked_code = intrusive[HTTPCODE]
223                        break
224            else:
225                result = False
226
227            if not result:
228                exit(colorize("[x] can't lock results to a non-blind match"))
229        else:
230            result = re.search(locked_regex, intrusive[RAW]) is not None and locked_code == intrusive[HTTPCODE]
231    elif options.string:
232        result = options.string in (intrusive[RAW] or "")
233    elif options.code:
234        result = options.code == intrusive[HTTPCODE]
235    else:
236        result = intrusive[HTTPCODE] != original[HTTPCODE] or (intrusive[HTTPCODE] != 200 and intrusive[TITLE] != original[TITLE]) or (re.search(protection_regex, intrusive[HTML]) is not None and re.search(protection_regex, original[HTML]) is None) or (difflib.SequenceMatcher(a=original[HTML] or "", b=intrusive[HTML] or "").quick_ratio() < QUICK_RATIO_THRESHOLD)
237
238    if not payload.isdigit():
239        if result:
240            if options.debug:
241                print("\r---%s" % (40 * ' '))
242                print(payload)
243                print(intrusive[HTTPCODE], intrusive[RAW])
244                print("---")
245
246            if intrusive[SERVER]:
247                servers.add(re.sub(r"\s*\(.+\)\Z", "", intrusive[SERVER]))
248                if len(servers) > 1:
249                    chained = True
250                    single_print(colorize("[!] multiple (reactive) rejection HTTP 'Server' headers detected (%s)" % ', '.join("'%s'" % _ for _ in sorted(servers))))
251
252            if intrusive[HTTPCODE]:
253                codes.add(intrusive[HTTPCODE])
254                if len(codes) > 1:
255                    chained = True
256                    single_print(colorize("[!] multiple (reactive) rejection HTTP codes detected (%s)" % ', '.join("%s" % _ for _ in sorted(codes))))
257
258            if heuristic and heuristic[HTML] and intrusive[HTML] and difflib.SequenceMatcher(a=heuristic[HTML] or "", b=intrusive[HTML] or "").quick_ratio() < QUICK_RATIO_THRESHOLD:
259                chained = True
260                single_print(colorize("[!] multiple (reactive) rejection HTML responses detected"))
261
262    if payload == HEURISTIC_PAYLOAD:
263        heuristic = intrusive
264
265    return result
266
267def colorize(message):
268    if COLORIZE:
269        message = re.sub(r"\[(.)\]", lambda match: "[%s%s\033[00;49m]" % (LEVEL_COLORS[match.group(1)], match.group(1)), message)
270
271        if any(_ in message for _ in ("rejected summary", "challenge detected")):
272            for match in re.finditer(r"[^\w]'([^)]+)'" if "rejected summary" in message else r"\('(.+)'\)", message):
273                message = message.replace("'%s'" % match.group(1), "'\033[37m%s\033[00;49m'" % match.group(1), 1)
274        else:
275            for match in re.finditer(r"[^\w]'([^']+)'", message):
276                message = message.replace("'%s'" % match.group(1), "'\033[37m%s\033[00;49m'" % match.group(1), 1)
277
278        if "blind match" in message:
279            for match in re.finditer(r"\(((\d+)%)\)", message):
280                message = message.replace(match.group(1), "\033[%dm%s\033[00;49m" % (92 if int(match.group(2)) >= 95 else (93 if int(match.group(2)) > 80 else 90), match.group(1)))
281
282        if "hardness" in message:
283            for match in re.finditer(r"\(((\d+)%)\)", message):
284                message = message.replace(match.group(1), "\033[%dm%s\033[00;49m" % (95 if " insane " in message else (91 if " hard " in message else (93 if " moderate " in message else 92)), match.group(1)))
285
286    return message
287
288def parse_args():
289    global options
290
291    parser = optparse.OptionParser(version=VERSION)
292    parser.add_option("--delay", dest="delay", type=int, help="Delay (sec) between tests (default: 0)")
293    parser.add_option("--timeout", dest="timeout", type=int, help="Response timeout (sec) (default: 10)")
294    parser.add_option("--proxy", dest="proxy", help="HTTP proxy address (e.g. \"http://127.0.0.1:8080\")")
295    parser.add_option("--proxy-file", dest="proxy_file", help="Load (rotating) HTTP(s) proxy list from a file")
296    parser.add_option("--random-agent", dest="random_agent", action="store_true", help="Use random HTTP User-Agent header value")
297    parser.add_option("--code", dest="code", type=int, help="Expected HTTP code in rejected responses")
298    parser.add_option("--string", dest="string", help="Expected string in rejected responses")
299    parser.add_option("--post", dest="post", action="store_true", help="Use POST body for sending payloads")
300    parser.add_option("--debug", dest="debug", action="store_true", help=optparse.SUPPRESS_HELP)
301    parser.add_option("--fast", dest="fast", action="store_true", help=optparse.SUPPRESS_HELP)
302    parser.add_option("--lock", dest="lock", action="store_true", help=optparse.SUPPRESS_HELP)
303
304    # Dirty hack(s) for help message
305    def _(self, *args):
306        retval = parser.formatter._format_option_strings(*args)
307        if len(retval) > MAX_HELP_OPTION_LENGTH:
308            retval = ("%%.%ds.." % (MAX_HELP_OPTION_LENGTH - parser.formatter.indent_increment)) % retval
309        return retval
310
311    parser.usage = "python %s <host|url>" % parser.usage
312    parser.formatter._format_option_strings = parser.formatter.format_option_strings
313    parser.formatter.format_option_strings = type(parser.formatter.format_option_strings)(_, parser)
314
315    for _ in ("-h", "--version"):
316        option = parser.get_option(_)
317        option.help = option.help.capitalize()
318
319    try:
320        options, _ = parser.parse_args()
321    except SystemExit:
322        raise
323
324    if len(sys.argv) > 1:
325        url = sys.argv[-1]
326        if not url.startswith("http"):
327            url = "http://%s" % url
328        options.url = url
329    else:
330        parser.print_help()
331        raise SystemExit
332
333    for key in DEFAULTS:
334        if getattr(options, key, None) is None:
335            setattr(options, key, DEFAULTS[key])
336
337def load_data():
338    global WAF_RECOGNITION_REGEX
339
340    if os.path.isfile(DATA_JSON_FILE):
341        with codecs.open(DATA_JSON_FILE, "rb", encoding="utf8") as f:
342            DATA_JSON.update(json.load(f))
343
344        WAF_RECOGNITION_REGEX = ""
345        for waf in DATA_JSON["wafs"]:
346            if DATA_JSON["wafs"][waf]["regex"]:
347                WAF_RECOGNITION_REGEX += "%s|" % ("(?P<waf_%s>%s)" % (waf, DATA_JSON["wafs"][waf]["regex"]))
348            for signature in DATA_JSON["wafs"][waf]["signatures"]:
349                SIGNATURES[signature] = waf
350        WAF_RECOGNITION_REGEX = WAF_RECOGNITION_REGEX.strip('|')
351
352        flags = "".join(set(_ for _ in "".join(re.findall(r"\(\?(\w+)\)", WAF_RECOGNITION_REGEX))))
353        WAF_RECOGNITION_REGEX = "(?%s)%s" % (flags, re.sub(r"\(\?\w+\)", "", WAF_RECOGNITION_REGEX))  # patch for "DeprecationWarning: Flags not at the start of the expression" in Python3.7
354    else:
355        exit(colorize("[x] file '%s' is missing" % DATA_JSON_FILE))
356
357def init():
358    os.chdir(os.path.abspath(os.path.dirname(__file__)))
359
360    # Reference: http://blog.mathieu-leplatre.info/python-utf-8-print-fails-when-redirecting-stdout.html
361    if not PY3 and not IS_TTY:
362        sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
363
364    print(colorize("[o] initializing handlers..."))
365
366    # Reference: https://stackoverflow.com/a/28052583
367    if hasattr(ssl, "_create_unverified_context"):
368        ssl._create_default_https_context = ssl._create_unverified_context
369
370    if options.proxy_file:
371        if os.path.isfile(options.proxy_file):
372            print(colorize("[o] loading proxy list..."))
373
374            with codecs.open(options.proxy_file, "rb", encoding="utf8") as f:
375                proxies.extend(re.sub(r"\s.*", "", _.strip()) for _ in f.read().strip().split('\n') if _.startswith("http"))
376                random.shuffle(proxies)
377        else:
378            exit(colorize("[x] file '%s' does not exist" % options.proxy_file))
379
380
381    cookie_jar = CookieJar()
382    opener = build_opener(HTTPCookieProcessor(cookie_jar))
383    install_opener(opener)
384
385    if options.proxy:
386        opener = build_opener(ProxyHandler({"http": options.proxy, "https": options.proxy}))
387        install_opener(opener)
388
389    if options.random_agent:
390        revision = random.randint(20, 64)
391        platform = random.sample(("X11; %s %s" % (random.sample(("Linux", "Ubuntu; Linux", "U; Linux", "U; OpenBSD", "U; FreeBSD"), 1)[0], random.sample(("amd64", "i586", "i686", "amd64"), 1)[0]), "Windows NT %s%s" % (random.sample(("5.0", "5.1", "5.2", "6.0", "6.1", "6.2", "6.3", "10.0"), 1)[0], random.sample(("", "; Win64", "; WOW64"), 1)[0]), "Macintosh; Intel Mac OS X 10.%s" % random.randint(1, 11)), 1)[0]
392        user_agent = "Mozilla/5.0 (%s; rv:%d.0) Gecko/20100101 Firefox/%d.0" % (platform, revision, revision)
393        HEADERS["User-Agent"] = user_agent
394
395def format_name(waf):
396    return "%s%s" % (DATA_JSON["wafs"][waf]["name"], (" (%s)" % DATA_JSON["wafs"][waf]["company"]) if DATA_JSON["wafs"][waf]["name"] != DATA_JSON["wafs"][waf]["company"] else "")
397
398def non_blind_check(raw, silent=False):
399    retval = False
400    match = re.search(WAF_RECOGNITION_REGEX, raw or "")
401    if match:
402        retval = True
403        for _ in match.groupdict():
404            if match.group(_):
405                waf = re.sub(r"\Awaf_", "", _)
406                non_blind.add(waf)
407                if not silent:
408                    single_print(colorize("[+] non-blind match: '%s'%s" % (format_name(waf), 20 * ' ')))
409    return retval
410
411def run():
412    global original
413
414    hostname = options.url.split("//")[-1].split('/')[0].split(':')[0]
415
416    if not hostname.replace('.', "").isdigit():
417        print(colorize("[i] checking hostname '%s'..." % hostname))
418        try:
419            socket.getaddrinfo(hostname, None)
420        except socket.gaierror:
421            exit(colorize("[x] host '%s' does not exist" % hostname))
422
423    results = ""
424    signature = b""
425    counter = 0
426    original = retrieve(options.url)
427
428    if 300 <= (original[HTTPCODE] or 0) < 400 and original[URL]:
429        original = retrieve(original[URL])
430
431    options.url = original[URL]
432
433    if original[HTTPCODE] is None:
434        exit(colorize("[x] missing valid response"))
435
436    if not any((options.string, options.code)) and original[HTTPCODE] >= 400:
437        non_blind_check(original[RAW])
438        if options.debug:
439            print("\r---%s" % (40 * ' '))
440            print(original[HTTPCODE], original[RAW])
441            print("---")
442        exit(colorize("[x] access to host '%s' seems to be restricted%s" % (hostname, (" (%d: '<title>%s</title>')" % (original[HTTPCODE], original[TITLE].strip())) if original[TITLE] else "")))
443
444    challenge = None
445    if all(_ in original[HTML].lower() for _ in ("eval", "<script")):
446        match = re.search(r"(?is)<body[^>]*>(.*)</body>", re.sub(r"(?is)<script.+?</script>", "", original[HTML]))
447        if re.search(r"(?i)<(body|div)", original[HTML]) is None or (match and len(match.group(1)) == 0):
448            challenge = re.search(r"(?is)<script.+</script>", original[HTML]).group(0).replace("\n", "\\n")
449            print(colorize("[x] anti-robot JS challenge detected ('%s%s')" % (challenge[:MAX_JS_CHALLENGE_SNAPLEN], "..." if len(challenge) > MAX_JS_CHALLENGE_SNAPLEN else "")))
450
451    protection_keywords = GENERIC_PROTECTION_KEYWORDS
452    protection_regex = GENERIC_PROTECTION_REGEX % '|'.join(keyword for keyword in protection_keywords if keyword not in original[HTML].lower())
453
454    print(colorize("[i] running basic heuristic test..."))
455    if not check_payload(HEURISTIC_PAYLOAD):
456        check = False
457        if options.url.startswith("https://"):
458            options.url = options.url.replace("https://", "http://")
459            check = check_payload(HEURISTIC_PAYLOAD)
460        if not check:
461            if non_blind_check(intrusive[RAW]):
462                exit(colorize("[x] unable to continue due to static responses%s" % (" (captcha)" if re.search(r"(?i)captcha", intrusive[RAW]) is not None else "")))
463            elif challenge is None:
464                exit(colorize("[x] host '%s' does not seem to be protected" % hostname))
465            else:
466                exit(colorize("[x] response not changing without JS challenge solved"))
467
468    if options.fast and not non_blind:
469        exit(colorize("[x] fast exit because of missing non-blind match"))
470
471    if not intrusive[HTTPCODE]:
472        print(colorize("[i] rejected summary: RST|DROP"))
473    else:
474        _ = "...".join(match.group(0) for match in re.finditer(GENERIC_ERROR_MESSAGE_REGEX, intrusive[HTML])).strip().replace("  ", " ")
475        print(colorize(("[i] rejected summary: %d ('%s%s')" % (intrusive[HTTPCODE], ("<title>%s</title>" % intrusive[TITLE]) if intrusive[TITLE] else "", "" if not _ or intrusive[HTTPCODE] < 400 else ("...%s" % _))).replace(" ('')", "")))
476
477    found = non_blind_check(intrusive[RAW] if intrusive[HTTPCODE] is not None else original[RAW])
478
479    if not found:
480        print(colorize("[-] non-blind match: -"))
481
482    for item in DATA_JSON["payloads"]:
483        info, payload = item.split("::", 1)
484        counter += 1
485
486        if IS_TTY:
487            sys.stdout.write(colorize("\r[i] running payload tests... (%d/%d)\r" % (counter, len(DATA_JSON["payloads"]))))
488            sys.stdout.flush()
489
490        if counter % VERIFY_OK_INTERVAL == 0:
491            for i in xrange(VERIFY_RETRY_TIMES):
492                if not check_payload(str(random.randint(1, 9)), protection_regex):
493                    break
494                elif i == VERIFY_RETRY_TIMES - 1:
495                    exit(colorize("[x] host '%s' seems to be misconfigured or rejecting benign requests%s" % (hostname, (" (%d: '<title>%s</title>')" % (intrusive[HTTPCODE], intrusive[TITLE].strip())) if intrusive[TITLE] else "")))
496                else:
497                    time.sleep(5)
498
499        last = check_payload(payload, protection_regex)
500        non_blind_check(intrusive[RAW])
501        signature += struct.pack(">H", ((calc_hash(payload, binary=False) << 1) | last) & 0xffff)
502        results += 'x' if last else '.'
503
504        if last and info not in blocked:
505            blocked.append(info)
506
507    _ = calc_hash(signature)
508    signature = "%s:%s" % (_.encode("hex") if not hasattr(_, "hex") else _.hex(), base64.b64encode(signature).decode("ascii"))
509
510    print(colorize("%s[=] results: '%s'" % ("\n" if IS_TTY else "", results)))
511
512    hardness = 100 * results.count('x') / len(results)
513    print(colorize("[=] hardness: %s (%d%%)" % ("insane" if hardness >= 80 else ("hard" if hardness >= 50 else ("moderate" if hardness >= 30 else "easy")), hardness)))
514
515    if blocked:
516        print(colorize("[=] blocked categories: %s" % ", ".join(blocked)))
517
518    if not results.strip('.') or not results.strip('x'):
519        print(colorize("[-] blind match: -"))
520
521        if re.search(r"(?i)captcha", original[HTML]) is not None:
522            exit(colorize("[x] there seems to be an activated captcha"))
523    else:
524        print(colorize("[=] signature: '%s'" % signature))
525
526        if signature in SIGNATURES:
527            waf = SIGNATURES[signature]
528            print(colorize("[+] blind match: '%s' (100%%)" % format_name(waf)))
529        elif results.count('x') < MIN_MATCH_PARTIAL:
530            print(colorize("[-] blind match: -"))
531        else:
532            matches = {}
533            markers = set()
534            decoded = base64.b64decode(signature.split(':')[-1])
535            for i in xrange(0, len(decoded), 2):
536                part = struct.unpack(">H", decoded[i: i + 2])[0]
537                markers.add(part)
538
539            for candidate in SIGNATURES:
540                counter_y, counter_n = 0, 0
541                decoded = base64.b64decode(candidate.split(':')[-1])
542                for i in xrange(0, len(decoded), 2):
543                    part = struct.unpack(">H", decoded[i: i + 2])[0]
544                    if part in markers:
545                        counter_y += 1
546                    elif any(_ in markers for _ in (part & ~1, part | 1)):
547                        counter_n += 1
548                result = int(round(100 * counter_y / (counter_y + counter_n)))
549                if SIGNATURES[candidate] in matches:
550                    if result > matches[SIGNATURES[candidate]]:
551                        matches[SIGNATURES[candidate]] = result
552                else:
553                    matches[SIGNATURES[candidate]] = result
554
555            if chained:
556                for _ in list(matches.keys()):
557                    if matches[_] < 90:
558                        del matches[_]
559
560            if not matches:
561                print(colorize("[-] blind match: - "))
562                print(colorize("[!] probably chained web protection systems"))
563            else:
564                matches = [(_[1], _[0]) for _ in matches.items()]
565                matches.sort(reverse=True)
566
567                print(colorize("[+] blind match: %s" % ", ".join("'%s' (%d%%)" % (format_name(matches[i][1]), matches[i][0]) for i in xrange(min(len(matches), MAX_MATCHES) if matches[0][0] != 100 else 1))))
568
569    print()
570
571def main():
572    if "--version" not in sys.argv:
573        print(BANNER)
574
575    parse_args()
576    init()
577    run()
578
579load_data()
580
581if __name__ == "__main__":
582    try:
583        main()
584    except KeyboardInterrupt:
585        exit(colorize("\r[x] Ctrl-C pressed"))
586