├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .landscape.yaml ├── .travis.yml ├── CREDITS.txt ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst └── wafw00f.8 ├── nose.cfg ├── setup.py └── wafw00f ├── __init__.py ├── bin └── wafw00f ├── lib ├── __init__.py ├── evillib.py └── proxy.py ├── main.py ├── manager.py ├── plugins ├── __init__.py ├── airlock.py ├── anquanbao.py ├── barracuda.py ├── betterwpsecurity.py ├── binarysec.py ├── blockdos.py ├── chinacache.py ├── ciscoacexml.py ├── cloudflare.py ├── comodo.py ├── denyall.py ├── dotdefender.py ├── edgecast.py ├── f5bigipapm.py ├── f5bigipasm.py ├── f5bigipltm.py ├── f5firepass.py ├── f5trafficshield.py ├── fortiweb.py ├── hyperguard.py ├── ibm.py ├── ibmdatapower.py ├── imperva.py ├── incapsula.py ├── isaserver.py ├── missioncontrol.py ├── modsecurity.py ├── modsecuritycrs.py ├── naxsi.py ├── netcontinuum.py ├── netscaler.py ├── nevisproxy.py ├── nsfocus.py ├── powercdn.py ├── profense.py ├── radware.py ├── safedog.py ├── secureiis.py ├── sucuri.py ├── teros.py ├── urlscan.py ├── uspses.py ├── wallarm.py ├── webknight.py ├── webscurity.py ├── west263cdn.py └── wzb360.py ├── tests ├── __init__.py └── test_main.py └── wafprio.py /.coveralls.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0tmi1k/wafw00f/0e3f47946ea3a0c80823dda408ba936318eccbc1/.coveralls.yml -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | 10 | [*.py] 11 | indent_size = 4 12 | 13 | [{*.bat,Makefile}] 14 | indent_style = tab 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.swp 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | include/ 18 | lib/ 19 | local/ 20 | lib64/ 21 | man/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # Installer logs 30 | pip-log.txt 31 | pip-delete-this-directory.txt 32 | 33 | # Unit test / coverage reports 34 | htmlcov/ 35 | .tox/ 36 | .coverage 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | 41 | # Translations 42 | *.mo 43 | 44 | # Mr Developer 45 | .mr.developer.cfg 46 | .project 47 | .pydevproject 48 | 49 | # Rope 50 | .ropeproject 51 | 52 | # Django stuff: 53 | *.log 54 | *.pot 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | 60 | .idea/* 61 | 62 | .vscode/* 63 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | doc-warnings: no 2 | test-warnings: no 3 | strictness: veryhigh 4 | autodetect: yes 5 | ignore-paths: 6 | - docs 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.4" 5 | - "2.7" 6 | - "2.6" 7 | 8 | install: 9 | - "pip install -q -e .[test]" 10 | 11 | script: make test 12 | 13 | after_success: 14 | - coveralls 15 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | Original code by Sandro Gauci && Wendel G. Henrique 2 | 3 | However, a number of people contributed (in no particular order): 4 | 5 | - Sebastien Gioria 6 | - W3AF (or Andres Riancho) 7 | - Charlie Campbell 8 | - @j0eMcCray 9 | - Mathieu Dessus 10 | - David S. Langlands 11 | - Nmap's http-waf-fingerprint.nse / Hani Benhabiles 12 | - Denis Kolegov 13 | - kun a 14 | - Louis-Philippe Huberdeau 15 | - Brendan Coles 16 | - Matt Foster 17 | - g0tmi1k (?) 18 | - MyKings 19 | 20 | 21 | If you did contributed and somehow I didn't put your name in there, please do 22 | let me know: . 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Sandro Gauci, Enable Security ltd 2 | and Wendel G. Henrique - Trustwave 2009 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the {organization} nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CREDITS.txt 2 | include LICENSE 3 | include MANIFEST.in 4 | include README.md 5 | include wafw00f/__init__.py 6 | include wafw00f/bin/wafw00f 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC_DIR = wafw00f 2 | DOC_DIR = docs 3 | MAKE = make 4 | 5 | all: 6 | make install 7 | make test 8 | make html 9 | make clean 10 | 11 | install: 12 | pip install -q -e .[dev,test,docs] 13 | 14 | lint: 15 | prospector $(SRC_DIR) --strictness veryhigh 16 | 17 | test: 18 | nosetests -c nose.cfg 19 | 20 | testall: 21 | tox 22 | 23 | html: 24 | cd $(DOC_DIR) && $(MAKE) html 25 | 26 | htmlci: 27 | curl -X POST http://readthedocs.org/build/wafw00f 28 | 29 | clean: 30 | rm -rf *.egg-info build dist .coverage 31 | find $(SRC_DIR) -name "*.pyc" | xargs rm -rf 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WAFW00F 2 | 3 | WAFW00F identifies and fingerprints Web Application Firewall (WAF) products. 4 | 5 | ## How does it work? 6 | 7 | To do its magic, WAFW00F does the following: 8 | 9 | - Sends a _normal_ HTTP request and analyses the response; this identifies a 10 | number of WAF solutions 11 | - If that is not successful, it sends a number of (potentially malicious) HTTP 12 | requests and uses simple logic to deduce which WAF it is 13 | - If that is also not successful, it analyses the responses previously 14 | returned and uses another simple algorithm to guess if a WAF or security 15 | solution is actively responding to our attacks 16 | 17 | For further details, check out the source code on the main site, 18 | [github.com/sandrogauci/wafw00f](https://github.com/sandrogauci/wafw00f). 19 | 20 | ## What does it detect? 21 | 22 | It detects a number of WAFs. To view which WAFs it is able to detect run 23 | WAFW00F with the `-l` option. At the time of writing the output is as follows: 24 | 25 | $ wafw00f -l 26 | 27 | ^ ^ 28 | _ __ _ ____ _ __ _ _ ____ 29 | ///7/ /.' \ / __////7/ /,' \ ,' \ / __/ 30 | | V V // o // _/ | V V // 0 // 0 // _/ 31 | |_n_,'/_n_//_/ |_n_,' \_,' \_,'/_/ 32 | < 33 | ...' 34 | 35 | WAFW00F - Web Application Firewall Detection Tool 36 | 37 | By Sandro Gauci && Wendel G. Henrique 38 | 39 | Can test for these WAFs: 40 | 41 | Anquanbao 42 | FortiWeb 43 | Naxsi 44 | Juniper WebApp Secure 45 | IBM Web Application Security 46 | Cisco ACE XML Gateway 47 | Better WP Security 48 | F5 BIG-IP ASM 49 | Citrix NetScaler 50 | ModSecurity (OWASP CRS) 51 | F5 BIG-IP APM 52 | 360WangZhanBao 53 | Mission Control Application Shield 54 | PowerCDN 55 | Safedog 56 | Sucuri WAF 57 | F5 FirePass 58 | DenyALL WAF 59 | Trustwave ModSecurity 60 | CloudFlare 61 | Profense 62 | Wallarm 63 | Incapsula WAF 64 | Radware AppWall 65 | F5 BIG-IP LTM 66 | Art of Defence HyperGuard 67 | Aqtronix WebKnight 68 | Teros WAF 69 | eEye Digital Security SecureIIS 70 | BinarySec 71 | IBM DataPower 72 | Microsoft ISA Server 73 | NetContinuum 74 | NSFocus 75 | ChinaCache-CDN 76 | West263CDN 77 | InfoGuard Airlock 78 | AdNovum nevisProxy 79 | Barracuda Application Firewall 80 | Comodo WAF 81 | Imperva SecureSphere 82 | BlockDoS 83 | Edgecast / Verizon Digital media 84 | Microsoft URLScan 85 | Applicure dotDefender 86 | USP Secure Entry Server 87 | F5 Trafficshield 88 | 89 | 90 | 91 | 92 | 93 | ## How do I use it? 94 | 95 | First, install the tools as described [here](#how-do-i-install-it). 96 | 97 | For help please make use of the `--help` option. The basic usage is to pass it 98 | a URL as an argument. Example: 99 | 100 | $ wafw00f https://www.ibm.com/ 101 | 102 | ^ ^ 103 | _ __ _ ____ _ __ _ _ ____ 104 | ///7/ /.' \ / __////7/ /,' \ ,' \ / __/ 105 | | V V // o // _/ | V V // 0 // 0 // _/ 106 | |_n_,'/_n_//_/ |_n_,' \_,' \_,'/_/ 107 | < 108 | ...' 109 | 110 | WAFW00F - Web Application Firewall Detection Tool 111 | 112 | By Sandro Gauci && Wendel G. Henrique 113 | 114 | Checking https://www.ibm.com/ 115 | The site https://www.ibm.com/ is behind a Citrix NetScaler 116 | Number of requests: 6 117 | 118 | 119 | ## How do I install it? 120 | 121 | The following should do the trick: 122 | 123 | python setup.py install 124 | 125 | or 126 | 127 | pip install wafw00f 128 | 129 | ## Need a freelance pentester? 130 | 131 | More information about the services that I offer at [Enable Security](http://enablesecurity.com/) 132 | 133 | ## How do I write my own new checks? 134 | 135 | Follow the instructions on the [wiki](https://github.com/EnableSecurity/wafw00f/wiki/How-to-write-new-WAF-checks) 136 | 137 | ## Questions? 138 | 139 | Contact [me](mailto:sandro@enablesecurity.com) 140 | 141 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/wafw00f.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/wafw00f.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/wafw00f" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/wafw00f" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # wafw00f documentation build configuration file, created by 4 | # sphinx-quickstart on Thu May 15 20:04:22 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sys 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 18 | sys.path.insert(0, BASE_DIR) 19 | from wafw00f import __version__ 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'wafw00f' 50 | copyright = u'2014, sandrogauci' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = __version__ 58 | # The full version, including alpha/beta/rc tags. 59 | release = __version__ 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ['_build'] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | # If true, keep warnings as "system message" paragraphs in the built documents. 97 | #keep_warnings = False 98 | 99 | 100 | # -- Options for HTML output ---------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = 'default' 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | #html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | #html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | #html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | #html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | #html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | #html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ['_static'] 134 | 135 | # Add any extra paths that contain custom files (such as robots.txt or 136 | # .htaccess) here, relative to this directory. These files are copied 137 | # directly to the root of the documentation. 138 | #html_extra_path = [] 139 | 140 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | #html_last_updated_fmt = '%b %d, %Y' 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | #html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | #html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | #html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | #html_domain_indices = True 157 | 158 | # If false, no index is generated. 159 | #html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | #html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | #html_show_sourcelink = True 166 | 167 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 168 | #html_show_sphinx = True 169 | 170 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 171 | #html_show_copyright = True 172 | 173 | # If true, an OpenSearch description file will be output, and all pages will 174 | # contain a tag referring to it. The value of this option must be the 175 | # base URL from which the finished HTML is served. 176 | #html_use_opensearch = '' 177 | 178 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 179 | #html_file_suffix = None 180 | 181 | # Output file base name for HTML help builder. 182 | htmlhelp_basename = 'wafw00fdoc' 183 | 184 | 185 | # -- Options for LaTeX output --------------------------------------------- 186 | 187 | latex_elements = { 188 | # The paper size ('letterpaper' or 'a4paper'). 189 | #'papersize': 'letterpaper', 190 | 191 | # The font size ('10pt', '11pt' or '12pt'). 192 | #'pointsize': '10pt', 193 | 194 | # Additional stuff for the LaTeX preamble. 195 | #'preamble': '', 196 | } 197 | 198 | # Grouping the document tree into LaTeX files. List of tuples 199 | # (source start file, target name, title, 200 | # author, documentclass [howto, manual, or own class]). 201 | latex_documents = [ 202 | ('index', 'wafw00f.tex', u'wafw00f Documentation', 203 | u'sandrogauci', 'manual'), 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | #latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | #latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | #latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | #latex_show_urls = False 219 | 220 | # Documents to append as an appendix to all manuals. 221 | #latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | #latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output --------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ('index', 'wafw00f', u'wafw00f Documentation', 233 | [u'sandrogauci'], 1) 234 | ] 235 | 236 | # If true, show URL addresses after external links. 237 | #man_show_urls = False 238 | 239 | 240 | # -- Options for Texinfo output ------------------------------------------- 241 | 242 | # Grouping the document tree into Texinfo files. List of tuples 243 | # (source start file, target name, title, author, 244 | # dir menu entry, description, category) 245 | texinfo_documents = [ 246 | ('index', 'wafw00f', u'wafw00f Documentation', 247 | u'sandrogauci', 'wafw00f', 'One line description of project.', 248 | 'Miscellaneous'), 249 | ] 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #texinfo_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #texinfo_domain_indices = True 256 | 257 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 258 | #texinfo_show_urls = 'footnote' 259 | 260 | # If true, do not generate a @detailmenu in the "Top" node's menu. 261 | #texinfo_no_detailmenu = False 262 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. wafw00f documentation master file, created by 2 | sphinx-quickstart on Thu May 15 20:04:22 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to wafw00f's documentation! 7 | =================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/wafw00f.8: -------------------------------------------------------------------------------- 1 | .TH WAFW00F "8" "November 2017" "wafw00f " "User Commands" 2 | .SH NAME 3 | wafw00f \- identify and fingerprint Web Application Firewall products 4 | .SH SYNOPSIS 5 | .B wafw00f 6 | \fI\,url1 \/\fR[\fI\,url2 \/\fR[\fI\,url3 \/\fR... ]] 7 | .SH DESCRIPTION 8 | .TP 9 | Identifies and fingerprints Web Application Firewall (WAF) products: 10 | .TP 11 | .TP 12 | To do its magic, WAFW00F does the following: 13 | Sends a normal HTTP request and analyses the response; this identifies a number of WAF solutions 14 | If that is not successful, it sends a number of (potentially malicious) HTTP requests and uses simple logic to deduce which WAF it is 15 | 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 16 | .SH OPTIONS 17 | .TP 18 | \fB\-h\fR, \fB\-\-help\fR 19 | Show available options 20 | .TP 21 | \fB\-v\fR, \fB\-\-verbose\fR 22 | Enable verbosity \- multiple \fB\-v\fR options increase 23 | verbosity 24 | .TP 25 | \fB\-a\fR, \fB\-\-findall\fR 26 | Find all WAFs, do not stop testing on the first one 27 | .TP 28 | \fB\-r\fR, \fB\-\-disableredirect\fR 29 | Do not follow redirections given by 3xx responses 30 | .TP 31 | \fB\-t\fR TEST, \fB\-\-test\fR=\fI\,TEST\/\fR 32 | Test for one specific WAF 33 | .TP 34 | \fB\-l\fR, \fB\-\-list\fR 35 | List all WAFs that we are able to detect 36 | .TP 37 | \fB\-p\fR PROXY, \fB\-\-proxy\fR=\fI\,PROXY\/\fR 38 | Use an HTTP proxy to perform requests, example: 39 | http://hostname:8080, socks5://hostname:1080 40 | .TP 41 | \fB\-V\fR, \fB\-\-version\fR 42 | Print out the version 43 | .TP 44 | \fB\-H\fR HEADERSFILE, \fB\-\-headersfile\fR=\fI\,HEADERSFILE\/\fR 45 | Pass custom headers, for example to overwrite the 46 | default User\-Agent string 47 | .SH AUTHORS 48 | Sandro Gauci 49 | .br 50 | Wendel G. Henrique 51 | .PP 52 | This manpage was written by Daniel Echeverry and Samuel Henrique for the Debian Project (but may be used by others), it was based on wafw00f's help output. 53 | -------------------------------------------------------------------------------- /nose.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | nocapture=1 3 | with-coverage=1 4 | cover-package=wafw00f 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | from setuptools import setup, find_packages 5 | 6 | 7 | setup( 8 | name='wafw00f', 9 | version=__import__('wafw00f').__version__, 10 | description=('WAFW00F identifies and fingerprints ' 11 | 'Web Application Firewall (WAF) products.'), 12 | author='sandrogauci', 13 | author_email='sandro@enablesecurity.com', 14 | license='BSD License', 15 | url='https://github.com/sandrogauci/wafw00f', 16 | packages=find_packages(), 17 | scripts=['wafw00f/bin/wafw00f'], 18 | install_requires=[ 19 | 'beautifulsoup4==4.6.0', 20 | 'pluginbase==0.3', 21 | ], 22 | classifiers=[ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Intended Audience :: System Administrators', 25 | 'Intended Audience :: Information Technology', 26 | 'Topic :: Internet', 27 | 'Topic :: Security', 28 | 'Topic :: System :: Networking :: Firewalls', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Programming Language :: Python :: 2', 31 | ], 32 | keywords='waf firewall detector fingerprint', 33 | extras_require={ 34 | 'dev': [ 35 | 'prospector==0.10.1', 36 | ], 37 | 'test': [ 38 | 'httpretty==0.8.10', 39 | 'coverage==3.7.1', 40 | 'coveralls==0.5', 41 | 'python-coveralls==2.5.0', 42 | 'nose==1.3.6', 43 | ], 44 | 'docs': [ 45 | 'Sphinx==1.3.1', 46 | ], 47 | }, 48 | test_suite='nose.collector', 49 | ) 50 | -------------------------------------------------------------------------------- /wafw00f/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | __version__ = '0.9.4' 5 | -------------------------------------------------------------------------------- /wafw00f/bin/wafw00f: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | from wafw00f import main 5 | 6 | 7 | if __name__ == '__main__': 8 | main.main() 9 | -------------------------------------------------------------------------------- /wafw00f/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0tmi1k/wafw00f/0e3f47946ea3a0c80823dda408ba936318eccbc1/wafw00f/lib/__init__.py -------------------------------------------------------------------------------- /wafw00f/lib/evillib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import sys 4 | 5 | import socket 6 | 7 | try: 8 | from urlparse import urlparse, urlunparse 9 | except ImportError: 10 | from urllib.parse import urlparse, urlunparse 11 | import logging 12 | 13 | try: 14 | from bs4 import BeautifulSoup 15 | except ImportError: 16 | sys.stderr.write('You need to get BeautifulSoup installed\n') 17 | sys.stderr.write('Do it now, as privileged user/root run: pip install beautifulsoup4\now') 18 | sys.exit(1) 19 | 20 | 21 | from .proxy import NullProxy, HttpProxy, Socks5Proxy, httplib, socks 22 | 23 | __license__ = """ 24 | Copyright (c) 2016, {Sandro Gauci|Wendel G. Henrique} 25 | All rights reserved. 26 | 27 | Redistribution and use in source and binary forms, with or without modification, 28 | are permitted provided that the following conditions are met: 29 | 30 | * Redistributions of source code must retain the above copyright notice, 31 | this list of conditions and the following disclaimer. 32 | * Redistributions in binary form must reproduce the above copyright notice, 33 | this list of conditions and the following disclaimer in the documentation 34 | and/or other materials provided with the distribution. 35 | * Neither the name of EnableSecurity or Trustwave nor the names of its contributors 36 | may be used to endorse or promote products derived from this software 37 | without specific prior written permission. 38 | 39 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 40 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 41 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 42 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 43 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 44 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 45 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 46 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 47 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 48 | OF THE POSSIBILITY OF SUCH DAMAGE. 49 | """ 50 | 51 | 52 | 53 | # unicode mapping borrowed from http://packetstormsecurity.org/web/unicode-fun.txt 54 | # by Gary O'leary-Steele of Sec-1 Ltd 55 | try: 56 | from urllib import quote, unquote 57 | except ImportError: 58 | from urllib.parse import quote, unquote 59 | unicodemapping = {' ': '%u0020', 60 | '/': '%u2215', 61 | '\\': '%u2215', 62 | "'": '%u02b9', 63 | '"': '%u0022', 64 | '>': '%u003e', 65 | '<': '%u003c', 66 | '#': '%uff03', 67 | '!': '%uff01', 68 | '$': '%uff04', 69 | '*': '%uff0a', 70 | '@': '%u0040', 71 | '.': '%uff0e', 72 | '_': '%uff3f', 73 | '(': '%uff08', 74 | ')': '%uff09', 75 | ',': '%uff0c', 76 | '%': '%u0025', 77 | '-': '%uff0d', 78 | ';': '%uff1b', 79 | ':': '%uff1a', 80 | '|': '%uff5c', 81 | '&': '%uff06', 82 | '+': '%uff0b', 83 | '=': '%uff1d', 84 | 'a': '%uff41', 85 | 'A': '%uff21', 86 | 'b': '%uff42', 87 | 'B': '%uff22', 88 | 'c': '%uff43', 89 | 'C': '%uff23', 90 | 'd': '%uff44', 91 | 'D': '%uff24', 92 | 'e': '%uff45', 93 | 'E': '%uff25', 94 | 'f': '%uff46', 95 | 'F': '%uff26', 96 | 'g': '%uff47', 97 | 'G': '%uff27', 98 | 'h': '%uff48', 99 | 'H': '%uff28', 100 | 'i': '%uff49', 101 | 'I': '%uff29', 102 | 'j': '%uff4a', 103 | 'J': '%uff2a', 104 | 'k': '%uff4b', 105 | 'K': '%uff2b', 106 | 'l': '%uff4c', 107 | 'L': '%uff2c', 108 | 'm': '%uff4d', 109 | 'M': '%uff2d', 110 | 'n': '%uff4e', 111 | 'N': '%uff2e', 112 | 'o': '%uff4f', 113 | 'O': '%uff2f', 114 | 'p': '%uff50', 115 | 'P': '%uff30', 116 | 'q': '%uff51', 117 | 'Q': '%uff31', 118 | 'r': '%uff52', 119 | 'R': '%uff32', 120 | 's': '%uff53', 121 | 'S': '%uff33', 122 | 't': '%uff54', 123 | 'T': '%uff34', 124 | 'u': '%uff55', 125 | 'U': '%uff35', 126 | 'v': '%uff56', 127 | 'V': '%uff36', 128 | 'w': '%uff57', 129 | 'W': '%uff37', 130 | 'x': '%uff58', 131 | 'X': '%uff38', 132 | 'y': '%uff59', 133 | 'Y': '%uff39', 134 | 'z': '%uff5a', 135 | 'Z': '%uff3a', 136 | '0': '%uff10', 137 | '1': '%uff11', 138 | '2': '%uff12', 139 | '3': '%uff13', 140 | '4': '%uff14', 141 | '5': '%uff15', 142 | '6': '%uff16', 143 | '7': '%uff17', 144 | '8': '%uff18', 145 | '9': '%uff19'} 146 | 147 | homoglyphicmapping = {"'": '%ca%bc'} 148 | 149 | 150 | def oururlparse(target): 151 | log = logging.getLogger('urlparser') 152 | 153 | ssl = False 154 | o = urlparse(target) 155 | if o[0] not in ['http', 'https', '']: 156 | log.error('scheme %s not supported' % o[0]) 157 | return 158 | if o[0] == 'https': 159 | ssl = True 160 | if len(o[2]) > 0: 161 | path = o[2] 162 | else: 163 | path = '/' 164 | tmp = o[1].split(':') 165 | if len(tmp) > 1: 166 | port = tmp[1] 167 | else: 168 | port = None 169 | hostname = tmp[0] 170 | query = o[4] 171 | return (hostname, port, path, query, ssl) 172 | 173 | 174 | def modifyurl(path, modfunc, log): 175 | path = path 176 | log.debug('path is currently %s' % path) 177 | #s = re.search('(\[.*?\])',path) 178 | for m in re.findall('(\[.*?\])', path): 179 | ourstr = m[1:-1] 180 | newstr = modfunc(ourstr) 181 | log.debug('String was %s' % ourstr) 182 | log.debug('String became %s' % newstr) 183 | path = path.replace(m, newstr) 184 | log.debug('the path is now %s' % path) 185 | return path 186 | 187 | 188 | def modifypath(path, newstrs, log, encode=True): 189 | log.debug('path is currently %s' % path) 190 | for m in re.findall('(\[.*?\])', path): 191 | ourstr = m[1:-1] 192 | for newstr in newstrs: 193 | if encode: 194 | newstr = quote(newstr) 195 | log.debug('String was %s' % ourstr) 196 | log.debug('String became %s' % newstr) 197 | newpath = path.replace(m, newstr).replace(']', '').replace('[', '') 198 | yield (newpath) 199 | 200 | 201 | def bruteforceascii(ourstr): 202 | listourstr = list(ourstr) 203 | for pos in range(len(ourstr)): 204 | for i in range(256): 205 | newlistourstr = listourstr[:] 206 | newlistourstr[pos] = chr(i) 207 | yield (quote(''.join(newlistourstr))) 208 | 209 | 210 | def unicodeurlencode(ourstr): 211 | newstr = str() 212 | for character in ourstr: 213 | if unicodemapping.has_key(character): 214 | newstr += unicodemapping[character] 215 | else: 216 | newstr += character 217 | return newstr 218 | 219 | 220 | def nullify(ourstr): 221 | newstr = str() 222 | for character in ourstr: 223 | newstr += character + "\x00" 224 | return quote(newstr) 225 | 226 | 227 | def replacechars(ourstr, origchar, newchar): 228 | newstr = ourstr.replace(origchar, newchar) 229 | return newstr 230 | 231 | 232 | def nullifyspaces(ourstr): 233 | return quote(replacechars(ourstr, ' ', '\x00')) 234 | 235 | 236 | def slashspaces(ourstr): 237 | return replacechars(ourstr, ' ', '/') 238 | 239 | 240 | def tabifyspaces(ourstr): 241 | return replacechars(ourstr, ' ', '\t') 242 | 243 | 244 | def crlfspaces(ourstr): 245 | return replacechars(ourstr, ' ', '\n') 246 | 247 | 248 | def backslashquotes(ourstr): 249 | return replacechars(ourstr, "'", "\''") 250 | 251 | 252 | class waftoolsengine: 253 | def __init__(self, target='www.microsoft.com', port=80, ssl=False, 254 | debuglevel=0, path='/', followredirect=True, extraheaders={}, 255 | proxy=False): 256 | """ 257 | target: the hostname or ip of the target server 258 | port: defaults to 80 259 | ssl: defaults to false 260 | """ 261 | self.target = target 262 | if port is None: 263 | if ssl: 264 | port = 443 265 | else: 266 | port = 80 267 | self.port = int(port) 268 | self.ssl = ssl 269 | self.debuglevel = debuglevel 270 | self.cachedresponses = dict() 271 | self.requestnumber = 0 272 | self.path = path 273 | self.redirectno = 0 274 | self.followredirect = followredirect 275 | self.crawlpaths = list() 276 | self.extraheaders = extraheaders 277 | 278 | try: 279 | self.proxy = self._parse_proxy(proxy, ssl) if proxy else NullProxy() 280 | except Exception as e: 281 | self.log.critical("Proxy disabled: %s" % e) 282 | self.proxy = NullProxy() 283 | 284 | def request(self, method='GET', path=None, usecache=True, 285 | cacheresponse=True, headers=None, 286 | comingfromredir=False): 287 | followredirect = self.followredirect 288 | if comingfromredir: 289 | self.redirectno += 1 290 | if self.redirectno >= 5: 291 | self.log.error('We received way too many redirects.. stopping that') 292 | followredirect = False 293 | else: 294 | self.redirectno = 0 295 | if path is None: 296 | path = self.path 297 | for hdr in self.extraheaders.keys(): 298 | if headers is None: 299 | headers = {} 300 | headers[hdr] = self.extraheaders[hdr] 301 | if headers is not None: 302 | knownheaders = map(lambda x: x.lower(), headers.keys()) 303 | else: 304 | knownheaders = {} 305 | headers = {} 306 | if not 'user-agent' in knownheaders: 307 | headers[ 308 | 'User-Agent'] = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b1) Gecko/20081007 Firefox/3.0' 309 | if not 'accept-charset' in knownheaders: 310 | headers['Accept-Charset'] = 'ISO-8859-1,utf-8;q=0.7,*;q=0.7' 311 | if not 'accept' in knownheaders: 312 | headers['Accept'] = '*/*' 313 | k = str([method, path, headers]) 314 | if usecache: 315 | if k in self.cachedresponses.keys(): 316 | self.log.debug('Using cached version of %s, %s' % (method, path)) 317 | return self.cachedresponses[k] 318 | else: 319 | self.log.debug('%s not found in %s' % (k, self.cachedresponses.keys())) 320 | r = self._request(method, path, headers) 321 | if cacheresponse: 322 | self.cachedresponses[k] = r 323 | 324 | if r: 325 | response, responsebody = r 326 | if response.status in [301, 302, 307]: 327 | if followredirect: 328 | if response.getheader('location'): 329 | newloc = response.getheader('location') 330 | self.log.info('Redirected to %s' % newloc) 331 | pret = oururlparse(newloc) 332 | if pret is not None: 333 | (target, port, path, query, ssl) = pret 334 | if not port: port = 80 335 | if target == '': 336 | target = self.target 337 | if port is None: 338 | port = self.port 339 | if not path.startswith('/'): 340 | path = '/' + path 341 | if (target, port, ssl) == (self.target, self.port, ssl): 342 | r = self.request(method, path, usecache, cacheresponse, 343 | headers, comingfromredir=True) 344 | else: 345 | self.log.warn('Tried to redirect to a different server %s' % newloc) 346 | else: 347 | self.log.warn('%s is not a well formatted url' % response.getheader('location')) 348 | return r 349 | 350 | def _request(self, method, path, headers): 351 | original_socket = socket.socket 352 | try: 353 | conn_factory, connect_host, connect_port, query_path = \ 354 | self.proxy.prepare(self.target, self.port, path, self.ssl) 355 | 356 | params = dict() 357 | if sys.hexversion > 0x2060000: 358 | params['timeout'] = 4 359 | if (sys.hexversion >= 0x2070900) and self.ssl and not isinstance(self.proxy,HttpProxy): 360 | import ssl as ssllib 361 | params['context'] = ssllib._create_unverified_context() 362 | h = conn_factory(connect_host, connect_port, **params) 363 | if self.ssl and isinstance(self.proxy,HttpProxy): 364 | import ssl as ssllib 365 | h.set_tunnel("%s:%s" % (self.target,self.port)) 366 | if self.debuglevel <= 10: 367 | if self.debuglevel > 1: 368 | h.set_debuglevel(self.debuglevel) 369 | try: 370 | self.log.info('Sending %s %s' % (method, path)) 371 | h.request(method, query_path, headers=headers) 372 | except socket.error: 373 | self.log.warn('Could not initialize connection to %s' % self.target) 374 | return 375 | self.requestnumber += 1 376 | 377 | response = h.getresponse() 378 | responsebody = response.read() 379 | h.close() 380 | r = response, responsebody 381 | except (socket.error, socket.timeout, httplib.BadStatusLine): 382 | self.log.warn('Hey.. they closed our connection!') 383 | r = None 384 | finally: 385 | self.proxy.terminate() 386 | 387 | return r 388 | 389 | 390 | def querycrawler(self, path=None, curdepth=0, maxdepth=1): 391 | self.log.debug('Crawler is visiting %s' % path) 392 | localcrawlpaths = list() 393 | if curdepth > maxdepth: 394 | self.log.info('maximum depth %s reached' % maxdepth) 395 | return 396 | r = self.request(path=path) 397 | if r is None: 398 | return 399 | response, responsebody = r 400 | try: 401 | soup = BeautifulSoup(responsebody) 402 | except: 403 | self.log.warn('could not parse the response body') 404 | return 405 | tags = soup('a') 406 | for tag in tags: 407 | try: 408 | href = tag["href"] 409 | if href is not None: 410 | tmpu = urlparse(href) 411 | if (tmpu[1] != '') and (self.target != tmpu[1]): 412 | # not on the same domain name .. ignore 413 | self.log.debug('Ignoring link because it is not on the same site %s' % href) 414 | continue 415 | if tmpu[0] not in ['http', 'https', '']: 416 | self.log.debug('Ignoring link because it is not an http uri %s' % href) 417 | continue 418 | path = tmpu[2] 419 | if not path.startswith('/'): 420 | path = '/' + path 421 | if len(tmpu[4]) > 0: 422 | # found a query .. thats all we need 423 | location = urlunparse(('', '', path, tmpu[3], tmpu[4], '')) 424 | self.log.info('Found query %s' % location) 425 | return href 426 | if path not in self.crawlpaths: 427 | href = unquote(path) 428 | self.log.debug('adding %s for crawling' % href) 429 | self.crawlpaths.append(href) 430 | localcrawlpaths.append(href) 431 | except KeyError: 432 | pass 433 | for nextpath in localcrawlpaths: 434 | r = self.querycrawler(path=nextpath, curdepth=curdepth + 1, maxdepth=maxdepth) 435 | if r: 436 | return r 437 | 438 | def _parse_proxy(self, proxy, ssl): 439 | parts = urlparse(proxy) 440 | if not parts.scheme or not parts.netloc: 441 | raise Exception("Invalid proxy specified, scheme required") 442 | 443 | netloc = parts.netloc.split(":") 444 | if len(netloc) != 2: 445 | raise Exception("Proxy port unspecified") 446 | 447 | try: 448 | if parts.scheme == "socks5": 449 | if socks is None: 450 | raise Exception("socks5 proxy requires PySocks") 451 | 452 | return Socks5Proxy(netloc[0], int(netloc[1])) 453 | elif parts.scheme == "http": 454 | if ssl: 455 | raise Exception("SSL over HTTP proxy is not yet supported, but proxychains or similar would fix this ;-)") 456 | return HttpProxy(netloc[0], int(netloc[1])) 457 | else: 458 | raise Exception("Unsupported proxy scheme") 459 | except ValueError: 460 | raise Exception("Invalid port number") 461 | 462 | 463 | 464 | def scrambledheader(header): 465 | c = 'connection' 466 | if len(header) != len(c): 467 | return False 468 | if header == c: 469 | return False 470 | for character in c: 471 | if c.count(character) != header.count(character): 472 | return False 473 | return True 474 | 475 | -------------------------------------------------------------------------------- /wafw00f/lib/proxy.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | try: 4 | import httplib 5 | except ImportError: 6 | import http.client as httplib 7 | 8 | try: 9 | import socks 10 | except ImportError: 11 | socks = None 12 | 13 | 14 | class NullProxy: 15 | 16 | def prepare(self, target, port, path, ssl): 17 | conn_factory = httplib.HTTPSConnection if ssl else httplib.HTTPConnection 18 | return conn_factory, target, port, path 19 | 20 | def terminate(self): 21 | pass 22 | 23 | 24 | class HttpProxy: 25 | 26 | def __init__(self, host, port): 27 | self.host = host 28 | self.port = port 29 | 30 | def prepare(self, target, port, path, ssl): 31 | if ssl: 32 | conn_factory = httplib.HTTPConnection 33 | query_path = path 34 | else: 35 | conn_factory = httplib.HTTPConnection 36 | query_path = "%(scheme)s://%(host)s%(path)s" % dict( 37 | scheme="http", 38 | host=target, 39 | path=path) 40 | return conn_factory, self.host, self.port, query_path 41 | 42 | def terminate(self): 43 | pass 44 | 45 | 46 | class Socks5Proxy: 47 | 48 | def __init__(self, host, port): 49 | self.host = host 50 | self.port = port 51 | self.original = socket.socket 52 | self.original_create = socket.create_connection 53 | 54 | def prepare(self, target, port, path, ssl): 55 | def proxy_create_connection(address, timeout=4, source_address=None): 56 | 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) 57 | 58 | socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, self.host, self.port) 59 | httplib.socket.socket = socks.socksocket 60 | httplib.socket.create_connection = proxy_create_connection 61 | 62 | conn_factory = httplib.HTTPSConnection if ssl else httplib.HTTPConnection 63 | return conn_factory, target, port, path 64 | 65 | def terminate(self): 66 | httplib.socket.socket = self.original 67 | httplib.socket.create_connection = self.original_create 68 | -------------------------------------------------------------------------------- /wafw00f/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # wafw00f - Web Application Firewall Detection Tool 3 | # by Sandro Gauci - enablesecurity.com (c) 2016 4 | # and Wendel G. Henrique - Trustwave 2009 5 | 6 | __license__ = """ 7 | Copyright (c) 2016, {Sandro Gauci|Wendel G. Henrique} 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without modification, 11 | are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, 14 | this list of conditions and the following disclaimer. 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | * Neither the name of EnableSecurity or Trustwave nor the names of its contributors 19 | may be used to endorse or promote products derived from this software 20 | without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 26 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 30 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 31 | OF THE POSSIBILITY OF SUCH DAMAGE. 32 | """ 33 | import os 34 | 35 | try: 36 | import httplib 37 | except ImportError: 38 | import http.client as httplib 39 | try: 40 | from urllib import quote, unquote 41 | except ImportError: 42 | from urllib.parse import quote, unquote 43 | from optparse import OptionParser 44 | import logging 45 | import sys 46 | import random 47 | 48 | currentDir = os.getcwd() 49 | scriptDir = os.path.dirname(sys.argv[0]) or '.' 50 | os.chdir(scriptDir) 51 | 52 | from wafw00f import __version__ 53 | from wafw00f.lib.evillib import oururlparse, scrambledheader, waftoolsengine 54 | from wafw00f.manager import load_plugins 55 | from wafw00f.wafprio import wafdetectionsprio 56 | 57 | lackofart = r''' 58 | ^ ^ 59 | _ __ _ ____ _ __ _ _ ____ 60 | ///7/ /.' \ / __////7/ /,' \ ,' \ / __/ 61 | | V V // o // _/ | V V // 0 // 0 // _/ 62 | |_n_,'/_n_//_/ |_n_,' \_,' \_,'/_/ 63 | < 64 | ...' 65 | 66 | WAFW00F - Web Application Firewall Detection Tool 67 | 68 | By Sandro Gauci && Wendel G. Henrique 69 | ''' 70 | 71 | 72 | class WafW00F(waftoolsengine): 73 | """ 74 | WAF detection tool 75 | """ 76 | 77 | AdminFolder = '/Admin_Files/' 78 | xssstring = '' 79 | dirtravstring = '../../../../etc/passwd' 80 | cleanhtmlstring = 'hello' 81 | isaservermatch = [ 82 | 'Forbidden ( The server denied the specified Uniform Resource Locator (URL). Contact the server administrator. )', 83 | 'Forbidden ( The ISA Server denied the specified Uniform Resource Locator (URL)'] 84 | 85 | def __init__(self, target='www.microsoft.com', port=80, ssl=False, 86 | debuglevel=0, path='/', followredirect=True, extraheaders={}, proxy=False): 87 | """ 88 | target: the hostname or ip of the target server 89 | port: defaults to 80 90 | ssl: defaults to false 91 | """ 92 | self.log = logging.getLogger('wafw00f') 93 | waftoolsengine.__init__(self, target, port, ssl, debuglevel, path, followredirect, extraheaders, proxy) 94 | self.knowledge = dict(generic=dict(found=False, reason=''), wafname=list()) 95 | 96 | def normalrequest(self, usecache=True, cacheresponse=True, headers=None): 97 | return self.request(usecache=usecache, cacheresponse=cacheresponse, headers=headers) 98 | 99 | def normalnonexistentfile(self, usecache=True, cacheresponse=True): 100 | path = self.path + str(random.randrange(1000, 9999)) + '.html' 101 | return self.request(path=path, usecache=usecache, cacheresponse=cacheresponse) 102 | 103 | def unknownmethod(self, usecache=True, cacheresponse=True): 104 | return self.request(method='OHYEA', usecache=usecache, cacheresponse=cacheresponse) 105 | 106 | def directorytraversal(self, usecache=True, cacheresponse=True): 107 | return self.request(path=self.path + self.dirtravstring, usecache=usecache, cacheresponse=cacheresponse) 108 | 109 | def invalidhost(self, usecache=True, cacheresponse=True): 110 | randomnumber = random.randrange(100000, 999999) 111 | return self.request(headers={'Host': str(randomnumber)}) 112 | 113 | def cleanhtmlencoded(self, usecache=True, cacheresponse=True): 114 | string = self.path + quote(self.cleanhtmlstring) + '.html' 115 | return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) 116 | 117 | def cleanhtml(self, usecache=True, cacheresponse=True): 118 | string = self.path + self.cleanhtmlstring + '.html' 119 | return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) 120 | 121 | def xssstandard(self, usecache=True, cacheresponse=True): 122 | xssstringa = self.path + self.xssstring + '.html' 123 | return self.request(path=xssstringa, usecache=usecache, cacheresponse=cacheresponse) 124 | 125 | def protectedfolder(self, usecache=True, cacheresponse=True): 126 | pfstring = self.path + self.AdminFolder 127 | return self.request(path=pfstring, usecache=usecache, cacheresponse=cacheresponse) 128 | 129 | def xssstandardencoded(self, usecache=True, cacheresponse=True): 130 | xssstringa = self.path + quote(self.xssstring) + '.html' 131 | return self.request(path=xssstringa, usecache=usecache, cacheresponse=cacheresponse) 132 | 133 | def cmddotexe(self, usecache=True, cacheresponse=True): 134 | # thanks j0e 135 | string = self.path + 'cmd.exe' 136 | return self.request(path=string, usecache=usecache, cacheresponse=cacheresponse) 137 | 138 | attacks = [cmddotexe, directorytraversal, xssstandard, protectedfolder, xssstandardencoded] 139 | 140 | def genericdetect(self, usecache=True, cacheresponse=True): 141 | knownflops = [ 142 | ('Microsoft-IIS/7.0','Microsoft-HTTPAPI/2.0'), 143 | ] 144 | reason = '' 145 | reasons = ['Blocking is being done at connection/packet level.', 146 | 'The server header is different when an attack is detected.', 147 | 'The server returned a different response code when a string trigged the blacklist.', 148 | 'It closed the connection for a normal request.', 149 | 'The connection header was scrambled.' 150 | ] 151 | # test if response for a path containing html tags with known evil strings 152 | # gives a different response from another containing invalid html tags 153 | try: 154 | cleanresponse, _tmp = self._perform_and_check(self.cleanhtml) 155 | xssresponse, _tmp = self._perform_and_check(self.xssstandard) 156 | if xssresponse.status != cleanresponse.status: 157 | self.log.info('Server returned a different response when a script tag was tried') 158 | reason = reasons[2] 159 | reason += '\r\n' 160 | reason += 'Normal response code is "%s",' % cleanresponse.status 161 | reason += ' while the response code to an attack is "%s"' % xssresponse.status 162 | self.knowledge['generic']['reason'] = reason 163 | self.knowledge['generic']['found'] = True 164 | return True 165 | cleanresponse, _tmp = self._perform_and_check(self.cleanhtmlencoded) 166 | xssresponse, _tmp = self._perform_and_check(self.xssstandardencoded) 167 | if xssresponse.status != cleanresponse.status: 168 | self.log.info('Server returned a different response when a script tag was tried') 169 | reason = reasons[2] 170 | reason += '\r\n' 171 | reason += 'Normal response code is "%s",' % cleanresponse.status 172 | reason += ' while the response code to an attack is "%s"' % xssresponse.status 173 | self.knowledge['generic']['reason'] = reason 174 | self.knowledge['generic']['found'] = True 175 | return True 176 | response, responsebody = self._perform_and_check(self.normalrequest) 177 | normalserver = response.getheader('Server') 178 | for attack in self.attacks: 179 | response, responsebody = self._perform_and_check(lambda: attack(self)) 180 | attackresponse_server = response.getheader('Server') 181 | if attackresponse_server: 182 | if attackresponse_server != normalserver: 183 | if (normalserver, attackresponse_server) in knownflops: 184 | return False 185 | self.log.info('Server header changed, WAF possibly detected') 186 | self.log.debug('attack response: %s' % attackresponse_server) 187 | self.log.debug('normal response: %s' % normalserver) 188 | reason = reasons[1] 189 | reason += '\r\nThe server header for a normal response is "%s",' % normalserver 190 | reason += ' while the server header a response to an attack is "%s.",' % attackresponse_server 191 | self.knowledge['generic']['reason'] = reason 192 | self.knowledge['generic']['found'] = True 193 | return True 194 | for attack in wafdetectionsprio: 195 | if self.wafdetections[attack](self) is None: 196 | self.knowledge['generic']['reason'] = reasons[0] 197 | self.knowledge['generic']['found'] = True 198 | return True 199 | for attack in self.attacks: 200 | response, responsebody = self._perform_and_check(lambda: attack(self)) 201 | for h, v in response.getheaders(): 202 | if scrambledheader(h): 203 | self.knowledge['generic']['reason'] = reasons[4] 204 | self.knowledge['generic']['found'] = True 205 | return True 206 | except RequestBlocked: 207 | self.knowledge['generic']['reason'] = reasons[0] 208 | self.knowledge['generic']['found'] = True 209 | return True 210 | 211 | return False 212 | 213 | def _perform_and_check(self, request_method): 214 | r = request_method() 215 | if r is None: 216 | raise RequestBlocked() 217 | 218 | return r 219 | 220 | def matchheader(self, headermatch, attack=False, ignorecase=True): 221 | import re 222 | 223 | detected = False 224 | header, match = headermatch 225 | if attack: 226 | requests = self.attacks 227 | else: 228 | requests = [self.normalrequest] 229 | for request in requests: 230 | r = request(self) 231 | if r is None: 232 | return 233 | response, responsebody = r 234 | headerval = response.getheader(header) 235 | if headerval: 236 | # set-cookie can have multiple headers, python gives it to us 237 | # concatinated with a comma 238 | if header == 'set-cookie': 239 | headervals = headerval.split(', ') 240 | else: 241 | headervals = [headerval] 242 | for headerval in headervals: 243 | if ignorecase: 244 | if re.match(match, headerval, re.IGNORECASE): 245 | detected = True 246 | break 247 | else: 248 | if re.match(match, headerval): 249 | detected = True 250 | break 251 | if detected: 252 | break 253 | return detected 254 | 255 | def matchcookie(self, match): 256 | """ 257 | a convenience function which calls matchheader 258 | """ 259 | return self.matchheader(('set-cookie', match)) 260 | 261 | wafdetections = dict() 262 | 263 | plugin_dict = load_plugins() 264 | result_dict = {} 265 | for plugin_module in plugin_dict.values(): 266 | wafdetections[plugin_module.NAME] = plugin_module.is_waf 267 | 268 | def identwaf(self, findall=False): 269 | detected = list() 270 | 271 | # Check for prioritized ones first, then check those added externally 272 | checklist = wafdetectionsprio 273 | checklist += list(set(self.wafdetections.keys()) - set(checklist)) 274 | 275 | for wafvendor in checklist: 276 | self.log.info('Checking for %s' % wafvendor) 277 | if self.wafdetections[wafvendor](self): 278 | detected.append(wafvendor) 279 | if not findall: 280 | break 281 | self.knowledge['wafname'] = detected 282 | return detected 283 | 284 | 285 | def calclogginglevel(verbosity): 286 | default = 40 # errors are printed out 287 | level = default - (verbosity * 10) 288 | if level < 0: 289 | level = 0 290 | return level 291 | 292 | 293 | def getheaders(fn): 294 | headers = {} 295 | fullfn = os.path.abspath(os.path.join(os.getcwd(), fn)) 296 | if not os.path.exists(fullfn): 297 | logging.getLogger('wafw00f').critical('Headers file "%s" does not exist!' % fullfn) 298 | return 299 | with open(fn, 'r') as f: 300 | for line in f.readlines(): 301 | _t = line.split(':', 2) 302 | if len(_t) == 2: 303 | h, v = map(lambda x: x.strip(), _t) 304 | headers[h] = v 305 | return headers 306 | 307 | 308 | def main(): 309 | print(lackofart) 310 | parser = OptionParser(usage='%prog url1 [url2 [url3 ... ]]\r\nexample: %prog http://www.victim.org/') 311 | parser.add_option('-v', '--verbose', action='count', dest='verbose', default=0, 312 | help='enable verbosity - multiple -v options increase verbosity') 313 | parser.add_option('-a', '--findall', action='store_true', dest='findall', default=False, 314 | help='Find all WAFs, do not stop testing on the first one') 315 | parser.add_option('-r', '--disableredirect', action='store_false', dest='followredirect', 316 | default=True, help='Do not follow redirections given by 3xx responses') 317 | parser.add_option('-t', '--test', dest='test', 318 | help='Test for one specific WAF') 319 | parser.add_option('-l', '--list', dest='list', action='store_true', 320 | default=False, help='List all WAFs that we are able to detect') 321 | parser.add_option('-p', '--proxy', dest='proxy', 322 | default=False, help='Use an HTTP proxy to perform requests, example: http://hostname:8080, socks5://hostname:1080') 323 | parser.add_option('--version', '-V', dest='version', action='store_true', 324 | default=False, help='Print out the version') 325 | parser.add_option('--headersfile', '-H', dest='headersfile', 326 | action='store', default=None, 327 | help='Pass custom headers, for example to overwrite the default User-Agent string') 328 | options, args = parser.parse_args() 329 | logging.basicConfig(level=calclogginglevel(options.verbose)) 330 | log = logging.getLogger() 331 | if options.list: 332 | print('Can test for these WAFs:\r\n') 333 | attacker = WafW00F(None) 334 | print('\r\n'.join(attacker.wafdetections.keys())) 335 | return 336 | if options.version: 337 | print('WAFW00F version %s' % __version__) 338 | return 339 | extraheaders = {} 340 | if options.headersfile: 341 | log.info('Getting extra headers from %s' % options.headersfile) 342 | extraheaders = getheaders(options.headersfile) 343 | if extraheaders is None: 344 | parser.error('Please provide a headers file with colon delimited header names and values') 345 | if len(args) == 0: 346 | parser.error('we need a target site') 347 | targets = args 348 | for target in targets: 349 | if not (target.startswith('http://') or target.startswith('https://')): 350 | log.info('The url %s should start with http:// or https:// .. fixing (might make this unusable)' % target) 351 | target = 'http://' + target 352 | print('Checking %s' % target) 353 | pret = oururlparse(target) 354 | if pret is None: 355 | log.critical('The url %s is not well formed' % target) 356 | sys.exit(1) 357 | (hostname, port, path, query, ssl) = pret 358 | log.info('starting wafw00f on %s' % target) 359 | attacker = WafW00F(hostname, port=port, ssl=ssl, 360 | debuglevel=options.verbose, path=path, 361 | followredirect=options.followredirect, 362 | extraheaders=extraheaders, 363 | proxy=options.proxy) 364 | if attacker.normalrequest() is None: 365 | log.error('Site %s appears to be down' % target) 366 | sys.exit(1) 367 | if options.test: 368 | if options.test in attacker.wafdetections: 369 | waf = attacker.wafdetections[options.test](attacker) 370 | if waf: 371 | print('The site %s is behind a %s' % (target, options.test)) 372 | else: 373 | print('WAF %s was not detected on %s' % (options.test, target)) 374 | else: 375 | print( 376 | 'WAF %s was not found in our list\r\nUse the --list option to see what is available' % options.test) 377 | return 378 | waf = attacker.identwaf(options.findall) 379 | log.info('Ident WAF: %s' % waf) 380 | if len(waf) > 0: 381 | print('The site %s is behind a %s' % (target, ' and/or '.join(waf))) 382 | if (options.findall) or len(waf) == 0: 383 | print('Generic Detection results:') 384 | if attacker.genericdetect(): 385 | log.info('Generic Detection: %s' % attacker.knowledge['generic']['reason']) 386 | print('The site %s seems to be behind a WAF or some sort of security solution' % target) 387 | print('Reason: %s' % attacker.knowledge['generic']['reason']) 388 | else: 389 | print('No WAF detected by the generic detection') 390 | print('Number of requests: %s' % attacker.requestnumber) 391 | 392 | 393 | class RequestBlocked(Exception): 394 | pass 395 | 396 | 397 | if __name__ == '__main__': 398 | if sys.hexversion < 0x2060000: 399 | sys.stderr.write('Your version of python is way too old .. please update to 2.6 or later\r\n') 400 | main() 401 | -------------------------------------------------------------------------------- /wafw00f/manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | import os 5 | from functools import partial 6 | 7 | from pluginbase import PluginBase 8 | 9 | 10 | def load_plugins(): 11 | here = os.path.abspath(os.path.dirname(__file__)) 12 | get_path = partial(os.path.join, here) 13 | plugin_dir = get_path('plugins') 14 | 15 | plugin_base = PluginBase( 16 | package='wafw00f.plugins', searchpath=[plugin_dir] 17 | ) 18 | plugin_source = plugin_base.make_plugin_source( 19 | searchpath=[plugin_dir], persist=True 20 | ) 21 | 22 | plugin_dict = {} 23 | for plugin_name in plugin_source.list_plugins(): 24 | plugin_dict[plugin_name] = plugin_source.load_plugin(plugin_name) 25 | 26 | return plugin_dict 27 | -------------------------------------------------------------------------------- /wafw00f/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0tmi1k/wafw00f/0e3f47946ea3a0c80823dda408ba936318eccbc1/wafw00f/plugins/__init__.py -------------------------------------------------------------------------------- /wafw00f/plugins/airlock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'InfoGuard Airlock' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | return self.matchcookie('^AL[_-]?(SESS|LB)=') 10 | -------------------------------------------------------------------------------- /wafw00f/plugins/anquanbao.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Anquanbao' 5 | 6 | 7 | def is_waf(self): 8 | return self.matchheader(('X-Powered-By-Anquanbao', '.+')) 9 | -------------------------------------------------------------------------------- /wafw00f/plugins/barracuda.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Barracuda Application Firewall' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | if self.matchcookie('^barra_counter_session='): 10 | return True 11 | # credit goes to Charlie Campbell 12 | if self.matchcookie('^BNI__BARRACUDA_LB_COOKIE='): 13 | return True 14 | # credit goes to yours truly 15 | if self.matchcookie('^BNI_persistence='): 16 | return True 17 | if self.matchcookie('^BN[IE]S_.*?='): 18 | return True 19 | return False 20 | -------------------------------------------------------------------------------- /wafw00f/plugins/betterwpsecurity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Better WP Security' 5 | 6 | 7 | def is_waf(self): 8 | r = self.normalrequest() 9 | 10 | if r is None: 11 | return False 12 | 13 | normalresponse, _ = r 14 | 15 | link_header = normalresponse.getheader('Link') or "" 16 | 17 | if "https://api.w.org/" not in link_header: 18 | # Does not appear to be a wordpress at all 19 | return False 20 | 21 | r = self.request("GET", self.path + "wp-content/plugins/better-wp-security/") 22 | 23 | if r is None: 24 | return False 25 | 26 | pluginresponse, _ = r 27 | 28 | return pluginresponse.status == 200 29 | -------------------------------------------------------------------------------- /wafw00f/plugins/binarysec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'BinarySec' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | if self.matchheader(('server', 'BinarySec')): 10 | return True 11 | # the following based on nmap's http-waf-fingerprint.nse 12 | elif self.matchheader(('x-binarysec-via', '.')): 13 | return True 14 | # the following based on nmap's http-waf-fingerprint.nse 15 | elif self.matchheader(('x-binarysec-nocache', '.')): 16 | return True 17 | else: 18 | return False 19 | -------------------------------------------------------------------------------- /wafw00f/plugins/blockdos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'BlockDoS' 4 | 5 | def is_waf(self): 6 | return self.matchheader(('server', "BlockDos\.net")) 7 | 8 | -------------------------------------------------------------------------------- /wafw00f/plugins/chinacache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'ChinaCache-CDN' 5 | 6 | 7 | def is_waf(self): 8 | return self.matchheader(('Powered-By-ChinaCache', '.+')) 9 | -------------------------------------------------------------------------------- /wafw00f/plugins/ciscoacexml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Cisco ACE XML Gateway' 5 | 6 | 7 | def is_waf(self): 8 | if self.matchheader(('server', 'ACE XML Gateway')): 9 | return True 10 | return False 11 | -------------------------------------------------------------------------------- /wafw00f/plugins/cloudflare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'CloudFlare' 5 | 6 | 7 | # the following based on nmap's http-waf-fingerprint.nse 8 | def is_waf(self): 9 | if self.matchheader(('server', 'cloudflare-nginx')): 10 | return True 11 | if self.matchcookie('__cfduid'): 12 | return True 13 | return False 14 | -------------------------------------------------------------------------------- /wafw00f/plugins/comodo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'Comodo WAF' 4 | 5 | def is_waf(self): 6 | return self.matchheader(('server', "Protected by COMODO WAF")) 7 | 8 | -------------------------------------------------------------------------------- /wafw00f/plugins/denyall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'DenyALL WAF' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | if self.matchcookie('^sessioncookie='): 10 | return True 11 | # credit goes to Sebastien Gioria 12 | # Tested against a Rweb 3.8 13 | # and modified by sandro gauci and someone else 14 | for attack in self.attacks: 15 | r = attack(self) 16 | if r is None: 17 | return 18 | response, responsebody = r 19 | if response.status == 200: 20 | if response.reason == 'Condition Intercepted': 21 | return True 22 | return False 23 | -------------------------------------------------------------------------------- /wafw00f/plugins/dotdefender.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Applicure dotDefender' 5 | 6 | 7 | def is_waf(self): 8 | # thanks to j0e 9 | return self.matchheader(['X-dotDefender-denied', '^1$'], attack=True) 10 | -------------------------------------------------------------------------------- /wafw00f/plugins/edgecast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'Edgecast / Verizon Digital media' 4 | 5 | def is_waf(self): 6 | return self.matchheader(('Server', '^ECD \(.*?\)$')) or self.matchheader(('Server', '^ECS \(.*?\)$')) 7 | -------------------------------------------------------------------------------- /wafw00f/plugins/f5bigipapm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'F5 BIG-IP APM' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | # the following based on nmap's http-waf-fingerprint.nse 10 | if self.matchcookie('^LastMRH_Session') and self.matchcookie('^MRHSession'): 11 | return True 12 | elif self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')) and self.matchcookie('^MRHSession'): 13 | return True 14 | if self.matchheader(('Location', '\/my.policy')) and self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): 15 | return True 16 | elif self.matchheader(('Location', '\/my\.logout\.php3')) and self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): 17 | return True 18 | elif self.matchheader(('Location', '.+\/f5\-w\-68747470.+')) and self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): 19 | return True 20 | elif self.matchheader(('server', 'BigIP|BIG-IP|BIGIP')): 21 | return True 22 | elif self.matchcookie('^F5_fullWT') or self.matchcookie('^F5_ST') or self.matchcookie('^F5_HT_shrinked'): 23 | return True 24 | elif self.matchcookie('^MRHSequence') or self.matchcookie('^MRHSHint') or self.matchcookie('^LastMRH_Session'): 25 | return True 26 | else: 27 | return False 28 | -------------------------------------------------------------------------------- /wafw00f/plugins/f5bigipasm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'F5 BIG-IP ASM' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | return self.matchcookie('^TS[a-zA-Z0-9]{3,8}=') 10 | -------------------------------------------------------------------------------- /wafw00f/plugins/f5bigipltm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'F5 BIG-IP LTM' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | if self.matchcookie('^BIGipServer'): 10 | return True 11 | elif self.matchheader(('X-Cnection', '^close$'), attack=True): 12 | return True 13 | else: 14 | return False 15 | -------------------------------------------------------------------------------- /wafw00f/plugins/f5firepass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'F5 FirePass' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | if self.matchheader(('Location', '\/my\.logon\.php3')) and self.matchcookie('^VHOST'): 10 | return True 11 | elif self.matchcookie('^MRHSession') and (self.matchcookie('^VHOST') or self.matchcookie('^uRoamTestCookie')): 12 | return True 13 | elif self.matchcookie('^MRHSession') and (self.matchcookie('^MRHCId') or self.matchcookie('^MRHIntranetSession')): 14 | return True 15 | elif self.matchcookie('^uRoamTestCookie') or self.matchcookie('^VHOST'): 16 | return True 17 | else: 18 | return False 19 | -------------------------------------------------------------------------------- /wafw00f/plugins/f5trafficshield.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'F5 Trafficshield' 5 | 6 | 7 | def is_waf(self): 8 | for hv in [['cookie', '^ASINFO='], ['server', 'F5-TrafficShield']]: 9 | r = self.matchheader(hv) 10 | if r is None: 11 | return 12 | elif r: 13 | return r 14 | # the following based on nmap's http-waf-fingerprint.nse 15 | if self.matchheader(('server', 'F5-TrafficShield')): 16 | return True 17 | return False 18 | -------------------------------------------------------------------------------- /wafw00f/plugins/fortiweb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'FortiWeb' 4 | 5 | def is_waf(self): 6 | return self.matchcookie('FORTIWAFSID=') 7 | -------------------------------------------------------------------------------- /wafw00f/plugins/hyperguard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Art of Defence HyperGuard' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | return self.matchcookie('^WODSESSION=') 10 | -------------------------------------------------------------------------------- /wafw00f/plugins/ibm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'IBM Web Application Security' 5 | 6 | 7 | def is_waf(self): 8 | normal = self.normalrequest() 9 | protected = self.protectedfolder() 10 | 11 | return protected is None and normal is not None 12 | -------------------------------------------------------------------------------- /wafw00f/plugins/ibmdatapower.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'IBM DataPower' 5 | 6 | 7 | def is_waf(self): 8 | # Added by Mathieu Dessus 9 | detected = False 10 | if self.matchheader(('X-Backside-Transport', '^(OK|FAIL)')): 11 | detected = True 12 | return detected 13 | -------------------------------------------------------------------------------- /wafw00f/plugins/imperva.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Imperva SecureSphere' 5 | 6 | 7 | def is_waf(self): 8 | # thanks to Mathieu Dessus for this 9 | # might lead to false positives so please report back to sandro@enablesecurity.com 10 | for attack in self.attacks: 11 | r = attack(self) 12 | if r is None: 13 | return 14 | response, responsebody = r 15 | if response.version == 10: 16 | return True 17 | return False 18 | -------------------------------------------------------------------------------- /wafw00f/plugins/incapsula.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Incapsula WAF' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to Charlie Campbell 9 | if self.matchcookie('^.incap_ses'): 10 | return True 11 | if self.matchcookie('^visid.*='): 12 | return True 13 | return False 14 | -------------------------------------------------------------------------------- /wafw00f/plugins/isaserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Microsoft ISA Server' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | r = self.invalidhost() 10 | if r is None: 11 | return 12 | response, responsebody = r 13 | if response.reason in self.isaservermatch: 14 | detected = True 15 | return detected 16 | -------------------------------------------------------------------------------- /wafw00f/plugins/missioncontrol.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Mission Control Application Shield' 5 | 6 | 7 | def is_waf(self): 8 | if self.matchheader(('server', 'Mission Control Application Shield')): 9 | return True 10 | return False 11 | -------------------------------------------------------------------------------- /wafw00f/plugins/modsecurity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Trustwave ModSecurity' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | for attack in self.attacks: 10 | r = attack(self) 11 | if r is None: 12 | return 13 | response, responsebody = r 14 | if response.status == 501: 15 | detected = True 16 | break 17 | # the following based on nmap's http-waf-fingerprint.nse 18 | if self.matchheader(('server', '(mod_security|Mod_Security|NOYB)')): 19 | return True 20 | return detected 21 | -------------------------------------------------------------------------------- /wafw00f/plugins/modsecuritycrs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'ModSecurity (OWASP CRS)' 5 | 6 | 7 | def is_waf(self): 8 | r = self.request('GET', self.path + '?id=' + self.xssstring) 9 | normal = self.normalrequest() 10 | 11 | if r is None or normal is None: 12 | return False 13 | 14 | response, _ = r 15 | normalresponse, _ = normal 16 | 17 | return normalresponse.status != response.status 18 | -------------------------------------------------------------------------------- /wafw00f/plugins/naxsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'Naxsi' 4 | 5 | def is_waf(self): 6 | return self.matchheader(('X-Data-Origin', '^naxsi')) 7 | -------------------------------------------------------------------------------- /wafw00f/plugins/netcontinuum.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'NetContinuum' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | return self.matchcookie('^NCI__SessionId=') 10 | -------------------------------------------------------------------------------- /wafw00f/plugins/netscaler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Citrix NetScaler' 5 | 6 | 7 | def is_waf(self): 8 | """ 9 | First checks if a cookie associated with Netscaler is present, 10 | if not it will try to find if a "Cneonction" or "nnCoection" is returned 11 | for any of the attacks sent 12 | """ 13 | # NSC_ and citrix_ns_id come from David S. Langlands 14 | if self.matchcookie('^(ns_af=|citrix_ns_id|NSC_)'): 15 | return True 16 | if self.matchheader(('Cneonction', 'close'), attack=True): 17 | return True 18 | if self.matchheader(('nnCoection', 'close'), attack=True): 19 | return True 20 | if self.matchheader(('Via', 'NS-CACHE'), attack=True): 21 | return True 22 | if self.matchheader(('x-client-ip', '.'), attack=True): 23 | return True 24 | if self.matchheader(('Location', '\/vpn\/index\.html')): 25 | return True 26 | if self.matchcookie('^pwcount'): 27 | return True 28 | return False 29 | -------------------------------------------------------------------------------- /wafw00f/plugins/nevisproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'AdNovum nevisProxy' 4 | 5 | def is_waf(self): 6 | # credit goes to an anonymous reporter 7 | if self.matchcookie('^Navajo.*?$'): 8 | return True 9 | return False -------------------------------------------------------------------------------- /wafw00f/plugins/nsfocus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'NSFocus' 5 | 6 | 7 | def is_waf(self): 8 | if self.matchheader(('server', 'NSFocus')): 9 | return True 10 | return False 11 | -------------------------------------------------------------------------------- /wafw00f/plugins/powercdn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'PowerCDN' 5 | 6 | 7 | def is_waf(self): 8 | return self.matchheader(('PowerCDN', '.+')) 9 | -------------------------------------------------------------------------------- /wafw00f/plugins/profense.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Profense' 5 | 6 | 7 | def is_waf(self): 8 | """ 9 | Checks for server headers containing "profense" 10 | """ 11 | return self.matchheader(('server', 'profense')) 12 | -------------------------------------------------------------------------------- /wafw00f/plugins/radware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'Radware AppWall' 4 | 5 | def is_waf(self): 6 | return self.matchheader(('X-SL-CompState', '.')) 7 | -------------------------------------------------------------------------------- /wafw00f/plugins/safedog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Safedog' 5 | 6 | 7 | def is_waf(self): 8 | if self.matchcookie('^safedog-flow-item='): 9 | return True 10 | if self.matchheader(('server', '^Safedog')): 11 | return True 12 | if self.matchheader(('x-powered-by', '^WAF/\d\.\d')): 13 | return True 14 | return False 15 | -------------------------------------------------------------------------------- /wafw00f/plugins/secureiis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'eEye Digital Security SecureIIS' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | detected = False 10 | r = self.normalrequest() 11 | if r is None: 12 | return 13 | response, responsebody = r 14 | if response.status == 404: 15 | return 16 | headers = dict() 17 | headers['Transfer-Encoding'] = 'z' * 1025 18 | r = self.normalrequest(headers=headers) 19 | if r is None: 20 | return 21 | response, responsebody = r 22 | if response.status == 404: 23 | detected = True 24 | return detected 25 | -------------------------------------------------------------------------------- /wafw00f/plugins/sucuri.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'Sucuri WAF' 4 | 5 | def is_waf(self): 6 | return self.matchheader(('X-Sucuri-ID', '.+')) 7 | -------------------------------------------------------------------------------- /wafw00f/plugins/teros.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Teros WAF' 5 | 6 | 7 | def is_waf(self): 8 | # credit goes to W3AF 9 | return self.matchcookie('^st8id=') 10 | -------------------------------------------------------------------------------- /wafw00f/plugins/urlscan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Microsoft URLScan' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | testheaders = dict() 10 | testheaders['Translate'] = 'z' * 10 11 | testheaders['If'] = 'z' * 10 12 | testheaders['Lock-Token'] = 'z' * 10 13 | testheaders['Transfer-Encoding'] = 'z' * 10 14 | r = self.normalrequest() 15 | if r is None: 16 | return 17 | response, _tmp = r 18 | r = self.normalrequest(headers=testheaders) 19 | if r is None: 20 | return 21 | response2, _tmp = r 22 | if response.status != response2.status: 23 | if response2.status == 404: 24 | detected = True 25 | return detected 26 | -------------------------------------------------------------------------------- /wafw00f/plugins/uspses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'USP Secure Entry Server' 5 | 6 | 7 | def is_waf(self): 8 | if self.matchheader(('server', 'Secure Entry Server')): 9 | return True 10 | return False 11 | -------------------------------------------------------------------------------- /wafw00f/plugins/wallarm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | NAME = 'Wallarm' 4 | 5 | def is_waf(self): 6 | return self.matchheader(('server', "nginx-wallarm")) 7 | 8 | -------------------------------------------------------------------------------- /wafw00f/plugins/webknight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Aqtronix WebKnight' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | for attack in self.attacks: 10 | r = attack(self) 11 | if r is None: 12 | return 13 | response, responsebody = r 14 | if response.status == 999: 15 | detected = True 16 | break 17 | return detected 18 | -------------------------------------------------------------------------------- /wafw00f/plugins/webscurity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'Juniper WebApp Secure' 5 | 6 | 7 | def is_waf(self): 8 | detected = False 9 | r = self.normalrequest() 10 | if r is None: 11 | return 12 | response, responsebody = r 13 | if response.status == 403: 14 | return detected 15 | newpath = self.path + '?nx=@@' 16 | r = self.request(path=newpath) 17 | if r is None: 18 | return 19 | response, responsebody = r 20 | if response.status == 403: 21 | detected = True 22 | return detected 23 | -------------------------------------------------------------------------------- /wafw00f/plugins/west263cdn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = 'West263CDN' 5 | 6 | 7 | def is_waf(self): 8 | return self.matchheader(('X-Cache', '.+WT263CDN-.+')) 9 | -------------------------------------------------------------------------------- /wafw00f/plugins/wzb360.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | NAME = '360WangZhanBao' 5 | 6 | 7 | def is_waf(self): 8 | return self.matchheader(('X-Powered-By-360WZB', '.+')) 9 | -------------------------------------------------------------------------------- /wafw00f/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0tmi1k/wafw00f/0e3f47946ea3a0c80823dda408ba936318eccbc1/wafw00f/tests/__init__.py -------------------------------------------------------------------------------- /wafw00f/tests/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | from unittest import TestCase 5 | 6 | import httpretty 7 | 8 | from wafw00f.main import WafW00F 9 | 10 | 11 | class WafW00FTestCase(TestCase): 12 | 13 | def test_is360wzb(self): 14 | """ 15 | :: 16 | 17 | $ curl -I http://www.58dm.com/ 18 | HTTP/1.1 200 OK 19 | Server: nginx/1.4.3.6 20 | Date: Tue, 10 Jun 2014 12:42:21 GMT 21 | Content-Type: text/html 22 | Connection: keep-alive 23 | X-Powered-By-360WZB: wangzhan.360.cn 24 | Content-Location: http://www.58dm.com/index.html 25 | Last-Modified: Tue, 10 Jun 2014 04:10:42 GMT 26 | ETag: "6a8d27f26184cf1:daa" 27 | VAR-Cache: HIT 28 | Accept-Ranges: bytes 29 | cache-control: max-age=7200 30 | age: 0 31 | """ 32 | self.__assert_waf('www.58dm.com', '360WangZhanBao', {'X-Powered-By-360WZB': 'wangzhan.360.cn'}) 33 | 34 | def test_isanquanbao(self): 35 | """ 36 | :: 37 | 38 | $ curl -I http://www.51cdz.com/ 39 | HTTP/1.1 200 OK 40 | Server: ASERVER/1.2.9-3 41 | Date: Tue, 10 Jun 2014 12:41:42 GMT 42 | Content-Type: text/html 43 | Content-Length: 76789 44 | Connection: keep-alive 45 | Content-Location: http://www.51cdz.com/index.html 46 | Last-Modified: Sun, 01 Jun 2014 15:39:34 GMT 47 | Accept-Ranges: bytes 48 | ETag: "766fe9afaf7dcf1:41fc" 49 | X-Powered-By-Anquanbao: MISS from chn-tj-ht-se2 50 | """ 51 | self.__assert_waf('www.51cdz.com', 'Anquanbao', {'X-Powered-By-Anquanbao': 'MISS from chn-tj-ht-se2'}) 52 | 53 | def test_ischinacache(self): 54 | """ 55 | :: 56 | 57 | $ curl -I http://s1.meituan.net/ 58 | HTTP/1.0 404 Not Found 59 | Server: Tengine 60 | Date: Tue, 10 Jun 2014 12:40:19 GMT 61 | Content-Type: text/html; charset=iso-8859-1 62 | Timing-Allow-Origin: * 63 | Powered-By-ChinaCache: MISS from 0100102396 64 | X-Cache: MISS from DXT-BJ-104 65 | X-Cache-Lookup: MISS from DXT-BJ-104:80 66 | X-Cache: MISS from DXT-BJ-219 67 | X-Cache-Lookup: MISS from DXT-BJ-219:80 68 | Connection: close 69 | """ 70 | self.__assert_waf('s1.meituan.net', 'ChinaCache-CDN', {'Powered-By-ChinaCache': 'fake'}) 71 | 72 | def test_ispowercdn(self): 73 | """ 74 | :: 75 | 76 | $ curl -I http://www.jjwxc.net/ 77 | HTTP/1.1 200 OK 78 | Date: Tue, 10 Jun 2014 01:17:09 GMT 79 | Content-Type: text/html 80 | Last-Modified: Mon, 09 Jun 2014 17:30:04 GMT 81 | Content-Encoding: gzip 82 | X-Cache: HIT from BGP-1-233-ZZ-JJCDN 83 | X-Cache-Lookup: HIT from BGP-1-233-ZZ-JJCDN:80 84 | Age: 6052 85 | PowerCDN: HIT from ak244.powercdn.com 86 | Via: 1.1 ak244.powercdn.com (PowerCDN/2.4) 87 | Connection: keep-alive 88 | """ 89 | self.__assert_waf('www.jjwxc.net', 'PowerCDN', {'PowerCDN': 'HIT from ak244.powercdn.com'}) 90 | 91 | def test_iswest263cdn(self): 92 | """ 93 | :: 94 | 95 | $ curl -I http://hsht.hs3w.com/ 96 | HTTP/1.0 400 Bad Request 97 | Content-Length: 39 98 | Content-Type: text/html 99 | Date: Tue, 10 Jun 2014 12:33:50 GMT 100 | X-Cache: MISS from WT263CDN-1231786 101 | X-Cache-Lookup: MISS from WT263CDN-1231786:80 102 | X-Cache: MISS from test.abc.com 103 | X-Cache-Lookup: MISS from test.abc.com:80 104 | Via: 1.0 WT263CDN-1231786 (squid/3.0.STABLE20), 1.0 test.abc.com (squid/3.0.STABLE20) 105 | Connection: close 106 | """ 107 | self.__assert_waf('hsht.hs3w.com', 'West263CDN', {'X-Cache': 'MISS from WT263CDN-1231786'}) 108 | 109 | @httpretty.activate 110 | def __assert_waf(self, host, vendor, fake_headers): 111 | httpretty.register_uri(httpretty.GET, 'http://%s/' % host, body='fake text', adding_headers=fake_headers) 112 | attacker = WafW00F(host) 113 | waf = attacker.wafdetections[vendor](attacker) 114 | self.assertTrue(waf) 115 | -------------------------------------------------------------------------------- /wafw00f/wafprio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # NOTE: this priority list is used so that each check can be prioritized, 4 | # so that the quick checks are done first and ones that require more 5 | # requests, are done later 6 | 7 | 8 | wafdetectionsprio = [ 9 | 'Profense', 10 | 'AdNovum nevisProxy', 11 | 'NetContinuum', 12 | 'Incapsula WAF', 13 | 'CloudFlare', 14 | 'NSFocus', 15 | 'Sucuri WAF', 16 | 'Edgecast / Verizon Digital media', 17 | 'Comodo WAF', 18 | 'FortiWeb', 19 | 'Wallarm', 20 | 'BlockDoS', 21 | 'Radware AppWall', 22 | 'Naxsi', 23 | 'Safedog', 24 | 'Mission Control Application Shield', 25 | 'USP Secure Entry Server', 26 | 'Cisco ACE XML Gateway', 27 | 'Barracuda Application Firewall', 28 | 'Art of Defence HyperGuard', 29 | 'BinarySec', 30 | 'Teros WAF', 31 | 'F5 BIG-IP LTM', 32 | 'F5 BIG-IP APM', 33 | 'F5 BIG-IP ASM', 34 | 'F5 FirePass', 35 | 'F5 Trafficshield', 36 | 'InfoGuard Airlock', 37 | 'Citrix NetScaler', 38 | 'Trustwave ModSecurity', 39 | 'IBM Web Application Security', 40 | 'IBM DataPower', 'DenyALL WAF', 41 | 'Applicure dotDefender', 42 | 'Juniper WebApp Secure', 43 | 'Microsoft URLScan', 44 | 'Aqtronix WebKnight', 45 | 'eEye Digital Security SecureIIS', 46 | 'Imperva SecureSphere', 47 | 'Microsoft ISA Server' 48 | ] 49 | --------------------------------------------------------------------------------