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