wafw00f-0.9.4/0000775000175000017500000000000012672245714012426 5ustar kunkun00000000000000wafw00f-0.9.4/setup.cfg0000664000175000017500000000007312672245714014247 0ustar kunkun00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 wafw00f-0.9.4/LICENSE0000664000175000017500000000301412672245364013432 0ustar kunkun00000000000000Copyright (c) 2016, Sandro Gauci, Enable Security ltd and Wendel G. Henrique - Trustwave 2009 All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.wafw00f-0.9.4/setup.py0000664000175000017500000000165412672245364014147 0ustar kunkun00000000000000#!/usr/bin/env python from setuptools import setup, find_packages setup( name='wafw00f', version=__import__('wafw00f').__version__, description=('WAFW00F identifies and fingerprints ' 'Web Application Firewall (WAF) products.'), author='sandrogauci', author_email='sandro@enablesecurity.com', license='BSD License', url='https://github.com/sandrogauci/wafw00f', packages=find_packages(), scripts=['wafw00f/bin/wafw00f'], install_requires=[ 'beautifulsoup4==4.4.1', 'pluginbase==0.3', ], extras_require={ 'dev': [ 'prospector==0.10.1', ], 'test': [ 'httpretty==0.8.10', 'coverage==3.7.1', 'coveralls==0.5', 'python-coveralls==2.5.0', 'nose==1.3.6', ], 'docs': [ 'Sphinx==1.3.1', ], }, test_suite='nose.collector', ) wafw00f-0.9.4/MANIFEST.in0000664000175000017500000000020212672245364014157 0ustar kunkun00000000000000include CREDITS.txt include LICENSE include MANIFEST.in include README.md include wafw00f/__init__.py include wafw00f/bin/wafw00f wafw00f-0.9.4/PKG-INFO0000664000175000017500000000046312672245714013526 0ustar kunkun00000000000000Metadata-Version: 1.0 Name: wafw00f Version: 0.9.4 Summary: WAFW00F identifies and fingerprints Web Application Firewall (WAF) products. Home-page: https://github.com/sandrogauci/wafw00f Author: sandrogauci Author-email: sandro@enablesecurity.com License: BSD License Description: UNKNOWN Platform: UNKNOWN wafw00f-0.9.4/wafw00f.egg-info/0000775000175000017500000000000012672245714015372 5ustar kunkun00000000000000wafw00f-0.9.4/wafw00f.egg-info/top_level.txt0000664000175000017500000000001012672245714020113 0ustar kunkun00000000000000wafw00f wafw00f-0.9.4/wafw00f.egg-info/SOURCES.txt0000664000175000017500000000270212672245714017257 0ustar kunkun00000000000000CREDITS.txt LICENSE MANIFEST.in README.md setup.py wafw00f/__init__.py wafw00f/main.py wafw00f/manager.py wafw00f.egg-info/PKG-INFO wafw00f.egg-info/SOURCES.txt wafw00f.egg-info/dependency_links.txt wafw00f.egg-info/requires.txt wafw00f.egg-info/top_level.txt wafw00f/bin/wafw00f wafw00f/lib/__init__.py wafw00f/lib/evillib.py wafw00f/lib/proxy.py wafw00f/plugins/__init__.py wafw00f/plugins/airlock.py wafw00f/plugins/anquanbao.py wafw00f/plugins/barracuda.py wafw00f/plugins/binarysec.py wafw00f/plugins/chinacache.py wafw00f/plugins/ciscoacexml.py wafw00f/plugins/cloudflare.py wafw00f/plugins/denyall.py wafw00f/plugins/dotdefender.py wafw00f/plugins/f5bigipapm.py wafw00f/plugins/f5bigipasm.py wafw00f/plugins/f5bigipltm.py wafw00f/plugins/f5firepass.py wafw00f/plugins/f5trafficshield.py wafw00f/plugins/hyperguard.py wafw00f/plugins/ibm.py wafw00f/plugins/ibmdatapower.py wafw00f/plugins/imperva.py wafw00f/plugins/incapsula.py wafw00f/plugins/isaserver.py wafw00f/plugins/missioncontrol.py wafw00f/plugins/modsecurity.py wafw00f/plugins/netcontinuum.py wafw00f/plugins/netscaler.py wafw00f/plugins/nsfocus.py wafw00f/plugins/powercdn.py wafw00f/plugins/profense.py wafw00f/plugins/safedog.py wafw00f/plugins/secureiis.py wafw00f/plugins/teros.py wafw00f/plugins/urlscan.py wafw00f/plugins/uspses.py wafw00f/plugins/webknight.py wafw00f/plugins/webscurity.py wafw00f/plugins/west263cdn.py wafw00f/plugins/wzb360.py wafw00f/tests/__init__.py wafw00f/tests/test_main.pywafw00f-0.9.4/wafw00f.egg-info/dependency_links.txt0000664000175000017500000000000112672245714021440 0ustar kunkun00000000000000 wafw00f-0.9.4/wafw00f.egg-info/PKG-INFO0000664000175000017500000000046312672245714016472 0ustar kunkun00000000000000Metadata-Version: 1.0 Name: wafw00f Version: 0.9.4 Summary: WAFW00F identifies and fingerprints Web Application Firewall (WAF) products. Home-page: https://github.com/sandrogauci/wafw00f Author: sandrogauci Author-email: sandro@enablesecurity.com License: BSD License Description: UNKNOWN Platform: UNKNOWN wafw00f-0.9.4/wafw00f.egg-info/requires.txt0000664000175000017500000000026212672245714017772 0ustar kunkun00000000000000beautifulsoup4==4.4.1 pluginbase==0.3 [test] httpretty==0.8.10 coverage==3.7.1 coveralls==0.5 python-coveralls==2.5.0 nose==1.3.6 [docs] Sphinx==1.3.1 [dev] prospector==0.10.1wafw00f-0.9.4/README.md0000664000175000017500000000576712672245364013725 0ustar kunkun00000000000000# WAFW00F WAFW00F identifies and fingerprints Web Application Firewall (WAF) products. ## How does it work? To do its magic, WAFW00F does the following: - Sends a _normal_ HTTP request and analyses the response; this identifies a number of WAF solutions - If that is not successful, it sends a number of (potentially malicious) HTTP requests and uses simple logic to deduce which WAF it is - If that is also not successful, it analyses the responses previously returned and uses another simple algorithm to guess if a WAF or security solution is actively responding to our attacks For further details, check out the source code on the main site, [github.com/sandrogauci/wafw00f](https://github.com/sandrogauci/wafw00f). ## What does it detect? It detects a number of WAFs. To view which WAFs it is able to detect run WAFW00F with the `-l` option. At the time of writing the output is as follows: $ ./wafw00f -l ^ ^ _ __ _ ____ _ __ _ _ ____ ///7/ /.' \ / __////7/ /,' \ ,' \ / __/ | V V // o // _/ | V V // 0 // 0 // _/ |_n_,'/_n_//_/ |_n_,' \_,' \_,'/_/ < ...' WAFW00F - Web Application Firewall Detection Tool By Sandro Gauci && Wendel G. Henrique Can test for these WAFs: Profense NetContinuum Incapsula WAF CloudFlare NSFocus 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 Microsoft URLScan Aqtronix WebKnight eEye Digital Security SecureIIS Imperva SecureSphere Microsoft ISA Server ## How do I use it? For help please make use of the `--help` option. The basic usage is to pass it a URL as an argument. Example: $./wafw00f https://www.ibm.com/ ^ ^ _ __ _ ____ _ __ _ _ ____ ///7/ /.' \ / __////7/ /,' \ ,' \ / __/ | V V // o // _/ | V V // 0 // 0 // _/ |_n_,'/_n_//_/ |_n_,' \_,' \_,'/_/ < ...' WAFW00F - Web Application Firewall Detection Tool By Sandro Gauci && Wendel G. Henrique Checking https://www.ibm.com/ The site https://www.ibm.com/ is behind a Citrix NetScaler Number of requests: 6 ## How do I install it? The following should do the trick: python setup.py install or pip install wafw00f ## Need a freelance pentester? More information about the services that I offer at [Enable Security](http://enablesecurity.com/) ## Questions? Contact [me](mailto:sandro@enablesecurity.com) wafw00f-0.9.4/wafw00f/0000775000175000017500000000000012672245714013700 5ustar kunkun00000000000000wafw00f-0.9.4/wafw00f/bin/0000775000175000017500000000000012672245714014450 5ustar kunkun00000000000000wafw00f-0.9.4/wafw00f/bin/wafw00f0000775000175000017500000000013612672245364015651 0ustar kunkun00000000000000#!/usr/bin/env python from wafw00f import main if __name__ == '__main__': main.main() wafw00f-0.9.4/wafw00f/main.py0000775000175000017500000004757412672245364015223 0ustar kunkun00000000000000#!/usr/bin/env python # wafw00f - Web Application Firewall Detection Tool # by Sandro Gauci - enablesecurity.com (c) 2016 # and Wendel G. Henrique - Trustwave 2009 __license__ = """ Copyright (c) 2016, {Sandro Gauci|Wendel G. Henrique} All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of EnableSecurity or Trustwave nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import os try: import httplib except ImportError: import http.client as httplib try: from urllib import quote, unquote except ImportError: from urllib.parse import quote, unquote from optparse import OptionParser import logging import sys import random currentDir = os.getcwd() scriptDir = os.path.dirname(sys.argv[0]) or '.' os.chdir(scriptDir) from wafw00f import __version__ from wafw00f.lib.evillib import oururlparse, scrambledheader, waftoolsengine from wafw00f.manager import load_plugins lackofart = """ ^ ^ _ __ _ ____ _ __ _ _ ____ ///7/ /.' \ / __////7/ /,' \ ,' \ / __/ | V V // o // _/ | V V // 0 // 0 // _/ |_n_,'/_n_//_/ |_n_,' \_,' \_,'/_/ < ...' WAFW00F - Web Application Firewall Detection Tool By Sandro Gauci && Wendel G. Henrique """ class WafW00F(waftoolsengine): """ WAF detection tool """ AdminFolder = '/Admin_Files/' xssstring = '' dirtravstring = '../../../../etc/passwd' cleanhtmlstring = '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() for wafvendor in self.wafdetectionsprio: 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 def calclogginglevel(verbosity): default = 40 # errors are printed out level = default - (verbosity * 10) if level < 0: level = 0 return level def getheaders(fn): headers = {} fullfn = os.path.abspath(os.path.join(os.getcwd(),fn)) if not os.path.exists(fullfn): logging.getLogger('wafw00f').critical('Headers file "%s" does not exist!'%fullfn) return with open(fn,'r') as f: for line in f.readlines(): _t = line.split(':',2) if len(_t) == 2: h,v = map(lambda x: x.strip(),_t) headers[h] = v return headers def main(): print(lackofart) parser = OptionParser(usage='%prog url1 [url2 [url3 ... ]]\r\nexample: %prog http://www.victim.org/') parser.add_option('-v', '--verbose', action='count', dest='verbose', default=0, help='enable verbosity - multiple -v options increase verbosity') parser.add_option('-a', '--findall', action='store_true', dest='findall', default=False, help='Find all WAFs, do not stop testing on the first one') parser.add_option('-r', '--disableredirect', action='store_false', dest='followredirect', default=True, help='Do not follow redirections given by 3xx responses') parser.add_option('-t', '--test', dest='test', help='Test for one specific WAF') parser.add_option('-l', '--list', dest='list', action='store_true', default=False, help='List all WAFs that we are able to detect') parser.add_option('-p', '--proxy', dest='proxy', default=False, help='Use an HTTP proxy to perform requests, example: http://hostname:8080, socks5://hostname:1080') parser.add_option('--version', '-V', dest='version', action='store_true', default=False, help='Print out the version') parser.add_option('--headersfile', '-H', dest='headersfile', action='store', default=None, help='Pass custom headers, for example to overwrite the default User-Agent string') options, args = parser.parse_args() logging.basicConfig(level=calclogginglevel(options.verbose)) log = logging.getLogger() if options.list: print('Can test for these WAFs:\r\n') attacker = WafW00F(None) print('\r\n'.join(attacker.wafdetectionsprio)) return if options.version: print('WAFW00F version %s' % __version__) return extraheaders = {} if options.headersfile: log.info('Getting extra headers from %s' % options.headersfile) extraheaders = getheaders(options.headersfile) if extraheaders is None: parser.error('Please provide a headers file with colon delimited header names and values') if len(args) == 0: parser.error('we need a target site') targets = args for target in targets: if not (target.startswith('http://') or target.startswith('https://')): log.info('The url %s should start with http:// or https:// .. fixing (might make this unusable)' % target) target = 'http://' + target print('Checking %s' % target) pret = oururlparse(target) if pret is None: log.critical('The url %s is not well formed' % target) sys.exit(1) (hostname, port, path, query, ssl) = pret log.info('starting wafw00f on %s' % target) attacker = WafW00F(hostname, port=port, ssl=ssl, debuglevel=options.verbose, path=path, followredirect=options.followredirect, extraheaders=extraheaders, proxy=options.proxy) if attacker.normalrequest() is None: log.error('Site %s appears to be down' % target) sys.exit(1) if options.test: if options.test in attacker.wafdetections: waf = attacker.wafdetections[options.test](attacker) if waf: print('The site %s is behind a %s' % (target, options.test)) else: print('WAF %s was not detected on %s' % (options.test, target)) else: print( 'WAF %s was not found in our list\r\nUse the --list option to see what is available' % options.test) return waf = attacker.identwaf(options.findall) log.info('Ident WAF: %s' % waf) if len(waf) > 0: print('The site %s is behind a %s' % (target, ' and/or '.join(waf))) if (options.findall) or len(waf) == 0: print('Generic Detection results:') if attacker.genericdetect(): log.info('Generic Detection: %s' % attacker.knowledge['generic']['reason']) print('The site %s seems to be behind a WAF or some sort of security solution' % target) print('Reason: %s' % attacker.knowledge['generic']['reason']) else: print('No WAF detected by the generic detection') print('Number of requests: %s' % attacker.requestnumber) if __name__ == '__main__': if sys.hexversion < 0x2060000: sys.stderr.write('Your version of python is way too old .. please update to 2.6 or later\r\n') main() wafw00f-0.9.4/wafw00f/plugins/0000775000175000017500000000000012672245714015361 5ustar kunkun00000000000000wafw00f-0.9.4/wafw00f/plugins/west263cdn.py0000664000175000017500000000017212672245364017636 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'West263CDN' def is_waf(self): return self.matchheader(('X-Cache', '.+WT263CDN-.+')) wafw00f-0.9.4/wafw00f/plugins/webscurity.py0000664000175000017500000000070512672245364020136 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Juniper WebApp Secure' def is_waf(self): detected = False r = self.normalrequest() if r is None: return response, responsebody = r if response.status == 403: return detected newpath = self.path + '?nx=@@' r = self.request(path=newpath) if r is None: return response, responsebody = r if response.status == 403: detected = True return detected wafw00f-0.9.4/wafw00f/plugins/imperva.py0000664000175000017500000000070112672245364017375 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Imperva SecureSphere' def is_waf(self): # thanks to Mathieu Dessus for this # might lead to false positives so please report back to sandro@enablesecurity.com for attack in self.attacks: r = attack(self) if r is None: return response, responsebody = r if response.version == 10: return True return False wafw00f-0.9.4/wafw00f/plugins/teros.py0000664000175000017500000000020012672245364017060 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Teros WAF' def is_waf(self): # credit goes to W3AF return self.matchcookie('^st8id=') wafw00f-0.9.4/wafw00f/plugins/incapsula.py0000664000175000017500000000036012672245364017712 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Incapsula WAF' def is_waf(self): # credit goes to Charlie Campbell if self.matchcookie('^.incap_ses'): return True if self.matchcookie('^visid.*='): return True return False wafw00f-0.9.4/wafw00f/plugins/anquanbao.py0000664000175000017500000000017512672245364017704 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Anquanbao' def is_waf(self): return self.matchheader(('X-Powered-By-Anquanbao', '.+')) wafw00f-0.9.4/wafw00f/plugins/ibm.py0000664000175000017500000000030412672245364016500 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'IBM Web Application Security' def is_waf(self): detected = False r = self.protectedfolder() if r is None: detected = True return detected wafw00f-0.9.4/wafw00f/plugins/ibmdatapower.py0000664000175000017500000000041212672245364020407 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'IBM DataPower' def is_waf(self): # Added by Mathieu Dessus detected = False if self.matchheader(('X-Backside-Transport', '^(OK|FAIL)')): detected = True return detected wafw00f-0.9.4/wafw00f/plugins/wzb360.py0000664000175000017500000000017712672245364016774 0ustar kunkun00000000000000#!/usr/bin/env python NAME = '360WangZhanBao' def is_waf(self): return self.matchheader(('X-Powered-By-360WZB', '.+')) wafw00f-0.9.4/wafw00f/plugins/modsecurity.py0000664000175000017500000000074412672245364020310 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Trustwave ModSecurity' def is_waf(self): detected = False for attack in self.attacks: r = attack(self) if r is None: return response, responsebody = r if response.status == 501: detected = True break # the following based on nmap's http-waf-fingerprint.nse if self.matchheader(('server', '(mod_security|Mod_Security|NOYB)')): return True return detected wafw00f-0.9.4/wafw00f/plugins/cloudflare.py0000664000175000017500000000042012672245364020050 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'CloudFlare' # the following based on nmap's http-waf-fingerprint.nse def is_waf(self): if self.matchheader(('server', 'cloudflare-nginx')): return True if self.matchcookie('__cfduid'): return True return False wafw00f-0.9.4/wafw00f/plugins/netcontinuum.py0000664000175000017500000000021412672245364020461 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'NetContinuum' def is_waf(self): # credit goes to W3AF return self.matchcookie('^NCI__SessionId=') wafw00f-0.9.4/wafw00f/plugins/binarysec.py0000664000175000017500000000071612672245364017717 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'BinarySec' def is_waf(self): # credit goes to W3AF if self.matchheader(('server', 'BinarySec')): return True # the following based on nmap's http-waf-fingerprint.nse elif self.matchheader(('x-binarysec-via', '.')): return True # the following based on nmap's http-waf-fingerprint.nse elif self.matchheader(('x-binarysec-nocache', '.')): return True else: return False wafw00f-0.9.4/wafw00f/plugins/chinacache.py0000664000175000017500000000020112672245364017773 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'ChinaCache-CDN' def is_waf(self): return self.matchheader(('Powered-By-ChinaCache', '.+')) wafw00f-0.9.4/wafw00f/plugins/isaserver.py0000664000175000017500000000042512672245364017740 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Microsoft ISA Server' def is_waf(self): detected = False r = self.invalidhost() if r is None: return response, responsebody = r if response.reason in self.isaservermatch: detected = True return detected wafw00f-0.9.4/wafw00f/plugins/hyperguard.py0000664000175000017500000000022512672245364020105 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Art of Defence HyperGuard' def is_waf(self): # credit goes to W3AF return self.matchcookie('^WODSESSION=') wafw00f-0.9.4/wafw00f/plugins/barracuda.py0000664000175000017500000000073112672245364017661 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Barracuda Application Firewall' def is_waf(self): # credit goes to W3AF if self.matchcookie('^barra_counter_session='): return True # credit goes to Charlie Campbell if self.matchcookie('^BNI__BARRACUDA_LB_COOKIE='): return True # credit goes to yours truly if self.matchcookie('^BNI_persistence='): return True if self.matchcookie('^BN[IE]S_.*?='): return True return False wafw00f-0.9.4/wafw00f/plugins/airlock.py0000664000175000017500000000022312672245364017355 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'InfoGuard Airlock' def is_waf(self): # credit goes to W3AF return self.matchcookie('^AL[_-]?(SESS|LB)=') wafw00f-0.9.4/wafw00f/plugins/denyall.py0000664000175000017500000000104712672245364017366 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'DenyALL WAF' def is_waf(self): # credit goes to W3AF if self.matchcookie('^sessioncookie='): return True # credit goes to Sebastien Gioria # Tested against a Rweb 3.8 # and modified by sandro gauci and someone else for attack in self.attacks: r = attack(self) if r is None: return response, responsebody = r if response.status == 200: if response.reason == 'Condition Intercepted': return True return False wafw00f-0.9.4/wafw00f/plugins/ciscoacexml.py0000664000175000017500000000025012672245364020223 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Cisco ACE XML Gateway' def is_waf(self): if self.matchheader(('server', 'ACE XML Gateway')): return True return False wafw00f-0.9.4/wafw00f/plugins/safedog.py0000664000175000017500000000044512672245364017347 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Safedog' def is_waf(self): if self.matchcookie('^safedog-flow-item='): return True if self.matchheader(('server', '^Safedog')): return True if self.matchheader(('x-powered-by', '^WAF/\d\.\d')): return True return False wafw00f-0.9.4/wafw00f/plugins/f5bigipasm.py0000664000175000017500000000022112672245364017755 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'F5 BIG-IP ASM' def is_waf(self): # credit goes to W3AF return self.matchcookie('^TS[a-zA-Z0-9]{3,8}=') wafw00f-0.9.4/wafw00f/plugins/dotdefender.py0000664000175000017500000000025112672245364020215 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Applicure dotDefender' def is_waf(self): # thanks to j0e return self.matchheader(['X-dotDefender-denied', '^1$'], attack=True) wafw00f-0.9.4/wafw00f/plugins/missioncontrol.py0000664000175000017500000000031012672245364021010 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Mission Control Application Shield' def is_waf(self): if self.matchheader(('server', 'Mission Control Application Shield')): return True return False wafw00f-0.9.4/wafw00f/plugins/__init__.py0000664000175000017500000000000012672245364017461 0ustar kunkun00000000000000wafw00f-0.9.4/wafw00f/plugins/f5bigipltm.py0000664000175000017500000000041312672245364017774 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'F5 BIG-IP LTM' def is_waf(self): detected = False if self.matchcookie('^BIGipServer'): return True elif self.matchheader(('X-Cnection', '^close$'), attack=True): return True else: return False wafw00f-0.9.4/wafw00f/plugins/uspses.py0000664000175000017500000000025612672245364017261 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'USP Secure Entry Server' def is_waf(self): if self.matchheader(('server', 'Secure Entry Server')): return True return False wafw00f-0.9.4/wafw00f/plugins/nsfocus.py0000664000175000017500000000022212672245364017410 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'NSFocus' def is_waf(self): if self.matchheader(('server', 'NSFocus')): return True return False wafw00f-0.9.4/wafw00f/plugins/urlscan.py0000664000175000017500000000112512672245364017402 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Microsoft URLScan' def is_waf(self): detected = False testheaders = dict() testheaders['Translate'] = 'z' * 10 testheaders['If'] = 'z' * 10 testheaders['Lock-Token'] = 'z' * 10 testheaders['Transfer-Encoding'] = 'z' * 10 r = self.normalrequest() if r is None: return response, _tmp = r r = self.normalrequest(headers=testheaders) if r is None: return response2, _tmp = r if response.status != response2.status: if response2.status == 404: detected = True return detected wafw00f-0.9.4/wafw00f/plugins/profense.py0000664000175000017500000000026612672245364017561 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Profense' def is_waf(self): """ Checks for server headers containing "profense" """ return self.matchheader(('server', 'profense')) wafw00f-0.9.4/wafw00f/plugins/webknight.py0000664000175000017500000000050712672245364017720 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Aqtronix WebKnight' def is_waf(self): detected = False for attack in self.attacks: r = attack(self) if r is None: return response, responsebody = r if response.status == 999: detected = True break return detected wafw00f-0.9.4/wafw00f/plugins/f5trafficshield.py0000664000175000017500000000063012672245364020775 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'F5 Trafficshield' def is_waf(self): for hv in [['cookie', '^ASINFO='], ['server', 'F5-TrafficShield']]: r = self.matchheader(hv) if r is None: return elif r: return r # the following based on nmap's http-waf-fingerprint.nse if self.matchheader(('server', 'F5-TrafficShield')): return True return False wafw00f-0.9.4/wafw00f/plugins/powercdn.py0000664000175000017500000000015612672245364017557 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'PowerCDN' def is_waf(self): return self.matchheader(('PowerCDN', '.+')) wafw00f-0.9.4/wafw00f/plugins/f5bigipapm.py0000664000175000017500000000220112672245364017752 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'F5 BIG-IP APM' def is_waf(self): detected = False # the following based on nmap's http-waf-fingerprint.nse if self.matchcookie('^LastMRH_Session') and self.matchcookie('^MRHSession'): return True elif self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')) and self.matchcookie('^MRHSession'): return True if self.matchheader(('Location', '\/my.policy')) and self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): return True elif self.matchheader(('Location', '\/my\.logout\.php3')) and self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): return True elif self.matchheader(('Location', '.+\/f5\-w\-68747470.+')) and self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): return True elif self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): return True elif self.matchcookie('^F5_fullWT') or self.matchcookie('^F5_ST') or self.matchcookie('^F5_HT_shrinked'): return True elif self.matchcookie('^MRHSequence') or self.matchcookie('^MRHSHint') or self.matchcookie('^LastMRH_Session'): return True else: return False wafw00f-0.9.4/wafw00f/plugins/secureiis.py0000664000175000017500000000101112672245364017720 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'eEye Digital Security SecureIIS' def is_waf(self): # credit goes to W3AF detected = False r = self.normalrequest() if r is None: return response, responsebody = r if response.status == 404: return headers = dict() headers['Transfer-Encoding'] = 'z' * 1025 r = self.normalrequest(headers=headers) if r is None: return response, responsebody = r if response.status == 404: detected = True return detected wafw00f-0.9.4/wafw00f/plugins/netscaler.py0000664000175000017500000000160312672245364017714 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'Citrix NetScaler' def is_waf(self): """ First checks if a cookie associated with Netscaler is present, if not it will try to find if a "Cneonction" or "nnCoection" is returned for any of the attacks sent """ # NSC_ and citrix_ns_id come from David S. Langlands if self.matchcookie('^(ns_af=|citrix_ns_id|NSC_)'): return True if self.matchheader(('Cneonction', 'close'), attack=True): return True if self.matchheader(('nnCoection', 'close'), attack=True): return True if self.matchheader(('Via', 'NS-CACHE'), attack=True): return True if self.matchheader(('x-client-ip', '.'), attack=True): return True if self.matchheader(('Location', '\/vpn\/index\.html')): return True if self.matchcookie('^pwcount'): return True return False wafw00f-0.9.4/wafw00f/plugins/f5firepass.py0000664000175000017500000000112712672245364020004 0ustar kunkun00000000000000#!/usr/bin/env python NAME = 'F5 FirePass' def is_waf(self): detected = False if self.matchheader(('Location', '\/my\.logon\.php3')) and self.matchcookie('^VHOST'): return True elif self.matchcookie('^MRHSession') and (self.matchcookie('^VHOST') or self.matchcookie('^uRoamTestCookie')): return True elif self.matchcookie('^MRHSession') and (self.matchcookie('^MRHCId') or self.matchcookie('^MRHIntranetSession')): return True elif self.matchcookie('^uRoamTestCookie') or self.matchcookie('^VHOST'): return True else: return False wafw00f-0.9.4/wafw00f/lib/0000775000175000017500000000000012672245714014446 5ustar kunkun00000000000000wafw00f-0.9.4/wafw00f/lib/evillib.py0000775000175000017500000004013012672245364016450 0ustar kunkun00000000000000#!/usr/bin/env python import re import sys import socket try: from urlparse import urlparse, urlunparse except ImportError: from urllib.parse import urlparse, urlunparse import logging try: from bs4 import BeautifulSoup except ImportError: sys.stderr.write('You need to get BeautifulSoup installed\n') sys.stderr.write('Do it now, as privileged user/root run: pip install beautifulsoup4\now') sys.exit(1) from .proxy import NullProxy, HttpProxy, Socks5Proxy, httplib, socks __license__ = """ Copyright (c) 2016, {Sandro Gauci|Wendel G. Henrique} All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of EnableSecurity or Trustwave nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ # unicode mapping borrowed from http://packetstormsecurity.org/web/unicode-fun.txt # by Gary O'leary-Steele of Sec-1 Ltd try: from urllib import quote, unquote except ImportError: from urllib.parse import quote, unquote unicodemapping = {' ': '%u0020', '/': '%u2215', '\\': '%u2215', "'": '%u02b9', '"': '%u0022', '>': '%u003e', '<': '%u003c', '#': '%uff03', '!': '%uff01', '$': '%uff04', '*': '%uff0a', '@': '%u0040', '.': '%uff0e', '_': '%uff3f', '(': '%uff08', ')': '%uff09', ',': '%uff0c', '%': '%u0025', '-': '%uff0d', ';': '%uff1b', ':': '%uff1a', '|': '%uff5c', '&': '%uff06', '+': '%uff0b', '=': '%uff1d', 'a': '%uff41', 'A': '%uff21', 'b': '%uff42', 'B': '%uff22', 'c': '%uff43', 'C': '%uff23', 'd': '%uff44', 'D': '%uff24', 'e': '%uff45', 'E': '%uff25', 'f': '%uff46', 'F': '%uff26', 'g': '%uff47', 'G': '%uff27', 'h': '%uff48', 'H': '%uff28', 'i': '%uff49', 'I': '%uff29', 'j': '%uff4a', 'J': '%uff2a', 'k': '%uff4b', 'K': '%uff2b', 'l': '%uff4c', 'L': '%uff2c', 'm': '%uff4d', 'M': '%uff2d', 'n': '%uff4e', 'N': '%uff2e', 'o': '%uff4f', 'O': '%uff2f', 'p': '%uff50', 'P': '%uff30', 'q': '%uff51', 'Q': '%uff31', 'r': '%uff52', 'R': '%uff32', 's': '%uff53', 'S': '%uff33', 't': '%uff54', 'T': '%uff34', 'u': '%uff55', 'U': '%uff35', 'v': '%uff56', 'V': '%uff36', 'w': '%uff57', 'W': '%uff37', 'x': '%uff58', 'X': '%uff38', 'y': '%uff59', 'Y': '%uff39', 'z': '%uff5a', 'Z': '%uff3a', '0': '%uff10', '1': '%uff11', '2': '%uff12', '3': '%uff13', '4': '%uff14', '5': '%uff15', '6': '%uff16', '7': '%uff17', '8': '%uff18', '9': '%uff19'} homoglyphicmapping = {"'": '%ca%bc'} def oururlparse(target): log = logging.getLogger('urlparser') ssl = False o = urlparse(target) if o[0] not in ['http', 'https', '']: log.error('scheme %s not supported' % o[0]) return if o[0] == 'https': ssl = True if len(o[2]) > 0: path = o[2] else: path = '/' tmp = o[1].split(':') if len(tmp) > 1: port = tmp[1] else: port = None hostname = tmp[0] query = o[4] return (hostname, port, path, query, ssl) def modifyurl(path, modfunc, log): path = path log.debug('path is currently %s' % path) #s = re.search('(\[.*?\])',path) for m in re.findall('(\[.*?\])', path): ourstr = m[1:-1] newstr = modfunc(ourstr) log.debug('String was %s' % ourstr) log.debug('String became %s' % newstr) path = path.replace(m, newstr) log.debug('the path is now %s' % path) return path def modifypath(path, newstrs, log, encode=True): log.debug('path is currently %s' % path) for m in re.findall('(\[.*?\])', path): ourstr = m[1:-1] for newstr in newstrs: if encode: newstr = quote(newstr) log.debug('String was %s' % ourstr) log.debug('String became %s' % newstr) newpath = path.replace(m, newstr).replace(']', '').replace('[', '') yield (newpath) def bruteforceascii(ourstr): listourstr = list(ourstr) for pos in range(len(ourstr)): for i in range(256): newlistourstr = listourstr[:] newlistourstr[pos] = chr(i) yield (quote(''.join(newlistourstr))) def unicodeurlencode(ourstr): newstr = str() for character in ourstr: if unicodemapping.has_key(character): newstr += unicodemapping[character] else: newstr += character return newstr def nullify(ourstr): newstr = str() for character in ourstr: newstr += character + "\x00" return quote(newstr) def replacechars(ourstr, origchar, newchar): newstr = ourstr.replace(origchar, newchar) return newstr def nullifyspaces(ourstr): return quote(replacechars(ourstr, ' ', '\x00')) def slashspaces(ourstr): return replacechars(ourstr, ' ', '/') def tabifyspaces(ourstr): return replacechars(ourstr, ' ', '\t') def crlfspaces(ourstr): return replacechars(ourstr, ' ', '\n') def backslashquotes(ourstr): return replacechars(ourstr, "'", "\''") class waftoolsengine: 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.target = target if port is None: if ssl: port = 443 else: port = 80 self.port = int(port) self.ssl = ssl self.debuglevel = debuglevel self.cachedresponses = dict() self.requestnumber = 0 self.path = path self.redirectno = 0 self.followredirect = followredirect self.crawlpaths = list() self.extraheaders = extraheaders try: self.proxy = self._parse_proxy(proxy) if proxy else NullProxy() except Exception as e: self.log.critical("Proxy disabled: %s" % e) self.proxy = NullProxy() def request(self, method='GET', path=None, usecache=True, cacheresponse=True, headers=None, comingfromredir=False): followredirect = self.followredirect if comingfromredir: self.redirectno += 1 if self.redirectno >= 5: self.log.error('We received way too many redirects.. stopping that') followredirect = False else: self.redirectno = 0 if path is None: path = self.path for hdr in self.extraheaders.keys(): if headers is None: headers = {} headers[hdr] = self.extraheaders[hdr] if headers is not None: knownheaders = map(lambda x: x.lower(), headers.keys()) else: knownheaders = {} headers = {} if not 'user-agent' in knownheaders: headers[ 'User-Agent'] = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b1) Gecko/20081007 Firefox/3.0' if not 'accept-charset' in knownheaders: headers['Accept-Charset'] = 'ISO-8859-1,utf-8;q=0.7,*;q=0.7' if not 'accept' in knownheaders: headers['Accept'] = '*/*' k = str([method, path, headers]) if usecache: if k in self.cachedresponses.keys(): self.log.debug('Using cached version of %s, %s' % (method, path)) return self.cachedresponses[k] else: self.log.debug('%s not found in %s' % (k, self.cachedresponses.keys())) r = self._request(method, path, headers) if cacheresponse: self.cachedresponses[k] = r if r: response, responsebody = r if response.status in [301, 302, 307]: if followredirect: if response.getheader('location'): newloc = response.getheader('location') self.log.info('Redirected to %s' % newloc) pret = oururlparse(newloc) if pret is not None: (target, port, path, query, ssl) = pret if not port: port = 80 if target == '': target = self.target if port is None: port = self.port if not path.startswith('/'): path = '/' + path if (target, port, ssl) == (self.target, self.port, ssl): r = self.request(method, path, usecache, cacheresponse, headers, comingfromredir=True) else: self.log.warn('Tried to redirect to a different server %s' % newloc) else: self.log.warn('%s is not a well formatted url' % response.getheader('location')) return r def _request(self, method, path, headers): original_socket = socket.socket try: conn_factory, connect_host, connect_port, query_path = \ self.proxy.prepare(self.target, self.port, path, self.ssl) params = dict() if sys.hexversion > 0x2060000: params['timeout'] = 4 if (sys.hexversion >= 0x2070900) and self.ssl: import ssl as ssllib params['context'] = ssllib._create_unverified_context() h = conn_factory(connect_host, connect_port,**params) if self.debuglevel <= 10: if self.debuglevel > 1: h.set_debuglevel(self.debuglevel) try: self.log.info('Sending %s %s' % (method, path)) h.request(method, query_path, headers=headers) except socket.error: self.log.warn('Could not initialize connection to %s' % self.target) return self.requestnumber += 1 response = h.getresponse() responsebody = response.read() h.close() r = response, responsebody except (socket.error, socket.timeout, httplib.BadStatusLine): self.log.warn('Hey.. they closed our connection!') r = None finally: self.proxy.terminate() return r def querycrawler(self, path=None, curdepth=0, maxdepth=1): self.log.debug('Crawler is visiting %s' % path) localcrawlpaths = list() if curdepth > maxdepth: self.log.info('maximum depth %s reached' % maxdepth) return r = self.request(path=path) if r is None: return response, responsebody = r try: soup = BeautifulSoup(responsebody) except: self.log.warn('could not parse the response body') return tags = soup('a') for tag in tags: try: href = tag["href"] if href is not None: tmpu = urlparse(href) if (tmpu[1] != '') and (self.target != tmpu[1]): # not on the same domain name .. ignore self.log.debug('Ignoring link because it is not on the same site %s' % href) continue if tmpu[0] not in ['http', 'https', '']: self.log.debug('Ignoring link because it is not an http uri %s' % href) continue path = tmpu[2] if not path.startswith('/'): path = '/' + path if len(tmpu[4]) > 0: # found a query .. thats all we need location = urlunparse(('', '', path, tmpu[3], tmpu[4], '')) self.log.info('Found query %s' % location) return href if path not in self.crawlpaths: href = unquote(path) self.log.debug('adding %s for crawling' % href) self.crawlpaths.append(href) localcrawlpaths.append(href) except KeyError: pass for nextpath in localcrawlpaths: r = self.querycrawler(path=nextpath, curdepth=curdepth + 1, maxdepth=maxdepth) if r: return r def _parse_proxy(self, proxy): parts = urlparse(proxy) if not parts.scheme or not parts.netloc: raise Exception("Invalid proxy specified, scheme required") netloc = parts.netloc.split(":") if len(netloc) != 2: raise Exception("Proxy port unspecified") try: if parts.scheme == "socks5": if socks is None: raise Exception("socks5 proxy requires PySocks") return Socks5Proxy(netloc[0], int(netloc[1])) elif parts.scheme == "http": return HttpProxy(netloc[0], int(netloc[1])) else: raise Exception("Unsupported proxy scheme") except ValueError: raise Exception("Invalid port number") def scrambledheader(header): c = 'connection' if len(header) != len(c): return False if header == c: return False for character in c: if c.count(character) != header.count(character): return False return True wafw00f-0.9.4/wafw00f/lib/proxy.py0000664000175000017500000000346212672245364016207 0ustar kunkun00000000000000import socket try: import httplib except ImportError: import http.client as httplib try: import socks except ImportError: socks = None class NullProxy: def prepare(self, target, port, path, ssl): conn_factory = httplib.HTTPSConnection if ssl else httplib.HTTPConnection return conn_factory, target, port, path def terminate(self): pass class HttpProxy: def __init__(self, host, port): self.host = host self.port = port def prepare(self, target, port, path, ssl): conn_factory = httplib.HTTPConnection query_path = "%(scheme)s://%(host)s%(path)s" % dict( scheme="https" if ssl else "http", host=target, path=path) return conn_factory, self.host, self.port, query_path def terminate(self): pass class Socks5Proxy: def __init__(self, host, port): self.host = host self.port = port self.original = socket.socket self.original_create = socket.create_connection def prepare(self, target, port, path, ssl): def proxy_create_connection(address, timeout=4, source_address=None): return socks.create_connection(address, proxy_type=socks.PROXY_TYPE_SOCKS5, proxy_addr=self.host, proxy_port=self.port, source_address=source_address, timeout=timeout) socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, self.host, self.port) httplib.socket.socket = socks.socksocket httplib.socket.create_connection = proxy_create_connection conn_factory = httplib.HTTPSConnection if ssl else httplib.HTTPConnection return conn_factory, target, port, path def terminate(self): httplib.socket.socket = self.original httplib.socket.create_connection = self.original_create wafw00f-0.9.4/wafw00f/lib/__init__.py0000664000175000017500000000000012672245364016546 0ustar kunkun00000000000000wafw00f-0.9.4/wafw00f/manager.py0000664000175000017500000000116412672245364015667 0ustar kunkun00000000000000#!/usr/bin/env python import os from functools import partial from pluginbase import PluginBase def load_plugins(): here = os.path.abspath(os.path.dirname(__file__)) get_path = partial(os.path.join, here) plugin_dir = get_path('plugins') plugin_base = PluginBase( package='wafw00f.plugins', searchpath=[plugin_dir] ) plugin_source = plugin_base.make_plugin_source( searchpath=[plugin_dir], persist=True ) plugin_dict = {} for plugin_name in plugin_source.list_plugins(): plugin_dict[plugin_name] = plugin_source.load_plugin(plugin_name) return plugin_dict wafw00f-0.9.4/wafw00f/__init__.py0000664000175000017500000000005612672245364016013 0ustar kunkun00000000000000#!/usr/bin/env python __version__ = '0.9.4' wafw00f-0.9.4/wafw00f/tests/0000775000175000017500000000000012672245714015042 5ustar kunkun00000000000000wafw00f-0.9.4/wafw00f/tests/test_main.py0000664000175000017500000000744612672245364017413 0ustar kunkun00000000000000#!/usr/bin/env python from unittest import TestCase import httpretty from wafw00f.main import WafW00F class WafW00FTestCase(TestCase): def test_is360wzb(self): """ :: $ curl -I http://www.58dm.com/ HTTP/1.1 200 OK Server: nginx/1.4.3.6 Date: Tue, 10 Jun 2014 12:42:21 GMT Content-Type: text/html Connection: keep-alive X-Powered-By-360WZB: wangzhan.360.cn Content-Location: http://www.58dm.com/index.html Last-Modified: Tue, 10 Jun 2014 04:10:42 GMT ETag: "6a8d27f26184cf1:daa" VAR-Cache: HIT Accept-Ranges: bytes cache-control: max-age=7200 age: 0 """ self.__assert_waf('www.58dm.com', '360WangZhanBao', {'X-Powered-By-360WZB': 'wangzhan.360.cn'}) def test_isanquanbao(self): """ :: $ curl -I http://www.51cdz.com/ HTTP/1.1 200 OK Server: ASERVER/1.2.9-3 Date: Tue, 10 Jun 2014 12:41:42 GMT Content-Type: text/html Content-Length: 76789 Connection: keep-alive Content-Location: http://www.51cdz.com/index.html Last-Modified: Sun, 01 Jun 2014 15:39:34 GMT Accept-Ranges: bytes ETag: "766fe9afaf7dcf1:41fc" X-Powered-By-Anquanbao: MISS from chn-tj-ht-se2 """ self.__assert_waf('www.51cdz.com', 'Anquanbao', {'X-Powered-By-Anquanbao': 'MISS from chn-tj-ht-se2'}) def test_ischinacache(self): """ :: $ curl -I http://s1.meituan.net/ HTTP/1.0 404 Not Found Server: Tengine Date: Tue, 10 Jun 2014 12:40:19 GMT Content-Type: text/html; charset=iso-8859-1 Timing-Allow-Origin: * Powered-By-ChinaCache: MISS from 0100102396 X-Cache: MISS from DXT-BJ-104 X-Cache-Lookup: MISS from DXT-BJ-104:80 X-Cache: MISS from DXT-BJ-219 X-Cache-Lookup: MISS from DXT-BJ-219:80 Connection: close """ self.__assert_waf('s1.meituan.net', 'ChinaCache-CDN', {'Powered-By-ChinaCache': 'fake'}) def test_ispowercdn(self): """ :: $ curl -I http://www.jjwxc.net/ HTTP/1.1 200 OK Date: Tue, 10 Jun 2014 01:17:09 GMT Content-Type: text/html Last-Modified: Mon, 09 Jun 2014 17:30:04 GMT Content-Encoding: gzip X-Cache: HIT from BGP-1-233-ZZ-JJCDN X-Cache-Lookup: HIT from BGP-1-233-ZZ-JJCDN:80 Age: 6052 PowerCDN: HIT from ak244.powercdn.com Via: 1.1 ak244.powercdn.com (PowerCDN/2.4) Connection: keep-alive """ self.__assert_waf('www.jjwxc.net', 'PowerCDN', {'PowerCDN': 'HIT from ak244.powercdn.com'}) def test_iswest263cdn(self): """ :: $ curl -I http://hsht.hs3w.com/ HTTP/1.0 400 Bad Request Content-Length: 39 Content-Type: text/html Date: Tue, 10 Jun 2014 12:33:50 GMT X-Cache: MISS from WT263CDN-1231786 X-Cache-Lookup: MISS from WT263CDN-1231786:80 X-Cache: MISS from test.abc.com X-Cache-Lookup: MISS from test.abc.com:80 Via: 1.0 WT263CDN-1231786 (squid/3.0.STABLE20), 1.0 test.abc.com (squid/3.0.STABLE20) Connection: close """ self.__assert_waf('hsht.hs3w.com', 'West263CDN', {'X-Cache': 'MISS from WT263CDN-1231786'}) @httpretty.activate def __assert_waf(self, host, vendor, fake_headers): httpretty.register_uri(httpretty.GET, 'http://%s/' % host, body='fake text', adding_headers=fake_headers) attacker = WafW00F(host) waf = attacker.wafdetections[vendor](attacker) self.assertTrue(waf) wafw00f-0.9.4/wafw00f/tests/__init__.py0000664000175000017500000000000012672245364017142 0ustar kunkun00000000000000wafw00f-0.9.4/CREDITS.txt0000664000175000017500000000152012672245364014263 0ustar kunkun00000000000000Original code by Sandro Gauci && Wendel G. Henrique However, a number of people contributed (in no particular order): - Sebastien Gioria - W3AF (or Andres Riancho) - Charlie Campbell - @j0eMcCray - Mathieu Dessus - David S. Langlands - Nmap's http-waf-fingerprint.nse / Hani Benhabiles - Denis Kolegov - kun a - Louis-Philippe Huberdeau - Brendan Coles - Matt Foster - g0tmi1k (?) - MyKings If you did contributed and somehow I didn't put your name in there, please do let me know: .