diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..d32665ea --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 80 +select = C,E,F,W,B,B950 +ignore = E203, E501, W503, E402, F401, W504 diff --git a/.travis.yml b/.travis.yml index 6eda7d46..c8811bd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,10 @@ install: - python setup.py install script: - - flake8 --ignore=E501,E402,F401,W504 src tests + - flake8 src tests - coverage run --append -m unittest discover -v -s tests/ - if [[ $TRAVIS_PYTHON_VERSION == '3.6' && $TRAVIS_BRANCH == 'master' ]]; then codecov; fi + - if [[ $TRAVIS_PYTHON_VERSION != '3.4' && $TRAVIS_PYTHON_VERSION != '3.5' ]]; then black --check src tests; fi deploy: provider: pypi user: x4vi_mendez diff --git a/Makefile b/Makefile index b9aa971b..4069e782 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ test: pip install tox tox --recreate flake8: - pip install flake8 - flake8 --ignore=E501,E402,F401,W504 src tests + black --check src tests + flake8 src tests publish: pip install 'twine>=1.5.0' python setup.py sdist @@ -21,7 +21,7 @@ docs: cd docs && make html coverage: - coverage report --skip-covered --include "*python3.5/site-packages/wfuzz*" -m + coverage report --skip-covered --include "*python3.8/site-packages/wfuzz*" -m install: install-dev pip install -r requirements.txt diff --git a/docs/conf.py b/docs/conf.py index b921f709..68a362ac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -93,7 +93,8 @@ 'github_user': 'xmendez', 'github_repo': 'wfuzz', 'github_banner': True, - 'show_related': False + 'show_related': False, + 'sidebar_collapse': True, } # Add any paths that contain custom static files (such as style sheets) here, @@ -109,7 +110,11 @@ # Custom sidebar templates, maps document names to template names. html_sidebars = { - '**': ['sidebarlogo.html', 'globaltoc.html'] + '**': [ + 'sidebarlogo.html', + 'navigation.html', + 'searchbox.html' + ] } # -- Options for LaTeX output --------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 9582fdec..04c2c243 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,9 +18,10 @@ Wfuzz: The Web fuzzer .. image:: https://codecov.io/github/xmendez/wfuzz/coverage.svg?branch=master :target: https://codecov.io/github/xmendez/wfuzz -Wfuzz supports Python 3. The use of **Python 3** is preferred (and faster) over Python 2. +Wfuzz provides a framework to automate web applications security assessments and could help you to secure your web applications by finding and exploiting web application vulnerabilities. -See Wfuzz in action: +See Wfuzz in action +------------------- * Wfuzz cli:: @@ -78,33 +79,46 @@ other tools included in the wfuzz framework. $ wfencode -e md5 test 098f6bcd4621d373cade4e832627b4f6 -Wfuzz has been created to facilitate the task in web applications assessments and it is based on a simple concept: it replaces any reference to the FUZZ keyword by the value of a given payload. + +How it works +------------ + +Wfuzz it is based on a simple concept: it replaces any reference to the FUZZ keyword by the value of a given payload. A payload in Wfuzz is a source of data. This simple concept allows any input to be injected in any field of an HTTP request, allowing to perform complex web security attacks in different web application components such as: parameters, authentication, forms, directories/files, headers, etc. -Wfuzz is more than a web content scanner: +Wfuzz is more than a web brute forcer: -- Wfuzz could help you to secure your web applications by finding and exploiting web application vulnerabilities. Wfuzz's web application vulnerability scanner is supported by plugins. +- Wfuzz's web application vulnerability scanner is supported by plugins. - Wfuzz is a completely modular framework and makes it easy for even the newest of Python developers to contribute. Building plugins is simple and takes little more than a few minutes. - Wfuzz exposes a simple language interface to the previous HTTP requests/responses performed using Wfuzz or other tools, such as Burp. This allows you to perform manual and semi-automatic tests with full context and understanding of your actions, without relying on a web application scanner underlying implementation. -User Guide -================== +Installation Guide +================== .. toctree:: - :maxdepth: 2 + :maxdepth: 4 + + user/installation + user/breaking + +User Guide +================== + +.. toctree:: + :maxdepth: 4 - user/installation - user/getting - user/basicusage - user/advanced + user/getting + user/basicusage + user/advanced + user/wfpayload Library Guide ================== .. toctree:: - :maxdepth: 2 + :maxdepth: 4 library/guide diff --git a/docs/library/guide.rst b/docs/library/guide.rst index e5f157c9..65387f63 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -1,6 +1,11 @@ -Library Options +Python library =============== +Wfuzz's Python library allows to automate tasks and integrate Wfuzz into new tools or scripts. + +Library Options +--------------- + All options that are available within the Wfuzz command line interface are available as library options: ======================== ===================================================================================== @@ -10,7 +15,7 @@ CLI Option Library Option --recipe recipe=["filename"] --oF save="filename" -f filename,printer printer=("filename", "printer") ---dry-run dryrun=True +--dry-run transport="dryrun" -p addr proxies=[("ip","port","type")] -t N concurrent=N -s N delay=0.0 @@ -30,7 +35,7 @@ CLI Option Library Option --sc/sl/sw/sh N[,N]+ sc/sl/sw/sh=[N,N] --ss/hs regex ss/hs="regex" --filter filter="filter exp" ---prefilter prefilter="prefilter exp" +--prefilter prefilter=["prefilter exp"] -b cookie cookie=["cookie1=value1",] -d postdata postdata="postdata" -H header headers=[("header1", "value1"),] @@ -40,7 +45,7 @@ CLI Option Library Option These options can be used in the main library interfaces: fuzz, payload or session indistinctly. Fuzzing a URL -============= +------------- Fuzzing a URL with wfuzz library is very simple. Firstly, import the wfuzz module:: @@ -61,7 +66,7 @@ Now, let's try to fuzz a web page to look for hidden content, such as directorie Now, we have a FuzzResult object called r. We can get all the information we need from this object. FuzzSession object -================== +------------------ A FuzzSession object has all the methods of the main wfuzz API. @@ -87,7 +92,7 @@ FuzzSession can also be used as context manager:: Get payload -=========== +----------- The get_payload function generates a Wfuzz payload from a Python iterable. It is a quick and flexible way of getting a payload programmatically without using Wfuzz payloads plugins. @@ -123,7 +128,7 @@ The get_payloads method can be used when various payloads are needed:: 00019: C=404 7 L 12 W 168 Ch "0 - a" Get session -=========== +----------- The get_session function generates a Wfuzz session object from the specified command line. It is a quick way of getting a payload programmatically from a string representing CLI options:: @@ -146,7 +151,7 @@ The get_session function generates a Wfuzz session object from the specified com 00010: C=404 7 L 12 W 168 Ch "9" Interacting with the results -============================ +---------------------------- Once a Wfuzz result is available the grammar defined in the filter language can be used to work with the results' values. For example:: diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 9db3a4dd..f17dd608 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -490,7 +490,8 @@ FuzzRequest object's attribute (you need to use the r. prefix) such as: ============================ ============================================= Name Description ============================ ============================================= -url HTTP request's value +url HTTP request's url +urlp HTTP request's parsed url (see section below). method HTTP request's verb scheme HTTP request's scheme host HTTP request's host @@ -504,8 +505,8 @@ cookies.response.<> Specified HTTP response cookie headers.all All HTTP request and response headers headers.request HTTP request headers headers.response HTTP response headers -headers.request.<> Specified HTTP request given header -headers.response.<> Specified HTTP response given header +headers.request.<> Specified HTTP request header case insensitive +headers.response.<> Specified HTTP response header insensitive params.all All HTTP request GET and POST parameters params.get All HTTP request GET parameters params.post HTTP request POST parameters in returned as a dictionary @@ -597,18 +598,18 @@ Results with plugin issues can be filter as well:: $ wfuzz -z list --zD index -u http://testphp.vulnweb.com/FUZZ.php --script headers --filter "plugins~'nginx'" -Filtering a payload +Payload mangling ^^^^^^^^^^^^^^^^^^^^^^^^^^ -Slice +Slicing a payload """"""" -The --slice command line parameter in conjunction with the described filter language allows you to filter a payload. +The --slice command line parameter in conjunction with the described language allows you to filter a payload. The payload to filter, specified by the -z switch must precede --slice command line parameter. -An example is shown below:: +The specified expression must return a boolean value, an example, using the unique operator is shown below:: - $ wfuzz-cli.py -z list,one-two-one-one --slice "FUZZ|u()" http://localhost:9000/FUZZ + $ wfuzz-cli.py -z list --zD one-two-one-one --slice "FUZZ|u()" http://localhost:9000/FUZZ ******************************************************** * Wfuzz 2.2 - The Web Fuzzer * @@ -629,9 +630,22 @@ An example is shown below:: Filtered Requests: 0 Requests/sec.: 62.85908 -It is worth noting that the type of payload dictates the available language symbols. For example, a dictionary payload such as the one in the example +It is worth noting that, the type of payload dictates the available language symbols. For example, a dictionary payload such as in the example above does not have a full FuzzResult object context and therefore object fields cannot be used. +Re-writing a payload +""""""" + +The slice command parameter also allows to re-write a payload. Any value, other than a boolean, returned by the +specified expression will be interpreted not to filter the source payload but to change its value. + +For example:: + + $ ./wfuzz -z list --zD one-two-three --slice "FUZZ|upper()" -u https://www.wfuzz.io/FUZZ + 000000001: 404 11 L 72 W 1560 Ch "ONE" + 000000003: 404 11 L 72 W 1562 Ch "THREE" + 000000002: 404 11 L 72 W 1560 Ch "TWO" + Prefilter """"""""" @@ -640,6 +654,8 @@ performed just before any HTTP request is done. In this context you are filtering a FuzzResult object, which is the result of combining all the input payloads, that is has not been updated with the result of performing its associated HTTP request yet and therefore lacking some information. +The --prefilter command cannot be used to re-write a payload. The assignment operators can be used to modify the FuzzResult object's fields but expressions other booleans will be ignored. + Reutilising previous results -------------------------------------- @@ -724,7 +740,10 @@ The above command will generate HTTP requests such as the following:: You can filter the payload using the filter grammar as described before. -The assignment operators can be used to modify previous requests easily, for example, let's add a quote to every parameter looking for SQL injection issues:: +Request mangling +^^^^^^^^^ + +The assignment operators can be used to modify previous requests, for example, let's add a quote to every string parameter prior of performing the HTTP request:: $ wfuzz -z range,1-5 --oF /tmp/session http://testphp.vulnweb.com/artists.php?artist=FUZZ 000003: C=200 118 L 455 W 5326 Ch "3" @@ -736,25 +755,5 @@ The assignment operators can be used to modify previous requests easily, for exa |_ Error identified: Warning: mysql_fetch_array() ... -wfpayload -^^^^^^^^^ - -If you do not want to perform any request, just find some specific HTTP request you can use the wfpayload executable. - -For example, the following will return a unique list of HTTP requests including the authtoken parameter as a GET parameter:: - - $ wfpayload -z burplog,a_burp_log.log --slice "params.get~'authtoken' and url.pstrip|u()" - -Authtoken is the parameter used by BEA WebLogic Commerce Servers (TM) as a CSRF token, and therefore the above will find all the requests exposing the CSRF token in the URL. - -You can also select the field to show, for example:: - - $ wfpayload -z wfuzzp --zD /tmp/session --field r.params.get - artist=5 - ... - -Or:: - $ wfpayload -z wfuzzp --zD /tmp/session --efield r.params.get - 000000006: 200 99 L 272 W 3868 Ch "5 | artist=5" - ... +The above command looks for simple SQL injection issues. diff --git a/docs/user/basicusage.rst b/docs/user/basicusage.rst index bf96b7c8..36e48146 100644 --- a/docs/user/basicusage.rst +++ b/docs/user/basicusage.rst @@ -252,9 +252,9 @@ For example, to show results in JSON format use the following command:: $ wfuzz -o json -w wordlist/general/common.txt http://testphp.vulnweb.com/FUZZ -When using the default output you can also select an additional FuzzResult's field to show together with the payload description:: +When using the default output you can also select additional FuzzResult's fields to show, using --efield, together with the payload description:: - $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --field r + $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --efield r ... 000000001: 200 99 L 272 W 3868 Ch 0 | GET /artists.php?artist=0 HTTP/1.1 Content-Type: application/x-www-form-urlencoded @@ -262,5 +262,21 @@ When using the default output you can also select an additional FuzzResult's fie Host: testphp.vulnweb.com ... +The above is useful, for example, to debug what exact HTTP request Wfuzz sent to the remote Web server. -The above is useful, for example, to debug what exact HTTP request Wfuzz sent to the remote Web server. Check the filter language section in the advance usage document for the available fields. +To completely replace the default payload output you can use --field instead:: + + $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --field url + ... + 000000001: 200 104 L 364 W 4735 Ch "http://testphp.vulnweb.com/artists.php?artist=0" + ... + +--efield and --field can be repeated to show several fields:: + + + $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --efield url --efield h + ... + 000000001: 200 104 L 364 W 4735 Ch "0 | http://testphp.vulnweb.com/artists.php?artist=0 | 4735" + ... + +--efield and --field are in fact filter expressions. Check the filter language section in the advance usage document for the available fields and operators. diff --git a/docs/user/breaking.rst b/docs/user/breaking.rst new file mode 100644 index 00000000..6a81cd6c --- /dev/null +++ b/docs/user/breaking.rst @@ -0,0 +1,8 @@ +Breaking changes +============= + +Following https://semver.org/ versioning since Wfuzz 3.0.0. + +* Wfuzz 3.0.0: + * In wfuzz library prefilter is a list of filters not a string. + * When using --recipe, stored options that are a list are appended. Previously, the last one took precedence. diff --git a/docs/user/wfpayload.rst b/docs/user/wfpayload.rst new file mode 100644 index 00000000..392d1202 --- /dev/null +++ b/docs/user/wfpayload.rst @@ -0,0 +1,85 @@ +wfpayload +========= + +wfpayload uses the same motor as wfuzz but instead of performing HTTP requests, uses wfuzz's payload plugins to generate new content or analyse saved sessions. + +Generating new dictionaries +------------------- + +You can use wfpayload to create new dictionaries:: + + $ wfpayload -z range --zD 0-10 + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + +The same wfuzz's syntax can be used, for example:: + + $ wfpayload -z range --zD 0-10 --filter "FUZZ<3" + 0 + 1 + 2 + + +Analysing saved sessions +------------------ + +Previously performed HTTP requests/responses contain a treasure trove of data. You can use wfpayload to filter and analyse previously saved sessions. Wfpayload can also read sessions from external tools, such as burp. + +This allows you to look for new vulnerabilities or understand the underlying target without performing new HTTP requests. + +For example, the following will return a unique list of HTTP requests including the authtoken parameter as a GET parameter:: + + $ wfpayload -z burplog,a_burp_log.log --slice "params.get~'authtoken'" + +Authtoken is the parameter used by BEA WebLogic Commerce Servers (TM) as a CSRF token, and therefore the above will find all the requests exposing the CSRF token in the URL. + +You can also look for specific parameters or headers, for example, the following will look for HTTP responses accepting any CORS origin:: + + $ wfpayload -z burplog --zD burp_log_05032020.log --prefilter "r.headers.response.Access-Control-Allow-Origin='*'" + +It is worth noting that, if the header is not present in the response it will be return an empty value, not raising any error. + +You can also select the fields to show with --efield and --field, for example:: + + $ wfpayload -z wfuzzp --zD /tmp/session --field r.params.get + artist=5 + ... + +Or:: + + $ wfpayload -z wfuzzp --zD /tmp/session --efield r.params.get + 000000006: 200 99 L 272 W 3868 Ch "5 | artist=5" + ... + +Running plugins against saved sessions +------------------- + +Plugins can be run against a saved session. For example:: + + $ ./wfpayload -z burplog --zD ./burp_log_05032020.log --script=headers --filter "plugins~'akamai'" + ... + 000000124: 302 0 L 0 W 0 Ch "https://trial-eum-clientnsv4-s.akamaihd.net/eum/getdns.txt?c=pjq71x1r7" + |_ New Server header - AkamaiGHost + 000000913: 200 10 L 6571 W 289832 Ch "https://assets.adobedtm.com/2eed2bf00c8bca0c98d97ffee50a306922bc8c98/satelliteLib-27b81756e778cc85cc1a2f067764cd3abf072aa9.js" + |_ New Server header - AkamaiNetStorage + ... + +Re-writing saved sessions +------------------- + +The content of a saved session can be re-written. For example, let's say there is a session with a bunch of 404/400 results that you want to remove:: + + $ wfpayload -z burplog --zD ./burp_log_05032020.log --hc 404 --oF /tmp/no404 + +and then:: + + $ wfpayload -z wfuzzp --zD /tmp/no404 diff --git a/requirements.txt b/requirements.txt index 23e36a31..cebf09a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,12 +2,17 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements.txt setup.py +# pip-compile --output-file=requirements.txt setup.py # -chardet==3.0.4 -configparser==3.5.0 -future==0.17.1 -pycurl==7.43.0.2 -pyparsing==2.3.0 -six==1.11.0 -colorama==0.4.3 +attrs==19.3.0 # via pytest +chardet==3.0.4 # via wfuzz (setup.py) +future==0.18.2 # via wfuzz (setup.py) +more-itertools==8.3.0 # via pytest +packaging==20.4 # via pytest +pluggy==0.13.1 # via pytest +py==1.8.1 # via pytest +pycurl==7.43.0.5 # via wfuzz (setup.py) +pyparsing==2.4.7 # via packaging +pytest==5.4.2 # via wfuzz (setup.py) +six==1.14.0 # via packaging, wfuzz (setup.py) +wcwidth==0.1.9 # via pytest diff --git a/setup.py b/setup.py index 6691c6e8..a5f1b818 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +import os import sys import re from setuptools import setup, find_packages @@ -22,17 +23,19 @@ 'codecov', 'netaddr', 'pip-tools', - 'flake8==3.7.9', + 'flake8==3.8.3', + 'black==19.10b0;python_version>"3.5"', ] install_requires = [ 'pycurl', - 'pyparsing<3;python_version<="3.4"', - 'pyparsing>3*;python_version>="3.5"', + 'pyparsing<2.4.2;python_version<="3.4"', + 'pyparsing>2.4*;python_version>="3.5"', 'future', 'six', 'configparser;python_version<"3.5"', 'chardet', + 'pytest', ] @@ -40,46 +43,49 @@ install_requires += ["colorama>=0.4.0"] -setup( - name="wfuzz", - include_package_data=True, - data_files=[('docs/user', ['docs/user/advanced.rst'])], - packages=find_packages(where='src'), - package_dir={'wfuzz': 'src/wfuzz'}, - entry_points={ - 'console_scripts': [ - 'wfuzz = wfuzz.wfuzz:main', - 'wfpayload = wfuzz.wfuzz:main_filter', - 'wfencode = wfuzz.wfuzz:main_encoder', - ], - 'gui_scripts': [ - 'wxfuzz = wfuzz.wfuzz:main_gui', - ] - }, - version=version, - description="Wfuzz - The web fuzzer", - long_description=long_descr, - author="Xavi Mendez (@x4vi_mendez)", - author_email="xmendez@edge-security.com", - url="http://wfuzz.org", - license="GPLv2", - install_requires=install_requires, - extras_require={ - 'dev': dev_requires, - }, - python_requires=">=2.6", - classifiers=( - 'Development Status :: 4 - Beta', - 'Natural Language :: English', - 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - ), -) +try: + os.symlink('../../docs/user/advanced.rst', 'src/wfuzz/advanced.rst') + setup( + name="wfuzz", + packages=find_packages(where='src'), + package_dir={'wfuzz': 'src/wfuzz'}, + include_package_data=True, + package_data={'wfuzz': ['*.rst']}, + entry_points={ + 'console_scripts': [ + 'wfuzz = wfuzz.wfuzz:main', + 'wfpayload = wfuzz.wfuzz:main_filter', + 'wfencode = wfuzz.wfuzz:main_encoder', + ], + 'gui_scripts': [ + 'wxfuzz = wfuzz.wfuzz:main_gui', + ] + }, + version=version, + description="Wfuzz - The web fuzzer", + long_description=long_descr, + author="Xavi Mendez (@x4vi_mendez)", + author_email="xmendez@edge-security.com", + url="http://wfuzz.org", + license="GPLv2", + install_requires=install_requires, + extras_require={ + 'dev': dev_requires, + 'docs': docs_requires, + }, + python_requires=">=2.6", + classifiers=( + 'Development Status :: 4 - Beta', + 'Natural Language :: English', + 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + ), + ) +finally: + os.unlink('src/wfuzz/advanced.rst') diff --git a/src/wfencode.py b/src/wfencode.py index 2576d082..59603536 100755 --- a/src/wfencode.py +++ b/src/wfencode.py @@ -1,5 +1,5 @@ #!/usr/bin/env python from wfuzz.wfuzz import main_encoder -if __name__ == '__main__': +if __name__ == "__main__": main_encoder() diff --git a/src/wfpayload.py b/src/wfpayload.py index 0c9f4293..ae37f316 100644 --- a/src/wfpayload.py +++ b/src/wfpayload.py @@ -1,5 +1,5 @@ #!/usr/bin/env python from wfuzz.wfuzz import main_filter -if __name__ == '__main__': +if __name__ == "__main__": main_filter() diff --git a/src/wfuzz-cli.py b/src/wfuzz-cli.py index b96ef02d..fb328f59 100644 --- a/src/wfuzz-cli.py +++ b/src/wfuzz-cli.py @@ -2,5 +2,5 @@ from wfuzz.wfuzz import main -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/wfuzz/__init__.py b/src/wfuzz/__init__.py index e0ffd894..05f04af1 100644 --- a/src/wfuzz/__init__.py +++ b/src/wfuzz/__init__.py @@ -1,36 +1,55 @@ -__title__ = 'wfuzz' -__version__ = "2.4.7" +__title__ = "wfuzz" +__version__ = "3.0.0" __build__ = 0x023000 -__author__ = 'Xavier Mendez' -__license__ = 'GPL 2.0' -__copyright__ = 'Copyright 2011-2018 Xavier Mendez' +__author__ = "Xavier Mendez" +__license__ = "GPL 2.0" +__copyright__ = "Copyright 2011-2018 Xavier Mendez" -# define a logging Handler import logging +import sys + +import warnings + +# define a logging Handler console = logging.StreamHandler() console.setLevel(logging.WARNING) -formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') +formatter = logging.Formatter("%(name)-12s: %(levelname)-8s %(message)s") console.setFormatter(formatter) -logging.getLogger('').addHandler(console) +logging.getLogger("").addHandler(console) + + +# define warnings format +def warning_on_one_line(message, category, filename, lineno, file=None, line=None): + return " %s:%s: %s:%s\n" % (filename, lineno, category.__name__, message) + + +warnings.formatwarning = warning_on_one_line -# Check for pycurl dependency -import sys try: import pycurl if "openssl".lower() not in pycurl.version.lower(): - print("\nWarning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.\n") + warnings.warn( + "Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information." + ) if not hasattr(pycurl, "CONNECT_TO"): - print("\nWarning: Pycurl and/or libcurl version is old. CONNECT_TO option is missing. Wfuzz --ip option will not be available.\n") + warnings.warn( + "Pycurl and/or libcurl version is old. CONNECT_TO option is missing. Wfuzz --ip option will not be available." + ) if not hasattr(pycurl, "PATH_AS_IS"): - print("\nWarning: Pycurl and/or libcurl version is old. PATH_AS_IS option is missing. Wfuzz might not correctly fuzz URLS with '..'.\n") + warnings.warn( + "Pycurl and/or libcurl version is old. PATH_AS_IS option is missing. Wfuzz might not correctly fuzz URLS with '..'." + ) + +except ImportError: + warnings.warn( + "fuzz needs pycurl to run. Pycurl could be installed using the following command: $ pip install pycurl" + ) -except ImportError as e: - print("\nFatal exception: {}. Wfuzz needs pycurl to run. Pycurl could be installed using the following command:\n\npip install pycurl".format(str(e))) sys.exit(1) from .options import FuzzSession diff --git a/src/wfuzz/api.py b/src/wfuzz/api.py index aff150f4..b6ec002c 100644 --- a/src/wfuzz/api.py +++ b/src/wfuzz/api.py @@ -2,9 +2,9 @@ from .facade import Facade from .ui.console.clparser import CLParser -''' +""" Wfuzz API -''' +""" def fuzz(**kwargs): diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index 845a5f39..8f07e8ba 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -1,238 +1,31 @@ -from .fuzzobjects import FuzzResult +from .fuzzobjects import FuzzType from .myqueues import MyPriorityQueue, QueueManager -from .fuzzqueues import SeedQ, SaveQ, PrinterQ, RoutingQ, FilterQ, SliceQ, JobQ, RecursiveQ, DryRunQ, HttpQueue, HttpReceiver - -from .fuzzobjects import FuzzResultFactory, FuzzStats -from .facade import Facade -from .exception import FuzzExceptBadOptions, FuzzExceptNoPluginError - -from .filter import FuzzResFilterSlice - -import re - -# Python 2 and 3: zip_longest -try: - from itertools import zip_longest -except ImportError: - from itertools import izip_longest as zip_longest +from .fuzzqueues import ( + SeedQ, + SaveQ, + PrinterQ, + RoutingQ, + FilterQ, + SliceQ, + JobQ, + RecursiveQ, + DryRunQ, + HttpQueue, + HttpReceiver, + AllVarQ, + CLIPrinterQ, + ConsolePrinterQ, + PassPayloadQ, +) # python 2 and 3: iterator from builtins import object -class sliceit(object): - def __init__(self, payload, slicestr): - self.ffilter = FuzzResFilterSlice(filter_string=slicestr) - self.payload = payload - - def __iter__(self): - return self - - def count(self): - return -1 - - def __next__(self): - item = next(self.payload) - while not self.ffilter.is_visible(item): - item = next(self.payload) - - return item - - -class tupleit(object): - def __init__(self, parent): - self.parent = parent - - def count(self): - return self.parent.count() - - def __next__(self): - return (next(self.parent),) - - def __iter__(self): - return self - - -class dictionary(object): - def __init__(self, payload, encoders_list): - self.__payload = payload - self.__encoders = encoders_list - self.__generator = self._gen() if self.__encoders else None - - def count(self): - return (self.__payload.count() * len(self.__encoders)) if self.__encoders else self.__payload.count() - - def __iter__(self): - return self - - def _gen(self): - while 1: - try: - payload_list = next(self.__payload) - except StopIteration: - return - - for name in self.__encoders: - if name.find('@') > 0: - string = payload_list - for i in reversed(name.split("@")): - string = Facade().encoders.get_plugin(i)().encode(string) - yield string - else: - plugin_list = Facade().encoders.get_plugins(name) - if not plugin_list: - raise FuzzExceptNoPluginError(name + " encoder does not exists (-e encodings for a list of available encoders)") - - for e in plugin_list: - yield e().encode(payload_list) - - def __next__(self): - return next(self.__generator) if self.__encoders else next(self.__payload) - - -class requestGenerator(object): - def __init__(self, options): - self.options = options - self.seed = FuzzResultFactory.from_options(options) - self.baseline = FuzzResultFactory.from_baseline(self.seed, options) - self._payload_list = [] - self.dictio = self.get_dictio() - - self.stats = FuzzStats.from_requestGenerator(self) - - self._allvar_gen = None - if self.seed.history.wf_allvars is not None: - self._allvar_gen = self.__allvars_gen(self.dictio) - - def stop(self): - self.stats.cancelled = True - self.close() - - def restart(self, seed): - self.seed = seed - self.dictio = self.get_dictio() - - def _check_dictio_len(self, element): - fuzz_words = self.options["compiled_filter"].get_fuzz_words() + self.options["compiled_prefilter"].get_fuzz_words() + self.get_fuzz_words() - - if len(element) != len(set(fuzz_words)): - raise FuzzExceptBadOptions("FUZZ words and number of payloads do not match!") - - def get_fuzz_words(self): - marker_regex = re.compile(r"FUZ\d*Z", re.MULTILINE | re.DOTALL) - fuzz_words = marker_regex.findall(str(self.seed.history)) - method, userpass = self.seed.history.auth - - fuzz_words += marker_regex.findall(self.seed.history.scheme) - - if method: - fuzz_words += marker_regex.findall(userpass) - - if self.options["seed_payload"]: - fuzz_words += ["FUZZ"] - - return fuzz_words - - def count(self): - v = self.dictio.count() - if self.seed.history.wf_allvars is not None: - v *= len(self.seed.history.wf_allvars_set) - - if self.baseline: - v += 1 - - return v - - def __iter__(self): - return self - - def __allvars_gen(self, dic): - # no FUZZ keyword allowed - marker_regex = re.compile(r"FUZ\d*Z", re.MULTILINE | re.DOTALL) - if len(marker_regex.findall(str(self.seed.history))) > 0: - raise FuzzExceptBadOptions("FUZZ words not allowed when using all parameters brute forcing.") - - if len(self.seed.history.wf_allvars_set) == 0: - raise FuzzExceptBadOptions("No variables on specified variable set: " + self.seed.history.wf_allvars) - - for payload in dic: - for r in FuzzResultFactory.from_all_fuzz_request(self.seed, payload): - yield r - - def __next__(self): - if self.stats.cancelled: - raise StopIteration - - if self.baseline and self.stats.processed() == 0 and self.stats.pending_seeds() <= 1: - return self.baseline - - if self.seed.history.wf_allvars is not None: - return next(self._allvar_gen) - else: - n = next(self.dictio) - if self.stats.processed() == 0 or (self.baseline and self.stats.processed() == 1): - self._check_dictio_len(n) - - return FuzzResultFactory.from_seed(self.seed, n, self.options) - - def close(self): - for payload in self._payload_list: - payload.close() - - def get_dictio(self): - class wrapper(object): - def __init__(self, iterator): - self._it = iter(iterator) - - def __iter__(self): - return self - - def count(self): - return -1 - - def __next__(self): - return str(next(self._it)) - - selected_dic = [] - self._payload_list = [] - - if self.options["dictio"]: - for d in [wrapper(x) for x in self.options["dictio"]]: - selected_dic.append(d) - else: - for payload in self.options["payloads"]: - try: - name, params, slicestr = [x[0] for x in zip_longest(payload, (None, None, None))] - except ValueError: - raise FuzzExceptBadOptions("You must supply a list of payloads in the form of [(name, {params}), ... ]") - - if not params: - raise FuzzExceptBadOptions("You must supply a list of payloads in the form of [(name, {params}), ... ]") - - p = Facade().payloads.get_plugin(name)(params) - self._payload_list.append(p) - pp = dictionary(p, params["encoder"]) if "encoder" in params else p - selected_dic.append(sliceit(pp, slicestr) if slicestr else pp) - - if not selected_dic: - raise FuzzExceptBadOptions("Empty dictionary! Check payload and filter") - - if len(selected_dic) == 1: - if self.options["iterator"]: - raise FuzzExceptBadOptions("Several dictionaries must be used when specifying an iterator") - return tupleit(selected_dic[0]) - elif self.options["iterator"]: - return Facade().iterators.get_plugin(self.options["iterator"])(*selected_dic) - else: - return Facade().iterators.get_plugin("product")(*selected_dic) - - class Fuzzer(object): def __init__(self, options): - self.genReq = options.get("compiled_genreq") - # Create queues # genReq ---> seed_queue -> [slice_queue] -> http_queue/dryrun -> [round_robin -> plugins_queue] * N # -> [recursive_queue -> routing_queue] -> [filter_queue] -> [save_queue] -> [printer_queue] ---> results @@ -240,16 +33,24 @@ def __init__(self, options): self.qmanager = QueueManager(options) self.results_queue = MyPriorityQueue() - self.qmanager.add("seed_queue", SeedQ(options)) - - if options.get('compiled_prefilter').is_active(): - self.qmanager.add("slice_queue", SliceQ(options)) - - if options.get("dryrun"): - self.qmanager.add("http_queue", DryRunQ(options)) + if options["allvars"]: + self.qmanager.add("allvars_queue", AllVarQ(options)) + else: + self.qmanager.add("seed_queue", SeedQ(options)) + + for prefilter_idx, prefilter in enumerate(options.get("compiled_prefilter")): + if prefilter.is_active(): + self.qmanager.add( + "slice_queue_{}".format(prefilter_idx), SliceQ(options, prefilter) + ) + + if options.get("transport") == "dryrun": + self.qmanager.add("transport_queue", DryRunQ(options)) + elif options.get("transport") == "payload": + self.qmanager.add("transport_queue", PassPayloadQ(options)) else: # http_queue breaks process rules due to being asynchronous. Someone has to collects its sends, for proper fuzzqueue's count and sync purposes - self.qmanager.add("http_queue", HttpQueue(options)) + self.qmanager.add("transport_queue", HttpQueue(options)) self.qmanager.add("http_receiver", HttpReceiver(options)) if options.get("script"): @@ -260,22 +61,36 @@ def __init__(self, options): rq = RoutingQ( options, { - FuzzResult.seed: self.qmanager["seed_queue"], - FuzzResult.backfeed: self.qmanager["http_queue"] - } + FuzzType.SEED: self.qmanager["seed_queue"], + FuzzType.BACKFEED: self.qmanager["transport_queue"], + }, ) self.qmanager.add("routing_queue", rq) - if options.get('compiled_filter').is_active(): - self.qmanager.add("filter_queue", FilterQ(options)) + if options.get("compiled_filter").is_active(): + self.qmanager.add( + "filter_queue", FilterQ(options, options["compiled_filter"]) + ) - if options.get('save'): + if options.get("compiled_simple_filter").is_active(): + self.qmanager.add( + "simple_filter_queue", + FilterQ(options, options["compiled_simple_filter"]), + ) + + if options.get("save"): self.qmanager.add("save_queue", SaveQ(options)) - if options.get('compiled_printer'): + if options.get("compiled_printer"): self.qmanager.add("printer_queue", PrinterQ(options)) + if options.get("exec_mode") == "cli": + if options["console_printer"]: + self.qmanager.add("printer_cli", ConsolePrinterQ(options)) + else: + self.qmanager.add("printer_cli", CLIPrinterQ(options)) + self.qmanager.bind(self.results_queue) # initial seed request @@ -292,19 +107,23 @@ def __next__(self): # done! (None sent has gone through all queues). if not res: raise StopIteration - elif res.type == FuzzResult.error: + elif res.item_type == FuzzType.ERROR: raise res.exception return res def stats(self): - return dict(list(self.qmanager.get_stats().items()) + list(self.qmanager["http_queue"].job_stats().items()) + list(self.genReq.stats.get_stats().items())) + return dict( + list(self.qmanager.get_stats().items()) + + list(self.qmanager["transport_queue"].job_stats().items()) + + list(self.options.stats.get_stats().items()) + ) def cancel_job(self): self.qmanager.cancel() def pause_job(self): - self.qmanager["http_queue"].pause.clear() + self.qmanager["transport_queue"].pause.clear() def resume_job(self): - self.qmanager["http_queue"].pause.set() + self.qmanager["transport_queue"].pause.set() diff --git a/src/wfuzz/dictionaries.py b/src/wfuzz/dictionaries.py new file mode 100644 index 00000000..4ab0d13a --- /dev/null +++ b/src/wfuzz/dictionaries.py @@ -0,0 +1,167 @@ +from .exception import FuzzExceptNoPluginError, FuzzExceptBadOptions +from .facade import Facade +from .filters.ppfilter import FuzzResFilterSlice +from .fuzzobjects import FuzzWord, FuzzWordType + + +class BaseIterator: + def count(self): + raise NotImplementedError + + def width(self): + raise NotImplementedError + + def payloads(self): + raise NotImplementedError + + def cleanup(self): + for payload in self.payloads(): + payload.close() + + +class BaseDictionary: + def count(self): + raise NotImplementedError + + def next_word(self): + raise NotImplementedError + + def __next__(self): + return self.next_word() + + def __iter__(self): + return self + + def close(self): + pass + + +class EncodeIt(BaseDictionary): + def __init__(self, parent, encoders_list): + self.parent = parent + self.encoders = encoders_list + self.__generator = self._gen() + + def count(self): + return self.parent.count() * len(self.encoders) + + def concatenate(self, encoder_name, payload_word): + string = payload_word.content + for plugin_name in reversed(encoder_name.split("@")): + string = Facade().encoders.get_plugin(plugin_name)().encode(string) + + return FuzzWord(string, FuzzWordType.WORD) + + def encode(self, encoder_name, payload_word): + plugin_list = Facade().encoders.get_plugins(encoder_name) + if not plugin_list: + raise FuzzExceptNoPluginError( + encoder_name + + " encoder does not exists (-e encodings for a list of available encoders)" + ) + + for plugin_class in plugin_list: + yield FuzzWord( + plugin_class().encode(payload_word.content), FuzzWordType.WORD + ) + + def next_word(self): + return next(self.__generator) + + def _gen(self): + while 1: + try: + payload_word = next(self.parent) + except StopIteration: + return + + for encoder_name in self.encoders: + if encoder_name.find("@") > 0: + yield self.concatenate(encoder_name, payload_word) + else: + for string in self.encode(encoder_name, payload_word): + yield string + + def __next__(self): + return next(self.__generator) + + +class TupleIt(BaseDictionary, BaseIterator): + def __init__(self, parent): + self.parent = parent + + def count(self): + return self.parent.count() + + def width(self): + return 1 + + def payloads(self): + return [self.parent] + + def next_word(self): + return (next(self.parent),) + + +class WrapperIt(BaseDictionary): + def __init__(self, iterator): + self._it = iter(iterator) + + def count(self): + return -1 + + def get_type(self): + return FuzzWordType.WORD + + def next_word(self): + return FuzzWord(str(next(self._it)), FuzzWordType.WORD) + + +class SliceIt(BaseDictionary): + def __init__(self, payload, slicestr): + self.ffilter = FuzzResFilterSlice(filter_string=slicestr) + self.payload = payload + + def count(self): + return -1 + + def get_type(self): + return self.payload.get_type() + + def next_word(self): + # can be refactored using the walrus operator in python 3.8 + item = next(self.payload) + filter_ret = self.ffilter.is_visible(item.content) + + if not isinstance(filter_ret, bool) and item.type == FuzzWordType.FUZZRES: + raise FuzzExceptBadOptions( + "The payload type cannot be modified from FuzzResult to word." + ) + + while isinstance(filter_ret, bool) and not filter_ret: + item = next(self.payload) + filter_ret = self.ffilter.is_visible(item.content) + + if not isinstance(filter_ret, bool): + return FuzzWord(filter_ret, item.type) + + return item + + +class AllVarDictio(BaseDictionary, BaseIterator): + def __init__(self, iterator, allvar_len): + self._it = iter(iterator) + self._count = allvar_len + + def count(self): + return self._count + + def width(self): + return 0 + + def payloads(self): + return [] + + def next_word(self): + var_name, fuzz_word = next(self._it) + return (FuzzWord(var_name, FuzzWordType.WORD), fuzz_word) diff --git a/src/wfuzz/exception.py b/src/wfuzz/exception.py index 50cd513b..60745abe 100644 --- a/src/wfuzz/exception.py +++ b/src/wfuzz/exception.py @@ -1,5 +1,3 @@ - - class FuzzException(Exception): pass diff --git a/src/wfuzz/externals/moduleman/loader.py b/src/wfuzz/externals/moduleman/loader.py index debd290a..4e7d81c8 100644 --- a/src/wfuzz/externals/moduleman/loader.py +++ b/src/wfuzz/externals/moduleman/loader.py @@ -28,7 +28,7 @@ def set_params(self, **params): self.filename = params["filename"] self.base_path = params["base_path"] - if self.base_path.endswith('/'): + if self.base_path.endswith("/"): self.base_path = self.base_path[:-1] def load(self, registrant): @@ -40,8 +40,8 @@ def _build_id(self, filename, objname): filepath, filename = os.path.split(filename) relative_path = os.path.relpath(filepath, self.base_path) - identifier = relative_path + '/' + objname - if identifier.startswith('./'): + identifier = relative_path + "/" + objname + if identifier.startswith("./"): identifier = identifier[2:] return identifier @@ -50,7 +50,7 @@ def _load_py_from_file(self, filename): """ Opens "filename", inspects it and calls the registrant """ - self.__logger.debug('__load_py_from_file. START, file=%s' % (filename,)) + self.__logger.debug("__load_py_from_file. START, file=%s" % (filename,)) dirname, filename = os.path.split(filename) fn = os.path.splitext(filename)[0] @@ -61,12 +61,16 @@ def _load_py_from_file(self, filename): exten_file, filename, description = imp.find_module(fn, [dirname]) module = imp.load_module(fn, exten_file, filename, description) except ImportError as msg: - self.__logger.critical('__load_py_from_file. Filename: %s Exception, msg=%s' % (filename, msg)) + self.__logger.critical( + "__load_py_from_file. Filename: %s Exception, msg=%s" % (filename, msg) + ) # raise msg pass except SyntaxError as msg: # incorrect python syntax in file - self.__logger.critical('__load_py_from_file. Filename: %s Exception, msg=%s' % (filename, msg)) + self.__logger.critical( + "__load_py_from_file. Filename: %s Exception, msg=%s" % (filename, msg) + ) # raise msg pass finally: @@ -78,13 +82,15 @@ def _load_py_from_file(self, filename): for objname in dir(module): obj = getattr(module, objname) - self.__logger.debug('__load_py_from_file. inspecting=%s' % (objname,)) + self.__logger.debug("__load_py_from_file. inspecting=%s" % (objname,)) if inspect.isclass(obj): - if '__PLUGIN_MODULEMAN_MARK' in dir(obj): + if "__PLUGIN_MODULEMAN_MARK" in dir(obj): if self.module_registrant: - self.module_registrant.register(self._build_id(filename, objname), obj) + self.module_registrant.register( + self._build_id(filename, objname), obj + ) - self.__logger.debug('__load_py_from_file. END, loaded file=%s' % (filename,)) + self.__logger.debug("__load_py_from_file. END, loaded file=%s" % (filename,)) class DirLoader(FileLoader): @@ -100,7 +106,7 @@ def set_params(self, **params): self.base_dir = params["base_dir"] self.base_path = params["base_path"] - if self.base_path.endswith('/'): + if self.base_path.endswith("/"): self.base_path = self.base_path[:-1] def load(self, registrant): @@ -110,9 +116,11 @@ def load(self, registrant): def _build_id(self, filename, objname): filepath, filename = os.path.split(filename) - relative_path = os.path.relpath(filepath, os.path.join(self.base_path, self.base_dir)) - identifier = relative_path + '/' + objname - if identifier.startswith('./'): + relative_path = os.path.relpath( + filepath, os.path.join(self.base_path, self.base_dir) + ) + identifier = relative_path + "/" + objname + if identifier.startswith("./"): identifier = identifier[2:] return identifier @@ -136,7 +144,7 @@ def __load_all(self, dir_name): def __walk_dir_tree(self, dirname): dir_list = [] - self.__logger.debug('__walk_dir_tree. START dir=%s', dirname) + self.__logger.debug("__walk_dir_tree. START dir=%s", dirname) for f in os.listdir(dirname): current = os.path.join(dirname, f) diff --git a/src/wfuzz/externals/moduleman/modulefilter.py b/src/wfuzz/externals/moduleman/modulefilter.py index 803f505a..9d4052f4 100644 --- a/src/wfuzz/externals/moduleman/modulefilter.py +++ b/src/wfuzz/externals/moduleman/modulefilter.py @@ -23,7 +23,17 @@ PYPARSING = True try: - from pyparsing import Word, Group, oneOf, Optional, Suppress, ZeroOrMore, Literal, alphas, alphanums + from pyparsing import ( + Word, + Group, + oneOf, + Optional, + Suppress, + ZeroOrMore, + Literal, + alphas, + alphanums, + ) except ImportError: PYPARSING = False @@ -41,9 +51,15 @@ def __init__(self): neg_operator = "not" elementRef = category definition = elementRef + ZeroOrMore(operator + elementRef) - nestedformula = Group(Suppress(Optional(Literal("("))) + definition + Suppress(Optional(Literal(")")))) + nestedformula = Group( + Suppress(Optional(Literal("("))) + + definition + + Suppress(Optional(Literal(")"))) + ) neg_nestedformula = Optional(neg_operator) + nestedformula - self.finalformula = neg_nestedformula + ZeroOrMore(operator + neg_nestedformula) + self.finalformula = neg_nestedformula + ZeroOrMore( + operator + neg_nestedformula + ) elementRef.setParseAction(self.__compute_element) neg_nestedformula.setParseAction(self.__compute_neg_formula) @@ -51,7 +67,7 @@ def __init__(self): self.finalformula.setParseAction(self.__myreduce) def __compute_neg_formula(self, tokens): - if len(tokens) > 1 and tokens[0] == 'not': + if len(tokens) > 1 and tokens[0] == "not": return not tokens[1] else: return tokens[0] @@ -64,17 +80,17 @@ def __compute_element(self, tokens): return self.plugin.name.startswith(item[:wildc_index]) else: if isinstance(self.plugin.category, list): - return (item in self.plugin.category or self.plugin.name == item) + return item in self.plugin.category or self.plugin.name == item else: - return (self.plugin.category == item or self.plugin.name == item) + return self.plugin.category == item or self.plugin.name == item def __myreduce(self, elements): first = elements[0] for i in range(1, len(elements), 2): if elements[i] == "and": - first = (first and elements[i + 1]) + first = first and elements[i + 1] elif elements[i] == "or" or elements[i] == ",": - first = (first or elements[i + 1]) + first = first or elements[i + 1] return first @@ -87,7 +103,12 @@ def simple_filter(self, plugin, filter_string): for item in filter_string.split(","): wildc_index = item.find("*") if wildc_index > 0: - ret.append((item in plugin.category or plugin.name.startswith(item[:wildc_index]))) + ret.append( + ( + item in plugin.category + or plugin.name.startswith(item[:wildc_index]) + ) + ) else: ret.append((item in plugin.category or plugin.name == item)) diff --git a/src/wfuzz/externals/moduleman/plugin.py b/src/wfuzz/externals/moduleman/plugin.py index 1aa6c6ce..e655f063 100644 --- a/src/wfuzz/externals/moduleman/plugin.py +++ b/src/wfuzz/externals/moduleman/plugin.py @@ -1,4 +1,7 @@ -import collections +try: + from collections.abc import Callable +except ImportError: + from collections import Callable def moduleman_plugin(*args): @@ -6,13 +9,13 @@ def moduleman_plugin(*args): def inner_decorator(cls): for method in method_args: - if (not (method in dir(cls))): + if not (method in dir(cls)): raise Exception("Required method %s not implemented" % method) cls.__PLUGIN_MODULEMAN_MARK = "Plugin mark" return cls - if not isinstance(args[0], collections.Callable): + if not isinstance(args[0], Callable): method_args += args return inner_decorator diff --git a/src/wfuzz/externals/moduleman/registrant.py b/src/wfuzz/externals/moduleman/registrant.py index c00ffbf4..352ae0a5 100644 --- a/src/wfuzz/externals/moduleman/registrant.py +++ b/src/wfuzz/externals/moduleman/registrant.py @@ -1,5 +1,6 @@ from .modulefilter import Filter from collections import defaultdict + try: from collections.abc import MutableMapping except ImportError: @@ -7,7 +8,7 @@ from threading import Lock -class IRegistrant(): +class IRegistrant: def __init__(self, loader, plg_filter): self.plg_filter = plg_filter self.loader = loader @@ -119,14 +120,21 @@ def get_plugin(self, identifier): if identifier in self.__plugins: return self.__plugins[identifier] else: - plugin_list = [plg for plg_id, plg in self.__get_plugins("$all$", True) if identifier in plg_id] + plugin_list = [ + plg + for plg_id, plg in self.__get_plugins("$all$", True) + if identifier in plg_id + ] if not plugin_list: raise KeyError("No plugins found!") elif len(plugin_list) == 1: return plugin_list[0] else: - raise KeyError("Multiple plugins found: %s" % ','.join([plg.name for plg in plugin_list])) + raise KeyError( + "Multiple plugins found: %s" + % ",".join([plg.name for plg in plugin_list]) + ) raise KeyError("No plugins found!") @@ -134,10 +142,18 @@ def get_plugins(self, category="$all$", sorting="true"): return [plg for plg_id, plg in self.__get_plugins(category, sorting)] def get_plugins_ext(self, category="$all$", sorting="true"): - plugin_list = [['Id', 'Priority', 'Category', 'Name', 'Summary']] + plugin_list = [["Id", "Priority", "Category", "Name", "Summary"]] for plg_id, plg in self.__get_plugins(category, sorting): - plugin_list.append([plg_id, str(plg.priority), ', '.join(plg.category), str(plg.name), str(plg.summary)]) + plugin_list.append( + [ + plg_id, + str(plg.priority), + ", ".join(plg.category), + str(plg.name), + str(plg.summary), + ] + ) return plugin_list @@ -150,5 +166,5 @@ def get_plugins_ids(self, category="$all$", sorting="true"): class MulRegistrant(BRegistrant): def load(self): - for l in self.loader: - l.load(self) + for loader in self.loader: + loader.load(self) diff --git a/src/wfuzz/externals/reqresp/Request.py b/src/wfuzz/externals/reqresp/Request.py index 19c903ba..1447f145 100644 --- a/src/wfuzz/externals/reqresp/Request.py +++ b/src/wfuzz/externals/reqresp/Request.py @@ -4,6 +4,7 @@ # Python 2 and 3 import sys + if sys.version_info >= (3, 0): from urllib.parse import urlparse from urllib.parse import urlunparse @@ -11,7 +12,6 @@ from urlparse import urlparse from urlparse import urlunparse -import string import re import pycurl @@ -19,7 +19,8 @@ from .exceptions import ReqRespException from .Response import Response -from wfuzz.utils import python2_3_convert_to_unicode +from wfuzz.helpers.str_func import python2_3_convert_to_unicode +from wfuzz.helpers.obj_dic import CaseInsensitiveDict from .TextParser import TextParser @@ -31,10 +32,10 @@ class Request: def __init__(self): - self.__host = None # www.google.com:80 - self.__path = None # /index.php - self.__params = None # Mierdaza de index.php;lskjflkasjflkasjfdlkasdf? - self.schema = "http" # http + self.__host = None # www.google.com:80 + self.__path = None # /index.php + self.__params = None # Mierdaza de index.php;lskjflkasjflkasjfdlkasdf? + self.schema = "http" # http # #### Variables calculadas por getters NO SE PUEDEN MODIFICAR # self.urlWithoutPath # http://www.google.es @@ -46,7 +47,9 @@ def __init__(self): # self.postdata="" # Datos por POST, toto el string # ############### - self.ContentType = "application/x-www-form-urlencoded" # None es normal encoding + self.ContentType = ( + "application/x-www-form-urlencoded" # None es normal encoding + ) self.multiPOSThead = {} self.__variablesGET = VariablesSet() @@ -54,26 +57,28 @@ def __init__(self): self._non_parsed_post = None # diccionario, por ejemplo headers["Cookie"] - self._headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - "User-Agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1)" - } + self._headers = CaseInsensitiveDict( + { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1)", + } + ) - self.response = None # Apunta a la response que produce dicha request + self.response = None # Apunta a la response que produce dicha request # ################## lo de debajo no se deberia acceder directamente - self.time = None # 23:00:00 - self.ip = None # 192.168.1.1 + self.time = None # 23:00:00 + self.ip = None # 192.168.1.1 self._method = None - self.protocol = "HTTP/1.1" # HTTP/1.1 + self.protocol = "HTTP/1.1" # HTTP/1.1 self.__performHead = "" self.__performBody = "" self.__authMethod = None self.__userpass = "" - self.description = "" # For temporally store imformation + self.description = "" # For temporally store imformation self.__proxy = None self.proxytype = None @@ -106,9 +111,9 @@ def setFinalUrl(self, fu): def __str__(self): str = "[ URL: %s" % (self.completeUrl) if self.postdata: - str += " - {}: \"{}\"".format(self.method, self.postdata) + str += ' - {}: "{}"'.format(self.method, self.postdata) if "Cookie" in self._headers: - str += " - COOKIE: \"%s\"" % self._headers["Cookie"] + str += ' - COOKIE: "%s"' % self._headers["Cookie"] str += " ]" return str @@ -134,11 +139,22 @@ def getXML(self, obj): def __getattr__(self, name): if name == "urlWithoutVariables": - return urlunparse((self.schema, self.__host, self.__path, '', '', '')) + return urlunparse((self.schema, self.__host, self.__path, "", "", "")) elif name == "pathWithVariables": - return urlunparse(('', '', self.__path, '', self.__variablesGET.urlEncoded(), '')) + return urlunparse( + ("", "", self.__path, "", self.__variablesGET.urlEncoded(), "") + ) elif name == "completeUrl": - return urlunparse((self.schema, self.__host, self.__path, self.__params, self.__variablesGET.urlEncoded(), '')) + return urlunparse( + ( + self.schema, + self.__host, + self.__path, + self.__params, + self.__variablesGET.urlEncoded(), + "", + ) + ) elif name == "finalUrl": if self.__finalurl: return self.__finalurl @@ -152,7 +168,7 @@ def __getattr__(self, name): return self._variablesPOST.urlEncoded() elif self.ContentType == "multipart/form-data": return self._variablesPOST.multipartEncoded() - elif self.ContentType == 'application/json': + elif self.ContentType == "application/json": return self._variablesPOST.json_encoded() else: return self._variablesPOST.urlEncoded() @@ -161,14 +177,16 @@ def __getattr__(self, name): def setUrl(self, urltmp): self.__variablesGET = VariablesSet() - self.schema, self.__host, self.__path, self.__params, variables, f = urlparse(urltmp) + self.schema, self.__host, self.__path, self.__params, variables, f = urlparse( + urltmp + ) if "Host" not in self._headers or (not self._headers["Host"]): self._headers["Host"] = self.__host if variables: self.__variablesGET.parseUrlEncoded(variables) -# ############## PROXY ################################## + # ############## PROXY ################################## def getProxy(self): return self.__proxy @@ -176,11 +194,11 @@ def setProxy(self, prox, ptype): self.__proxy = prox self.proxytype = ptype -# ############## FOLLOW LOCATION ######################## + # ############## FOLLOW LOCATION ######################## def setFollowLocation(self, value): self.followLocation = value -# ############# TIMEOUTS ################################ + # ############# TIMEOUTS ################################ def setConnTimeout(self, time): self.__timeout = time @@ -193,7 +211,7 @@ def setTotalTimeout(self, time): def getTotalTimeout(self): return self.__totaltimeout -# ############# Autenticacion ########################### + # ############# Autenticacion ########################### def setAuth(self, method, string): self.__authMethod = method self.__userpass = string @@ -201,7 +219,7 @@ def setAuth(self, method, string): def getAuth(self): return self.__authMethod, self.__userpass -# ############# TRATAMIENTO VARIABLES GET & POST ######################### + # ############# TRATAMIENTO VARIABLES GET & POST ######################### def existsGETVar(self, key): return self.__variablesGET.existsVar(key) @@ -212,7 +230,8 @@ def existPOSTVar(self, key): def setVariablePOST(self, key, value): v = self._variablesPOST.getVariable(key) v.update(value) -# self._headers["Content-Length"] = str(len(self.postdata)) + + # self._headers["Content-Length"] = str(len(self.postdata)) def setVariableGET(self, key, value): v = self.__variablesGET.getVariable(key) @@ -231,7 +250,7 @@ def setPostData(self, pd, boundary=None): try: if self.ContentType == "multipart/form-data": self._variablesPOST.parseMultipart(pd, boundary) - elif self.ContentType == 'application/json': + elif self.ContentType == "application/json": self._variablesPOST.parse_json_encoded(pd) else: self._variablesPOST.parseUrlEncoded(pd) @@ -242,21 +261,18 @@ def setPostData(self, pd, boundary=None): print("Warning: POST parameters not parsed") pass -############################################################################ + ############################################################################ def addHeader(self, key, value): - k = string.capwords(key, "-") - self._headers[k] = value + self._headers[key] = value def delHeader(self, key): - k = string.capwords(key, "-") - if k in self._headers: - del self._headers[k] + if key in self._headers: + del self._headers[key] def __getitem__(self, key): - k = string.capwords(key, "-") - if k in self._headers: - return self._headers[k] + if key in self._headers: + return self._headers[key] else: return "" @@ -282,7 +298,7 @@ def head(self): self.response = rp def createPath(self, newpath): - '''Creates new url from a location header || Hecho para el followLocation=true''' + """Creates new url from a location header || Hecho para el followLocation=true""" if "http" in newpath[:4].lower(): return newpath @@ -290,7 +306,7 @@ def createPath(self, newpath): if "/" != newpath[0]: newpath = "/".join(parts[2].split("/")[:-1]) + "/" + newpath - return urlunparse([parts[0], parts[1], newpath, '', '', '']) + return urlunparse([parts[0], parts[1], newpath, "", "", ""]) # pycurl - reqresp conversions @staticmethod @@ -347,7 +363,9 @@ def to_pycurl_object(c, req): c.setopt(pycurl.CUSTOMREQUEST, req.method) if req._non_parsed_post is not None: - c.setopt(pycurl.POSTFIELDS, python2_3_convert_to_unicode(req._non_parsed_post)) + c.setopt( + pycurl.POSTFIELDS, python2_3_convert_to_unicode(req._non_parsed_post) + ) c.setopt(pycurl.FOLLOWLOCATION, 1 if req.followLocation else 0) @@ -394,8 +412,15 @@ def perform(self): # ######## ESTE conjunto de funciones no es necesario para el uso habitual de la clase def getAll(self): - pd = self._non_parsed_post if self._non_parsed_post else '' - string = str(self.method) + " " + str(self.pathWithVariables) + " " + str(self.protocol) + "\n" + pd = self._non_parsed_post if self._non_parsed_post else "" + string = ( + str(self.method) + + " " + + str(self.pathWithVariables) + + " " + + str(self.protocol) + + "\n" + ) for i, j in self._headers.items(): string += i + ": " + j + "\n" string += "\n" + pd @@ -418,16 +443,16 @@ def Substitute(self, src, dst): self.parseRequest(b, self.schema) def parseRequest(self, rawRequest, prot="http"): - ''' Aun esta en fase BETA y por probar''' + """ Aun esta en fase BETA y por probar""" tp = TextParser() tp.setSource("string", rawRequest) self._variablesPOST = VariablesSet() - self._headers = {} # diccionario, por ejemplo headers["Cookie"] + self._headers = {} # diccionario, por ejemplo headers["Cookie"] tp.readLine() try: - tp.search(r"^(\w+) (.*) (HTTP\S*)$") + tp.search(r"^(\S+) (.*) (HTTP\S*)$") self.method = tp[0][0] self.protocol = tp[0][2] except Exception as a: @@ -435,12 +460,12 @@ def parseRequest(self, rawRequest, prot="http"): raise a pathTMP = tp[0][1].replace(" ", "%20") - pathTMP = ('', '') + urlparse(pathTMP)[2:] + pathTMP = ("", "") + urlparse(pathTMP)[2:] pathTMP = urlunparse(pathTMP) while True: tp.readLine() - if (tp.search("^([^:]+): (.*)$")): + if tp.search("^([^:]+): (.*)$"): self.addHeader(tp[0][0], tp[0][1]) else: break @@ -448,7 +473,7 @@ def parseRequest(self, rawRequest, prot="http"): self.setUrl(prot + "://" + self._headers["Host"] + pathTMP) # ignore CRLFs until request line - while tp.lastline == '' and tp.readLine(): + while tp.lastline == "" and tp.readLine(): pass # TODO: hacky, might need to change tp.readline returning read bytes instead diff --git a/src/wfuzz/externals/reqresp/Response.py b/src/wfuzz/externals/reqresp/Response.py index a8a13c7b..52a7bd97 100644 --- a/src/wfuzz/externals/reqresp/Response.py +++ b/src/wfuzz/externals/reqresp/Response.py @@ -1,14 +1,13 @@ import re import cgi -import string from io import BytesIO import gzip import zlib from .TextParser import TextParser -from wfuzz.utils import python2_3_convert_from_unicode +from wfuzz.helpers.str_func import python2_3_convert_from_unicode def get_encoding_from_headers(headers): @@ -18,24 +17,24 @@ def get_encoding_from_headers(headers): :rtype: str """ - content_type = headers.get('Content-Type') + content_type = headers.get("Content-Type") if not content_type: return None content_type, params = cgi.parse_header(content_type) - if 'charset' in params: - return params['charset'].strip("'\"") + if "charset" in params: + return params["charset"].strip("'\"") - if 'text' in content_type: - return 'ISO-8859-1' + if "text" in content_type: + return "ISO-8859-1" - if 'image' in content_type: - return 'utf-8' + if "image" in content_type: + return "utf-8" - if 'application/json' in content_type: - return 'utf-8' + if "application/json" in content_type: + return "utf-8" def get_encodings_from_content(content): @@ -47,24 +46,27 @@ def get_encodings_from_content(content): pragma_re = re.compile(r']', flags=re.I) xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]') - return (charset_re.findall(content) + - pragma_re.findall(content) + - xml_re.findall(content)) + return ( + charset_re.findall(content) + + pragma_re.findall(content) + + xml_re.findall(content) + ) class Response: def __init__(self, protocol="", code="", message=""): - self.protocol = protocol # HTTP/1.1 - self.code = code # 200 - self.message = message # OK - self._headers = [] # bueno pues las cabeceras igual que en la request - self.__content = "" # contenido de la response (si i solo si Content-Length existe) - self.md5 = "" # hash de los contenidos del resultado - self.charlen = "" # Cantidad de caracteres de la respuesta + self.protocol = protocol # HTTP/1.1 + self.code = code # 200 + self.message = message # OK + self._headers = [] # bueno pues las cabeceras igual que en la request + self.__content = ( + "" # contenido de la response (si i solo si Content-Length existe) + ) + self.md5 = "" # hash de los contenidos del resultado + self.charlen = "" # Cantidad de caracteres de la respuesta def addHeader(self, key, value): - k = string.capwords(key, "-") - self._headers += [(k, value)] + self._headers += [(key, value)] def delHeader(self, key): for i in self._headers: @@ -112,7 +114,9 @@ def getContent(self): return self.__content def getTextHeaders(self): - string = str(self.protocol) + " " + str(self.code) + " " + str(self.message) + "\r\n" + string = ( + str(self.protocol) + " " + str(self.code) + " " + str(self.message) + "\r\n" + ) for i, j in self._headers: string += i + ": " + j + "\r\n" @@ -128,7 +132,9 @@ def Substitute(self, src, dst): self.parseResponse(b) def getAll_wpost(self): - string = str(self.protocol) + " " + str(self.code) + " " + str(self.message) + "\r\n" + string = ( + str(self.protocol) + " " + str(self.code) + " " + str(self.message) + "\r\n" + ) for i, j in self._headers: string += i + ": " + j + "\r\n" return string @@ -162,7 +168,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): while True: tp.readLine() - if (tp.search("^([^:]+): ?(.*)$")): + if tp.search("^([^:]+): ?(.*)$"): self.addHeader(tp[0][0], tp[0][1]) else: break @@ -176,7 +182,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): self._headers = [] # ignore CRLFs until request line - while tp.lastline == '' and tp.readLine(): + while tp.lastline == "" and tp.readLine(): pass # TODO: this should be added to rawbody not directly to __content @@ -186,7 +192,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): while tp.skip(1): self.addContent(tp.lastFull_line) - if type == 'curl': + if type == "curl": self.delHeader("Transfer-Encoding") if self.header_equal("Transfer-Encoding", "chunked"): @@ -220,7 +226,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): deflated_data = deflater.decompress(rawbody) deflated_data += deflater.flush() except zlib.error: - deflated_data = '' + deflated_data = "" rawbody = deflated_data self.delHeader("Content-Encoding") @@ -232,4 +238,6 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): if content_encoding is None: content_encoding = "utf-8" - self.__content = python2_3_convert_from_unicode(rawbody.decode(content_encoding, errors='replace')) + self.__content = python2_3_convert_from_unicode( + rawbody.decode(content_encoding, errors="replace") + ) diff --git a/src/wfuzz/externals/reqresp/TextParser.py b/src/wfuzz/externals/reqresp/TextParser.py index c9780478..a8553db8 100755 --- a/src/wfuzz/externals/reqresp/TextParser.py +++ b/src/wfuzz/externals/reqresp/TextParser.py @@ -39,10 +39,10 @@ def __next__(self): raise StopIteration def setSource(self, t, *args): - '''Se especifica el tipo de entrada. Puede ser fichero o entrada estandard + """Se especifica el tipo de entrada. Puede ser fichero o entrada estandard Ejemplos: setSource("file","/tmp/file") - setSource("stdin")\n''' + setSource("stdin")\n""" if t == "file": self.type = t @@ -70,9 +70,9 @@ def readUntil(self, pattern, caseSens=True): "Lee lineas hasta que el patron (pattern) conincide en alguna linea" while True: - if (self.readLine() == 0): + if self.readLine() == 0: return False - if (self.search(pattern, caseSens) is True): + if self.search(pattern, caseSens) is True: break return True @@ -91,8 +91,8 @@ def search(self, pattern, caseSens=True, debug=0): self.matches[j] = tuple([self.matches[j]]) j += 1 -# DEBUG PARA MATCHING - if (debug == 1): + # DEBUG PARA MATCHING + if debug == 1: print(("[", self.lastline, "-", pattern, "]")) print((len(self.matches))) print((self.matches)) @@ -111,7 +111,7 @@ def skip(self, lines): "Salta las lines que se indiquen en el parametro" for i in range(lines): - if (self.readLine() == 0): + if self.readLine() == 0: return False return True @@ -130,22 +130,22 @@ def readLine(self): if self.oldindex >= 0: self.newindex = self.string.find("\n", self.oldindex, len(self.string)) if self.newindex == -1: - self.lastFull_line = self.string[self.oldindex:len(self.string)] + self.lastFull_line = self.string[self.oldindex : len(self.string)] else: - self.lastFull_line = self.string[self.oldindex:self.newindex + 1] + self.lastFull_line = self.string[self.oldindex : self.newindex + 1] self.oldindex = self.newindex + 1 else: - self.lastFull_line = '' + self.lastFull_line = "" bytes_read = len(self.lastFull_line) s = self.lastFull_line self.lastline = s - if s[-2:] == '\r\n': + if s[-2:] == "\r\n": self.lastline = s[:-2] - elif s[-1:] == '\r' or s[-1:] == '\n': + elif s[-1:] == "\r" or s[-1:] == "\n": self.lastline = s[:-1] return bytes_read diff --git a/src/wfuzz/externals/reqresp/Variables.py b/src/wfuzz/externals/reqresp/Variables.py index 8058f8ea..ebfefeab 100644 --- a/src/wfuzz/externals/reqresp/Variables.py +++ b/src/wfuzz/externals/reqresp/Variables.py @@ -60,7 +60,12 @@ def getVariable(self, name): return dicc[0] def urlEncoded(self): - return "&".join(["=".join([i.name, i.value]) if i.value is not None else i.name for i in self.variables]) + return "&".join( + [ + "=".join([i.name, i.value]) if i.value is not None else i.name + for i in self.variables + ] + ) def json_encoded(self): dicc = {i.name: i.value for i in self.variables} @@ -78,8 +83,8 @@ def parse_json_encoded(self, cad): def parseUrlEncoded(self, cad): dicc = [] - if cad == '': - dicc.append(Variable('', None)) + if cad == "": + dicc.append(Variable("", None)) for i in cad.split("&"): if i: @@ -109,7 +114,7 @@ def parseMultipart(self, cad, boundary): while True: headers = [] - if not tp.readUntil("name=\"([^\"]+)\""): + if not tp.readUntil('name="([^"]+)"'): break var = tp[0][0] headers.append(tp.lastFull_line.strip()) diff --git a/src/wfuzz/externals/reqresp/cache.py b/src/wfuzz/externals/reqresp/cache.py index 1a3d76ac..7959be98 100644 --- a/src/wfuzz/externals/reqresp/cache.py +++ b/src/wfuzz/externals/reqresp/cache.py @@ -6,7 +6,7 @@ def __init__(self): # cache control self.__cache_map = defaultdict(list) - def update_cache(self, req, category='default'): + def update_cache(self, req, category="default"): key = req.to_cache_key() # first hit @@ -19,7 +19,7 @@ def update_cache(self, req, category='default'): return False - def msg_in_cache(self, req, category='default'): + def msg_in_cache(self, req, category="default"): key = req.to_cache_key() return key in self.__cache_map and category in self.__cache_map[key] diff --git a/src/wfuzz/externals/reqresp/exceptions.py b/src/wfuzz/externals/reqresp/exceptions.py index 939ee32a..b095d908 100644 --- a/src/wfuzz/externals/reqresp/exceptions.py +++ b/src/wfuzz/externals/reqresp/exceptions.py @@ -1,4 +1,3 @@ - class ReqRespException(Exception): FATAL, RESOLVE_PROXY, RESOLVE_HOST, CONNECT_HOST, SSL, TIMEOUT = list(range(6)) diff --git a/src/wfuzz/externals/settings/settings.py b/src/wfuzz/externals/settings/settings.py index 1e46c908..1dd7e131 100644 --- a/src/wfuzz/externals/settings/settings.py +++ b/src/wfuzz/externals/settings/settings.py @@ -11,21 +11,24 @@ class SettingsBase: """ Contains application settings. uses a ConfigParser """ + def __init__(self, save=False): self.cparser = ConfigParser() self.set_all(self.set_defaults()) - self.filename = os.path.join(self._path_to_program_dir(), self.get_config_file()) + self.filename = os.path.join( + self._path_to_program_dir(), self.get_config_file() + ) self.cparser.read(self.filename) # Base members should implement def get_config_file(self): - '''Returns the name of the file where the config is saved.''' + """Returns the name of the file where the config is saved.""" raise NotImplementedError def set_defaults(self): - ''' + """ Returns a dictionary with the default settings in the form of { \ Section: [ \ @@ -35,7 +38,7 @@ def set_defaults(self): ], ... } - ''' + """ raise NotImplementedError def has_option(self, section, setting): @@ -78,7 +81,7 @@ def set_all(self, sett): def save(self): try: - with open(self.filename, 'w') as iniFile: + with open(self.filename, "w") as iniFile: self.cparser.write(iniFile) except Exception: return False @@ -94,6 +97,6 @@ def _path_to_program_dir(self): path = os.path.dirname(path) if not path: - return '.' + return "." return path diff --git a/src/wfuzz/facade.py b/src/wfuzz/facade.py index a1cd80b7..b93227d4 100644 --- a/src/wfuzz/facade.py +++ b/src/wfuzz/facade.py @@ -1,4 +1,5 @@ -from . import utils +from .helpers.file_func import get_home, get_path +from .helpers.obj_factory import Singleton from . import __version__ as version from .externals.moduleman.registrant import MulRegistrant from .externals.moduleman.loader import DirLoader @@ -16,28 +17,30 @@ class Settings(SettingsBase): def get_config_file(self): - return os.path.join(utils.get_home(check=True), "wfuzz.ini") + return os.path.join(get_home(check=True), "wfuzz.ini") def set_defaults(self): return dict( - plugins=[ - ("bing_apikey", ''), - ("shodan_apikey", '') + plugins=[("bing_apikey", ""), ("shodan_apikey", "")], + kbase=[ + ( + "discovery.blacklist", + ".svg-.css-.js-.jpg-.gif-.png-.jpeg-.mov-.avi-.flv-.ico", + ) ], - kbase=[("discovery.blacklist", '.svg-.css-.js-.jpg-.gif-.png-.jpeg-.mov-.avi-.flv-.ico')], connection=[ - ("concurrent", '10'), - ("conn_delay", '90'), - ("req_delay", '90'), - ("retries", '3'), - ("User-Agent", "Wfuzz/%s" % version) + ("concurrent", "10"), + ("conn_delay", "90"), + ("req_delay", "90"), + ("retries", "3"), + ("User-Agent", "Wfuzz/%s" % version), ], general=[ - ("default_printer", 'raw'), + ("default_printer", "raw"), ("cancel_on_plugin_except", "0"), - ("concurrent_plugins", '3'), - ("lookup_dirs", '.'), - ("encode_space", '1') + ("concurrent_plugins", "3"), + ("lookup_dirs", "."), + ("encode_space", "1"), ], ) @@ -47,19 +50,17 @@ def get_plugin(self, identifier): try: return MulRegistrant.get_plugin(self, identifier) except KeyError as e: - raise FuzzExceptNoPluginError("Requested plugin %s. Error: %s" % (identifier, str(e))) + raise FuzzExceptNoPluginError( + "Requested plugin %s. Error: %s" % (identifier, str(e)) + ) # python2 and 3: class Facade(metaclass=utils.Singleton): -class Facade(with_metaclass(utils.Singleton, object)): +class Facade(with_metaclass(Singleton, object)): def __init__(self): self.__plugins = dict( - printers=None, - scripts=None, - encoders=None, - iterators=None, - payloads=None, + printers=None, scripts=None, encoders=None, iterators=None, payloads=None, ) self.sett = Settings() @@ -71,8 +72,12 @@ def _load(self, cat): if not self.__plugins[cat]: loader_list = [] - loader_list.append(DirLoader(**{"base_dir": cat, "base_path": utils.get_path("plugins")})) - loader_list.append(DirLoader(**{"base_dir": cat, "base_path": utils.get_home()})) + loader_list.append( + DirLoader(**{"base_dir": cat, "base_path": get_path("../plugins")}) + ) + loader_list.append( + DirLoader(**{"base_dir": cat, "base_path": get_home()}) + ) self.__plugins[cat] = MyRegistrant(loader_list) return self.__plugins[cat] diff --git a/src/wfuzz/factories/__init__.py b/src/wfuzz/factories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/wfuzz/factories/dictfactory.py b/src/wfuzz/factories/dictfactory.py new file mode 100644 index 00000000..3e75e6f8 --- /dev/null +++ b/src/wfuzz/factories/dictfactory.py @@ -0,0 +1,127 @@ +# Python 2 and 3: zip_longest +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest + +from ..helpers.obj_factory import ObjectFactory +from ..exception import FuzzExceptBadOptions +from ..facade import Facade +from ..dictionaries import ( + TupleIt, + WrapperIt, + SliceIt, + EncodeIt, + AllVarDictio, +) + + +class DictionaryFactory(ObjectFactory): + def __init__(self): + ObjectFactory.__init__( + self, + { + "dictio_from_iterable": DictioFromIterableBuilder(), + "dictio_from_payload": DictioFromPayloadBuilder(), + "dictio_from_allvar": DictioFromAllVarBuilder(), + "dictio_from_options": DictioFromOptions(), + }, + ) + + +class BaseDictioBuilder: + @staticmethod + def validate(options, selected_dic): + if not selected_dic: + raise FuzzExceptBadOptions("Empty dictionary! Check payload and filter") + + if len(selected_dic) == 1 and options["iterator"]: + raise FuzzExceptBadOptions( + "Several dictionaries must be used when specifying an iterator" + ) + + @staticmethod + def get_dictio(options, selected_dic): + if len(selected_dic) == 1: + return TupleIt(selected_dic[0]) + elif options["iterator"]: + return Facade().iterators.get_plugin(options["iterator"])(*selected_dic) + else: + return Facade().iterators.get_plugin("product")(*selected_dic) + + +class DictioFromIterableBuilder(BaseDictioBuilder): + def __call__(self, options): + selected_dic = [] + self._payload_list = [] + + for d in [WrapperIt(x) for x in options["dictio"]]: + selected_dic.append(d) + + self.validate(options, selected_dic) + + return self.get_dictio(options, selected_dic) + + +class DictioFromPayloadBuilder(BaseDictioBuilder): + def __call__(self, options): + selected_dic = [] + + for payload in options["payloads"]: + try: + name, params, slicestr = [ + x[0] for x in zip_longest(payload, (None, None, None)) + ] + except ValueError: + raise FuzzExceptBadOptions( + "You must supply a list of payloads in the form of [(name, {params}), ... ]" + ) + + if not params: + raise FuzzExceptBadOptions( + "You must supply a list of payloads in the form of [(name, {params}), ... ]" + ) + + dictionary = Facade().payloads.get_plugin(name)(params) + if "encoder" in params and params["encoder"] is not None: + dictionary = EncodeIt(dictionary, params["encoder"]) + + selected_dic.append( + SliceIt(dictionary, slicestr) if slicestr else dictionary + ) + + self.validate(options, selected_dic) + + return self.get_dictio(options, selected_dic) + + +class DictioFromAllVarBuilder(BaseDictioBuilder): + @staticmethod + def from_all_fuzz_request_gen(options, dictio_list): + for payload in dictio_list: + if len(payload) > 1: + raise FuzzExceptBadOptions( + "Only one payload is allowed when fuzzing all parameters!" + ) + + for var_name in options["compiled_seed"].history.wf_allvars_set.keys(): + yield (var_name, payload[0]) + + def __call__(self, options): + dictio_list = DictioFromOptions()(options) + + return AllVarDictio( + self.from_all_fuzz_request_gen(options, dictio_list), + dictio_list.count() * len(options["compiled_seed"].history.wf_allvars_set), + ) + + +class DictioFromOptions(BaseDictioBuilder): + def __call__(self, options): + if options["dictio"]: + return DictioFromIterableBuilder()(options) + else: + return DictioFromPayloadBuilder()(options) + + +dictionary_factory = DictionaryFactory() diff --git a/src/wfuzz/factories/fuzzfactory.py b/src/wfuzz/factories/fuzzfactory.py new file mode 100644 index 00000000..18a113ec --- /dev/null +++ b/src/wfuzz/factories/fuzzfactory.py @@ -0,0 +1,37 @@ +from ..fuzzrequest import FuzzRequest + +from ..helpers.obj_factory import ObjectFactory, SeedBuilderHelper + + +class FuzzRequestFactory(ObjectFactory): + def __init__(self): + ObjectFactory.__init__( + self, + { + "request_from_options": RequestBuilder(), + "seed_from_options": SeedBuilder(), + }, + ) + + +class RequestBuilder: + def __call__(self, options): + fr = FuzzRequest() + + fr.url = options["url"] + fr.wf_fuzz_methods = options["method"] + fr.update_from_options(options) + + return fr + + +class SeedBuilder: + def __call__(self, options): + seed = reqfactory.create("request_from_options", options) + marker_dict = SeedBuilderHelper.get_marker_dict(seed) + SeedBuilderHelper.remove_baseline_markers(seed, marker_dict) + + return seed + + +reqfactory = FuzzRequestFactory() diff --git a/src/wfuzz/factories/fuzzresfactory.py b/src/wfuzz/factories/fuzzresfactory.py new file mode 100644 index 00000000..3ddd5828 --- /dev/null +++ b/src/wfuzz/factories/fuzzresfactory.py @@ -0,0 +1,125 @@ +import copy + +from .fuzzfactory import reqfactory +from .payman import payman_factory + +from ..fuzzobjects import FuzzResult, FuzzType, FuzzWord, FuzzWordType +from ..helpers.obj_factory import ObjectFactory, SeedBuilderHelper + + +class FuzzResultFactory(ObjectFactory): + def __init__(self): + ObjectFactory.__init__( + self, + { + "fuzzres_from_options_and_dict": FuzzResultDictioBuilder(), + "fuzzres_from_allvar": FuzzResultAllVarBuilder(), + "fuzzres_from_recursion": FuzzResRecursiveBuilder(), + "seed_from_recursion": SeedRecursiveBuilder(), + "seed_from_options": SeedResultBuilder(), + "seed_from_options_and_dict": FuzzResultDictSeedBuilder(), + "baseline_from_options": BaselineResultBuilder(), + }, + ) + + +class FuzzResultDictioBuilder: + def __call__(self, options, dictio_item): + res = copy.deepcopy(options["compiled_seed"]) + res.item_type = FuzzType.RESULT + res.payload_man.update_from_dictio(dictio_item) + res.update_from_options(options) + + SeedBuilderHelper.replace_markers(res.history, res.payload_man) + res.nres = next(FuzzResult.newid) + + return res + + +class SeedResultBuilder: + def __call__(self, options): + seed = reqfactory.create("seed_from_options", options) + res = FuzzResult(seed) + res.payload_man = payman_factory.create("payloadman_from_request", seed) + + return res + + +class BaselineResultBuilder: + def __call__(self, options): + raw_seed = reqfactory.create("request_from_options", options) + baseline_payloadman = payman_factory.create( + "payloadman_from_baseline", raw_seed + ) + + if baseline_payloadman.payloads: + res = FuzzResult(raw_seed) + res.payload_man = baseline_payloadman + res.update_from_options(options) + res.is_baseline = True + + SeedBuilderHelper.replace_markers(raw_seed, baseline_payloadman) + + return res + else: + return None + + +class FuzzResultAllVarBuilder: + def __call__(self, options, var_name, payload): + fuzzres = copy.deepcopy(options["compiled_seed"]) + fuzzres.item_type = FuzzType.RESULT + fuzzres.payload_man = payman_factory.create("empty_payloadman", payload) + fuzzres.payload_man.update_from_dictio([payload]) + fuzzres.history.wf_allvars_set = {var_name: payload.content} + fuzzres.nres = next(FuzzResult.newid) + + return fuzzres + + +class FuzzResultDictSeedBuilder: + def __call__(self, options, dictio): + fuzzres = copy.deepcopy(dictio[0].content) + fuzzres.history.update_from_options(options) + fuzzres.update_from_options(options) + fuzzres.payload_man = payman_factory.create("empty_payloadman", dictio[0]) + fuzzres.payload_man.update_from_dictio(dictio) + + return fuzzres + + +class SeedRecursiveBuilder: + def __call__(self, seed): + new_seed = copy.deepcopy(seed) + new_seed.history.url = seed.history.recursive_url + "FUZZ" + new_seed.rlevel += 1 + if new_seed.rlevel_desc: + new_seed.rlevel_desc += " - " + new_seed.rlevel_desc += seed.payload_man.description() + new_seed.item_type = FuzzType.SEED + new_seed.payload_man = payman_factory.create( + "payloadman_from_request", new_seed.history + ) + + return new_seed + + +class FuzzResRecursiveBuilder: + def __call__(self, seed, url): + fr = copy.deepcopy(seed) + fr.history.url = str(url) + fr.rlevel = seed.rlevel + 1 + if fr.rlevel_desc: + fr.rlevel_desc += " - " + fr.rlevel_desc += seed.payload_man.description() + fr.item_type = FuzzType.BACKFEED + fr.is_baseline = False + + fr.payload_man = payman_factory.create( + "empty_payloadman", FuzzWord(url, FuzzWordType.WORD) + ) + + return fr + + +resfactory = FuzzResultFactory() diff --git a/src/wfuzz/factories/payman.py b/src/wfuzz/factories/payman.py new file mode 100644 index 00000000..600df9ca --- /dev/null +++ b/src/wfuzz/factories/payman.py @@ -0,0 +1,56 @@ +from ..fuzzobjects import FPayloadManager, FuzzWord, FuzzWordType + +from ..helpers.obj_factory import ObjectFactory, SeedBuilderHelper + + +class PayManFactory(ObjectFactory): + def __init__(self): + ObjectFactory.__init__( + self, + { + "payloadman_from_baseline": BaselinePayloadManBuilder(), + "payloadman_from_request": FuzzReqPayloadManBuilder(), + "empty_payloadman": OnePayloadManBuilder(), + }, + ) + + +class FuzzReqPayloadManBuilder: + def __call__(self, freq): + fpm = FPayloadManager() + + for pdict in [ + pdict + for pdict in SeedBuilderHelper.get_marker_dict(freq) + if pdict["word"] is not None + ]: + fpm.add(pdict) + + return fpm + + +class OnePayloadManBuilder: + def __call__(self, content): + fpm = FPayloadManager() + fpm.add( + {"full_marker": None, "word": None, "index": None, "field": None}, content + ) + + return fpm + + +class BaselinePayloadManBuilder: + def __call__(self, freq): + fpm = FPayloadManager() + + for pdict in [ + pdict + for pdict in SeedBuilderHelper.get_marker_dict(freq) + if pdict["bl_value"] is not None + ]: + fpm.add(pdict, FuzzWord(pdict["bl_value"], FuzzWordType.WORD), True) + + return fpm + + +payman_factory = PayManFactory() diff --git a/src/wfuzz/factories/plugin_factory.py b/src/wfuzz/factories/plugin_factory.py new file mode 100644 index 00000000..4a558341 --- /dev/null +++ b/src/wfuzz/factories/plugin_factory.py @@ -0,0 +1,51 @@ +from ..helpers.obj_factory import ObjectFactory + +from ..fuzzobjects import FuzzPlugin, FuzzError +from ..factories.fuzzresfactory import resfactory + + +class PluginFactory(ObjectFactory): + def __init__(self): + ObjectFactory.__init__( + self, + { + "plugin_from_recursion": PluginRecursiveBuilder(), + "plugin_from_error": PluginErrorBuilder(), + "plugin_from_finding": PluginFindingBuilder(), + }, + ) + + +class PluginRecursiveBuilder: + def __call__(self, name, seed, url): + plugin = FuzzPlugin() + plugin.source = name + plugin._exception = None + plugin._seed = resfactory.create("fuzzres_from_recursion", seed, url) + + return plugin + + +class PluginErrorBuilder: + def __call__(self, name, exception): + plugin = FuzzPlugin() + plugin.source = name + plugin.issue = "Exception within plugin %s: %s" % (name, str(exception)) + plugin._exception = FuzzError(exception) + plugin._seed = None + + return plugin + + +class PluginFindingBuilder: + def __call__(self, name, message): + plugin = FuzzPlugin() + plugin.source = name + plugin.issue = message + plugin._exception = None + plugin._seed = None + + return plugin + + +plugin_factory = PluginFactory() diff --git a/src/wfuzz/factories/reqresp_factory.py b/src/wfuzz/factories/reqresp_factory.py new file mode 100644 index 00000000..1c39d337 --- /dev/null +++ b/src/wfuzz/factories/reqresp_factory.py @@ -0,0 +1,116 @@ +import abc + +import pycurl + +from ..helpers.obj_factory import HttpRequestFactory +from ..helpers.str_func import ( + python2_3_convert_to_unicode, + python2_3_convert_from_unicode, +) + + +from ..externals.reqresp import Response + + +PYCURL_PATH_AS_IS = True +if not hasattr(pycurl, "PATH_AS_IS"): + PYCURL_PATH_AS_IS = False + + +class ReqRespRequestFactory(HttpRequestFactory): + def to_http_object(options, req, pycurl_c): + pycurl_c.setopt(pycurl.MAXREDIRS, 5) + + pycurl_c.setopt(pycurl.WRITEFUNCTION, req._request.body_callback) + pycurl_c.setopt(pycurl.HEADERFUNCTION, req._request.header_callback) + + pycurl_c.setopt(pycurl.NOSIGNAL, 1) + pycurl_c.setopt(pycurl.SSL_VERIFYPEER, False) + pycurl_c.setopt(pycurl.SSL_VERIFYHOST, 0) + + if PYCURL_PATH_AS_IS: + pycurl_c.setopt(pycurl.PATH_AS_IS, 1) + + pycurl_c.setopt( + pycurl.URL, python2_3_convert_to_unicode(req._request.completeUrl) + ) + + if req._request.getConnTimeout(): + pycurl_c.setopt(pycurl.CONNECTTIMEOUT, req._request.getConnTimeout()) + + if req._request.getTotalTimeout(): + pycurl_c.setopt(pycurl.TIMEOUT, req._request.getTotalTimeout()) + + authMethod, userpass = req._request.getAuth() + if authMethod or userpass: + if authMethod == "basic": + pycurl_c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) + elif authMethod == "ntlm": + pycurl_c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_NTLM) + elif authMethod == "digest": + pycurl_c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST) + pycurl_c.setopt(pycurl.USERPWD, python2_3_convert_to_unicode(userpass)) + else: + pycurl_c.unsetopt(pycurl.USERPWD) + + pycurl_c.setopt( + pycurl.HTTPHEADER, python2_3_convert_to_unicode(req._request.getHeaders()) + ) + + curl_options = { + "GET": pycurl.HTTPGET, + "POST": pycurl.POST, + "PATCH": pycurl.UPLOAD, + "HEAD": pycurl.NOBODY, + } + + for verb in curl_options.values(): + pycurl_c.setopt(verb, False) + + if req._request.method in curl_options: + pycurl_c.unsetopt(pycurl.CUSTOMREQUEST) + pycurl_c.setopt(curl_options[req._request.method], True) + else: + pycurl_c.setopt(pycurl.CUSTOMREQUEST, req._request.method) + + if req._request._non_parsed_post is not None: + pycurl_c.setopt( + pycurl.POSTFIELDS, + python2_3_convert_to_unicode(req._request._non_parsed_post), + ) + + pycurl_c.setopt(pycurl.FOLLOWLOCATION, 1 if req._request.followLocation else 0) + + # proxy = req._request.getProxy() + # if proxy is not None: + # pycurl_c.setopt(pycurl.PROXY, python2_3_convert_to_unicode(proxy)) + # if req._request.proxytype == "SOCKS5": + # pycurl_c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5) + # elif req._request.proxytype == "SOCKS4": + # pycurl_c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS4) + # req._request.delHeader("Proxy-Connection") + # else: + # pycurl_c.setopt(pycurl.PROXY, "") + + if req.wf_ip: + pycurl_c.setopt( + pycurl.CONNECT_TO, + ["::{}:{}".format(req.wf_ip["ip"], req.wf_ip["port"])], + ) + + return pycurl_c + + def from_http_object(options, req, pycurl_c, header, body): + raw_header = python2_3_convert_from_unicode( + header.decode("utf-8", errors="surrogateescape") + ) + + if pycurl_c.getinfo(pycurl.EFFECTIVE_URL) != req._request.completeUrl: + req._request.setFinalUrl(pycurl_c.getinfo(pycurl.EFFECTIVE_URL)) + + req._request.totaltime = pycurl_c.getinfo(pycurl.TOTAL_TIME) + + req._request.response = Response() + req._request.response.parseResponse(raw_header, rawbody=body) + + return req._request.response diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py deleted file mode 100644 index 8c9abd7f..00000000 --- a/src/wfuzz/filter.py +++ /dev/null @@ -1,359 +0,0 @@ -from .exception import FuzzExceptIncorrectFilter, FuzzExceptBadOptions, FuzzExceptInternalError, FuzzException -from .utils import rgetattr, rsetattr, value_in_any_list_item - -import re -import collections -import operator - -# Python 2 and 3: alternative 4 -try: - from urllib.parse import unquote -except ImportError: - from urllib import unquote - -from .facade import Facade, ERROR_CODE, BASELINE_CODE - - -PYPARSING = True -try: - from pyparsing import Word, Group, oneOf, Optional, Suppress, ZeroOrMore, Literal, alphas, QuotedString - from pyparsing import ParseException -except ImportError: - PYPARSING = False - - -class FuzzResFilter: - def __init__(self, ffilter=None, filter_string=None): - if PYPARSING: - quoted_str_value = QuotedString('\'', unquoteResults=True, escChar='\\') - int_values = Word("0123456789").setParseAction(lambda s, l, t: [int(t[0])]) - error_value = Literal("XXX").setParseAction(self.__compute_xxx_value) - bbb_value = Literal("BBB").setParseAction(self.__compute_bbb_value) - field_value = Word(alphas + "." + "_" + "-") - reserverd_words = oneOf("BBB XXX") - - basic_primitives = int_values | quoted_str_value - - operator_names = oneOf("m d e un u r l sw gre gregex unique startswith decode encode unquote replace lower upper").setParseAction(lambda s, l, t: [(l, t[0])]) - - fuzz_symbol = (Suppress("FUZ") + Optional(Word("23456789"), 1).setParseAction(lambda s, l, t: [int(t[0]) - 1]) + Suppress("Z")).setParseAction(self._compute_fuzz_symbol) - operator_call = Group(Suppress("|") + operator_names + Suppress("(") + Optional(basic_primitives, None) + Optional(Suppress(",") + basic_primitives, None) + Suppress(")")) - - fuzz_value = (fuzz_symbol + Optional(Suppress("[") + field_value + Suppress("]"), None)).setParseAction(self.__compute_fuzz_value) - fuzz_value_op = ((fuzz_symbol + Suppress("[") + Optional(field_value)).setParseAction(self.__compute_fuzz_value) + operator_call + Suppress("]")).setParseAction(self.__compute_perl_value) - fuzz_value_op2 = ((fuzz_symbol + operator_call).setParseAction(self.__compute_perl_value)) - - res_value_op = (~reserverd_words + Word("0123456789" + alphas + "." + "_" + "-").setParseAction(self.__compute_res_value) + Optional(operator_call, None)).setParseAction(self.__compute_perl_value) - basic_primitives_op = (basic_primitives + Optional(operator_call, None)).setParseAction(self.__compute_perl_value) - - fuzz_statement = basic_primitives_op ^ fuzz_value ^ fuzz_value_op ^ fuzz_value_op2 ^ res_value_op - - operator = oneOf("and or") - not_operator = Optional(oneOf("not"), "notpresent") - - symbol_expr = Group(fuzz_statement + oneOf("= == != < > >= <= =~ !~ ~ := =+ =-") + (bbb_value ^ error_value ^ basic_primitives ^ fuzz_statement)).setParseAction(self.__compute_expr) - - definition = symbol_expr ^ fuzz_statement - definition_not = not_operator + definition - definition_expr = definition_not + ZeroOrMore(operator + definition_not) - - nested_definition = Group(Suppress("(") + definition_expr + Suppress(")")) - nested_definition_not = not_operator + nested_definition - - self.finalformula = (nested_definition_not ^ definition_expr) + ZeroOrMore(operator + (nested_definition_not ^ definition_expr)) - - definition_not.setParseAction(self.__compute_not_operator) - nested_definition_not.setParseAction(self.__compute_not_operator) - nested_definition.setParseAction(self.__compute_formula) - self.finalformula.setParseAction(self.__myreduce) - - if ffilter is not None and filter_string is not None: - raise FuzzExceptInternalError(FuzzException.FATAL, "A filter must be initilized with a filter string or an object, not both") - - self.res = None - if ffilter: - self.hideparams = ffilter - else: - self.hideparams = dict( - regex_show=None, - codes_show=None, - codes=[], - words=[], - lines=[], - chars=[], - regex=None, - filter_string="" - ) - - if filter_string: - self.hideparams['filter_string'] = filter_string - - self.baseline = None - self.stack = [] - - self._cache = collections.defaultdict(set) - - def set_baseline(self, res): - if BASELINE_CODE in self.hideparams['lines']: - self.hideparams['lines'].append(res.lines) - if BASELINE_CODE in self.hideparams['codes']: - self.hideparams['codes'].append(res.code) - if BASELINE_CODE in self.hideparams['words']: - self.hideparams['words'].append(res.words) - if BASELINE_CODE in self.hideparams['chars']: - self.hideparams['chars'].append(res.chars) - - self.baseline = res - - def __compute_res_value(self, tokens): - self.stack.append(tokens[0]) - - try: - return rgetattr(self.res, tokens[0]) - except AttributeError: - raise FuzzExceptIncorrectFilter("Non-existing introspection field or HTTP parameter \"{}\"!".format(tokens[0])) - - def _compute_fuzz_symbol(self, tokens): - i = tokens[0] - - try: - return self.res.payload[i].content - except IndexError: - raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") - - def __compute_fuzz_value(self, tokens): - fuzz_val, field = tokens - - self.stack.append(field) - - try: - return rgetattr(fuzz_val, field) if field else fuzz_val - except IndexError: - raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") - except AttributeError as e: - raise FuzzExceptIncorrectFilter("A field expression must be used with a fuzzresult payload not a string. %s" % str(e)) - - def __compute_bbb_value(self, tokens): - element = self.stack[0] if self.stack else None - - if self.baseline is None: - raise FuzzExceptBadOptions("FilterQ: specify a baseline value when using BBB") - - if element == 'l' or element == 'lines': - ret = self.baseline.lines - elif element == 'c' or element == 'code': - ret = self.baseline.code - elif element == 'w' or element == 'words': - ret = self.baseline.words - elif element == 'h' or element == 'chars': - return self.baseline.chars - elif element == 'index' or element == 'i': - ret = self.baseline.nres - - return str(ret) - - def __compute_perl_value(self, tokens): - leftvalue, exp = tokens - # import pdb; pdb.set_trace() - - if exp: - loc_op, middlevalue, rightvalue = exp - loc, op = loc_op - else: - return leftvalue - - if (op == "un" or op == "unquote") and middlevalue is None and rightvalue is None: - ret = unquote(leftvalue) - elif (op == "e" or op == "encode") and middlevalue is not None and rightvalue is None: - ret = Facade().encoders.get_plugin(middlevalue)().encode(leftvalue) - elif (op == "d" or op == "decode") and middlevalue is not None and rightvalue is None: - ret = Facade().encoders.get_plugin(middlevalue)().decode(leftvalue) - elif op == "r" or op == "replace": - return leftvalue.replace(middlevalue, rightvalue) - elif op == "upper": - return leftvalue.upper() - elif op == "lower" or op == "l": - return leftvalue.lower() - elif op == 'gregex' or op == "gre": - search_res = None - try: - regex = re.compile(middlevalue) - search_res = regex.search(leftvalue) - except re.error as e: - raise FuzzExceptBadOptions("Invalid regex expression used in expression: %s" % str(e)) - - if search_res is None: - return '' - return search_res.group(1) - elif op == 'startswith' or op == "sw": - return leftvalue.strip().startswith(middlevalue) - elif op == 'unique' or op == "u": - if leftvalue not in self._cache[loc]: - self._cache[loc].add(leftvalue) - return True - else: - return False - else: - raise FuzzExceptBadOptions("Bad format, expression should be m,d,e,r,s(value,value)") - - return ret - - def __compute_xxx_value(self, tokens): - return ERROR_CODE - - def __compute_expr(self, tokens): - leftvalue, exp_operator, rightvalue = tokens[0] - - field_to_set = self.stack[0] if self.stack else None - - try: - if exp_operator in ["=", '==']: - return str(leftvalue) == str(rightvalue) - elif exp_operator == "<=": - return leftvalue <= rightvalue - elif exp_operator == ">=": - return leftvalue >= rightvalue - elif exp_operator == "<": - return leftvalue < rightvalue - elif exp_operator == ">": - return leftvalue > rightvalue - elif exp_operator == "!=": - return leftvalue != rightvalue - elif exp_operator == "=~": - regex = re.compile(rightvalue, re.MULTILINE | re.DOTALL) - return regex.search(leftvalue) is not None - elif exp_operator in ["!~", "~"]: - ret = True - - if isinstance(leftvalue, str): - ret = rightvalue.lower() in leftvalue.lower() - elif isinstance(leftvalue, list): - ret = value_in_any_list_item(rightvalue, leftvalue) - elif isinstance(leftvalue, dict): - return len({k: v for (k, v) in leftvalue.items() if rightvalue.lower() in k.lower() or value_in_any_list_item(rightvalue, v)}) > 0 - else: - raise FuzzExceptBadOptions("Invalid operand type {}".format(rightvalue)) - - return ret if exp_operator == "~" else not ret - elif exp_operator == ":=": - rsetattr(self.res, field_to_set, rightvalue, None) - elif exp_operator == "=+": - rsetattr(self.res, field_to_set, rightvalue, operator.add) - elif exp_operator == "=-": - rsetattr(self.res, field_to_set, rightvalue, lambda x, y: y + x) - except re.error as e: - raise FuzzExceptBadOptions("Invalid regex expression used in expression: %s" % str(e)) - except TypeError as e: - raise FuzzExceptBadOptions("Invalid operand types used in expression: %s" % str(e)) - except ParseException as e: - raise FuzzExceptBadOptions("Invalid filter: %s" % str(e)) - - return True - - def __myreduce(self, elements): - first = elements[0] - for i in range(1, len(elements), 2): - if elements[i] == "and": - first = (first and elements[i + 1]) - elif elements[i] == "or": - first = (first or elements[i + 1]) - - self.stack = [] - return first - - def __compute_not_operator(self, tokens): - operator, value = tokens - - if operator == "not": - return not value - - return value - - def __compute_formula(self, tokens): - return self.__myreduce(tokens[0]) - - def is_active(self): - return any([ - self.hideparams['regex_show'] is not None, - self.hideparams['codes_show'] is not None, - self.hideparams['filter_string'] != "", - ]) - - def is_visible(self, res): - filter_string = self.hideparams['filter_string'] - if filter_string and PYPARSING: - self.res = res - try: - return self.finalformula.parseString(filter_string, parseAll=True)[0] - except ParseException as e: - raise FuzzExceptIncorrectFilter("Incorrect filter expression, check documentation. {}".format(str(e))) - except AttributeError as e: - raise FuzzExceptIncorrectFilter("It is only possible to use advanced filters when using a non-string payload. %s" % str(e)) - else: - if self.hideparams['codes_show'] is None: - cond1 = True - else: - cond1 = not self.hideparams['codes_show'] - - if self.hideparams['regex_show'] is None: - cond2 = True - else: - cond2 = not self.hideparams['regex_show'] - - if res.code in self.hideparams['codes'] or res.lines in self.hideparams['lines'] \ - or res.words in self.hideparams['words'] or res.chars in self.hideparams['chars']: - cond1 = self.hideparams['codes_show'] - - if self.hideparams['regex']: - if self.hideparams['regex'].search(res.history.content): - cond2 = self.hideparams['regex_show'] - - return (cond1 and cond2) - - @staticmethod - def from_options(filter_options): - ffilter = FuzzResFilter() - - ffilter.hideparams["filter_string"] = filter_options["filter"] - - try: - if filter_options["ss"] is not None: - ffilter.hideparams['regex_show'] = True - ffilter.hideparams['regex'] = re.compile(filter_options['ss'], re.MULTILINE | re.DOTALL) - - elif filter_options["hs"] is not None: - ffilter.hideparams['regex_show'] = False - ffilter.hideparams['regex'] = re.compile(filter_options['hs'], re.MULTILINE | re.DOTALL) - except Exception as e: - raise FuzzExceptBadOptions("Invalid regex expression used in filter: %s" % str(e)) - - if [x for x in ["sc", "sw", "sh", "sl"] if len(filter_options[x]) > 0]: - ffilter.hideparams['codes_show'] = True - ffilter.hideparams['codes'] = filter_options["sc"] - ffilter.hideparams['words'] = filter_options["sw"] - ffilter.hideparams['lines'] = filter_options["sl"] - ffilter.hideparams['chars'] = filter_options["sh"] - elif [x for x in ["hc", "hw", "hh", "hl"] if len(filter_options[x]) > 0]: - ffilter.hideparams['codes_show'] = False - ffilter.hideparams['codes'] = filter_options["hc"] - ffilter.hideparams['words'] = filter_options["hw"] - ffilter.hideparams['lines'] = filter_options["hl"] - ffilter.hideparams['chars'] = filter_options["hh"] - - return ffilter - - def get_fuzz_words(self): - marker_regex = re.compile(r"FUZ\d*Z", re.MULTILINE | re.DOTALL) - fuzz_words = marker_regex.findall(self.hideparams["filter_string"]) - - return fuzz_words - - -class FuzzResFilterSlice(FuzzResFilter): - def _compute_fuzz_symbol(self, tokens): - i = tokens[0] - - if i != 0: - raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") - - return self.res diff --git a/src/wfuzz/filters/__init__.py b/src/wfuzz/filters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/wfuzz/filters/ppfilter.py b/src/wfuzz/filters/ppfilter.py new file mode 100644 index 00000000..50b76704 --- /dev/null +++ b/src/wfuzz/filters/ppfilter.py @@ -0,0 +1,380 @@ +from ..exception import FuzzExceptIncorrectFilter, FuzzExceptBadOptions +from ..helpers.obj_dyn import ( + rgetattr, + rsetattr, +) +from ..helpers.str_func import value_in_any_list_item +from ..helpers.obj_dic import DotDict + +import re +import collections +import operator + +# Python 2 and 3: alternative 4 +try: + from urllib.parse import unquote +except ImportError: + from urllib import unquote + +from ..facade import Facade, ERROR_CODE + + +PYPARSING = True +try: + from pyparsing import ( + Word, + Group, + oneOf, + Optional, + Suppress, + ZeroOrMore, + Literal, + QuotedString, + ParseException, + Regex, + ) +except ImportError: + PYPARSING = False + + +class FuzzResFilter: + FUZZ_MARKER_REGEX = re.compile(r"FUZ\d*Z", re.MULTILINE | re.DOTALL) + + def __init__(self, filter_string=None): + self.filter_string = filter_string + self.baseline = None + + quoted_str_value = QuotedString("'", unquoteResults=True, escChar="\\") + int_values = Word("0123456789").setParseAction(lambda s, l, t: [int(t[0])]) + error_value = Literal("XXX").setParseAction(self.__compute_xxx_value) + + operator_call = Regex( + r"\|(?P(m|d|e|un|u|r|l|sw|gre|gregex|unique|startswith|decode|encode|unquote|replace|lower|upper))" + r"\((?:(?P('.*?'|\d+))(?:,(?P('.*?'|\d+)))?)?\)", + asMatch=True, + ).setParseAction(lambda s, l, t: [(l, t[0])]) + + fuzz_symbol = Regex( + r"FUZ(?P\d)*Z(?:\[(?P(\w|_|-|\.)+)\])?", asMatch=True + ).setParseAction(self._compute_fuzz_symbol) + res_symbol = Regex( + r"(description|nres|code|chars|lines|words|md5|content|timer|url|plugins|l|w|c|(r|history)(\w|_|-|\.)*|h)" + ).setParseAction(self._compute_res_symbol) + bbb_symbol = Regex( + r"BBB(?:\[(?P(\w|_|-|\.)+)\])?", asMatch=True + ).setParseAction(self.__compute_bbb_symbol) + + fuzz_statement = Group( + (fuzz_symbol | res_symbol | bbb_symbol | int_values | quoted_str_value) + + Optional(operator_call, None) + ).setParseAction(self.__compute_res_value) + + operator = oneOf("and or") + not_operator = Optional(oneOf("not"), "notpresent") + + symbol_expr = Group( + fuzz_statement + + oneOf("= == != < > >= <= =~ !~ ~ := =+ =-") + + (error_value | fuzz_statement) + ).setParseAction(self.__compute_expr) + + definition = symbol_expr ^ fuzz_statement + definition_not = not_operator + definition + definition_expr = definition_not + ZeroOrMore(operator + definition_not) + + nested_definition = Group(Suppress("(") + definition_expr + Suppress(")")) + nested_definition_not = not_operator + nested_definition + + self.finalformula = (nested_definition_not | definition_expr) + ZeroOrMore( + operator + (nested_definition_not | definition_expr) + ) + + definition_not.setParseAction(self.__compute_not_operator) + nested_definition_not.setParseAction(self.__compute_not_operator) + nested_definition.setParseAction(self.__compute_formula) + self.finalformula.setParseAction(self.__myreduce) + + self.res = None + self.stack = [] + self._cache = collections.defaultdict(set) + + def set_baseline(self, res): + self.baseline = res + + def _compute_res_symbol(self, tokens): + return self._get_field_value(self.res, tokens[0]) + + def _compute_fuzz_symbol(self, tokens): + match_dict = tokens[0].groupdict() + p_index = int(match_dict["index"]) if match_dict["index"] is not None else 1 + fuzz_val = None + + try: + fuzz_val = self.res.payload_man.get_payload_content(p_index) + except IndexError: + raise FuzzExceptIncorrectFilter( + "Non existent FUZZ payload! Use a correct index." + ) + + if match_dict["field"]: + fuzz_val = self._get_field_value(fuzz_val, match_dict["field"]) + + return fuzz_val + + def __compute_res_value(self, tokens): + fuzz_val, token_tuple = tokens[0] + + if token_tuple: + location, operator_match = token_tuple + + if operator_match and operator_match.groupdict()["operator"]: + fuzz_val = self._get_operator_value( + location, fuzz_val, operator_match.groupdict() + ) + + return fuzz_val + + def _get_payload_value(self, p_index): + try: + return self.res.payload_man.get_payload_content(p_index) + except IndexError: + raise FuzzExceptIncorrectFilter( + "Non existent FUZZ payload! Use a correct index." + ) + + def _get_field_value(self, fuzz_val, field): + self.stack.append(field) + + try: + return rgetattr(fuzz_val, field) + except IndexError: + raise FuzzExceptIncorrectFilter( + "Non existent FUZZ payload! Use a correct index." + ) + except AttributeError as e: + raise FuzzExceptIncorrectFilter( + "Attribute {} not found in fuzzresult or using a string payload. {}".format( + field, str(e) + ) + ) + + def __compute_bbb_symbol(self, tokens): + if self.baseline is None: + raise FuzzExceptBadOptions( + "FilterQ: specify a baseline value when using BBB" + ) + + match_dict = tokens[0].groupdict() + + ret = None + + if match_dict["field"]: + ret = self._get_field_value(self.baseline, match_dict["field"]) + else: + element = self.stack.pop() if self.stack else None + + if element == "l" or element == "lines": + ret = self.baseline.lines + elif element == "c" or element == "code": + ret = self.baseline.code + elif element == "w" or element == "words": + ret = self.baseline.words + elif element == "h" or element == "chars": + return self.baseline.chars + elif element == "index" or element == "i": + ret = self.baseline.nres + else: + ret = self.baseline.payload_man.get_payload_content(1) + + return ret + + def _get_operator_value(self, location, fuzz_val, match_dict): + op = match_dict["operator"] + param1 = match_dict["param1"] + param2 = match_dict["param2"] + + if param1: + param1 = param1.strip("'") + if param2: + param2 = param2.strip("'") + + if (op == "un" or op == "unquote") and param1 is None and param2 is None: + ret = unquote(fuzz_val) + elif (op == "e" or op == "encode") and param1 is not None and param2 is None: + ret = Facade().encoders.get_plugin(param1)().encode(fuzz_val) + elif (op == "d" or op == "decode") and param1 is not None and param2 is None: + ret = Facade().encoders.get_plugin(param1)().decode(fuzz_val) + elif op == "r" or op == "replace": + return fuzz_val.replace(param1, param2) + elif op == "upper": + return fuzz_val.upper() + elif op == "lower" or op == "l": + return fuzz_val.lower() + elif op == "gregex" or op == "gre": + search_res = None + try: + regex = re.compile(param1) + search_res = regex.search(fuzz_val) + except re.error as e: + raise FuzzExceptBadOptions( + "Invalid regex expression used in expression: %s" % str(e) + ) + + if search_res is None: + return "" + return search_res.group(1) + elif op == "startswith" or op == "sw": + return fuzz_val.strip().startswith(param1) + elif op == "unique" or op == "u": + if fuzz_val not in self._cache[location]: + self._cache[location].add(fuzz_val) + return True + else: + return False + else: + raise FuzzExceptBadOptions( + "Bad format, expression should be m,d,e,r,s(value,value)" + ) + + return ret + + def __compute_xxx_value(self, tokens): + return ERROR_CODE + + def __compute_expr(self, tokens): + leftvalue, exp_operator, rightvalue = tokens[0] + + # a bit hacky but we don't care about fields in the right hand side of the expression + if len(self.stack) > 1: + self.stack.pop() + + field_to_set = self.stack.pop() if self.stack else None + + try: + if exp_operator in ["=", "=="]: + return str(leftvalue) == str(rightvalue) + elif exp_operator == "<=": + return int(leftvalue) <= int(rightvalue) + elif exp_operator == ">=": + return int(leftvalue) >= int(rightvalue) + elif exp_operator == "<": + return int(leftvalue) < int(rightvalue) + elif exp_operator == ">": + return int(leftvalue) > int(rightvalue) + elif exp_operator == "!=": + return leftvalue != rightvalue + elif exp_operator == "=~": + regex = re.compile(rightvalue, re.MULTILINE | re.DOTALL) + return regex.search(leftvalue) is not None + elif exp_operator in ["!~", "~"]: + ret = True + + if isinstance(leftvalue, str): + ret = rightvalue.lower() in leftvalue.lower() + elif isinstance(leftvalue, list): + ret = value_in_any_list_item(rightvalue, leftvalue) + elif isinstance(leftvalue, dict) or isinstance(leftvalue, DotDict): + return ( + len( + { + k: v + for (k, v) in leftvalue.items() + if rightvalue.lower() in k.lower() + or value_in_any_list_item(rightvalue, v) + } + ) + > 0 + ) + else: + raise FuzzExceptBadOptions( + "Invalid operand type {}".format(rightvalue) + ) + + return ret if exp_operator == "~" else not ret + elif exp_operator == ":=": + rsetattr(self.res, field_to_set, rightvalue, None) + elif exp_operator == "=+": + rsetattr(self.res, field_to_set, rightvalue, operator.add) + elif exp_operator == "=-": + if isinstance(rightvalue, str): + rsetattr(self.res, field_to_set, rightvalue, lambda x, y: y + x) + else: + rsetattr(self.res, field_to_set, rightvalue, operator.sub) + except re.error as e: + raise FuzzExceptBadOptions( + "Invalid regex expression used in expression: %s" % str(e) + ) + except TypeError as e: + raise FuzzExceptBadOptions( + "Invalid operand types used in expression: %s" % str(e) + ) + except ParseException as e: + raise FuzzExceptBadOptions("Invalid filter: %s" % str(e)) + + return True + + def __myreduce(self, elements): + first = elements[0] + for i in range(1, len(elements), 2): + if elements[i] == "and": + first = first and elements[i + 1] + elif elements[i] == "or": + first = first or elements[i + 1] + + self.stack = [] + return first + + def __compute_not_operator(self, tokens): + operator, value = tokens + + if operator == "not": + return not value + + return value + + def __compute_formula(self, tokens): + return self.__myreduce(tokens[0]) + + def is_active(self): + return self.filter_string + + def is_visible(self, res, filter_string=None): + if filter_string is None: + filter_string = self.filter_string + self.res = res + try: + return self.finalformula.parseString(filter_string, parseAll=True)[0] + except ParseException as e: + raise FuzzExceptIncorrectFilter( + "Incorrect filter expression, check documentation. {}".format(str(e)) + ) + except AttributeError as e: + raise FuzzExceptIncorrectFilter( + "It is only possible to use advanced filters when using a non-string payload. %s" + % str(e) + ) + + def get_fuzz_words(self): + fuzz_words = self.FUZZ_MARKER_REGEX.findall(self.filter_string) + + return fuzz_words + + +class FuzzResFilterSlice(FuzzResFilter): + # When using slice we don't have previous payload context but directly a word from the dictionary + def _compute_fuzz_symbol(self, tokens): + match_dict = tokens[0].groupdict() + + p_index = match_dict["index"] if match_dict["index"] is not None else 1 + + if p_index != 1: + raise FuzzExceptIncorrectFilter( + "Non existent FUZZ payload! Use a correct index." + ) + + fuzz_val = self.res + + if match_dict["field"]: + fuzz_val = self._get_field_value(self.res, match_dict["field"]) + + return fuzz_val diff --git a/src/wfuzz/filters/simplefilter.py b/src/wfuzz/filters/simplefilter.py new file mode 100644 index 00000000..ff7dc7b2 --- /dev/null +++ b/src/wfuzz/filters/simplefilter.py @@ -0,0 +1,105 @@ +from ..exception import FuzzExceptBadOptions + +import re +import collections + +from ..facade import BASELINE_CODE + + +class FuzzResSimpleFilter: + def __init__(self, ffilter=None): + self.hideparams = dict( + regex_show=None, + codes_show=None, + codes=[], + words=[], + lines=[], + chars=[], + regex=None, + ) + + if ffilter is not None: + self.hideparams = ffilter + + self.stack = [] + + self._cache = collections.defaultdict(set) + + def is_active(self): + return any( + [ + self.hideparams["regex_show"] is not None, + self.hideparams["codes_show"] is not None, + ] + ) + + def set_baseline(self, res): + if BASELINE_CODE in self.hideparams["lines"]: + self.hideparams["lines"].append(res.lines) + if BASELINE_CODE in self.hideparams["codes"]: + self.hideparams["codes"].append(res.code) + if BASELINE_CODE in self.hideparams["words"]: + self.hideparams["words"].append(res.words) + if BASELINE_CODE in self.hideparams["chars"]: + self.hideparams["chars"].append(res.chars) + + def is_visible(self, res): + if self.hideparams["codes_show"] is None: + cond1 = True + else: + cond1 = not self.hideparams["codes_show"] + + if self.hideparams["regex_show"] is None: + cond2 = True + else: + cond2 = not self.hideparams["regex_show"] + + if ( + res.code in self.hideparams["codes"] + or res.lines in self.hideparams["lines"] + or res.words in self.hideparams["words"] + or res.chars in self.hideparams["chars"] + ): + cond1 = self.hideparams["codes_show"] + + if self.hideparams["regex"]: + if self.hideparams["regex"].search(res.history.content): + cond2 = self.hideparams["regex_show"] + + return cond1 and cond2 + + @staticmethod + def from_options(filter_options): + ffilter = FuzzResSimpleFilter() + + try: + if filter_options["ss"] is not None: + ffilter.hideparams["regex_show"] = True + ffilter.hideparams["regex"] = re.compile( + filter_options["ss"], re.MULTILINE | re.DOTALL + ) + + elif filter_options["hs"] is not None: + ffilter.hideparams["regex_show"] = False + ffilter.hideparams["regex"] = re.compile( + filter_options["hs"], re.MULTILINE | re.DOTALL + ) + except Exception as e: + raise FuzzExceptBadOptions( + "Invalid regex expression used in filter: %s" % str(e) + ) + + if [x for x in ["sc", "sw", "sh", "sl"] if len(filter_options[x]) > 0]: + ffilter.hideparams["codes_show"] = True + ffilter.hideparams["codes"] = filter_options["sc"] + ffilter.hideparams["words"] = filter_options["sw"] + ffilter.hideparams["lines"] = filter_options["sl"] + ffilter.hideparams["chars"] = filter_options["sh"] + elif [x for x in ["hc", "hw", "hh", "hl"] if len(filter_options[x]) > 0]: + ffilter.hideparams["codes_show"] = False + ffilter.hideparams["codes"] = filter_options["hc"] + ffilter.hideparams["words"] = filter_options["hw"] + ffilter.hideparams["lines"] = filter_options["hl"] + ffilter.hideparams["chars"] = filter_options["hh"] + + return ffilter diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index b3c514d4..74440077 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -2,582 +2,67 @@ import hashlib import re import itertools -import operator -import pycurl - -# Python 2 and 3 -import sys -if sys.version_info >= (3, 0): - from urllib.parse import urlparse -else: - from urlparse import urlparse +from enum import Enum from threading import Lock -from collections import namedtuple -from collections import defaultdict - -from .filter import FuzzResFilter -from .externals.reqresp import Request, Response -from .exception import FuzzExceptBadAPI, FuzzExceptBadOptions, FuzzExceptInternalError, FuzzException -from .facade import Facade, ERROR_CODE -from .mixins import FuzzRequestUrlMixing, FuzzRequestSoupMixing - -from .utils import python2_3_convert_to_unicode, python2_3_convert_from_unicode -from .utils import MyCounter -from .utils import rgetattr -from .utils import DotDict - -auth_header = namedtuple("auth_header", "method credentials") - - -class headers(object): - class header(DotDict): - def __str__(self): - return "\n".join(["{}: {}".format(k, v) for k, v in self.items()]) - - def __init__(self, req): - self._req = req - - @property - def response(self): - return headers.header(self._req.response.getHeaders()) if self._req.response else {} - - @property - def request(self): - return headers.header([x.split(": ", 1) for x in self._req.getHeaders()]) - - @request.setter - def request(self, values_dict): - self._req._headers.update(values_dict) - if "Content-Type" in values_dict: - self._req.ContentType = values_dict['Content-Type'] - - @property - def all(self): - return headers.header(self.request + self.response) - - -class cookies(object): - class cookie(DotDict): - def __str__(self): - return "\n".join(["{}={}".format(k, v) for k, v in self.items()]) - - def __init__(self, req): - self._req = req - - @property - def response(self): - if self._req.response: - c = self._req.response.getCookie().split("; ") - if c[0]: - return cookies.cookie([[x[0], x[2]] for x in [x.partition("=") for x in c]]) - - return cookies.cookie({}) - - @property - def request(self): - if 'Cookie' in self._req._headers: - c = self._req._headers['Cookie'].split("; ") - if c[0]: - return cookies.cookie([[x[0], x[2]] for x in [x.partition("=") for x in c]]) - - return cookies.cookie({}) - - @request.setter - def request(self, values): - self._req._headers["Cookie"] = "; ".join(values) - - @property - def all(self): - return cookies.cookie(self.request + self.response) - - -class params(object): - class param(DotDict): - def __str__(self): - return "\n".join(["{}={}".format(k, v) for k, v in self.items()]) - - def __init__(self, req): - self._req = req - - @property - def get(self): - return params.param([(x.name, x.value) for x in self._req.getGETVars()]) - - @get.setter - def get(self, values): - if isinstance(values, dict): - for key, value in values.items(): - self._req.setVariableGET(key, str(value)) - else: - raise FuzzExceptBadAPI("GET Parameters must be specified as a dictionary") - - @property - def post(self): - return params.param([(x.name, x.value) for x in self._req.getPOSTVars()]) +from collections import defaultdict, namedtuple - @post.setter - def post(self, pp): - if isinstance(pp, dict): - for key, value in pp.items(): - self._req.setVariablePOST(key, str(value) if value is not None else value) +from .filters.ppfilter import FuzzResFilter +from .facade import ERROR_CODE - self._req._non_parsed_post = self._req._variablesPOST.urlEncoded() +from .helpers.str_func import python2_3_convert_to_unicode +from .helpers.obj_dyn import rgetattr +from .helpers.utils import MyCounter - elif isinstance(pp, str): - self._req.setPostData(pp) - @property - def raw_post(self): - return self._req._non_parsed_post +FuzzWord = namedtuple("FuzzWord", ["content", "type"]) - @property - def all(self): - return params.param(self.get + self.post) - @all.setter - def all(self, values): - self.get = values - self.post = values +class FuzzWordType(Enum): + WORD, FUZZRES = range(2) -class FuzzRequest(FuzzRequestUrlMixing, FuzzRequestSoupMixing): - def __init__(self): - self._request = Request() +class FuzzType(Enum): + ( + SEED, + BACKFEED, + RESULT, + ERROR, + STARTSEED, + ENDSEED, + CANCEL, + DISCARDED, + PLUGIN, + ) = range(9) - self._proxy = None - self._allvars = None - self.wf_fuzz_methods = None - self.wf_retries = 0 - self.wf_ip = None - self.headers.request = {"User-Agent": Facade().sett.get("connection", "user-agent")} +class FuzzItem(object): + newid = itertools.count(0) - # methods for accessing HTTP requests information consistenly accross the codebase + def __init__(self, item_type): + self.item_id = next(FuzzItem.newid) + self.item_type = item_type def __str__(self): - return self._request.getAll() - - @property - def raw_content(self): - if self._request.response: - return self._request.response.getAll() - - return "" - - @property - def headers(self): - return headers(self._request) - - @property - def params(self): - return params(self._request) - - @property - def cookies(self): - return cookies(self._request) - - @property - def method(self): - return self._request.method - - @method.setter - def method(self, method): - self._request.method = method - - @property - def scheme(self): - return self._request.schema - - @scheme.setter - def scheme(self, s): - self._request.schema = s - - @property - def host(self): - return self._request.getHost() - - @property - def path(self): - return self._request.path - - @property - def redirect_url(self): - return self._request.completeUrl - - @property - def url(self): - return self._request.finalUrl - - @url.setter - def url(self, u): - # urlparse goes wrong with IP:port without scheme (https://bugs.python.org/issue754016) - if not u.startswith("FUZ") and (urlparse(u).netloc == "" or urlparse(u).scheme == ""): - u = "http://" + u - - if urlparse(u).path == "": - u += '/' - - if Facade().sett.get("general", "encode_space") == "1": - u = u.replace(" ", "%20") - - self._request.setUrl(u) - if self.scheme.startswith("fuz") and self.scheme.endswith("z"): - # avoid FUZZ to become fuzz - self.scheme = self.scheme.upper() - - @property - def content(self): - return self._request.response.getContent() if self._request.response else "" - - @property - def code(self): - return self._request.response.code if self._request.response else 0 - - @code.setter - def code(self, c): - self._request.response.code = int(c) - - @property - def auth(self): - m, up = self._request.getAuth() - return auth_header(m, up) - - @auth.setter - def auth(self, ah): - method, credentials = ah - self._request.setAuth(method, credentials) - - @property - def follow(self): - return self._request.followLocation - - @follow.setter - def follow(self, f): - self._request.setFollowLocation(f) - - @property - def reqtime(self): - return self._request.totaltime - - @reqtime.setter - def reqtime(self, t): - self._request.totaltime = t - - # Info extra that wfuzz needs within an HTTP request - @property - def wf_allvars_set(self): - if self.wf_allvars == "allvars": - return self.params.get - elif self.wf_allvars == "allpost": - return self.params.post - elif self.wf_allvars == "allheaders": - return self.headers.request - else: - raise FuzzExceptBadOptions("Unknown variable set: " + self.wf_allvars) - - @wf_allvars_set.setter - def wf_allvars_set(self, varset): - try: - if self.wf_allvars == "allvars": - self.params.get = varset - elif self.wf_allvars == "allpost": - self.params.post = varset - elif self.wf_allvars == "allheaders": - self._request.headers.request = varset - else: - raise FuzzExceptBadOptions("Unknown variable set: " + self.wf_allvars) - except TypeError: - raise FuzzExceptBadOptions("It is not possible to use all fuzzing with duplicated parameters.") - - @property - def wf_allvars(self): - return self._allvars - - @wf_allvars.setter - def wf_allvars(self, bl): - if bl is not None and bl not in ['allvars', 'allpost', 'allheaders']: - raise FuzzExceptBadOptions("Incorrect all parameters brute forcing type specified, correct values are allvars, allpost or allheaders.") - - self._allvars = bl - - @property - def wf_proxy(self): - return self._proxy - - @wf_proxy.setter - def wf_proxy(self, l): - if l: - prox, ptype = l - self._request.setProxy("%s" % prox, ptype if ptype else "HTML") - self._proxy = l - - # methods wfuzz needs to perform HTTP requests (this might change in the future). - - def perform(self): - res = FuzzResult(self, track_id=False) - return Facade().http_pool.perform(res) - - def to_http_object(self, c): - pycurl_c = Request.to_pycurl_object(c, self._request) - - if self.wf_ip: - pycurl_c.setopt(pycurl.CONNECT_TO, ["::{}:{}".format(self.wf_ip['ip'], self.wf_ip['port'])]) - - return pycurl_c - - def from_http_object(self, c, bh, bb): - raw_header = python2_3_convert_from_unicode(bh.decode("utf-8", errors='surrogateescape')) - return self._request.response_from_conn_object(c, raw_header, bb) - - def update_from_raw_http(self, raw, scheme, raw_response=None, raw_content=None): - self._request.parseRequest(raw, scheme) - - # Parse request sets postdata = '' when there's POST request without data - if self.method == "POST" and self.params.raw_post is None: - self.params.post = '' - - if raw_response: - rp = Response() - if not isinstance(raw_response, str): - raw_response = python2_3_convert_from_unicode(raw_response.decode("utf-8", errors='surrogateescape')) - rp.parseResponse(raw_response, raw_content) - self._request.response = rp - - return self._request - - def to_cache_key(self): - key = self._request.urlWithoutVariables - - dicc = {'g{}'.format(key): True for key in self.params.get.keys()} - dicc.update({'p{}'.format(key): True for key in self.params.post.keys()}) - - # take URL parameters into consideration - url_params = list(dicc.keys()) - url_params.sort() - key += "-" + "-".join(url_params) - - return key - - # methods wfuzz needs for substituing payloads and building dictionaries - - def update_from_options(self, options): - if options["url"] != "FUZZ": - self.url = options["url"] - - # headers must be parsed first as they might affect how reqresp parases other params - self.headers.request = dict(options['headers']) - - if options['auth'][0] is not None: - self.auth = (options['auth'][0], options['auth'][1]) - - if options['follow']: - self.follow = options['follow'] - - if options['postdata'] is not None: - self.params.post = options['postdata'] - - if options['connect_to_ip']: - self.wf_ip = options['connect_to_ip'] - - if options['method']: - self.method = options['method'] - self.wf_fuzz_methods = options['method'] - - if options['cookie']: - self.cookies.request = options['cookie'] - - if options['allvars']: - self.wf_allvars = options['allvars'] - - def from_copy(self): - newreq = FuzzRequest() - - newreq.wf_proxy = self.wf_proxy - newreq.wf_allvars = self.wf_allvars - newreq.wf_fuzz_methods = self.wf_fuzz_methods - newreq.wf_ip = self.wf_ip - - newreq.headers.request = self.headers.request - newreq.params.post = self.params.raw_post - - newreq.follow = self.follow - newreq.auth = self.auth - newreq.url = self.url - newreq.reqtime = self.reqtime - newreq.scheme = self.scheme - newreq.method = self.wf_fuzz_methods if self.wf_fuzz_methods else self.method - - return newreq - + return "FuzzItem, type: {}".format(self.item_type.name) -class FuzzResultFactory: - @staticmethod - def replace_fuzz_word(text, fuzz_word, payload): - marker_regex = re.compile(r"(%s)(?:\[(.*?)\])?" % (fuzz_word,), re.MULTILINE | re.DOTALL) - - for fuzz_word, field in marker_regex.findall(text): - if field: - marker_regex = re.compile(r"(%s)(?:\[(.*?)\])?" % (fuzz_word,), re.MULTILINE | re.DOTALL) - fields_array = [] - - for fuzz_word, field in marker_regex.findall(text): - if not field: - raise FuzzExceptBadOptions("You must specify a field when using a payload containing a full fuzz request, ie. FUZZ[url], or use FUZZ only to repeat the same request.") - - try: - subs = str(rgetattr(payload, field)) - except AttributeError: - raise FuzzExceptBadOptions("A FUZZ[field] expression must be used with a fuzzresult payload not a string.") - - text = text.replace("%s[%s]" % (fuzz_word, field), subs) - fields_array.append(field) - - return (text, fields_array) - else: - try: - return (text.replace(fuzz_word, payload), [None]) - except TypeError: - raise FuzzExceptBadOptions("Tried to replace {} with a whole fuzzresult payload.".format(fuzz_word)) - - @staticmethod - def from_seed(seed, payload, seed_options): - newres = seed.from_soft_copy() - - rawReq = str(newres.history) - rawUrl = newres.history.redirect_url - scheme = newres.history.scheme - auth_method, userpass = newres.history.auth - - for payload_pos, payload_content in enumerate(payload, start=1): - fuzz_word = "FUZ" + str(payload_pos) + "Z" if payload_pos > 1 else "FUZZ" - - fuzz_values_array = [] - - # substitute entire seed when using a request payload generator without specifying field - if fuzz_word == "FUZZ" and seed_options["seed_payload"] and isinstance(payload_content, FuzzResult): - # new seed - newres = payload_content.from_soft_copy() - newres.payload = [] - - fuzz_values_array.append(None) - - newres.history.update_from_options(seed_options) - newres.update_from_options(seed_options) - rawReq = str(newres.history) - rawUrl = newres.history.redirect_url - scheme = newres.history.scheme - auth_method, userpass = newres.history.auth - - desc = [] - - if auth_method and (userpass.count(fuzz_word)): - userpass, desc = FuzzResultFactory.replace_fuzz_word(userpass, fuzz_word, payload_content) - if newres.history.redirect_url.count(fuzz_word): - rawUrl, desc = FuzzResultFactory.replace_fuzz_word(rawUrl, fuzz_word, payload_content) - if rawReq.count(fuzz_word): - rawReq, desc = FuzzResultFactory.replace_fuzz_word(rawReq, fuzz_word, payload_content) - - if scheme.count(fuzz_word): - scheme, desc = FuzzResultFactory.replace_fuzz_word(scheme, fuzz_word, payload_content) - - if desc: - fuzz_values_array += desc - - newres.payload.append(FuzzPayload(payload_content, fuzz_values_array)) - - newres.history.update_from_raw_http(rawReq, scheme) - newres.history.url = rawUrl - if auth_method != 'None': - newres.history.auth = (auth_method, userpass) - - newres.type = FuzzResult.result - - return newres - - @staticmethod - def from_baseline(fuzzresult, options): - scheme = fuzzresult.history.scheme - rawReq = str(fuzzresult.history) - auth_method, userpass = fuzzresult.history.auth - - # get the baseline payload ordered by fuzz number and only one value per same fuzz keyword. - b1 = dict([matchgroup.groups() for matchgroup in re.finditer(r"FUZ(\d*)Z(?:\[.*?\])?(?:{(.*?)})?", rawReq, re.MULTILINE | re.DOTALL)]) - b2 = dict([matchgroup.groups() for matchgroup in re.finditer(r"FUZ(\d*)Z(?:\[.*?\])?(?:{(.*?)})?", userpass, re.MULTILINE | re.DOTALL)]) - baseline_control = dict(list(b1.items()) + list(b2.items())) - baseline_payload = [x[1] for x in sorted(list(baseline_control.items()), key=operator.itemgetter(0))] - - # if there is no marker, there is no baseline request - if not [x for x in baseline_payload if x is not None]: - return None - - # remove baseline marker from seed request - for i in baseline_payload: - if not i: - raise FuzzExceptBadOptions("You must supply a baseline value for all the FUZZ words.") - rawReq = rawReq.replace("{" + i + "}", '') - - if fuzzresult.history.wf_fuzz_methods: - fuzzresult.history.wf_fuzz_methods = fuzzresult.history.wf_fuzz_methods.replace("{" + i + "}", '') - - if auth_method: - userpass = userpass.replace("{" + i + "}", '') - - # re-parse seed without baseline markers - fuzzresult.history.update_from_raw_http(rawReq, scheme) - if auth_method: - fuzzresult.history.auth = (auth_method, userpass) - - # create baseline request from seed - baseline_res = fuzzresult.from_soft_copy() - - # remove field markers from baseline - marker_regex = re.compile(r"(FUZ\d*Z)\[(.*?)\]", re.DOTALL) - results = marker_regex.findall(rawReq) - if results: - for fw, f in results: - rawReq = rawReq.replace("%s[%s]" % (fw, f), fw) - - if fuzzresult.history.wf_fuzz_methods: - fuzzresult.history.wf_fuzz_methods = fuzzresult.history.wf_fuzz_methods.replace("{" + i + "}", '') - - if auth_method: - userpass = userpass.replace("{" + i + "}", '') - - baseline_res.history.update_from_raw_http(rawReq, scheme) - - baseline_res = FuzzResultFactory.from_seed(baseline_res, baseline_payload, options) - baseline_res.is_baseline = True - - return baseline_res - - @staticmethod - def from_all_fuzz_request(seed, payload): - # only a fuzz payload is allowed using this technique - if len(payload) > 1: - raise FuzzExceptBadOptions("Only one payload is allowed when fuzzing all parameters!") - - for var_name in seed.history.wf_allvars_set.keys(): - payload_content = payload[0] - fuzzres = seed.from_soft_copy() - fuzzres.payload.append(FuzzPayload(payload_content, [None])) - - fuzzres.history.wf_allvars_set = {var_name: payload_content} + def __lt__(self, other): + return self.item_id < other.item_id - yield fuzzres + def __le__(self, other): + return self.item_id <= other.item_id - @staticmethod - def from_options(options): - fr = FuzzRequest() + def __gt__(self, other): + return self.item_id > other.item_id - fr.url = options['url'] - fr.wf_fuzz_methods = options['method'] - fr.update_from_options(options) + def __ge__(self, other): + return self.item_id >= other.item_id - fuzz_res = FuzzResult(fr) - fuzz_res.update_from_options(options) + def __eq__(self, other): + return self.item_id == other.item_id - return fuzz_res + def __ne__(self, other): + return self.item_id != other.item_id class FuzzStats: @@ -600,12 +85,12 @@ def __init__(self): self._cancelled = False @staticmethod - def from_requestGenerator(rg): + def from_options(options): tmp_stats = FuzzStats() - tmp_stats.url = rg.seed.history.redirect_url - tmp_stats.total_req = rg.count() - tmp_stats.seed = rg.seed + tmp_stats.url = options["compiled_seed"].history.redirect_url + tmp_stats.total_req = options["compiled_dictio"].count() + tmp_stats.seed = options["compiled_seed"] return tmp_stats @@ -613,14 +98,11 @@ def get_stats(self): return { "url": self.url, "total": self.total_req, - "backfed": self.backfeed(), "Processed": self.processed(), "Pending": self.pending_fuzz(), "filtered": self.filtered(), - "Pending_seeds": self.pending_seeds(), - "totaltime": self._totaltime, } @@ -648,11 +130,18 @@ def __str__(self): string += "Total time: %s\n" % str(self.totaltime)[:8] if self.backfeed() > 0: - string += "Processed Requests: %s (%d + %d)\n" % (str(self.processed())[:8], (self.processed() - self.backfeed()), self.backfeed()) + string += "Processed Requests: %s (%d + %d)\n" % ( + str(self.processed())[:8], + (self.processed() - self.backfeed()), + self.backfeed(), + ) else: string += "Processed Requests: %s\n" % (str(self.processed())[:8]) string += "Filtered Requests: %s\n" % (str(self.filtered())[:8]) - string += "Requests/sec.: %s\n" % str(self.processed() / self.totaltime if self.totaltime > 0 else 0)[:8] + string += ( + "Requests/sec.: %s\n" + % str(self.processed() / self.totaltime if self.totaltime > 0 else 0)[:8] + ) return string @@ -668,40 +157,130 @@ def update(self, fuzzstats2): self.pending_seeds._operation(fuzzstats2.pending_seeds()) -class FuzzPayload(): - def __init__(self, content, fields): - self.content = content - self.fields = fields +class FuzzPayload: + def __init__(self): + self.marker = None + self.word = None + self.index = None + self.field = None + self.content = None + self.is_baseline = False + self.type = None + + @property + def value(self): + if self.content is None: + return None + return ( + self.content + if self.field is None + else str(rgetattr(self.content, self.field)) + ) def description(self, default): - ret_str_values = [] - for fuzz_value in self.fields: - if fuzz_value is None and isinstance(self.content, FuzzResult): - ret_str_values.append(default) - elif fuzz_value is not None and isinstance(self.content, FuzzResult): - ret_str_values.append(str(rgetattr(self.content, fuzz_value))) - elif fuzz_value is None: - ret_str_values.append(self.content) - else: - ret_str_values.append(fuzz_value) - - return " - ".join(ret_str_values) + if self.is_baseline: + return self.content + + if self.marker is None: + return "" + + if self.field is None and isinstance(self.content, FuzzResult): + return rgetattr(self.content, default) + elif self.field is not None and isinstance(self.content, FuzzResult): + return str(rgetattr(self.content, self.field)) + + return self.value def __str__(self): - return "content: {} fields: {}".format(self.content, self.fields) + return "type: {} index: {} marker: {} content: {} field: {} value: {}".format( + self.type, + self.index, + self.marker, + self.content.__class__, + self.field, + self.value, + ) -class FuzzResult: - seed, backfeed, result, error, startseed, endseed, cancel, discarded = list(range(8)) +class FPayloadManager: + def __init__(self): + self.payloads = defaultdict(list) + + def add(self, payload_dict, fuzzword=None, is_baseline=False): + fp = FuzzPayload() + fp.marker = payload_dict["full_marker"] + fp.word = payload_dict["word"] + fp.index = ( + int(payload_dict["index"]) if payload_dict["index"] is not None else 1 + ) + fp.field = payload_dict["field"] + fp.content = fuzzword.content if fuzzword else None + fp.type = fuzzword.type if fuzzword else None + fp.is_baseline = is_baseline + + self.payloads[fp.index].append(fp) + + def update_from_dictio(self, dictio_item): + for index, dictio_payload in enumerate(dictio_item, 1): + fuzz_payload = None + for fuzz_payload in self.payloads[index]: + fuzz_payload.content = dictio_payload.content + fuzz_payload.type = dictio_payload.type + + # payload generated not used in seed but in filters + if fuzz_payload is None: + self.add( + {"full_marker": None, "word": None, "index": index, "field": None}, + dictio_item[index - 1], + ) + + def get_fuzz_words(self): + return [payload.word for payload in self.get_payloads()] + + def get_payload(self, index): + return self.payloads[index] + + def get_payload_type(self, index): + return self.get_payload(index)[0].type + + def get_payload_content(self, index): + return self.get_payload(index)[0].content + + def get_payloads(self): + for index, elem_list in sorted(self.payloads.items()): + for elem in elem_list: + yield elem + + def description(self): + payl_descriptions = [ + payload.description("url") for payload in self.get_payloads() + ] + ret_str = " - ".join([p_des for p_des in payl_descriptions if p_des]) + + return ret_str + + def __str__(self): + return "\n".join([str(payload) for payload in self.get_payloads()]) + + +class FuzzError(FuzzItem): + def __init__(self, exception): + FuzzItem.__init__(self, FuzzType.ERROR) + self.exception = exception + + +class FuzzResult(FuzzItem): newid = itertools.count(0) + FUZZRESULT_SHARED_FILTER = FuzzResFilter() def __init__(self, history=None, exception=None, track_id=True): + FuzzItem.__init__(self, FuzzType.RESULT) self.history = history - self.type = None self.exception = exception self.is_baseline = False self.rlevel = 1 + self.rlevel_desc = "" self.nres = next(FuzzResult.newid) if track_id else 0 self.chars = 0 @@ -712,11 +291,10 @@ def __init__(self, history=None, exception=None, track_id=True): self.update() self.plugins_res = [] - self.plugins_backfeed = [] - self.payload = [] + self.payload_man = None - self._description = None + self._fields = None self._show_field = False @property @@ -729,8 +307,7 @@ def plugins(self): return dic def update(self, exception=None): - self.type = FuzzResult.result - + self.item_type = FuzzType.RESULT if exception: self.exception = exception @@ -746,42 +323,49 @@ def update(self, exception=None): return self def __str__(self): - if self.type == FuzzResult.result: - res = "%05d: C=%03d %4d L\t %5d W\t %5d Ch\t \"%s\"" % (self.nres, self.code, self.lines, self.words, self.chars, self.description) - for i in self.plugins_res: - res += "\n |_ %s" % i.issue - - return res - else: - return "Control result, type: %s" % ("seed", "backfeed", "result", "error", "startseed", "endseed", "cancel", "discarded")[self.type] - - def _payload_description(self): - if not self.payload: - return self.url + res = '%05d: C=%03d %4d L\t %5d W\t %5d Ch\t "%s"' % ( + self.nres, + self.code, + self.lines, + self.words, + self.chars, + self.description, + ) + for plugin in self.plugins_res: + res += "\n |_ %s" % plugin.issue - payl_descriptions = [payload.description(self.url) for payload in self.payload] - ret_str = ' - '.join([p_des for p_des in payl_descriptions if p_des]) - - return ret_str + return res @property def description(self): + res_description = ( + self.payload_man.description() if self.payload_man else self.url + ) ret_str = "" if self._show_field is True: - ret_str = self.eval(self._description) - elif self._show_field is False and self._description is not None: - ret_str = "{} | {}".format(self._payload_description(), self.eval(self._description)) + ret_str = self._field() + elif self._show_field is False and self._fields is not None: + ret_str = "{} | {}".format(res_description, self._field()) else: - ret_str = self._payload_description() + ret_str = res_description + + if not ret_str: + ret_str = self.url if self.exception: return ret_str + "! " + str(self.exception) + if self.rlevel > 1: + return self.rlevel_desc + " - " + ret_str + return ret_str def eval(self, expr): - return FuzzResFilter(filter_string=expr).is_visible(self) + return self.FUZZRESULT_SHARED_FILTER.is_visible(self, expr) + + def _field(self): + return " | ".join([str(self.eval(field)) for field in self._fields]) # parameters in common with fuzzrequest @property @@ -797,7 +381,7 @@ def code(self): if self.history and self.history.code >= 0 and not self.exception: return int(self.history.code) # elif not self.history.code: - # return 0 + # return 0 else: return ERROR_CODE @@ -807,103 +391,15 @@ def timer(self): # factory methods - def to_new_seed(self): - seed = self.from_soft_copy(False) - - if seed.type == FuzzResult.error: - raise FuzzExceptInternalError("A new seed cannot be created with a Fuzz item representing an error.") - - seed.history.url = self.history.recursive_url - seed.rlevel += 1 - seed.type = FuzzResult.seed - - return seed - - def from_soft_copy(self, track_id=True): - fr = FuzzResult(self.history.from_copy(), track_id=track_id) - - fr.exception = self.exception - fr.is_baseline = self.is_baseline - fr.type = self.type - fr.rlevel = self.rlevel - fr.payload = list(self.payload) - fr._description = self._description - fr._show_field = self._show_field - - return fr - def update_from_options(self, options): - self._description = options['description'] - self._show_field = options['show_field'] + self._fields = options["fields"] + self._show_field = options["show_field"] - @staticmethod - def to_new_exception(exception): - fr = FuzzResult(exception=exception, track_id=False) - fr.type = FuzzResult.error - - return fr - @staticmethod - def to_new_signal(signal): - fr = FuzzResult(track_id=False) - fr.type = signal - - return fr - - def to_new_url(self, url): - fr = self.from_soft_copy() - fr.history.url = str(url) - fr.rlevel = self.rlevel + 1 - fr.type = FuzzResult.backfeed - fr.is_baseline = False - - return fr - - def __lt__(self, other): - return self.nres < other.nres - - def __le__(self, other): - return self.nres <= other.nres - - def __gt__(self, other): - return self.nres > other.nres - - def __ge__(self, other): - return self.nres >= other.nres - - def __eq__(self, other): - return self.nres == other.nres - - def __ne__(self, other): - return self.nres != other.nres - - -class PluginItem: - undefined, result, backfeed = list(range(3)) - - def __init__(self, ptype): - self.source = "" - self.plugintype = ptype - - -class PluginResult(PluginItem): +class FuzzPlugin(FuzzItem): def __init__(self): - PluginItem.__init__(self, PluginItem.result) - + FuzzItem.__init__(self, FuzzType.PLUGIN) + self.source = "" self.issue = "" - - -class PluginRequest(PluginItem): - def __init__(self): - PluginItem.__init__(self, PluginItem.backfeed) - - self.fuzzitem = None - - @staticmethod - def from_fuzzRes(res, url, source): - plreq = PluginRequest() - plreq.source = source - plreq.fuzzitem = res.to_new_url(url) - plreq.fuzzitem.payload = [FuzzPayload(url, [None])] - - return plreq + self._exception = None + self._seed = None diff --git a/src/wfuzz/fuzzqueues.py b/src/wfuzz/fuzzqueues.py index e49c7bd3..f77abc2c 100644 --- a/src/wfuzz/fuzzqueues.py +++ b/src/wfuzz/fuzzqueues.py @@ -3,66 +3,130 @@ import gzip from threading import Thread, Event from queue import Queue +from collections import defaultdict -from .fuzzobjects import FuzzResult +from .factories.fuzzresfactory import resfactory +from .factories.plugin_factory import plugin_factory +from .fuzzobjects import FuzzType, FuzzItem from .myqueues import FuzzQueue -from .exception import FuzzExceptInternalError, FuzzExceptBadOptions, FuzzExceptBadFile, FuzzExceptPluginLoadError, FuzzExceptPluginError +from .exception import ( + FuzzExceptInternalError, + FuzzExceptBadOptions, + FuzzExceptBadFile, + FuzzExceptPluginLoadError, +) from .myqueues import FuzzRRQueue from .facade import Facade -from .fuzzobjects import PluginResult, PluginItem +from .fuzzobjects import FuzzWordType +from .ui.console.mvc import View + + +class AllVarQ(FuzzQueue): + def __init__(self, options): + FuzzQueue.__init__(self, options) + self.delay = options.get("delay") + self.seed = options["compiled_seed"] + + def get_name(self): + return "AllVarQ" + + def cancel(self): + self.options["compiled_stats"].cancelled = True + + def items_to_process(self, item): + return item.item_type in [FuzzType.STARTSEED] + + def process(self, item): + self.stats.pending_seeds.inc() + + for var_name, payload in self.options["compiled_dictio"]: + if self.options["compiled_stats"].cancelled: + break + self.stats.pending_fuzz.inc() + if self.delay: + time.sleep(self.delay) + self.send( + resfactory.create( + "fuzzres_from_allvar", self.options, var_name.content, payload + ) + ) + + self.send_last(FuzzItem(FuzzType.ENDSEED)) class SeedQ(FuzzQueue): def __init__(self, options): FuzzQueue.__init__(self, options) self.delay = options.get("delay") - self.genReq = options.get("compiled_genreq") def get_name(self): - return 'SeedQ' + return "SeedQ" def cancel(self): - self.genReq.stop() + self.options["compiled_stats"].cancelled = True + + def items_to_process(self, item): + return item.item_type in [FuzzType.STARTSEED, FuzzType.SEED] + + def send_baseline(self): + fuzz_baseline = self.options["compiled_baseline"] + + if fuzz_baseline is not None and self.stats.pending_seeds() == 1: + self.stats.pending_fuzz.inc() + self.send_first(fuzz_baseline) + + # wait for BBB to be completed before generating more items + while self.stats.processed() == 0 and not self.stats.cancelled: + time.sleep(0.0001) + + def restart(self, seed): + self.options["compiled_seed"] = seed + self.options.compile_dictio() def process(self, item): - if item.type == FuzzResult.startseed: - self.genReq.stats.pending_seeds.inc() - elif item.type == FuzzResult.seed: - self.genReq.restart(item) + if item.item_type == FuzzType.STARTSEED: + self.stats.pending_seeds.inc() + elif item.item_type == FuzzType.SEED: + self.restart(item) else: raise FuzzExceptInternalError("SeedQ: Unknown item type in queue!") - # Empty dictionary? - try: - fuzzres = next(self.genReq) - - if fuzzres.is_baseline: - self.genReq.stats.pending_fuzz.inc() - self.send_first(fuzzres) + self.send_baseline() + self.send_dictionary() - # wait for BBB to be completed before generating more items - while(self.genReq.stats.processed() == 0 and not self.genReq.stats.cancelled): - time.sleep(0.0001) + def get_fuzz_res(self, dictio_item): + if self.options["seed_payload"] and dictio_item[0].type == FuzzWordType.FUZZRES: + return resfactory.create( + "seed_from_options_and_dict", self.options, dictio_item + ) + else: + return resfactory.create( + "fuzzres_from_options_and_dict", self.options, dictio_item + ) + def send_dictionary(self): + # Empty dictionary? + try: + fuzzres = next(self.options["compiled_dictio"]) except StopIteration: - raise FuzzExceptBadOptions("Empty dictionary! Please check payload or filter.") + raise FuzzExceptBadOptions( + "Empty dictionary! Please check payload or filter." + ) # Enqueue requests try: - if fuzzres.is_baseline: - # more after baseline? - fuzzres = next(self.genReq) - while fuzzres: - self.genReq.stats.pending_fuzz.inc() + if self.options["compiled_stats"].cancelled: + break + self.stats.pending_fuzz.inc() if self.delay: time.sleep(self.delay) - self.send(fuzzres) - fuzzres = next(self.genReq) + self.send(self.get_fuzz_res(fuzzres)) + fuzzres = next(self.options["compiled_dictio"]) except StopIteration: pass - self.send_last(FuzzResult.to_new_signal(FuzzResult.endseed)) + self.send_last(FuzzItem(FuzzType.ENDSEED)) class SaveQ(FuzzQueue): @@ -71,12 +135,12 @@ def __init__(self, options): self.output_fn = None try: - self.output_fn = gzip.open(options.get("save"), 'w+b') + self.output_fn = gzip.open(options.get("save"), "w+b") except IOError as e: raise FuzzExceptBadFile("Error opening results file!. %s" % str(e)) def get_name(self): - return 'SaveQ' + return "SaveQ" def _cleanup(self): self.output_fn.close() @@ -86,6 +150,52 @@ def process(self, item): self.send(item) +class ConsolePrinterQ(FuzzQueue): + def __init__(self, options): + FuzzQueue.__init__(self, options) + self.printer = Facade().printers.get_plugin(self.options["console_printer"])( + None + ) + + def mystart(self): + self.printer.header(self.stats) + + def items_to_process(self, item): + return item.item_type in [FuzzType.RESULT] + + def get_name(self): + return "ConsolePrinterQ" + + def _cleanup(self): + self.printer.footer(self.stats) + + def process(self, item): + self.printer.result(item) + self.send(item) + + +class CLIPrinterQ(FuzzQueue): + def __init__(self, options): + FuzzQueue.__init__(self, options) + self.printer = View(self.options) + + def mystart(self): + self.printer.header(self.stats) + + def items_to_process(self, item): + return item.item_type in [FuzzType.RESULT, FuzzType.DISCARDED] + + def get_name(self): + return "CLIPrinterQ" + + def _cleanup(self): + self.printer.footer(self.stats) + + def process(self, item): + self.printer.result(item) + self.send(item) + + class PrinterQ(FuzzQueue): def __init__(self, options): FuzzQueue.__init__(self, options) @@ -94,7 +204,7 @@ def __init__(self, options): self.printer.header(self.stats) def get_name(self): - return 'PrinterQ' + return "PrinterQ" def _cleanup(self): self.printer.footer(self.stats) @@ -110,23 +220,26 @@ def __init__(self, options, routes): self.routes = routes def get_name(self): - return 'RoutingQ' + return "RoutingQ" + + def items_to_process(self, item): + return item.item_type in [FuzzType.SEED, FuzzType.BACKFEED] def process(self, item): - if item.type in self.routes: - self.routes[item.type].put(item) + if item.item_type in self.routes: + self.routes[item.item_type].put(item) else: self.queue_out.put(item) class FilterQ(FuzzQueue): - def __init__(self, options): + def __init__(self, options, ffilter): FuzzQueue.__init__(self, options) - self.ffilter = options.get("compiled_filter") + self.ffilter = ffilter def get_name(self): - return 'filter_thread' + return "filter_thread" def process(self, item): if item.is_baseline: @@ -139,13 +252,13 @@ def process(self, item): class SliceQ(FuzzQueue): - def __init__(self, options): + def __init__(self, options, prefilter): FuzzQueue.__init__(self, options) - self.ffilter = options.get("compiled_prefilter") + self.ffilter = prefilter def get_name(self): - return 'slice_thread' + return "slice_thread" def process(self, item): if item.is_baseline or self.ffilter.is_visible(item): @@ -160,13 +273,17 @@ def __init__(self, options): lplugins = [x() for x in Facade().scripts.get_plugins(options.get("script"))] if not lplugins: - raise FuzzExceptBadOptions("No plugin selected, check the --script name or category introduced.") + raise FuzzExceptBadOptions( + "No plugin selected, check the --script name or category introduced." + ) - concurrent = int(Facade().sett.get('general', 'concurrent_plugins')) - FuzzRRQueue.__init__(self, options, [JobMan(options, lplugins) for i in range(concurrent)]) + concurrent = int(Facade().sett.get("general", "concurrent_plugins")) + FuzzRRQueue.__init__( + self, options, [JobMan(options, lplugins) for i in range(concurrent)] + ) def get_name(self): - return 'JobQ' + return "JobQ" def process(self, item): self.send(item) @@ -178,9 +295,10 @@ def __init__(self, options, selected_plugins): self.__walking_threads = Queue(20) self.selected_plugins = selected_plugins self.cache = options.cache + self.max_dlevel = options.get("dlevel") def get_name(self): - return 'Jobman' + return "Jobman" # ------------------------------------------------ # threading @@ -188,7 +306,9 @@ def get_name(self): def process(self, res): # process request through plugins if not res.exception: - if self.options['no_cache'] or self.cache.update_cache(res.history, "processed"): + if self.options["no_cache"] or self.cache.update_cache( + res.history, "processed" + ): plugins_res_queue = Queue() @@ -196,30 +316,60 @@ def process(self, res): try: if not pl.validate(res): continue - th = Thread(target=pl.run, kwargs={"fuzzresult": res, "control_queue": self.__walking_threads, "results_queue": plugins_res_queue}) + th = Thread( + target=pl.run, + kwargs={ + "fuzzresult": res, + "control_queue": self.__walking_threads, + "results_queue": plugins_res_queue, + }, + ) except Exception as e: - raise FuzzExceptPluginLoadError("Error initialising plugin %s: %s " % (pl.name, str(e))) + raise FuzzExceptPluginLoadError( + "Error initialising plugin %s: %s " % (pl.name, str(e)) + ) self.__walking_threads.put(th) th.start() self.__walking_threads.join() - while not plugins_res_queue.empty(): - item = plugins_res_queue.get() - - if item.plugintype == PluginItem.result: - if Facade().sett.get("general", "cancel_on_plugin_except") == "1" and item.source == "$$exception$$": - self._throw(FuzzExceptPluginError(item.issue)) - res.plugins_res.append(item) - elif item.plugintype == PluginItem.backfeed: - if self.options['no_cache'] or self.cache.update_cache(item.fuzzitem.history, "backfeed"): - res.plugins_backfeed.append(item) - else: - raise FuzzExceptInternalError("Jobman: Unknown pluginitem type in queue!") + self.process_results(res, plugins_res_queue) # add result to results queue self.send(res) + def process_results(self, res, plugins_res_queue): + enq_item = defaultdict(int) + + while not plugins_res_queue.empty(): + item = plugins_res_queue.get() + + if item._exception is not None: + if Facade().sett.get("general", "cancel_on_plugin_except") == "1": + self._throw(item._exception) + res.plugins_res.append(item) + elif item._seed is not None: + cache_hit = self.cache.update_cache(item._seed.history, "backfeed") + if (self.options["no_cache"] or cache_hit) and ( + self.max_dlevel == 0 or self.max_dlevel >= res.rlevel + ): + self.stats.backfeed.inc() + self.stats.pending_fuzz.inc() + self.send(item._seed) + enq_item[item.source] += 1 + else: + res.plugins_res.append(item) + + for plugin_name, enq_num in enq_item.items(): + res.plugins_res.append( + plugin_factory.create( + "plugin_from_finding", + "Backfeed", + "Plugin %s enqueued %d more requests (rlevel=%d)" + % (plugin_name, enq_num, res.rlevel), + ) + ) + class RecursiveQ(FuzzQueue): def __init__(self, options): @@ -229,47 +379,42 @@ def __init__(self, options): self.max_rlevel = options.get("rlevel") def get_name(self): - return 'RecursiveQ' + return "RecursiveQ" def process(self, fuzz_res): - # Getting results from plugins or directly from http if not activated - enq_item = 0 - plugin_name = "" - - # Check for plugins new enqueued requests - while fuzz_res.plugins_backfeed: - plg_backfeed = fuzz_res.plugins_backfeed.pop() - plugin_name = plg_backfeed.source - - self.stats.backfeed.inc() - self.stats.pending_fuzz.inc() - self.send(plg_backfeed.fuzzitem) - enq_item += 1 - - if enq_item > 0: - plres = PluginResult() - plres.source = "Backfeed" - fuzz_res.plugins_res.append(plres) - plres.issue = "Plugin %s enqueued %d more requests (rlevel=%d)" % (plugin_name, enq_item, fuzz_res.rlevel) - # check if recursion is needed if self.max_rlevel >= fuzz_res.rlevel and fuzz_res.history.is_path: if self.cache.update_cache(fuzz_res.history, "recursion"): - self.send_new_seed(fuzz_res) + self.stats.pending_seeds.inc() + seed = resfactory.create("seed_from_recursion", fuzz_res) + self.send(seed) + + fuzz_res.plugins_res.append( + plugin_factory.create( + "plugin_from_finding", + "Recursion", + "Enqueued response for recursion (level=%d)" % (seed.rlevel), + ) + ) # send new result self.send(fuzz_res) - def send_new_seed(self, res): - # Little hack to output that the result generates a new recursion seed - plres = PluginResult() - plres.source = "Recursion" - plres.issue = "Enqueued response for recursion (level=%d)" % (res.rlevel) - res.plugins_res.append(plres) - # send new seed - self.stats.pending_seeds.inc() - self.send(res.to_new_seed()) +class PassPayloadQ(FuzzQueue): + def __init__(self, options): + FuzzQueue.__init__(self, options) + self.pause = Event() + + def get_name(self): + return "PassPayloadQ" + + def process(self, item): + if item.payload_man.get_payload_type(1) == FuzzWordType.FUZZRES: + item = item.payload_man.get_payload_content(1) + item.update_from_options(self.options) + + self.send(item) class DryRunQ(FuzzQueue): @@ -278,7 +423,7 @@ def __init__(self, options): self.pause = Event() def get_name(self): - return 'DryRunQ' + return "DryRunQ" def process(self, item): self.send(item) @@ -301,16 +446,19 @@ def mystart(self): self.poolid = self.http_pool.register() th2 = Thread(target=self.__read_http_results) - th2.setName('__read_http_results') + th2.setName("__read_http_results") th2.start() def get_name(self): - return 'HttpQueue' + return "HttpQueue" def _cleanup(self): self.http_pool.deregister() self.exit_job = True + def items_to_process(self, item): + return item.item_type in [FuzzType.RESULT, FuzzType.BACKFEED] + def process(self, obj): self.pause.wait() self.http_pool.enqueue(obj, self.poolid) @@ -329,7 +477,7 @@ def __init__(self, options): FuzzQueue.__init__(self, options, limit=options.get("concurrent") * 5) def get_name(self): - return 'HttpReceiver' + return "HttpReceiver" def process(self, res): if res.exception and not self.options.get("scanmode"): diff --git a/src/wfuzz/fuzzrequest.py b/src/wfuzz/fuzzrequest.py new file mode 100644 index 00000000..5d143579 --- /dev/null +++ b/src/wfuzz/fuzzrequest.py @@ -0,0 +1,395 @@ +import pycurl + +# Python 2 and 3 +import sys + +if sys.version_info >= (3, 0): + from urllib.parse import urlparse +else: + from urlparse import urlparse + +from collections import namedtuple + +from .externals.reqresp import Request, Response +from .exception import FuzzExceptBadAPI, FuzzExceptBadOptions +from .facade import Facade +from .mixins import FuzzRequestUrlMixing, FuzzRequestSoupMixing + +from .helpers.str_func import python2_3_convert_from_unicode +from .helpers.obj_dic import DotDict + + +class headers(object): + class header(DotDict): + def __str__(self): + return "\n".join(["{}: {}".format(k, v) for k, v in self.items()]) + + def __init__(self, req): + self._req = req + + @property + def response(self): + return ( + headers.header(self._req.response.getHeaders()) + if self._req.response + else headers.header() + ) + + @property + def request(self): + return headers.header(self._req._headers) + + @request.setter + def request(self, values_dict): + self._req._headers.update(values_dict) + if "Content-Type" in values_dict: + self._req.ContentType = values_dict["Content-Type"] + + @property + def all(self): + return headers.header(self.request + self.response) + + +class cookies(object): + class cookie(DotDict): + def __str__(self): + return "\n".join(["{}={}".format(k, v) for k, v in self.items()]) + + def __init__(self, req): + self._req = req + + @property + def response(self): + if self._req.response: + c = self._req.response.getCookie().split("; ") + if c[0]: + return cookies.cookie( + {x[0]: x[2] for x in [x.partition("=") for x in c]} + ) + + return cookies.cookie({}) + + @property + def request(self): + if "Cookie" in self._req._headers: + c = self._req._headers["Cookie"].split("; ") + if c[0]: + return cookies.cookie( + {x[0]: x[2] for x in [x.partition("=") for x in c]} + ) + + return cookies.cookie({}) + + @request.setter + def request(self, values): + self._req._headers["Cookie"] = "; ".join(values) + + @property + def all(self): + return cookies.cookie(self.request + self.response) + + +class params(object): + class param(DotDict): + def __str__(self): + return "\n".join(["{}={}".format(k, v) for k, v in self.items()]) + + def __init__(self, req): + self._req = req + + @property + def get(self): + return params.param({x.name: x.value for x in self._req.getGETVars()}) + + @get.setter + def get(self, values): + if isinstance(values, dict) or isinstance(values, DotDict): + for key, value in values.items(): + self._req.setVariableGET(key, str(value)) + else: + raise FuzzExceptBadAPI("GET Parameters must be specified as a dictionary") + + @property + def post(self): + return params.param({x.name: x.value for x in self._req.getPOSTVars()}) + + @post.setter + def post(self, pp): + if isinstance(pp, dict) or isinstance(pp, DotDict): + for key, value in pp.items(): + self._req.setVariablePOST( + key, str(value) if value is not None else value + ) + + self._req._non_parsed_post = self._req._variablesPOST.urlEncoded() + + elif isinstance(pp, str): + self._req.setPostData(pp) + + @property + def raw_post(self): + return self._req._non_parsed_post + + @property + def all(self): + return params.param(self.get + self.post) + + @all.setter + def all(self, values): + self.get = values + self.post = values + + +class FuzzRequest(FuzzRequestUrlMixing, FuzzRequestSoupMixing): + def __init__(self): + self._request = Request() + + self._proxy = None + self._allvars = None + self.wf_fuzz_methods = None + self.wf_retries = 0 + self.wf_ip = None + + self.headers.request = { + "User-Agent": Facade().sett.get("connection", "user-agent") + } + + # methods for accessing HTTP requests information consistenly accross the codebase + + def __str__(self): + return self._request.getAll() + + @property + def raw_request(self): + return self._request.getAll() + + @raw_request.setter + def raw_request(self, rawReq, scheme): + self.update_from_raw_http(rawReq, scheme) + + @property + def raw_content(self): + if self._request.response: + return self._request.response.getAll() + + return "" + + @property + def headers(self): + return headers(self._request) + + @property + def params(self): + return params(self._request) + + @property + def cookies(self): + return cookies(self._request) + + @property + def method(self): + return self._request.method + + @method.setter + def method(self, method): + self._request.method = method + + @property + def scheme(self): + return self._request.schema + + @scheme.setter + def scheme(self, s): + self._request.schema = s + + @property + def host(self): + return self._request.getHost() + + @property + def path(self): + return self._request.path + + @property + def redirect_url(self): + return self._request.completeUrl + + @property + def url(self): + return self._request.finalUrl + + @url.setter + def url(self, u): + # urlparse goes wrong with IP:port without scheme (https://bugs.python.org/issue754016) + if not u.startswith("FUZ") and ( + urlparse(u).netloc == "" or urlparse(u).scheme == "" + ): + u = "http://" + u + + if urlparse(u).path == "": + u += "/" + + if Facade().sett.get("general", "encode_space") == "1": + u = u.replace(" ", "%20") + + self._request.setUrl(u) + if self.scheme.startswith("fuz") and self.scheme.endswith("z"): + # avoid FUZZ to become fuzz + self.scheme = self.scheme.upper() + + @property + def content(self): + return self._request.response.getContent() if self._request.response else "" + + @property + def code(self): + return self._request.response.code if self._request.response else 0 + + @code.setter + def code(self, c): + self._request.response.code = int(c) + + @property + def auth(self): + method, creds = self._request.getAuth() + + return DotDict({"method": method, "credentials": creds}) + + @auth.setter + def auth(self, creds_dict): + self._request.setAuth(creds_dict["method"], creds_dict["credentials"]) + method, creds = self._request.getAuth() + + return DotDict({"method": method, "credentials": creds}) + + @property + def follow(self): + return self._request.followLocation + + @follow.setter + def follow(self, f): + self._request.setFollowLocation(f) + + @property + def reqtime(self): + return self._request.totaltime + + @reqtime.setter + def reqtime(self, t): + self._request.totaltime = t + + # Info extra that wfuzz needs within an HTTP request + @property + def wf_allvars_set(self): + if self.wf_allvars == "allvars": + return self.params.get + elif self.wf_allvars == "allpost": + return self.params.post + elif self.wf_allvars == "allheaders": + return self.headers.request + else: + raise FuzzExceptBadOptions("Unknown variable set: " + self.wf_allvars) + + @wf_allvars_set.setter + def wf_allvars_set(self, varset): + try: + if self.wf_allvars == "allvars": + self.params.get = varset + elif self.wf_allvars == "allpost": + self.params.post = varset + elif self.wf_allvars == "allheaders": + self._request.headers.request = varset + else: + raise FuzzExceptBadOptions("Unknown variable set: " + self.wf_allvars) + except TypeError: + raise FuzzExceptBadOptions( + "It is not possible to use all fuzzing with duplicated parameters." + ) + + @property + def wf_allvars(self): + return self._allvars + + @wf_allvars.setter + def wf_allvars(self, bl): + if bl is not None and bl not in ["allvars", "allpost", "allheaders"]: + raise FuzzExceptBadOptions( + "Incorrect all parameters brute forcing type specified, correct values are allvars, allpost or allheaders." + ) + + self._allvars = bl + + @property + def wf_proxy(self): + return self._proxy + + @wf_proxy.setter + def wf_proxy(self, proxy_tuple): + if proxy_tuple: + prox, ptype = proxy_tuple + self._request.setProxy("%s" % prox, ptype if ptype else "HTML") + self._proxy = proxy_tuple + + # methods wfuzz needs to perform HTTP requests (this might change in the future). + + def update_from_raw_http(self, raw, scheme, raw_response=None, raw_content=None): + self._request.parseRequest(raw, scheme) + + # Parse request sets postdata = '' when there's POST request without data + if self.method == "POST" and self.params.raw_post is None: + self.params.post = "" + + if raw_response: + rp = Response() + if not isinstance(raw_response, str): + raw_response = python2_3_convert_from_unicode( + raw_response.decode("utf-8", errors="surrogateescape") + ) + rp.parseResponse(raw_response, raw_content) + self._request.response = rp + + return self._request + + def to_cache_key(self): + key = self._request.urlWithoutVariables + + dicc = {"g{}".format(key): True for key in self.params.get.keys()} + dicc.update({"p{}".format(key): True for key in self.params.post.keys()}) + + # take URL parameters into consideration + url_params = list(dicc.keys()) + url_params.sort() + key += "-" + "-".join(url_params) + + return key + + # methods wfuzz needs for substituing payloads and building dictionaries + + def update_from_options(self, options): + if options["url"] != "FUZZ": + self.url = options["url"] + + # headers must be parsed first as they might affect how reqresp parases other params + self.headers.request = dict(options["headers"]) + + if options["auth"].get("method") is not None: + self.auth = options["auth"] + + if options["follow"]: + self.follow = options["follow"] + + if options["postdata"] is not None: + self.params.post = options["postdata"] + + if options["connect_to_ip"]: + self.wf_ip = options["connect_to_ip"] + + if options["method"]: + self.method = options["method"] + self.wf_fuzz_methods = options["method"] + + if options["cookie"]: + self.cookies.request = options["cookie"] + + if options["allvars"]: + self.wf_allvars = options["allvars"] diff --git a/src/wfuzz/helpers/__init__.py b/src/wfuzz/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/wfuzz/helpers/file_func.py b/src/wfuzz/helpers/file_func.py new file mode 100644 index 00000000..f267082a --- /dev/null +++ b/src/wfuzz/helpers/file_func.py @@ -0,0 +1,157 @@ +import os +import sys +import re +import pkg_resources + +from chardet.universaldetector import UniversalDetector +import chardet + +from ..exception import FuzzExceptInternalError + + +def get_filter_help_file(): + FILTER_HELP_FILE = "advanced.rst" + FILTER_HELP_DEV_FILE = "../../../docs/user/advanced.rst" + + filter_help_text = None + try: + fname = pkg_resources.resource_filename("wfuzz", FILTER_HELP_FILE) + filter_help_text = open(fname).read() + except IOError: + filter_help_text = open(get_path(FILTER_HELP_DEV_FILE)).read() + + return filter_help_text + + +def get_home(check=False, directory=None): + path = os.path.join(os.path.expanduser("~"), ".wfuzz") + if check: + if not os.path.exists(path): + os.makedirs(path) + + return os.path.join(path, directory) if directory else path + + +def get_path(directory=None): + abspath = os.path.abspath(__file__) + ret = os.path.dirname(abspath) + + return os.path.join(ret, directory) if directory else ret + + +def find_file_in_paths(name, path): + for root, dirs, files in os.walk(path): + if name in files: + return os.path.join(root, name) + + return None + + +class FileDetOpener: + typical_encodings = [ + "UTF-8", + "ISO-8859-1", + "Windows-1251", + "Shift JIS", + "Windows-1252", + "GB2312", + "EUC-KR", + "EUC-JP", + "GBK", + "ISO-8859-2", + "Windows-1250", + "ISO-8859-15", + "Windows-1256", + "ISO-8859-9", + "Big5", + "Windows-1254", + ] + + def __init__(self, file_path, encoding=None): + self.cache = [] + self.file_des = open(file_path, mode="rb") + self.det_encoding = encoding + self.encoding_forced = False + + def close(self): + self.file_des.close() + + def reset(self): + self.file_des.seek(0) + + def __iter__(self): + return self + + def __next__(self): + decoded_line = None + line = None + last_error = None + + while decoded_line is None: + + while self.det_encoding is None: + detect_encoding = self.detect_encoding().get("encoding", "utf-8") + self.det_encoding = ( + detect_encoding if detect_encoding is not None else "utf-8" + ) + + if line is None: + if self.cache: + line = self.cache.pop() + else: + line = next(self.file_des) + if not line: + raise StopIteration + + try: + decoded_line = line.decode(self.det_encoding) + except UnicodeDecodeError: + if last_error is not None and last_error: + self.det_encoding = last_error.pop() + elif last_error is None and not self.encoding_forced: + last_error = list(reversed(self.typical_encodings)) + last_error.append(chardet.detect(line).get("encoding")) + elif not last_error: + raise FuzzExceptInternalError("Unable to decode wordlist file!") + + decoded_line = None + + return decoded_line + + def detect_encoding(self): + detector = UniversalDetector() + detector.reset() + + for line in self.file_des: + detector.feed(line) + self.cache.append(line) + if detector.done: + break + + detector.close() + + return detector.result + + next = __next__ # for Python 2 + + +def open_file_detect_encoding(file_path): + def detect_encoding(file_path): + detector = UniversalDetector() + detector.reset() + + with open(file_path, mode="rb") as file_to_detect: + for line in file_to_detect: + detector.feed(line) + if detector.done: + break + detector.close() + + return detector.result + + if sys.version_info >= (3, 0): + return open( + file_path, "r", encoding=detect_encoding(file_path).get("encoding", "utf-8") + ) + else: + return open(file_path, "r") diff --git a/src/wfuzz/helpers/obj_dic.py b/src/wfuzz/helpers/obj_dic.py new file mode 100644 index 00000000..75c3bde0 --- /dev/null +++ b/src/wfuzz/helpers/obj_dic.py @@ -0,0 +1,67 @@ +from collections.abc import MutableMapping + + +class CaseInsensitiveDict(MutableMapping): + def __init__(self, *args, **kwargs): + self.store = dict() + self.proxy = dict() + + self.update(dict(*args, **kwargs)) # use the free update to set keys + + def __contains__(self, k): + return k.lower() in self.proxy + + def __delitem__(self, k): + key = self.proxy[k.lower()] + + del self.store[key] + del self.proxy[k.lower()] + + def __getitem__(self, k): + key = self.proxy[k.lower()] + return self.store[key] + + def get(self, k, default=None): + key = self.proxy[k.lower()] + return self.store[key] if key in self.store else default + + def __setitem__(self, k, v): + self.store[k] = v + self.proxy[k.lower()] = k + + def __iter__(self): + return iter(self.store) + + def __len__(self): + return len(self.store) + + +class DotDict(CaseInsensitiveDict): + def __getattr__(obj, name): + # Return {} if non-existent attr + if name not in obj: + return DotDict({}) + + # python 3 val = dict.get(*args, None) + val = obj.get(name) + return DotDict(val) if type(val) is dict else val + # return DotDict(val) if type(val) is dict else DotDict({args[1]: val}) + + def __add__(self, other): + if isinstance(other, str): + return DotDict({k: v + other for k, v in self.items() if v}) + elif isinstance(other, DotDict): + # python 3 return DotDict({**self, **other}) + new_dic = DotDict(self) + new_dic.update(other) + return new_dic + + def __radd__(self, other): + if isinstance(other, str): + return DotDict({k: other + v for k, v in self.items() if v}) + + def __getitem__(self, key): + try: + return super(DotDict, self).__getitem__(key) + except KeyError: + return DotDict({}) diff --git a/src/wfuzz/helpers/obj_dyn.py b/src/wfuzz/helpers/obj_dyn.py new file mode 100644 index 00000000..6221294e --- /dev/null +++ b/src/wfuzz/helpers/obj_dyn.py @@ -0,0 +1,118 @@ +import functools +from .obj_dic import DotDict + + +allowed_fields = [ + "description", + "nres", + "code", + "chars", + "lines", + "words", + "md5", + "l", + "h", + "w", + "c", + "history", + "plugins", + "url", + "content", + "history.url", + "history.method", + "history.scheme", + "history.host", + "history.content", + "history.raw_content" "history.is_path", + "history.pstrip", + "history.cookies", + "history.headers", + "history.params", + "r", + "r.reqtime", + "r.url", + "r.method", + "r.scheme", + "r.host", + "r.content", + "r.raw_content" "r.is_path", + "r.pstrip", + "r.cookies.", + "r.headers.", + "r.params.", +] + + +def _check_allowed_field(attr): + if [field for field in allowed_fields if attr.startswith(field)]: + return True + return False + + +def _get_alias(attr): + attr_alias = { + "l": "lines", + "h": "chars", + "w": "words", + "c": "code", + "r": "history", + } + + if attr in attr_alias: + return attr_alias[attr] + + return attr + + +def rsetattr(obj, attr, new_val, operation): + # if not _check_allowed_field(attr): + # raise AttributeError("Unknown field {}".format(attr)) + + pre, _, post = attr.rpartition(".") + + pre_post = None + if len(attr.split(".")) > 3: + pre_post = post + pre, _, post = pre.rpartition(".") + + post = _get_alias(post) + + try: + obj_to_set = rgetattr(obj, pre) if pre else obj + prev_val = rgetattr(obj, attr) + if pre_post is not None: + prev_val = DotDict({pre_post: prev_val}) + + if operation is not None: + val = operation(prev_val, new_val) + else: + if isinstance(prev_val, DotDict): + val = {k: new_val for k, v in prev_val.items()} + else: + val = new_val + + return setattr(obj_to_set, post, val) + except AttributeError: + raise AttributeError( + "rsetattr: Can't set '{}' attribute of {}.".format( + post, obj_to_set.__class__ + ) + ) + + +def rgetattr(obj, attr, *args): + def _getattr(obj, attr): + attr = _get_alias(attr) + try: + return getattr(obj, attr, *args) + except AttributeError: + raise AttributeError( + "rgetattr: Can't get '{}' attribute from '{}'.".format( + attr, obj.__class__ + ) + ) + + # if not _check_allowed_field(attr): + # raise AttributeError("Unknown field {}".format(attr)) + + return functools.reduce(_getattr, [obj] + attr.split(".")) diff --git a/src/wfuzz/helpers/obj_factory.py b/src/wfuzz/helpers/obj_factory.py new file mode 100644 index 00000000..51b1c336 --- /dev/null +++ b/src/wfuzz/helpers/obj_factory.py @@ -0,0 +1,139 @@ +import re +import abc + +from ..helpers.obj_dyn import ( + rgetattr, + rsetattr, +) +from ..exception import FuzzExceptBadOptions + + +class Singleton(type): + """ Singleton metaclass. Use by defining the metaclass of a class Singleton, + e.g.: class ThereCanBeOnlyOne: + __metaclass__ = Singleton + """ + + def __call__(class_, *args, **kwargs): + if not class_.hasInstance(): + class_.instance = super(Singleton, class_).__call__(*args, **kwargs) + return class_.instance + + def deleteInstance(class_): + """ Delete the (only) instance. This method is mainly for unittests so + they can start with a clean slate. """ + if class_.hasInstance(): + del class_.instance + + def hasInstance(class_): + """ Has the (only) instance been created already? """ + return hasattr(class_, "instance") + + +class ObjectFactory: + def __init__(self, builders): + self._builders = builders + + def create(self, key, *args, **kwargs): + builder = self._builders.get(key) + if not builder: + raise ValueError(key) + return builder(*args, **kwargs) + + +class HttpRequestFactory(abc.ABC): + @staticmethod + @abc.abstractmethod + def to_http_object(options, to_http, from_req): + pass + + @staticmethod + @abc.abstractmethod + def from_http_object(options, from_http, raw_header, raw_body): + pass + + +class SeedBuilderHelper: + FUZZ_MARKERS_REGEX = re.compile( + r"(?P(?PFUZ(?P\d)*Z)(?P(?:\[(?P.*?)\])?(?P\{(?P.*?)\})?))" + ) + REQ_ATTR = ["raw_request", "scheme", "method", "auth.credentials"] + + @staticmethod + def _get_markers(text): + return [ + m.groupdict() for m in SeedBuilderHelper.FUZZ_MARKERS_REGEX.finditer(text) + ] + + @staticmethod + def get_marker_dict(freq): + marker_dict_list = [] + + for text in [rgetattr(freq, field) for field in SeedBuilderHelper.REQ_ATTR]: + marker_dict_list += SeedBuilderHelper._get_markers(text) + + # validate + if len({bd["bl_value"] is None for bd in marker_dict_list}) > 1: + raise FuzzExceptBadOptions( + "You must supply a baseline value per FUZZ word." + ) + + return marker_dict_list + + @staticmethod + def _remove_markers(freq, markers, mark_name): + scheme = freq.scheme + for mark in [ + mark[mark_name] for mark in markers if mark[mark_name] is not None + ]: + for field in SeedBuilderHelper.REQ_ATTR: + old_value = rgetattr(freq, field) + new_value = old_value.replace(mark, "") + + if field == "raw_request": + freq.update_from_raw_http(new_value, scheme) + else: + rsetattr(freq, field, new_value, None) + + @staticmethod + def remove_baseline_markers(freq, markers): + SeedBuilderHelper._remove_markers(freq, markers, "full_bl") + return freq + + @staticmethod + def remove_nonfuzz_markers(freq, markers): + SeedBuilderHelper._remove_markers(markers, "nonfuzz_marker") + return freq + + # Not working due to reqresp internals + # def replace_markers(self, seed, fpm): + # for payload in fpm.get_payloads(): + # for field in self.REQ_ATTR: + # old_value = rgetattr(seed, field) + # new_value = old_value.replace(payload.marker, payload.value) + # rsetattr(seed, field, new_value , None) + + @staticmethod + def replace_markers(freq, fpm): + rawReq = str(freq) + rawUrl = freq.redirect_url + scheme = freq.scheme + old_auth = freq.auth + + for payload in [ + payload for payload in fpm.get_payloads() if payload.marker is not None + ]: + if old_auth.method: + old_auth["credentials"] = old_auth["credentials"].replace( + payload.marker, str(payload.value) + ) + rawUrl = rawUrl.replace(payload.marker, str(payload.value)) + rawReq = rawReq.replace(payload.marker, str(payload.value)) + scheme = scheme.replace(payload.marker, str(payload.value)) + + freq.update_from_raw_http(rawReq, scheme) + freq.url = rawUrl + if old_auth.method: + freq.auth = old_auth + + return freq diff --git a/src/wfuzz/helpers/str_func.py b/src/wfuzz/helpers/str_func.py new file mode 100644 index 00000000..44a1147f --- /dev/null +++ b/src/wfuzz/helpers/str_func.py @@ -0,0 +1,98 @@ +import re +import sys +import six + + +from .obj_dic import DotDict + + +def json_minify(string, strip_space=True): + """ + Created on 20/01/2011 + v0.2 (C) Gerald Storer + MIT License + Based on JSON.minify.js: + https://github.com/getify/JSON.minify + Contributers: + - Pradyun S. Gedam (conditions and variable names changed) + """ + + tokenizer = re.compile(r'"|(/\*)|(\*/)|(//)|\n|\r') + end_slashes_re = re.compile(r"(\\)*$") + + in_string = False + in_multi = False + in_single = False + + new_str = [] + index = 0 + + for match in re.finditer(tokenizer, string): + + if not (in_multi or in_single): + tmp = string[index : match.start()] + if not in_string and strip_space: + # replace white space as defined in standard + tmp = re.sub("[ \t\n\r]+", "", tmp) + new_str.append(tmp) + + index = match.end() + val = match.group() + + if val == '"' and not (in_multi or in_single): + escaped = end_slashes_re.search(string, 0, match.start()) + + # start of string or unescaped quote character to end string + if not in_string or (escaped is None or len(escaped.group()) % 2 == 0): + in_string = not in_string + # include " character in next catch + index -= 1 + elif not (in_string or in_multi or in_single): + if val == "/*": + in_multi = True + elif val == "//": + in_single = True + elif val == "*/" and in_multi and not (in_string or in_single): + in_multi = False + elif val in "\r\n" and not (in_multi or in_string) and in_single: + in_single = False + elif not ((in_multi or in_single) or (val in " \r\n\t" and strip_space)): + new_str.append(val) + + new_str.append(string[index:]) + return "".join(new_str) + + +def python2_3_convert_from_unicode(text): + if sys.version_info >= (3, 0): + return text + else: + return convert_to_unicode(text) + + +def python2_3_convert_to_unicode(text): + if sys.version_info >= (3, 0): + return convert_to_unicode(text) + else: + return text + + +def convert_to_unicode(text): + if isinstance(text, dict) or isinstance(text, DotDict): + return { + convert_to_unicode(key): convert_to_unicode(value) + for key, value in list(text.items()) + } + elif isinstance(text, list): + return [convert_to_unicode(element) for element in text] + elif isinstance(text, six.string_types): + return text.encode("utf-8", errors="ignore") + else: + return text + + +def value_in_any_list_item(value, list_obj): + if isinstance(list_obj, list): + return len([item for item in list_obj if value.lower() in item.lower()]) > 0 + elif isinstance(list_obj, str): + return value.lower() in list_obj.lower() diff --git a/src/wfuzz/helpers/utils.py b/src/wfuzz/helpers/utils.py new file mode 100644 index 00000000..eb6151bb --- /dev/null +++ b/src/wfuzz/helpers/utils.py @@ -0,0 +1,22 @@ +from threading import Lock + + +class MyCounter: + def __init__(self, count=0): + self._count = count + self._mutex = Lock() + + def inc(self): + return self._operation(1) + + def dec(self): + return self._operation(-1) + + def _operation(self, dec): + with self._mutex: + self._count += dec + return self._count + + def __call__(self): + with self._mutex: + return self._count diff --git a/src/wfuzz/mixins.py b/src/wfuzz/mixins.py index 7fe3c79f..5cad1c40 100644 --- a/src/wfuzz/mixins.py +++ b/src/wfuzz/mixins.py @@ -3,10 +3,11 @@ # python 2 and 3 import sys + if sys.version_info >= (3, 0): - from urllib.parse import urljoin + from urllib.parse import urljoin, urlparse else: - from urlparse import urljoin + from urlparse import urljoin, urlparse class FuzzRequestSoupMixing(object): @@ -16,7 +17,7 @@ def get_soup(self): except ImportError: raise FuzzExceptBadInstall("You need to install beautifulsoup4 first!") - soup = BeautifulSoup(self.content, 'html.parser') + soup = BeautifulSoup(self.content, "html.parser") return soup @@ -37,30 +38,20 @@ def pstrip(self): @property def is_path(self): - if self.code == 200 and self.url[-1] == '/': + if self.recursive_url and self.recursive_url[-1] == "/": return True - elif self.code >= 300 and self.code < 400: - if "Location" in self.headers.response and self.headers.response["Location"][-1] == '/': - return True - elif self.code == 401: - if self.url[-1] == '/': - return True return False @property def recursive_url(self): - if self.code >= 300 and self.code < 400 and "Location" in self.headers.response: - new_url = self.headers.response["Location"] - if not new_url[-1] == '/': - new_url += "/" - # taking into consideration redirections to /xxx/ without full URL - new_url = urljoin(self.url, new_url) - elif self.code == 401 or self.code == 200: - new_url = self.url - if not self.url[-1] == '/': - new_url = "/" - else: - raise Exception("Error generating recursive url") + if self.code >= 300 and self.code < 308 and "Location" in self.headers.response: + location_url = self.headers.response["Location"] + location_parsed_url = urlparse(location_url) + + if not location_parsed_url.scheme and not location_parsed_url.netloc: + return urljoin(self.url, location_url) + elif self.code in [200, 401] and self.url[-1] == "/": + return self.url - return new_url + "FUZZ" + return None diff --git a/src/wfuzz/myhttp.py b/src/wfuzz/myhttp.py index 7a09d028..948cd76f 100644 --- a/src/wfuzz/myhttp.py +++ b/src/wfuzz/myhttp.py @@ -7,9 +7,23 @@ from .exception import FuzzExceptBadOptions, FuzzExceptNetError +from .factories.reqresp_factory import ReqRespRequestFactory + +# See https://curl.haxx.se/libcurl/c/libcurl-errors.html +UNRECOVERABLE_PYCURL_EXCEPTIONS = [ + 28, # Operation timeout. The specified time-out period was reached according to the conditions. + 7, # Failed to connect() to host or proxy. + 6, # Couldn't resolve host. The given remote host was not resolved. + 5, # Couldn't resolve proxy. The given proxy host could not be resolved. +] + +# Other common pycurl exceptions: +# Exception in perform (35, 'error:0B07C065:x509 certificate routines:X509_STORE_add_cert:cert already in hash table') +# Exception in perform (18, 'SSL read: error:0B07C065:x509 certificate routines:X509_STORE_add_cert:cert already in hash table, errno 11') + class HttpPool: - HTTPAUTH_BASIC, HTTPAUTH_NTLM, HTTPAUTH_DIGEST = ('basic', 'ntlm', 'digest') + HTTPAUTH_BASIC, HTTPAUTH_NTLM, HTTPAUTH_DIGEST = ("basic", "ntlm", "digest") newid = itertools.count(0) def __init__(self, options): @@ -54,7 +68,7 @@ def job_stats(self): with self.mutex_stats: dic = { "http_processed": self.processed, - "http_registered": len(self._registered) + "http_registered": len(self._registered), } return dic @@ -75,15 +89,19 @@ def _new_pool(self): self.pool_map[poolid]["proxy"] = None if self.options.get("proxies"): - self.pool_map[poolid]["proxy"] = self._get_next_proxy(self.options.get("proxies")) + self.pool_map[poolid]["proxy"] = self._get_next_proxy( + self.options.get("proxies") + ) return poolid def _prepare_curl_h(self, curl_h, fuzzres, poolid): - new_curl_h = fuzzres.history.to_http_object(curl_h) + new_curl_h = ReqRespRequestFactory.to_http_object( + self.options, fuzzres.history, curl_h + ) new_curl_h = self._set_extra_options(new_curl_h, fuzzres, poolid) - new_curl_h.response_queue = ((BytesIO(), BytesIO(), fuzzres, poolid)) + new_curl_h.response_queue = (BytesIO(), BytesIO(), fuzzres, poolid) new_curl_h.setopt(pycurl.WRITEFUNCTION, new_curl_h.response_queue[0].write) new_curl_h.setopt(pycurl.HEADERFUNCTION, new_curl_h.response_queue[1].write) @@ -142,7 +160,9 @@ def _set_extra_options(self, c, fuzzres, poolid): elif ptype == "HTTP": c.setopt(pycurl.PROXY, "%s:%s" % (ip, port)) else: - raise FuzzExceptBadOptions("Bad proxy type specified, correct values are HTTP, SOCKS4 or SOCKS5.") + raise FuzzExceptBadOptions( + "Bad proxy type specified, correct values are HTTP, SOCKS4 or SOCKS5." + ) else: c.setopt(pycurl.PROXY, "") @@ -160,7 +180,13 @@ def _process_curl_handle(self, curl_h): buff_body, buff_header, res, poolid = curl_h.response_queue try: - res.history.from_http_object(curl_h, buff_header.getvalue(), buff_body.getvalue()) + ReqRespRequestFactory.from_http_object( + self.options, + res.history, + curl_h, + buff_header.getvalue(), + buff_body.getvalue(), + ) except Exception as e: self.pool_map[poolid]["queue"].put(res.update(exception=e)) else: @@ -171,19 +197,7 @@ def _process_curl_handle(self, curl_h): self.processed += 1 def _process_curl_should_retry(self, res, errno, poolid): - # Usual suspects: - - # Exception in perform (35, 'error:0B07C065:x509 certificate routines:X509_STORE_add_cert:cert already in hash table') - # Exception in perform (18, 'SSL read: error:0B07C065:x509 certificate routines:X509_STORE_add_cert:cert already in hash table, errno 11') - # Exception in perform (28, 'Connection time-out') - # Exception in perform (7, "couldn't connect to host") - # Exception in perform (6, "Couldn't resolve host 'www.xxx.com'") - # (28, 'Operation timed out after 20000 milliseconds with 0 bytes received') - # Exception in perform (28, 'SSL connection timeout') - # 5 Couldn't resolve proxy 'aaa' - - # retry requests with recoverable errors - if errno not in [28, 7, 6, 5]: + if errno not in UNRECOVERABLE_PYCURL_EXCEPTIONS: res.history.wf_retries += 1 if res.history.wf_retries < self.options.get("retries"): @@ -227,9 +241,7 @@ def _read_multi_stack(self): curl_h = self.curlh_freelist.pop() fuzzres, poolid = self._request_list.popleft() - self.m.add_handle( - self._prepare_curl_h(curl_h, fuzzres, poolid) - ) + self.m.add_handle(self._prepare_curl_h(curl_h, fuzzres, poolid)) self._stop_to_pools() diff --git a/src/wfuzz/myqueues.py b/src/wfuzz/myqueues.py index 7bf7157c..620b71c2 100644 --- a/src/wfuzz/myqueues.py +++ b/src/wfuzz/myqueues.py @@ -8,7 +8,7 @@ from queue import PriorityQueue from threading import Thread, RLock -from .fuzzobjects import FuzzResult +from .fuzzobjects import FuzzError, FuzzType, FuzzItem from .exception import FuzzException, FuzzExceptInternalError @@ -45,7 +45,7 @@ def __init__(self, options, queue_out=None, limit=0): self.duplicated = False self.syncq = None - self.stats = options.get("compiled_genreq").stats + self.stats = options.get("compiled_stats") self.options = options Thread.__init__(self) @@ -60,6 +60,9 @@ def process(self, item): def get_name(self): raise NotImplementedError + def items_to_process(self, item): + return item.item_type in [FuzzType.RESULT] + # Override this method if needed. This will be called just before cancelling the job. def cancel(self): pass @@ -88,11 +91,13 @@ def send(self, item): self.queue_out.put(item) def discard(self, item): - if item.type == FuzzResult.result: - item.type = FuzzResult.discarded - self.syncq.put(item) + if item.item_type == FuzzType.RESULT: + item.item_type = FuzzType.DISCARDED + self.send(item) else: - raise FuzzExceptInternalError(FuzzException.FATAL, "Only results can be discarded") + raise FuzzExceptInternalError( + FuzzException.FATAL, "Only results can be discarded" + ) def join(self): MyPriorityQueue.join(self) @@ -105,7 +110,7 @@ def _cleanup(self): pass def _throw(self, e): - self.syncq.put_first(FuzzResult.to_new_exception(e)) + self.syncq.put_first(FuzzError(e)) def get_stats(self): return {self.get_name(): self.qsize()} @@ -125,20 +130,23 @@ def run(self): elif cancelling: self.task_done() continue - elif item.type == FuzzResult.startseed: + elif item.item_type == FuzzType.STARTSEED: self.stats.mark_start() - elif item.type == FuzzResult.endseed: + elif item.item_type == FuzzType.ENDSEED: if not self.duplicated: self.send_last(item) self.task_done() continue - elif item.type == FuzzResult.cancel: + elif item.item_type == FuzzType.CANCEL: cancelling = True self.send_first(item) self.task_done() continue - self.process(item) + if self.items_to_process(item): + self.process(item) + else: + self.send(item) self.task_done() except Exception as e: @@ -152,10 +160,7 @@ class LastFuzzQueue(FuzzQueue): def __init__(self, options, queue_out=None, limit=0): FuzzQueue.__init__(self, options, queue_out, limit) - self.items_to_send = [FuzzResult.result] - - if options["send_discarded"]: - self.items_to_send.append(FuzzResult.discarded) + self.items_to_send = [FuzzType.RESULT] def get_name(self): return "LastFuzzQueue" @@ -167,11 +172,11 @@ def _cleanup(self): pass def send(self, item): - if item.type in self.items_to_send: + if item.item_type in self.items_to_send: self.queue_out.put(item) def _throw(self, e): - self.queue_out.put_first(FuzzResult.to_new_exception(e)) + self.queue_out.put_first(FuzzError(e)) def run(self): cancelling = False @@ -186,22 +191,22 @@ def run(self): break elif cancelling: continue - elif item.type == FuzzResult.error: + elif item.item_type == FuzzType.ERROR: self.qmanager.cancel() self.send_first(item) continue - elif item.type == FuzzResult.cancel: + elif item.item_type == FuzzType.CANCEL: cancelling = True continue self.send(item) - if item.type == FuzzResult.endseed: + if item.item_type == FuzzType.ENDSEED: self.stats.pending_seeds.dec() - elif item.type in [FuzzResult.result, FuzzResult.discarded]: + elif item.item_type in [FuzzType.RESULT, FuzzType.DISCARDED]: self.stats.processed.inc() self.stats.pending_fuzz.dec() - if item.type == FuzzResult.discarded: + if item.item_type == FuzzType.DISCARDED: self.stats.filtered.inc() if self.stats.pending_fuzz() == 0 and self.stats.pending_seeds() == 0: @@ -319,7 +324,7 @@ def join(self, remove=False): for k, q in list(self._queues.items()): q.join() if remove: - del(self._queues[k]) + del self._queues[k] def start(self): with self._mutex: @@ -328,14 +333,14 @@ def start(self): for q in list(self._queues.values()): q.qstart() - list(self._queues.values())[0].put_first(FuzzResult.to_new_signal(FuzzResult.startseed)) + list(self._queues.values())[0].put_first(FuzzItem(FuzzType.STARTSEED)) def cleanup(self): with self._mutex: if self._queues: list(self._queues.values())[0].put_last(None) self.join(remove=True) - self.options.get("compiled_genreq").stats.mark_end() + self.options.get("compiled_stats").mark_end() self._lastq.put_last(None, wait=False) self._queues = collections.OrderedDict() @@ -347,7 +352,7 @@ def cancel(self): # stop processing pending items for q in list(self._queues.values()): q.cancel() - q.put_first(FuzzResult.to_new_signal(FuzzResult.cancel)) + q.put_first(FuzzItem(FuzzType.CANCEL)) # wait for cancel to be processed self.join() diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 3c208a08..9e96f174 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -1,10 +1,20 @@ -from .exception import FuzzExceptBadRecipe, FuzzExceptBadOptions, FuzzExceptBadFile -from .facade import Facade, ERROR_CODE, BASELINE_CODE +from .exception import ( + FuzzExceptBadRecipe, + FuzzExceptBadOptions, + FuzzExceptBadFile, +) +from .facade import ( + Facade, + ERROR_CODE, + BASELINE_CODE, +) +from .factories.fuzzresfactory import resfactory +from .factories.dictfactory import dictionary_factory from .fuzzobjects import FuzzStats -from .filter import FuzzResFilter -from .core import requestGenerator -from .utils import ( +from .filters.ppfilter import FuzzResFilter +from .filters.simplefilter import FuzzResSimpleFilter +from .helpers.str_func import ( json_minify, python2_3_convert_from_unicode, ) @@ -28,7 +38,20 @@ class FuzzSession(UserDict): def __init__(self, **kwargs): self.data = self._defaults() - self.keys_not_to_dump = ["interactive", "recipe", "seed_payload", "send_discarded", "compiled_genreq", "compiled_filter", "compiled_prefilter", "compiled_printer", "description", "show_field"] + self.keys_not_to_dump = [ + "interactive", + "recipe", + "seed_payload", + "compiled_stats", + "compiled_dictio", + "compiled_simple_filter", + "compiled_filter", + "compiled_prefilter", + "compiled_printer", + "description", + "show_field", + "transport", + ] # recipe must be superseded by options if "recipe" in kwargs and kwargs["recipe"]: @@ -44,7 +67,6 @@ def __init__(self, **kwargs): def _defaults(self): return dict( - send_discarded=False, console_printer="", hs=None, hc=[], @@ -63,20 +85,21 @@ def _defaults(self): previous=False, verbose=False, interactive=False, - dryrun=False, + transport="http", recipe=[], save="", proxies=None, - conn_delay=int(Facade().sett.get('connection', 'conn_delay')), - req_delay=int(Facade().sett.get('connection', 'req_delay')), - retries=int(Facade().sett.get('connection', 'retries')), + conn_delay=int(Facade().sett.get("connection", "conn_delay")), + req_delay=int(Facade().sett.get("connection", "req_delay")), + retries=int(Facade().sett.get("connection", "retries")), rlevel=0, + dlevel=4, scanmode=False, delay=None, - concurrent=int(Facade().sett.get('connection', 'concurrent')), + concurrent=int(Facade().sett.get("connection", "concurrent")), url="", method=None, - auth=(None, None), + auth={}, follow=False, postdata=None, headers=[], @@ -85,21 +108,23 @@ def _defaults(self): script="", script_args={}, connect_to_ip=None, - description=None, + fields=[], no_cache=False, show_field=None, - # this is equivalent to payloads but in a different format dictio=None, - # these will be compiled seed_payload=False, filter="", - prefilter="", - compiled_genreq=None, + prefilter=[], compiled_filter=None, - compiled_prefilter=None, + compiled_prefilter=[], compiled_printer=None, + compiled_seed=None, + compiled_baseline=None, + compiled_stats=None, + compiled_dictio=None, + exec_mode="api", ) def update(self, options): @@ -108,75 +133,83 @@ def update(self, options): def validate(self): error_list = [] - if self.data['dictio'] and self.data['payloads']: - raise FuzzExceptBadOptions("Bad usage: Dictio and payloads options are mutually exclusive. Only one could be specified.") + if self.data["dictio"] and self.data["payloads"]: + raise FuzzExceptBadOptions( + "Bad usage: Dictio and payloads options are mutually exclusive. Only one could be specified." + ) - if self.data['rlevel'] > 0 and self.data['dryrun']: - error_list.append("Bad usage: Recursion cannot work without making any HTTP request.") + if self.data["rlevel"] > 0 and self.data["transport"] == "dryrun": + error_list.append( + "Bad usage: Recursion cannot work without making any HTTP request." + ) - if self.data['script'] and self.data['dryrun']: - error_list.append("Bad usage: Plugins cannot work without making any HTTP request.") + if self.data["script"] and self.data["transport"] == "dryrun": + error_list.append( + "Bad usage: Plugins cannot work without making any HTTP request." + ) - if self.data['no_cache'] not in [True, False]: + if self.data["no_cache"] not in [True, False]: raise FuzzExceptBadOptions("Bad usage: No-cache is a boolean value") - if not self.data['url']: + if not self.data["url"]: error_list.append("Bad usage: You must specify an URL.") - if not self.data['payloads'] and not self.data["dictio"]: + if not self.data["payloads"] and not self.data["dictio"]: error_list.append("Bad usage: You must specify a payload.") if self.data["hs"] and self.data["ss"]: - raise FuzzExceptBadOptions("Bad usage: Hide and show regex filters flags are mutually exclusive. Only one could be specified.") + raise FuzzExceptBadOptions( + "Bad usage: Hide and show regex filters flags are mutually exclusive. Only one could be specified." + ) if self.data["rlevel"] < 0: - raise FuzzExceptBadOptions("Bad usage: Recursion level must be a positive int.") + raise FuzzExceptBadOptions( + "Bad usage: Recursion level must be a positive int." + ) - if self.data['allvars'] not in [None, 'allvars', 'allpost', 'allheaders']: - raise FuzzExceptBadOptions("Bad options: Incorrect all parameters brute forcing type specified, correct values are allvars,allpost or allheaders.") + if self.data["allvars"] not in [None, "allvars", "allpost", "allheaders"]: + raise FuzzExceptBadOptions( + "Bad options: Incorrect all parameters brute forcing type specified, correct values are allvars,allpost or allheaders." + ) - if self.data['proxies']: - for ip, port, ttype in self.data['proxies']: + if self.data["proxies"]: + for ip, port, ttype in self.data["proxies"]: if ttype not in ("SOCKS5", "SOCKS4", "HTTP"): - raise FuzzExceptBadOptions("Bad proxy type specified, correct values are HTTP, SOCKS4 or SOCKS5.") - - try: - if [x for x in ["sc", "sw", "sh", "sl"] if len(self.data[x]) > 0] and \ - [x for x in ["hc", "hw", "hh", "hl"] if len(self.data[x]) > 0]: - raise FuzzExceptBadOptions("Bad usage: Hide and show filters flags are mutually exclusive. Only one group could be specified.") - - if ([x for x in ["sc", "sw", "sh", "sl"] if len(self.data[x]) > 0] or - [x for x in ["hc", "hw", "hh", "hl"] if len(self.data[x]) > 0]) and \ - self.data['filter']: - raise FuzzExceptBadOptions("Bad usage: Advanced and filter flags are mutually exclusive. Only one could be specified.") - except TypeError: - raise FuzzExceptBadOptions("Bad options: Filter must be specified in the form of [int, ... , int].") + raise FuzzExceptBadOptions( + "Bad proxy type specified, correct values are HTTP, SOCKS4 or SOCKS5." + ) return error_list def export_to_file(self, filename): try: - with open(filename, 'w') as f: + with open(filename, "w") as f: f.write(self.export_json()) except IOError: raise FuzzExceptBadFile("Error writing recipe file.") def import_from_file(self, filename): try: - with open(filename, 'r') as f: + with open(filename, "r") as f: self.import_json(f.read()) except IOError: - raise FuzzExceptBadFile("Error loading recipe file.") + raise FuzzExceptBadFile("Error loading recipe file {}.".format(filename)) + except json.decoder.JSONDecodeError as e: + raise FuzzExceptBadRecipe( + "Incorrect JSON recipe {} format: {}".format(filename, str(e)) + ) def import_json(self, data): js = json.loads(json_minify(data)) try: - if js['version'] == "0.2" and 'wfuzz_recipe' in js: - for section in js['wfuzz_recipe'].keys(): - for k, v in js['wfuzz_recipe'].items(): - if k not in self.keys_not_to_dump: - # python 2 and 3 hack + if js["version"] == "0.2" and "wfuzz_recipe" in js: + for k, v in js["wfuzz_recipe"].items(): + if k not in self.keys_not_to_dump: + # python 2 and 3 hack + if k in self.data and isinstance(self.data[k], list): + self.data[k] += python2_3_convert_from_unicode(v) + else: self.data[k] = python2_3_convert_from_unicode(v) else: raise FuzzExceptBadRecipe("Unsupported recipe version.") @@ -184,27 +217,25 @@ def import_json(self, data): raise FuzzExceptBadRecipe("Incorrect recipe format.") def export_json(self): - tmp = dict( - version="0.2", - wfuzz_recipe=defaultdict(dict) - ) + tmp = dict(version="0.2", wfuzz_recipe=defaultdict(dict)) defaults = self._defaults() # Only dump the non-default options for k, v in self.data.items(): if v != defaults[k] and k not in self.keys_not_to_dump: - tmp['wfuzz_recipe'][k] = self.data[k] + tmp["wfuzz_recipe"][k] = self.data[k] - return json.dumps(tmp, sort_keys=True, indent=4, separators=(',', ': ')) + return json.dumps(tmp, sort_keys=True, indent=4, separators=(",", ": ")) def payload(self, **kwargs): try: self.data.update(kwargs) - self.data['compiled_genreq'] = requestGenerator(self) - for r in self.data['compiled_genreq'].get_dictio(): - yield r + self.compile_seeds() + self.compile_dictio() + for r in self.data["compiled_dictio"]: + yield tuple((fuzz_word.content for fuzz_word in r)) finally: - self.data['compiled_genreq'].close() + self.data["compiled_dictio"].cleanup() def fuzz(self, **kwargs): self.data.update(kwargs) @@ -219,7 +250,7 @@ def fuzz(self, **kwargs): finally: if fz: fz.cancel_job() - self.stats.update(fz.genReq.stats) + self.stats.update(self.data["compiled_stats"]) if self.http_pool: self.http_pool.deregister() @@ -241,6 +272,37 @@ def __enter__(self): def __exit__(self, *args): self.close() + def get_fuzz_words(self): + fuzz_words = self.data["compiled_filter"].get_fuzz_words() + + for comp_obj in ["compiled_seed", "compiled_baseline"]: + if self.data[comp_obj]: + fuzz_words += self.data[comp_obj].payload_man.get_fuzz_words() + + for prefilter in self.data["compiled_prefilter"]: + fuzz_words += prefilter.get_fuzz_words() + + if self.data["url"] == "FUZZ": + fuzz_words.append("FUZZ") + + return set(fuzz_words) + + def compile_dictio(self): + if self.data["allvars"]: + self.data["compiled_dictio"] = dictionary_factory.create( + "dictio_from_allvar", self + ) + else: + self.data["compiled_dictio"] = dictionary_factory.create( + "dictio_from_options", self + ) + + def compile_seeds(self): + self.data["compiled_seed"] = resfactory.create("seed_from_options", self) + self.data["compiled_baseline"] = resfactory.create( + "baseline_from_options", self + ) + def compile(self): # Validate options error = self.validate() @@ -253,43 +315,65 @@ def compile(self): try: filename, printer = self.data["printer"] except ValueError: - raise FuzzExceptBadOptions("Bad options: Printer must be specified in the form of ('filename', 'printer')") + raise FuzzExceptBadOptions( + "Bad options: Printer must be specified in the form of ('filename', 'printer')" + ) if filename: if printer == "default" or not printer: - printer = Facade().sett.get('general', 'default_printer') - self.data["compiled_printer"] = Facade().printers.get_plugin(printer)(filename) + printer = Facade().sett.get("general", "default_printer") + self.data["compiled_printer"] = Facade().printers.get_plugin(printer)( + filename + ) try: - self.data['hc'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['hc']] - self.data['hw'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['hw']] - self.data['hl'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['hl']] - self.data['hh'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['hh']] - - self.data['sc'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['sc']] - self.data['sw'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['sw']] - self.data['sl'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['sl']] - self.data['sh'] = [BASELINE_CODE if i == "BBB" else ERROR_CODE if i == "XXX" else int(i) for i in self.data['sh']] + for filter_option in ["hc", "hw", "hl", "hh", "sc", "sw", "sl", "sh"]: + self.data[filter_option] = [ + BASELINE_CODE + if i == "BBB" + else ERROR_CODE + if i == "XXX" + else int(i) + for i in self.data[filter_option] + ] except ValueError: - raise FuzzExceptBadOptions("Bad options: Filter must be specified in the form of [int, ... , int, BBB, XXX].") + raise FuzzExceptBadOptions( + "Bad options: Filter must be specified in the form of [int, ... , int, BBB, XXX]." + ) + + self.compile_seeds() + self.compile_dictio() # filter options - self.data["compiled_filter"] = FuzzResFilter.from_options(self) - self.data["compiled_prefilter"] = FuzzResFilter(filter_string=self.data['prefilter']) + self.data["compiled_simple_filter"] = FuzzResSimpleFilter.from_options(self) + self.data["compiled_filter"] = FuzzResFilter(self.data["filter"]) + for prefilter in self.data["prefilter"]: + self.data["compiled_prefilter"].append( + FuzzResFilter(filter_string=prefilter) + ) - # seed - self.data["compiled_genreq"] = requestGenerator(self) + self.data["compiled_stats"] = FuzzStats.from_options(self) # Check payload num - fuzz_words = self.data["compiled_filter"].get_fuzz_words() + self.data["compiled_prefilter"].get_fuzz_words() + self.data["compiled_genreq"].get_fuzz_words() + fuzz_words = self.get_fuzz_words() - if self.data['allvars'] is None and len(set(fuzz_words)) == 0: + if self.data["compiled_dictio"].width() != len(fuzz_words): + raise FuzzExceptBadOptions( + "FUZZ words and number of payloads do not match!" + ) + + if self.data["allvars"] is None and len(fuzz_words) == 0: raise FuzzExceptBadOptions("You must specify at least a FUZZ word!") - if self.data["compiled_genreq"].baseline is None and (BASELINE_CODE in self.data['hc'] or - BASELINE_CODE in self.data['hl'] or BASELINE_CODE in self.data['hw'] or - BASELINE_CODE in self.data['hh']): - raise FuzzExceptBadOptions("Bad options: specify a baseline value when using BBB") + if self.data["compiled_baseline"] is None and ( + BASELINE_CODE in self.data["hc"] + or BASELINE_CODE in self.data["hl"] + or BASELINE_CODE in self.data["hw"] + or BASELINE_CODE in self.data["hh"] + ): + raise FuzzExceptBadOptions( + "Bad options: specify a baseline value when using BBB" + ) if self.data["script"]: Facade().scripts.kbase.update(self.data["script_args"]) @@ -305,6 +389,9 @@ def compile(self): return self def close(self): + if self.data["compiled_dictio"]: + self.data["compiled_dictio"].cleanup() + if self.http_pool: self.http_pool.deregister() self.http_pool = None diff --git a/src/wfuzz/plugin_api/base.py b/src/wfuzz/plugin_api/base.py index 34b91c05..80cfdbae 100644 --- a/src/wfuzz/plugin_api/base.py +++ b/src/wfuzz/plugin_api/base.py @@ -1,8 +1,12 @@ -from wfuzz.fuzzobjects import PluginResult, PluginRequest -from wfuzz.exception import FuzzExceptBadFile, FuzzExceptBadOptions, FuzzExceptPluginError +from wfuzz.fuzzobjects import FuzzWord +from wfuzz.exception import ( + FuzzExceptBadFile, + FuzzExceptBadOptions, + FuzzExceptPluginError, +) from wfuzz.facade import Facade - -from wfuzz.utils import find_file_in_paths +from wfuzz.factories.plugin_factory import plugin_factory +from wfuzz.helpers.file_func import find_file_in_paths import sys import os @@ -12,7 +16,7 @@ # Util methods for accessing search results -class BasePlugin(): +class BasePlugin: def __init__(self): self.results_queue = None self.base_fuzz_res = None @@ -22,7 +26,9 @@ def __init__(self): param_name = "%s.%s" % (self.name, name) if required and param_name not in list(self.kbase.keys()): - raise FuzzExceptBadOptions("Plugins, missing parameter %s!" % (param_name,)) + raise FuzzExceptBadOptions( + "Plugins, missing parameter %s!" % (param_name,) + ) if param_name not in list(self.kbase.keys()): self.kbase[param_name] = default_value @@ -33,37 +39,36 @@ def run(self, fuzzresult, control_queue, results_queue): self.base_fuzz_res = fuzzresult self.process(fuzzresult) except Exception as e: - plres = PluginResult() - plres.source = "$$exception$$" - plres.issue = "Exception within plugin %s: %s" % (self.name, str(e)) - results_queue.put(plres) + results_queue.put(plugin_factory.create("plugin_from_error", self.name, e)) finally: control_queue.get() control_queue.task_done() return def process(self, fuzzresult): - ''' + """ This is were the plugin processing is done. Any wfuzz plugin must implement this method, do its job with the fuzzresult received and: - queue_url: if it is a discovery plugin enqueing more HTTP request that at some point will generate more results - add_result: Add information about the obtained results after the processing with an accurate description A kbase (get_kbase, has_kbase, add_kbase) is shared between all plugins. this can be used to store and retrieve relevant "collaborative" information. - ''' + """ raise NotImplementedError def validate(self): raise FuzzExceptPluginError("Method count not implemented") def add_result(self, issue): - plres = PluginResult() - plres.source = self.name - plres.issue = issue - - self.results_queue.put(plres) + self.results_queue.put( + plugin_factory.create("plugin_from_finding", self.name, issue) + ) def queue_url(self, url): - self.results_queue.put(PluginRequest.from_fuzzRes(self.base_fuzz_res, url, self.name)) + self.results_queue.put( + plugin_factory.create( + "plugin_from_recursion", self.name, self.base_fuzz_res, url + ) + ) class BasePrinter: @@ -71,7 +76,7 @@ def __init__(self, output): self.f = None if output: try: - self.f = open(output, 'w') + self.f = open(output, "w") except IOError as e: raise FuzzExceptBadFile("Error opening file. %s" % str(e)) else: @@ -101,25 +106,40 @@ def __init__(self, params): raise FuzzExceptBadOptions("Too many plugin parameters specified") # Check for allowed parameters - if [k for k in list(self.params.keys()) if k not in [x[0] for x in self.parameters] and k not in ["encoder", "default"]]: - raise FuzzExceptBadOptions("Plugin %s, unknown parameter specified!" % (self.name)) + if [ + k + for k in list(self.params.keys()) + if k not in [x[0] for x in self.parameters] + and k not in ["encoder", "default"] + ]: + raise FuzzExceptBadOptions( + "Plugin %s, unknown parameter specified!" % (self.name) + ) # check mandatory params, assign default values for name, default_value, required, description in self.parameters: if required and name not in self.params: - raise FuzzExceptBadOptions("Plugin %s, missing parameter %s!" % (self.name, name)) + raise FuzzExceptBadOptions( + "Plugin %s, missing parameter %s!" % (self.name, name) + ) if name not in self.params: self.params[name] = default_value + def get_type(self): + raise FuzzExceptPluginError("Method get_type not implemented") + + def get_next(self): + raise FuzzExceptPluginError("Method get_next not implemented") + def __next__(self): - raise FuzzExceptPluginError("Method next not implemented") + return FuzzWord(self.get_next(), self.get_type()) def count(self): raise FuzzExceptPluginError("Method count not implemented") def __iter__(self): - raise FuzzExceptPluginError("Method iter not implemented") + return self def close(self): pass @@ -128,7 +148,7 @@ def find_file(self, name): if os.path.exists(name): return name - for pa in Facade().sett.get('general', 'lookup_dirs').split(","): + for pa in Facade().sett.get("general", "lookup_dirs").split(","): fn = find_file_in_paths(name, pa) if fn is not None: diff --git a/src/wfuzz/plugin_api/payloadtools.py b/src/wfuzz/plugin_api/payloadtools.py index ee91833a..5871f49a 100644 --- a/src/wfuzz/plugin_api/payloadtools.py +++ b/src/wfuzz/plugin_api/payloadtools.py @@ -1,6 +1,10 @@ -from wfuzz.exception import FuzzExceptMissingAPIKey, FuzzExceptResourceParseError +from wfuzz.exception import ( + FuzzExceptMissingAPIKey, + FuzzExceptResourceParseError, + FuzzExceptPluginLoadError, +) from wfuzz.facade import Facade -from wfuzz.utils import MyCounter +from wfuzz.helpers.utils import MyCounter # Python 2 and 3: alternative 4 @@ -18,13 +22,118 @@ from threading import Thread from queue import Queue -import shodan +IMPORTED_SHODAN = True +try: + import shodan +except ImportError: + IMPORTED_SHODAN = False -# TODO: test cases m = { - 'matches': [ - {'_shodan': {'id': '54e0ae62-9e22-404b-91b4-92f99e89c987', 'options': {}, 'ptr': True, 'module': 'auto', 'crawler': '62861a86c4e4b71dceed5113ce9593b98431f89a'}, 'hash': -1355923443, 'os': None, 'ip': 1240853908, 'isp': 'Comcast Cable', 'http': {'html_hash': -2142469325, 'robots_hash': None, 'redirects': [], 'securitytxt': None, 'title': '400 Bad Request', 'sitemap_hash': None, 'robots': None, 'favicon': None, 'host': '73.245.237.148', 'html': '\n\n400 Bad Request\n\n

Bad Request

\n

Your browser sent a request that this server could not understand.
\nReason: You\'re speaking plain HTTP to an SSL-enabled server port.
\n Instead use the HTTPS scheme to access this URL, please.
\n

\n

Additionally, a 404 Not Found\nerror was encountered while trying to use an ErrorDocument to handle the request.

\n\n', 'location': '/', 'components': {}, 'server': 'Apache', 'sitemap': None, 'securitytxt_hash': None}, 'port': 9445, 'hostnames': ['c-73-245-237-148.hsd1.fl.comcast.net'], 'location': {'city': 'Fort Lauderdale', 'region_code': 'FL', 'area_code': 954, 'longitude': -80.3704, 'country_code3': 'USA', 'country_name': 'United States', 'postal_code': '33331', 'dma_code': 528, 'country_code': 'US', 'latitude': 26.065200000000004}, 'timestamp': '2019-04-10T10:30:48.297701', 'domains': ['comcast.net'], 'org': 'Comcast Cable', 'data': 'HTTP/1.1 400 Bad Request\r\nDate: Wed, 10 Apr 2019 10:19:07 GMT\r\nServer: Apache\r\nContent-Length: 481\r\nConnection: close\r\nContent-Type: text/html; charset=iso-8859-1\r\n\r\n', 'asn': 'AS7922', 'transport': 'tcp', 'ip_str': '73.245.237.148'}, - {'_shodan': {'id': '4ace6fd1-8295-4aea-a086-2280598ca9e7', 'options': {}, 'ptr': True, 'module': 'auto', 'crawler': '62861a86c4e4b71dceed5113ce9593b98431f89a'}, 'product': 'Apache httpd', 'hash': 370611044, 'os': None, 'ip': 35226500, 'isp': 'EE High Speed Internet', 'http': {'html_$ ash': -163723763, 'robots_hash': None, 'redirects': [], 'securitytxt': None, 'title': '401 Authorization Required', 'sitemap_hash': None, 'robots': None, 'favicon': None, 'host': '2.25.131.132', 'html': '401 Authorization Required\n

401 Authoriza$ ion Required

\nBrowser not authentication-capable or authentication failed.\n\n', 'location': '/', 'components': {}, 'server': 'Apache', 'sitemap': None, 'securitytxt_hash': None}, 'cpe': ['cpe:/a:apache:http_server'], 'port': 8085, 'hostnames': [], 'location': {'city': '$ helmsford', 'region_code': 'E4', 'area_code': None, 'longitude': 0.48330000000001405, 'country_code3': 'GBR', 'country_name': 'United Kingdom', 'postal_code': 'CM2', 'dma_code': None, 'country_code': 'GB', 'latitude': 51.733300000000014}, 'timestamp': '2019-04-10T11:03:59.955967', '$ omains': [], 'org': 'EE High Speed Internet', 'data': 'HTTP/1.1 401 Unauthorized\r\nServer: Apache\r\nConnection: Close\r\nContent-type: text/html\r\nWWW-Authenticate: Digest realm="DSLForum CPE Management", algorithm=MD5, qop=auth, stale=FALSE, nonce="3d7a3f71e72e095dba31fd77d4db74$5", opaque="5ccc069c403ebaf9f0171e9517f40e41"\r\n\r\n', 'asn': 'AS12576', 'transport': 'tcp', 'ip_str': '2.25.131.132'}, + "matches": [ + { + "_shodan": { + "id": "54e0ae62-9e22-404b-91b4-92f99e89c987", + "options": {}, + "ptr": True, + "module": "auto", + "crawler": "62861a86c4e4b71dceed5113ce9593b98431f89a", + }, + "hash": -1355923443, + "os": None, + "ip": 1240853908, + "isp": "Comcast Cable", + "http": { + "html_hash": -2142469325, + "robots_hash": None, + "redirects": [], + "securitytxt": None, + "title": "400 Bad Request", + "sitemap_hash": None, + "robots": None, + "favicon": None, + "host": "73.245.237.148", + "html": '\n\n400 Bad Request\n\n

Bad Request

\n

Your browser sent a request that this server could not understand.
\nReason: You\'re speaking plain HTTP to an SSL-enabled server port.
\n Instead use the HTTPS scheme to access this URL, please.
\n

\n

Additionally, a 404 Not Found\nerror was encountered while trying to use an ErrorDocument to handle the request.

\n\n', + "location": "/", + "components": {}, + "server": "Apache", + "sitemap": None, + "securitytxt_hash": None, + }, + "port": 9445, + "hostnames": ["c-73-245-237-148.hsd1.fl.comcast.net"], + "location": { + "city": "Fort Lauderdale", + "region_code": "FL", + "area_code": 954, + "longitude": -80.3704, + "country_code3": "USA", + "country_name": "United States", + "postal_code": "33331", + "dma_code": 528, + "country_code": "US", + "latitude": 26.065200000000004, + }, + "timestamp": "2019-04-10T10:30:48.297701", + "domains": ["comcast.net"], + "org": "Comcast Cable", + "data": "HTTP/1.1 400 Bad Request\r\nDate: Wed, 10 Apr 2019 10:19:07 GMT\r\nServer: Apache\r\nContent-Length: 481\r\nConnection: close\r\nContent-Type: text/html; charset=iso-8859-1\r\n\r\n", + "asn": "AS7922", + "transport": "tcp", + "ip_str": "73.245.237.148", + }, + { + "_shodan": { + "id": "4ace6fd1-8295-4aea-a086-2280598ca9e7", + "options": {}, + "ptr": True, + "module": "auto", + "crawler": "62861a86c4e4b71dceed5113ce9593b98431f89a", + }, + "product": "Apache httpd", + "hash": 370611044, + "os": None, + "ip": 35226500, + "isp": "EE High Speed Internet", + "http": { + "html_$ ash": -163723763, + "robots_hash": None, + "redirects": [], + "securitytxt": None, + "title": "401 Authorization Required", + "sitemap_hash": None, + "robots": None, + "favicon": None, + "host": "2.25.131.132", + "html": "401 Authorization Required\n

401 Authoriza$ ion Required

\nBrowser not authentication-capable or authentication failed.\n\n", + "location": "/", + "components": {}, + "server": "Apache", + "sitemap": None, + "securitytxt_hash": None, + }, + "cpe": ["cpe:/a:apache:http_server"], + "port": 8085, + "hostnames": [], + "location": { + "city": "$ helmsford", + "region_code": "E4", + "area_code": None, + "longitude": 0.48330000000001405, + "country_code3": "GBR", + "country_name": "United Kingdom", + "postal_code": "CM2", + "dma_code": None, + "country_code": "GB", + "latitude": 51.733300000000014, + }, + "timestamp": "2019-04-10T11:03:59.955967", + "$ omains": [], + "org": "EE High Speed Internet", + "data": 'HTTP/1.1 401 Unauthorized\r\nServer: Apache\r\nConnection: Close\r\nContent-type: text/html\r\nWWW-Authenticate: Digest realm="DSLForum CPE Management", algorithm=MD5, qop=auth, stale=FALSE, nonce="3d7a3f71e72e095dba31fd77d4db74$5", opaque="5ccc069c403ebaf9f0171e9517f40e41"\r\n\r\n', + "asn": "AS12576", + "transport": "tcp", + "ip_str": "2.25.131.132", + }, ] } @@ -32,10 +141,12 @@ class BingIter(object): def __init__(self, dork, offset=0, limit=0, key=None): if key is None: - key = Facade().sett.get('plugins', 'bing_apikey') + key = Facade().sett.get("plugins", "bing_apikey") if not key: - raise FuzzExceptMissingAPIKey("An api Bing key is needed. Please chek wfuzz.ini.") + raise FuzzExceptMissingAPIKey( + "An api Bing key is needed. Please chek wfuzz.ini." + ) self._key = key self._dork = dork @@ -48,7 +159,9 @@ def __init__(self, dork, offset=0, limit=0, key=None): # first bing request to get estimated total count (it does not take into consideration offset). if limit > 0 and limit < 50: - total_results, self._retrieved, self._results = self._do_search(offset, limit) + total_results, self._retrieved, self._results = self._do_search( + offset, limit + ) else: total_results, self._retrieved, self._results = self._do_search(offset) @@ -67,14 +180,18 @@ def __init__(self, dork, offset=0, limit=0, key=None): def _do_search(self, offset=0, limit=50): # some code taken from http://www.securitybydefault.com/2014/07/search2auditpy-deja-que-bing-haga-el.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+SecurityByDefault+%28Security+By+Default%29 # api doc http://go.microsoft.com/fwlink/?LinkID=248077 - user_agent = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; FDM; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 1.1.4322)' - creds = (':%s' % self._key).encode('base64')[:-1] - auth = 'Basic %s' % creds + user_agent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; FDM; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 1.1.4322)" + creds = (":%s" % self._key).encode("base64")[:-1] + auth = "Basic %s" % creds result = None try: - urlstr = 'https://api.datamarket.azure.com/Data.ashx/Bing/Search/Composite?Sources=%27web%27&Query=%27' + self._dork + '%27&$format=json' + urlstr = ( + "https://api.datamarket.azure.com/Data.ashx/Bing/Search/Composite?Sources=%27web%27&Query=%27" + + self._dork + + "%27&$format=json" + ) if limit != 50: urlstr += "&$top=%d" % limit if offset != 0: @@ -82,21 +199,23 @@ def _do_search(self, offset=0, limit=50): request = Request(urlstr) - request.add_header('Authorization', auth) - request.add_header('User-Agent', user_agent) + request.add_header("Authorization", auth) + request.add_header("User-Agent", user_agent) requestor = build_opener() result = requestor.open(request) except Exception as e: - raise FuzzExceptResourceParseError("Error when retrieving Bing API results: %s." % str(e)) + raise FuzzExceptResourceParseError( + "Error when retrieving Bing API results: %s." % str(e) + ) results = json.loads(result.read()) # WebTotal is not reliable, it is usually much bigger than the actual results, therefore # if your offset increases over the real number of results, you get a dict # without values and counters to ''. It gets updated when you are close to that limit though. - if results['d']['results'][0]["WebTotal"]: - res_total = int(results['d']['results'][0]["WebTotal"]) - res_list = results['d']['results'][0]['Web'] + if results["d"]["results"][0]["WebTotal"]: + res_total = int(results["d"]["results"][0]["WebTotal"]) + res_list = results["d"]["results"][0]["Web"] return res_total, len(res_list), res_list else: @@ -111,7 +230,9 @@ def __next__(self): # Result buffer already consumed if self._index >= self._retrieved: - realcount, self._retrieved, self._results = self._do_search(self.current + self._offset) + realcount, self._retrieved, self._results = self._do_search( + self.current + self._offset + ) self._index = 0 @@ -119,28 +240,35 @@ def __next__(self): if self.max_count > realcount: self.max_count = realcount - elem = self._results[self._index]['Url'].strip() + elem = self._results[self._index]["Url"].strip() self.current += 1 self._index += 1 # pycurl does not like unicode if isinstance(elem, str): - return elem.encode('utf-8') + return elem.encode("utf-8") else: return elem -class ShodanIter(): +class ShodanIter: SHODAN_RES_PER_PAGE = 100 MAX_ENQUEUED_RES = SHODAN_RES_PER_PAGE + 1 NUM_OF_WORKERS = 1 SLOW_START = True def __init__(self, dork, page, limit): - key = Facade().sett.get('plugins', 'shodan_apikey') + if IMPORTED_SHODAN is False: + raise FuzzExceptPluginLoadError( + "shodan module not imported. Please, install shodan using pip" + ) + + key = Facade().sett.get("plugins", "shodan_apikey") if not key: - raise FuzzExceptMissingAPIKey("A Shodan api key is needed. Please check ~/.wfuzz/wfuzz.ini") + raise FuzzExceptMissingAPIKey( + "A Shodan api key is needed. Please check ~/.wfuzz/wfuzz.ini" + ) self.api = shodan.Shodan(key) self._dork = dork @@ -173,7 +301,7 @@ def _do_search(self): try: results = self.api.search(self._dork, page=page) - for item in results['matches']: + for item in results["matches"]: if not self._cancel_job: self.results_queue.put(item) @@ -196,7 +324,7 @@ def __iter__(self): def _start(self): for th_n in range(self.NUM_OF_WORKERS): worker = Thread(target=self._do_search) - worker.setName('_do_search_{}'.format(str(th_n))) + worker.setName("_do_search_{}".format(str(th_n))) self._threads.append(worker) worker.start() diff --git a/src/wfuzz/plugin_api/urlutils.py b/src/wfuzz/plugin_api/urlutils.py index 5e4fe71c..441f22e2 100644 --- a/src/wfuzz/plugin_api/urlutils.py +++ b/src/wfuzz/plugin_api/urlutils.py @@ -3,12 +3,15 @@ # Python 2 and 3 import sys + if sys.version_info >= (3, 0): from urllib.parse import ParseResult from urllib.parse import urlparse + from urllib.parse import parse_qs else: from urlparse import ParseResult from urlparse import urlparse + from urlparse import parse_qs from wfuzz.facade import Facade from wfuzz.exception import FuzzExceptBadAPI @@ -17,51 +20,76 @@ class FuzzRequestParse(ParseResult): @property def ffname(self): - ''' + """ Returns script plus extension from an URL. ie. http://www.localhost.com/kk/index.html?id=3 will return index.html - ''' - u = self.path.split('/')[-1:][0] + """ + u = self.path.split("/")[-1:][0] return u @property def fext(self): - ''' + """ Returns script extension from an URL. ie. http://www.localhost.com/kk/index.html?id=3 will return .html - ''' + """ return os.path.splitext(self.ffname)[1] @property def fname(self): - ''' + """ Returns script name from an URL. ie. http://www.localhost.com/kk/index.html?id=3 will return index - ''' + """ return os.path.splitext(self.ffname)[0] @property def isbllist(self): fext = self.fext - return fext != "." and fext in Facade().sett.get("kbase", "discovery.blacklist").split("-") + return fext != "." and fext in Facade().sett.get( + "kbase", "discovery.blacklist" + ).split("-") @property def hasquery(self): return self.query != "" + def cache_key(self, base_urlp=None): + scheme = self.scheme + netloc = self.netloc + + if base_urlp: + scheme = self.scheme if self.scheme else base_urlp.scheme + netloc = self.netloc if self.netloc else base_urlp.netloc + + key = "{}-{}-{}-{}".format(scheme, netloc, self.path, self.params) + dicc = {"g{}".format(key): True for key in parse_qs(self.query).keys()} + + # take URL parameters into consideration + url_params = list(dicc.keys()) + url_params.sort() + key += "-" + "-".join(url_params) + + return key + def parse_url(url): + # >>> urlparse.urlparse("http://some.page.pl/nothing.py;someparam=some;otherparam=other?query1=val1&query2=val2#frag") + # ParseResult(scheme='http', netloc='some.page.pl', path='/nothing.py', params='someparam=some;otherparam=other', query='query1=val1&query2=val2', fragment='frag') + scheme, netloc, path, params, query, fragment = urlparse(url) return FuzzRequestParse(scheme, netloc, path, params, query, fragment) def check_content_type(fuzzresult, which): ctype = None - if 'Content-Type' in fuzzresult.history.headers.response: - ctype = fuzzresult.history.headers.response['Content-Type'] + if "Content-Type" in fuzzresult.history.headers.response: + ctype = fuzzresult.history.headers.response["Content-Type"] - if which == 'text': - return not ctype or (ctype and any([ctype.find(x) >= 0 for x in ['text/plain']])) + if which == "text": + return not ctype or ( + ctype and any([ctype.find(x) >= 0 for x in ["text/plain"]]) + ) else: raise FuzzExceptBadAPI("Unknown content type") diff --git a/src/wfuzz/plugins/encoders/encoders.py b/src/wfuzz/plugins/encoders/encoders.py index 76f7219a..23ea797d 100644 --- a/src/wfuzz/plugins/encoders/encoders.py +++ b/src/wfuzz/plugins/encoders/encoders.py @@ -42,7 +42,11 @@ def decode(self, string): @moduleman_plugin("encode") class urlencode: name = "urlencode" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replace special characters in string using the %xx escape. Letters, digits, and the characters '_.-' are never quoted." category = ["url_safe", "url"] @@ -58,7 +62,11 @@ def decode(self, string): @moduleman_plugin("encode") class double_urlencode: name = "double urlencode" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Applies a double encode to special characters in string using the %25xx escape. Letters, digits, and the characters '_.-' are never quoted." category = ["url_safe", "url"] @@ -74,23 +82,31 @@ def decode(self, string): @moduleman_plugin("encode") class base64: name = "base64" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Encodes the given string using base64" category = ["hashes"] priority = 99 def encode(self, string): - return standard_b64encode(string.encode('utf-8')).decode('utf-8') + return standard_b64encode(string.encode("utf-8")).decode("utf-8") def decode(self, string): - return b64decode(string.encode('utf-8')).decode('utf-8') + return b64decode(string.encode("utf-8")).decode("utf-8") @moduleman_plugin("encode") class uri_triple_hex: name = "uri_triple_hex" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Encodes ALL charachers using the %25%xx%xx escape." category = ["url"] @@ -111,7 +127,11 @@ def encode(self, string): @moduleman_plugin("encode") class uri_double_hex: name = "uri_double_hex" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Encodes ALL charachers using the %25xx escape." category = ["url"] @@ -132,7 +152,11 @@ def encode(self, string): @moduleman_plugin("encode") class uri_hex: name = "uri_hex" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Encodes ALL charachers using the %xx escape." category = ["url"] @@ -153,7 +177,11 @@ def encode(self, string): @moduleman_plugin("encode") class random_upper: name = "random_upper" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces random characters in string with its capitals letters" category = ["default"] @@ -174,7 +202,11 @@ def encode(self, string): @moduleman_plugin("encode") class second_nibble_hex: name = "second_nibble_hex" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the %?%dd escape" category = ["url"] @@ -195,7 +227,11 @@ def encode(self, string): @moduleman_plugin("encode") class first_nibble_hex: name = "first_nibble_hex" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the %%dd? escape" category = ["url"] @@ -216,7 +252,11 @@ def encode(self, string): @moduleman_plugin("encode") class doble_nibble_hex: name = "doble_nibble_hex" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the %%dd%dd escape" category = ["url"] @@ -244,14 +284,18 @@ def encode(self, string): class sha1: name = "sha1" summary = "Applies a sha1 hash to the given string" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" category = ["hashes"] priority = 99 def encode(self, string): s = hashlib.sha1() - s.update(string.encode('utf-8')) + s.update(string.encode("utf-8")) res = s.hexdigest() return res @@ -259,15 +303,19 @@ def encode(self, string): @moduleman_plugin("encode") class md5: name = "md5" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Applies a md5 hash to the given string" category = ["hashes"] priority = 99 def encode(self, string): - m = hashlib.new('md5') - m.update(string.encode('utf-8')) + m = hashlib.new("md5") + m.update(string.encode("utf-8")) res = m.hexdigest() return res @@ -275,25 +323,33 @@ def encode(self, string): @moduleman_plugin("encode") class hexlify: name = "hexlify" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Every byte of data is converted into the corresponding 2-digit hex representation." category = ["default"] priority = 99 def encode(self, string): - return binascii.hexlify(string.encode('utf-8')).decode('utf-8') + return binascii.hexlify(string.encode("utf-8")).decode("utf-8") def decode(self, string): - return binascii.unhexlify(string.encode('utf-8')).decode('utf-8') + return binascii.unhexlify(string.encode("utf-8")).decode("utf-8") @moduleman_plugin("encode") class html_escape: name = "html_escape" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" - summary = "Convert the characters &<>\" in string to HTML-safe sequences." + summary = 'Convert the characters &<>" in string to HTML-safe sequences.' category = ["html"] priority = 99 @@ -304,7 +360,11 @@ def encode(self, string): @moduleman_plugin("encode") class html_decimal: name = "html_decimal" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the &#dd; escape" category = ["html"] @@ -320,7 +380,11 @@ def encode(self, string): @moduleman_plugin("encode") class html_hexadecimal: name = "html_hexadecimal" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the &#xx; escape" category = ["html"] @@ -337,7 +401,11 @@ def encode(self, string): @moduleman_plugin("encode") class utf8_binary: name = "utf8_binary" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the \\uxx escape" category = ["url"] @@ -354,7 +422,11 @@ def encode(self, string): @moduleman_plugin("encode") class utf8: name = "utf8" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the \\u00xx escape" category = ["url"] @@ -374,7 +446,11 @@ def encode(self, string): @moduleman_plugin("encode") class uri_unicode: name = "uri_unicode" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Replaces ALL characters in string using the %u00xx escape" category = ["url"] @@ -394,7 +470,11 @@ def encode(self, string): @moduleman_plugin("encode") class mysql_char: name = "mysql_char" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Converts ALL characters to MySQL's char(xx)" category = ["db"] @@ -420,7 +500,11 @@ def decode(self, string): @moduleman_plugin("encode") class mssql_char: name = "mssql_char" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Converts ALL characters to MsSQL's char(xx)" category = ["db"] @@ -446,7 +530,11 @@ def decode(self, string): @moduleman_plugin("encode") class oracle_char: name = "oracle_char" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Converts ALL characters to Oracle's chr(xx)" category = ["db"] diff --git a/src/wfuzz/plugins/iterators/iterations.py b/src/wfuzz/plugins/iterators/iterations.py index 8fcc7bd0..ab67cf24 100644 --- a/src/wfuzz/plugins/iterators/iterations.py +++ b/src/wfuzz/plugins/iterators/iterations.py @@ -1,16 +1,14 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin +from wfuzz.dictionaries import BaseIterator import itertools from functools import reduce -# python 2 and 3: iterator -from builtins import object - from builtins import zip as builtinzip @moduleman_plugin -class zip(object): +class zip(BaseIterator): name = "zip" author = ("Xavi Mendez (@xmendez)",) version = "0.1" @@ -19,12 +17,20 @@ class zip(object): priority = 99 def __init__(self, *i): + self._payload_list = i + self.__width = len(i) self.__count = min([x.count() for x in i]) self.it = builtinzip(*i) def count(self): return self.__count + def width(self): + return self.__width + + def payloads(self): + return self._payload_list + def __next__(self): return next(self.it) @@ -33,7 +39,7 @@ def __iter__(self): @moduleman_plugin -class product(object): +class product(BaseIterator): name = "product" author = ("Xavi Mendez (@xmendez)",) version = "0.1" @@ -42,12 +48,20 @@ class product(object): priority = 99 def __init__(self, *i): + self._payload_list = i + self.__width = len(i) self.__count = reduce(lambda x, y: x * y.count(), i[1:], i[0].count()) self.it = itertools.product(*i) def count(self): return self.__count + def width(self): + return self.__width + + def payloads(self): + return self._payload_list + def __next__(self): return next(self.it) @@ -56,7 +70,7 @@ def __iter__(self): @moduleman_plugin -class chain(object): +class chain(BaseIterator): name = "chain" author = ("Xavi Mendez (@xmendez)",) version = "0.1" @@ -64,13 +78,20 @@ class chain(object): category = ["default"] priority = 99 - def count(self): - return self.__count - def __init__(self, *i): + self._payload_list = i self.__count = sum([x.count() for x in i]) self.it = itertools.chain(*i) + def count(self): + return self.__count + + def width(self): + return 1 + + def payloads(self): + return self._payload_list + def __next__(self): return (next(self.it),) diff --git a/src/wfuzz/plugins/payloads/autorize.py b/src/wfuzz/plugins/payloads/autorize.py index c7c85251..93af77bb 100644 --- a/src/wfuzz/plugins/payloads/autorize.py +++ b/src/wfuzz/plugins/payloads/autorize.py @@ -2,18 +2,18 @@ import base64 from wfuzz.exception import FuzzExceptBadFile -from wfuzz.fuzzobjects import FuzzResult -from wfuzz.fuzzobjects import FuzzRequest +from wfuzz.fuzzobjects import FuzzResult, FuzzWordType +from wfuzz.fuzzrequest import FuzzRequest from wfuzz.plugin_api.base import BasePayload from wfuzz.externals.moduleman.plugin import moduleman_plugin -from wfuzz.utils import rgetattr +from wfuzz.helpers.obj_dyn import rgetattr @moduleman_plugin class autorize(BasePayload): name = "autorize" author = ("Xavi Mendez (@xmendez)",) - version = "0.1" + version = "0.2" description = ("Reads burp extension autorize states",) summary = "Returns fuzz results' from autorize." category = ["default"] @@ -21,7 +21,12 @@ class autorize(BasePayload): parameters = ( ("fn", "", True, "Filename of a valid autorize state file."), - ("attr", None, False, "Attribute of fuzzresult to return. If not specified the whole object is returned."), + ( + "attr", + None, + False, + "Attribute of fuzzresult to return. If not specified the whole object is returned.", + ), ) default_parameter = "fn" @@ -33,22 +38,40 @@ def __init__(self, params): self.attr = self.params["attr"] self._it = self._gen_wfuzz(self.params["fn"]) - def __iter__(self): - return self - def count(self): return self.__max - def __next__(self): + def get_next(self): next_item = next(self._it) return next_item if not self.attr else rgetattr(next_item, self.attr) + def get_type(self): + return FuzzWordType.WORD + def _gen_wfuzz(self, output_fn): try: - with open(self.find_file(output_fn), 'r') as f: - for url1, port1, schema1, req1, resp1, url2, port2, schema2, req2, resp2, url3, port3, schema3, req3, resp3, res1, res2 in [re.split(r'\t+', x) for x in f.readlines()]: + with open(self.find_file(output_fn), "r") as f: + for ( + url1, + port1, + schema1, + req1, + resp1, + url2, + port2, + schema2, + req2, + resp2, + url3, + port3, + schema3, + req3, + resp3, + res1, + res2, + ) in [re.split(r"\t+", x) for x in f.readlines()]: raw_req1 = base64.decodestring(req2) # raw_res1 = base64.decodestring(res2) @@ -56,8 +79,6 @@ def _gen_wfuzz(self, output_fn): item.history = FuzzRequest() item.history.update_from_raw_http(raw_req1, schema1) - item.type = FuzzResult.result - yield item except IOError as e: raise FuzzExceptBadFile("Error opening wfuzz payload file. %s" % str(e)) diff --git a/src/wfuzz/plugins/payloads/bing.py b/src/wfuzz/plugins/payloads/bing.py index 6ba8c11b..cc540f73 100644 --- a/src/wfuzz/plugins/payloads/bing.py +++ b/src/wfuzz/plugins/payloads/bing.py @@ -1,17 +1,18 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.payloadtools import BingIter from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin class bing(BasePayload): name = "bing" author = ("Xavi Mendez (@xmendez)",) - version = "0.1" + version = "0.2" description = ( - "intitle:\"JBoss JMX Management Console\"", + 'intitle:"JBoss JMX Management Console"', "Some examples of bing hacking:", - "http://www.elladodelmal.com/2010/02/un-poco-de-bing-hacking-i-de-iii.html" + "http://www.elladodelmal.com/2010/02/un-poco-de-bing-hacking-i-de-iii.html", ) summary = "Returns URL results of a given bing API search (needs api key)." @@ -34,11 +35,11 @@ def __init__(self, params): self._it = BingIter(params["dork"], offset, limit) - def __iter__(self): - return self - def count(self): return self._it.max_count - def __next__(self): + def get_next(self): return next(self._it) + + def get_type(self): + return FuzzWordType.WORD diff --git a/src/wfuzz/plugins/payloads/buffer_overflow.py b/src/wfuzz/plugins/payloads/buffer_overflow.py index 4f7cdd42..6bac99c1 100644 --- a/src/wfuzz/plugins/payloads/buffer_overflow.py +++ b/src/wfuzz/plugins/payloads/buffer_overflow.py @@ -1,39 +1,38 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin class buffer_overflow(BasePayload): name = "buffer_overflow" author = ("Xavi Mendez (@xmendez)",) - version = "0.1" + version = "0.2" description = () summary = "Returns a string using the following pattern A * given number." category = ["default"] priority = 99 - parameters = ( - ("size", "", True, "Size of the overflow string."), - ) + parameters = (("size", "", True, "Size of the overflow string."),) default_parameter = "size" def __init__(self, params): BasePayload.__init__(self, params) - self.bov_list = ['A' * int(self.params["size"])] + self.bov_list = ["A" * int(self.params["size"])] self.current = 0 - def __iter__(self): - return self - def count(self): return 1 - def __next__(self): + def get_next(self): if self.current == 0: elem = self.bov_list[self.current] self.current += 1 return elem else: raise StopIteration + + def get_type(self): + return FuzzWordType.WORD diff --git a/src/wfuzz/plugins/payloads/burpitem.py b/src/wfuzz/plugins/payloads/burpitem.py index 6c41c978..e97de7ef 100644 --- a/src/wfuzz/plugins/payloads/burpitem.py +++ b/src/wfuzz/plugins/payloads/burpitem.py @@ -1,8 +1,9 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptBadFile -from wfuzz.fuzzobjects import FuzzResult, FuzzRequest +from wfuzz.fuzzobjects import FuzzResult, FuzzWordType +from wfuzz.fuzzrequest import FuzzRequest from wfuzz.plugin_api.base import BasePayload -from wfuzz.utils import rgetattr +from wfuzz.helpers.obj_dyn import rgetattr import xml.etree.cElementTree as ET from base64 import b64decode @@ -21,7 +22,12 @@ class burpitem(BasePayload): parameters = ( ("fn", "", True, "Filename of a valid Burp item file."), - ("attr", None, False, "Attribute of fuzzresult to return. If not specified the whole object is returned."), + ( + "attr", + None, + False, + "Attribute of fuzzresult to return. If not specified the whole object is returned.", + ), ) default_parameter = "fn" @@ -33,32 +39,39 @@ def __init__(self, params): self.attr = self.params["attr"] self._it = self._gen_burpitem(self.params["fn"]) - def __iter__(self): - return self - def count(self): return self.__max - def __next__(self): + def get_next(self): next_item = next(self._it) return next_item if not self.attr else rgetattr(next_item, self.attr) + def get_type(self): + return FuzzWordType.FUZZRES if not self.attr else FuzzWordType.WORD + def _gen_burpitem(self, output_fn): try: tree = ET.parse(self.find_file(output_fn)) - for item in tree.getroot().iter('item'): + for item in tree.getroot().iter("item"): fr = FuzzRequest() - fr.update_from_raw_http(raw=b64decode(item.find('request').text or "").decode('utf-8'), - scheme=item.find('protocol').text, - raw_response=b64decode(item.find('response').text or "")) - fr.wf_ip = {'ip': item.find('host').attrib.get('ip', None) or item.find('host').text, - 'port': item.find('port').text} + fr.update_from_raw_http( + raw=b64decode(item.find("request").text or "").decode("utf-8"), + scheme=item.find("protocol").text, + raw_response=b64decode(item.find("response").text or ""), + ) + fr.wf_ip = { + "ip": item.find("host").attrib.get("ip", None) + or item.find("host").text, + "port": item.find("port").text, + } frr = FuzzResult(history=fr) yield frr.update() return except IOError as e: - raise FuzzExceptBadFile("Error opening Burp items payload file. %s" % str(e)) + raise FuzzExceptBadFile( + "Error opening Burp items payload file. %s" % str(e) + ) except EOFError: return diff --git a/src/wfuzz/plugins/payloads/burplog.py b/src/wfuzz/plugins/payloads/burplog.py index 36b6a1a4..4ece4f20 100644 --- a/src/wfuzz/plugins/payloads/burplog.py +++ b/src/wfuzz/plugins/payloads/burplog.py @@ -1,19 +1,24 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptBadFile -from wfuzz.fuzzobjects import FuzzResult, FuzzRequest +from wfuzz.fuzzobjects import FuzzResult +from wfuzz.fuzzrequest import FuzzRequest from wfuzz.plugin_api.base import BasePayload -from wfuzz.utils import rgetattr +from wfuzz.helpers.obj_dyn import rgetattr +from wfuzz.fuzzobjects import FuzzWordType import re import sys + if sys.version_info < (3, 0): from io import open CRLF = "\n" -DELIMITER = "%s%s" % ('=' * 54, CRLF) +DELIMITER = "%s%s" % ("=" * 54, CRLF) CRLF_DELIMITER = CRLF + DELIMITER -HEADER = re.compile(r'(\d{1,2}:\d{2}:\d{2} (AM|PM|))[ \t]+(\S+)([ \t]+\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|unknown host)\])?') +HEADER = re.compile( + r"(\d{1,2}:\d{2}:\d{2} (AM|PM|))[ \t]+(\S+)([ \t]+\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|unknown host)\])?" +) @moduleman_plugin @@ -28,7 +33,12 @@ class burplog(BasePayload): parameters = ( ("fn", "", True, "Filename of a valid Burp log file."), - ("attr", None, False, "Attribute of fuzzresult to return. If not specified the whole object is returned."), + ( + "attr", + None, + False, + "Attribute of fuzzresult to return. If not specified the whole object is returned.", + ), ) default_parameter = "fn" @@ -40,13 +50,13 @@ def __init__(self, params): self.attr = self.params["attr"] self._it = self.parse_burp_log(self.params["fn"]) - def __iter__(self): - return self - def count(self): return self.__max - def __next__(self): + def get_type(self): + return FuzzWordType.FUZZRES if not self.attr else FuzzWordType.WORD + + def get_next(self): next_item = next(self._it) return next_item if not self.attr else rgetattr(next_item, self.attr) @@ -55,9 +65,14 @@ def parse_burp_log(self, burp_log): burp_file = None try: - burp_file = open(self.find_file(burp_log), 'r', encoding="utf-8", errors="surrogateescape") + burp_file = open( + self.find_file(burp_log), + "r", + encoding="utf-8", + errors="surrogateescape", + ) - history = 'START' + history = "START" rl = burp_file.readline() while rl != "": @@ -97,7 +112,9 @@ def parse_burp_log(self, burp_log): elif history == "DELIM4": if rl == CRLF: fr = FuzzRequest() - fr.update_from_raw_http(raw_request, host[:host.find("://")], raw_response) + fr.update_from_raw_http( + raw_request, host[: host.find("://")], raw_response + ) frr = FuzzResult(history=fr) yield frr.update() diff --git a/src/wfuzz/plugins/payloads/burpstate.py b/src/wfuzz/plugins/payloads/burpstate.py index 7bf2872f..b2ef7d54 100644 --- a/src/wfuzz/plugins/payloads/burpstate.py +++ b/src/wfuzz/plugins/payloads/burpstate.py @@ -1,8 +1,10 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptBadFile, FuzzExceptBadOptions -from wfuzz.fuzzobjects import FuzzResult, FuzzRequest +from wfuzz.fuzzobjects import FuzzResult +from wfuzz.fuzzrequest import FuzzRequest from wfuzz.plugin_api.base import BasePayload -from wfuzz.utils import rgetattr +from wfuzz.helpers.obj_dyn import rgetattr +from wfuzz.fuzzobjects import FuzzWordType import datetime @@ -11,14 +13,14 @@ import struct import zipfile -TAG = re.compile(r'', re.M) # Match a XML tag -nvprint = string.printable.replace('\x0b', '').replace('\x0c', '') # Printables +TAG = re.compile(r"", re.M) # Match a XML tag +nvprint = string.printable.replace("\x0b", "").replace("\x0c", "") # Printables @moduleman_plugin class burpstate(BasePayload): name = "burpstate" - author = ("Xavi Mendez (@xmendez)", ) + author = ("Xavi Mendez (@xmendez)",) version = "0.1" description = ( "*ALERT*: https://portswigger.net/blog/goodbye-state-files-we-wont-miss-you", @@ -36,9 +38,24 @@ class burpstate(BasePayload): parameters = ( ("fn", "", True, "Filename of a valid Burp state file."), - ("attr", None, False, "Fuzzresult attribute to return. If not specified the whole object is returned."), - ("source", "proxy, target", False, "A list of separated Burp sources to get the HTTP requests and responses from. It could be proxy or target tool."), - ("checkversion", False, False, "If the Burp log file version is unknown an exception will be raised and execution will fail. Checked with burp state file version 65, 67."), + ( + "attr", + None, + False, + "Fuzzresult attribute to return. If not specified the whole object is returned.", + ), + ( + "source", + "proxy, target", + False, + "A list of separated Burp sources to get the HTTP requests and responses from. It could be proxy or target tool.", + ), + ( + "checkversion", + False, + False, + "If the Burp log file version is unknown an exception will be raised and execution will fail. Checked with burp state file version 65, 67.", + ), ) default_parameter = "fn" @@ -70,74 +87,87 @@ def __iter__(self): def count(self): return self.__max - def __next__(self): + def get_type(self): + return FuzzWordType.FUZZRES if not self.attr else FuzzWordType.WORD + + def get_next(self): next_item = next(self._it) return next_item if not self.attr else rgetattr(next_item, self.attr) def milliseconds_to_date(self, milliseconds): - '''Convert milliseconds since Epoch (from Java) to Python date structure: + """Convert milliseconds since Epoch (from Java) to Python date structure: See: http://java.sun.com/j2se/1.4.2/docs/api/java/util/Date.html There is no direct way to convert milliseconds since Epoch to Python object So we convert the milliseconds to seconds first as a POSIX timestamp which can be used to get a valid date, and then use the parsed values from that - object along with converting mili -> micro seconds in a new date object.''' + object along with converting mili -> micro seconds in a new date object.""" try: d = datetime.datetime.fromtimestamp(milliseconds / 1000) - date = datetime.datetime(d.year, d.month, d.day, d.hour, d.minute, d.second, (milliseconds % 1000) * 1000) + date = datetime.datetime( + d.year, + d.month, + d.day, + d.hour, + d.minute, + d.second, + (milliseconds % 1000) * 1000, + ) except ValueError: # Bad date, just return the milliseconds date = str(milliseconds) return None return date def burp_binary_field(self, field, i): - '''Strip Burp Suite's binary format characters types from our data. - The first character after the leading tag describes the type of the data.''' + """Strip Burp Suite's binary format characters types from our data. + The first character after the leading tag describes the type of the data.""" if len(field) <= i: return None, -1 - elif field[i] == '\x00': # 4 byte integer value - return str(struct.unpack('>I', field[i + 1:i + 5])[0]), 5 - elif field[i] == '\x01': # Two possible unsigned long long types - if field[i + 1] == '\x00': # (64bit) 8 Byte Java Date - ms = struct.unpack('>Q', field[i + 1:i + 9])[0] + elif field[i] == "\x00": # 4 byte integer value + return str(struct.unpack(">I", field[i + 1 : i + 5])[0]), 5 + elif field[i] == "\x01": # Two possible unsigned long long types + if field[i + 1] == "\x00": # (64bit) 8 Byte Java Date + ms = struct.unpack(">Q", field[i + 1 : i + 9])[0] date = self.milliseconds_to_date(ms) - value = date.ctime() if date else 0 # Use the ctime string format for date + value = ( + date.ctime() if date else 0 + ) # Use the ctime string format for date else: # Serial Number only used ocasionally in Burp - value = str(struct.unpack('>Q', field[i + 1:i + 9])[0]) + value = str(struct.unpack(">Q", field[i + 1 : i + 9])[0]) return value, 9 - elif field[i] == '\x02': # Boolean Object True/False - return str(struct.unpack('?', field[i + 1:i + 2])[0]), 2 - elif field[i] == '\x03' or field[i] == '\x04': # 4 byte length + string - length = struct.unpack('>I', field[i + 1:i + 5])[0] + elif field[i] == "\x02": # Boolean Object True/False + return str(struct.unpack("?", field[i + 1 : i + 2])[0]), 2 + elif field[i] == "\x03" or field[i] == "\x04": # 4 byte length + string + length = struct.unpack(">I", field[i + 1 : i + 5])[0] # print "Saw string of length", length, "at", i + 5, i + 5+length - value = field[i + 5:i + 5 + length] - if '<' in value or '>' in value or '&' in value: # Sanatize HTML w/CDATA - value = '', ']]>' - value = ''.join(c for c in value if c in nvprint) # Remove nonprintables + value = field[i + 5 : i + 5 + length] + if "<" in value or ">" in value or "&" in value: # Sanatize HTML w/CDATA + value = "", "]]>" + value = "".join(c for c in value if c in nvprint) # Remove nonprintables return value, 5 + length # ** TODO: Verify length by matching end tag ** print("Unknown binary format", repr(field[i])) return None, -1 def strip_cdata(self, data): - if data.startswith(''): + if data.endswith("]]>"): data = data[:-3] return data def burp_to_xml(self, filename): - '''Unzip Burp's file, remove non-printable characters, CDATA any HTML, - include a valid XML header and trailer, and return a valid XML string.''' + """Unzip Burp's file, remove non-printable characters, CDATA any HTML, + include a valid XML header and trailer, and return a valid XML string.""" z = zipfile.ZipFile(self.find_file(filename)) # Open Burp's zip file - burp = z.read('burp', 'rb') # Read-in the main burp file + burp = z.read("burp", "rb") # Read-in the main burp file m = TAG.match(burp, 0) # Match a tag at the start of the string while m: index = m.end() - etag = m.group().replace('<', '" and value not in ["65", "67"]: + if ( + self.params["checkversion"] + and etag == "" + and value not in ["65", "67"] + ): raise FuzzExceptBadFile("Unknown burp log version %s" % value) if etag == "": @@ -160,7 +194,11 @@ def burp_to_xml(self, filename): if etag in self.response_tags: fr = FuzzRequest() - fr.update_from_raw_http(raw_request, "http" if not https_tag else "https", self.strip_cdata(value)) + fr.update_from_raw_http( + raw_request, + "http" if not https_tag else "https", + self.strip_cdata(value), + ) frr = FuzzResult(history=fr) raw_request = "" diff --git a/src/wfuzz/plugins/payloads/dirwalk.py b/src/wfuzz/plugins/payloads/dirwalk.py index 54cca980..33679ff5 100644 --- a/src/wfuzz/plugins/payloads/dirwalk.py +++ b/src/wfuzz/plugins/payloads/dirwalk.py @@ -1,5 +1,6 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType import os @@ -19,7 +20,7 @@ class dirwalk(BasePayload): "Returns all the file paths found in the specified directory.", "Handy if you want to check a directory structure against a webserver,", "for example, because you have previously downloaded a specific version", - "of what is supposed to be on-line." + "of what is supposed to be on-line.", ) summary = "Returns filename's recursively from a local directory." category = ["default"] @@ -42,11 +43,11 @@ def _my_gen(self, directory): relative_path = os.path.relpath(os.path.join(root, f), directory) yield quote(relative_path) - def __next__(self): + def get_next(self): return next(self.g) + def get_type(self): + return FuzzWordType.WORD + def count(self): return -1 - - def __iter__(self): - return self diff --git a/src/wfuzz/plugins/payloads/file.py b/src/wfuzz/plugins/payloads/file.py index f5f2084f..fafc5692 100644 --- a/src/wfuzz/plugins/payloads/file.py +++ b/src/wfuzz/plugins/payloads/file.py @@ -1,25 +1,33 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptBadFile from wfuzz.plugin_api.base import BasePayload -from wfuzz.utils import FileDetOpener +from wfuzz.helpers.file_func import FileDetOpener +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin class file(BasePayload): name = "file" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") - version = "0.1" - description = ( - "Returns the contents of a dictionary file line by line.", + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", ) + version = "0.2" + description = ("Returns the contents of a dictionary file line by line.",) summary = "Returns each word from a file." category = ["default"] priority = 99 parameters = ( ("fn", "", True, "Filename of a valid dictionary"), - ("count", 'True', False, "Indicates if the number of words in the file should be counted."), - ("encoding", 'Auto', False, "Indicates the file encoding."), + ( + "count", + "True", + False, + "Indicates if the number of words in the file should be counted.", + ), + ("encoding", "Auto", False, "Indicates the file encoding."), ) default_parameter = "fn" @@ -28,14 +36,21 @@ def __init__(self, params): BasePayload.__init__(self, params) try: - encoding = self.params['encoding'] if self.params['encoding'].lower() != 'auto' else None + encoding = ( + self.params["encoding"] + if self.params["encoding"].lower() != "auto" + else None + ) self.f = FileDetOpener(self.find_file(self.params["fn"]), encoding) except IOError as e: raise FuzzExceptBadFile("Error opening file. %s" % str(e)) self.__count = None - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): line = next(self.f) if not line: self.f.close() @@ -43,7 +58,7 @@ def __next__(self): return line.strip() def count(self): - if self.params["count"].lower() == 'false': + if self.params["count"].lower() == "false": return -1 if self.__count is None: @@ -51,6 +66,3 @@ def count(self): self.f.reset() return self.__count - - def __iter__(self): - return self diff --git a/src/wfuzz/plugins/payloads/guitab.py b/src/wfuzz/plugins/payloads/guitab.py index 8b401314..24c61d84 100644 --- a/src/wfuzz/plugins/payloads/guitab.py +++ b/src/wfuzz/plugins/payloads/guitab.py @@ -1,5 +1,6 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType from wfuzz.facade import Facade @@ -19,7 +20,12 @@ class guitab(BasePayload): parameters = ( ("tab", "", True, "Name of a valid GUI tab."), - ("attr", None, False, "Attribute of fuzzresult to return. If not specified the whole object is returned."), + ( + "attr", + None, + False, + "Attribute of fuzzresult to return. If not specified the whole object is returned.", + ), ) default_parameter = "tab" @@ -30,13 +36,13 @@ def __init__(self, params): self.attr = self.params["attr"] self._it = iter(Facade().data[self.params["tab"]]) - def __iter__(self): - return self - def count(self): return len(Facade().data[self.params["tab"]]) - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): next_item = next(self._it) return next_item if not self.attr else next_item.get_field(self.attr) diff --git a/src/wfuzz/plugins/payloads/hexrand.py b/src/wfuzz/plugins/payloads/hexrand.py index d07616d1..31323c93 100644 --- a/src/wfuzz/plugins/payloads/hexrand.py +++ b/src/wfuzz/plugins/payloads/hexrand.py @@ -1,6 +1,7 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload from wfuzz.exception import FuzzExceptPluginBadParams +from wfuzz.fuzzobjects import FuzzWordType import random @@ -8,7 +9,11 @@ @moduleman_plugin class hexrand(BasePayload): name = "hexrand" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" description = () summary = "Returns random hex numbers from the given range." @@ -16,7 +21,12 @@ class hexrand(BasePayload): priority = 99 parameters = ( - ("range", "", True, "Range of hex numbers to randomly generate in the form of 00-ff."), + ( + "range", + "", + True, + "Range of hex numbers to randomly generate in the form of 00-ff.", + ), ) default_parameter = "range" @@ -30,7 +40,7 @@ def __init__(self, params): self.maximum = int(ran[1], 16) self.__count = -1 except ValueError: - raise FuzzExceptPluginBadParams("Bad range format (eg. \"0-ffa\")") + raise FuzzExceptPluginBadParams('Bad range format (eg. "0-ffa")') def __iter__(self): return self @@ -38,7 +48,10 @@ def __iter__(self): def count(self): return self.__count - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): self.current = random.SystemRandom().randint(self.minimum, self.maximum) lgth = len(hex(self.maximum).replace("0x", "")) diff --git a/src/wfuzz/plugins/payloads/hexrange.py b/src/wfuzz/plugins/payloads/hexrange.py index 02f8b421..b6102f35 100644 --- a/src/wfuzz/plugins/payloads/hexrange.py +++ b/src/wfuzz/plugins/payloads/hexrange.py @@ -1,12 +1,17 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload from wfuzz.exception import FuzzExceptBadOptions +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin class hexrange(BasePayload): name = "hexrange" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" description = () summary = "Returns each hex number of the given hex range." @@ -28,17 +33,19 @@ def __init__(self, params): self.maximum = int(ran[1], 16) self.__count = self.maximum - self.minimum + 1 self.current = self.minimum - self.lgth = max(len(ran[0]), len(ran[1]), len(hex(self.maximum).replace("0x", ""))) + self.lgth = max( + len(ran[0]), len(ran[1]), len(hex(self.maximum).replace("0x", "")) + ) except ValueError: - raise FuzzExceptBadOptions("Bad range format (eg. \"0-ffa\")") - - def __iter__(self): - return self + raise FuzzExceptBadOptions('Bad range format (eg. "0-ffa")') def count(self): return self.__count - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): if self.current > self.maximum: raise StopIteration diff --git a/src/wfuzz/plugins/payloads/ipnet.py b/src/wfuzz/plugins/payloads/ipnet.py index e21c7cef..157d8512 100644 --- a/src/wfuzz/plugins/payloads/ipnet.py +++ b/src/wfuzz/plugins/payloads/ipnet.py @@ -1,21 +1,20 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptPluginBadParams, FuzzExceptBadInstall from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin class ipnet(BasePayload): name = "ipnet" - author = ("Xavi Mendez (@xmendez)", ) + author = ("Xavi Mendez (@xmendez)",) version = "0.1" description = ("ie. 192.168.1.0/24", "Requires: netaddr module") summary = "Returns list of IP addresses of a network." category = ["default"] priority = 99 - parameters = ( - ("net", "", True, "Network range in the form ip/mask."), - ) + parameters = (("net", "", True, "Network range in the form ip/mask."),) default_parameter = "net" @@ -26,25 +25,33 @@ def __init__(self, params): from netaddr import IPNetwork from netaddr.core import AddrFormatError - net = IPNetwork('%s' % self.params["net"]) + net = IPNetwork("%s" % self.params["net"]) self.f = net.iter_hosts() self.__count = net.size - 2 if self.__count <= 0: - raise FuzzExceptPluginBadParams("There are not hosts in the specified network") + raise FuzzExceptPluginBadParams( + "There are not hosts in the specified network" + ) except ValueError: - raise FuzzExceptPluginBadParams("The specified network has an incorrect format.") + raise FuzzExceptPluginBadParams( + "The specified network has an incorrect format." + ) except ImportError: - raise FuzzExceptBadInstall("ipnet plugin requires netaddr module. Please install it using pip.") + raise FuzzExceptBadInstall( + "ipnet plugin requires netaddr module. Please install it using pip." + ) except AddrFormatError: - raise FuzzExceptPluginBadParams("The specified network has an incorrect format.") + raise FuzzExceptPluginBadParams( + "The specified network has an incorrect format." + ) - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): return str(next(self.f)) def count(self): return self.__count - - def __iter__(self): - return self diff --git a/src/wfuzz/plugins/payloads/iprange.py b/src/wfuzz/plugins/payloads/iprange.py index f4a85bb6..48e2d1e6 100644 --- a/src/wfuzz/plugins/payloads/iprange.py +++ b/src/wfuzz/plugins/payloads/iprange.py @@ -1,6 +1,7 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptPluginBadParams, FuzzExceptBadInstall from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin @@ -8,7 +9,10 @@ class iprange(BasePayload): name = "iprange" author = ("Xavi Mendez (@xmendez)",) version = "0.1" - description = ("ie. 192.168.1.0-192.168.1.12", "Requires: netaddr module",) + description = ( + "ie. 192.168.1.0-192.168.1.12", + "Requires: netaddr module", + ) summary = "Returns list of IP addresses of a given IP range." category = ["default"] priority = 99 @@ -31,17 +35,23 @@ def __init__(self, params): self.f = iter(net) self.__count = net.size except ImportError: - raise FuzzExceptBadInstall("ipnet plugin requires netaddr module. Please install it using pip.") + raise FuzzExceptBadInstall( + "ipnet plugin requires netaddr module. Please install it using pip." + ) except AddrFormatError: - raise FuzzExceptPluginBadParams("The specified network range has an incorrect format.") + raise FuzzExceptPluginBadParams( + "The specified network range has an incorrect format." + ) except IndexError: - raise FuzzExceptPluginBadParams("The specified network range has an incorrect format.") + raise FuzzExceptPluginBadParams( + "The specified network range has an incorrect format." + ) + + def get_type(self): + return FuzzWordType.WORD - def __next__(self): + def get_next(self): return str(next(self.f)) def count(self): return self.__count - - def __iter__(self): - return self diff --git a/src/wfuzz/plugins/payloads/list.py b/src/wfuzz/plugins/payloads/list.py index 6db4ad8b..223ec666 100644 --- a/src/wfuzz/plugins/payloads/list.py +++ b/src/wfuzz/plugins/payloads/list.py @@ -1,5 +1,6 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin @@ -36,13 +37,13 @@ def __init__(self, params): self.__count = len(self.value_list) self.current = 0 - def __iter__(self): - return self - def count(self): return self.__count - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): if self.current >= self.__count: raise StopIteration else: diff --git a/src/wfuzz/plugins/payloads/names.py b/src/wfuzz/plugins/payloads/names.py index e887103e..32e73dc4 100644 --- a/src/wfuzz/plugins/payloads/names.py +++ b/src/wfuzz/plugins/payloads/names.py @@ -1,20 +1,22 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin class names(BasePayload): name = "names" - author = ("Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" - description = ("ie. jon-smith", ) + description = ("ie. jon-smith",) summary = "Returns possible usernames by mixing the given words, separated by -, using known typical constructions." category = ["default"] priority = 99 - parameters = ( - ("name", "", True, "Name and surname in the form of name-surname."), - ) + parameters = (("name", "", True, "Name and surname in the form of name-surname."),) default_parameter = "name" @@ -77,10 +79,10 @@ def __init__(self, params): def count(self): return self.__count - def __iter__(self): - return self + def get_type(self): + return FuzzWordType.WORD - def __next__(self): + def get_next(self): if self.creatednames: payl = self.creatednames.pop() return payl diff --git a/src/wfuzz/plugins/payloads/permutation.py b/src/wfuzz/plugins/payloads/permutation.py index 2d294e9f..aea1cc9a 100644 --- a/src/wfuzz/plugins/payloads/permutation.py +++ b/src/wfuzz/plugins/payloads/permutation.py @@ -1,6 +1,7 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload from wfuzz.exception import FuzzExceptBadOptions +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin @@ -13,9 +14,7 @@ class permutation(BasePayload): category = ["default"] priority = 99 - parameters = ( - ("ch", "", True, "Charset and len to permute in the form of abc-2."), - ) + parameters = (("ch", "", True, "Charset and len to permute in the form of abc-2."),) default_parameter = "ch" @@ -28,7 +27,7 @@ def __init__(self, params): self.charset = ran[0] self.width = int(ran[1]) except ValueError: - raise FuzzExceptBadOptions("Bad range format (eg. \"0-ffa\")") + raise FuzzExceptBadOptions('Bad range format (eg. "0-ffa")') pset = [] for x in self.charset: @@ -37,17 +36,17 @@ def __init__(self, params): words = self.xcombinations(pset, self.width) self.lista = [] for x in words: - self.lista.append(''.join(x)) + self.lista.append("".join(x)) self.__count = len(self.lista) - def __iter__(self): - return self - def count(self): return self.__count - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): if self.lista != []: payl = self.lista.pop() return payl diff --git a/src/wfuzz/plugins/payloads/range.py b/src/wfuzz/plugins/payloads/range.py index 5d6381a9..e4fc9dfc 100644 --- a/src/wfuzz/plugins/payloads/range.py +++ b/src/wfuzz/plugins/payloads/range.py @@ -1,21 +1,24 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptPluginBadParams from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin class range(BasePayload): name = "range" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" description = ("ie. 0-10",) summary = "Returns each number of the given range." category = ["default"] priority = 99 - parameters = ( - ("range", "", True, "Range of numbers in the form 0-10."), - ) + parameters = (("range", "", True, "Range of numbers in the form 0-10."),) default_parameter = "range" @@ -30,9 +33,12 @@ def __init__(self, params): self.width = len(ran[0]) self.current = self.minimum except ValueError: - raise FuzzExceptPluginBadParams("Bad range format (eg. \"23-56\")") + raise FuzzExceptPluginBadParams('Bad range format (eg. "23-56")') + + def get_type(self): + return FuzzWordType.WORD - def __next__(self): + def get_next(self): if self.current > self.maximum: raise StopIteration else: diff --git a/src/wfuzz/plugins/payloads/shodanp.py b/src/wfuzz/plugins/payloads/shodanp.py index 6664ab55..b9a76da4 100644 --- a/src/wfuzz/plugins/payloads/shodanp.py +++ b/src/wfuzz/plugins/payloads/shodanp.py @@ -1,6 +1,7 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.payloadtools import ShodanIter from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType @moduleman_plugin @@ -8,9 +9,7 @@ class shodanp(BasePayload): name = "shodanp" author = ("Xavi Mendez (@xmendez)",) version = "0.1" - description = ( - "Queries the Shodan API", - ) + description = ("Queries the Shodan API",) summary = "Returns URLs of a given Shodan API search (needs api key)." category = ["default"] @@ -19,7 +18,12 @@ class shodanp(BasePayload): parameters = ( ("search", "", True, "Shodan search string."), ("page", "0", False, "Offset page, starting at zero."), - ("limit", "0", False, "Number of pages (1 query credit = 100 results). Zero for all."), + ( + "limit", + "0", + False, + "Number of pages (1 query credit = 100 results). Zero for all.", + ), ) default_parameter = "search" @@ -33,23 +37,23 @@ def __init__(self, params): self._it = ShodanIter(search, page, limit) - def __iter__(self): - return self - def count(self): return -1 def close(self): self._it._stop() - def __next__(self): + def get_type(self): + return FuzzWordType.WORD + + def get_next(self): match = next(self._it) - port = match['port'] - scheme = 'https' if 'ssl' in match or port == 443 else 'http' + port = match["port"] + scheme = "https" if "ssl" in match or port == 443 else "http" - if match['hostnames']: - for hostname in match['hostnames']: + if match["hostnames"]: + for hostname in match["hostnames"]: return "{}://{}:{}".format(scheme, hostname, port) else: - return "{}://{}:{}".format(scheme, match['ip_str'], port) + return "{}://{}:{}".format(scheme, match["ip_str"], port) diff --git a/src/wfuzz/plugins/payloads/stdin.py b/src/wfuzz/plugins/payloads/stdin.py index 83ffd3d4..5aa70115 100644 --- a/src/wfuzz/plugins/payloads/stdin.py +++ b/src/wfuzz/plugins/payloads/stdin.py @@ -1,5 +1,6 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePayload +from wfuzz.fuzzobjects import FuzzWordType import sys @@ -14,8 +15,7 @@ class stdin(BasePayload): category = ["default"] priority = 99 - parameters = ( - ) + parameters = () default_parameter = "" @@ -26,10 +26,10 @@ def __init__(self, params): def count(self): return self.__count - def __iter__(self): - return self + def get_type(self): + return FuzzWordType.WORD - def __next__(self): + def get_next(self): line = next(sys.stdin).strip() return line diff --git a/src/wfuzz/plugins/payloads/wfuzzp.py b/src/wfuzz/plugins/payloads/wfuzzp.py index c1c51a5d..f6a30ec4 100644 --- a/src/wfuzz/plugins/payloads/wfuzzp.py +++ b/src/wfuzz/plugins/payloads/wfuzzp.py @@ -3,16 +3,16 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptBadFile -from wfuzz.fuzzobjects import FuzzResult +from wfuzz.fuzzobjects import FuzzResult, FuzzWordType from wfuzz.plugin_api.base import BasePayload -from wfuzz.utils import rgetattr +from wfuzz.helpers.obj_dyn import rgetattr @moduleman_plugin class wfuzzp(BasePayload): name = "wfuzzp" author = ("Xavi Mendez (@xmendez)",) - version = "0.1" + version = "0.2" description = ( "This payload uses pickle.", "Warning: The pickle module is not intended to be secure against erroneous or maliciously constructed data.", @@ -25,7 +25,12 @@ class wfuzzp(BasePayload): parameters = ( ("fn", "", True, "Filename of a valid wfuzz result file."), - ("attr", None, False, "Attribute of fuzzresult to return. If not specified the whole object is returned."), + ( + "attr", + None, + False, + "Attribute of fuzzresult to return. If not specified the whole object is returned.", + ), ) default_parameter = "fn" @@ -37,24 +42,26 @@ def __init__(self, params): self.attr = self.params["attr"] self._it = self._gen_wfuzz(self.params["fn"]) - def __iter__(self): - return self - def count(self): return self.__max - def __next__(self): + def get_next(self): next_item = next(self._it) return next_item if not self.attr else rgetattr(next_item, self.attr) + def get_type(self): + return FuzzWordType.FUZZRES if not self.attr else FuzzWordType.WORD + def _gen_wfuzz(self, output_fn): try: - with gzip.open(self.find_file(output_fn), 'r+b') as output: + with gzip.open(self.find_file(output_fn), "r+b") as output: while 1: item = pickle.load(output) if not isinstance(item, FuzzResult): - raise FuzzExceptBadFile("Wrong wfuzz payload format, the object read is not a valid fuzz result.") + raise FuzzExceptBadFile( + "Wrong wfuzz payload format, the object read is not a valid fuzz result." + ) yield item except IOError as e: diff --git a/src/wfuzz/plugins/printers/printers.py b/src/wfuzz/plugins/printers/printers.py index a5cc689b..7552729b 100644 --- a/src/wfuzz/plugins/printers/printers.py +++ b/src/wfuzz/plugins/printers/printers.py @@ -5,7 +5,6 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePrinter -from wfuzz.fuzzobjects import FuzzResult @moduleman_plugin @@ -73,21 +72,24 @@ def header(self, summary): self.node_service = self.__create_xml_element(node_port, "service", "http") def result(self, fuzz_result): - if fuzz_result.type == FuzzResult.result: - node_url = self.__create_xml_element(self.node_service, "url", str(fuzz_result.url)) + node_url = self.__create_xml_element( + self.node_service, "url", str(fuzz_result.url) + ) - if 'Server' in fuzz_result.history.headers.response: - self.__create_xml_element(node_url, "HTTPServer", fuzz_result.history.headers.response['Server']) + if "Server" in fuzz_result.history.headers.response: + self.__create_xml_element( + node_url, "HTTPServer", fuzz_result.history.headers.response["Server"] + ) - location = "" - if 'Location' in fuzz_result.history.headers.response: - location = fuzz_result.history.headers.response['Location'] + location = "" + if "Location" in fuzz_result.history.headers.response: + location = fuzz_result.history.headers.response["Location"] - if fuzz_result.code == 301 or fuzz_result.code == 302 and location: - self.__create_xml_element(node_url, "RedirectLocation", location) + if fuzz_result.code == 301 or fuzz_result.code == 302 and location: + self.__create_xml_element(node_url, "RedirectLocation", location) - self.__create_xml_element(node_url, "ResponseCode", str(fuzz_result.code)) - self.__create_xml_element(node_url, "source", "WFuzz") + self.__create_xml_element(node_url, "ResponseCode", str(fuzz_result.code)) + self.__create_xml_element(node_url, "source", "WFuzz") def footer(self, summary): self.f.write(self.node_mt.toxml()) @@ -96,7 +98,11 @@ def footer(self, summary): @moduleman_plugin class html(BasePrinter): name = "html" - author = ("Carlos del Ojo", "Christian Martorella", "Adapted to newer versions Xavi Mendez (@xmendez)") + author = ( + "Carlos del Ojo", + "Christian Martorella", + "Adapted to newer versions Xavi Mendez (@xmendez)", + ) version = "0.1" summary = "Prints results in html format" category = ["default"] @@ -108,27 +114,52 @@ def __init__(self, output): def header(self, summary): url = summary.url - self.f.write("

Fuzzing %s

\r\n\r\n\r\n" % (url)) + self.f.write( + '

Fuzzing %s

\r\n
#requestCode#lines#wordsUrl
\r\n\r\n' + % (url) + ) def result(self, fuzz_result): - if fuzz_result.type == FuzzResult.result: - htmlc = "" - - if fuzz_result.code >= 400 and fuzz_result.code < 500: - htmlc = "" - elif fuzz_result.code >= 300 and fuzz_result.code < 400: - htmlc = "" - elif fuzz_result.code >= 200 and fuzz_result.code < 300: - htmlc = "" - - if fuzz_result.history.method.lower() == "post": - inputs = "" - for n, v in list(fuzz_result.history.params.post.items()): - inputs += "" % (n, v) - - self.f.write("\r\n\r\n\r\n\r\n\r\n\r\n\r\n" % (fuzz_result.nres, htmlc, fuzz_result.code, fuzz_result.lines, fuzz_result.words, fuzz_result.description, fuzz_result.url, inputs)) - else: - self.f.write("\r\n\r\n" % (fuzz_result.nres, htmlc, fuzz_result.code, fuzz_result.lines, fuzz_result.words, fuzz_result.url, fuzz_result.url)) + htmlc = "" + + if fuzz_result.code >= 400 and fuzz_result.code < 500: + htmlc = "" + elif fuzz_result.code >= 300 and fuzz_result.code < 400: + htmlc = "" + elif fuzz_result.code >= 200 and fuzz_result.code < 300: + htmlc = "" + + if fuzz_result.history.method.lower() == "post": + inputs = "" + for n, v in list(fuzz_result.history.params.post.items()): + inputs += '' % (n, v) + + self.f.write( + '\r\n\r\n\r\n\r\n\r\n\r\n\r\n' + % ( + fuzz_result.nres, + htmlc, + fuzz_result.code, + fuzz_result.lines, + fuzz_result.words, + fuzz_result.description, + fuzz_result.url, + inputs, + ) + ) + else: + self.f.write( + "\r\n\r\n" + % ( + fuzz_result.nres, + htmlc, + fuzz_result.code, + fuzz_result.lines, + fuzz_result.words, + fuzz_result.url, + fuzz_result.url, + ) + ) def footer(self, summary): self.f.write("
#requestCode#lines#wordsUrl
%05d%s%d%4dL%5dW
%s
%s
%05d%s%d%4dL%5dW%s
%05d%s%d%4dL%5dW
%s
%s
%05d%s%d%4dL%5dW%s
Wfuzz by EdgeSecurity
\r\n") @@ -151,33 +182,32 @@ def header(self, res): pass def result(self, res): - if res.type == FuzzResult.result: - server = "" - if 'Server' in res.history.headers.response: - server = res.history.headers.response['Server'] - location = "" - if 'Location' in res.history.headers.response: - location = res.history.headers.response['Location'] - elif res.history.url != res.history.redirect_url: - location = "(*) %s" % res.history.url - post_data = [] - if res.history.method.lower() == "post": - for n, v in list(res.history.params.post.items()): - post_data.append({"parameter": n, "value": v}) - - res_entry = { - "chars": res.chars, - "code": res.code, - "payload": res.description, - "lines": res.lines, - "location": location, - "method": res.history.method, - "post_data": post_data, - "server": server, - "url": res.url, - "words": res.words - } - self.json_res.append(res_entry) + server = "" + if "Server" in res.history.headers.response: + server = res.history.headers.response["Server"] + location = "" + if "Location" in res.history.headers.response: + location = res.history.headers.response["Location"] + elif res.history.url != res.history.redirect_url: + location = "(*) %s" % res.history.url + post_data = [] + if res.history.method.lower() == "post": + for n, v in list(res.history.params.post.items()): + post_data.append({"parameter": n, "value": v}) + + res_entry = { + "chars": res.chars, + "code": res.code, + "payload": res.description, + "lines": res.lines, + "location": location, + "method": res.history.method, + "post_data": post_data, + "server": server, + "url": res.url, + "words": res.words, + } + self.json_res.append(res_entry) def footer(self, summary): self.f.write(jjson.dumps(self.json_res)) @@ -204,34 +234,56 @@ def header(self, summary): self.f.write("Total requests: <>\n") if self.verbose: - self.f.write("==============================================================================================================================================\n") - self.f.write("ID C.Time Response Lines Word Chars Server Redirect Payload \n") - self.f.write("==============================================================================================================================================\n") + self.f.write( + "==============================================================================================================================================\n" + ) + self.f.write( + "ID C.Time Response Lines Word Chars Server Redirect Payload \n" + ) + self.f.write( + "==============================================================================================================================================\n" + ) else: - self.f.write("==================================================================\n") - self.f.write("ID Response Lines Word Chars Request \n") - self.f.write("==================================================================\n") + self.f.write( + "==================================================================\n" + ) + self.f.write( + "ID Response Lines Word Chars Request \n" + ) + self.f.write( + "==================================================================\n" + ) def _print_verbose(self, res): self.f.write("%05d: " % res.nres) self.f.write("%.3fs C=" % res.timer) location = "" - if 'Location' in res.history.headers.response: - location = res.history.headers.response['Location'] + if "Location" in res.history.headers.response: + location = res.history.headers.response["Location"] elif res.history.url != res.history.redirect_url: location = "(*) %s" % res.history.url server = "" - if 'Server' in res.history.headers.response: - server = res.history.headers.response['Server'] + if "Server" in res.history.headers.response: + server = res.history.headers.response["Server"] if res.exception: self.f.write("XXX") else: self.f.write("%05d: C=%03d" % (res.nres, res.code)) - self.f.write(" %4d L\t %5d W\t %5d Ch %20.20s %51.51s \"%s\"\n" % (res.lines, res.words, res.chars, server[:17], location[:48], res.description)) + self.f.write( + ' %4d L\t %5d W\t %5d Ch %20.20s %51.51s "%s"\n' + % ( + res.lines, + res.words, + res.chars, + server[:17], + location[:48], + res.description, + ) + ) for i in res.plugins_res: self.f.write(" |_ %s\n" % i.issue) @@ -242,34 +294,51 @@ def _print(self, res): else: self.f.write("%05d: C=%03d" % (res.nres, res.code)) - self.f.write(" %4d L\t %5d W\t %5d Ch\t \"%s\"\n" % (res.lines, res.words, res.chars, res.description)) + self.f.write( + ' %4d L\t %5d W\t %5d Ch\t "%s"\n' + % (res.lines, res.words, res.chars, res.description) + ) for i in res.plugins_res: self.f.write(" |_ %s\n" % i.issue) def result(self, res): - if res.type == FuzzResult.result: - if self.verbose: - self._print_verbose(res) - else: - self._print(res) + if self.verbose: + self._print_verbose(res) + else: + self._print(res) def footer(self, summary): self.f.write("\n") self.f.write("Total time: %s\n" % str(summary.totaltime)[:8]) if summary.backfeed() > 0: - self.f.write("Processed Requests: %s (%d + %d)\n" % (str(summary.processed())[:8], (summary.processed() - summary.backfeed()), summary.backfeed())) + self.f.write( + "Processed Requests: %s (%d + %d)\n" + % ( + str(summary.processed())[:8], + (summary.processed() - summary.backfeed()), + summary.backfeed(), + ) + ) else: self.f.write("Processed Requests: %s\n" % (str(summary.processed())[:8])) self.f.write("Filtered Requests: %s\n" % (str(summary.filtered())[:8])) - self.f.write("Requests/sec.: %s\n" % str(summary.processed() / summary.totaltime if summary.totaltime > 0 else 0)[:8]) + self.f.write( + "Requests/sec.: %s\n" + % str( + summary.processed() / summary.totaltime if summary.totaltime > 0 else 0 + )[:8] + ) @moduleman_plugin class csv(BasePrinter): name = "csv" - author = ("@Yoginski initial version", "Adapted by @egilas to work in newer version of wfuzz") + author = ( + "@Yoginski initial version", + "Adapted by @egilas to work in newer version of wfuzz", + ) summary = "CSV printer ftw" version = "1.0" category = ["default"] @@ -284,18 +353,21 @@ def __init__(self, output): self.csv_writer = csvmod.writer(self) def header(self, summary): - self._print_csv(["id", "response", "lines", "word", "chars", "request", "success"]) + self._print_csv( + ["id", "response", "lines", "word", "chars", "request", "success"] + ) def result(self, res): - if res.type == FuzzResult.result: - line = [res.nres, - res.code, - res.lines, - res.words, - res.chars, - res.description, - 0 if res.exception else 1] - self._print_csv(line) + line = [ + res.nres, + res.code, + res.lines, + res.words, + res.chars, + res.description, + 0 if res.exception else 1, + ] + self._print_csv(line) def noresult(self, res): pass diff --git a/src/wfuzz/plugins/scripts/backups.py b/src/wfuzz/plugins/scripts/backups.py index 02d5db4c..880566a5 100644 --- a/src/wfuzz/plugins/scripts/backups.py +++ b/src/wfuzz/plugins/scripts/backups.py @@ -23,7 +23,12 @@ class backups(BasePlugin): priority = 99 parameters = ( - ("ext", ".bak,.tgz,.zip,.tar.gz,~,.rar,.old,.-.swp", False, "Extensions to look for."), + ( + "ext", + ".bak,.tgz,.zip,.tar.gz,~,.rar,.old,.-.swp", + False, + "Extensions to look for.", + ), ) def __init__(self): @@ -31,7 +36,9 @@ def __init__(self): self.extensions = self.kbase["backups.ext"][0].split(",") def validate(self, fuzzresult): - return fuzzresult.code != 404 and (fuzzresult.history.urlparse.fext not in self.extensions) + return fuzzresult.code != 404 and ( + fuzzresult.history.urlparse.fext not in self.extensions + ) def process(self, fuzzresult): # >>> urlparse.urlparse("http://www.localhost.com/kk/index.html?id=1") @@ -41,9 +48,15 @@ def process(self, fuzzresult): pre, nothing, extension = pre_extension.partition("-") # http://localhost/dir/test.html -----> test.BAKKK - self.queue_url(urljoin(fuzzresult.url, pre + fuzzresult.history.urlparse.fname + extension)) + self.queue_url( + urljoin( + fuzzresult.url, pre + fuzzresult.history.urlparse.fname + extension + ) + ) # http://localhost/dir/test.html ---> test.html.BAKKK - self.queue_url(urljoin(fuzzresult.url, fuzzresult.history.urlparse.ffname + extension)) + self.queue_url( + urljoin(fuzzresult.url, fuzzresult.history.urlparse.ffname + extension) + ) # http://localhost/dir/test.html ----> dir.BAKKK diff --git a/src/wfuzz/plugins/scripts/cookies.py b/src/wfuzz/plugins/scripts/cookies.py index 61f66927..eb835929 100644 --- a/src/wfuzz/plugins/scripts/cookies.py +++ b/src/wfuzz/plugins/scripts/cookies.py @@ -8,12 +8,11 @@ class cookies(BasePlugin): author = ("Xavi Mendez (@xmendez)",) version = "0.1" summary = "Looks for new cookies" - description = ("Looks for new cookies", ) + description = ("Looks for new cookies",) category = ["verbose", "passive"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) @@ -27,6 +26,10 @@ def process(self, fuzzresult): if len(new_cookies) > 0: for name, value in new_cookies: - if name != "" and "cookie" not in self.kbase or name not in self.kbase["cookie"]: + if ( + name != "" + and "cookie" not in self.kbase + or name not in self.kbase["cookie"] + ): self.kbase["cookie"] = name self.add_result("Cookie first set - %s=%s" % (name, value)) diff --git a/src/wfuzz/plugins/scripts/cvs_extractor.py b/src/wfuzz/plugins/scripts/cvs_extractor.py index 9212b738..05835598 100644 --- a/src/wfuzz/plugins/scripts/cvs_extractor.py +++ b/src/wfuzz/plugins/scripts/cvs_extractor.py @@ -24,14 +24,17 @@ class cvs_extractor(BasePlugin, DiscoveryPluginMixin): description = ("Parses CVS/Entries file and enqueues found entries",) category = ["default", "active", "discovery"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) def validate(self, fuzzresult): - return fuzzresult.url.find("CVS/Entries") >= 0 and fuzzresult.code == 200 and check_content_type(fuzzresult, 'text') + return ( + fuzzresult.url.find("CVS/Entries") >= 0 + and fuzzresult.code == 200 + and check_content_type(fuzzresult, "text") + ) def process(self, fuzzresult): base_url = urljoin(fuzzresult.url, "..") @@ -42,6 +45,6 @@ def process(self, fuzzresult): self.queue_url(urljoin(base_url, record[1])) # Directory - if record[0] == 'D': + if record[0] == "D": self.queue_url(urljoin(base_url, record[1])) self.queue_url(urljoin(base_url, "%s/CVS/Entries" % (record[1]))) diff --git a/src/wfuzz/plugins/scripts/errors.py b/src/wfuzz/plugins/scripts/errors.py index 35e6663f..31208f54 100644 --- a/src/wfuzz/plugins/scripts/errors.py +++ b/src/wfuzz/plugins/scripts/errors.py @@ -14,92 +14,91 @@ class errors(BasePlugin): category = ["default", "passive"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) regex_list = [ - 'A syntax error has occurred', - 'ADODB.Field error', - 'ASP.NET is configured to show verbose error messages', - 'ASP.NET_SessionId', - 'Active Server Pages error', - 'An illegal character has been found in the statement', + "A syntax error has occurred", + "ADODB.Field error", + "ASP.NET is configured to show verbose error messages", + "ASP.NET_SessionId", + "Active Server Pages error", + "An illegal character has been found in the statement", 'An unexpected token "END-OF-STATEMENT" was found', - 'Can\'t connect to local', - 'Custom Error Message', - 'DB2 Driver', - 'DB2 Error', - 'DB2 ODBC', - 'Disallowed Parent Path', - 'Error Diagnostic Information', - 'Error Message : Error loading required libraries.', - 'Error Report', - 'Error converting data type varchar to numeric', - 'Fatal error', - 'Incorrect syntax near', - 'Internal Server Error', - 'Invalid Path Character', - 'Invalid procedure call or argument', - 'Invision Power Board Database Error', - 'JDBC Driver', - 'JDBC Error', - 'JDBC MySQL', - 'JDBC Oracle', - 'JDBC SQL', - 'Microsoft OLE DB Provider for ODBC Drivers', - 'Microsoft VBScript compilation error', - 'Microsoft VBScript error', - 'MySQL Driver', - 'MySQL Error', - 'MySQL ODBC', - 'ODBC DB2', - 'ODBC Driver', - 'ODBC Error', - 'ODBC Microsoft Access', - 'ODBC Oracle', - 'ODBC SQL', - 'ODBC SQL Server', - 'OLE/DB provider returned message', - 'ORA-0', - 'ORA-1', - 'Oracle DB2', - 'Oracle Driver', - 'Oracle Error', - 'Oracle ODBC', - 'PHP Error', - 'PHP Parse error', - 'PHP Warning', - 'Permission denied: \'GetObject\'', - 'PostgreSQL query failed: ERROR: parser: parse error', - r'SQL Server Driver\]\[SQL Server', - 'SQL command not properly ended', - 'SQLException', - 'Supplied argument is not a valid PostgreSQL result', - 'Syntax error in query expression', - 'The error occurred in', - 'The script whose uid is', - 'Type mismatch', - 'Unable to jump to row', - 'Unclosed quotation mark before the character string', - 'Unterminated string constant', - 'Warning: Cannot modify header information - headers already sent', - 'Warning: Supplied argument is not a valid File-Handle resource in', - r'Warning: mysql_query\(\)', - r'Warning: mysql_fetch_array\(\)', - r'Warning: pg_connect\(\): Unable to connect to PostgreSQL server: FATAL', - 'You have an error in your SQL syntax near', - 'data source=', - 'detected an internal error [IBM][CLI Driver][DB2/6000]', - 'invalid query', - 'is not allowed to access', - 'missing expression', - 'mySQL error with query', - 'mysql error', - 'on MySQL result index', - 'supplied argument is not a valid MySQL result resource', + "Can't connect to local", + "Custom Error Message", + "DB2 Driver", + "DB2 Error", + "DB2 ODBC", + "Disallowed Parent Path", + "Error Diagnostic Information", + "Error Message : Error loading required libraries.", + "Error Report", + "Error converting data type varchar to numeric", + "Fatal error", + "Incorrect syntax near", + "Internal Server Error", + "Invalid Path Character", + "Invalid procedure call or argument", + "Invision Power Board Database Error", + "JDBC Driver", + "JDBC Error", + "JDBC MySQL", + "JDBC Oracle", + "JDBC SQL", + "Microsoft OLE DB Provider for ODBC Drivers", + "Microsoft VBScript compilation error", + "Microsoft VBScript error", + "MySQL Driver", + "MySQL Error", + "MySQL ODBC", + "ODBC DB2", + "ODBC Driver", + "ODBC Error", + "ODBC Microsoft Access", + "ODBC Oracle", + "ODBC SQL", + "ODBC SQL Server", + "OLE/DB provider returned message", + "ORA-0", + "ORA-1", + "Oracle DB2", + "Oracle Driver", + "Oracle Error", + "Oracle ODBC", + "PHP Error", + "PHP Parse error", + "PHP Warning", + "Permission denied: 'GetObject'", + "PostgreSQL query failed: ERROR: parser: parse error", + r"SQL Server Driver\]\[SQL Server", + "SQL command not properly ended", + "SQLException", + "Supplied argument is not a valid PostgreSQL result", + "Syntax error in query expression", + "The error occurred in", + "The script whose uid is", + "Type mismatch", + "Unable to jump to row", + "Unclosed quotation mark before the character string", + "Unterminated string constant", + "Warning: Cannot modify header information - headers already sent", + "Warning: Supplied argument is not a valid File-Handle resource in", + r"Warning: mysql_query\(\)", + r"Warning: mysql_fetch_array\(\)", + r"Warning: pg_connect\(\): Unable to connect to PostgreSQL server: FATAL", + "You have an error in your SQL syntax near", + "data source=", + "detected an internal error [IBM][CLI Driver][DB2/6000]", + "invalid query", + "is not allowed to access", + "missing expression", + "mySQL error with query", + "mysql error", + "on MySQL result index", + "supplied argument is not a valid MySQL result resource", ] self.error_regex = [] diff --git a/src/wfuzz/plugins/scripts/grep.py b/src/wfuzz/plugins/scripts/grep.py index 5165c148..df6ff295 100644 --- a/src/wfuzz/plugins/scripts/grep.py +++ b/src/wfuzz/plugins/scripts/grep.py @@ -18,17 +18,19 @@ class grep(BasePlugin): category = ["tools"] priority = 99 - parameters = ( - ("regex", "", True, "Regex to perform the grep against."), - ) + parameters = (("regex", "", True, "Regex to perform the grep against."),) def __init__(self): BasePlugin.__init__(self) try: print(self.kbase["grep.regex"]) - self.regex = re.compile(self.kbase["grep.regex"][0], re.MULTILINE | re.DOTALL) + self.regex = re.compile( + self.kbase["grep.regex"][0], re.MULTILINE | re.DOTALL + ) except Exception: - raise FuzzExceptPluginBadParams("Incorrect regex or missing regex parameter.") + raise FuzzExceptPluginBadParams( + "Incorrect regex or missing regex parameter." + ) def validate(self, fuzzresult): return True diff --git a/src/wfuzz/plugins/scripts/headers.py b/src/wfuzz/plugins/scripts/headers.py index 7e360a0f..19732412 100644 --- a/src/wfuzz/plugins/scripts/headers.py +++ b/src/wfuzz/plugins/scripts/headers.py @@ -11,8 +11,7 @@ class headers(BasePlugin): description = ("Looks for new server headers",) category = ["verbose", "passive"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) diff --git a/src/wfuzz/plugins/scripts/links.py b/src/wfuzz/plugins/scripts/links.py index a40586ce..9c148d40 100644 --- a/src/wfuzz/plugins/scripts/links.py +++ b/src/wfuzz/plugins/scripts/links.py @@ -23,6 +23,8 @@ class links(BasePlugin, DiscoveryPluginMixin): priority = 99 parameters = ( + ("add_path", False, False, "Add parsed paths as results."), + ("regex", None, False, "Regex of accepted domains."), ) def __init__(self): @@ -41,27 +43,70 @@ def __init__(self): for i in regex: self.regex.append(re.compile(i, re.MULTILINE | re.DOTALL)) + self.add_path = self.kbase["links.add_path"] + + self.domain_regex = None + if self.kbase["links.regex"][0]: + self.domain_regex = re.compile( + self.kbase["links.regex"][0], re.MULTILINE | re.DOTALL + ) + def validate(self, fuzzresult): return fuzzresult.code in [200] def process(self, fuzzresult): - list_links = [] - + list_links = set() # O # ParseResult(scheme='', netloc='', path='www.owasp.org/index.php/OWASP_EU_Summit_2008', params='', query='', fragment='') - for r in self.regex: - for i in r.findall(fuzzresult.history.content): - parsed_link = parse_url(i) + for regex in self.regex: + for link_url in regex.findall(fuzzresult.history.content): + if not link_url: + continue + + parsed_link = parse_url(link_url) + + if ( + not parsed_link.scheme + or parsed_link.scheme == "http" + or parsed_link.scheme == "https" + ) and self.from_domain(fuzzresult, parsed_link): + cache_key = parsed_link.cache_key(self.base_fuzz_res.history.urlp) + if cache_key not in list_links: + list_links.add(cache_key) + self.enqueue_link(fuzzresult, link_url, parsed_link) + + def enqueue_link(self, fuzzresult, link_url, parsed_link): + # dir path + if self.add_path: + split_path = parsed_link.path.split("/") + newpath = "/".join(split_path[:-1]) + "/" + self.queue_url(urljoin(fuzzresult.url, newpath)) + + # file path + self.queue_url(urljoin(fuzzresult.url, link_url)) + + def from_domain(self, fuzzresult, parsed_link): + # relative path + if not parsed_link.netloc and parsed_link.path: + return True - if (not parsed_link.scheme or parsed_link.scheme == "http" or parsed_link.scheme == "https") and (not parsed_link.netloc and parsed_link.path): - if i not in list_links: - list_links.append(i) + # same domain + if parsed_link.netloc == self.base_fuzz_res.history.urlp.netloc: + return True - # dir path - split_path = parsed_link.path.split("/") - newpath = '/'.join(split_path[:-1]) + "/" - self.queue_url(urljoin(fuzzresult.url, newpath)) + # regex domain + if ( + self.domain_regex + and self.domain_regex.search(parsed_link.netloc) is not None + ): + return True - # file path - self.queue_url(urljoin(fuzzresult.url, i)) + if ( + parsed_link.netloc + and parsed_link.netloc not in self.kbase["links.new_domains"] + ): + self.kbase["links.new_domains"].append(parsed_link.netloc) + self.add_result( + "New domain found, link not enqueued %s" % parsed_link.netloc + ) diff --git a/src/wfuzz/plugins/scripts/listing.py b/src/wfuzz/plugins/scripts/listing.py index 501da7e7..d49b5247 100644 --- a/src/wfuzz/plugins/scripts/listing.py +++ b/src/wfuzz/plugins/scripts/listing.py @@ -14,8 +14,7 @@ class listing(BasePlugin): category = ["default", "passive"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) @@ -31,8 +30,12 @@ def __init__(self): dir_indexing_regexes.append("Folder Listing.") dir_indexing_regexes.append('<table summary="Directory Listing" ') dir_indexing_regexes.append("- Browsing directory ") - dir_indexing_regexes.append('">\\[To Parent Directory\\]</a><br><br>') # IIS 6.0 and 7.0 - dir_indexing_regexes.append('<A HREF=".*?">.*?</A><br></pre><hr></body></html>') # IIS 5.0 + dir_indexing_regexes.append( + '">\\[To Parent Directory\\]</a><br><br>' + ) # IIS 6.0 and 7.0 + dir_indexing_regexes.append( + '<A HREF=".*?">.*?</A><br></pre><hr></body></html>' + ) # IIS 5.0 self.regex = [] for i in dir_indexing_regexes: diff --git a/src/wfuzz/plugins/scripts/npm_deps.py b/src/wfuzz/plugins/scripts/npm_deps.py new file mode 100644 index 00000000..c54e9bf2 --- /dev/null +++ b/src/wfuzz/plugins/scripts/npm_deps.py @@ -0,0 +1,49 @@ +import re + +from wfuzz.plugin_api.base import BasePlugin +from wfuzz.exception import FuzzExceptPluginBadParams +from wfuzz.externals.moduleman.plugin import moduleman_plugin + + +@moduleman_plugin +class npm_deps(BasePlugin): + name = "npm_deps" + author = ("Xavi Mendez (@xmendez)",) + version = "0.1" + summary = "Looks for npm dependencies definition in js code" + description = ( + "Extracts npm packages by using regex pattern from the HTTP response and prints it", + ) + category = ["default"] + priority = 99 + + parameters = () + + REGEX_PATT = re.compile(r'"([^"]+)":"([^"]+)"', re.MULTILINE | re.DOTALL) + REGEX_DEP = re.compile( + r"dependencies:\{(.*?)\}", re.MULTILINE | re.DOTALL | re.IGNORECASE + ) + REGEX_DEV_DEP = re.compile( + r"devdependencies:\{(.*?)\}", re.MULTILINE | re.DOTALL | re.IGNORECASE + ) + + def __init__(self): + BasePlugin.__init__(self) + + def validate(self, fuzzresult): + if fuzzresult.history.urlparse.fext != ".js" or fuzzresult.code != 200: + return False + + self.match = self.REGEX_DEP.search(fuzzresult.history.content) + self.match_dev = self.REGEX_DEV_DEP.search(fuzzresult.history.content) + + return self.match is not None or self.match_dev is not None + + def process(self, fuzzresult): + if self.match_dev: + for name, version in self.REGEX_PATT.findall(self.match_dev.group(1)): + self.add_result(name) + + if self.match: + for name, version in self.REGEX_PATT.findall(self.match.group(1)): + self.add_result(name) diff --git a/src/wfuzz/plugins/scripts/robots.py b/src/wfuzz/plugins/scripts/robots.py index c16b314f..57851ae6 100644 --- a/src/wfuzz/plugins/scripts/robots.py +++ b/src/wfuzz/plugins/scripts/robots.py @@ -22,23 +22,34 @@ class robots(BasePlugin, DiscoveryPluginMixin): category = ["default", "active", "discovery"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) def validate(self, fuzzresult): - return fuzzresult.history.urlparse.ffname == "robots.txt" and fuzzresult.code == 200 and check_content_type(fuzzresult, 'text') + return ( + fuzzresult.history.urlparse.ffname == "robots.txt" + and fuzzresult.code == 200 + and check_content_type(fuzzresult, "text") + ) def process(self, fuzzresult): # Shamelessly (partially) copied from w3af's plugins/discovery/robotsReader.py - for line in fuzzresult.history.content.split('\n'): + for line in fuzzresult.history.content.split("\n"): line = line.strip() - if len(line) > 0 and line[0] != '#' and (line.upper().find('ALLOW') == 0 or line.upper().find('DISALLOW') == 0 or line.upper().find('SITEMAP') == 0): - - url = line[line.find(':') + 1:] + if ( + len(line) > 0 + and line[0] != "#" + and ( + line.upper().find("ALLOW") == 0 + or line.upper().find("DISALLOW") == 0 + or line.upper().find("SITEMAP") == 0 + ) + ): + + url = line[line.find(":") + 1 :] url = url.strip(" *") if url: diff --git a/src/wfuzz/plugins/scripts/screenshot.py b/src/wfuzz/plugins/scripts/screenshot.py index a30046dc..0e658856 100644 --- a/src/wfuzz/plugins/scripts/screenshot.py +++ b/src/wfuzz/plugins/scripts/screenshot.py @@ -20,8 +20,7 @@ class screenshot(BasePlugin): category = ["tools", "active"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) @@ -35,5 +34,11 @@ def process(self, fuzzresult): filename = os.path.join(defult_tmp_dir, temp_name + ".png") - subprocess.call(['cutycapt', '--url=%s' % pipes.quote(fuzzresult.url), '--out=%s' % filename]) + subprocess.call( + [ + "cutycapt", + "--url=%s" % pipes.quote(fuzzresult.url), + "--out=%s" % filename, + ] + ) self.add_result("Screnshot taken, output at %s" % filename) diff --git a/src/wfuzz/plugins/scripts/sitemap.py b/src/wfuzz/plugins/scripts/sitemap.py index 76afe30d..3328e1e7 100644 --- a/src/wfuzz/plugins/scripts/sitemap.py +++ b/src/wfuzz/plugins/scripts/sitemap.py @@ -16,20 +16,24 @@ class sitemap(BasePlugin, DiscoveryPluginMixin): category = ["default", "active", "discovery"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) def validate(self, fuzzresult): - return fuzzresult.history.urlparse.ffname == "sitemap.xml" and fuzzresult.code == 200 + return ( + fuzzresult.history.urlparse.ffname == "sitemap.xml" + and fuzzresult.code == 200 + ) def process(self, fuzzresult): try: dom = xml.dom.minidom.parseString(fuzzresult.history.content) except Exception: - raise FuzzExceptResourceParseError('Error while parsing %s.' % fuzzresult.url) + raise FuzzExceptResourceParseError( + "Error while parsing %s." % fuzzresult.url + ) urlList = dom.getElementsByTagName("loc") for url in urlList: diff --git a/src/wfuzz/plugins/scripts/svn_extractor.py b/src/wfuzz/plugins/scripts/svn_extractor.py index cbf21d40..8e1bf468 100644 --- a/src/wfuzz/plugins/scripts/svn_extractor.py +++ b/src/wfuzz/plugins/scripts/svn_extractor.py @@ -19,8 +19,7 @@ class svn_extractor(BasePlugin, DiscoveryPluginMixin): category = ["default", "active", "discovery"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) @@ -29,10 +28,10 @@ def validate(self, fuzzresult): return fuzzresult.url.find(".svn/entries") > 0 and fuzzresult.code == 200 def readsvn(self, content): - ''' + """ Function shamesly copied (and adapted) from https://github.com/anantshri/svn-extractor/ Credit (C) Anant Shrivastava http://anantshri.info - ''' + """ old_line = "" file_list = [] dir_list = [] @@ -40,13 +39,13 @@ def readsvn(self, content): for a in content.splitlines(): # below functionality will find all usernames from svn entries file - if (a == "has-props"): + if a == "has-props": if old_line not in author_list: author_list.append(old_line) - if (a == "file"): + if a == "file": if old_line not in file_list: file_list.append(old_line) - if (a == "dir"): + if a == "dir": if old_line != "": dir_list.append(old_line) old_line = a @@ -58,11 +57,13 @@ def process(self, fuzzresult): file_list, dir_list, author_list = self.readsvn(fuzzresult.history.content) if author_list: - self.add_result("SVN authors: %s" % ', '.join(author_list)) + self.add_result("SVN authors: %s" % ", ".join(author_list)) for f in file_list: u = urljoin(base_url.replace("/.svn/", "/"), f) self.queue_url(u) for d in dir_list: - self.queue_url(urljoin(base_url.replace("/.svn/", "/"), d) + "/.svn/entries") + self.queue_url( + urljoin(base_url.replace("/.svn/", "/"), d) + "/.svn/entries" + ) diff --git a/src/wfuzz/plugins/scripts/title.py b/src/wfuzz/plugins/scripts/title.py index 6a3c07fb..36552854 100644 --- a/src/wfuzz/plugins/scripts/title.py +++ b/src/wfuzz/plugins/scripts/title.py @@ -12,8 +12,7 @@ class title(BasePlugin): category = ["verbose", "passive"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) @@ -25,6 +24,10 @@ def process(self, fuzzresult): soup = fuzzresult.history.get_soup() title = soup.title.string if soup.title else "" - if title != "" and "title" not in self.kbase or title not in self.kbase["title"]: + if ( + title != "" + and "title" not in self.kbase + or title not in self.kbase["title"] + ): self.kbase["title"] = title self.add_result("Page title: %s" % title) diff --git a/src/wfuzz/plugins/scripts/wcdb.py b/src/wfuzz/plugins/scripts/wcdb.py index 8756ef9c..3d128412 100644 --- a/src/wfuzz/plugins/scripts/wcdb.py +++ b/src/wfuzz/plugins/scripts/wcdb.py @@ -23,8 +23,7 @@ class wcdb_extractor(BasePlugin, DiscoveryPluginMixin): category = ["default", "active", "discovery"] priority = 99 - parameters = ( - ) + parameters = () def __init__(self): BasePlugin.__init__(self) @@ -33,10 +32,10 @@ def validate(self, fuzzresult): return fuzzresult.url.find(".svn/wc.d") > 0 and fuzzresult.code == 200 def readwc(self, content): - ''' + """ Function shamesly copied (and adapted) from https://github.com/anantshri/svn-extractor/ Credit (C) Anant Shrivastava http://anantshri.info - ''' + """ author_list = [] list_items = None (fd, filename) = tempfile.mkstemp() @@ -47,14 +46,18 @@ def readwc(self, content): conn = sqlite3.connect(filename) c = conn.cursor() try: - c.execute('select local_relpath, ".svn/pristine/" || substr(checksum,7,2) || "/" || substr(checksum,7) || ".svn-base" as alpha from NODES where kind="file";') + c.execute( + 'select local_relpath, ".svn/pristine/" || substr(checksum,7,2) || "/" || substr(checksum,7) || ".svn-base" as alpha from NODES where kind="file";' + ) list_items = c.fetchall() # below functionality will find all usernames who have commited atleast once. - c.execute('select distinct changed_author from nodes;') + c.execute("select distinct changed_author from nodes;") author_list = [r[0] for r in c.fetchall()] c.close() except Exception: - raise FuzzExceptResourceParseError("Error reading wc.db, either database corrupt or invalid file") + raise FuzzExceptResourceParseError( + "Error reading wc.db, either database corrupt or invalid file" + ) return author_list, list_items @@ -62,7 +65,7 @@ def process(self, fuzzresult): author_list, list_items = self.readwc(fuzzresult.history.content) if author_list: - self.add_result("SVN authors: %s" % ', '.join(author_list)) + self.add_result("SVN authors: %s" % ", ".join(author_list)) if list_items: for f, pristine in list_items: diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 0622ecb8..3a87676d 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -1,10 +1,12 @@ import re import sys import getopt +import warnings from collections import defaultdict -from wfuzz.utils import allowed_fields, get_path -from wfuzz.filter import PYPARSING +from wfuzz.helpers.file_func import get_filter_help_file +from wfuzz.helpers.obj_dyn import allowed_fields +from wfuzz.filters.ppfilter import PYPARSING from wfuzz.facade import Facade from wfuzz.options import FuzzSession from wfuzz.exception import FuzzException, FuzzExceptBadOptions, FuzzExceptBadInstall @@ -15,46 +17,128 @@ from wfuzz import __version__ as version from .output import table_print -short_opts = "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:" -long_opts = ['efield=', 'no-cache', 'ee=', 'zE=', 'zD=', 'field=', 'ip=', 'filter-help', 'AAA', 'AA', 'slice=', 'zP=', 'oF=', 'recipe=', 'dump-recipe=', 'req-delay=', 'conn-delay=', 'sc=', 'sh=', 'sl=', 'sw=', 'ss=', 'hc=', 'hh=', 'hl=', 'hw=', 'hs=', 'ntlm=', 'basic=', 'digest=', 'follow', 'script-help=', 'script=', 'script-args=', 'prefilter=', 'filter=', 'interact', 'help', 'version', 'dry-run', 'prev'] +short_opts = "hLAZX:vcb:e:R:D:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:" +long_opts = [ + "efield=", + "no-cache", + "ee=", + "zE=", + "zD=", + "field=", + "ip=", + "filter-help", + "AAA", + "AA", + "slice=", + "zP=", + "oF=", + "recipe=", + "dump-recipe=", + "req-delay=", + "conn-delay=", + "sc=", + "sh=", + "sl=", + "sw=", + "ss=", + "hc=", + "hh=", + "hl=", + "hw=", + "hs=", + "ntlm=", + "basic=", + "digest=", + "follow", + "script-help=", + "script=", + "script-args=", + "prefilter=", + "filter=", + "interact", + "help", + "version", + "dry-run", + "prev", +] +REPEATABLE_OPTS = [ + "--efield", + "--field", + "--prefilter", + "--recipe", + "-z", + "--zP", + "--zD", + "--slice", + "payload", + "-w", + "-b", + "-H", + "-p", +] class CLParser: - def __init__(self, argv): + def __init__( + self, + argv, + short_opts=short_opts, + long_opts=long_opts, + help_banner=help_banner, + brief_usage=brief_usage, + verbose_usage=verbose_usage, + usage=usage, + ): self.argv = argv + self.short_opts = short_opts + self.long_opts = long_opts + self.help_banner = help_banner + self.brief_usage = brief_usage + self.verbose_usage = verbose_usage + self.usage = usage def show_brief_usage(self): - print(help_banner) - print(brief_usage) + print(self.help_banner) + print(self.brief_usage) def show_verbose_usage(self): - print(help_banner) - print(verbose_usage) + print(self.help_banner) + print(self.verbose_usage) def show_usage(self): - print(help_banner) - print(usage) + print(self.help_banner) + print(self.usage) def show_plugins_help(self, registrant, cols=3, category="$all$"): print("\nAvailable %s:\n" % registrant) - table_print([x[cols:] for x in Facade().proxy(registrant).get_plugins_ext(category)]) + table_print( + [x[cols:] for x in Facade().proxy(registrant).get_plugins_ext(category)] + ) sys.exit(0) def show_plugins_names(self, registrant): print("\n".join(Facade().proxy(registrant).get_plugins_names("$all$"))) def show_plugin_ext_help(self, registrant, category="$all$"): - for p in Facade().proxy(registrant).get_plugins(category): - print("Name: %s %s" % (p.name, p.version)) - print("Categories: %s" % ','.join(p.category)) - print("Summary: %s" % p.summary) - print("Author: %s" % ','.join(p.author)) + for plugin in Facade().proxy(registrant).get_plugins(category): + print("Name: %s %s" % (plugin.name, plugin.version)) + print("Categories: %s" % ",".join(plugin.category)) + print("Summary: %s" % plugin.summary) + print("Author: %s" % ",".join(plugin.author)) print("Description:") - for l in p.description: - print(" %s" % l) + for desc_lines in plugin.description: + print(" %s" % desc_lines) print("Parameters:") - for l in p.parameters: - print(" %s %s%s: %s" % ("+" if l[2] else "-", l[0], " (= %s)" % str(l[1]) if l[1] else "", l[3])) + for param in plugin.parameters: + print( + " %s %s%s: %s" + % ( + "+" if param[2] else "-", + param[0], + " (= %s)" % str(param[1]) if param[1] else "", + param[3], + ) + ) print("\n") sys.exit(0) @@ -62,7 +146,7 @@ def show_plugin_ext_help(self, registrant, category="$all$"): def parse_cl(self): # Usage and command line help try: - opts, args = getopt.getopt(self.argv[1:], short_opts, long_opts) + opts, args = getopt.getopt(self.argv[1:], self.short_opts, self.long_opts) optsd = defaultdict(list) payload_cache = {} @@ -97,7 +181,9 @@ def parse_cl(self): cli_url = None if "-u" in optsd: if (url is not None and url != "FUZZ") or url == optsd["-u"][0]: - raise FuzzExceptBadOptions("Specify the URL either with -u or last argument. If you want to use a full payload, it can only be specified with FUZZ.") + raise FuzzExceptBadOptions( + "Specify the URL either with -u or last argument. If you want to use a full payload, it can only be specified with FUZZ." + ) cli_url = optsd["-u"][0] @@ -124,7 +210,7 @@ def parse_cl(self): print(exec_banner) for error_msg in options.validate(): - print("WARNING: {}".format(error_msg)) + warnings.warn("Recipe parsing error: {}".format(error_msg)) print("") @@ -157,11 +243,14 @@ def _parse_help_opt(self, optsd): sys.exit(0) if "--filter-help" in optsd: - text_regex = re.compile("Filter Language\n---------------\n\n(.*?)Filtering results", re.MULTILINE | re.DOTALL) - try: - print(text_regex.search(open(get_path("../docs/user/advanced.rst")).read()).group(1)) - except IOError: - print(text_regex.search(open(get_path("../../docs/user/advanced.rst")).read()).group(1)) + FILTER_HELP_REGEX_EXP = ( + "Filter Language\n---------------\n\n(.*?)Filtering results" + ) + FILTER_HELP_REGEX = re.compile( + FILTER_HELP_REGEX_EXP, re.MULTILINE | re.DOTALL + ) + + print(FILTER_HELP_REGEX.search(get_filter_help_file()).group(1)) sys.exit(0) @@ -185,16 +274,26 @@ def _parse_help_opt(self, optsd): elif "scripts" in optsd["--ee"]: self.show_plugins_names("scripts") elif "fields" in optsd["--ee"]: - print('\n'.join(allowed_fields)) + print("\n".join(allowed_fields)) elif "files" in optsd["--ee"]: - print('\n'.join(Facade().sett.get('general', 'lookup_dirs').split(","))) + print("\n".join(Facade().sett.get("general", "lookup_dirs").split(","))) elif "registrants" in optsd["--ee"]: - print('\n'.join(Facade().get_registrants())) + print("\n".join(Facade().get_registrants())) elif "options" in optsd["--ee"]: - print("\n".join(["-{}".format(opt) for opt in short_opts.replace(":", "")])) - print("\n".join(["--{}".format(opt.replace("=", "")) for opt in long_opts])) + print( + "\n".join( + ["-{}".format(opt) for opt in self.short_opts.replace(":", "")] + ) + ) + print( + "\n".join( + ["--{}".format(opt.replace("=", "")) for opt in self.long_opts] + ) + ) else: - raise FuzzExceptBadOptions("Unknown category. Valid values are: payloads, encoders, iterators, printers or scripts.") + raise FuzzExceptBadOptions( + "Unknown category. Valid values are: payloads, encoders, iterators, printers or scripts." + ) sys.exit(0) if "-e" in optsd: @@ -209,7 +308,9 @@ def _parse_help_opt(self, optsd): elif "scripts" in optsd["-e"]: self.show_plugins_help("scripts", 2) else: - raise FuzzExceptBadOptions("Unknown category. Valid values are: payloads, encoders, iterators, printers or scripts.") + raise FuzzExceptBadOptions( + "Unknown category. Valid values are: payloads, encoders, iterators, printers or scripts." + ) if "-f" in optsd: if "help" in optsd["-f"]: @@ -227,19 +328,28 @@ def _parse_help_opt(self, optsd): def _check_options(self, optsd): # Check for repeated flags - opt_list = [i for i in optsd if i not in ["--recipe", "-z", "--zP", "--zD", "--slice", "payload", "-w", "-b", "-H", "-p"] and len(optsd[i]) > 1] + opt_list = [i for i in optsd if i not in REPEATABLE_OPTS and len(optsd[i]) > 1] if opt_list: - raise FuzzExceptBadOptions("Bad usage: Only one %s option could be specified at the same time." % " ".join(opt_list)) + raise FuzzExceptBadOptions( + "Bad usage: Only one %s option could be specified at the same time." + % " ".join(opt_list) + ) # -A and script not allowed at the same time - if "--script" in list(optsd.keys()) and [key for key in optsd.keys() if key in ['-A', '--AA', '--AAA']]: - raise FuzzExceptBadOptions("Bad usage: --scripts and -A, --AA, --AAA are incompatible options.") + if "--script" in list(optsd.keys()) and [ + key for key in optsd.keys() if key in ["-A", "--AA", "--AAA"] + ]: + raise FuzzExceptBadOptions( + "Bad usage: --scripts and -A, --AA, --AAA are incompatible options." + ) if "-s" in list(optsd.keys()) and "-t" in list(optsd.keys()): - print("WARNING: When using delayed requests concurrent requests are limited to 1, therefore the -s switch will be ignored.") + warnings.warn( + "When using delayed requests concurrent requests are limited to 1, therefore the -s switch will be ignored." + ) def _parse_filters(self, optsd, filter_params): - ''' + """ filter_params = dict( hs = None, hc = [], @@ -254,52 +364,56 @@ def _parse_filters(self, optsd, filter_params): filter = "", prefilter = "", ), - ''' + """ if "--prefilter" in optsd: if not PYPARSING: raise FuzzExceptBadInstall("--prefilter switch needs pyparsing module.") - filter_params['prefilter'] = optsd["--prefilter"][0] + + for prefilter_opt in optsd["--prefilter"]: + filter_params["prefilter"].append(prefilter_opt) if "--filter" in optsd: if not PYPARSING: raise FuzzExceptBadInstall("--filter switch needs pyparsing module.") - filter_params['filter'] = optsd["--filter"][0] + filter_params["filter"] = optsd["--filter"][0] if "--hc" in optsd: - filter_params['hc'] = optsd["--hc"][0].split(",") + filter_params["hc"] = optsd["--hc"][0].split(",") if "--hw" in optsd: - filter_params['hw'] = optsd["--hw"][0].split(",") + filter_params["hw"] = optsd["--hw"][0].split(",") if "--hl" in optsd: - filter_params['hl'] = optsd["--hl"][0].split(",") + filter_params["hl"] = optsd["--hl"][0].split(",") if "--hh" in optsd: - filter_params['hh'] = optsd["--hh"][0].split(",") + filter_params["hh"] = optsd["--hh"][0].split(",") if "--hs" in optsd: - filter_params['hs'] = optsd["--hs"][0] + filter_params["hs"] = optsd["--hs"][0] if "--sc" in optsd: - filter_params['sc'] = optsd["--sc"][0].split(",") + filter_params["sc"] = optsd["--sc"][0].split(",") if "--sw" in optsd: - filter_params['sw'] = optsd["--sw"][0].split(",") + filter_params["sw"] = optsd["--sw"][0].split(",") if "--sl" in optsd: - filter_params['sl'] = optsd["--sl"][0].split(",") + filter_params["sl"] = optsd["--sl"][0].split(",") if "--sh" in optsd: - filter_params['sh'] = optsd["--sh"][0].split(",") + filter_params["sh"] = optsd["--sh"][0].split(",") if "--ss" in optsd: - filter_params['ss'] = optsd["--ss"][0] + filter_params["ss"] = optsd["--ss"][0] def _parse_payload(self, optsd, options): - ''' + """ options = dict( payloads = [], iterator = None, ) - ''' + """ payloads_list = [] for payload in optsd["payload"]: if "-z" not in payload and "-w" not in payload: - raise FuzzExceptBadOptions("--zP and --slice must be preceded by a -z or -w switch.") + raise FuzzExceptBadOptions( + "--zP and --slice must be preceded by a -z or -w switch." + ) zpayl = payload["-z"] if "-z" in payload else "file,%s" % payload["-w"] extraparams = payload["--zP"] if "--zP" in payload else None @@ -324,7 +438,7 @@ def _parse_payload(self, optsd, options): if extraparams: params = dict([x.split("=", 1) for x in extraparams.split(",")]) if default_param: - params['default'] = default_param + params["default"] = default_param encoders = vals[2] if len(vals) == 3 else None encoders_cli = payload["--zE"] if "--zE" in payload else None @@ -334,58 +448,47 @@ def _parse_payload(self, optsd, options): encoders = encoders_cli if encoders: - params['encoder'] = encoders.split("-") + params["encoder"] = encoders.split("-") elif "encoder" in params: - params['encoder'] = params['encoder'].split("-") + params["encoder"] = params["encoder"].split("-") else: - params['encoder'] = None + params["encoder"] = None payloads_list.append((name, params, sliceit)) if "-m" in optsd: - options["iterator"] = optsd['-m'][0] + options["iterator"] = optsd["-m"][0] if payloads_list: options["payloads"] = payloads_list def _parse_seed(self, url, optsd, options): - ''' - options = dict( - url = url, - method = None, - auth = (None, None), - follow = False, - head = False, - postdata = None, - headers = [(header, value)], - cookie = [], - allvars = None, - ) - ''' - if url: - options['url'] = url + options["url"] = url if "-X" in optsd: - options['method'] = optsd["-X"][0] + options["method"] = optsd["-X"][0] if "--basic" in optsd: - options['auth'] = ("basic", optsd["--basic"][0]) + options["auth"] = {"method": "basic", "credentials": optsd["--basic"][0]} if "--digest" in optsd: - options['auth'] = ("digest", optsd["--digest"][0]) + options["auth"] = {"method": "digest", "credentials": optsd["--digest"][0]} if "--ntlm" in optsd: - options['auth'] = ("ntlm", optsd["--ntlm"][0]) + options["auth"] = {"method": "ntlm", "credentials": optsd["--ntlm"][0]} if "--follow" in optsd or "-L" in optsd: - options['follow'] = True + options["follow"] = True if "--field" in optsd: - options['description'] = optsd["--field"][0] + for field in optsd["--field"]: + options["fields"].append(field) options["show_field"] = True elif "--efield" in optsd: - options['description'] = optsd["--efield"][0] + for field in optsd["--efield"]: + options["fields"].append(field) + options["show_field"] = False else: options["show_field"] = None @@ -397,37 +500,27 @@ def _parse_seed(self, url, optsd, options): options["connect_to_ip"] = { "ip": splitted[0], - "port": splitted[2] if splitted[2] else "80" + "port": splitted[2] if splitted[2] else "80", } if "-d" in optsd: - options['postdata'] = optsd["-d"][0] + options["postdata"] = optsd["-d"][0] for bb in optsd["-b"]: - options['cookie'].append(bb) + options["cookie"].append(bb) for x in optsd["-H"]: splitted = x.partition(":") if splitted[1] != ":": - raise FuzzExceptBadOptions("Wrong header specified, it should be in the format \"name: value\".") - options['headers'].append((splitted[0], splitted[2].strip())) + raise FuzzExceptBadOptions( + 'Wrong header specified, it should be in the format "name: value".' + ) + options["headers"].append((splitted[0], splitted[2].strip())) if "-V" in optsd: - options['allvars'] = str(optsd["-V"][0]) + options["allvars"] = str(optsd["-V"][0]) def _parse_conn_options(self, optsd, conn_options): - ''' - conn_options = dict( - proxies = None, - conn_delay = 90, - req_delay = None, - rlevel = 0, - scanmode = False, - delay = None, - concurrent = 10, - ) - ''' - if "-p" in optsd: proxy = [] @@ -441,10 +534,10 @@ def _parse_conn_options(self, optsd, conn_options): else: raise FuzzExceptBadOptions("Bad proxy parameter specified.") - conn_options['proxies'] = proxy + conn_options["proxies"] = proxy if "--conn-delay" in optsd: - conn_options['conn_delay'] = int(optsd["--conn-delay"][0]) + conn_options["conn_delay"] = int(optsd["--conn-delay"][0]) if "--req-delay" in optsd: conn_options["req_delay"] = int(optsd["--req-delay"][0]) @@ -452,6 +545,9 @@ def _parse_conn_options(self, optsd, conn_options): if "-R" in optsd: conn_options["rlevel"] = int(optsd["-R"][0]) + if "-D" in optsd: + conn_options["dlevel"] = int(optsd["-D"][0]) + if "-Z" in optsd: conn_options["scanmode"] = True @@ -462,18 +558,8 @@ def _parse_conn_options(self, optsd, conn_options): conn_options["concurrent"] = int(optsd["-t"][0]) def _parse_options(self, optsd, options): - ''' - options = dict( - printer = (None,None), - colour = False, - interactive = False, - dryrun = False, - recipe = "", - ) - ''' - if "--oF" in optsd: - options["save"] = optsd['--oF'][0] + options["save"] = optsd["--oF"][0] if "-v" in optsd: options["verbose"] = True @@ -487,12 +573,12 @@ def _parse_options(self, optsd, options): if "-c" in optsd: options["colour"] = True - if [key for key in optsd.keys() if key in ['-A', '--AA', '--AAA']]: + if [key for key in optsd.keys() if key in ["-A", "--AA", "--AAA"]]: options["verbose"] = True options["colour"] = True if "-f" in optsd: - vals = optsd['-f'][0].split(",", 1) + vals = optsd["-f"][0].split(",", 1) if len(vals) == 1: options["printer"] = (vals[0], None) @@ -500,24 +586,24 @@ def _parse_options(self, optsd, options): options["printer"] = vals if "-o" in optsd: - options["console_printer"] = optsd['-o'][0] + options["console_printer"] = optsd["-o"][0] if "--recipe" in optsd: - options["recipe"] = optsd['--recipe'][0] + options["recipe"] = optsd["--recipe"] if "--dry-run" in optsd: - options["dryrun"] = True + options["transport"] = "dryrun" if "--interact" in optsd: options["interactive"] = True def _parse_scripts(self, optsd, options): - ''' + """ options = dict( script = "", script_args = {}, ) - ''' + """ if "-A" in optsd: options["script"] = "default" @@ -529,10 +615,16 @@ def _parse_scripts(self, optsd, options): options["script"] = "default,discovery,verbose" if "--script" in optsd: - options["script"] = "default" if optsd["--script"][0] == "" else optsd["--script"][0] + options["script"] = ( + "default" if optsd["--script"][0] == "" else optsd["--script"][0] + ) if "--script-args" in optsd: try: - options['script_args'] = dict([x.split("=", 1) for x in optsd["--script-args"][0].split(",")]) + options["script_args"] = dict( + [x.split("=", 1) for x in optsd["--script-args"][0].split(",")] + ) except ValueError: - raise FuzzExceptBadOptions("Script arguments: Incorrect arguments format supplied.") + raise FuzzExceptBadOptions( + "Script arguments: Incorrect arguments format supplied." + ) diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index 6eae245b..cd609268 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -1,20 +1,24 @@ import sys from wfuzz import __version__ as version import os + if os.name == "nt": import colorama + colorama.init() -examples_banner = '''Examples:\n\twfuzz -c -z file,users.txt -z file,pass.txt --sc 200 http://www.site.com/log.asp?user=FUZZ&pass=FUZ2Z +examples_banner = """Examples:\n\twfuzz -c -z file,users.txt -z file,pass.txt --sc 200 http://www.site.com/log.asp?user=FUZZ&pass=FUZ2Z \twfuzz -c -z range,1-10 --hc=BBB http://www.site.com/FUZZ{something not there} -\twfuzz --script=robots -z list,robots.txt http://www.webscantest.com/FUZZ''' +\twfuzz --script=robots -z list,robots.txt http://www.webscantest.com/FUZZ""" -exec_banner = '''********************************************************\r +exec_banner = """********************************************************\r * Wfuzz {version} - The Web Fuzzer {align: <{width1}}*\r -********************************************************\r\n'''.format(version=version, align=' ', width1=29 - len(version)) +********************************************************\r\n""".format( + version=version, align=" ", width1=29 - len(version) +) -help_banner = '''******************************************************** +help_banner = """******************************************************** * Wfuzz {version} - The Web Fuzzer {align: <{width1}}* * * * Version up to 1.4c coded by: * @@ -23,24 +27,34 @@ * * * Version 1.4d to {version} coded by: {align: <{width2}}* * Xavier Mendez (xmendez@edge-security.com) * -********************************************************\r\n'''.format(version=version, width1=29 - len(version), align=' ', width2=26 - len(version)) +********************************************************\r\n""".format( + version=version, width1=29 - len(version), align=" ", width2=26 - len(version) +) -help_banner2 = '''******************************************************** +help_banner2 = """******************************************************** * Wfuzz {version} - The Web Fuzzer {align: <{width1}}* * * * Coded by: * * * * Xavier Mendez (xmendez@edge-security.com) * -********************************************************\r\n'''.format(version=version, align=' ', width1=29 - len(version)) +********************************************************\r\n""".format( + version=version, align=" ", width1=29 - len(version) +) + +header_usage_wfpayload = """Usage:\twfpayload [options] -z payload --zD params\r\n +""" -header_usage = '''Usage:\twfuzz [options] -z payload,params <url>\r\n +header_usage = """Usage:\twfuzz [options] -z payload,params <url>\r\n \tFUZZ, ..., FUZnZ wherever you put these keywords wfuzz will replace them with the values of the specified payload. \tFUZZ{baseline_value} FUZZ will be replaced by baseline_value. It will be the first request performed and could be used as a base for filtering. -''' +""" -brief_usage = '''%s\n\n%s\n\nType wfuzz -h for further information or --help for advanced usage.''' % (header_usage, examples_banner) +brief_usage = ( + """%s\n\n%s\n\nType wfuzz -h for further information or --help for advanced usage.""" + % (header_usage, examples_banner) +) -usage = '''%s\n\nOptions: +usage = """%s\n\nOptions: \t-h : This help \t--help : Advanced help \t--version : Wfuzz version details @@ -55,7 +69,8 @@ \t \t-t N : Specify the number of concurrent connections (10 default) \t-s N : Specify time delay between requests (0 default) -\t-R depth : Recursive path discovery being depth the maximum recursion level. +\t-R depth : Recursive path discovery being depth the maximum recursion level (0 default) +\t-D depth : Maximum link depth level (4 default) \t-L, --follow : Follow HTTP redirections \t \t-u url : Specify a URL for the request. @@ -75,9 +90,11 @@ \t--hc/hl/hw/hh N[,N]+ : Hide responses with the specified code/lines/words/chars (Use BBB for taking values from baseline) \t--sc/sl/sw/sh N[,N]+ : Show responses with the specified code/lines/words/chars (Use BBB for taking values from baseline) \t--ss/hs regex : Show/Hide responses with the specified regex within the content -''' % (header_usage) +""" % ( + header_usage +) -verbose_usage = '''%s\n\nOptions: +verbose_usage = """%s\n\nOptions: \t-h/--help : This help \t--help : Advanced help \t--filter-help : Filter language specification @@ -95,8 +112,8 @@ \t--interact : (beta) If selected,all key presses are captured. This allows you to interact with the program. \t--dry-run : Print the results of applying the requests without actually making any HTTP request. \t--prev : Print the previous HTTP requests (only when using payloads generating fuzzresults) -\t--efield <expr> : Show the specified language expression together with the current payload -\t--field <expr> : Do not show the payload but only the specified language expression +\t--efield <expr> : Show the specified language expression together with the current payload. Repeat for various fields. +\t--field <expr> : Do not show the payload but only the specified language expression. Repeat for various fields. \t \t-p addr : Use Proxy in format ip:port:type. Repeat option for using various proxies. \t Where type could be SOCKS4,SOCKS5 or HTTP if omitted. @@ -104,6 +121,7 @@ \t-t N : Specify the number of concurrent connections (10 default) \t-s N : Specify time delay between requests (0 default) \t-R depth : Recursive path discovery being depth the maximum recursion level. +\t-D depth : Maximum link depth level. \t-L,--follow : Follow HTTP redirections \t--ip host:port : Specify an IP to connect to instead of the URL's host in the format ip:port \t-Z : Scan mode (Connection errors will be ignored). @@ -140,8 +158,52 @@ \t--sc/sl/sw/sh N[,N]+ : Show responses with the specified code/lines/words/chars (Use BBB for taking values from baseline) \t--ss/hs regex : Show/hide responses with the specified regex within the content \t--filter <filter> : Show/hide responses using the specified filter expression (Use BBB for taking values from baseline) -\t--prefilter <filter> : Filter items before fuzzing using the specified expression. -''' % (header_usage) +\t--prefilter <filter> : Filter items before fuzzing using the specified expression. Repeat for concatenating filters. +""" % ( + header_usage +) + + +wfpayload_usage = """%s\n\nOptions: +\t-h/--help : This help +\t--help : Advanced help +\t--version : Wfuzz version details +\t-e <type> : List of available encoders/payloads/iterators/printers/scripts +\t +\t--recipe <filename> : Reads options from a recipe. Repeat for various recipes. +\t--dump-recipe <filename> : Prints current options as a recipe +\t--oF <filename> : Saves fuzz results to a file. These can be consumed later using the wfuzz payload. +\t +\t-c : Output with colors +\t-v : Verbose information. +\t-f filename,printer : Store results in the output file using the specified printer (raw printer if omitted). +\t-o printer : Show results using the specified printer. +\t--efield <expr> : Show the specified language expression together with the current payload. Repeat option for various fields. +\t--field <expr> : Do not show the payload but only the specified language expression. Repeat option for various fields. +\t +\t--script= : Equivalent to --script=default +\t--script=<plugins> : Runs script's scan. <plugins> is a comma separated list of plugin-files or plugin-categories +\t--script-help=<plugins> : Show help about scripts. +\t--script-args n1=v1,... : Provide arguments to scripts. ie. --script-args grep.regex=\"<A href=\\\"(.*?)\\\">\" +\t +\t-z payload : Specify a payload for each FUZZ keyword used in the form of name[,parameter][,encoder]. +\t A list of encoders can be used, ie. md5-sha1. Encoders can be chained, ie. md5@sha1. +\t Encoders category can be used. ie. url +\t Use help as a payload to show payload plugin's details (you can filter using --slice) +\t--zP <params> : Arguments for the specified payload (it must be preceded by -z or -w). +\t--zD <default> : Default parameter for the specified payload (it must be preceded by -z or -w). +\t--zE <encoder> : Encoder for the specified payload (it must be preceded by -z or -w). +\t--slice <filter> : Filter payload\'s elements using the specified expression. It must be preceded by -z. +\t-w wordlist : Specify a wordlist file (alias for -z file,wordlist). +\t +\t--hc/hl/hw/hh N[,N]+ : Hide responses with the specified code/lines/words/chars (Use BBB for taking values from baseline) +\t--sc/sl/sw/sh N[,N]+ : Show responses with the specified code/lines/words/chars (Use BBB for taking values from baseline) +\t--ss/hs regex : Show/hide responses with the specified regex within the content +\t--filter <filter> : Show/hide responses using the specified filter expression (Use BBB for taking values from baseline) +\t--prefilter <filter> : Filter items before fuzzing using the specified expression. Repeat for concatenating filters. +""" % ( + header_usage_wfpayload +) class Term: @@ -205,9 +267,8 @@ def go_up(self, lines): sys.stdout.write("\033[" + str(lines) + "A") def erase_lines(self, lines): - if lines <= 1: + for i in range(lines - 1): sys.stdout.write("\r" + Term.delete) - else: - for i in range(lines - 1): - sys.stdout.write("\r" + Term.delete) - sys.stdout.write(Term.oneup) + sys.stdout.write(Term.oneup) + + sys.stdout.write("\r" + Term.delete) diff --git a/src/wfuzz/ui/console/getch.py b/src/wfuzz/ui/console/getch.py index 5807b913..baa5a26f 100644 --- a/src/wfuzz/ui/console/getch.py +++ b/src/wfuzz/ui/console/getch.py @@ -9,6 +9,7 @@ class _Getch: """Gets a single character from standard input. Does not echo to the screen.""" + def __init__(self): try: self.impl = _GetchWindows() @@ -57,15 +58,18 @@ class _GetchMacCarbon: page http://www.mactech.com/macintosh-c/chap02-1.html was very helpful in figuring out how to do this. """ + def __init__(self): import Carbon + # see if it has this (in Unix, it doesn't) Carbon.Evt def __call__(self): import Carbon + if Carbon.Evt.EventAvail(0x0008)[0] == 0: # 0x0008 is the keyDownMask - return '' + return "" else: # # The event contains the following info: @@ -80,13 +84,14 @@ def __call__(self): return chr(msg & 0x000000FF) -if __name__ == '__main__': - print('Press a key') +if __name__ == "__main__": + print("Press a key") inkey = _Getch() import sys + for i in range(sys.maxsize): k = inkey() - if k != '': + if k != "": break - print('you pressed ', k) + print("you pressed ", k) diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index dac7070e..a0fcb805 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -1,25 +1,26 @@ import sys from collections import defaultdict import threading + try: from itertools import zip_longest except ImportError: from itertools import izip_longest as zip_longest -from wfuzz.fuzzobjects import FuzzResult +from wfuzz.fuzzobjects import FuzzWordType, FuzzType from .common import exec_banner, Term from .getch import _Getch -from .output import getTerminalSize, wrap_always +from .output import getTerminalSize, wrap_always_list -usage = '''\r\n +usage = """\r\n Interactive keyboard commands:\r\n ?: Show this help p: Pause s: Show stats q: Cancel -''' +""" class SimpleEventDispatcher: @@ -31,13 +32,13 @@ def create_event(self, msg): def subscribe(self, func, msg, dynamic=False): if msg not in self.publisher and not dynamic: - raise KeyError('subscribe. No such event: %s' % (msg)) + raise KeyError("subscribe. No such event: %s" % (msg)) else: self.publisher[msg].append(func) def notify(self, msg, **event): if msg not in self.publisher: - raise KeyError('notify. Event not subscribed: %s' % (msg,)) + raise KeyError("notify. Event not subscribed: %s" % (msg,)) else: for functor in self.publisher[msg]: functor(**event) @@ -65,13 +66,13 @@ def run(self): k = self.inkey() if k and ord(k) == 3: self.dispatcher.notify("q", key="q") - elif k == 'p': + elif k == "p": self.dispatcher.notify("p", key="p") - elif k == 's': + elif k == "s": self.dispatcher.notify("s", key="s") - elif k == '?': + elif k == "?": self.dispatcher.notify("?", key="?") - elif k == 'q': + elif k == "q": self.dispatcher.notify("q", key="q") @@ -118,18 +119,30 @@ def on_stats(self, **event): print("%s: %s" % (k, v)) print("\n=========================================") else: - pending = self.fuzzer.genReq.stats.total_req - self.fuzzer.genReq.stats.processed() + pending = ( + self.fuzzer.genReq.stats.total_req + - self.fuzzer.genReq.stats.processed() + ) summary = self.fuzzer.genReq.stats summary.mark_end() print("\nTotal requests: %s\r" % str(summary.total_req)) print("Pending requests: %s\r" % str(pending)) if summary.backfeed() > 0: - print("Processed Requests: %s (%d + %d)\r" % (str(summary.processed())[:8], (summary.processed() - summary.backfeed()), summary.backfeed())) + print( + "Processed Requests: %s (%d + %d)\r" + % ( + str(summary.processed())[:8], + (summary.processed() - summary.backfeed()), + summary.backfeed(), + ) + ) else: print("Processed Requests: %s\r" % (str(summary.processed())[:8])) print("Filtered Requests: %s\r" % (str(summary.filtered())[:8])) - req_sec = summary.processed() / summary.totaltime if summary.totaltime > 0 else 0 + req_sec = ( + summary.processed() / summary.totaltime if summary.totaltime > 0 else 0 + ) print("Total time: %s\r" % str(summary.totaltime)[:8]) if req_sec > 0: print("Requests/sec.: %s\r" % str(req_sec)[:8]) @@ -149,33 +162,38 @@ def __init__(self, session_options): self.verbose = session_options["verbose"] self.previous = session_options["previous"] self.term = Term() - self.printed_lines = 1 + self.printed_lines = 0 def _print_verbose(self, res, print_nres=True): - txt_colour = Term.noColour if not res.is_baseline or not self.colour else Term.fgCyan + txt_colour = ( + Term.noColour if not res.is_baseline or not self.colour else Term.fgCyan + ) if self.previous and self.colour and not print_nres: txt_colour = Term.fgCyan location = "" - if 'Location' in res.history.headers.response: - location = res.history.headers.response['Location'] + if "Location" in res.history.headers.response: + location = res.history.headers.response["Location"] elif res.history.url != res.history.redirect_url: location = "(*) %s" % res.history.url server = "" - if 'Server' in res.history.headers.response: - server = res.history.headers.response['Server'] + if "Server" in res.history.headers.response: + server = res.history.headers.response["Server"] rows = [ ("%09d:" % res.nres if print_nres else " |_", txt_colour), ("%.3fs" % res.timer, txt_colour), - ("%s" % "XXX" if res.exception else str(res.code), self.term.get_colour(res.code) if self.colour else txt_colour), + ( + "%s" % "XXX" if res.exception else str(res.code), + self.term.get_colour(res.code) if self.colour else txt_colour, + ), ("%d L" % res.lines, txt_colour), ("%d W" % res.words, txt_colour), ("%d Ch" % res.chars, txt_colour), (server, txt_colour), (location, txt_colour), - ("\"%s\"" % res.description, txt_colour), + ('"%s"' % res.description, txt_colour), ] self.term.set_colour(txt_colour) @@ -184,39 +202,54 @@ def _print_verbose(self, res, print_nres=True): def _print_header(self, rows, maxWidths): print("=" * (3 * len(maxWidths) + sum(maxWidths[:-1]) + 10)) self._print_line(rows, maxWidths) - sys.stdout.write("\n\r") print("=" * (3 * len(maxWidths) + sum(maxWidths[:-1]) + 10)) print("") def _print_line(self, rows, maxWidths): def wrap_row(rows, maxWidths): - newRows = [wrap_always(item[0], width).split('\n') for item, width in zip(rows, maxWidths)] - return [[substr or '' for substr in item] for item in zip_longest(*newRows)] + newRows = [ + wrap_always_list(item[0], width) for item, width in zip(rows, maxWidths) + ] + return [[substr or "" for substr in item] for item in zip_longest(*newRows)] + + def print_row(row, rows): + sys.stdout.write( + " ".join( + [ + colour + str.ljust(str(item), width) + Term.reset + for (item, width, colour) in zip( + row, maxWidths, [colour[1] for colour in rows] + ) + ] + ) + ) new_rows = wrap_row(rows, maxWidths) - for row in new_rows[:-1]: - sys.stdout.write(" ".join([colour + str.ljust(str(item), width) + Term.reset for (item, width, colour) in zip(row, maxWidths, [colour[1] for colour in rows])])) + for row in new_rows: + print_row(row, rows) sys.stdout.write("\n\r") - for row in new_rows[-1:]: - sys.stdout.write(" ".join([colour + str.ljust(str(item), width) + Term.reset for (item, width, colour) in zip(row, maxWidths, [colour[1] for colour in rows])])) - sys.stdout.flush() return len(new_rows) def _print(self, res, print_nres=True): - txt_colour = Term.noColour if not res.is_baseline or not self.colour else Term.fgCyan + txt_colour = ( + Term.noColour if not res.is_baseline or not self.colour else Term.fgCyan + ) if self.previous and self.colour and not print_nres: txt_colour = Term.fgCyan rows = [ ("%09d:" % res.nres if print_nres else " |_", txt_colour), - ("%s" % "XXX" if res.exception else str(res.code), self.term.get_colour(res.code) if self.colour else txt_colour), + ( + "%s" % "XXX" if res.exception else str(res.code), + self.term.get_colour(res.code) if self.colour else txt_colour, + ), ("%d L" % res.lines, txt_colour), ("%d W" % res.words, txt_colour), ("%d Ch" % res.chars, txt_colour), - ("\"%s\"" % res.description, txt_colour), + ('"%s"' % res.description, txt_colour), ] self.term.set_colour(txt_colour) @@ -260,37 +293,34 @@ def header(self, summary): self._print_header(rows, widths) def result(self, res): - self.term.erase_lines(self.printed_lines) - if self.verbose: self._print_verbose(res) else: self._print(res) - if res.type == FuzzResult.result: - if self.previous and len(res.payload) > 0 and isinstance(res.payload[0].content, FuzzResult): - sys.stdout.write("\n\r") + if res.item_type == FuzzType.RESULT: + if ( + self.previous + and res.payload_man + and res.payload_man.get_payload_type(1) == FuzzWordType.FUZZRES + ): + prev_res = res.payload_man.get_payload_content(1) if self.verbose: - self._print_verbose(res.payload[0].content, print_nres=False) + self._print_verbose(prev_res, print_nres=False) else: - self._print(res.payload[0].content, print_nres=False) + self._print(prev_res, print_nres=False) if res.plugins_res: - sys.stdout.write("\n\r") - - for i in res.plugins_res[:-1]: + for i in res.plugins_res: sys.stdout.write(" |_ %s\r" % i.issue) sys.stdout.write("\n\r") - for i in res.plugins_res[-1:]: - sys.stdout.write(" |_ %s\r" % i.issue) + self.printed_lines = 0 - for i in range(self.printed_lines): - sys.stdout.write("\n\r") + if self.printed_lines > 0: + self.term.erase_lines(self.printed_lines + 1) def footer(self, summary): - self.term.erase_lines(self.printed_lines + 1) - sys.stdout.write("\n\r") sys.stdout.write("\n\r") print(summary) diff --git a/src/wfuzz/ui/console/output.py b/src/wfuzz/ui/console/output.py index 9abbbb18..74e2b91e 100644 --- a/src/wfuzz/ui/console/output.py +++ b/src/wfuzz/ui/console/output.py @@ -2,18 +2,30 @@ from __future__ import print_function import math +import string import operator from functools import reduce # Python 2 and 3: zip_longest from six import StringIO + try: from itertools import zip_longest except ImportError: from itertools import izip_longest as zip_longest -def indent(rows, hasHeader=False, headerChar='-', delim=' | ', justify='left', separateRows=False, prefix='', postfix='', wrapfunc=lambda x: x): +def indent( + rows, + hasHeader=False, + headerChar="-", + delim=" | ", + justify="left", + separateRows=False, + prefix="", + postfix="", + wrapfunc=lambda x: x, +): """ @author http://code.activestate.com/recipes/267662-table-indentation/ @@ -33,23 +45,35 @@ def indent(rows, hasHeader=False, headerChar='-', delim=' | ', justify='left', s the table is first wrapped by this function.""" # closure for breaking logical rows to physical, using wrapfunc def rowWrapper(row): - newRows = [wrapfunc(item).split('\n') for item in row] - return [[substr or '' for substr in item] for item in zip_longest(*newRows)] + newRows = [wrapfunc(item).split("\n") for item in row] + return [[substr or "" for substr in item] for item in zip_longest(*newRows)] + # break each logical row into one or more physical ones logicalRows = [rowWrapper(row) for row in rows] # columns of physical rows columns = zip_longest(*reduce(operator.add, logicalRows)) # get the maximum of each column by the string length of its items maxWidths = [max([len(str(item)) for item in column]) for column in columns] - rowSeparator = headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + len(delim) * (len(maxWidths) - 1)) + rowSeparator = headerChar * ( + len(prefix) + len(postfix) + sum(maxWidths) + len(delim) * (len(maxWidths) - 1) + ) # select the appropriate justify method - justify = {'center': str.center, 'right': str.rjust, 'left': str.ljust}[justify.lower()] + justify = {"center": str.center, "right": str.rjust, "left": str.ljust}[ + justify.lower() + ] output = StringIO() if separateRows: print(rowSeparator, file=output) for physicalRows in logicalRows: for row in physicalRows: - print(prefix + delim.join([justify(str(item), width) for (item, width) in zip(row, maxWidths)]) + postfix, file=output) + print( + prefix + + delim.join( + [justify(str(item), width) for (item, width) in zip(row, maxWidths)] + ) + + postfix, + file=output, + ) if separateRows or hasHeader: print(rowSeparator, file=output) hasHeader = False @@ -59,28 +83,60 @@ def rowWrapper(row): def wrap_always(text, width): """A simple word-wrap function that wraps text on exactly width characters. It doesn't split the text in words.""" - return '\n'.join([text[width * i:width * (i + 1)] for i in range(int(math.ceil(1. * len(text) / width)))]) + return "\n".join( + [ + text[width * i : width * (i + 1)] + for i in range(int(math.ceil(1.0 * len(text) / width))) + ] + ) + + +def wrap_always_list(alltext, width): + text_list = [] + for text in alltext.splitlines(): + for subtext in [ + text[width * i : width * (i + 1)] + for i in range(int(math.ceil(1.0 * len(text) / width))) + ]: + text_list.append( + "".join([char if char in string.printable else "." for char in subtext]) + ) + return text_list def table_print(rows, width=80): - print(indent(rows, hasHeader=True, separateRows=False, prefix=' ', postfix=' ', wrapfunc=lambda x: wrap_always(x, width))) + print( + indent( + rows, + hasHeader=True, + separateRows=False, + prefix=" ", + postfix=" ", + wrapfunc=lambda x: wrap_always(x, width), + ) + ) def getTerminalSize(): # http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python import platform + current_os = platform.system() tuple_xy = None - if current_os == 'Windows': + if current_os == "Windows": tuple_xy = _getTerminalSize_windows() if tuple_xy is None: tuple_xy = _getTerminalSize_tput() # needed for window's python in cygwin's xterm! - if current_os == 'Linux' or current_os == 'Darwin' or current_os.startswith('CYGWIN'): + if ( + current_os == "Linux" + or current_os == "Darwin" + or current_os.startswith("CYGWIN") + ): tuple_xy = _getTerminalSize_linux() if tuple_xy is None: print("default") - tuple_xy = (80, 25) # default value + tuple_xy = (80, 25) # default value return tuple_xy @@ -101,8 +157,20 @@ def _getTerminalSize_windows(): return None if res: import struct - (bufx, bufy, curx, cury, wattr, - left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) + + ( + bufx, + bufy, + curx, + cury, + wattr, + left, + top, + right, + bottom, + maxx, + maxy, + ) = struct.unpack("hhhhHhhhhhh", csbi.raw) sizex = right - left + 1 sizey = bottom - top + 1 return sizex, sizey @@ -115,10 +183,15 @@ def _getTerminalSize_tput(): # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window try: import subprocess - proc = subprocess.Popen(["tput", "cols"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + proc = subprocess.Popen( + ["tput", "cols"], stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) output = proc.communicate(input=None) cols = int(output[0]) - proc = subprocess.Popen(["tput", "lines"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + proc = subprocess.Popen( + ["tput", "lines"], stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) output = proc.communicate(input=None) rows = int(output[0]) return (cols, rows) @@ -134,10 +207,11 @@ def _getTerminalSize_linux(): def ioctl_GWINSZ(fd): try: - cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) except Exception: return None return cr + cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not cr: try: @@ -148,7 +222,7 @@ def ioctl_GWINSZ(fd): pass if not cr: try: - cr = (os.environ.get('LINES'), os.environ.get('COLUMNS')) + cr = (os.environ.get("LINES"), os.environ.get("COLUMNS")) except Exception: return None if not cr[0]: diff --git a/src/wfuzz/ui/gui/controller.py b/src/wfuzz/ui/gui/controller.py index 3333e606..db8baef3 100644 --- a/src/wfuzz/ui/gui/controller.py +++ b/src/wfuzz/ui/gui/controller.py @@ -29,6 +29,7 @@ def onecmd(self, cmd): def do_wfilter(self, cmd): from wfuzz.core import dictionary + try: session_options = CLParser(cmd).parse_cl() except SystemExit: @@ -40,7 +41,7 @@ def do_wfilter(self, cmd): for res in dictionary.from_options(session_options): r = res[0] if "FuzzResult" in str(r.__class__): - r._description = r.url + r._fields = r.url self.model.AddRow(r) @@ -62,7 +63,9 @@ def do_delete(self, cmd): def do_tab(self, cmd): data = Facade().data[cmd[1]] = [] model = GUIModel(data) - pub.sendMessage("create_tab", name=cmd[1], model=model, interp=WfuzzInterpreter(model)) + pub.sendMessage( + "create_tab", name=cmd[1], model=model, interp=WfuzzInterpreter(model) + ) class GUIController: diff --git a/src/wfuzz/ui/gui/guicontrols.py b/src/wfuzz/ui/gui/guicontrols.py index 4e10e42c..2d76a41e 100644 --- a/src/wfuzz/ui/gui/guicontrols.py +++ b/src/wfuzz/ui/gui/guicontrols.py @@ -43,7 +43,13 @@ def __init__(self, parent, interpreter): self.index = 0 self.prompt = ">>" - self.textctrl = wx.TextCtrl(self, -1, '', style=wx.TE_PROCESS_ENTER | wx.TE_MULTILINE | wx.TE_RICH, size=(-1, 250)) + self.textctrl = wx.TextCtrl( + self, + -1, + "", + style=wx.TE_PROCESS_ENTER | wx.TE_MULTILINE | wx.TE_RICH, + size=(-1, 250), + ) self.textctrl.SetForegroundColour(wx.WHITE) self.textctrl.SetBackgroundColour(wx.BLACK) @@ -73,7 +79,7 @@ def __bind_events(self, e): if e.GetKeyCode() == 13: self.index = len(self.history) - 1 - self.value = (self.textctrl.GetValue()) + self.value = self.textctrl.GetValue() ln = self.get_last_line() ln = ln.strip() @@ -82,6 +88,7 @@ def __bind_events(self, e): self.index += 1 if ln: import shlex + cmd = shlex.split(ln) # out en retvalue retvalue = self._interp.onecmd(cmd) @@ -118,10 +125,11 @@ def __bind_events(self, e): def get_last_line(self): nl = self.textctrl.GetNumberOfLines() ln = self.textctrl.GetLineText(nl - 1) - ln = ln[len(self.prompt):] + ln = ln[len(self.prompt) :] return ln + # ---------------------------------------------------------------------- @@ -131,7 +139,9 @@ def __init__(self, parent, log, model, interpreter): self._interp = interpreter wx.Panel.__init__(self, parent, -1) - self.dvc = dv.DataViewCtrl(self, style=wx.BORDER_THEME | dv.DV_ROW_LINES | dv.DV_VERT_RULES) + self.dvc = dv.DataViewCtrl( + self, style=wx.BORDER_THEME | dv.DV_ROW_LINES | dv.DV_VERT_RULES + ) self.model = model self.dvc.AssociateModel(self.model) @@ -143,7 +153,9 @@ def __init__(self, parent, log, model, interpreter): c.Sortable = True c.Reorderable = True - self.cp = cp = PCP.PyCollapsiblePane(self, label="Show console", agwStyle=wx.CP_GTK_EXPANDER) + self.cp = cp = PCP.PyCollapsiblePane( + self, label="Show console", agwStyle=wx.CP_GTK_EXPANDER + ) self.MakePaneContent(cp.GetPane()) self.Sizer = wx.BoxSizer(wx.VERTICAL) @@ -181,7 +193,9 @@ def __init__(self, parent, frame): # self.req_txt = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE|wx.TE_READONLY) self.req_txt = webview.WebView.New(self) # self.resp_txt = webview.WebView.New(self) - self.resp_txt = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE | wx.TE_READONLY) + self.resp_txt = wx.TextCtrl( + self, -1, "", style=wx.TE_MULTILINE | wx.TE_READONLY + ) sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -231,7 +245,9 @@ def on_selected_row(self, row): from pygments.lexers import get_lexer_by_name from pygments.formatters import HtmlFormatter - result = highlight(str(row.history), get_lexer_by_name("http"), HtmlFormatter(full=True)) + result = highlight( + str(row.history), get_lexer_by_name("http"), HtmlFormatter(full=True) + ) # result2 = highlight(str(row.history.raw_content), get_lexer_by_name("http"), HtmlFormatter(full=True)) self.renderpanel.SetPage(row.history.content, row.url) @@ -240,6 +256,7 @@ def on_selected_row(self, row): # self.rawpanel.resp_txt.SetPage(result2, "") self.rawpanel.resp_txt.SetValue(str(row.history.raw_content)) + # ---------------------------------------------------------------------- @@ -247,10 +264,15 @@ def on_selected_row(self, row): class WfuzzFrame(wx.Frame): - def __init__(self, parent, id=-1, title="Wfuzz", pos=wx.DefaultPosition, - size=wx.DefaultSize, - style=wx.DEFAULT_FRAME_STYLE | wx.SUNKEN_BORDER | wx.CLIP_CHILDREN - ): + def __init__( + self, + parent, + id=-1, + title="Wfuzz", + pos=wx.DefaultPosition, + size=wx.DefaultSize, + style=wx.DEFAULT_FRAME_STYLE | wx.SUNKEN_BORDER | wx.CLIP_CHILDREN, + ): wx.Frame.__init__(self, parent, id, title, pos, size, style) def start_gui(self, controller): @@ -276,8 +298,17 @@ def start_gui(self, controller): self.SetMinSize(wx.Size(400, 300)) # create some center panes - self._mgr.AddPane(MainNotebookPanel(self, self, controller._interp), wx.aui.AuiPaneInfo().Caption("Raw HTTP Content").Name("analysis_notebook").CenterPane()) - self._mgr.AddPane(self.CreateNotebook(), wx.aui.AuiPaneInfo().Name("main_notebook").CenterPane()) + self._mgr.AddPane( + MainNotebookPanel(self, self, controller._interp), + wx.aui.AuiPaneInfo() + .Caption("Raw HTTP Content") + .Name("analysis_notebook") + .CenterPane(), + ) + self._mgr.AddPane( + self.CreateNotebook(), + wx.aui.AuiPaneInfo().Name("main_notebook").CenterPane(), + ) self._mgr.Update() self.Bind(wx.EVT_CLOSE, self.OnClose) @@ -306,12 +337,23 @@ def CreateNotebook(self): bookStyle = aui.AUI_NB_DEFAULT_STYLE # bookStyle &= ~(aui.AUI_NB_CLOSE_ON_ACTIVE_TAB) - bookStyle = aui.AUI_NB_DEFAULT_STYLE | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER + bookStyle = ( + aui.AUI_NB_DEFAULT_STYLE | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER + ) client_size = self.GetClientSize() - nb = aui.AuiNotebook(self, -1, wx.Point(client_size.x, client_size.y), wx.Size(430, 200), agwStyle=bookStyle) - - nb.AddPage(ListPanel(self, self, self.controller._model, self.controller._interp), "Main") + nb = aui.AuiNotebook( + self, + -1, + wx.Point(client_size.x, client_size.y), + wx.Size(430, 200), + agwStyle=bookStyle, + ) + + nb.AddPage( + ListPanel(self, self, self.controller._model, self.controller._interp), + "Main", + ) return nb diff --git a/src/wfuzz/ui/gui/model.py b/src/wfuzz/ui/gui/model.py index cad2d35d..c24227de 100644 --- a/src/wfuzz/ui/gui/model.py +++ b/src/wfuzz/ui/gui/model.py @@ -1,9 +1,9 @@ from collections import namedtuple import wx.dataview as dv -from wfuzz.filter import FuzzResFilter +from wfuzz.filters.ppfilter import FuzzResFilter -Row = namedtuple('Row', 'title colid width rtype field') +Row = namedtuple("Row", "title colid width rtype field") class GUIModel(dv.PyDataViewIndexListModel): @@ -17,7 +17,9 @@ def __init__(self, data=None): 2: Row(title="Lines", colid=2, width=170, rtype="int", field="lines"), 3: Row(title="Words", colid=3, width=170, rtype="int", field="words"), 4: Row(title="Chars", colid=4, width=170, rtype="int", field="chars"), - 5: Row(title="Payload", colid=5, width=170, rtype="string", field="description"), + 5: Row( + title="Payload", colid=5, width=170, rtype="string", field="description" + ), } def GetColumnType(self, col): @@ -57,7 +59,7 @@ def Compare(self, item1, item2, col, ascending): value1 = int(value1) value2 = int(value2) - return ((value1 > value2) - (value1 < value2)) + return (value1 > value2) - (value1 < value2) def DeleteRows(self, rows): # make a copy since we'll be sorting(mutating) the list diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py deleted file mode 100644 index 5300bab8..00000000 --- a/src/wfuzz/utils.py +++ /dev/null @@ -1,411 +0,0 @@ -import re -import os -import sys -import six -from threading import Lock -import functools - -from chardet.universaldetector import UniversalDetector -import chardet -from .exception import FuzzExceptInternalError - -allowed_fields = [ - "description", - "nres", - "code", - "chars", - "lines", - "words", - "md5", - "l", - "h", - "w", - "c", - "history", - "plugins", - - "url", - "content", - - "history.url", - "history.method", - "history.scheme", - "history.host", - "history.content", - "history.raw_content" - "history.is_path", - "history.pstrip", - "history.cookies", - "history.headers", - "history.params", - - "r", - "r.reqtime", - "r.url", - "r.method", - "r.scheme", - "r.host", - "r.content", - "r.raw_content" - "r.is_path", - "r.pstrip", - "r.cookies.", - "r.headers.", - "r.params.", -] - - -def json_minify(string, strip_space=True): - ''' - Created on 20/01/2011 - v0.2 (C) Gerald Storer - MIT License - Based on JSON.minify.js: - https://github.com/getify/JSON.minify - Contributers: - - Pradyun S. Gedam (conditions and variable names changed) - ''' - - tokenizer = re.compile(r'"|(/\*)|(\*/)|(//)|\n|\r') - end_slashes_re = re.compile(r'(\\)*$') - - in_string = False - in_multi = False - in_single = False - - new_str = [] - index = 0 - - for match in re.finditer(tokenizer, string): - - if not (in_multi or in_single): - tmp = string[index:match.start()] - if not in_string and strip_space: - # replace white space as defined in standard - tmp = re.sub('[ \t\n\r]+', '', tmp) - new_str.append(tmp) - - index = match.end() - val = match.group() - - if val == '"' and not (in_multi or in_single): - escaped = end_slashes_re.search(string, 0, match.start()) - - # start of string or unescaped quote character to end string - if not in_string or (escaped is None or len(escaped.group()) % 2 == 0): - in_string = not in_string - # include " character in next catch - index -= 1 - elif not (in_string or in_multi or in_single): - if val == '/*': - in_multi = True - elif val == '//': - in_single = True - elif val == '*/' and in_multi and not (in_string or in_single): - in_multi = False - elif val in '\r\n' and not (in_multi or in_string) and in_single: - in_single = False - elif not ((in_multi or in_single) or (val in ' \r\n\t' and strip_space)): - new_str.append(val) - - new_str.append(string[index:]) - return ''.join(new_str) - - -class Singleton(type): - ''' Singleton metaclass. Use by defining the metaclass of a class Singleton, - e.g.: class ThereCanBeOnlyOne: - __metaclass__ = Singleton - ''' - - def __call__(class_, *args, **kwargs): - if not class_.hasInstance(): - class_.instance = super(Singleton, class_).__call__(*args, **kwargs) - return class_.instance - - def deleteInstance(class_): - ''' Delete the (only) instance. This method is mainly for unittests so - they can start with a clean slate. ''' - if class_.hasInstance(): - del class_.instance - - def hasInstance(class_): - ''' Has the (only) instance been created already? ''' - return hasattr(class_, 'instance') - - -def get_home(check=False, directory=None): - path = os.path.join(os.path.expanduser("~"), ".wfuzz") - if check: - if not os.path.exists(path): - os.makedirs(path) - - return os.path.join(path, directory) if directory else path - - -def get_path(directory=None): - abspath = os.path.abspath(__file__) - ret = os.path.dirname(abspath) - - return os.path.join(ret, directory) if directory else ret - - -def find_file_in_paths(name, path): - for root, dirs, files in os.walk(path): - if name in files: - return os.path.join(root, name) - - return None - - -def python2_3_convert_from_unicode(text): - if sys.version_info >= (3, 0): - return text - else: - return convert_to_unicode(text) - - -def python2_3_convert_to_unicode(text): - if sys.version_info >= (3, 0): - return convert_to_unicode(text) - else: - return text - - -def convert_to_unicode(text): - if isinstance(text, dict): - return {convert_to_unicode(key): convert_to_unicode(value) for key, value in list(text.items())} - elif isinstance(text, list): - return [convert_to_unicode(element) for element in text] - elif isinstance(text, six.string_types): - return text.encode("utf-8", errors='ignore') - else: - return text - - -class FileDetOpener: - typical_encodings = [ - 'UTF-8', - 'ISO-8859-1', - 'Windows-1251', - 'Shift JIS', - 'Windows-1252', - 'GB2312', - 'EUC-KR', - 'EUC-JP', - 'GBK', - 'ISO-8859-2', - 'Windows-1250', - 'ISO-8859-15', - 'Windows-1256', - 'ISO-8859-9', - 'Big5', - 'Windows-1254', - ] - - def __init__(self, file_path, encoding=None): - self.cache = [] - self.file_des = open(file_path, mode='rb') - self.det_encoding = encoding - self.encoding_forced = False - - def close(self): - self.file_des.close() - - def reset(self): - self.file_des.seek(0) - - def __iter__(self): - return self - - def __next__(self): - decoded_line = None - line = None - last_error = None - - while decoded_line is None: - - while self.det_encoding is None: - detect_encoding = self.detect_encoding().get('encoding', 'utf-8') - self.det_encoding = detect_encoding if detect_encoding is not None else 'utf-8' - - if line is None: - if self.cache: - line = self.cache.pop() - else: - line = next(self.file_des) - if not line: - raise StopIteration - - try: - decoded_line = line.decode(self.det_encoding) - except UnicodeDecodeError: - if last_error is not None and last_error: - self.det_encoding = last_error.pop() - elif last_error is None and not self.encoding_forced: - last_error = list(reversed(self.typical_encodings)) - last_error.append(chardet.detect(line).get('encoding')) - elif not last_error: - raise FuzzExceptInternalError("Unable to decode wordlist file!") - - decoded_line = None - - return decoded_line - - def detect_encoding(self): - detector = UniversalDetector() - detector.reset() - - for line in self.file_des: - detector.feed(line) - self.cache.append(line) - if detector.done: - break - - detector.close() - - return detector.result - - next = __next__ # for Python 2 - - -def open_file_detect_encoding(file_path): - def detect_encoding(file_path): - detector = UniversalDetector() - detector.reset() - - with open(file_path, mode='rb') as file_to_detect: - for line in file_to_detect: - detector.feed(line) - if detector.done: - break - detector.close() - - return detector.result - - if sys.version_info >= (3, 0): - return open(file_path, "r", encoding=detect_encoding(file_path).get('encoding', 'utf-8')) - else: - return open(file_path, "r") - - -class MyCounter: - def __init__(self, count=0): - self._count = count - self._mutex = Lock() - - def inc(self): - return self._operation(1) - - def dec(self): - return self._operation(-1) - - def _operation(self, dec): - with self._mutex: - self._count += dec - return self._count - - def __call__(self): - with self._mutex: - return self._count - - -def _check_allowed_field(attr): - if [field for field in allowed_fields if attr.startswith(field)]: - return True - return False - - -def _get_alias(attr): - attr_alias = { - 'l': 'lines', - 'h': 'chars', - 'w': 'words', - 'c': 'code', - 'r': 'history', - } - - if attr in attr_alias: - return attr_alias[attr] - - return attr - - -def rsetattr(obj, attr, new_val, operation): - if not _check_allowed_field(attr): - raise AttributeError("Unknown field {}".format(attr)) - - pre, _, post = attr.rpartition('.') - - pre_post = None - if len(attr.split('.')) > 3: - pre_post = post - pre, _, post = pre.rpartition('.') - - post = _get_alias(post) - - try: - obj_to_set = rgetattr(obj, pre) if pre else obj - prev_val = rgetattr(obj, attr) - if pre_post is not None: - prev_val = DotDict({pre_post: prev_val}) - - if operation is not None: - val = operation(prev_val, new_val) - else: - if isinstance(prev_val, DotDict): - val = {k: new_val for k, v in prev_val.items()} - else: - val = new_val - - return setattr(obj_to_set, post, val) - except AttributeError: - raise AttributeError("rsetattr: Can't set '{}' attribute of {}.".format(post, obj_to_set.__class__)) - - -def rgetattr(obj, attr, *args): - def _getattr(obj, attr): - attr = _get_alias(attr) - try: - return getattr(obj, attr, *args) - except AttributeError: - raise AttributeError("rgetattr: Can't get '{}' attribute from '{}'.".format(attr, obj.__class__)) - - if not _check_allowed_field(attr): - raise AttributeError("Unknown field {}".format(attr)) - - return functools.reduce(_getattr, [obj] + attr.split('.')) - - -class DotDict(dict): - __setattr__ = dict.__setitem__ - __delattr__ = dict.__delitem__ - - def __getattr__(*args): - if args[1] not in args[0]: - raise KeyError("DotDict: Non-existing field {}".format(args[1])) - - # python 3 val = dict.get(*args, None) - val = dict.get(*args) - return DotDict(val) if type(val) is dict else val - # return DotDict(val) if type(val) is dict else DotDict({args[1]: val}) - - def __add__(self, other): - if isinstance(other, str): - return DotDict({k: v + other for k, v in self.items() if v}) - elif isinstance(other, DotDict): - # python 3 return DotDict({**self, **other}) - new_dic = DotDict(self) - new_dic.update(other) - return new_dic - - def __radd__(self, other): - if isinstance(other, str): - return DotDict({k: other + v for k, v in self.items() if v}) - - -def value_in_any_list_item(value, list_obj): - if isinstance(list_obj, list): - return len([item for item in list_obj if value.lower() in item.lower()]) > 0 - elif isinstance(list_obj, str): - return value.lower() in list_obj.lower() diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index c36a4a4d..41ccb552 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -1,27 +1,29 @@ #!/usr/bin/env python import sys +import warnings from .core import Fuzzer from .facade import Facade from .exception import FuzzException, FuzzExceptBadInstall - -from .ui.console.mvc import Controller, KeyPress, View -from .ui.console.common import help_banner2 +from .ui.console.mvc import Controller, KeyPress +from .ui.console.common import ( + help_banner2, + wfpayload_usage, +) from .ui.console.clparser import CLParser -from .fuzzobjects import FuzzResult +from .fuzzobjects import FuzzWordType def main(): kb = None fz = None - printer = None session_options = None try: # parse command line session_options = CLParser(sys.argv).parse_cl().compile() - session_options["send_discarded"] = True + session_options["exec_mode"] = "cli" # Create fuzzer's engine fz = Fuzzer(session_options) @@ -31,30 +33,28 @@ def main(): try: kb = KeyPress() except ImportError as e: - raise FuzzExceptBadInstall("Error importing necessary modules for interactive mode: %s" % str(e)) + raise FuzzExceptBadInstall( + "Error importing necessary modules for interactive mode: %s" + % str(e) + ) else: Controller(fz, kb) kb.start() - printer = View(session_options) - if session_options["console_printer"]: - printer = Facade().printers.get_plugin(session_options["console_printer"])(None) - printer.header(fz.genReq.stats) - for res in fz: - printer.result(res) - - printer.footer(fz.genReq.stats) + pass except FuzzException as e: - print("\nFatal exception: {}".format(str(e))) + warnings.warn("Fatal exception: {}".format(str(e))) except KeyboardInterrupt: - print("\nFinishing pending requests...") + warnings.warn("Finishing pending requests...") if fz: fz.cancel_job() except NotImplementedError as e: - print("\nFatal exception: Error importing wfuzz extensions: {}".format(str(e))) + warnings.warn( + "Fatal exception: Error importing wfuzz extensions: {}".format(str(e)) + ) except Exception as e: - print("\nUnhandled exception: {}".format(str(e))) + warnings.warn("Unhandled exception: {}".format(str(e))) finally: if session_options: session_options.close() @@ -66,83 +66,74 @@ def main(): def main_filter(): def usage(): print(help_banner2) - print("""Usage: -\n\twfpayload [Options]\n\n -\nOptions:\n -\t--help : This help -\t-v : Verbose output -\t-z payload : Specify a payload for each FUZZ keyword used in the form of type,parameters,encoder. -\t A list of encoders can be used, ie. md5-sha1. Encoders can be chained, ie. md5@sha1. -\t Encoders category can be used. ie. url -\t--zD default : Default argument for the specified payload (it must be preceded by -z or -w). -\t--zP <params> : Arguments for the specified payload (it must be preceded by -z or -w). -\t--slice <filter> : Filter payload\'s elements using the specified expression. -\t-w wordlist : Specify a wordlist file (alias for -z file,wordlist). -\t-m iterator : Specify an iterator for combining payloads (product by default) -\t--field <expr> : Do not show the payload but the specified language expression -\t--efield <expr> : Show the specified language expression together with the current payload -""") - - from .api import payload - from .exception import FuzzExceptBadOptions - import getopt - - try: - opts, args = getopt.getopt(sys.argv[1:], "vhz:m:w:", ["field=", "help", "slice=", "zD=", "zP=", "efield="]) - except getopt.GetoptError as err: - print((str(err))) - usage() - sys.exit(2) - - if len(opts) == 0 or len(args) > 0: - usage() - sys.exit() + print(wfpayload_usage) - field = None - raw_output = False - - for o, value in opts: - if o in ("-h", "--help"): - usage() - sys.exit() - if o in ("--efield"): - field = value - if o in ("--field"): - field = value - raw_output = True + from .api import fuzz try: - session_options = CLParser(sys.argv).parse_cl() - printer = None - - for res in payload(**session_options): - if len(res) > 1: - raise FuzzExceptBadOptions("wfpayload can only be used to generate one word dictionaries") - else: - r = res[0] - - # TODO: all should be same object type and no need for isinstance - if isinstance(r, FuzzResult): - if raw_output: - print(r.eval(field if field is not None else "url")) - else: - if printer is None: - printer = View(session_options) - printer.header(None) - - if field: - r._description = field - r._show_field = False - printer.result(r) - else: - print(r) + short_opts = "hvce:z:f:w:o:" + long_opts = [ + "efield=", + "ee=", + "zE=", + "zD=", + "field=", + "slice=", + "zP=", + "oF=", + "recipe=", + "dump-recipe=", + "sc=", + "sh=", + "sl=", + "sw=", + "ss=", + "hc=", + "hh=", + "hl=", + "hw=", + "hs=", + "prefilter=", + "filter=", + "help", + "version", + "script-help=", + "script=", + "script-args=", + ] + session_options = CLParser( + sys.argv, + short_opts, + long_opts, + help_banner2, + wfpayload_usage, + wfpayload_usage, + wfpayload_usage, + ).parse_cl() + session_options["transport"] = "payload" + session_options["url"] = "FUZZ" + + session_options.compile_dictio() + payload_type = session_options["compiled_dictio"].payloads()[0].get_type() + + if ( + payload_type == FuzzWordType.FUZZRES + and session_options["show_field"] is not True + ): + session_options["exec_mode"] = "cli" + + for res in fuzz(**session_options): + if payload_type == FuzzWordType.WORD: + print(res.description) + elif payload_type == FuzzWordType.FUZZRES and session_options["show_field"]: + print(res._field()) except KeyboardInterrupt: pass except FuzzException as e: - print(("\nFatal exception: %s" % str(e))) + warnings.warn(("Fatal exception: %s" % str(e))) except Exception as e: - print(("\nUnhandled exception: %s" % str(e))) + warnings.warn(("Unhandled exception: %s" % str(e))) def main_encoder(): @@ -160,7 +151,7 @@ def usage(): try: opts, args = getopt.getopt(sys.argv[1:], "he:d:", ["help"]) except getopt.GetoptError as err: - print((str(err))) + warnings.warn(str(err)) usage() sys.exit(2) @@ -179,12 +170,18 @@ def usage(): sys.exit() except IndexError as e: usage() - print("\nFatal exception: Specify a string to encode or decode.{}\n".format(str(e))) + warnings.warn( + "\nFatal exception: Specify a string to encode or decode.{}\n".format( + str(e) + ) + ) sys.exit() except AttributeError as e: - print("\nEncoder plugin missing encode or decode functionality. {}".format(str(e))) + warnings.warn( + "\nEncoder plugin missing encode or decode functionality. {}".format(str(e)) + ) except FuzzException as e: - print(("\nFatal exception: %s" % str(e))) + warnings.warn(("\nFatal exception: %s" % str(e))) def main_gui(): diff --git a/src/wxfuzz.py b/src/wxfuzz.py index 86ae676e..937beb3d 100644 --- a/src/wxfuzz.py +++ b/src/wxfuzz.py @@ -2,5 +2,5 @@ from wfuzz.wfuzz import main_gui -if __name__ == '__main__': +if __name__ == "__main__": main_gui() diff --git a/tests/api/test_encoders.py b/tests/api/test_encoders.py new file mode 100644 index 00000000..6d49248d --- /dev/null +++ b/tests/api/test_encoders.py @@ -0,0 +1,43 @@ +import pytest +import wfuzz + + +@pytest.mark.parametrize( + "encoder, text, expected_result", + [ + ("none", "test", "test"), + ("urlencode", "../=?&", "../%3D%3F%26"), + ("double_urlencode", "../=?&", "../%253D%253F%2526"), + ("double_urlencode", "../=?&", "../%253D%253F%2526"), + ("base64", "admin", "YWRtaW4="), + ("sha1", "admin", "d033e22ae348aeb5660fc2140aec35850c4da997"), + ("md5", "admin", "21232f297a57a5a743894a0e4a801fc3"), + ("hexlify", "admin", "61646d696e"), + ("html_escape", "<>&'\"/", "<>&'"/"), + ("html_decimal", "<>&'\"/", "<>&'"/"), + ("html_hexadecimal", "<>&'\"/", "<>&'"/"), + ("mysql_char", "admin", "CHAR(97,100,109,105,110)"), + ("mssql_char", "admin", "CHAR(97)+CHAR(100)+CHAR(109)+CHAR(105)+CHAR(110)"), + ("oracle_char", "admin", "chr(97)||chr(100)||chr(109)||chr(105)||chr(110)"), + ], +) +def test_encode(encoder, text, expected_result): + assert wfuzz.encode(encoder, text) == expected_result + + +@pytest.mark.parametrize( + "encoder, text, expected_result", + [ + ("none", "test", "test"), + ("urlencode", "../=?&", "../%3D%3F%26"), + ("double_urlencode", "../=?&", "../%253D%253F%2526"), + ("double_urlencode", "../=?&", "../%253D%253F%2526"), + ("base64", "admin", "YWRtaW4="), + ("hexlify", "admin", "61646d696e"), + ("mysql_char", "admin", "CHAR(97,100,109,105,110)"), + ("mssql_char", "admin", "CHAR(97)+CHAR(100)+CHAR(109)+CHAR(105)+CHAR(110)"), + ("oracle_char", "admin", "chr(97)||chr(100)||chr(109)||chr(105)||chr(110)"), + ], +) +def test_decode(encoder, text, expected_result): + assert wfuzz.decode(encoder, expected_result) == text diff --git a/tests/api/test_payload.py b/tests/api/test_payload.py new file mode 100644 index 00000000..d306556d --- /dev/null +++ b/tests/api/test_payload.py @@ -0,0 +1,113 @@ +import pytest +import wfuzz + + +@pytest.mark.parametrize( + "params, expected_result", + [ + ( + { + "iterator": "zip", + "payloads": [ + ("range", {"default": "0-2", "encoder": None}, None), + ("range", {"default": "0-2", "encoder": None}, None), + ], + }, + [("0", "0"), ("1", "1"), ("2", "2")], + ), + ( + { + "iterator": "chain", + "payloads": [ + ("range", {"default": "0-2", "encoder": None}, None), + ("range", {"default": "0-2", "encoder": None}, None), + ], + }, + [("0",), ("0",), ("1",), ("1",), ("2",), ("2",)], + ), + ( + { + "iterator": "product", + "payloads": [ + ("range", {"default": "0-2", "encoder": None}, None), + ("range", {"default": "0-2", "encoder": None}, None), + ], + }, + [ + ("0", "0"), + ("0", "1"), + ("0", "2"), + ("1", "0"), + ("1", "1"), + ("1", "2"), + ("2", "0"), + ("2", "1"), + ("2", "2"), + ], + ), + ( + {"payloads": [("range", {"default": "0-4", "encoder": None}, None)]}, + [("0",), ("1",), ("2",), ("3",), ("4",)], + ), + ( + { + "payloads": [ + ("buffer_overflow", {"default": "10", "encoder": None}, None) + ] + }, + [("AAAAAAAAAA",)], + ), + ( + {"payloads": [("hexrange", {"default": "09-10", "encoder": None}, None)]}, + [("09",), ("0a",), ("0b",), ("0c",), ("0d",), ("0e",), ("0f",), ("10",)], + ), + ( + {"payloads": [("hexrange", {"default": "009-00B", "encoder": None}, None)]}, + [("009",), ("00a",), ("00b",)], + ), + ( + { + "payloads": [ + ("ipnet", {"default": "192.168.0.1/30", "encoder": None}, None) + ] + }, + [("192.168.0.1",), ("192.168.0.2",)], + ), + ( + { + "payloads": [ + ( + "iprange", + {"default": "192.168.0.1-192.168.0.2", "encoder": None}, + None, + ) + ] + }, + [("192.168.0.1",), ("192.168.0.2",)], + ), + ( + {"payloads": [("list", {"default": "a-b", "encoder": None}, None)]}, + [("a",), ("b",)], + ), + ( + {"payloads": [("list", {"default": "a\\-b-b", "encoder": None}, None)]}, + [("a-b",), ("b",)], + ), + ( + {"payloads": [("range", {"default": "1-2", "encoder": None}, None)]}, + [("1",), ("2",)], + ), + ], +) +def test_payload_iterator(params, expected_result): + assert sorted(list(wfuzz.payload(**params))) == sorted(expected_result) + + +@pytest.mark.parametrize( + "payload, expected_result", + [(range(4), [0, 1, 2, 3]), ([list(range(2)), list(range(2))], [[0, 1], [0, 1]])], +) +def test_get_payload(payload, expected_result): + assert sorted(wfuzz.get_payload(payload).data.get("dictio")[0]) == sorted( + expected_result + ) diff --git a/tests/api/test_session.py b/tests/api/test_session.py new file mode 100644 index 00000000..21f69bf5 --- /dev/null +++ b/tests/api/test_session.py @@ -0,0 +1,72 @@ +import pytest +import wfuzz + + +@pytest.mark.parametrize( + "session, expected_result", + [ + ( + "-z range,0-4 http://127.0.0.1/FUZZ", + { + "allvars": None, + "auth": {}, + "colour": False, + "compiled_baseline": None, + "compiled_dictio": None, + "compiled_filter": None, + "compiled_prefilter": [], + "compiled_printer": None, + "compiled_seed": None, + "compiled_stats": None, + "concurrent": 10, + "conn_delay": 90, + "connect_to_ip": None, + "console_printer": "", + "cookie": [], + "delay": None, + "dictio": None, + "exec_mode": "api", + "fields": [], + "filter": "", + "follow": False, + "hc": [], + "headers": [], + "hh": [], + "hl": [], + "hs": None, + "hw": [], + "interactive": False, + "iterator": None, + "method": None, + "no_cache": False, + "payloads": [("range", {"default": "0-4", "encoder": None}, None)], + "postdata": None, + "prefilter": [], + "previous": False, + "printer": (None, None), + "proxies": None, + "recipe": [], + "req_delay": 90, + "retries": 3, + "rlevel": 0, + "dlevel": 4, + "save": "", + "sc": [], + "scanmode": False, + "script": "", + "script_args": {}, + "seed_payload": False, + "sh": [], + "show_field": None, + "sl": [], + "ss": None, + "sw": [], + "transport": "http", + "url": "http://127.0.0.1/FUZZ", + "verbose": False, + }, + ) + ], +) +def test_get_payload(session, expected_result): + assert wfuzz.get_session(session).data == expected_result diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..30bcf66f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,117 @@ +import pytest + + +from wfuzz.fuzzrequest import FuzzRequest +from wfuzz.fuzzobjects import FuzzResult +from wfuzz.fuzzobjects import FPayloadManager +from wfuzz.filters.ppfilter import FuzzResFilter +from wfuzz.facade import Facade + + +@pytest.fixture +def full_fuzzres(request): + raw_req, raw_resp = request.param + fr = FuzzRequest() + fr.update_from_raw_http(raw_req, "http", raw_resp, None) + + return FuzzResult(history=fr) + + +@pytest.fixture +def full_fuzzreq(request): + raw_req, raw_resp = request.param + fr = FuzzRequest() + fr.update_from_raw_http(raw_req, "http", raw_resp, None) + + return fr + + +@pytest.fixture +def fuzzres_from_url(request): + fr = FuzzRequest() + fr.url = request.param + + return FuzzResult(history=fr) + + +@pytest.fixture +def filter_obj(): + return FuzzResFilter() + + +@pytest.fixture +def example_full_fuzzres(): + raw_req, raw_resp = ( + "GET /path?param1=1¶m2=2 HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "User-Agent: curl/7.58.0\n" + "Accept: */*\n" + "Cookie: cookie1=1\n", + "HTTP/1.1 302 Found\n" + "Content-Type: text/html; charset=utf-8\n" + "Content-Language: en\n" + "Location: https://wfuzz.readthedocs.io/en/latest/\n" + "Vary: Accept-Language, Cookie\n" + "Server: nginx/1.14.0 (Ubuntu)\n" + "X-Fallback: True\n" + "X-Served: Django\n" + "X-Deity: web01\n" + "Date: Wed, 23 Jan 2019 21:43:59 GMT\n" + "Content-Length: 0\n" + "Set-Cookie: name=Nicholas; expires=Sat, 02 May 2009 23:38:25 GMT\n", + ) + fr = FuzzRequest() + fr.update_from_raw_http( + raw_req, "http", raw_resp, b"Some line\n and words\nasdsdas" + ) + + return FuzzResult(history=fr) + + +@pytest.fixture +def example_full_fuzzres_content(request): + raw_content = request.param + + raw_req, raw_resp = ( + "GET /path?param1=1¶m2=2 HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "User-Agent: curl/7.58.0\n" + "Accept: */*\n" + "Cookie: cookie1=1\n", + "HTTP/1.1 200 OK\n" + "Content-Type: text/html; charset=utf-8\n" + "Content-Language: en\n" + "Vary: Accept-Language, Cookie\n" + "Server: nginx/1.14.0 (Ubuntu)\n" + "X-Fallback: True\n" + "X-Served: Django\n" + "X-Deity: web01\n" + "Date: Wed, 23 Jan 2019 21:43:59 GMT\n" + "Content-Length: 0\n" + "Set-Cookie: name=Nicholas; expires=Sat, 02 May 2009 23:38:25 GMT\n", + ) + fr = FuzzRequest() + fr.update_from_raw_http(raw_req, "http", raw_resp, raw_content) + + fuzzres = FuzzResult(history=fr) + fuzzres.payload_man = FPayloadManager() + + return fuzzres + + +@pytest.fixture +def example_full_fuzzres_no_response(): + raw_req = "GET /path?param1=1¶m2=2 HTTP/1.1\nHost: www.wfuzz.org\nUser-Agent: curl/7.58.0\nAccept: */*\n" + + fr = FuzzRequest() + fr.update_from_raw_http(raw_req, "http", None, None) + + return FuzzResult(history=fr) + + +@pytest.fixture +def get_plugin(): + def _get_customer_plugin(name): + return [x() for x in Facade().scripts.get_plugins(name)] + + return _get_customer_plugin diff --git a/tests/factories/test_seedbasebuilder.py b/tests/factories/test_seedbasebuilder.py new file mode 100644 index 00000000..6d1d0bca --- /dev/null +++ b/tests/factories/test_seedbasebuilder.py @@ -0,0 +1,140 @@ +import pytest + +from wfuzz.fuzzobjects import FuzzWord, FuzzWordType +from wfuzz.factories.fuzzfactory import SeedBuilderHelper +from wfuzz.ui.console.clparser import CLParser +from wfuzz.factories.fuzzresfactory import resfactory + +from wfuzz.helpers.obj_dyn import rgetattr +import wfuzz.api + + +@pytest.mark.parametrize( + "full_fuzzreq, expected_result", + [ + ( + ( + "GET /FUZZ HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + None, + ), + [ + { + "bl_value": None, + "field": None, + "full_bl": None, + "full_marker": "FUZZ", + "index": None, + "nonfuzz_marker": "", + "word": "FUZZ", + } + ], + ), + ( + ( + "GET /FUZZ{a_bl_value} HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + None, + ), + [ + { + "bl_value": "a_bl_value", + "field": None, + "full_bl": "{a_bl_value}", + "full_marker": "FUZZ{a_bl_value}", + "index": None, + "nonfuzz_marker": "{a_bl_value}", + "word": "FUZZ", + } + ], + ), + ( + ( + "GET /FUZZ[url] HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + None, + ), + [ + { + "bl_value": None, + "field": "url", + "full_bl": None, + "full_marker": "FUZZ[url]", + "index": None, + "nonfuzz_marker": "[url]", + "word": "FUZZ", + } + ], + ), + ( + ( + "GET /FUZZ/FUZ2Z[url] HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + None, + ), + [ + { + "bl_value": None, + "field": None, + "full_bl": None, + "full_marker": "FUZZ", + "index": None, + "nonfuzz_marker": "", + "word": "FUZZ", + }, + { + "bl_value": None, + "field": "url", + "full_bl": None, + "full_marker": "FUZ2Z[url]", + "index": "2", + "nonfuzz_marker": "[url]", + "word": "FUZ2Z", + }, + ], + ), + ], + indirect=["full_fuzzreq"], +) +def test_get_marker_dict(full_fuzzreq, expected_result): + assert SeedBuilderHelper().get_marker_dict(full_fuzzreq) == expected_result + + +@pytest.mark.parametrize( + "session_string, dictio, expected_field, expected_result", + [ + ( + "wfuzz http://www.wfuzz.io/FUZZ", + [FuzzWord("sub1", FuzzWordType.WORD)], + "url", + "http://www.wfuzz.io/sub1", + ), + ( + "wfuzz --basic FUZZ:FUZ2Z http://www.wfuzz.io/", + (FuzzWord("sub1", FuzzWordType.WORD), FuzzWord("sub2", FuzzWordType.WORD)), + "auth.credentials", + "sub1:sub2", + ), + ( + "wfuzz --basic FUZZ:FUZ2Z http://www.wfuzz.io/", + (FuzzWord("sub1", FuzzWordType.WORD), FuzzWord("sub2", FuzzWordType.WORD)), + "auth.method", + "basic", + ), + ], +) +def test_replace_markers(session_string, dictio, expected_field, expected_result): + options = CLParser(session_string.split(" ")).parse_cl() + options.compile_seeds() + + res = resfactory.create("fuzzres_from_options_and_dict", options, dictio) + + assert rgetattr(res.history, expected_field) == expected_result diff --git a/tests/filters/test_filter.py b/tests/filters/test_filter.py new file mode 100644 index 00000000..905ed5ad --- /dev/null +++ b/tests/filters/test_filter.py @@ -0,0 +1,48 @@ +import pytest + + +@pytest.mark.parametrize( + "filter_string, expected_result", + [ + ("h=28 or w=6 or l=2", True), + ("r.params.get.param2='2'", True), + ("r.headers.response.Location", "https://wfuzz.readthedocs.io/en/latest/"), + ("r.headers.response.notthere", {}), + ("r.params.get.notthere", {}), + ("r.cookies.response.notthere", {}), + ("r.cookies.response.notthere='something'", False), + ("r.cookies.response.notthere~'something'", False), + ("r.headers.request.Host", "www.wfuzz.org"), + ("r.headers.request.host", "www.wfuzz.org"), + ("r.headers.response.SeRVEr", "nginx/1.14.0 (Ubuntu)"), + ("r.headers.response.server", "nginx/1.14.0 (Ubuntu)"), + ("r.cookies.request.cookie1", "1"), + ("r.cookies.request.cOOkiE1", "1"), + ("r.cookies.response.name", "Nicholas"), + ("r.cookies.response.nAMe", "Nicholas"), + ("r.params.get.param1", "1"), + ("r.params.get.pAraM1", "1"), + ], +) +def test_filter_ret_values( + filter_obj, example_full_fuzzres, filter_string, expected_result +): + assert filter_obj.is_visible(example_full_fuzzres, filter_string) == expected_result + + +@pytest.mark.parametrize( + "filter_string, expected_result", + [ + ("r.headers.response.notthere", {}), + ("r.params.get.notthere", {}), + ("r.cookies.response.notthere", {}), + ("r.cookies.response.notthere='something'", False), + ], +) +def test_filter_ret_values_no_response( + filter_obj, example_full_fuzzres_no_response, filter_string, expected_result +): + assert ( + filter_obj.is_visible(example_full_fuzzres_no_response, filter_string) + == expected_result + ) diff --git a/tests/filters/test_filter_codes.py b/tests/filters/test_filter_codes.py new file mode 100644 index 00000000..4ed84823 --- /dev/null +++ b/tests/filters/test_filter_codes.py @@ -0,0 +1,70 @@ +import pytest + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [ + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.scheme='http'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.netloc='www.wfuzz.org'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.path='/path/test.php'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.ffname='test.php'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.fname='test'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.hasquery", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "not r.urlp.isbllist", + True, + ), + ], + indirect=["fuzzres_from_url"], +) +def test_urlp(filter_obj, fuzzres_from_url, filter_string, expected_result): + assert filter_obj.is_visible(fuzzres_from_url, filter_string) == expected_result + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [("http://www.wfuzz.org/path?param=1¶m2=2", "r.is_path", False)], + indirect=["fuzzres_from_url"], +) +def test_ispath(filter_obj, fuzzres_from_url, filter_string, expected_result): + assert filter_obj.is_visible(fuzzres_from_url, filter_string) == expected_result + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [ + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.pstrip", + "http://www.wfuzz.org/path-gparam-gparam2", + ), + ], + indirect=["fuzzres_from_url"], +) +def test_pstrip(filter_obj, fuzzres_from_url, filter_string, expected_result): + assert filter_obj.is_visible(fuzzres_from_url, filter_string) == expected_result diff --git a/tests/filters/test_filter_urlp.py b/tests/filters/test_filter_urlp.py new file mode 100644 index 00000000..4ed84823 --- /dev/null +++ b/tests/filters/test_filter_urlp.py @@ -0,0 +1,70 @@ +import pytest + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [ + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.scheme='http'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.netloc='www.wfuzz.org'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.path='/path/test.php'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.ffname='test.php'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.fname='test'", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "r.urlp.hasquery", + True, + ), + ( + "http://www.wfuzz.org/path/test.php?param=1¶m2=2", + "not r.urlp.isbllist", + True, + ), + ], + indirect=["fuzzres_from_url"], +) +def test_urlp(filter_obj, fuzzres_from_url, filter_string, expected_result): + assert filter_obj.is_visible(fuzzres_from_url, filter_string) == expected_result + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [("http://www.wfuzz.org/path?param=1¶m2=2", "r.is_path", False)], + indirect=["fuzzres_from_url"], +) +def test_ispath(filter_obj, fuzzres_from_url, filter_string, expected_result): + assert filter_obj.is_visible(fuzzres_from_url, filter_string) == expected_result + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [ + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.pstrip", + "http://www.wfuzz.org/path-gparam-gparam2", + ), + ], + indirect=["fuzzres_from_url"], +) +def test_pstrip(filter_obj, fuzzres_from_url, filter_string, expected_result): + assert filter_obj.is_visible(fuzzres_from_url, filter_string) == expected_result diff --git a/tests/filters/test_prefilter_mangle.py b/tests/filters/test_prefilter_mangle.py new file mode 100644 index 00000000..12fe4afa --- /dev/null +++ b/tests/filters/test_prefilter_mangle.py @@ -0,0 +1,81 @@ +import pytest + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [ + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.url=+'test'", + "http://www.wfuzz.org/path?param=1¶m2=2test", + ), + ("http://www.wfuzz.org/path?param=1¶m2=2", "r.url:='test'", "http://test/"), + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.url=-'test'", + "testhttp://www.wfuzz.org/path?param=1¶m2=2", + ), + ], + indirect=["fuzzres_from_url"], +) +def test_url_set(filter_obj, fuzzres_from_url, filter_string, expected_result): + filter_obj.is_visible(fuzzres_from_url, filter_string) + assert fuzzres_from_url.history.url == expected_result + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [("http://www.wfuzz.org/path?param", "r.params.all=+'test'", {"param": None})], + indirect=["fuzzres_from_url"], +) +def test_params_set_no_value( + filter_obj, fuzzres_from_url, filter_string, expected_result +): + filter_obj.is_visible(fuzzres_from_url, filter_string) + assert fuzzres_from_url.history.params.get == expected_result + + +@pytest.mark.parametrize( + "fuzzres_from_url, filter_string, expected_result", + [ + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.params.get.param=+'test'", + {"param": "1test", "param2": "2"}, + ), + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.params.get.param=-'test'", + {"param": "test1", "param2": "2"}, + ), + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.params.all=+'2'", + {"param": "12", "param2": "22"}, + ), + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.params.all:='2'", + {"param": "2", "param2": "2"}, + ), + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.params.get.notthere=-'2'", + {"param": "1", "param2": "2"}, + ), + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.params.get.notthere=+'2'", + {"param": "1", "param2": "2"}, + ), + ( + "http://www.wfuzz.org/path?param=1¶m2=2", + "r.params.get.notthere:='2'", + {"notthere": "2", "param": "1", "param2": "2"}, + ), + ], + indirect=["fuzzres_from_url"], +) +def test_params_set(filter_obj, fuzzres_from_url, filter_string, expected_result): + filter_obj.is_visible(fuzzres_from_url, filter_string) + assert fuzzres_from_url.history.params.all == expected_result diff --git a/tests/filters/test_prefilter_mangle_codes.py b/tests/filters/test_prefilter_mangle_codes.py new file mode 100644 index 00000000..76353845 --- /dev/null +++ b/tests/filters/test_prefilter_mangle_codes.py @@ -0,0 +1,10 @@ +import pytest + + +@pytest.mark.parametrize( + "filter_string, expected_result", + [("r.code:=429", 429), ("r.c:=404", 404), ("r.c=+404", 706), ("r.c=-2", 300)], +) +def test_code_set(filter_obj, example_full_fuzzres, filter_string, expected_result): + filter_obj.is_visible(example_full_fuzzres, filter_string) + assert example_full_fuzzres.code == expected_result diff --git a/tests/helpers/test_dotdict.py b/tests/helpers/test_dotdict.py new file mode 100644 index 00000000..227cd603 --- /dev/null +++ b/tests/helpers/test_dotdict.py @@ -0,0 +1,30 @@ +import pytest + +from wfuzz.helpers.obj_dic import DotDict +from wfuzz.helpers.obj_dyn import rgetattr + + +@pytest.fixture +def dotdict_ex1(): + return DotDict({"a": "1"}) + + +@pytest.fixture +def dotdict_ex2(): + return DotDict({"a": "2"}) + + +def test_operators(dotdict_ex1, dotdict_ex2): + assert dotdict_ex1 == {"a": "1"} + assert dotdict_ex1 + "test" == {"a": "1test"} + assert "test" + dotdict_ex1 == {"a": "test1"} + assert dotdict_ex1 + dotdict_ex2 == {"a": "2"} + assert dotdict_ex2 + dotdict_ex1 == {"a": "1"} + + +def test_nonexisting_key_returns_none(dotdict_ex1): + assert dotdict_ex1["anything"] == {} + + +def test_nonexisting_attr_returns_empty_dict(dotdict_ex1): + assert rgetattr(dotdict_ex1, "anything") == {} diff --git a/tests/helpers/test_insensitive_dict.py b/tests/helpers/test_insensitive_dict.py new file mode 100644 index 00000000..437ba323 --- /dev/null +++ b/tests/helpers/test_insensitive_dict.py @@ -0,0 +1,34 @@ +import pytest + +from wfuzz.helpers.obj_dic import CaseInsensitiveDict + + +@pytest.fixture +def case_dict(): + return CaseInsensitiveDict({"OnE": 1}) + + +@pytest.mark.parametrize("key, expected_result", [("one", 1), ("oNe", 1)]) +def test_key_get_item(case_dict, key, expected_result): + assert case_dict[key] == expected_result + assert case_dict.get(key) == expected_result + + +@pytest.mark.parametrize( + "key, expected_result", + [("One", True), ("OnE", True), ("one", True), ("onetwo", False)], +) +def test_key_in_item(case_dict, key, expected_result): + assert (key in case_dict) == expected_result + + +def test_update(): + dd = CaseInsensitiveDict({}) + dd.update({"OnE": 1}) + + assert dd["one"] == 1 + assert dd["oNe"] == 1 + + +def test_key_in(case_dict): + assert list(case_dict.keys()) == ["OnE"] diff --git a/tests/plugins/test_links.py b/tests/plugins/test_links.py new file mode 100644 index 00000000..a9408d08 --- /dev/null +++ b/tests/plugins/test_links.py @@ -0,0 +1,73 @@ +import pytest +import re + +from queue import Queue + + +@pytest.mark.parametrize( + "example_full_fuzzres_content, expected_links", + [ + ( + b'<link rel="manifest" href="/android-chrome-manifest.json">\n', + ["http://www.wfuzz.org/android-chrome-manifest.json"], + ), + ( + b'<link rel="alternate" href="https://www.wfuzz.org/" hreflang="en-AE" />', + ["https://www.wfuzz.org/"], + ), + (b'<link rel="dns-prefetch" href="https://www.wfuzz.io">\n', [],), + (b'<script src="//js.wfuzz.org/sttc/main.93d0d236.js"></script>', [],), + ], + indirect=["example_full_fuzzres_content"], +) +def test_parsed_links(example_full_fuzzres_content, get_plugin, expected_links): + links_plugin = get_plugin("links")[0] + links_plugin.results_queue = Queue() + links_plugin.base_fuzz_res = example_full_fuzzres_content + links_plugin.add_path = False + + assert links_plugin.name == "links" + + links_plugin.process(example_full_fuzzres_content) + + results = [] + while not links_plugin.results_queue.empty(): + results.append(links_plugin.results_queue.get()) + + assert [ + fzres._seed.history.url for fzres in results if fzres._seed + ] == expected_links + + +@pytest.mark.parametrize( + "example_full_fuzzres_content, expected_links", + [ + ( + b'<link rel="dns-prefetch" href="https://www.wfuzz.io">\n', + ["https://www.wfuzz.io/"], + ), + ( + b'<script src="//js.wfuzz.org/sttc/main.93d0d236.js"></script>', + ["http://js.wfuzz.org/sttc/main.93d0d236.js"], + ), + ], + indirect=["example_full_fuzzres_content"], +) +def test_regex_option(example_full_fuzzres_content, get_plugin, expected_links): + links_plugin = get_plugin("links")[0] + links_plugin.results_queue = Queue() + links_plugin.base_fuzz_res = example_full_fuzzres_content + links_plugin.add_path = False + links_plugin.domain_regex = re.compile("wfuzz", re.MULTILINE | re.DOTALL) + + assert links_plugin.name == "links" + + links_plugin.process(example_full_fuzzres_content) + + results = [] + while not links_plugin.results_queue.empty(): + results.append(links_plugin.results_queue.get()) + + assert [ + fzres._seed.history.url for fzres in results if fzres._seed + ] == expected_links diff --git a/tests/server_dir/simple_server.py b/tests/server_dir/simple_server.py index f934f6b4..61dc0ff0 100644 --- a/tests/server_dir/simple_server.py +++ b/tests/server_dir/simple_server.py @@ -10,26 +10,27 @@ class GetHandler(SimpleHTTPRequestHandler): def do_HEAD(self): parsed_path = urllib.parse.urlparse(self.path) if parsed_path.path.startswith("/echo"): - message = '\n'.join( + message = "\n".join( [ - 'CLIENT VALUES:', - 'client_address=%s (%s)' % (self.client_address, self.address_string()), - 'command=%s' % self.command, - 'path=%s' % self.path, - 'real path=%s' % parsed_path.path, - 'query=%s' % parsed_path.query, - 'request_version=%s' % self.request_version, - '', - 'HEADERS:', - '%s' % self.headers, + "CLIENT VALUES:", + "client_address=%s (%s)" + % (self.client_address, self.address_string()), + "command=%s" % self.command, + "path=%s" % self.path, + "real path=%s" % parsed_path.path, + "query=%s" % parsed_path.query, + "request_version=%s" % self.request_version, + "", + "HEADERS:", + "%s" % self.headers, ] ) self.send_response(200) self.end_headers() - self.wfile.write(message.encode('utf-8')) + self.wfile.write(message.encode("utf-8")) elif parsed_path.path.startswith("/redirect"): self.send_response(301) - self.send_header('Location', "/echo") + self.send_header("Location", "/echo") self.end_headers() else: SimpleHTTPRequestHandler.do_HEAD(self) @@ -39,26 +40,27 @@ def do_HEAD(self): def do_GET(self): parsed_path = urllib.parse.urlparse(self.path) if parsed_path.path.startswith("/echo"): - message = '\n'.join( + message = "\n".join( [ - 'CLIENT VALUES:', - 'client_address=%s (%s)' % (self.client_address, self.address_string()), - 'command=%s' % self.command, - 'path=%s' % self.path, - 'real path=%s' % parsed_path.path, - 'query=%s' % parsed_path.query, - 'request_version=%s' % self.request_version, - '', - 'HEADERS:', - '%s' % self.headers, + "CLIENT VALUES:", + "client_address=%s (%s)" + % (self.client_address, self.address_string()), + "command=%s" % self.command, + "path=%s" % self.path, + "real path=%s" % parsed_path.path, + "query=%s" % parsed_path.query, + "request_version=%s" % self.request_version, + "", + "HEADERS:", + "%s" % self.headers, ] ) self.send_response(200) self.end_headers() - self.wfile.write(message.encode('utf-8')) + self.wfile.write(message.encode("utf-8")) elif parsed_path.path.startswith("/redirect"): self.send_response(301) - self.send_header('Location', "/echo") + self.send_header("Location", "/echo") self.end_headers() else: SimpleHTTPRequestHandler.do_GET(self) @@ -68,33 +70,34 @@ def do_GET(self): def do_POST(self): parsed_path = urllib.parse.urlparse(self.path) if parsed_path.path.startswith("/echo"): - content_len = int(self.headers.get('content-length')) - post_body = self.rfile.read(content_len).decode('utf-8') + content_len = int(self.headers.get("content-length")) + post_body = self.rfile.read(content_len).decode("utf-8") self.send_response(200) self.end_headers() - message = '\n'.join( + message = "\n".join( [ - 'CLIENT VALUES:', - 'client_address=%s (%s)' % (self.client_address, self.address_string()), - 'command=%s' % self.command, - 'path=%s' % self.path, - 'real path=%s' % parsed_path.path, - 'query=%s' % parsed_path.query, - 'request_version=%s' % self.request_version, - '', - 'HEADERS:', - '%s' % self.headers, - 'POST_DATA=%s' % post_body, - '', + "CLIENT VALUES:", + "client_address=%s (%s)" + % (self.client_address, self.address_string()), + "command=%s" % self.command, + "path=%s" % self.path, + "real path=%s" % parsed_path.path, + "query=%s" % parsed_path.query, + "request_version=%s" % self.request_version, + "", + "HEADERS:", + "%s" % self.headers, + "POST_DATA=%s" % post_body, + "", ] ) - self.wfile.write(message.encode('utf-8')) + self.wfile.write(message.encode("utf-8")) return -if __name__ == '__main__': - server = HTTPServer(('0.0.0.0', 8000), GetHandler) +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", 8000), GetHandler) server.serve_forever() diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 9d3e03ef..a594a66b 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -16,10 +16,10 @@ HTTPBIN_URL = "http://localhost:9000" REPLACE_HOSTNAMES = [ - ('localhost:8000', 'httpserver:8000'), - ('localhost:9000', 'httpbin:80'), - ('9000', '80'), - ('localhost', 'httpserver'), + ("localhost:8000", "httpserver:8000"), + ("localhost:9000", "httpbin:80"), + ("9000", "80"), + ("localhost", "httpserver"), ] # $ export PYTHONPATH=./src @@ -38,218 +38,1277 @@ # conn delays? # script args -testing_savedsession_tests = [ -] +testing_savedsession_tests = [] -testing_tests = [ -] +testing_tests = [] savedsession_tests = [ # parse post params - ("test_novalue_post_fuzz", "-z list --zD a -u {}/anything -d FUZZ".format(HTTPBIN_URL), "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --filter r.params.post.a:=1 --field r.params.post.a", ["1"], None), - ("test_json_post_fuzz2", "-z list --zD anything -u {}/FUZZ -d {{\"a\":\"2\"}} -H Content-Type:application/json".format(HTTPBIN_URL), "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --field r.params.post.a", ["2"], None), - ("test_json_post_fuzz3", "-z list --zD anything -u {}/FUZZ -d {{\"a\":\"2\"}} -H Content-Type:application/json".format(HTTPBIN_URL), "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --filter r.params.post.a:=1 --field r.params.post.a", ["1"], None), - ("test_json_nested", "-z list --zD anything -u {}/FUZZ -d {{\"test\":\"me\",\"another\":1,\"nested\":{{\"this\":2}}}} -H Content-Type:application/json".format(HTTPBIN_URL), "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --field r.params.post.nested.this", [2], None), - ("test_json_nested2", "-z list --zD anything -u {}/FUZZ -d {{\"test\":\"me\",\"another\":1,\"nested\":{{\"this\":2}}}} -H Content-Type:application/json".format(HTTPBIN_URL), "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --field r.params.post.another", [1], None), - + ( + "test_novalue_post_fuzz", + "-z list --zD a -u {}/anything -d FUZZ".format(HTTPBIN_URL), + "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --filter r.params.post.a:=1 --field r.params.post.a", + ["1"], + None, + ), + ( + "test_json_post_fuzz2", + '-z list --zD anything -u {}/FUZZ -d {{"a":"2"}} -H Content-Type:application/json'.format( + HTTPBIN_URL + ), + "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --field r.params.post.a", + ["2"], + None, + ), + ( + "test_json_post_fuzz3", + '-z list --zD anything -u {}/FUZZ -d {{"a":"2"}} -H Content-Type:application/json'.format( + HTTPBIN_URL + ), + "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --filter r.params.post.a:=1 --field r.params.post.a", + ["1"], + None, + ), + ( + "test_json_nested", + '-z list --zD anything -u {}/FUZZ -d {{"test":"me","another":1,"nested":{{"this":2}}}} -H Content-Type:application/json'.format( + HTTPBIN_URL + ), + "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --field r.params.post.nested.this", + ["2"], + None, + ), + ( + "test_json_nested2", + '-z list --zD anything -u {}/FUZZ -d {{"test":"me","another":1,"nested":{{"this":2}}}} -H Content-Type:application/json'.format( + HTTPBIN_URL + ), + "-z wfuzzp --zD $$PREVFILE$$ -u FUZZ --field r.params.post.another", + ["1"], + None, + ), # field fuzz values - ("test_desc_fuzz", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ FUZZ", ["http://localhost:9000/1"], None), - ("test_desc_attr", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ FUZZ[url]", ["http://localhost:9000/1"], None), - ("test_desc_concat_number", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ FUZZ[url]FUZZ[c]", ["http://localhost:9000/1 - 404"], None), - ("test_desc_url_number", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ FUZZ[c]", ["http://localhost:9000/1 - 404"], "Pycurl error 7:"), - + ( + "test_desc_fuzz", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ FUZZ", + ["http://localhost:9000/1"], + None, + ), + ( + "test_desc_attr", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ FUZZ[url]", + ["http://localhost:9000/1"], + None, + ), + ( + "test_desc_concat_number", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ FUZZ[url]FUZZ[c]", + ["http://localhost:9000/1 - 404"], + None, + ), + ( + "test_desc_url_number", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ http://localhost:FUZZ[c]/", + ["http://localhost:9000/1 - 404"], + "Pycurl error 7:", + ), # set values - ("test_desc_concat_number", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice r.c:=302 FUZZ[url]FUZZ[c]", ["http://localhost:9000/1 - 302"], None), - ("test_desc_rewrite_url", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --prefilter=r.url:=r.url|replace('1','2') FUZZ", ["http://localhost:9000/2"], None), - ("test_desc_rewrite_url2", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice r.url:=r.url|replace('1','2') FUZZ[url]", ["http://localhost:9000/2"], None), - + ( + "test_desc_concat_number", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice r.c:=302 FUZZ[url]FUZZ[c]", + ["http://localhost:9000/1 - 302"], + None, + ), + ( + "test_desc_rewrite_url", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --prefilter=r.url:=r.url|replace('1','2') FUZZ", + ["http://localhost:9000/2"], + None, + ), + ( + "test_desc_rewrite_url2", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice r.url:=r.url|replace('1','2') FUZZ[url]", + ["http://localhost:9000/2"], + None, + ), # fuzz value slice filters - ("test_desc_concat_fuzz_symbol_op", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --prefilter FUZZ[r.url]=+'2' FUZZ", ["http://localhost:9000/12"], None), - ("test_fuzz_symbol_code", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice FUZZ[c]=404 FUZZ", ["http://localhost:9000/1"], None), - ("test_fuzz_value_code", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice c=404 FUZZ", ["http://localhost:9000/1"], None), - + ( + "test_desc_concat_fuzz_symbol_op", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --prefilter FUZZ[r.url]=+'2' FUZZ", + ["http://localhost:9000/12"], + None, + ), + ( + "test_fuzz_symbol_code", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice FUZZ[c]=404 FUZZ", + ["http://localhost:9000/1"], + None, + ), + ( + "test_fuzz_value_code", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice c=404 FUZZ", + ["http://localhost:9000/1"], + None, + ), # fuzz value exceptions - ("test_fuzz_symbol_code", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice FUZ1Z[c]=404 FUZZ", ["http://localhost:9000/1"], "Unknown field"), - ("test_fuzz_symbol_code2", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice FUZ2Z[c]=404 FUZZ", ["http://localhost:9000/1"], "Non existent FUZZ payload"), - ("test_desc_assign_fuzz_symbol_op", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice FUZZ[r.url]:=FUZZ[r.url|replace('1','2')] FUZZ[url]", ["http://localhost:9000/2"], None), - ("test_fuzz_param_int", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice r.params.get:=2 FUZZ", ["http://localhost:9000/2"], "Non existent FUZZ payload"), - + ( + "test_fuzz_symbol_code", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice FUZ1Z[c]=404 FUZZ", + ["http://localhost:9000/1"], + "Unknown field", + ), + ( + "test_fuzz_symbol_code2", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice FUZ2Z[c]=404 FUZZ", + ["http://localhost:9000/1"], + "Non existent FUZZ payload", + ), + ( + "test_desc_assign_fuzz_symbol_op", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice FUZZ[r.url]:=FUZZ[r.url]|replace('1','2') FUZZ[url]", + ["http://localhost:9000/2"], + None, + ), + ( + "test_fuzz_param_int", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --slice r.params.get:=2 FUZZ", + ["http://localhost:9000/2"], + "Non existent FUZZ payload", + ), # filter based on various payloads - ("test_fuzz_fuz2z_code", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z list,404-302-200 --prefilter FUZZ[code]=FUZ2Z FUZZ[url]/FUZ2Z", ['http://localhost:9000/1 - 404'], None), - ("test_fuzz_fuz2z_code2", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z list,404-302-200 --prefilter FUZZ[code]=FUZ2Z FUZZ[url]", ['http://localhost:9000/1'], None), - ("test_fuzz_fuz2z_code3", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z list,404-302-200 --prefilter FUZZ[code]=FUZ2Z FUZZ", ['http://localhost:9000/1'], None), - + ( + "test_fuzz_fuz2z_code", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ -z list,404-302-200 --prefilter FUZZ[code]=FUZ2Z FUZZ[url]/FUZ2Z", + ["http://localhost:9000/1 - 404"], + None, + ), + ( + "test_fuzz_fuz2z_code2", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ -z list,404-302-200 --prefilter FUZZ[code]=FUZ2Z FUZZ[url]", + ["http://localhost:9000/1"], + None, + ), + ( + "test_fuzz_fuz2z_code3", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ -z list,404-302-200 --prefilter FUZZ[code]=FUZ2Z FUZZ", + ["http://localhost:9000/1"], + None, + ), # set values various payloads - ("test_set_fuzz_from_fuz2z_full", "-z range,1-1 {}/FUZZ?param=1".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z list,6-3 --prefilter r.params.get.param:=FUZ2Z FUZZ", ["http://localhost:9000/1?param=6", "http://localhost:9000/1?param=3"], None), - ("test_set_fuzz_from_fuz2z_full2", "-z range,1-1 {}/FUZZ?param=1".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z list,6-3 --prefilter FUZZ[r.params.get.param]:=FUZ2Z FUZZ", ["http://localhost:9000/1?param=6", "http://localhost:9000/1?param=3"], None), - ("test_set_fuzz_from_fuz2z_full_all", "-z range,1-1 {}/FUZZ?param=1¶m2=2".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z range,6-6 --prefilter r.params.all:=FUZ2Z FUZZ", ["http://localhost:9000/1?param=6¶m2=6"], None), - ("test_app_fuzz_from_fuz2z_full_all", "-z range,1-1 {}/FUZZ?param=1¶m2=2".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z range,6-6 --prefilter r.params.all=+FUZ2Z FUZZ", ["http://localhost:9000/1?param=16¶m2=26"], None), + ( + "test_set_fuzz_from_fuz2z_full", + "-z range,1-1 {}/FUZZ?param=1".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ -z list,6-3 --prefilter r.params.get.param:=FUZ2Z FUZZ", + ["http://localhost:9000/1?param=6", "http://localhost:9000/1?param=3"], + None, + ), + ( + "test_set_fuzz_from_fuz2z_full2", + "-z range,1-1 {}/FUZZ?param=1".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ -z list,6-3 --prefilter FUZZ[r.params.get.param]:=FUZ2Z FUZZ", + ["http://localhost:9000/1?param=6", "http://localhost:9000/1?param=3"], + None, + ), + ( + "test_set_fuzz_from_fuz2z_full_all", + "-z range,1-1 {}/FUZZ?param=1¶m2=2".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ -z range,6-6 --prefilter r.params.all:=FUZ2Z FUZZ", + ["http://localhost:9000/1?param=6¶m2=6"], + None, + ), + ( + "test_app_fuzz_from_fuz2z_full_all", + "-z range,1-1 {}/FUZZ?param=1¶m2=2".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ -z range,6-6 --prefilter r.params.all=+FUZ2Z FUZZ", + ["http://localhost:9000/1?param=16¶m2=26"], + None, + ), # fails ("test_set_fuzz_from_fuz2z_url", "-z range,1-1 {}/FUZZ?param=1".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ -z list,6-3 --prefilter r.params.get.param:=FUZ2Z FUZZ[url]", ["http://localhost:9000/1?param=6", "http://localhost:9000/1?param=3"], None), - # test different field - ("test_field", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --field c FUZZ", [404], None), - + ( + "test_field", + "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), + "-z wfuzzp,$$PREVFILE$$ --field c FUZZ", + ["404"], + None, + ), ] basic_tests = [ # different connect host ip # travis has an old pycurl version ("test_static_strquery_set_ip", "http://wfuzz.org/FUZZ?var=1&var2=2", [["anything"], ['PUT', 'GET', 'DELETE']], dict(connect_to_ip={'ip': '127.0.0.1', 'port': '9000'}, method='FUZ2Z', filter="content~'url' and content~'http://wfuzz.org'"), [(200, '/anything')] * 3, None), - # encoding tests - ("test_encode_cookie2_utf8_return", "%s/anything" % HTTPBIN_URL, [["は国"]], dict(cookie=["test=FUZZ"], filter="content~'test=\\\\u00e3\\\\u0081\\\\u00af\\\\u00e5\\\\u009b\\\\u00bd'"), [(200, '/anything')], None), - ("test_encode_header_utf8_return", "%s/headers" % HTTPBIN_URL, [["は国"]], dict(headers=[("myheader", "FUZZ")], filter="content~'Myheader' and content~'\\\\u00e3\\\\u0081\\\\u00af\\\\u00e5\\\\u009b\\\\u00bd'"), [(200, '/headers')], None), - ("test_encode_path", "%s/FUZZ" % HTTPBIN_URL, [["は国"]], dict(), [(404, '/は国')], None), - ("test_encode_basic_auth", "%s/basic-auth/FUZZ/FUZZ" % HTTPBIN_URL, [["は国"]], dict(auth=("basic", "FUZZ:FUZZ")), [(200, '/basic-auth/は国/は国')], None), + ( + "test_encode_cookie2_utf8_return", + "%s/anything" % HTTPBIN_URL, + [["は国"]], + dict( + cookie=["test=FUZZ"], + filter="content~'test=\\\\u00e3\\\\u0081\\\\u00af\\\\u00e5\\\\u009b\\\\u00bd'", + ), + [(200, "/anything")], + None, + ), + ( + "test_encode_header_utf8_return", + "%s/headers" % HTTPBIN_URL, + [["は国"]], + dict( + headers=[("myheader", "FUZZ")], + filter="content~'Myheader' and content~'\\\\u00e3\\\\u0081\\\\u00af\\\\u00e5\\\\u009b\\\\u00bd'", + ), + [(200, "/headers")], + None, + ), + ( + "test_encode_path", + "%s/FUZZ" % HTTPBIN_URL, + [["は国"]], + dict(), + [(404, "/は国")], + None, + ), + ( + "test_encode_basic_auth", + "%s/basic-auth/FUZZ/FUZZ" % HTTPBIN_URL, + [["は国"]], + dict(auth={"method": "basic", "credentials": "FUZZ:FUZZ"}), + [(200, "/basic-auth/は国/は国")], + None, + ), # ("test_encode_postdata", "%s/anything" % HTTPBIN_URL, [["は国"]], dict(postdata="a=FUZZ", filter="content~'は国'"), [(200, '/anything')], None), - ("test_encode_postdata", "%s/anything" % HTTPBIN_URL, [["は国"]], dict(postdata="a=FUZZ", filter="content~'\\\\u306f\\\\u56fd'"), [(200, '/anything')], None), - ("test_encode_url_filter", "%s/FUZZ" % HTTPBIN_URL, [["は国"]], dict(filter="url~'は国'"), [(404, '/は国')], None), + ( + "test_encode_postdata", + "%s/anything" % HTTPBIN_URL, + [["は国"]], + dict(postdata="a=FUZZ", filter="content~'\\\\u306f\\\\u56fd'"), + [(200, "/anything")], + None, + ), + ( + "test_encode_url_filter", + "%s/FUZZ" % HTTPBIN_URL, + [["は国"]], + dict(filter="url~'は国'"), + [(404, "/は国")], + None, + ), # ("test_encode_var", "%s/anything?var=FUZZ" % HTTPBIN_URL, [["は国"]], dict(filter="content~'\"は国\"'"), [(200, '/anything')], None), - ("test_encode_var", "%s/anything?var=FUZZ" % HTTPBIN_URL, [["は国"]], dict(filter="content~'\"\\\\u306f\\\\u56fd\"'"), [(200, '/anything')], None), - ("test_encode_redirect", "%s/redirect-to?url=FUZZ" % HTTPBIN_URL, [["は国"]], dict(filter="r.headers.response.Location='%C3%A3%C2%81%C2%AF%C3%A5%C2%9B%C2%BD'"), [(302, '/redirect-to')], None), + ( + "test_encode_var", + "%s/anything?var=FUZZ" % HTTPBIN_URL, + [["は国"]], + dict(filter="content~'\"\\\\u306f\\\\u56fd\"'"), + [(200, "/anything")], + None, + ), + ( + "test_encode_redirect", + "%s/redirect-to?url=FUZZ" % HTTPBIN_URL, + [["は国"]], + dict( + filter="r.headers.response.Location='%C3%A3%C2%81%C2%AF%C3%A5%C2%9B%C2%BD'" + ), + [(302, "/redirect-to")], + None, + ), # ("test_encode_cookie", "%s/cookies" % HTTPBIN_URL, [["は国"]], dict(cookie=["cookie1=FUZZ"], follow=True, filter="content~FUZZ"), [(200, '/cookies')], None), - ("test_encode_cookie", "%s/cookies" % HTTPBIN_URL, [["は国"]], dict(cookie=["cookie1=FUZZ"], follow=True, filter="content~'\\\\u306f\\\\u56fd'"), [(200, '/cookies')], None), - + ( + "test_encode_cookie", + "%s/cookies" % HTTPBIN_URL, + [["は国"]], + dict( + cookie=["cookie1=FUZZ"], follow=True, filter="content~'\\\\u306f\\\\u56fd'" + ), + [(200, "/cookies")], + None, + ), # postdata tests # pycurl does not allow it ("test_get_postdata", "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, [["anything"]], dict(postdata='a=1', filter="content~'\"form\":{\"a\":\"1\"}'"), [(200, '/anything')], None), - ("test_allmethods_postdata", "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, [["anything"], ['PUT', 'POST', 'DELETE'], ['333888']], dict(method='FUZ2Z', postdata='a=FUZ3Z', filter="content~FUZ2Z and content~'\"a\": \"' and content~FUZ3Z"), [(200, '/anything')] * 3, None), - + ( + "test_allmethods_postdata", + "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, + [["anything"], ["PUT", "POST", "DELETE"], ["333888"]], + dict( + method="FUZ2Z", + postdata="a=FUZ3Z", + filter='content~FUZ2Z and content~\'"a": "\' and content~FUZ3Z', + ), + [(200, "/anything")] * 3, + None, + ), # httpbin extra tests - ("test_gzip", "%s/FUZZ" % HTTPBIN_URL, [["gzip"]], dict(filter="content~'\"gzipped\": true'"), [(200, '/gzip')], None), - ("test_response_utf8", "%s/encoding/FUZZ" % HTTPBIN_URL, [["utf8"]], dict(), [(200, '/encoding/utf8')], None), - ("test_image", "%s/image/FUZZ" % HTTPBIN_URL, [["jpeg"]], dict(filter="content~'JFIF'"), [(200, '/image/jpeg')], None), - ("test_deflate", "%s/FUZZ" % HTTPBIN_URL, [["deflate"]], dict(filter="content~'\"deflated\": true'"), [(200, '/deflate')], None), - - ("test_robots_disallow", "%s/FUZZ" % HTTPBIN_URL, [["robots.txt"]], dict(script="robots"), [(200, '/deny'), (200, '/robots.txt')], None), - ("test_response_base64", "%s/base64/FUZZ" % HTTPBIN_URL, None, dict(filter="content~'HTTPBIN is awesome'", payloads=[("list", dict(values="HTTPBIN is awesome", encoder=["base64"]))]), [(200, '/base64/SFRUUEJJTiBpcyBhd2Vzb21l')], None), + ( + "test_gzip", + "%s/FUZZ" % HTTPBIN_URL, + [["gzip"]], + dict(filter="content~'\"gzipped\": true'"), + [(200, "/gzip")], + None, + ), + ( + "test_response_utf8", + "%s/encoding/FUZZ" % HTTPBIN_URL, + [["utf8"]], + dict(), + [(200, "/encoding/utf8")], + None, + ), + ( + "test_image", + "%s/image/FUZZ" % HTTPBIN_URL, + [["jpeg"]], + dict(filter="content~'JFIF'"), + [(200, "/image/jpeg")], + None, + ), + ( + "test_deflate", + "%s/FUZZ" % HTTPBIN_URL, + [["deflate"]], + dict(filter="content~'\"deflated\": true'"), + [(200, "/deflate")], + None, + ), + ( + "test_robots_disallow", + "%s/FUZZ" % HTTPBIN_URL, + [["robots.txt"]], + dict(script="robots"), + [(200, "/deny"), (200, "/robots.txt")], + None, + ), + ( + "test_response_base64", + "%s/base64/FUZZ" % HTTPBIN_URL, + None, + dict( + filter="content~'HTTPBIN is awesome'", + payloads=[("list", dict(values="HTTPBIN is awesome", encoder=["base64"]))], + ), + [(200, "/base64/SFRUUEJJTiBpcyBhd2Vzb21l")], + None, + ), # this does not work as you get the encoded value ("test_response_base64_FUZZ", "%s/base64/FUZZ" % HTTPBIN_URL, None, dict(filter="content~FUZZ", payloads=[("list", dict(values="HTTPBIN is awesome", encoder=["base64"]))]), [(200, '/base64/SFRUUEJJTiBpcyBhd2Vzb21l')], None), - ("test_basic_auth", "%s/basic-auth/FUZZ/FUZZ" % HTTPBIN_URL, [["userpass"]], dict(auth=("basic", "FUZZ:FUZZ")), [(200, '/basic-auth/userpass/userpass')], None), - ("test_digest_auth", "%s/digest-auth/auth/FUZZ/FUZZ" % HTTPBIN_URL, [["userpass"]], dict(auth=("digest", "FUZZ:FUZZ")), [(200, '/digest-auth/auth/userpass/userpass')], None), - ("test_delayed_response", "%s/delay/FUZZ" % HTTPBIN_URL, [["2"]], dict(req_delay=1), [(200, '/delay/2')], 'Operation timed out'), - ("test_static_strquery_set_multiple_method", "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, [["anything"], ['PUT', 'GET', 'POST', 'DELETE']], dict(method='FUZ2Z', filter="content~FUZ2Z and content~'\"var\": \"1\"' and content~'\"var2\": \"2\"'"), [(200, '/anything')] * 4, None), - ("test_static_strquery_set_multiple_method_gre", "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, [["anything"], ['PUT', 'GET', 'POST', 'DELETE']], dict(method='FUZ2Z', filter="content|gre('\"method\": \"(.*)?\",')=FUZ2Z and content~'\"var\": \"1\"' and content~'\"var2\": \"2\"'"), [(200, '/anything')] * 4, None), - + ( + "test_basic_auth", + "%s/basic-auth/FUZZ/FUZZ" % HTTPBIN_URL, + [["userpass"]], + dict(auth={"method": "basic", "credentials": "FUZZ:FUZZ"}), + [(200, "/basic-auth/userpass/userpass")], + None, + ), + ( + "test_digest_auth", + "%s/digest-auth/auth/FUZZ/FUZZ" % HTTPBIN_URL, + [["userpass"]], + dict(auth={"method": "digest", "credentials": "FUZZ:FUZZ"}), + [(200, "/digest-auth/auth/userpass/userpass")], + None, + ), + ( + "test_delayed_response", + "%s/delay/FUZZ" % HTTPBIN_URL, + [["2"]], + dict(req_delay=1), + [(200, "/delay/2")], + "Operation timed out", + ), + ( + "test_static_strquery_set_multiple_method", + "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, + [["anything"], ["PUT", "GET", "POST", "DELETE"]], + dict( + method="FUZ2Z", + filter='content~FUZ2Z and content~\'"var": "1"\' and content~\'"var2": "2"\'', + ), + [(200, "/anything")] * 4, + None, + ), + ( + "test_static_strquery_set_multiple_method_gre", + "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, + [["anything"], ["PUT", "GET", "POST", "DELETE"]], + dict( + method="FUZ2Z", + filter='content|gre(\'"method": "(.*)?",\')=FUZ2Z and content~\'"var": "1"\' and content~\'"var2": "2"\'', + ), + [(200, "/anything")] * 4, + None, + ), # set static HTTP values - ("test_static_strquery_set", "%s:8000/FUZZ?var=1&var=2" % LOCAL_DOMAIN, [["echo"]], dict(filter="content=~'query=var=1&var=2$'"), [(200, '/echo')], None), - ("test_static_postdata_set", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(postdata="a=2", filter="content=~'POST_DATA=a=2$'"), [(200, '/echo')], None), - ("test_static_postdata2_set", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(postdata="2", filter="content=~'POST_DATA=2$'"), [(200, '/echo')], None), - ("test_empty_postdata", "%s/FUZZ" % HTTPBIN_URL, [["anything"]], dict(postdata='', filter="content~'POST' and content~'\"form\": {},' and r.method='POST'"), [(200, '/anything')], None), - ("test_static_postdata3_set", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(headers=[("Content-Type", "application/json")], postdata="2", filter="content=~'POST_DATA=2$' and content=~'command=POST$' and content~'Content-Type: application/json'"), [(200, '/echo')], None), - ("test_static_postdata3_set2", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(headers=[("Content-Type", "aaaa")], postdata="a=2&b=3", filter="(content=~'POST_DATA=a=2&b=3$' or content=~'POST_DATA=b=3&a=2$') and content=~'command=POST$' and content~'Content-Type: aaaa'"), [(200, '/echo')], None), - ("test_static_postdata3_set3", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(headers=[("Content-Type", "application/json")], postdata="{\"a\": \"2\"}", filter="content=~'POST_DATA={\"a\": \"2\"}$' and content=~'command=POST$' and content~'Content-Type: application/json'"), [(200, '/echo')], None), - ("test_static_method_set", "%s/FUZZ" % URL_LOCAL, [["dir"]], dict(method="OPTIONS", filter="content~'Message: Unsupported method (\\\'OPTIONS\\\')'"), [(501, '/dir/dir')], None), - ("test_static_header_set", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(headers=[("myheader", "isset")], filter="content~'Myheader: isset'"), [(200, '/echo')], None), - ("test_static_cookie_set", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(cookie=["cookie1=value1", ], filter="content~'Cookie: cookie1=value1'"), [(200, '/echo')], None), - ("test_static_basic_auth_set", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(auth=("basic", "user:pass"), filter="content~'Authorization: Basic dXNlcjpwYXNz'"), [(200, '/echo')], None), - ("test_static_ntlm_auth_set", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["echo"]], dict(auth=("ntlm", "user:pass"), filter="content~'Authorization: NTLM TlRMTVNTUAABAAAABoIIAAAAAAAAAAAAAAAAAAAAAAA='"), [(200, '/echo')], None), - + ( + "test_static_strquery_set", + "%s:8000/FUZZ?var=1&var=2" % LOCAL_DOMAIN, + [["echo"]], + dict(filter="content=~'query=var=1&var=2$'"), + [(200, "/echo")], + None, + ), + ( + "test_static_postdata_set", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict(postdata="a=2", filter="content=~'POST_DATA=a=2$'"), + [(200, "/echo")], + None, + ), + ( + "test_static_postdata2_set", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict(postdata="2", filter="content=~'POST_DATA=2$'"), + [(200, "/echo")], + None, + ), + ( + "test_empty_postdata", + "%s/FUZZ" % HTTPBIN_URL, + [["anything"]], + dict( + postdata="", + filter="content~'POST' and content~'\"form\": {},' and r.method='POST'", + ), + [(200, "/anything")], + None, + ), + ( + "test_static_postdata3_set", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict( + headers=[("Content-Type", "application/json")], + postdata="2", + filter="content=~'POST_DATA=2$' and content=~'command=POST$' and content~'Content-Type: application/json'", + ), + [(200, "/echo")], + None, + ), + ( + "test_static_postdata3_set2", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict( + headers=[("Content-Type", "aaaa")], + postdata="a=2&b=3", + filter="(content=~'POST_DATA=a=2&b=3$' or content=~'POST_DATA=b=3&a=2$') and content=~'command=POST$' and content~'Content-Type: aaaa'", + ), + [(200, "/echo")], + None, + ), + ( + "test_static_postdata3_set3", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict( + headers=[("Content-Type", "application/json")], + postdata='{"a": "2"}', + filter="content=~'POST_DATA={\"a\": \"2\"}$' and content=~'command=POST$' and content~'Content-Type: application/json'", + ), + [(200, "/echo")], + None, + ), + ( + "test_static_method_set", + "%s/FUZZ" % URL_LOCAL, + [["dir"]], + dict( + method="OPTIONS", + filter="content~'Message: Unsupported method (\\'OPTIONS\\')'", + ), + [(501, "/dir/dir")], + None, + ), + ( + "test_static_header_set", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict(headers=[("myheader", "isset")], filter="content~'Myheader: isset'"), + [(200, "/echo")], + None, + ), + ( + "test_static_cookie_set", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict(cookie=["cookie1=value1"], filter="content~'Cookie: cookie1=value1'"), + [(200, "/echo")], + None, + ), + ( + "test_static_basic_auth_set", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict( + auth={"method": "basic", "credentials": "user:pass"}, + filter="content~'Authorization: Basic dXNlcjpwYXNz'", + ), + [(200, "/echo")], + None, + ), + ( + "test_static_ntlm_auth_set", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["echo"]], + dict( + auth={"method": "ntlm", "credentials": "user:pass"}, + filter="content~'Authorization: NTLM TlRMTVNTUAABAAAABoIIAAAAAAAAAAAAAAAAAAAAAAA='", + ), + [(200, "/echo")], + None, + ), # fuzzing HTTP values - ("test_basic_path_fuzz", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(), [(200, '/dir/a'), (200, '/dir/b'), (200, '/dir/c')], None), - ("test_multi_path_fuzz", "%s/FUZZ/FUZ2Z/FUZ3Z" % ECHO_URL, [["a"], ["b"], ["c"]], dict(filter="content~'path=/echo/a/b/c'"), [(200, '/echo/a/b/c')], None), - ("test_basic_method_fuzz", "%s" % URL_LOCAL, [["OPTIONS", "HEAD"]], dict(method="FUZZ", filter="content~'Unsupported method' and content~FUZZ"), [(501, '/dir')], None), - ("test_basic_postdata_fuzz", "%s" % ECHO_URL, [["onevalue", "twovalue"]], dict(postdata="a=FUZZ", filter="content~FUZZ and content~'POST_DATA=a='"), [(200, '/echo'), (200, '/echo')], None), - ("test_basic_postdata2_fuzz", "%s" % ECHO_URL, [["onevalue", "twovalue"]], dict(postdata="FUZZ=1234", filter="content~'POST_DATA=twovalue=1234' or content~'POST_DATA=onevalue=1234'"), [(200, '/echo'), (200, '/echo')], None), - ("test_basic_postdata3_fuzz", "%s" % ECHO_URL, [["onevalue", "twovalue"]], dict(postdata="FUZZ", filter="content~'POST_DATA=twovalue' or content~'POST_DATA=onevalue'"), [(200, '/echo'), (200, '/echo')], None), - ("test_basic_header_fuzz", "%s" % ECHO_URL, [["onevalue", "twovalue"]], dict(headers=[("myheader", "FUZZ")], filter="content~'Myheader:' and content~FUZZ"), [(200, '/echo'), (200, '/echo')], None), - ("test_basic_header_name_fuzz", "%s" % ECHO_URL, [["onevalue", "twovalue"]], dict(headers=[("FUZZ", "myheadervalue")], filter="content~': myheadervalue' and content~FUZZ"), [(200, '/echo'), (200, '/echo')], None), - ("test_static_strquery_fuzz", "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, [["value1"]], dict(filter="content~'query=var=value1'"), [(200, '/echo')], None), - ("test_static_strquery2_fuzz", "%s:8000/echo?FUZZ=value1" % LOCAL_DOMAIN, [["var"]], dict(filter="content~'query=var=value1'"), [(200, '/echo')], None), - ("test_basic_cookie_fuzz", "%s/anything" % HTTPBIN_URL, [["cookievalue"]], dict(cookie=["test=FUZZ"], filter="content~FUZZ"), [(200, '/anything')], None), - + ( + "test_basic_path_fuzz", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(), + [(200, "/dir/a"), (200, "/dir/b"), (200, "/dir/c")], + None, + ), + ( + "test_multi_path_fuzz", + "%s/FUZZ/FUZ2Z/FUZ3Z" % ECHO_URL, + [["a"], ["b"], ["c"]], + dict(filter="content~'path=/echo/a/b/c'"), + [(200, "/echo/a/b/c")], + None, + ), + ( + "test_basic_method_fuzz", + "%s" % URL_LOCAL, + [["OPTIONS", "HEAD"]], + dict(method="FUZZ", filter="content~'Unsupported method' and content~FUZZ"), + [(501, "/dir")], + None, + ), + ( + "test_basic_postdata_fuzz", + "%s" % ECHO_URL, + [["onevalue", "twovalue"]], + dict(postdata="a=FUZZ", filter="content~FUZZ and content~'POST_DATA=a='"), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_basic_postdata2_fuzz", + "%s" % ECHO_URL, + [["onevalue", "twovalue"]], + dict( + postdata="FUZZ=1234", + filter="content~'POST_DATA=twovalue=1234' or content~'POST_DATA=onevalue=1234'", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_basic_postdata3_fuzz", + "%s" % ECHO_URL, + [["onevalue", "twovalue"]], + dict( + postdata="FUZZ", + filter="content~'POST_DATA=twovalue' or content~'POST_DATA=onevalue'", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_basic_header_fuzz", + "%s" % ECHO_URL, + [["onevalue", "twovalue"]], + dict( + headers=[("myheader", "FUZZ")], + filter="content~'Myheader:' and content~FUZZ", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_basic_header_name_fuzz", + "%s" % ECHO_URL, + [["onevalue", "twovalue"]], + dict( + headers=[("FUZZ", "myheadervalue")], + filter="content~': myheadervalue' and content~FUZZ", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_static_strquery_fuzz", + "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, + [["value1"]], + dict(filter="content~'query=var=value1'"), + [(200, "/echo")], + None, + ), + ( + "test_static_strquery2_fuzz", + "%s:8000/echo?FUZZ=value1" % LOCAL_DOMAIN, + [["var"]], + dict(filter="content~'query=var=value1'"), + [(200, "/echo")], + None, + ), + ( + "test_basic_cookie_fuzz", + "%s/anything" % HTTPBIN_URL, + [["cookievalue"]], + dict(cookie=["test=FUZZ"], filter="content~FUZZ"), + [(200, "/anything")], + None, + ), # url fuzzing - ("test_url_with_no_path", "http://localhost:8000", [["GET"]], dict(method="FUZZ"), [(200, '/')], None), + ( + "test_url_with_no_path", + "http://localhost:8000", + [["GET"]], + dict(method="FUZZ"), + [(200, "/")], + None, + ), # travis uses old pycurl version ("test_url_not_normalized_by_lib", "http://localhost:8000/echo/FUZZ", [["../../etc/pass"]], dict(), [(200, '/echo/../../etc/pass')], None), - ("test_url_port_fuzz", "%s:FUZZ/dir/a" % LOCAL_DOMAIN, [["8000"]], dict(), [(200, '/dir/a')], None), - ("test_url_hostname_fuzz", "http://FUZZ:8000/dir/a", [["localhost"]], dict(), [(200, '/dir/a')], None), - ("test_url_hostname2_fuzz", "http://FUZZ/dir/a", [["localhost:8000"]], dict(), [(200, '/dir/a')], None), - ("test_url_schema_fuzz", "FUZZ://localhost:8000/dir/a", [["http"]], dict(), [(200, '/dir/a')], None), - ("test_url_all_url_fuzz", "FUZZ", [["http://localhost:8000/dir/a"]], dict(), [(200, '/dir/a')], None), - ("test_url_all_url_fuzz2", "FUZZ", [["%s/anything/datastore/search_get_by_name.php?name=Rake" % HTTPBIN_URL]], dict(), [(200, '/anything/datastore/search_get_by_name.php')], None), - + ( + "test_url_port_fuzz", + "%s:FUZZ/dir/a" % LOCAL_DOMAIN, + [["8000"]], + dict(), + [(200, "/dir/a")], + None, + ), + ( + "test_url_hostname_fuzz", + "http://FUZZ:8000/dir/a", + [["localhost"]], + dict(), + [(200, "/dir/a")], + None, + ), + ( + "test_url_hostname2_fuzz", + "http://FUZZ/dir/a", + [["localhost:8000"]], + dict(), + [(200, "/dir/a")], + None, + ), + ( + "test_url_schema_fuzz", + "FUZZ://localhost:8000/dir/a", + [["http"]], + dict(), + [(200, "/dir/a")], + None, + ), + ( + "test_url_all_url_fuzz", + "FUZZ", + [["http://localhost:8000/dir/a"]], + dict(), + [(200, "/dir/a")], + None, + ), + ( + "test_url_all_url_fuzz2", + "FUZZ", + [["%s/anything/datastore/search_get_by_name.php?name=Rake" % HTTPBIN_URL]], + dict(), + [(200, "/anything/datastore/search_get_by_name.php")], + None, + ), # edge cases - ("test_vhost_fuzz", "%s" % ECHO_URL, [["onevalue", "twovalue"]], dict(headers=[("Host", "FUZZ")], filter="content~'Host:' and content~FUZZ"), [(200, '/echo'), (200, '/echo')], None), - + ( + "test_vhost_fuzz", + "%s" % ECHO_URL, + [["onevalue", "twovalue"]], + dict(headers=[("Host", "FUZZ")], filter="content~'Host:' and content~FUZZ"), + [(200, "/echo"), (200, "/echo")], + None, + ), # payload encoder tests - ("test_encoding", "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, None, dict(payloads=[("list", dict(values="value1", encoder=["md5"]))], filter="content~'path=/echo?var=9946687e5fa0dab5993ededddb398d2e'"), [(200, '/echo')], None), - ("test_nested_encoding", "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, None, dict(payloads=[("list", dict(values="value1", encoder=["none@md5"]))], filter="content~'path=/echo?var=9946687e5fa0dab5993ededddb398d2e'"), [(200, '/echo')], None), - ("test_cat_encoding", "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, None, dict(payloads=[("list", dict(values="value1", encoder=["default"]))], filter="content~'path=/echo?var=' and (content~'9946687e5fa0dab5993ededddb398d2e' or content~'value1')"), [(200, '/echo'), (200, '/echo')], None), - + ( + "test_encoding", + "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, + None, + dict( + payloads=[("list", dict(values="value1", encoder=["md5"]))], + filter="content~'path=/echo?var=9946687e5fa0dab5993ededddb398d2e'", + ), + [(200, "/echo")], + None, + ), + ( + "test_nested_encoding", + "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, + None, + dict( + payloads=[("list", dict(values="value1", encoder=["none@md5"]))], + filter="content~'path=/echo?var=9946687e5fa0dab5993ededddb398d2e'", + ), + [(200, "/echo")], + None, + ), + ( + "test_cat_encoding", + "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, + None, + dict( + payloads=[("list", dict(values="value1", encoder=["default"]))], + filter="content~'path=/echo?var=' and (content~'9946687e5fa0dab5993ededddb398d2e' or content~'value1')", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), # prefilter, slice - ("test_prefilter", "%s/FUZZ" % URL_LOCAL, [["a", "a", "a", "a", "a", "a"]], dict(prefilter="FUZZ|u()", ss="one"), [(200, '/dir/a')], None), - ("test_slice", "%s/FUZZ" % URL_LOCAL, None, dict(payloads=[("list", dict(default="a-a-a-a-a"), "FUZZ|u()")], ss="one"), [(200, '/dir/a')], None), - ("test_slice2", "%s/FUZZ" % URL_LOCAL, None, dict(payloads=[("range", dict(default="1-10"), "FUZZ='1'")]), [(404, '/dir/1')], None), - + ( + "test_prefilter", + "%s/FUZZ" % URL_LOCAL, + [["a", "a", "a", "a", "a", "a"]], + dict(prefilter=["FUZZ|u()"], ss="one"), + [(200, "/dir/a")], + None, + ), + ( + "test_slice", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("list", dict(default="a-a-a-a-a"), "FUZZ|u()")], ss="one"), + [(200, "/dir/a")], + None, + ), + ( + "test_slice2", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("range", dict(default="1-10"), "FUZZ='1'")]), + [(404, "/dir/1")], + None, + ), + ( + "test_slice_rw", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("range", dict(default="1-3"), "'1'")]), + [(404, "/dir/1"), (404, "/dir/1"), (404, "/dir/1")], + None, + ), + ( + "test_slice_rw_int", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("range", dict(default="1-3"), "1")]), + [(404, "/dir/1"), (404, "/dir/1"), (404, "/dir/1")], + None, + ), + ( + "test_slice_rw_upper_int", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("range", dict(default="1-3"), "FUZZ|upper()")]), + [(404, "/dir/1"), (404, "/dir/2"), (404, "/dir/3")], + None, + ), + ( + "test_slice_rw_upper_int", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("range", dict(default="1-3"), "FUZZ|upper()")]), + [(404, "/dir/1"), (404, "/dir/2"), (404, "/dir/3")], + None, + ), + ( + "test_slice_rw_replace_int", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("range", dict(default="1-3"), "FUZZ|replace(1,'one')")]), + [(200, "/dir/one"), (404, "/dir/2"), (404, "/dir/3")], + None, + ), + ( + "test_slice_rw_replace_int_2", + "%s/FUZZ" % URL_LOCAL, + None, + dict(payloads=[("range", dict(default="1-3"), "FUZZ|replace('1','one')")]), + [(200, "/dir/one"), (404, "/dir/2"), (404, "/dir/3")], + None, + ), + ( + "test_slice_rw_replace_int_3", + "%s/FUZZ" % URL_LOCAL, + None, + dict( + payloads=[("list", dict(default="one-two"), "FUZZ|replace('one','three')")] + ), + [(404, "/dir/two"), (404, "/dir/three")], + None, + ), # follow - ("test_follow", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["redirect"]], dict(follow=True, filter="content~'path=/echo'"), [(200, '/echo')], None), - + ( + "test_follow", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [["redirect"]], + dict(follow=True, filter="content~'path=/echo'"), + [(200, "/echo")], + None, + ), # all params - ("test_all_params_get", "%s:8000/echo?var=1&var2=2" % LOCAL_DOMAIN, [["avalue"]], dict(allvars="allvars", filter="content~'query=var=avalue&var2=2' or content~'var=1&var2=avalue'"), [(200, '/echo'), (200, '/echo')], None), - ("test_all_params_post", "%s" % ECHO_URL, [["onevalue"]], dict(allvars="allpost", postdata="a=1&b=2", filter="content~'command=POST' and (content~'a=onevalue' and content~'b=2') or (content~'a=1' and content~'b=onevalue')"), [(200, '/echo'), (200, '/echo')], None), - + ( + "test_all_params_get", + "%s:8000/echo?var=1&var2=2" % LOCAL_DOMAIN, + [["avalue"]], + dict( + allvars="allvars", + filter="content~'query=var=avalue&var2=2' or content~'var=1&var2=avalue'", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_all_params_post", + "%s" % ECHO_URL, + [["onevalue"]], + dict( + allvars="allpost", + postdata="a=1&b=2", + filter="content~'command=POST' and (content~'a=onevalue' and content~'b=2') or (content~'a=1' and content~'b=onevalue')", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), # simple filter - ("test_codes_HC", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(hc=[404]), [(200, '/dir/a'), (200, '/dir/b'), (200, '/dir/c')], None), - ("test_codes_SC", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(sc=[200]), [(200, '/dir/a'), (200, '/dir/b'), (200, '/dir/c')], None), - ("test_codes_HL", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(hl=[4]), [(200, '/dir/b')], None), - ("test_codes_SL", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(sl=[4]), [(200, '/dir/a'), (200, '/dir/c')], None), - ("test_codes_HW", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(hw=[11]), [(200, '/dir/a'), (200, '/dir/b')], None), - ("test_codes_SW", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(sw=[11]), [(200, '/dir/c')], None), - ("test_codes_HH", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(hh=[28]), [(200, '/dir/b'), (200, '/dir/c')], None), - ("test_codes_SH", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(sh=[28]), [(200, '/dir/a')], None), - + ( + "test_codes_HC", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(hc=[404]), + [(200, "/dir/a"), (200, "/dir/b"), (200, "/dir/c")], + None, + ), + ( + "test_codes_SC", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(sc=[200]), + [(200, "/dir/a"), (200, "/dir/b"), (200, "/dir/c")], + None, + ), + ( + "test_codes_HL", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(hl=[4]), + [(200, "/dir/b")], + None, + ), + ( + "test_codes_SL", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(sl=[4]), + [(200, "/dir/a"), (200, "/dir/c")], + None, + ), + ( + "test_codes_HW", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(hw=[11]), + [(200, "/dir/a"), (200, "/dir/b")], + None, + ), + ( + "test_codes_SW", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(sw=[11]), + [(200, "/dir/c")], + None, + ), + ( + "test_codes_HH", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(hh=[28]), + [(200, "/dir/b"), (200, "/dir/c")], + None, + ), + ( + "test_codes_SH", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(sh=[28]), + [(200, "/dir/a")], + None, + ), # combining simple filters - ("test_hchlhhhw", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c", "d", "e", "f"]], dict(hc=[404], hl=[4], hh=[300]), [(200, '/dir/b')], None), - ("test_shsw", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c", "d", "e", "f"]], dict(sh=[28], sw=[6]), [(200, '/dir/a')], None), - + ( + "test_hchlhhhw", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c", "d", "e", "f"]], + dict(hc=[404], hl=[4], hh=[300]), + [(200, "/dir/b")], + None, + ), + ( + "test_shsw", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c", "d", "e", "f"]], + dict(sh=[28], sw=[6]), + [(200, "/dir/a")], + None, + ), # regex filter - ("test_ss", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c", "d", "e", "f"]], dict(ss="one"), [(200, '/dir/a'), (200, '/dir/b')], None), - ("test_hs", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(hs="one"), [(200, '/dir/c')], None), - ("test_regex_sc", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c", "d", "e", "f"]], dict(sc=[200], ss="one"), [(200, '/dir/a'), (200, '/dir/b')], None), - ("test_regex_hc", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c", "d", "e", "f"]], dict(hc=[200], ss="one"), [], None), - + ( + "test_ss", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c", "d", "e", "f"]], + dict(ss="one"), + [(200, "/dir/a"), (200, "/dir/b")], + None, + ), + ( + "test_hs", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(hs="one"), + [(200, "/dir/c")], + None, + ), + ( + "test_regex_sc", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c", "d", "e", "f"]], + dict(sc=[200], ss="one"), + [(200, "/dir/a"), (200, "/dir/b")], + None, + ), + ( + "test_regex_hc", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c", "d", "e", "f"]], + dict(hc=[200], ss="one"), + [], + None, + ), # complex filter - ("test_filter_clh", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(filter="c!=404 and l!=4 and h!=300 and w!=6"), [(200, '/dir/b')], None), - ("test_filter_hw", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(filter="h=28 or w=6"), [(200, '/dir/a')], None), - ("test_filter_intext", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(filter="content~'one'"), [(200, '/dir/a'), (200, '/dir/b')], None), - ("test_filter_intext2", "%s/FUZZ" % URL_LOCAL, [["a", "b", "c"]], dict(filter="content!~'one'"), [(200, '/dir/c')], None), - ("test_dict_filter_strquery_fuzz", "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, [["value1"]], dict(filter="r.params.get~'value1'"), [(200, '/echo')], None), - + ( + "test_filter_clh", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(filter="c!=404 and l!=4 and h!=300 and w!=6"), + [(200, "/dir/b")], + None, + ), + ( + "test_filter_hw", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(filter="h=28 or w=6"), + [(200, "/dir/a")], + None, + ), + ( + "test_combined_filter", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(filter="h=28", sw=[6]), + [(200, "/dir/a")], + None, + ), + ( + "test_filter_intext", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(filter="content~'one'"), + [(200, "/dir/a"), (200, "/dir/b")], + None, + ), + ( + "test_filter_intext2", + "%s/FUZZ" % URL_LOCAL, + [["a", "b", "c"]], + dict(filter="content!~'one'"), + [(200, "/dir/c")], + None, + ), + ( + "test_dict_filter_strquery_fuzz", + "%s:8000/echo?var=FUZZ" % LOCAL_DOMAIN, + [["value1"]], + dict(filter="r.params.get~'value1'"), + [(200, "/echo")], + None, + ), # baseline - ("test_baseline", "%s/FUZZ{notthere}" % URL_LOCAL, [["a", "b", "c"]], dict(), [(200, '/dir/a'), (200, '/dir/b'), (200, '/dir/c'), (404, "/dir/notthere")], None), - ("test_baseline2", "%s/FUZZ{notthere}" % URL_LOCAL, [["a", "b", "c", "d", "e", "f"]], dict(hc=["BBB"]), [(200, '/dir/a'), (200, '/dir/b'), (200, '/dir/c')] + [(404, '/dir/notthere')], None), - ("test_baseline3", "%s/FUZZ{notthere}" % URL_LOCAL, [["a", "b", "c"]], dict(hc=[200]), [(404, "/dir/notthere")], None), + ( + "test_baseline_header", + "%s" % ECHO_URL, + [["twovalue"]], + dict( + headers=[("FUZZ{onevalue}", "admin")], + filter="(content~'onevalue:' or content~'twovalue:') and content~'admin'", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_baseline_header_content", + "%s" % ECHO_URL, + [["twovalue"]], + dict( + headers=[("myheader", "FUZZ{onevalue}")], + filter="content~'Myheader:' and (content~FUZZ or content~BBB)", + ), + [(200, "/echo"), (200, "/echo")], + None, + ), + ( + "test_baseline", + "%s/FUZZ{notthere}" % URL_LOCAL, + [["a", "b", "c"]], + dict(), + [(200, "/dir/a"), (200, "/dir/b"), (200, "/dir/c"), (404, "/dir/notthere")], + None, + ), + ( + "test_baseline2", + "%s/FUZZ{notthere}" % URL_LOCAL, + [["a", "b", "c", "d", "e", "f"]], + dict(hc=["BBB"]), + [(200, "/dir/a"), (200, "/dir/b"), (200, "/dir/c")] + [(404, "/dir/notthere")], + None, + ), + ( + "test_baseline_filter", + "%s/FUZZ{notthere}" % URL_LOCAL, + [["a", "b", "c", "d", "e", "f"]], + dict(filter="c!=BBB"), + [(200, "/dir/a"), (200, "/dir/b"), (200, "/dir/c")] + [(404, "/dir/notthere")], + None, + ), + ( + "test_baseline3", + "%s/FUZZ{notthere}" % URL_LOCAL, + [["a", "b", "c"]], + dict(hc=[200]), + [(404, "/dir/notthere")], + None, + ), # XXX("test_scheme_baseline_fuzz", "FUZZ{HTTP}://localhost:8000/dir/a", [["https"]], dict(), [(200, '/dir/a')], None), - # iterators - ("test_product", "%s:8000/iterators/FUZZFUZ2Z" % LOCAL_DOMAIN, [["a", "b"], ["c"]], dict(iterator="product"), [(200, '/iterators/ac'), (404, '/iterators/bc')], None), - ("test_zip", "%s:8000/iterators/FUZZFUZ2Z" % LOCAL_DOMAIN, [["a", "b"], ["c"]], dict(iterator="zip"), [(200, '/iterators/ac')], None), - ("test_chain", "%s/FUZZ" % URL_LOCAL, [["a", "b"], ["c"]], dict(iterator="chain"), [(200, '/dir/a'), (200, '/dir/b'), (200, '/dir/c')], None), - + ( + "test_product", + "%s:8000/iterators/FUZZFUZ2Z" % LOCAL_DOMAIN, + [["a", "b"], ["c"]], + dict(iterator="product"), + [(200, "/iterators/ac"), (404, "/iterators/bc")], + None, + ), + ( + "test_zip", + "%s:8000/iterators/FUZZFUZ2Z" % LOCAL_DOMAIN, + [["a", "b"], ["c"]], + dict(iterator="zip"), + [(200, "/iterators/ac")], + None, + ), + ( + "test_chain", + "%s/FUZZ" % URL_LOCAL, + [["a", "b"], ["c"]], + dict(iterator="chain"), + [(200, "/dir/a"), (200, "/dir/b"), (200, "/dir/c")], + None, + ), # recursive - ("test_rlevel_1", "%s:8000/recursive_dir/FUZZ" % LOCAL_DOMAIN, [["a", "b", "c"]], dict(sc=[301], rlevel=1), [(301, '/recursive_dir/a'), (301, '/recursive_dir/a/b')], None), - ("test_rlevel_2", "%s:8000/recursive_dir/FUZZ" % LOCAL_DOMAIN, [["a", "b", "c"]], dict(sc=[301], rlevel=2), [(301, '/recursive_dir/a'), (301, '/recursive_dir/a/b'), (301, '/recursive_dir/a/b/c')], None), - ("test_rlevel_1_post", "%s:8000/echo/FUZZ/" % LOCAL_DOMAIN, [["a"]], dict(filter="content~'command=POST' and content~'POST_DATA=a=1'", postdata="a=1", rlevel=1), [(200, '/echo/a/'), (200, '/echo/a/a')], None), - + ( + "test_rlevel_1", + "%s:8000/recursive_dir/FUZZ" % LOCAL_DOMAIN, + [["a", "b", "c"]], + dict(sc=[301], rlevel=1), + [(301, "/recursive_dir/a"), (301, "/recursive_dir/a/b")], + None, + ), + ( + "test_rlevel_2", + "%s:8000/recursive_dir/FUZZ" % LOCAL_DOMAIN, + [["a", "b", "c"]], + dict(sc=[301], rlevel=2), + [ + (301, "/recursive_dir/a"), + (301, "/recursive_dir/a/b"), + (301, "/recursive_dir/a/b/c"), + ], + None, + ), + ( + "test_rlevel_1_post", + "%s:8000/echo/FUZZ/" % LOCAL_DOMAIN, + [["a"]], + dict( + filter="content~'command=POST' and content~'POST_DATA=a=1'", + postdata="a=1", + rlevel=1, + ), + [(200, "/echo/a/"), (200, "/echo/a/a")], + None, + ), # plugins - ("test_robots", "%s:8000/plugins/FUZZ" % LOCAL_DOMAIN, [["robots.txt"]], dict(script="robots"), [(404, '/cal_endar/'), (404, '/crawlsnags/'), (404, '/osrun/'), (200, '/plugins/robots.txt'), (200, '/static/')], None), - ("test_robots_hc", "%s:8000/plugins/FUZZ" % LOCAL_DOMAIN, [["robots.txt"]], dict(hc=[404], script="robots"), [(200, '/plugins/robots.txt'), (200, '/static/')], None), - ("test_plugins_filter", "%s/FUZZ" % HTTPBIN_URL, [["anything"]], dict(script='headers', filter="plugins~'unicorn'"), [(200, '/anything')], None), + ( + "test_robots", + "%s:8000/plugins/FUZZ" % LOCAL_DOMAIN, + [["robots.txt"]], + dict(script="robots"), + [ + (404, "/cal_endar/"), + (404, "/crawlsnags/"), + (404, "/osrun/"), + (200, "/plugins/robots.txt"), + (200, "/static/"), + ], + None, + ), + ( + "test_robots_hc", + "%s:8000/plugins/FUZZ" % LOCAL_DOMAIN, + [["robots.txt"]], + dict(hc=[404], script="robots"), + [(200, "/plugins/robots.txt"), (200, "/static/")], + None, + ), + ( + "test_plugins_filter", + "%s/FUZZ" % HTTPBIN_URL, + [["anything"]], + dict(script="headers", filter="plugins~'unicorn'"), + [(200, "/anything")], + None, + ), ] scanmode_tests = [ - ("test_scanmode", "%s:666/FUZZ" % LOCAL_DOMAIN, [["a", "b", "c"]], dict(scanmode=True), [(-1, '/a'), (-1, '/b'), (-1, '/c')], None), - ("test_scanmode_sc", "%s:666/FUZZ" % LOCAL_DOMAIN, [["a", "b", "c"]], dict(scanmode=True, sc=[-1]), [(-1, '/a'), (-1, '/b'), (-1, '/c')], None), - ("test_scanmode_sc_xxx", "%s:666/FUZZ" % LOCAL_DOMAIN, [["a", "b", "c"]], dict(scanmode=True, sc=["XXX"]), [(-1, '/a'), (-1, '/b'), (-1, '/c')], None), - ("test_scanmode_hc", "%s:666/FUZZ" % LOCAL_DOMAIN, [["a", "b", "c"]], dict(scanmode=True, hc=[-1]), [], None), - ("test_scanmode_hc_xxx", "%s:666/FUZZ" % LOCAL_DOMAIN, [["a", "b", "c"]], dict(scanmode=True, hc=["XXX"]), [], None), + ( + "test_scanmode", + "%s:666/FUZZ" % LOCAL_DOMAIN, + [["a", "b", "c"]], + dict(scanmode=True), + [(-1, "/a"), (-1, "/b"), (-1, "/c")], + None, + ), + ( + "test_scanmode_sc", + "%s:666/FUZZ" % LOCAL_DOMAIN, + [["a", "b", "c"]], + dict(scanmode=True, sc=[-1]), + [(-1, "/a"), (-1, "/b"), (-1, "/c")], + None, + ), + ( + "test_scanmode_sc_xxx", + "%s:666/FUZZ" % LOCAL_DOMAIN, + [["a", "b", "c"]], + dict(scanmode=True, sc=["XXX"]), + [(-1, "/a"), (-1, "/b"), (-1, "/c")], + None, + ), + ( + "test_scanmode_hc", + "%s:666/FUZZ" % LOCAL_DOMAIN, + [["a", "b", "c"]], + dict(scanmode=True, hc=[-1]), + [], + None, + ), + ( + "test_scanmode_hc_xxx", + "%s:666/FUZZ" % LOCAL_DOMAIN, + [["a", "b", "c"]], + dict(scanmode=True, hc=["XXX"]), + [], + None, + ), ] error_tests = [ - ("test_url_schema_error_fuzz", "FUZZ://localhost:8000/dir/a", [["https"]], dict(), [(200, '/dir/a')], "Pycurl error 35"), - ("test_all_params_fuzz_error", "%s:8000/echo?var=FUZZ&var2=2" % LOCAL_DOMAIN, [["avalue"]], dict(allvars="allvars", filter="content~'query=var=avalue&var2=2' or content~'var=1&var2=avalue'"), [(200, '/echo'), (200, '/echo')], "FUZZ words not allowed when using all parameters brute forcing"), - ("test_all_params_no_var", "%s:8000/echo" % LOCAL_DOMAIN, [["avalue"]], dict(allvars="allvars", filter="content~'query=var=avalue&var2=2' or content~'var=1&var2=avalue'"), [(200, '/echo'), (200, '/echo')], "No variables on specified variable set"), - ("test_bad_port", "%s:6666/FUZZ" % LOCAL_DOMAIN, [list(range(1))], dict(), [], 'Failed to connect to localhost port 6666'), - ("test_bad_num_payloads", "%s:8000/FUZZ" % LOCAL_DOMAIN, [list(range(1)), list(range(1))], dict(), [], 'FUZZ words and number of payloads do not match'), - ("test_bad_proxy", "%s:8000/FUZZ" % LOCAL_DOMAIN, [list(range(1))], dict(proxies=[("localhost", 888, "HTTP")]), [], 'Failed to connect to localhost port 888'), - ("test_bad_num_dic", "%s:8000/iterators/FUZZ" % LOCAL_DOMAIN, [list(range(1))], dict(iterator="zip"), [], 'Several dictionaries must be used when specifying an iterator'), + ( + "test_url_schema_error_fuzz", + "FUZZ://localhost:8000/dir/a", + [["https"]], + dict(), + [(200, "/dir/a")], + "Pycurl error 35", + ), + ( + "test_all_params_fuzz_error", + "%s:8000/echo?var=FUZZ&var2=2" % LOCAL_DOMAIN, + [["avalue"]], + dict( + allvars="allvars", + filter="content~'query=var=avalue&var2=2' or content~'var=1&var2=avalue'", + ), + [(200, "/echo"), (200, "/echo")], + "FUZZ words not allowed when using all parameters brute forcing", + ), + ( + "test_all_params_no_var", + "%s:8000/echo" % LOCAL_DOMAIN, + [["avalue"]], + dict( + allvars="allvars", + filter="content~'query=var=avalue&var2=2' or content~'var=1&var2=avalue'", + ), + [(200, "/echo"), (200, "/echo")], + "No variables on specified variable set", + ), + ( + "test_bad_port", + "%s:6666/FUZZ" % LOCAL_DOMAIN, + [list(range(1))], + dict(), + [], + "Failed to connect to localhost port 6666", + ), + ( + "test_bad_num_payloads", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [list(range(1)), list(range(1))], + dict(), + [], + "FUZZ words and number of payloads do not match", + ), + ( + "test_bad_proxy", + "%s:8000/FUZZ" % LOCAL_DOMAIN, + [list(range(1))], + dict(proxies=[("localhost", 888, "HTTP")]), + [], + "Failed to connect to localhost port 888", + ), + ( + "test_bad_num_dic", + "%s:8000/iterators/FUZZ" % LOCAL_DOMAIN, + [list(range(1))], + dict(iterator="zip"), + [], + "Several dictionaries must be used when specifying an iterator", + ), ] @@ -257,6 +1316,7 @@ class DynamicTests(unittest.TestCase): """ Dummy class that will be populated dynamically with all the tests """ + pass @@ -280,14 +1340,23 @@ def test(self): for original_host, proxied_host in REPLACE_HOSTNAMES: proxied_url = proxied_url.replace(original_host, proxied_host) if proxied_payloads: - proxied_payloads = [[payload.replace(original_host, proxied_host) for payload in payloads_list] for payloads_list in proxied_payloads] - - if 'connect_to_ip' in extra_params and extra_params['connect_to_ip']: - extra_params['connect_to_ip']['ip'] = 'httpbin' - extra_params['connect_to_ip']['port'] = '80' + proxied_payloads = [ + [ + payload.replace(original_host, proxied_host) + for payload in payloads_list + ] + for payloads_list in proxied_payloads + ] + + if "connect_to_ip" in extra_params and extra_params["connect_to_ip"]: + extra_params["connect_to_ip"]["ip"] = "httpbin" + extra_params["connect_to_ip"]["port"] = "80" with wfuzz.FuzzSession(url=proxied_url) as s: - same_list = [(x.code, x.history.urlparse.path) for x in s.get_payloads(proxied_payloads).fuzz(**extra_params)] + same_list = [ + (x.code, x.history.urlparse.path) + for x in s.get_payloads(proxied_payloads).fuzz(**extra_params) + ] self.assertEqual(sorted(ret_list), sorted(same_list)) else: @@ -315,7 +1384,9 @@ def test(self): filename = os.path.join(defult_tmp_dir, temp_name) # Wfuzz results - with wfuzz.FuzzSession(url=url, **dict(list(params.items()) + list(dict(save=filename).items()))) as s: + with wfuzz.FuzzSession( + url=url, **dict(list(params.items()) + list(dict(save=filename).items())) + ) as s: if payloads is None: fuzzed = s.fuzz() else: @@ -324,13 +1395,17 @@ def test(self): ret_list = [(x.code, x.history.urlparse.path) for x in fuzzed] # repeat test with performaing same saved request - with wfuzz.FuzzSession(payloads=[("wfuzzp", dict(fn=filename))], url="FUZZ") as s: + with wfuzz.FuzzSession( + payloads=[("wfuzzp", dict(fn=filename))], url="FUZZ" + ) as s: same_list = [(x.code, x.history.urlparse.path) for x in s.fuzz()] self.assertEqual(sorted(ret_list), sorted(same_list)) # repeat test with performaing FUZZ[url] saved request - with wfuzz.FuzzSession(payloads=[("wfuzzp", dict(fn=filename))], url="FUZZ[url]") as s: + with wfuzz.FuzzSession( + payloads=[("wfuzzp", dict(fn=filename))], url="FUZZ[url]" + ) as s: same_list = [(x.code, x.history.urlparse.path) for x in s.fuzz()] self.assertEqual(sorted(ret_list), sorted(same_list)) @@ -361,14 +1436,19 @@ def test(self): if payloads is None: same_list = [(x.code, x.history.urlparse.path) for x in s.fuzz()] else: - same_list = [(x.code, x.history.urlparse.path) for x in s.get_payloads(payloads).fuzz()] + same_list = [ + (x.code, x.history.urlparse.path) + for x in s.get_payloads(payloads).fuzz() + ] self.assertEqual(sorted(ret_list), sorted(same_list)) return test -def wfuzz_me_test_generator_previous_session(prev_session_cli, next_session_cli, expected_list): +def wfuzz_me_test_generator_previous_session( + prev_session_cli, next_session_cli, expected_list +): def test(self): temp_name = next(tempfile._get_candidate_names()) defult_tmp_dir = tempfile._get_default_tempdir() @@ -377,18 +1457,23 @@ def test(self): # first session with wfuzz.get_session(prev_session_cli) as s: - ret_list = [x.eval(x._description) if x._description else x.description for x in s.fuzz(save=filename)] + ret_list = [ + x._field() if x._fields else x.description + for x in s.fuzz(save=filename) + ] # second session wfuzzp as payload with wfuzz.get_session(next_session_cli.replace("$$PREVFILE$$", filename)) as s: - ret_list = [x.eval(x._description) if x._description else x.description for x in s.fuzz()] + ret_list = [x._field() if x._fields else x.description for x in s.fuzz()] self.assertEqual(sorted(ret_list), sorted(expected_list)) return test -def create_test(test_name, url, payloads, params, expected_res, extra_params, exception_str): +def create_test( + test_name, url, payloads, params, expected_res, extra_params, exception_str +): test_fn = wfuzz_me_test_generator(url, payloads, params, expected_res, extra_params) if exception_str: test_fn_exc = wfuzz_me_test_generator_exception(test_fn, exception_str) @@ -405,7 +1490,9 @@ def create_tests_from_list(test_list): create_test(test_name, url, payloads, params, expected_res, None, exception_str) -def duplicate_tests_diff_params(test_list, group, next_extra_params, previous_extra_params): +def duplicate_tests_diff_params( + test_list, group, next_extra_params, previous_extra_params +): """ Ignores expected_res and generates wfuzz tests that run 2 times with different params, expecting same results. @@ -424,7 +1511,9 @@ def duplicate_tests_diff_params(test_list, group, next_extra_params, previous_ex if previous_extra_params: prev_extra.update(previous_extra_params) - create_test(new_test, url, payloads, prev_extra, None, next_extra, exception_str) + create_test( + new_test, url, payloads, prev_extra, None, next_extra, exception_str + ) def duplicate_tests(test_list, group, test_gen_fun): @@ -463,14 +1552,18 @@ def create_tests(): """ if testing_savedsession_tests: - create_savedsession_tests(testing_savedsession_tests, wfuzz_me_test_generator_previous_session) + create_savedsession_tests( + testing_savedsession_tests, wfuzz_me_test_generator_previous_session + ) return if testing_tests: create_tests_from_list(testing_tests) duplicate_tests(testing_tests, "recipe", wfuzz_me_test_generator_recipe) duplicate_tests(testing_tests, "saveres", wfuzz_me_test_generator_saveres) - duplicate_tests_diff_params(testing_tests, "_proxy_", dict(proxies=[("localhost", 8080, "HTTP")]), None) + duplicate_tests_diff_params( + testing_tests, "_proxy_", dict(proxies=[("localhost", 8080, "HTTP")]), None + ) else: # this are the basics basic_functioning_tests = [error_tests, scanmode_tests, basic_tests] @@ -479,7 +1572,9 @@ def create_tests(): create_tests_from_list(t) # description tests - create_savedsession_tests(savedsession_tests, wfuzz_me_test_generator_previous_session) + create_savedsession_tests( + savedsession_tests, wfuzz_me_test_generator_previous_session + ) # duplicate tests with recipe duplicate_tests(basic_tests, "recipe", wfuzz_me_test_generator_recipe) @@ -488,10 +1583,12 @@ def create_tests(): duplicate_tests(basic_tests, "saveres", wfuzz_me_test_generator_saveres) # duplicate tests with proxy - duplicate_tests_diff_params(basic_tests, "_proxy_", dict(proxies=[("localhost", 8080, "HTTP")]), None) + duplicate_tests_diff_params( + basic_tests, "_proxy_", dict(proxies=[("localhost", 8080, "HTTP")]), None + ) create_tests() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_api.py b/tests/test_api.py index 5b953230..0c7f8a65 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,8 +6,8 @@ import wfuzz from wfuzz.facade import Facade -from wfuzz.fuzzobjects import FuzzRequest from wfuzz.fuzzobjects import FuzzResult +from wfuzz.fuzzrequest import FuzzRequest try: # Python >= 3.3 @@ -16,67 +16,15 @@ # Python < 3.3 import mock -LOCAL_DOMAIN = "http://localhost" -URL_LOCAL = "%s:8000/dir" % (LOCAL_DOMAIN) -HTTPD_PORT = 8000 - -ECHO_URL = "%s:8000/echo" % (LOCAL_DOMAIN) - class APITests(unittest.TestCase): - def test_get_payload(self): - payload_list = wfuzz.get_payload(list(range(4))).data.get('dictio')[0] - self.assertEqual(sorted(payload_list), sorted([0, 1, 2, 3])) - - def test_get_payloads(self): - payload_list = wfuzz.get_payload([list(range(2)), list(range(2))]).data.get('dictio')[0] - self.assertEqual(sorted(payload_list), sorted([[0, 1], [0, 1]])) - - def test_encoders(self): - encoders = { - 'none': ('test', 'test'), - 'urlencode': ("../=?&", '../%3D%3F%26'), - 'double_urlencode': ("../=?&", '../%253D%253F%2526'), - 'double_urlencode': ("../=?&", '../%253D%253F%2526'), - 'base64': ('admin', 'YWRtaW4='), - - 'sha1': ('admin', 'd033e22ae348aeb5660fc2140aec35850c4da997'), - 'md5': ('admin', '21232f297a57a5a743894a0e4a801fc3'), - 'hexlify': ('admin', '61646d696e'), - 'html_escape': ('<>&\'"/', "<>&'"/"), - 'html_decimal': ('<>&\'"/', '<>&'"/'), - 'html_hexadecimal': ('<>&\'"/', '<>&'"/'), - - 'mysql_char': ('admin', 'CHAR(97,100,109,105,110)'), - 'mssql_char': ('admin', 'CHAR(97)+CHAR(100)+CHAR(109)+CHAR(105)+CHAR(110)'), - 'oracle_char': ('admin', 'chr(97)||chr(100)||chr(109)||chr(105)||chr(110)'), - } - - for key, values in list(encoders.items()): - value, expected = values - self.assertEqual(wfuzz.encode(key, value), expected) - - for key, values in [items for items in list(encoders.items()) if items[0] not in ['html_hexadecimal', 'html_decimal', 'md5', 'sha1', 'html_escape']]: - value, expected = values - self.assertEqual(wfuzz.decode(key, expected), value) - - def test_decode(self): - payload_list = wfuzz.get_payload([list(range(2)), list(range(2))]).data.get('dictio')[0] - self.assertEqual(sorted(payload_list), sorted([[0, 1], [0, 1]])) - - def test_get_session(self): - data = wfuzz.get_session('-z range,0-4 http://127.0.0.1/FUZZ').data - - self.assertEqual(data.get('url'), 'http://127.0.0.1/FUZZ') - self.assertEqual(data.get('payloads'), [('range', {'default': '0-4', 'encoder': None}, None)]) - def test_payload_description(self): class mock_saved_session(object): - def __init__(self, description, show_field): + def __init__(self, fields, show_field): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" fuzz_res = FuzzResult(history=fr) - fuzz_res._description = description + fuzz_res._fields = fields fuzz_res._show_field = show_field self.outfile = BytesIO() @@ -102,44 +50,95 @@ def tell(self): # load plugins before mocking file object Facade().payloads - m = mock.MagicMock(name='open', spec=open) - m.return_value = mock_saved_session("r.params.all", True) + m = mock.MagicMock(name="open", spec=open) + m.return_value = mock_saved_session(["r.params.all"], True) - mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + mocked_fun = ( + "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + ) with mock.patch(mocked_fun, m): - payload_list = list(wfuzz.payload(**{'show_field': True, 'description': 'r', 'payloads': [('wfuzzp', {'default': 'mockedfile', 'encoder': None}, None)]})) - self.assertEqual([res[0].description for res in payload_list], [{'param': '1', 'param2': '2'}]) - - m = mock.MagicMock(name='open', spec=open) - m.return_value = mock_saved_session("url", None) - - mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + payload_list = list( + wfuzz.payload( + **{ + "show_field": True, + "fields": ["r"], + "payloads": [ + ("wfuzzp", {"default": "mockedfile", "encoder": None}, None) + ], + } + ) + ) + self.assertEqual( + sorted( + "-".join([res[0].description for res in payload_list]).split("\n") + ), + sorted(["param=1", "param2=2"]), + ) + + m = mock.MagicMock(name="open", spec=open) + m.return_value = mock_saved_session(["url"], None) + + mocked_fun = ( + "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + ) with mock.patch(mocked_fun, m): - payload_list = list(wfuzz.payload(**{'show_field': True, 'description': 'r', 'payloads': [('wfuzzp', {'default': 'mockedfile', 'encoder': None}, None)]})) - self.assertEqual([res[0].description for res in payload_list], ['http://www.wfuzz.org/path?param=1¶m2=2']) - - m = mock.MagicMock(name='open', spec=open) - m.return_value = mock_saved_session("r.scheme", False) - - mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + payload_list = list( + wfuzz.payload( + **{ + "show_field": True, + "fields": ["r"], + "payloads": [ + ("wfuzzp", {"default": "mockedfile", "encoder": None}, None) + ], + } + ) + ) + self.assertEqual( + [res[0].description for res in payload_list], + ["http://www.wfuzz.org/path?param=1¶m2=2"], + ) + + m = mock.MagicMock(name="open", spec=open) + m.return_value = mock_saved_session(["r.scheme"], False) + + mocked_fun = ( + "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + ) with mock.patch(mocked_fun, m): - payload_list = list(wfuzz.payload(**{'show_field': True, 'description': 'r', 'payloads': [('wfuzzp', {'default': 'mockedfile', 'encoder': None}, None)]})) - self.assertEqual([res[0].description for res in payload_list], ['http://www.wfuzz.org/path?param=1¶m2=2 | http']) + payload_list = list( + wfuzz.payload( + **{ + "show_field": True, + "fields": ["r"], + "payloads": [ + ("wfuzzp", {"default": "mockedfile", "encoder": None}, None) + ], + } + ) + ) + self.assertEqual( + [res[0].description for res in payload_list], + ["http://www.wfuzz.org/path?param=1¶m2=2 | http"], + ) def test_payload(self): - payload_list = list(wfuzz.payload(**{'payloads': [('range', {'default': '0-4', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('0',), ('1',), ('2',), ('3',), ('4',)]) - - payload_list = list(wfuzz.payload(**{'payloads': [('buffer_overflow', {'default': '10', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('AAAAAAAAAA',)]) - - with mock.patch('os.walk') as mocked_oswalk: + with mock.patch("os.walk") as mocked_oswalk: mocked_oswalk.return_value = [ - ('foo', ('bar',), ('baz',)), - ('foo/bar', (), ('spam', 'eggs')), + ("foo", ("bar",), ("baz",)), + ("foo/bar", (), ("spam", "eggs")), ] - payload_list = list(wfuzz.payload(**{'payloads': [('dirwalk', {'default': 'foo', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('baz',), ('bar/spam',), ('bar/eggs',)]) + payload_list = list( + wfuzz.payload( + **{ + "payloads": [ + ("dirwalk", {"default": "foo", "encoder": None}, None) + ] + } + ) + ) + self.assertEqual( + sorted(payload_list), sorted([("baz",), ("bar/spam",), ("bar/eggs",)]) + ) class mock_file(object): def __init__(self): @@ -156,41 +155,20 @@ def seek(self, pos): next = __next__ # for Python 2 - m = mock.MagicMock(name='open', spec=open) + m = mock.MagicMock(name="open", spec=open) m.return_value = mock_file() - mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + mocked_fun = ( + "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + ) with mock.patch(mocked_fun, m): - payload_list = list(wfuzz.payload(**{'payloads': [('file', {'default': 'mockedfile', 'encoder': None}, None)]})) - self.assertEqual(sorted(payload_list), sorted([('one',), ('two',)])) - - payload_list = list(wfuzz.payload(**{'payloads': [('hexrange', {'default': '09-10', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('09',), ('0a',), ('0b',), ('0c',), ('0d',), ('0e',), ('0f',), ('10',)]) - - payload_list = list(wfuzz.payload(**{'payloads': [('hexrange', {'default': '009-00B', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('009',), ('00a',), ('00b',)]) - - payload_list = list(wfuzz.payload(**{'payloads': [('ipnet', {'default': '192.168.0.1/30', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('192.168.0.1',), ('192.168.0.2',)]) - - payload_list = list(wfuzz.payload(**{'payloads': [('iprange', {'default': '192.168.0.1-192.168.0.2', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('192.168.0.1',), ('192.168.0.2',)]) - - payload_list = list(wfuzz.payload(**{'payloads': [('list', {'default': 'a-b', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('a',), ('b',)]) - - payload_list = list(wfuzz.payload(**{'payloads': [('list', {'default': 'a\\-b-b', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('a-b',), ('b',)]) - - payload_list = list(wfuzz.payload(**{'payloads': [('range', {'default': '1-2', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('1',), ('2',)]) - - def test_iterator(self): - payload_list = list(wfuzz.payload(**{'iterator': 'zip', 'payloads': [('range', {'default': '0-2', 'encoder': None}, None), ('range', {'default': '0-2', 'encoder': None}, None)]})) - self.assertEqual(sorted(payload_list), sorted([('0', '0'), ('1', '1'), ('2', '2')])) - - payload_list = list(wfuzz.payload(**{'iterator': 'chain', 'payloads': [('range', {'default': '0-2', 'encoder': None}, None), ('range', {'default': '0-2', 'encoder': None}, None)]})) - self.assertEqual(sorted(payload_list), sorted([('0',), ('0',), ('1',), ('1',), ('2',), ('2',)])) - - payload_list = list(wfuzz.payload(**{'iterator': 'product', 'payloads': [('range', {'default': '0-2', 'encoder': None}, None), ('range', {'default': '0-2', 'encoder': None}, None)]})) - self.assertEqual(sorted(payload_list), sorted([('0', '0'), ('0', '1'), ('0', '2'), ('1', '0'), ('1', '1'), ('1', '2'), ('2', '0'), ('2', '1'), ('2', '2')])) + payload_list = list( + wfuzz.payload( + **{ + "payloads": [ + ("file", {"default": "mockedfile", "encoder": None}, None) + ] + } + ) + ) + self.assertEqual(sorted(payload_list), sorted([("one",), ("two",)])) diff --git a/tests/test_clparser.py b/tests/test_clparser.py index a4180a2a..2213539d 100644 --- a/tests/test_clparser.py +++ b/tests/test_clparser.py @@ -6,46 +6,69 @@ class CLParserTest(unittest.TestCase): def test_listplugins(self): with self.assertRaises(SystemExit) as cm: - CLParser(['wfuzz', '-e', 'iterators']).parse_cl() + CLParser(["wfuzz", "-e", "iterators"]).parse_cl() self.assertEqual(cm.exception.code, 0) def test_ip_option(self): - options = CLParser(['wfuzz', '--ip', '127.0.0.1']).parse_cl() + options = CLParser(["wfuzz", "--ip", "127.0.0.1"]).parse_cl() - self.assertEqual(options.data['connect_to_ip']['ip'], '127.0.0.1') - self.assertEqual(options.data['connect_to_ip']['port'], '80') + self.assertEqual(options.data["connect_to_ip"]["ip"], "127.0.0.1") + self.assertEqual(options.data["connect_to_ip"]["port"], "80") - options = CLParser(['wfuzz', '--ip', '127.0.0.1:22']).parse_cl() + options = CLParser(["wfuzz", "--ip", "127.0.0.1:22"]).parse_cl() - self.assertEqual(options.data['connect_to_ip']['ip'], '127.0.0.1') - self.assertEqual(options.data['connect_to_ip']['port'], '22') + self.assertEqual(options.data["connect_to_ip"]["ip"], "127.0.0.1") + self.assertEqual(options.data["connect_to_ip"]["port"], "22") - options = CLParser(['wfuzz', '--ip', '127.0.0.1:']).parse_cl() + options = CLParser(["wfuzz", "--ip", "127.0.0.1:"]).parse_cl() - self.assertEqual(options.data['connect_to_ip']['ip'], '127.0.0.1') - self.assertEqual(options.data['connect_to_ip']['port'], '80') + self.assertEqual(options.data["connect_to_ip"]["ip"], "127.0.0.1") + self.assertEqual(options.data["connect_to_ip"]["port"], "80") with self.assertRaises(Exception) as cm: - options = CLParser(['wfuzz', '--ip', ':80']).parse_cl() + options = CLParser(["wfuzz", "--ip", ":80"]).parse_cl() self.assertTrue("An IP must be specified" in str(cm.exception)) def test_ze_zd_option(self): with self.assertRaises(Exception) as cm: - options = CLParser(['wfuzz', '-z', 'range,0-10', '--zD', '0-10', 'url']).parse_cl() + options = CLParser( + ["wfuzz", "-z", "range,0-10", "--zD", "0-10", "url"] + ).parse_cl() self.assertTrue("exclusive" in str(cm.exception)) - options = CLParser(['wfuzz', '-z', 'range', '--zD', '0-1', '--zE', 'md5', 'url']).parse_cl() - self.assertEqual(options.data['payloads'], [('range', {'default': '0-1', 'encoder': ['md5']}, None)]) - - options = CLParser(['wfuzz', '-z', 'range,0-1', '--zE', 'md5', 'url']).parse_cl() - self.assertEqual(options.data['payloads'], [('range', {'default': '0-1', 'encoder': ['md5']}, None)]) - - options = CLParser(['wfuzz', '-z', 'range', '--zD', '0-1', '--zE', 'md5', 'url']).parse_cl() - self.assertEqual(options.data['payloads'], [('range', {'default': '0-1', 'encoder': ['md5']}, None)]) - - options = CLParser(['wfuzz', '-z', 'range', '--zD', '0-1']).parse_cl() - self.assertEqual(options.data['payloads'], [('range', {'default': '0-1', 'encoder': None}, None)]) - - options = CLParser(['wfuzz', '-z', 'range,0-1']).parse_cl() - self.assertEqual(options.data['payloads'], [('range', {'default': '0-1', 'encoder': None}, None)]) + options = CLParser( + ["wfuzz", "-z", "range", "--zD", "0-1", "--zE", "md5", "url"] + ).parse_cl() + self.assertEqual( + options.data["payloads"], + [("range", {"default": "0-1", "encoder": ["md5"]}, None)], + ) + + options = CLParser( + ["wfuzz", "-z", "range,0-1", "--zE", "md5", "url"] + ).parse_cl() + self.assertEqual( + options.data["payloads"], + [("range", {"default": "0-1", "encoder": ["md5"]}, None)], + ) + + options = CLParser( + ["wfuzz", "-z", "range", "--zD", "0-1", "--zE", "md5", "url"] + ).parse_cl() + self.assertEqual( + options.data["payloads"], + [("range", {"default": "0-1", "encoder": ["md5"]}, None)], + ) + + options = CLParser(["wfuzz", "-z", "range", "--zD", "0-1"]).parse_cl() + self.assertEqual( + options.data["payloads"], + [("range", {"default": "0-1", "encoder": None}, None)], + ) + + options = CLParser(["wfuzz", "-z", "range,0-1"]).parse_cl() + self.assertEqual( + options.data["payloads"], + [("range", {"default": "0-1", "encoder": None}, None)], + ) diff --git a/tests/test_dotdict.py b/tests/test_dotdict.py deleted file mode 100644 index e752f61c..00000000 --- a/tests/test_dotdict.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - -from wfuzz.utils import DotDict - - -class FilterDotDict(unittest.TestCase): - def __init__(self, *args, **kwargs): - super(FilterDotDict, self).__init__(*args, **kwargs) - self.maxDiff = 1000 - - def test_code_set(self): - dd = DotDict({'a': '1'}) - dd2 = DotDict({'a': '2'}) - - self.assertEqual(dd + "test", {'a': "1test"}) - self.assertEqual("test" + dd, {'a': "test1"}) - self.assertEqual(dd + dd2, {'a': "2"}) diff --git a/tests/test_filterintro.py b/tests/test_filterintro.py index 37b9e5c9..f355c835 100644 --- a/tests/test_filterintro.py +++ b/tests/test_filterintro.py @@ -2,9 +2,9 @@ # Python 2 and 3: urlib.parse -from wfuzz.fuzzobjects import FuzzRequest from wfuzz.fuzzobjects import FuzzResult -from wfuzz.filter import FuzzResFilter +from wfuzz.fuzzrequest import FuzzRequest +from wfuzz.filters.ppfilter import FuzzResFilter raw_req = """GET / HTTP/1.1 @@ -43,30 +43,6 @@ def get_filtered_fuzzrequest(self, filter_str): return fuzz_res - def test_code_set(self): - self.assertEqual(self.get_filtered_fuzzrequest("r.code:=429").code, 429) - self.assertEqual(self.get_filtered_fuzzrequest("r.c:=404").code, 404) - self.assertEqual(self.get_filtered_fuzzrequest("r.c=+404").code, 706) - self.assertEqual(self.get_filtered_fuzzrequest("r.c=-404").code, 706) - - def test_url_set(self): - fr = FuzzRequest() - fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" - - fuzz_res = FuzzResult(history=fr) - - ffilter = FuzzResFilter(filter_string="r.url=+'test'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.url, "http://www.wfuzz.org/path?param=1¶m2=2test") - - ffilter = FuzzResFilter(filter_string="r.url:='test'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.url, "http://test/") - - ffilter = FuzzResFilter(filter_string="r.url=-'test'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.url, "testhttp://test/") - def test_nonexisting(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" @@ -87,108 +63,3 @@ def test_nonexisting(self): ffilter = FuzzResFilter(filter_string="r.params.get.notthere=-'test'") ffilter.is_visible(fuzz_res) self.assertTrue("DotDict: Non-existing field" in str(context.exception)) - - def test_params_set_no_value(self): - fr = FuzzRequest() - fr.url = "http://www.wfuzz.org/path?param" - - fuzz_res = FuzzResult(history=fr) - - ffilter = FuzzResFilter(filter_string="r.params.all=+'test'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.params.get, {'param': None}) - - def test_params_set(self): - fr = FuzzRequest() - fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" - - fuzz_res = FuzzResult(history=fr) - - ffilter = FuzzResFilter(filter_string="r.params.get.param=+'test'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.params.get.param, "1test") - self.assertEqual(fuzz_res.history.params.get, {'param': "1test", 'param2': "2"}) - - ffilter = FuzzResFilter(filter_string="r.params.get.param=-'test'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.params.get.param, "test1test") - self.assertEqual(fuzz_res.history.params.get, {'param': "test1test", 'param2': "2"}) - - ffilter = FuzzResFilter(filter_string="r.params.get.param:='test'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.params.get.param, "test") - self.assertEqual(fuzz_res.history.params.get, {'param': "test", 'param2': "2"}) - - ffilter = FuzzResFilter(filter_string="r.params.get.param2='2'") - self.assertEqual(ffilter.is_visible(fuzz_res), True) - - fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" - ffilter = FuzzResFilter(filter_string="r.params.all=+'2'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.params.all, {'param': "12", 'param2': "22"}) - - fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" - ffilter = FuzzResFilter(filter_string="r.params.all:='2'") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.history.params.all, {'param': "2", 'param2': "2"}) - - def test_urlp(self): - fr = FuzzRequest() - fr.url = "http://www.wfuzz.org/path/test.php?param=1¶m2=2" - - fuzz_res = FuzzResult(history=fr) - - ffilter = FuzzResFilter(filter_string="r.urlp.scheme='http'") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="r.urlp.netloc='www.wfuzz.org'") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="r.urlp.path='/path/test.php'") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="r.urlp.ffname='test.php'") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="r.urlp.fext='.php'") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="r.urlp.fname='test'") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="r.urlp.hasquery") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="not r.urlp.isbllist") - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - def test_ispath(self): - fr = FuzzRequest() - fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" - fuzz_res = FuzzResult(history=fr) - - ffilter = FuzzResFilter(filter_string="r.is_path") - self.assertEqual(False, ffilter.is_visible(fuzz_res)) - - ffilter = FuzzResFilter(filter_string="r.pstrip") - self.assertEqual(ffilter.is_visible(fuzz_res), "http://www.wfuzz.org/path-gparam-gparam2") - - def test_lwh(self): - fr = FuzzRequest() - fr.update_from_raw_http(raw_req, "http", raw_resp, b"Some line\n and words\nasdsdas") - - fuzz_res = FuzzResult(history=fr) - - ffilter = FuzzResFilter(filter_string="h=28 or w=6 or l=2") - ffilter.is_visible(fuzz_res) - self.assertEqual(True, ffilter.is_visible(fuzz_res)) - - def test_location(self): - fr = FuzzRequest() - fr.update_from_raw_http(raw_req, "http", raw_resp, b"Some line\n and words\nasdsdas") - - fuzz_res = FuzzResult(history=fr) - - ffilter = FuzzResFilter(filter_string="r.headers.response.Location") - ffilter.is_visible(fuzz_res) - self.assertEqual('https://wfuzz.readthedocs.io/en/latest/', ffilter.is_visible(fuzz_res)) diff --git a/tests/test_moduleman.py b/tests/test_moduleman.py index dddee523..1c9edb51 100644 --- a/tests/test_moduleman.py +++ b/tests/test_moduleman.py @@ -20,87 +20,119 @@ @moduleman_plugin class test_plugin1: name = "test_plugin1" - author = ("test plugin 1"), + author = (("test plugin 1"),) version = "0.1" - description = ( - "test plugin 1", - ) + description = ("test plugin 1",) summary = "test plugin 1." category = ["aggressive"] priority = 79 - parameters = ( - ("test", "", True, "test plugin 1"), - ) + parameters = (("test", "", True, "test plugin 1"),) @moduleman_plugin class test_plugin2: name = "test_plugin2" - author = ("test plugin 2"), + author = (("test plugin 2"),) version = "0.1" - description = ( - "test plugin 2", - ) + description = ("test plugin 2",) summary = "test plugin 2." category = ["default"] priority = 89 - parameters = ( - ("test", "", True, "test plugin 2"), - ) + parameters = (("test", "", True, "test plugin 2"),) @moduleman_plugin class test_plugin3: name = "test_plugin3" - author = ("test plugin 3"), + author = (("test plugin 3"),) version = "0.1" - description = ( - "test plugin 3", - ) + description = ("test plugin 3",) summary = "test plugin 3." category = ["safe", "default"] priority = 99 - parameters = ( - ("test", "", True, "test plugin 3"), - ) + parameters = (("test", "", True, "test plugin 3"),) class ModuleFilterTests(unittest.TestCase): def test_load_dir2(self): - with mock.patch('os.listdir') as mocked_listdir: - with mock.patch('os.path.isdir') as mocked_isdir: - with mock.patch('os.path.isfile') as mocked_isfile: - with mock.patch('imp.find_module') as mocked_find_module: - with mock.patch('imp.load_module') as mocked_load_module: - mocked_listdir.return_value = ['alpha', 'project.py'] + with mock.patch("os.listdir") as mocked_listdir: + with mock.patch("os.path.isdir") as mocked_isdir: + with mock.patch("os.path.isfile") as mocked_isfile: + with mock.patch("imp.find_module") as mocked_find_module: + with mock.patch("imp.load_module") as mocked_load_module: + mocked_listdir.return_value = ["alpha", "project.py"] mocked_isdir.side_effect = [True, False] mocked_isfile.return_value = True - mocked_find_module.return_value = (None, '/any/alpha/project.py', ('.py', 'U', 1)) + mocked_find_module.return_value = ( + None, + "/any/alpha/project.py", + (".py", "U", 1), + ) mocked_load_module.return_value = sys.modules[__name__] - br = BRegistrant(DirLoader(**{"base_dir": 'beta', "base_path": 'any'})) - - self.assertEqual(sorted(br.get_plugins_names()), sorted(['test_plugin1', 'test_plugin2', 'test_plugin3'])) - self.assertEqual(br.get_plugins_names('default'), ['test_plugin2', 'test_plugin3']) - self.assertEqual(br.get_plugins_names('aggressive'), ['test_plugin1']) - self.assertEqual(sorted(br.get_plugins_names('not aggressive')), sorted(['test_plugin2', 'test_plugin3'])) - self.assertEqual(sorted(br.get_plugins_names('default or aggressive')), sorted(['test_plugin1', 'test_plugin2', 'test_plugin3'])) - self.assertEqual(sorted(br.get_plugins_names('default and safe')), sorted(['test_plugin3'])) - self.assertEqual(sorted(br.get_plugins_names('test_pl*')), sorted(['test_plugin1', 'test_plugin2', 'test_plugin3'])) - self.assertEqual(sorted(br.get_plugins_names('test_plugin1')), sorted(['test_plugin1'])) + br = BRegistrant( + DirLoader(**{"base_dir": "beta", "base_path": "any"}) + ) + + self.assertEqual( + sorted(br.get_plugins_names()), + sorted( + ["test_plugin1", "test_plugin2", "test_plugin3"] + ), + ) + self.assertEqual( + br.get_plugins_names("default"), + ["test_plugin2", "test_plugin3"], + ) + self.assertEqual( + br.get_plugins_names("aggressive"), ["test_plugin1"] + ) + self.assertEqual( + sorted(br.get_plugins_names("not aggressive")), + sorted(["test_plugin2", "test_plugin3"]), + ) + self.assertEqual( + sorted(br.get_plugins_names("default or aggressive")), + sorted( + ["test_plugin1", "test_plugin2", "test_plugin3"] + ), + ) + self.assertEqual( + sorted(br.get_plugins_names("default and safe")), + sorted(["test_plugin3"]), + ) + self.assertEqual( + sorted(br.get_plugins_names("test_pl*")), + sorted( + ["test_plugin1", "test_plugin2", "test_plugin3"] + ), + ) + self.assertEqual( + sorted(br.get_plugins_names("test_plugin1")), + sorted(["test_plugin1"]), + ) def test_load_file(self): - with mock.patch('imp.find_module') as mocked_find_module: - with mock.patch('imp.load_module') as mocked_load_module: - mocked_find_module.return_value = (None, 'any/project.py', ('.py', 'U', 1)) + with mock.patch("imp.find_module") as mocked_find_module: + with mock.patch("imp.load_module") as mocked_load_module: + mocked_find_module.return_value = ( + None, + "any/project.py", + (".py", "U", 1), + ) mocked_load_module.return_value = sys.modules[__name__] - br = BRegistrant(FileLoader(**{"filename": 'project1.py', "base_path": 'any'})) + br = BRegistrant( + FileLoader(**{"filename": "project1.py", "base_path": "any"}) + ) - self.assertEqual(sorted(br.get_plugins_names()), sorted(['test_plugin1', 'test_plugin2', 'test_plugin3'])) + self.assertEqual( + sorted(br.get_plugins_names()), + sorted(["test_plugin1", "test_plugin2", "test_plugin3"]), + ) self.assertTrue(br.get_plugin("test_plugin1").name == "test_plugin1") self.assertTrue(br.get_plugin("test_plugin2").name == "test_plugin2") @@ -110,28 +142,48 @@ def test_load_file(self): self.assertTrue("Multiple plugins found" in str(context.exception)) def test_simple_filter(self): - with mock.patch('imp.find_module') as mocked_find_module: - with mock.patch('imp.load_module') as mocked_load_module: - mocked_find_module.return_value = (None, '/any/project.py', ('.py', 'U', 1)) + with mock.patch("imp.find_module") as mocked_find_module: + with mock.patch("imp.load_module") as mocked_load_module: + mocked_find_module.return_value = ( + None, + "/any/project.py", + (".py", "U", 1), + ) mocked_load_module.return_value = sys.modules[__name__] - br = BRegistrant(FileLoader(**{"filename": 'project1.py', "base_path": 'any'})) + br = BRegistrant( + FileLoader(**{"filename": "project1.py", "base_path": "any"}) + ) with self.assertRaises(Exception) as context: modulefilter.PYPARSING = False - br.get_plugins_names('not aggressive') - self.assertTrue("Pyparsing missing, complex filters not allowed." in str(context.exception)) + br.get_plugins_names("not aggressive") + self.assertTrue( + "Pyparsing missing, complex filters not allowed." + in str(context.exception) + ) modulefilter.PYPARSING = False - self.assertEqual(sorted(br.get_plugins_names("test*")), sorted(['test_plugin1', 'test_plugin2', 'test_plugin3'])) - self.assertEqual(sorted(br.get_plugins_names("test_plugin1,test_plugin2")), sorted(['test_plugin1', 'test_plugin2'])) - self.assertEqual(sorted(br.get_plugins_names("test_plugin5")), sorted([])) + self.assertEqual( + sorted(br.get_plugins_names("test*")), + sorted(["test_plugin1", "test_plugin2", "test_plugin3"]), + ) + self.assertEqual( + sorted(br.get_plugins_names("test_plugin1,test_plugin2")), + sorted(["test_plugin1", "test_plugin2"]), + ) + self.assertEqual( + sorted(br.get_plugins_names("test_plugin5")), sorted([]) + ) def test_plugin_decorator(self): with self.assertRaises(Exception) as context: + @moduleman_plugin("method1") class test_plugin4: pass test_plugin4() - self.assertTrue("Required method method4 not implemented" in str(context.exception)) + self.assertTrue( + "Required method method4 not implemented" in str(context.exception) + ) diff --git a/tests/test_relativeurl.py b/tests/test_relativeurl.py new file mode 100644 index 00000000..db2113a4 --- /dev/null +++ b/tests/test_relativeurl.py @@ -0,0 +1,135 @@ +import pytest + +# Python 2 and 3: urlib.parse + +from wfuzz.fuzzrequest import FuzzRequest + + +@pytest.fixture +def full_fuzzreq(request): + http_req, http_response = request.param + fr = FuzzRequest() + fr.update_from_raw_http(http_req, "http", http_response, None) + + return fr + + +@pytest.mark.parametrize( + "full_fuzzreq, expected_result", + [ + ( + ( + "GET /a HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + "HTTP/1.0 301 Moved Permanently\n" + "Server: SimpleHTTP/0.6 Python/3.6.5\n" + "Date: Tue, 21 Apr 2020 21:10:53 GMT\n" + "Location: /recursive_dir/a/\n", + ), + "http://www.wfuzz.org/recursive_dir/a/", + ), + ( + ( + "GET /a HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + "HTTP/1.1 301 Moved Permanently\n" + "Date: Fri, 24 Apr 2020 11:17:51 GMT\n" + "Server: Apache/2.4.41 () OpenSSL/1.0.2k-fips\n" + "Strict-Transport-Security: max-age=31536000; includeSubdomains; preload\n" + "Location: https://www.wfuzz.org/\n" + "Content-Type: text/html; charset=iso-8859-1\n", + ), + None, + ), + ( + ( + "GET /a HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + "HTTP/1.0 404 File not found\n" + "Server: SimpleHTTP/0.6 Python/3.6.5\n" + "Date: Fri, 24 Apr 2020 12:37:54 GMT\n" + "Connection: close\n" + "Content-Type: text/html;charset=utf-8\n" + "Content-Length: 469\n", + ), + None, + ), + ], + indirect=["full_fuzzreq"], +) +def test_relative_url(full_fuzzreq, expected_result): + assert full_fuzzreq.recursive_url == expected_result + + +@pytest.mark.parametrize( + "full_fuzzreq, expected_result", + [ + ( + ( + "GET /a HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + "HTTP/1.0 301 Moved Permanently\n" + "Server: SimpleHTTP/0.6 Python/3.6.5\n" + "Date: Tue, 21 Apr 2020 21:10:53 GMT\n" + "Location: /recursive_dir/a/\n", + ), + True, + ), + ( + ( + "GET /a HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + "HTTP/1.1 301 Moved Permanently\n" + "Date: Fri, 24 Apr 2020 11:17:51 GMT\n" + "Server: Apache/2.4.41 () OpenSSL/1.0.2k-fips\n" + "Strict-Transport-Security: max-age=31536000; includeSubdomains; preload\n" + "Location: https://www.wfuzz.org/\n" + "Content-Type: text/html; charset=iso-8859-1\n", + ), + False, + ), + ( + ( + "GET /a HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + "HTTP/1.0 404 File not found\n" + "Server: SimpleHTTP/0.6 Python/3.6.5\n" + "Date: Fri, 24 Apr 2020 12:37:54 GMT\n" + "Connection: close\n" + "Content-Type: text/html;charset=utf-8\n" + "Content-Length: 469\n", + ), + False, + ), + ( + ( + "GET /a/ HTTP/1.1\n" + "Host: www.wfuzz.org\n" + "Content-Type: application/x-www-form-urlencoded\n" + "User-Agent: Wfuzz/2.1\n", + "HTTP/1.0 200\n" + "Server: SimpleHTTP/0.6 Python/3.6.5\n" + "Date: Fri, 24 Apr 2020 12:37:54 GMT\n" + "Connection: close\n" + "Content-Type: text/html;charset=utf-8\n" + "Content-Length: 469\n", + ), + True, + ), + ], + indirect=["full_fuzzreq"], +) +def test_is_path(full_fuzzreq, expected_result): + assert full_fuzzreq.is_path == expected_result diff --git a/tests/test_req_parse.py b/tests/test_req_parse.py index ada961ac..1a39568e 100644 --- a/tests/test_req_parse.py +++ b/tests/test_req_parse.py @@ -1,9 +1,9 @@ import unittest -from wfuzz.fuzzobjects import FuzzRequest +from wfuzz.fuzzrequest import FuzzRequest -http_post_request = '''POST /slipstream/view HTTP/1.1 +http_post_request = """POST /slipstream/view HTTP/1.1 Host: www User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0 Accept: */* @@ -17,10 +17,10 @@ -a=1''' +a=1""" -http_get_request = '''GET /sttc/bpk-fonts/55b577a1.woff2 HTTP/1.1 +http_get_request = """GET /sttc/bpk-fonts/55b577a1.woff2 HTTP/1.1 Host: js.skyscnr.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0 Accept: application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8 @@ -31,9 +31,9 @@ Referer: https://js.skyscnr.com/sttc/oc-registry/components/base-stylesheet/0.1.33/build//static/css/main.e09b44e2.css -''' +""" -http_response = '''HTTP/1.1 201 Created +http_response = """HTTP/1.1 201 Created Content-Type: application/json Content-Length: 51 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0 @@ -44,9 +44,9 @@ Date: Mon, 30 Dec 2019 13:16:57 GMT Connection: close -LINE_1''' +LINE_1""" -http_response_no_content = '''HTTP/1.1 201 Created +http_response_no_content = """HTTP/1.1 201 Created Content-Type: application/json Content-Length: 51 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0 @@ -56,9 +56,9 @@ Server: Unspecified Date: Mon, 30 Dec 2019 13:16:57 GMT Connection: close -''' +""" -http_multi_request = '''POST /tr/ HTTP/1.1 +http_multi_request = """POST /tr/ HTTP/1.1 Host: www.facebook.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0 Accept: */* @@ -80,9 +80,9 @@ SB -----------------------------18698393981150719881279620016-- -''' +""" -http_follow_response = '''HTTP/1.1 301 Moved Permanently +http_follow_response = """HTTP/1.1 301 Moved Permanently Location: http://www.google.com/ Content-Type: text/html; charset=UTF-8 Date: Mon, 30 Dec 2019 20:26:23 GMT @@ -108,7 +108,7 @@ Vary: Accept-Encoding Transfer-Encoding: chunked -LINE_1''' +LINE_1""" class ParseRequestTest(unittest.TestCase): @@ -121,7 +121,9 @@ def test_2_ways_of_parsing_content(self): fr.update_from_raw_http(http_multi_request, "https", http_response) fr2 = FuzzRequest() - fr2.update_from_raw_http(http_multi_request, "https", http_response_no_content, b"LINE_1") + fr2.update_from_raw_http( + http_multi_request, "https", http_response_no_content, b"LINE_1" + ) # raw content takes precedence fr3 = FuzzRequest() @@ -157,4 +159,4 @@ def test_parse_crlf_post_request(self): fr.update_from_raw_http(http_post_request, "https", "\n\n\n") self.assertEqual(fr.method, "POST") - self.assertEqual(fr.params.post, {'a': '1'}) + self.assertEqual(fr.params.post, {"a": "1"}) diff --git a/tests/test_reqresp.py b/tests/test_reqresp.py index 2624ac9d..db685e11 100644 --- a/tests/test_reqresp.py +++ b/tests/test_reqresp.py @@ -2,8 +2,7 @@ # Python 2 and 3: urlib.parse -from wfuzz.fuzzobjects import FuzzRequest -from wfuzz.fuzzobjects import FuzzResultFactory +from wfuzz.fuzzrequest import FuzzRequest from wfuzz.ui.console.clparser import CLParser from wfuzz import __version__ as wfuzz_version @@ -13,7 +12,9 @@ Content-Type: application/x-www-form-urlencoded User-Agent: Wfuzz/{} -""".format(wfuzz_version) +""".format( + wfuzz_version +) raw_response_header = b"""HTTP/1.0 200 Connection established @@ -36,21 +37,34 @@ def __init__(self, *args, **kwargs): self.maxDiff = 1000 def test_baseline(self): - options = CLParser(['wfuzz', '-z', 'range,1-1', 'http://localhost:9000/FUZZ{first}']).parse_cl() - seed = FuzzResultFactory.from_options(options) - baseline = FuzzResultFactory.from_baseline(seed, options) - - self.assertEqual(baseline.description, 'first') - - options = CLParser(['wfuzz', '-z', 'range,1-1', '-z', 'range,2-2', 'http://localhost:9000/FUZZ{first}/FUZ2Z{second}']).parse_cl() - seed = FuzzResultFactory.from_options(options) - baseline = FuzzResultFactory.from_baseline(seed, options) - - self.assertEqual(baseline.description, 'first - second') + options = CLParser( + ["wfuzz", "-z", "range,1-1", "http://localhost:9000/FUZZ{first}"] + ).parse_cl() + options.compile_seeds() + baseline = options["compiled_baseline"] + + self.assertEqual(baseline.description, "first") + + options = CLParser( + [ + "wfuzz", + "-z", + "range,1-1", + "-z", + "range,2-2", + "http://localhost:9000/FUZZ{first}/FUZ2Z{second}", + ] + ).parse_cl() + options.compile_seeds() + baseline = options["compiled_baseline"] + + self.assertEqual(baseline.description, "first - second") def test_from_conn(self): fr = FuzzRequest() - fr.update_from_raw_http(raw_req, 'https', raw_response_header, raw_response_body) + fr.update_from_raw_http( + raw_req, "https", raw_response_header, raw_response_body + ) self.assertEqual(fr.code, 404) self.assertEqual(fr.content.count("\n"), 11) @@ -92,8 +106,8 @@ def test_seturl(self): fr.url = "http://www.wfuzz.org/a" self.assertEqual(sorted(str(fr).split("\n")), sorted(raw_req.split("\n"))) - fr.auth = ('basic', 'admin:admin') - self.assertEqual(fr.auth, ('basic', 'admin:admin')) + fr.auth = {"method": "basic", "credentials": "admin:admin"} + self.assertEqual(fr.auth, {"method": "basic", "credentials": "admin:admin"}) fr.url = "FUZZ" self.assertEqual(fr.url, "FUZZ") @@ -121,17 +135,17 @@ def test_seturl(self): def test_empy_post(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = '' + fr.params.post = "" self.assertEqual(fr.method, "POST") - self.assertEqual(fr.params.post, {'': None}) - self.assertEqual(fr.params.raw_post, '') + self.assertEqual(fr.params.post, {"": None}) + self.assertEqual(fr.params.raw_post, "") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" fr.params.post = {} self.assertEqual(fr.method, "POST") self.assertEqual(fr.params.post, {}) - self.assertEqual(fr.params.raw_post, '') + self.assertEqual(fr.params.raw_post, "") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" @@ -143,31 +157,31 @@ def test_empy_post(self): def test_setpostdata(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = 'a=1' + fr.params.post = "a=1" self.assertEqual(fr.method, "POST") - self.assertEqual(fr.params.raw_post, 'a=1') - self.assertEqual(fr.params.post, {'a': '1'}) + self.assertEqual(fr.params.raw_post, "a=1") + self.assertEqual(fr.params.post, {"a": "1"}) fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = '1' + fr.params.post = "1" self.assertEqual(fr.method, "POST") - self.assertEqual(fr.params.post, {'1': None}) - self.assertEqual(fr.params.raw_post, '1') + self.assertEqual(fr.params.post, {"1": None}) + self.assertEqual(fr.params.raw_post, "1") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = {'a': 1} + fr.params.post = {"a": 1} self.assertEqual(fr.method, "POST") - self.assertEqual(fr.params.post, {'a': '1'}) - self.assertEqual(fr.params.raw_post, 'a=1') + self.assertEqual(fr.params.post, {"a": "1"}) + self.assertEqual(fr.params.raw_post, "a=1") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = {'a': '1'} + fr.params.post = {"a": "1"} self.assertEqual(fr.method, "POST") - self.assertEqual(fr.params.post, {'a': '1'}) - self.assertEqual(fr.params.raw_post, 'a=1') + self.assertEqual(fr.params.post, {"a": "1"}) + self.assertEqual(fr.params.raw_post, "a=1") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" @@ -179,28 +193,30 @@ def test_setgetdata(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.get = {'a': '1'} + fr.params.get = {"a": "1"} self.assertEqual(fr.method, "GET") - self.assertEqual(fr.params.get, {'a': '1'}) + self.assertEqual(fr.params.get, {"a": "1"}) def test_allvars(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.get = {'a': '1', 'b': '2'} + fr.params.get = {"a": "1", "b": "2"} fr.wf_allvars = "allvars" - self.assertEqual(fr.wf_allvars_set, {'a': '1', 'b': '2'}) + self.assertEqual(fr.wf_allvars_set, {"a": "1", "b": "2"}) fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = {'a': '1', 'b': '2'} + fr.params.post = {"a": "1", "b": "2"} fr.wf_allvars = "allpost" - self.assertEqual(fr.wf_allvars_set, {'a': '1', 'b': '2'}) + self.assertEqual(fr.wf_allvars_set, {"a": "1", "b": "2"}) - default_headers = dict([ - ('Content-Type', 'application/x-www-form-urlencoded'), - ('User-Agent', 'Wfuzz/{}'.format(wfuzz_version)), - ('Host', 'www.wfuzz.org') - ]) + default_headers = dict( + [ + ("Content-Type", "application/x-www-form-urlencoded"), + ("User-Agent", "Wfuzz/{}".format(wfuzz_version)), + ("Host", "www.wfuzz.org"), + ] + ) fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" @@ -210,66 +226,66 @@ def test_allvars(self): def test_cache_key(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-') + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.get = {'a': '1', 'b': '2'} - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-ga-gb') + fr.params.get = {"a": "1", "b": "2"} + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-ga-gb") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = {'c': '1', 'd': '2'} - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-pc-pd') + fr.params.post = {"c": "1", "d": "2"} + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-pc-pd") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.get = {'a': '1', 'b': '2'} - fr.params.post = {'c': '1', 'd': '2'} - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-ga-gb-pc-pd') + fr.params.get = {"a": "1", "b": "2"} + fr.params.post = {"c": "1", "d": "2"} + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-ga-gb-pc-pd") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.get = {'a': '1', 'b': '2'} - fr.params.post = {'a': '1', 'b': '2'} - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-ga-gb-pa-pb') + fr.params.get = {"a": "1", "b": "2"} + fr.params.post = {"a": "1", "b": "2"} + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-ga-gb-pa-pb") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = '1' - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-p1') + fr.params.post = "1" + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-p1") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = '' - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-p') + fr.params.post = "" + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-p") def test_cache_key_json_header_before(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.params.post = '1' - fr.headers.request = {'Content-Type': 'application/json'} + fr.params.post = "1" + fr.headers.request = {"Content-Type": "application/json"} - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-p1') + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-p1") def test_cache_key_json_header_after(self): fr = FuzzRequest() - fr.headers.request = {'Content-Type': 'application/json'} + fr.headers.request = {"Content-Type": "application/json"} fr.url = "http://www.wfuzz.org/" - fr.params.post = '1' + fr.params.post = "1" - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-p1') + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-p1") def test_cache_key_get_var(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/?a&b=1" - self.assertEqual(fr.to_cache_key(), 'http://www.wfuzz.org/-ga-gb') + self.assertEqual(fr.to_cache_key(), "http://www.wfuzz.org/-ga-gb") def test_get_vars(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/?a&b=1" - self.assertEqual(fr.params.get, {'a': None, 'b': '1'}) + self.assertEqual(fr.params.get, {"a": None, "b": "1"}) fr = FuzzRequest() fr.url = "http://www.wfuzz.org/?" @@ -281,35 +297,35 @@ def test_get_vars(self): def test_setpostdata_with_json(self): fr = FuzzRequest() - fr.headers.request = {'Content-Type': 'application/json'} + fr.headers.request = {"Content-Type": "application/json"} fr.url = "http://www.wfuzz.org/" fr.params.post = '{"string": "Foo bar","boolean": false}' - self.assertEqual(fr.params.post, {'string': 'Foo bar', 'boolean': False}) + self.assertEqual(fr.params.post, {"string": "Foo bar", "boolean": False}) fr = FuzzRequest() - fr.headers.request = {'Content-Type': 'application/json'} + fr.headers.request = {"Content-Type": "application/json"} fr.url = "http://www.wfuzz.org/" fr.params.post = '{"array": [1,2]}' - self.assertEqual(fr.params.post, {'array': [1, 2]}) + self.assertEqual(fr.params.post, {"array": [1, 2]}) def test_post_bad_json(self): fr = FuzzRequest() - fr.headers.request = {'Content-Type': 'application/json'} + fr.headers.request = {"Content-Type": "application/json"} fr.url = "http://www.wfuzz.org/" - fr.params.post = '1' + fr.params.post = "1" self.assertEqual(fr.method, "POST") - self.assertEqual(fr.params.post, {'1': None}) - self.assertEqual(fr.params.raw_post, '1') + self.assertEqual(fr.params.post, {"1": None}) + self.assertEqual(fr.params.raw_post, "1") fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" - fr.headers.request = {'Content-Type': 'application/json'} - fr.params.post = 'a=1' + fr.headers.request = {"Content-Type": "application/json"} + fr.params.post = "a=1" self.assertEqual(fr.method, "POST") self.assertEqual(fr.params.raw_post, "a=1") - self.assertEqual(fr.params.post, {'a': '1'}) + self.assertEqual(fr.params.post, {"a": "1"}) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index dd058fb8..1b703c7c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,21 @@ [tox] -envlist = begin,docker,py27,py36,py37,end +envlist = begin,docker,py38,end [testenv] +allowlist_externals = make commands = - make install-dev - flake8 --ignore=E501,E402,F401,W504 src tests - coverage run --append -m unittest discover -v -s tests/ + make flake8 + coverage run --append -m pytest -v -s tests/ +deps = + flake8 + black + netaddr + mock + coverage + discover [testenv:docker] +allowlist_externals = docker-compose changedir = tests commands = docker-compose -f server_dir/docker-compose.yml up -d @@ -16,7 +24,8 @@ commands = coverage erase deps = coverage [testenv:end] -commands = coverage report --skip-covered --include '*python3.6/site-packages/wfuzz*' -m +allowlist_externals = make +commands = make coverage deps = coverage [testenv:codecov]