class WafW00F(waftoolsengine): AdminFolder = '/Admin_Files/' xssstring = '<script>alert(1)</script>' dirtravstring = '../../../../etc/passwd' cleanhtmlstring = '<invalid>hello' def __init__(self, target='www.microsoft.com', port=80, ssl=False, debuglevel=0, path='/', followredirect=True, extraheaders={}, proxy=False): """ target: the hostname or ip of the target server port: defaults to 80 ssl: defaults to false """ self.log = logging.getLogger('wafw00f') waftoolsengine.__init__(self, target, port, ssl, debuglevel, path, followredirect, extraheaders, proxy) self.knowledge = dict(generic=dict(found=False, reason=''), wafname=list()) def normalrequest(self, usecache=True, cacheresponse=True, headers=None): return self.request(usecache=usecache, cacheresponse=cacheresponse, headers=headers) def normalnonexistentfile(self, usecache=True, cacheresponse=True): path = self.path + str(random.randrange(1000, 9999)) + '.html' return self.request(path=path, usecache=usecache, cacheresponse=cacheresponse) def unknownmethod(self, usecache=True, cacheresponse=True): return self.request(method='OHYEA', usecache=usecache, cacheresponse=cacheresponse) def directorytraversal(self, usecache=True, cacheresponse=True): return self.request(path=self.path + self.dirtravstring, usecache=usecache, cacheresponse=cacheresponse) def invalidhost(self, usecache=True, cacheresponse=True): randomnumber = random.randrange(100000, 999999) return self.request(headers={'Host': str(randomnumber)}) def cleanhtmlencoded(self, usecache=True, cacheresponse=True): string = self.path + quote(self.cleanhtmlstring) + '.html' return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) def cleanhtml(self, usecache=True, cacheresponse=True): string = self.path + '?htmli=' + self.cleanhtmlstring return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) def xssstandard(self, usecache=True, cacheresponse=True): xssstringa = self.path + '?xss=' + self.xssstring return self.request(path=xssstringa, usecache=usecache, cacheresponse=cacheresponse) def protectedfolder(self, usecache=True, cacheresponse=True): pfstring = self.path + self.AdminFolder return self.request(path=pfstring, usecache=usecache, cacheresponse=cacheresponse) def xssstandardencoded(self, usecache=True, cacheresponse=True): xssstringb = self.path + quote(self.xssstring) + '.html' return self.request(path=xssstringb, usecache=usecache, cacheresponse=cacheresponse) attacks = [ xssstandard, directorytraversal, protectedfolder, xssstandardencoded ] def genericdetect(self, usecache=True, cacheresponse=True): knownflops = [ ('Microsoft-IIS/7.0', 'Microsoft-HTTPAPI/2.0'), ] reason = '' reasons = [ 'Blocking is being done at connection/packet level.', 'The server header is different when an attack is detected.', 'The server returned a different response code when a string trigged the blacklist.', 'It closed the connection for a normal request.', 'The connection header was scrambled.' ] # test if response for a path containing html tags with known evil strings # gives a different response from another containing invalid html tags try: cleanresponse, _tmp = self._perform_and_check(self.cleanhtml) xssresponse, _tmp = self._perform_and_check(self.xssstandard) if xssresponse.status != cleanresponse.status: self.log.info( 'Server returned a different response when a script tag was tried' ) reason = reasons[2] reason += '\r\n' reason += 'Normal response code is "%s",' % cleanresponse.status reason += ' while the response code to an attack is "%s"' % xssresponse.status self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True cleanresponse, _tmp = self._perform_and_check( self.cleanhtmlencoded) xssresponse, _tmp = self._perform_and_check( self.xssstandardencoded) if xssresponse.status != cleanresponse.status: self.log.info( 'Server returned a different response when a script tag was tried' ) reason = reasons[2] reason += '\r\n' reason += 'Normal response code is "%s",' % cleanresponse.status reason += ' while the response code to an attack is "%s"' % xssresponse.status self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True response, _ = self._perform_and_check(self.normalrequest) normalserver = response.getheader('Server') for attack in self.attacks: response, _ = self._perform_and_check(lambda: attack(self)) attackresponse_server = response.getheader('Server') if attackresponse_server: if attackresponse_server != normalserver: if (normalserver, attackresponse_server) in knownflops: return False self.log.info( 'Server header changed, WAF possibly detected') self.log.debug('attack response: %s' % attackresponse_server) self.log.debug('normal response: %s' % normalserver) reason = reasons[1] reason += '\r\nThe server header for a normal response is "%s",' % normalserver reason += ' while the server header a response to an attack is "%s",' % attackresponse_server self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True for attack in wafdetectionsprio: if self.wafdetections[attack](self) is None: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True for attack in self.attacks: response, _ = self._perform_and_check(lambda: attack(self)) for h, _ in response.getheaders(): if scrambledheader(h): self.knowledge['generic']['reason'] = reasons[4] self.knowledge['generic']['found'] = True return True except RequestBlocked: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True return False def _perform_and_check(self, request_method): r = request_method() if r is None: raise RequestBlocked() return r def matchheader(self, headermatch, attack=False, ignorecase=True): import re detected = False header, match = headermatch if attack: requests = self.attacks else: requests = [self.normalrequest] for request in requests: r = request(self) if r is None: return response, _ = r headerval = response.getheader(header) if headerval: # set-cookie can have multiple headers, python gives it to us # concatinated with a comma if header == 'set-cookie': headervals = headerval.split(', ') else: headervals = [headerval] for headerval in headervals: if ignorecase: if re.search(match, headerval, re.IGNORECASE): detected = True break else: if re.search(match, headerval): detected = True break if detected: break return detected def matchcookie(self, match): """ a convenience function which calls matchheader """ return self.matchheader(('set-cookie', match)) wafdetections = dict() plugin_dict = load_plugins() result_dict = {} for plugin_module in plugin_dict.values(): wafdetections[plugin_module.NAME] = plugin_module.is_waf def identwaf(self, findall=False): detected = list() # Check for prioritized ones first, then check those added externally checklist = wafdetectionsprio checklist += list(set(self.wafdetections.keys()) - set(checklist)) for wafvendor in checklist: self.log.info('Checking for %s' % wafvendor) if self.wafdetections[wafvendor](self): detected.append(wafvendor) if not findall: break self.knowledge['wafname'] = detected return detected
class WAFW00F(waftoolsengine): xsstring = '<script>alert("XSS");</script>' sqlistring = "UNION SELECT ALL FROM information_schema AND ' or SLEEP(5) or '" lfistring = '../../../../etc/passwd' rcestring = '/bin/cat /etc/passwd; ping 127.0.0.1; curl google.com' xxestring = '<!ENTITY xxe SYSTEM "file:///etc/shadow">]><pwn>&hack;</pwn>' def __init__(self, target='www.example.com', debuglevel=0, path='/', followredirect=True, extraheaders={}, proxies=None): self.log = logging.getLogger('wafw00f') self.attackres = None waftoolsengine.__init__(self, target, debuglevel, path, proxies, followredirect, extraheaders) self.knowledge = dict(generic=dict(found=False, reason=''), wafname=list()) def normalRequest(self): return self.Request() def customRequest(self, headers=None): return self.Request(headers=headers) def nonExistent(self): return self.Request(path=self.path + str(random.randrange(100, 999)) + '.html') def xssAttack(self): return self.Request(path=self.path, params={'s': self.xsstring}) def xxeAttack(self): return self.Request(path=self.path, params={'s': self.xxestring}) def lfiAttack(self): return self.Request(path=self.path + self.lfistring) def centralAttack(self): return self.Request(path=self.path, params={'a': self.xsstring, 'b': self.sqlistring, 'c': self.lfistring}) def sqliAttack(self): return self.Request(path=self.path, params={'s': self.sqlistring}) def oscAttack(self): return self.Request(path=self.path, params={'s': self.rcestring}) def performCheck(self, request_method): r = request_method() if r is None: raise RequestBlocked() return r # Most common attacks used to detect WAFs attcom = [xssAttack, sqliAttack, lfiAttack] attacks = [xssAttack, xxeAttack, lfiAttack, sqliAttack, oscAttack] def genericdetect(self): reason = '' reasons = ['Blocking is being done at connection/packet level.', 'The server header is different when an attack is detected.', 'The server returns a different response code when an attack string is used.', 'It closed the connection for a normal request.', 'The response was different when the request wasn\'t made from a browser.' ] try: # Testing for no user-agent response. Detects almost all WAFs out there. resp1 = self.performCheck(self.normalRequest) if 'User-Agent' in self.headers: del self.headers['User-Agent'] # Deleting the user-agent key from object not dict. resp3 = self.customRequest(headers=def_headers) if resp1.status_code != resp3.status_code: self.log.info( 'Server returned a different response when request didn\'t contain the User-Agent header.') reason = reasons[4] reason += '\r\n' reason += 'Normal response code is "%s",' % resp1.status_code reason += ' while the response code to a modified request is "%s"' % resp3.status_code self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True # Testing the status code upon sending a xss attack resp2 = self.performCheck(self.xssAttack) if resp1.status_code != resp2.status_code: self.log.info('Server returned a different response when a XSS attack vector was tried.') reason = reasons[2] reason += '\r\n' reason += 'Normal response code is "%s",' % resp1.status_code reason += ' while the response code to cross-site scripting attack is "%s"' % resp2.status_code self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True # Testing the status code upon sending a lfi attack resp2 = self.performCheck(self.lfiAttack) if resp1.status_code != resp2.status_code: self.log.info('Server returned a different response when a directory traversal was attempted.') reason = reasons[2] reason += '\r\n' reason += 'Normal response code is "%s",' % resp1.status_code reason += ' while the response code to a file inclusion attack is "%s"' % resp2.status_code self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True # Testing the status code upon sending a sqli attack resp2 = self.performCheck(self.sqliAttack) if resp1.status_code != resp2.status_code: self.log.info('Server returned a different response when a SQLi was attempted.') reason = reasons[2] reason += '\r\n' reason += 'Normal response code is "%s",' % resp1.status_code reason += ' while the response code to a SQL injection attack is "%s"' % resp2.status_code self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True # Checking for the Server header after sending malicious requests response = self.attackres normalserver = resp1.headers.get('Server') attackresponse_server = response.headers.get('Server') if attackresponse_server: if attackresponse_server != normalserver: self.log.info('Server header changed, WAF possibly detected') self.log.debug('Attack response: %s' % attackresponse_server) self.log.debug('Normal response: %s' % normalserver) reason = reasons[1] reason += '\r\nThe server header for a normal response is "%s",' % normalserver reason += ' while the server header a response to an attack is "%s",' % attackresponse_server self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True # If at all request doesn't go, press F except RequestBlocked: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True return False def matchHeader(self, headermatch, attack=False): if attack: r = self.attackres else: r = rq if r is None: return header, match = headermatch headerval = r.headers.get(header) if headerval: # set-cookie can have multiple headers, python gives it to us # concatinated with a comma if header == 'Set-Cookie': headervals = headerval.split(', ') else: headervals = [headerval] for headerval in headervals: if re.search(match, headerval, re.I): return True return False def matchStatus(self, statuscode, attack=True): if attack: r = self.attackres else: r = rq if r is None: return if r.status_code == statuscode: return True return False def matchCookie(self, match, attack=False): return self.matchHeader(('Set-Cookie', match), attack=attack) def matchReason(self, reasoncode, attack=True): if attack: r = self.attackres else: r = rq if r is None: return # We may need to match multiline context in response body if str(r.reason) == reasoncode: return True return False def matchContent(self, regex, attack=True): if attack: r = self.attackres else: r = rq if r is None: return # We may need to match multiline context in response body if re.search(regex, r.text, re.I): return True return False wafdetections = dict() plugin_dict = load_plugins() result_dict = {} for plugin_module in plugin_dict.values(): wafdetections[plugin_module.NAME] = plugin_module.is_waf # Check for prioritized ones first, then check those added externally checklist = wafdetectionsprio checklist += list(set(wafdetections.keys()) - set(checklist)) def identwaf(self, findall=False): detected = list() try: self.attackres = self.performCheck(self.centralAttack) except RequestBlocked: return detected for wafvendor in self.checklist: self.log.info('Checking for %s' % wafvendor) if self.wafdetections[wafvendor](self): detected.append(wafvendor) if not findall: break self.knowledge['wafname'] = detected return detected
class WafW00F(waftoolsengine): """ WAF detection tool """ AdminFolder = '/Admin_Files/' xssstring = '<script>alert(1)</script>' dirtravstring = '../../../../etc/passwd' cleanhtmlstring = '<invalid>hello' isaservermatch = [ 'Forbidden ( The server denied the specified Uniform Resource Locator (URL). Contact the server administrator. )', 'Forbidden ( The ISA Server denied the specified Uniform Resource Locator (URL)' ] def __init__(self, target='www.microsoft.com', port=80, ssl=False, debuglevel=0, path='/', followredirect=True, extraheaders={}, proxy=False): """ target: the hostname or ip of the target server port: defaults to 80 ssl: defaults to false """ self.log = logging.getLogger('wafw00f') waftoolsengine.__init__(self, target, port, ssl, debuglevel, path, followredirect, extraheaders, proxy) self.knowledge = dict(generic=dict(found=False, reason=''), wafname=list()) def normalrequest(self, usecache=True, cacheresponse=True, headers=None): return self.request(usecache=usecache, cacheresponse=cacheresponse, headers=headers) def normalnonexistentfile(self, usecache=True, cacheresponse=True): path = self.path + str(random.randrange(1000, 9999)) + '.html' return self.request(path=path, usecache=usecache, cacheresponse=cacheresponse) def unknownmethod(self, usecache=True, cacheresponse=True): return self.request(method='OHYEA', usecache=usecache, cacheresponse=cacheresponse) def directorytraversal(self, usecache=True, cacheresponse=True): return self.request(path=self.path + self.dirtravstring, usecache=usecache, cacheresponse=cacheresponse) def invalidhost(self, usecache=True, cacheresponse=True): randomnumber = random.randrange(100000, 999999) return self.request(headers={'Host': str(randomnumber)}) def cleanhtmlencoded(self, usecache=True, cacheresponse=True): string = self.path + quote(self.cleanhtmlstring) + '.html' return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) def cleanhtml(self, usecache=True, cacheresponse=True): string = self.path + self.cleanhtmlstring + '.html' return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) def xssstandard(self, usecache=True, cacheresponse=True): xssstringa = self.path + self.xssstring + '.html' return self.request(path=xssstringa, usecache=usecache, cacheresponse=cacheresponse) def protectedfolder(self, usecache=True, cacheresponse=True): pfstring = self.path + self.AdminFolder return self.request(path=pfstring, usecache=usecache, cacheresponse=cacheresponse) def xssstandardencoded(self, usecache=True, cacheresponse=True): xssstringa = self.path + quote(self.xssstring) + '.html' return self.request(path=xssstringa, usecache=usecache, cacheresponse=cacheresponse) def cmddotexe(self, usecache=True, cacheresponse=True): # thanks j0e string = self.path + 'cmd.exe' return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) attacks = [ cmddotexe, directorytraversal, xssstandard, protectedfolder, xssstandardencoded ] def genericdetect(self, usecache=True, cacheresponse=True): knownflops = [ ('Microsoft-IIS/7.0', 'Microsoft-HTTPAPI/2.0'), ] reason = '' reasons = [ 'Blocking is being done at connection/packet level.', 'The server header is different when an attack is detected.', 'The server returned a different response code when a string trigged the blacklist.', 'It closed the connection for a normal request.', 'The connection header was scrambled.' ] # test if response for a path containing html tags with known evil strings # gives a different response from another containing invalid html tags r = self.cleanhtml() if r is None: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True cleanresponse, _tmp = r r = self.xssstandard() if r is None: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True xssresponse, _tmp = r if xssresponse.status != cleanresponse.status: self.log.info( 'Server returned a different response when a script tag was tried' ) reason = reasons[2] reason += '\r\n' reason += 'Normal response code is "%s",' % cleanresponse.status reason += ' while the response code to an attack is "%s"' % xssresponse.status self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True r = self.cleanhtmlencoded() cleanresponse, _tmp = r r = self.xssstandardencoded() if r is None: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True xssresponse, _tmp = r if xssresponse.status != cleanresponse.status: self.log.info( 'Server returned a different response when a script tag was tried' ) reason = reasons[2] reason += '\r\n' reason += 'Normal response code is "%s",' % cleanresponse.status reason += ' while the response code to an attack is "%s"' % xssresponse.status self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True response, responsebody = self.normalrequest() normalserver = response.getheader('Server') for attack in self.attacks: r = attack(self) if r is None: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True response, responsebody = r attackresponse_server = response.getheader('Server') if attackresponse_server: if attackresponse_server != normalserver: if (normalserver, attackresponse_server) in knownflops: return False self.log.info( 'Server header changed, WAF possibly detected') self.log.debug('attack response: %s' % attackresponse_server) self.log.debug('normal response: %s' % normalserver) reason = reasons[1] reason += '\r\nThe server header for a normal response is "%s",' % normalserver reason += ' while the server header a response to an attack is "%s.",' % attackresponse_server self.knowledge['generic']['reason'] = reason self.knowledge['generic']['found'] = True return True for attack in self.wafdetectionsprio: if self.wafdetections[attack](self) is None: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True for attack in self.attacks: r = attack(self) if r is None: self.knowledge['generic']['reason'] = reasons[0] self.knowledge['generic']['found'] = True return True response, responsebody = r for h, v in response.getheaders(): if scrambledheader(h): self.knowledge['generic']['reason'] = reasons[4] self.knowledge['generic']['found'] = True return True return False def matchheader(self, headermatch, attack=False, ignorecase=True): import re detected = False header, match = headermatch if attack: requests = self.attacks else: requests = [self.normalrequest] for request in requests: r = request(self) if r is None: return response, responsebody = r headerval = response.getheader(header) if headerval: # set-cookie can have multiple headers, python gives it to us # concatinated with a comma if header == 'set-cookie': headervals = headerval.split(', ') else: headervals = [headerval] for headerval in headervals: if ignorecase: if re.match(match, headerval, re.IGNORECASE): detected = True break else: if re.match(match, headerval): detected = True break if detected: break return detected def matchcookie(self, match): """ a convenience function which calls matchheader """ return self.matchheader(('set-cookie', match)) def isbeeware(self): # disabled cause it was giving way too many false positives # credit goes to Sebastien Gioria detected = False r = self.xssstandard() if r is None: return response, responsebody = r if (response.status != 200) or (response.reason == 'Forbidden'): r = self.directorytraversal() if r is None: return response, responsebody = r if response.status == 403: if response.reason == 'Forbidden': detected = True return detected def ismodsecuritypositive(self): detected = False self.normalrequest(usecache=False, cacheresponse=False) randomfn = self.path + str(random.randrange(1000, 9999)) + '.html' r = self.request(path=randomfn) if r is None: return response, responsebody = r if response.status != 302: return False randomfnnull = randomfn + '%00' r = self.request(path=randomfnnull) if r is None: return response, responsebody = r if response.status == 404: detected = True return detected wafdetections = dict() # easy ones # lil bit more complex #wafdetections['BeeWare'] = isbeeware #wafdetections['ModSecurity (positive model)'] = ismodsecuritypositive removed for now wafdetectionsprio = [ 'Profense', 'NetContinuum', 'Incapsula WAF', 'CloudFlare', 'NSFocus', 'Safedog', 'Mission Control Application Shield', 'USP Secure Entry Server', 'Cisco ACE XML Gateway', 'Barracuda Application Firewall', 'Art of Defence HyperGuard', 'BinarySec', 'Teros WAF', 'F5 BIG-IP LTM', 'F5 BIG-IP APM', 'F5 BIG-IP ASM', 'F5 FirePass', 'F5 Trafficshield', 'InfoGuard Airlock', 'Citrix NetScaler', 'Trustwave ModSecurity', 'IBM Web Application Security', 'IBM DataPower', 'DenyALL WAF', 'Applicure dotDefender', 'Juniper WebApp Secure', # removed for now 'ModSecurity (positive model)', 'Microsoft URLScan', 'Aqtronix WebKnight', 'eEye Digital Security SecureIIS', 'Imperva SecureSphere', 'Microsoft ISA Server' ] plugin_dict = load_plugins() result_dict = {} for plugin_module in plugin_dict.values(): wafdetections[plugin_module.NAME] = plugin_module.is_waf def identwaf(self, findall=False): detected = list() # Check for prioritized ones first, then check those added externally checklist = self.wafdetectionsprio checklist = checklist + list( set(self.wafdetections.keys()) - set(checklist)) for wafvendor in checklist: self.log.info('Checking for %s' % wafvendor) if self.wafdetections[wafvendor](self): detected.append(wafvendor) if not findall: break self.knowledge['wafname'] = detected return detected