diff --git a/Makefile b/Makefile index 60370f7b..4a8f6c1f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ .PHONY: docs -test: +tox: pip install tox tox --recreate +test: + pytest -v -s tests/ flake8: black --check src tests flake8 src tests diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index f17dd608..3ab9337e 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -466,6 +466,7 @@ value|replace('what', 'with') value|r('what', 'with') Returns value replacing value|unique() value|u() Returns True if a value is unique. value|startswith('value') value|sw('value') Returns true if the value string starts with param value|gregex('expression') value|gre('exp') Returns first regex group that matches in value +value|diff(expression) Returns diff comparison between value and expression ================================ ======================= ============================================= * When a FuzzResult is available, you could perform runtime introspection of the objects using the following symbols @@ -482,7 +483,7 @@ lines l Wfuzz's result HTTP response lines words w Wfuzz's result HTTP response words md5 Wfuzz's result HTTP response md5 hash history r Wfuzz's result associated FuzzRequest object -plugins Wfuzz's results associated plugins result in the form of {'plugin id': ['result']} +plugins Wfuzz's plugins scan results ============ ============== ============================================= FuzzRequest object's attribute (you need to use the r. prefix) such as: @@ -609,7 +610,7 @@ The payload to filter, specified by the -z switch must precede --slice command l The specified expression must return a boolean value, an example, using the unique operator is shown below:: - $ wfuzz-cli.py -z list --zD one-two-one-one --slice "FUZZ|u()" http://localhost:9000/FUZZ + $ wfuzz -z list --zD one-two-one-one --slice "FUZZ|u()" http://localhost:9000/FUZZ ******************************************************** * Wfuzz 2.2 - The Web Fuzzer * @@ -633,6 +634,37 @@ The specified expression must return a boolean value, an example, using the uniq 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. +When slicing a FuzzResult payload, you are accessing the FuzzResult directly, therefore given a previous session such as:: + + $ wfuzz -z range --zD 0-0 -u http://www.google.com/FUZZ --oF /tmp/test1 + ... + 000000001: 404 11 L 72 W 1558 Ch "0" + ... + +this can be used to filter the payload:: + + $ wfpayload -z wfuzzp --zD /tmp/test1 --slice "c=404" + ... + 000000001: 404 11 L 72 W 1558 Ch "0" + ... + + $ wfpayload -z wfuzzp --zD /tmp/test1 --slice "c!=404" + ... + wfuzz.py:168: UserWarning:Fatal exception: Empty dictionary! Please check payload or filter. + ... + +In fact, in this situation, FUZZ refers to the previous result (if any):: + + $ wfuzz -z wfuzzp --zD /tmp/test1 -u FUZZ --oF /tmp/test2 + ... + 000000001: 404 11 L 72 W 1558 Ch "http://www.google.com/0" + ... + + $ wfpayload -z wfuzzp --zD /tmp/test2 --efield r.headers.response.date --efield FUZZ[r.headers.response.date] + ... + 000000001: 404 11 L 72 W 1558 Ch "http://www.google.com/0 | Mon, 02 Nov 2020 19:29:03 GMT | Mon, 02 Nov 2020 19:27:27 GMT" + ... + Re-writing a payload """"""" @@ -740,6 +772,12 @@ The above command will generate HTTP requests such as the following:: You can filter the payload using the filter grammar as described before. +Reutilising previous results +-------------------------------------- + +Plugins results contain a treasure trove of data. Wfuzz payloads and object introspection (explained in the filter grammar section) exposes a Python object interface to plugins results. +This allows you to perform semi-automatic tests based on plugins results or compile a set of results to be used in another tool. + Request mangling ^^^^^^^^^ diff --git a/docs/user/basicusage.rst b/docs/user/basicusage.rst index 36e48146..69800bbe 100644 --- a/docs/user/basicusage.rst +++ b/docs/user/basicusage.rst @@ -252,7 +252,7 @@ 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 additional FuzzResult's fields to show, using --efield, together with the payload description:: +When using the default or raw 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 --efield r ... @@ -262,7 +262,7 @@ When using the default output you can also select additional FuzzResult's fields 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 command is useful, for example, to debug what exact HTTP request Wfuzz sent to the remote Web server. To completely replace the default payload output you can use --field instead:: @@ -279,4 +279,14 @@ To completely replace the default payload output you can use --field instead:: 000000001: 200 104 L 364 W 4735 Ch "0 | http://testphp.vulnweb.com/artists.php?artist=0 | 4735" ... +The field printer can be used with a --efield or --field expression to list only the specified filter expressions without a header or footer:: + + + $ wfuzz -z list --zD https://www.airbnb.com/ --script=links --script-args=links.regex=.*js$,links.enqueue=False -u FUZZ -o field --field plugins.links.link | head -n3 + https://a0.muscache.com/airbnb/static/packages/4e8d-d5c346ee.js + https://a0.muscache.com/airbnb/static/packages/7afc-ac814a17.js + https://a0.muscache.com/airbnb/static/packages/7642-dcf4f8dc.js + +The above command is useful, for example, to pipe wfuzz into other tools or perform console scripts. + --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/installation.rst b/docs/user/installation.rst index c21f6566..ce7069ec 100644 --- a/docs/user/installation.rst +++ b/docs/user/installation.rst @@ -26,7 +26,7 @@ You can either clone the public repository:: $ git clone git://github.com/xmendez/wfuzz.git -Or download last `release _`. +Or download last `release `_. Once you have a copy of the source, you can embed it in your own Python package, or install it into your site-packages easily:: @@ -91,6 +91,16 @@ If you get errors such as:: Run brew update && brew upgrade +If you get an error such as:: + + ImportError: pycurl: libcurl link-time ssl backends (secure-transport, openssl) do not include compile-time ssl backend (none/other) + +That might indicate that pycurl was reinstalled and not linked to the SSL correctly. Uninstall pycurl as follows:: + + $ pip uninstall pycurl + +and re-install pycurl starting from step 4 above. + Pycurl on Windows ----------------- diff --git a/requirements.txt b/requirements.txt index 44d01ad6..daf8a36f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ # attrs==20.1.0 # via pytest chardet==3.0.4 # via wfuzz (setup.py) -future==0.18.2 # via wfuzz (setup.py) iniconfig==1.0.1 # via pytest more-itertools==8.5.0 # via pytest packaging==20.4 # via pytest diff --git a/setup.py b/setup.py index 4ef20099..d75874ec 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,6 @@ 'pycurl', 'pyparsing<2.4.2;python_version<="3.4"', 'pyparsing>=2.4*;python_version>="3.5"', - 'future', 'six', 'configparser;python_version<"3.5"', 'chardet', diff --git a/src/wfuzz/__init__.py b/src/wfuzz/__init__.py index 02950616..159dab7e 100644 --- a/src/wfuzz/__init__.py +++ b/src/wfuzz/__init__.py @@ -1,5 +1,5 @@ __title__ = "wfuzz" -__version__ = "3.0.3" +__version__ = "3.1.0" __build__ = 0x023000 __author__ = "Xavier Mendez" __license__ = "GPL 2.0" diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index 8f07e8ba..947251df 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -30,11 +30,12 @@ def __init__(self, options): # genReq ---> seed_queue -> [slice_queue] -> http_queue/dryrun -> [round_robin -> plugins_queue] * N # -> [recursive_queue -> routing_queue] -> [filter_queue] -> [save_queue] -> [printer_queue] ---> results + self.options = options self.qmanager = QueueManager(options) self.results_queue = MyPriorityQueue() if options["allvars"]: - self.qmanager.add("allvars_queue", AllVarQ(options)) + self.qmanager.add("seed_queue", AllVarQ(options)) else: self.qmanager.add("seed_queue", SeedQ(options)) @@ -56,8 +57,12 @@ def __init__(self, options): if options.get("script"): self.qmanager.add("plugins_queue", JobQ(options)) - if options.get("script") or options.get("rlevel") > 0: + if options.get("rlevel") > 0: self.qmanager.add("recursive_queue", RecursiveQ(options)) + + if (options.get("script") or options.get("rlevel") > 0) and options.get( + "transport" + ) == "http": rq = RoutingQ( options, { @@ -115,7 +120,7 @@ def __next__(self): def stats(self): return dict( list(self.qmanager.get_stats().items()) - + list(self.qmanager["transport_queue"].job_stats().items()) + + list(self.qmanager["transport_queue"].http_pool.job_stats().items()) + list(self.options.stats.get_stats().items()) ) diff --git a/src/wfuzz/dictionaries.py b/src/wfuzz/dictionaries.py index 4ab0d13a..b2ebe413 100644 --- a/src/wfuzz/dictionaries.py +++ b/src/wfuzz/dictionaries.py @@ -1,6 +1,6 @@ from .exception import FuzzExceptNoPluginError, FuzzExceptBadOptions from .facade import Facade -from .filters.ppfilter import FuzzResFilterSlice +from .filters.ppfilter import FuzzResFilterSlice, FuzzResFilter from .fuzzobjects import FuzzWord, FuzzWordType @@ -119,7 +119,8 @@ def next_word(self): class SliceIt(BaseDictionary): def __init__(self, payload, slicestr): - self.ffilter = FuzzResFilterSlice(filter_string=slicestr) + self.ffilter = FuzzResFilter(filter_string=slicestr) + self.ffilter_slice = FuzzResFilterSlice(filter_string=slicestr) self.payload = payload def count(self): @@ -128,10 +129,18 @@ def count(self): def get_type(self): return self.payload.get_type() + def _get_filtered_value(self, item): + if item.type == FuzzWordType.FUZZRES: + filter_ret = self.ffilter.is_visible(item.content) + else: + filter_ret = self.ffilter_slice.is_visible(item.content) + + return filter_ret + 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) + filter_ret = self._get_filtered_value(item) if not isinstance(filter_ret, bool) and item.type == FuzzWordType.FUZZRES: raise FuzzExceptBadOptions( @@ -140,7 +149,7 @@ def next_word(self): while isinstance(filter_ret, bool) and not filter_ret: item = next(self.payload) - filter_ret = self.ffilter.is_visible(item.content) + filter_ret = self._get_filtered_value(item) if not isinstance(filter_ret, bool): return FuzzWord(filter_ret, item.type) diff --git a/src/wfuzz/externals/reqresp/Response.py b/src/wfuzz/externals/reqresp/Response.py index 52a7bd97..c456cd19 100644 --- a/src/wfuzz/externals/reqresp/Response.py +++ b/src/wfuzz/externals/reqresp/Response.py @@ -146,7 +146,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): tp = TextParser() tp.setSource("string", rawheader) - tp.readUntil(r"(HTTP\S*) ([0-9]+)") + tp.readUntil(r"(HTTP/[0-9.]+) ([0-9]+)") while True: while True: try: @@ -162,7 +162,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): if self.code != "100": break else: - tp.readUntil(r"(HTTP\S*) ([0-9]+)") + tp.readUntil(r"(HTTP/[0-9.]+) ([0-9]+)") self.code = int(self.code) @@ -176,7 +176,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): # curl sometimes sends two headers when using follow, 302 and the final header # also when using proxies tp.readLine() - if not tp.search(r"(HTTP\S*) ([0-9]+)"): + if not tp.search(r"(HTTP/[0-9.]+) ([0-9]+)"): break else: self._headers = [] diff --git a/src/wfuzz/externals/reqresp/TextParser.py b/src/wfuzz/externals/reqresp/TextParser.py index a8553db8..28e97a94 100755 --- a/src/wfuzz/externals/reqresp/TextParser.py +++ b/src/wfuzz/externals/reqresp/TextParser.py @@ -130,9 +130,8 @@ 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)] - else: - self.lastFull_line = self.string[self.oldindex : self.newindex + 1] + self.newindex = len(self.string) - 1 + self.lastFull_line = self.string[self.oldindex : self.newindex + 1] self.oldindex = self.newindex + 1 else: diff --git a/src/wfuzz/facade.py b/src/wfuzz/facade.py index 4f4f4462..e427377c 100644 --- a/src/wfuzz/facade.py +++ b/src/wfuzz/facade.py @@ -8,8 +8,6 @@ import os -# python2 and 3: metaclass -from future.utils import with_metaclass ERROR_CODE = -1 BASELINE_CODE = -2 @@ -64,8 +62,7 @@ def get_plugin(self, identifier): ) -# python2 and 3: class Facade(metaclass=utils.Singleton): -class Facade(with_metaclass(Singleton, object)): +class Facade(metaclass=Singleton): def __init__(self): self.__plugins = dict( diff --git a/src/wfuzz/factories/fuzzresfactory.py b/src/wfuzz/factories/fuzzresfactory.py index 3ddd5828..4734c32a 100644 --- a/src/wfuzz/factories/fuzzresfactory.py +++ b/src/wfuzz/factories/fuzzresfactory.py @@ -27,6 +27,7 @@ class FuzzResultDictioBuilder: def __call__(self, options, dictio_item): res = copy.deepcopy(options["compiled_seed"]) res.item_type = FuzzType.RESULT + res.discarded = False res.payload_man.update_from_dictio(dictio_item) res.update_from_options(options) @@ -69,6 +70,7 @@ class FuzzResultAllVarBuilder: def __call__(self, options, var_name, payload): fuzzres = copy.deepcopy(options["compiled_seed"]) fuzzres.item_type = FuzzType.RESULT + fuzzres.discarded = False 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} @@ -97,6 +99,7 @@ def __call__(self, seed): new_seed.rlevel_desc += " - " new_seed.rlevel_desc += seed.payload_man.description() new_seed.item_type = FuzzType.SEED + new_seed.discarded = False new_seed.payload_man = payman_factory.create( "payloadman_from_request", new_seed.history ) @@ -113,6 +116,7 @@ def __call__(self, seed, url): fr.rlevel_desc += " - " fr.rlevel_desc += seed.payload_man.description() fr.item_type = FuzzType.BACKFEED + fr.discarded = False fr.is_baseline = False fr.payload_man = payman_factory.create( diff --git a/src/wfuzz/factories/plugin_factory.py b/src/wfuzz/factories/plugin_factory.py index 4a558341..a660bea1 100644 --- a/src/wfuzz/factories/plugin_factory.py +++ b/src/wfuzz/factories/plugin_factory.py @@ -12,6 +12,7 @@ def __init__(self): "plugin_from_recursion": PluginRecursiveBuilder(), "plugin_from_error": PluginErrorBuilder(), "plugin_from_finding": PluginFindingBuilder(), + "plugin_from_summary": PluginFindingSummaryBuilder(), }, ) @@ -38,12 +39,29 @@ def __call__(self, name, exception): class PluginFindingBuilder: - def __call__(self, name, message): + def __call__(self, name, itype, message, data, severity): plugin = FuzzPlugin() plugin.source = name plugin.issue = message + plugin.itype = itype + plugin.data = data plugin._exception = None plugin._seed = None + plugin.severity = severity + + return plugin + + +class PluginFindingSummaryBuilder: + def __call__(self, message): + plugin = FuzzPlugin() + plugin.source = FuzzPlugin.OUTPUT_SOURCE + plugin.itype = FuzzPlugin.SUMMARY_ITYPE + plugin.severity = FuzzPlugin.NONE + plugin._exception = None + plugin.data = None + plugin._seed = None + plugin.issue = message return plugin diff --git a/src/wfuzz/filters/ppfilter.py b/src/wfuzz/filters/ppfilter.py index 50b76704..1c9ead09 100644 --- a/src/wfuzz/filters/ppfilter.py +++ b/src/wfuzz/filters/ppfilter.py @@ -5,6 +5,7 @@ ) from ..helpers.str_func import value_in_any_list_item from ..helpers.obj_dic import DotDict +from ..helpers.utils import diff import re import collections @@ -58,15 +59,23 @@ def __init__(self, filter_string=None): 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)" + r"(description|nres|code|chars|lines|words|md5|content|timer|url|l|w|c|(r|history|plugins)(\w|_|-|\.)*|h)" ).setParseAction(self._compute_res_symbol) bbb_symbol = Regex( r"BBB(?:\[(?P(\w|_|-|\.)+)\])?", asMatch=True ).setParseAction(self.__compute_bbb_symbol) + diff_call = Group( + Suppress(Literal("|")) + + Literal("diff") + + Suppress(Literal("(")) + + (fuzz_symbol | res_symbol | bbb_symbol | int_values | quoted_str_value) + + Suppress(")") + ) + fuzz_statement = Group( (fuzz_symbol | res_symbol | bbb_symbol | int_values | quoted_str_value) - + Optional(operator_call, None) + + Optional(diff_call | operator_call, None) ).setParseAction(self.__compute_res_value) operator = oneOf("and or") @@ -127,11 +136,16 @@ def __compute_res_value(self, tokens): 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() - ) + if location == "diff": + return diff(operator_match, fuzz_val) + else: + if operator_match and operator_match.groupdict()["operator"]: + fuzz_val = self._get_operator_value( + location, fuzz_val, operator_match.groupdict() + ) + if isinstance(fuzz_val, list): + return [fuzz_val] return fuzz_val def _get_payload_value(self, p_index): @@ -146,7 +160,7 @@ def _get_field_value(self, fuzz_val, field): self.stack.append(field) try: - return rgetattr(fuzz_val, field) + ret = rgetattr(fuzz_val, field) except IndexError: raise FuzzExceptIncorrectFilter( "Non existent FUZZ payload! Use a correct index." @@ -158,6 +172,10 @@ def _get_field_value(self, fuzz_val, field): ) ) + if isinstance(ret, list): + return [ret] + return ret + def __compute_bbb_symbol(self, tokens): if self.baseline is None: raise FuzzExceptBadOptions( @@ -274,17 +292,7 @@ def __compute_expr(self, tokens): 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 - ) + ret = rightvalue.lower() in str(leftvalue).lower() else: raise FuzzExceptBadOptions( "Invalid operand type {}".format(rightvalue) @@ -322,6 +330,9 @@ def __myreduce(self, elements): first = first or elements[i + 1] self.stack = [] + + if isinstance(first, list): + return [first] return first def __compute_not_operator(self, tokens): @@ -330,6 +341,8 @@ def __compute_not_operator(self, tokens): if operator == "not": return not value + if isinstance(value, list): + return [value] return value def __compute_formula(self, tokens): diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 74440077..fbd2a526 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -13,6 +13,7 @@ from .helpers.str_func import python2_3_convert_to_unicode from .helpers.obj_dyn import rgetattr from .helpers.utils import MyCounter +from .helpers.obj_dic import DotDict FuzzWord = namedtuple("FuzzWord", ["content", "type"]) @@ -23,17 +24,7 @@ class FuzzWordType(Enum): class FuzzType(Enum): - ( - SEED, - BACKFEED, - RESULT, - ERROR, - STARTSEED, - ENDSEED, - CANCEL, - DISCARDED, - PLUGIN, - ) = range(9) + (SEED, BACKFEED, RESULT, ERROR, STARTSEED, ENDSEED, CANCEL, PLUGIN,) = range(8) class FuzzItem(object): @@ -42,6 +33,8 @@ class FuzzItem(object): def __init__(self, item_type): self.item_id = next(FuzzItem.newid) self.item_type = item_type + self.rlevel = 1 + self.discarded = False def __str__(self): return "FuzzItem, type: {}".format(self.item_type.name) @@ -99,11 +92,11 @@ def get_stats(self): "url": self.url, "total": self.total_req, "backfed": self.backfeed(), - "Processed": self.processed(), - "Pending": self.pending_fuzz(), + "processed": self.processed(), + "pending": self.pending_fuzz(), "filtered": self.filtered(), - "Pending_seeds": self.pending_seeds(), - "totaltime": self._totaltime, + "pending_seeds": self.pending_seeds(), + "totaltime": time.time() - self.__starttime, } def mark_start(self): @@ -177,15 +170,16 @@ def value(self): else str(rgetattr(self.content, self.field)) ) - def description(self, default): + def description(self): if self.is_baseline: return self.content if self.marker is None: return "" + # return default value if self.field is None and isinstance(self.content, FuzzResult): - return rgetattr(self.content, default) + return self.content.url elif self.field is not None and isinstance(self.content, FuzzResult): return str(rgetattr(self.content, self.field)) @@ -252,9 +246,7 @@ def get_payloads(self): yield elem def description(self): - payl_descriptions = [ - payload.description("url") for payload in self.get_payloads() - ] + payl_descriptions = [payload.description() for payload in self.get_payloads()] ret_str = " - ".join([p_des for p_des in payl_descriptions if p_des]) return ret_str @@ -279,7 +271,6 @@ def __init__(self, history=None, exception=None, track_id=True): self.exception = exception self.is_baseline = False - self.rlevel = 1 self.rlevel_desc = "" self.nres = next(FuzzResult.newid) if track_id else 0 @@ -299,12 +290,20 @@ def __init__(self, history=None, exception=None, track_id=True): @property def plugins(self): - dic = defaultdict(list) + dic = defaultdict(lambda: defaultdict(list)) for pl in self.plugins_res: - dic[pl.source].append(pl.issue) + if pl.source == FuzzPlugin.OUTPUT_SOURCE: + continue + dic[pl.source][pl.itype].append(pl.data) + + ret = DotDict() + for key, first in dic.items(): + ret[key] = DotDict() + for seckey, second in first.items(): + ret[key][seckey] = second - return dic + return ret def update(self, exception=None): self.item_type = FuzzType.RESULT @@ -332,16 +331,19 @@ def __str__(self): self.description, ) for plugin in self.plugins_res: - res += "\n |_ %s" % plugin.issue + if plugin.itype == FuzzPlugin.SUMMARY_ITYPE: + res += "\n |_ %s" % plugin.issue return res @property def description(self): - res_description = ( - self.payload_man.description() if self.payload_man else self.url - ) - ret_str = "" + res_description = self.payload_man.description() if self.payload_man else None + + if not res_description: + res_description = self.url + + ret_str = None if self._show_field is True: ret_str = self._field() @@ -350,9 +352,6 @@ def description(self): else: ret_str = res_description - if not ret_str: - ret_str = self.url - if self.exception: return ret_str + "! " + str(self.exception) @@ -364,8 +363,14 @@ def description(self): def eval(self, expr): return self.FUZZRESULT_SHARED_FILTER.is_visible(self, expr) - def _field(self): - return " | ".join([str(self.eval(field)) for field in self._fields]) + def _field(self, separator=", "): + list_eval = [self.eval(field) for field in self._fields] + return " | ".join( + [ + separator.join(el) if isinstance(el, list) else str(el) + for el in list_eval + ] + ) # parameters in common with fuzzrequest @property @@ -397,9 +402,26 @@ def update_from_options(self, options): class FuzzPlugin(FuzzItem): + OUTPUT_SOURCE = "output" + SUMMARY_ITYPE = "summary" + NONE, INFO, LOW, MEDIUM, HIGH, CRITICAL = range(6) + MIN_VERBOSE = INFO + def __init__(self): FuzzItem.__init__(self, FuzzType.PLUGIN) self.source = "" self.issue = "" + self.itype = "" + self.data = "" self._exception = None self._seed = None + self.severity = self.INFO + + def is_visible(self, verbose): + if verbose and self.itype == self.SUMMARY_ITYPE: + return False + + if not verbose and self.severity >= self.MIN_VERBOSE: + return False + + return True diff --git a/src/wfuzz/fuzzqueues.py b/src/wfuzz/fuzzqueues.py index f77abc2c..f710aba6 100644 --- a/src/wfuzz/fuzzqueues.py +++ b/src/wfuzz/fuzzqueues.py @@ -7,7 +7,8 @@ from .factories.fuzzresfactory import resfactory from .factories.plugin_factory import plugin_factory -from .fuzzobjects import FuzzType, FuzzItem +from .factories.payman import payman_factory +from .fuzzobjects import FuzzType, FuzzItem, FuzzWord, FuzzWordType from .myqueues import FuzzQueue from .exception import ( FuzzExceptInternalError, @@ -17,7 +18,6 @@ ) from .myqueues import FuzzRRQueue from .facade import Facade -from .fuzzobjects import FuzzWordType from .ui.console.mvc import View @@ -33,8 +33,8 @@ def get_name(self): def cancel(self): self.options["compiled_stats"].cancelled = True - def items_to_process(self, item): - return item.item_type in [FuzzType.STARTSEED] + def items_to_process(self): + return [FuzzType.STARTSEED] def process(self, item): self.stats.pending_seeds.inc() @@ -65,8 +65,8 @@ def get_name(self): def cancel(self): self.options["compiled_stats"].cancelled = True - def items_to_process(self, item): - return item.item_type in [FuzzType.STARTSEED, FuzzType.SEED] + def items_to_process(self): + return [FuzzType.STARTSEED, FuzzType.SEED] def send_baseline(self): fuzz_baseline = self.options["compiled_baseline"] @@ -160,9 +160,6 @@ def __init__(self, options): 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" @@ -182,8 +179,8 @@ def __init__(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 process_discarded(self): + return True def get_name(self): return "CLIPrinterQ" @@ -222,8 +219,8 @@ def __init__(self, options, routes): def get_name(self): return "RoutingQ" - def items_to_process(self, item): - return item.item_type in [FuzzType.SEED, FuzzType.BACKFEED] + def items_to_process(self): + return [FuzzType.SEED, FuzzType.BACKFEED] def process(self, item): if item.item_type in self.routes: @@ -339,7 +336,7 @@ def process(self, res): self.send(res) def process_results(self, res, plugins_res_queue): - enq_item = defaultdict(int) + enq_item = defaultdict(lambda: defaultdict(int)) while not plugins_res_queue.empty(): item = plugins_res_queue.get() @@ -348,7 +345,7 @@ def process_results(self, res, plugins_res_queue): 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: + elif item._seed is not None and self.options["transport"] == "http": 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 @@ -356,19 +353,21 @@ def process_results(self, res, plugins_res_queue): self.stats.backfeed.inc() self.stats.pending_fuzz.inc() self.send(item._seed) - enq_item[item.source] += 1 - else: + enq_item[item.source]["request enqueued"] += 1 + elif item.issue: + enq_item[item.source][item.itype] += 1 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), + for plugin_name, plugin_type in enq_item.items(): + for domain, enq_num in plugin_type.items(): + res.plugins_res.append( + plugin_factory.create( + "plugin_from_summary", + "Plugin {}: {} new {}(s) found.".format( + plugin_name, enq_num, domain + ), + ) ) - ) class RecursiveQ(FuzzQueue): @@ -391,8 +390,7 @@ def process(self, fuzz_res): fuzz_res.plugins_res.append( plugin_factory.create( - "plugin_from_finding", - "Recursion", + "plugin_from_summary", "Enqueued response for recursion (level=%d)" % (seed.rlevel), ) ) @@ -413,7 +411,10 @@ 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) - + if not item.payload_man: + item.payload_man = payman_factory.create( + "empty_payloadman", FuzzWord(item.url, FuzzWordType.WORD) + ) self.send(item) @@ -456,8 +457,8 @@ 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 items_to_process(self): + return [FuzzType.RESULT, FuzzType.BACKFEED] def process(self, obj): self.pause.wait() @@ -474,7 +475,7 @@ def __read_http_results(self): class HttpReceiver(FuzzQueue): def __init__(self, options): - FuzzQueue.__init__(self, options, limit=options.get("concurrent") * 5) + FuzzQueue.__init__(self, options) def get_name(self): return "HttpReceiver" diff --git a/src/wfuzz/helpers/obj_dic.py b/src/wfuzz/helpers/obj_dic.py index 75c3bde0..68e91bcc 100644 --- a/src/wfuzz/helpers/obj_dic.py +++ b/src/wfuzz/helpers/obj_dic.py @@ -1,4 +1,5 @@ from collections.abc import MutableMapping +from itertools import chain class CaseInsensitiveDict(MutableMapping): @@ -65,3 +66,11 @@ def __getitem__(self, key): return super(DotDict, self).__getitem__(key) except KeyError: return DotDict({}) + + def __str__(self): + return "\n".join( + [ + "{}{} {}".format(k, "->" if isinstance(v, DotDict) else ":", v) + for k, v in self.items() + ] + ) diff --git a/src/wfuzz/helpers/utils.py b/src/wfuzz/helpers/utils.py index eb6151bb..92576c58 100644 --- a/src/wfuzz/helpers/utils.py +++ b/src/wfuzz/helpers/utils.py @@ -1,4 +1,5 @@ from threading import Lock +import difflib class MyCounter: @@ -20,3 +21,15 @@ def _operation(self, dec): def __call__(self): with self._mutex: return self._count + + +def diff(param1, param2): + delta = difflib.unified_diff( + str(param1).splitlines(False), + str(param2).splitlines(False), + fromfile="prev", + tofile="current", + n=0, + ) + + return "\n".join(delta) diff --git a/src/wfuzz/myhttp.py b/src/wfuzz/myhttp.py index 948cd76f..1cb58695 100644 --- a/src/wfuzz/myhttp.py +++ b/src/wfuzz/myhttp.py @@ -68,7 +68,7 @@ def job_stats(self): with self.mutex_stats: dic = { "http_processed": self.processed, - "http_registered": len(self._registered), + "http_registered": self._registered, } return dic diff --git a/src/wfuzz/myqueues.py b/src/wfuzz/myqueues.py index 620b71c2..7c7d7399 100644 --- a/src/wfuzz/myqueues.py +++ b/src/wfuzz/myqueues.py @@ -60,8 +60,11 @@ def process(self, item): def get_name(self): raise NotImplementedError - def items_to_process(self, item): - return item.item_type in [FuzzType.RESULT] + def process_discarded(self): + return False + + def items_to_process(self): + return [FuzzType.RESULT] # Override this method if needed. This will be called just before cancelling the job. def cancel(self): @@ -91,13 +94,8 @@ def send(self, item): self.queue_out.put(item) def discard(self, 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" - ) + item.discarded = True + self.send(item) def join(self): MyPriorityQueue.join(self) @@ -143,7 +141,9 @@ def run(self): self.task_done() continue - if self.items_to_process(item): + if ( + not item.discarded or (item.discarded and self.process_discarded()) + ) and item.item_type in self.items_to_process(): self.process(item) else: self.send(item) @@ -160,8 +160,6 @@ class LastFuzzQueue(FuzzQueue): def __init__(self, options, queue_out=None, limit=0): FuzzQueue.__init__(self, options, queue_out, limit) - self.items_to_send = [FuzzType.RESULT] - def get_name(self): return "LastFuzzQueue" @@ -171,10 +169,6 @@ def process(self): def _cleanup(self): pass - def send(self, item): - if item.item_type in self.items_to_send: - self.queue_out.put(item) - def _throw(self, e): self.queue_out.put_first(FuzzError(e)) @@ -199,14 +193,15 @@ def run(self): cancelling = True continue - self.send(item) + if item.item_type == FuzzType.RESULT and not item.discarded: + self.send(item) if item.item_type == FuzzType.ENDSEED: self.stats.pending_seeds.dec() - elif item.item_type in [FuzzType.RESULT, FuzzType.DISCARDED]: + elif item.item_type == FuzzType.RESULT: self.stats.processed.inc() self.stats.pending_fuzz.dec() - if item.item_type == FuzzType.DISCARDED: + if item.discarded: self.stats.filtered.inc() if self.stats.pending_fuzz() == 0 and self.stats.pending_seeds() == 0: diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 9e96f174..44d8c610 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -386,6 +386,12 @@ def compile(self): self.http_pool = HttpPool(self) self.http_pool.register() + if self.data["colour"]: + Facade().printers.kbase["colour"] = True + + if self.data["verbose"]: + Facade().printers.kbase["verbose"] = True + return self def close(self): diff --git a/src/wfuzz/plugin_api/base.py b/src/wfuzz/plugin_api/base.py index 80cfdbae..14b836f1 100644 --- a/src/wfuzz/plugin_api/base.py +++ b/src/wfuzz/plugin_api/base.py @@ -1,4 +1,4 @@ -from wfuzz.fuzzobjects import FuzzWord +from wfuzz.fuzzobjects import FuzzWord, FuzzPlugin from wfuzz.exception import ( FuzzExceptBadFile, FuzzExceptBadOptions, @@ -10,6 +10,7 @@ import sys import os +from distutils import util # python 2 and 3: iterator from builtins import object @@ -23,7 +24,7 @@ def __init__(self): # check mandatory params, assign default values for name, default_value, required, description in self.parameters: - param_name = "%s.%s" % (self.name, name) + param_name = "{}.{}".format(self.name, name) if required and param_name not in list(self.kbase.keys()): raise FuzzExceptBadOptions( @@ -58,9 +59,11 @@ def process(self, fuzzresult): def validate(self): raise FuzzExceptPluginError("Method count not implemented") - def add_result(self, issue): + def add_result(self, itype, issue, data, severity=FuzzPlugin.INFO): self.results_queue.put( - plugin_factory.create("plugin_from_finding", self.name, issue) + plugin_factory.create( + "plugin_from_finding", self.name, itype, issue, data, severity + ) ) def queue_url(self, url): @@ -70,6 +73,9 @@ def queue_url(self, url): ) ) + def _bool(self, value): + return bool(util.strtobool(value)) + class BasePrinter: def __init__(self, output): diff --git a/src/wfuzz/plugins/payloads/burplog.py b/src/wfuzz/plugins/payloads/burplog.py index 4ece4f20..7dcd0f8a 100644 --- a/src/wfuzz/plugins/payloads/burplog.py +++ b/src/wfuzz/plugins/payloads/burplog.py @@ -112,8 +112,9 @@ def parse_burp_log(self, burp_log): elif history == "DELIM4": if rl == CRLF: fr = FuzzRequest() + # last read line contains an extra CRLF fr.update_from_raw_http( - raw_request, host[: host.find("://")], raw_response + raw_request, host[: host.find("://")], raw_response[:-1] ) frr = FuzzResult(history=fr) diff --git a/src/wfuzz/plugins/printers/printers.py b/src/wfuzz/plugins/printers/printers.py index 7552729b..6677cffe 100644 --- a/src/wfuzz/plugins/printers/printers.py +++ b/src/wfuzz/plugins/printers/printers.py @@ -5,6 +5,7 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.plugin_api.base import BasePrinter +from wfuzz.exception import FuzzExceptPluginBadParams @moduleman_plugin @@ -285,8 +286,13 @@ def _print_verbose(self, res): ) ) - for i in res.plugins_res: - self.f.write(" |_ %s\n" % i.issue) + for plugin_res in res.plugins_res: + if plugin_res.is_visible(self.verbose): + self.f.write( + " |_ {} {}\n".format( + plugin_res.issue, plugin_res.data if plugin_res.data else "" + ) + ) def _print(self, res): if res.exception: @@ -299,8 +305,13 @@ def _print(self, res): % (res.lines, res.words, res.chars, res.description) ) - for i in res.plugins_res: - self.f.write(" |_ %s\n" % i.issue) + for plugin_res in res.plugins_res: + if plugin_res.is_visible(self.verbose): + self.f.write( + " |_ {} {}\n".format( + plugin_res.issue, plugin_res.data if plugin_res.data else "" + ) + ) def result(self, res): if self.verbose: @@ -332,6 +343,35 @@ def footer(self, summary): ) +@moduleman_plugin +class field(BasePrinter): + name = "field" + author = ("Xavi Mendez (@xmendez)",) + version = "0.1" + summary = "Raw output format only showing the specified field expression. No header or footer." + category = ["default"] + priority = 99 + + def __init__(self, output): + BasePrinter.__init__(self, output) + + def header(self, summary): + pass + + def result(self, res): + if res._fields: + to_print = res._field("\n") + if to_print: + print(to_print) + else: + raise FuzzExceptPluginBadParams( + "You need to supply valid --field or --efield expression for unsing this printer." + ) + + def footer(self, summary): + pass + + @moduleman_plugin class csv(BasePrinter): name = "csv" diff --git a/src/wfuzz/plugins/scripts/backups.py b/src/wfuzz/plugins/scripts/backups.py index 880566a5..eb0a70a0 100644 --- a/src/wfuzz/plugins/scripts/backups.py +++ b/src/wfuzz/plugins/scripts/backups.py @@ -19,7 +19,7 @@ class backups(BasePlugin): "* http://localhost/dir.EXTENSIONS", author = ("Xavi Mendez (@xmendez)",) version = "0.1" - category = ["re-enqueue", "active", "discovery"] + category = ["fuzzer", "active"] priority = 99 parameters = ( diff --git a/src/wfuzz/plugins/scripts/cookies.py b/src/wfuzz/plugins/scripts/cookies.py index eb835929..ed31ec25 100644 --- a/src/wfuzz/plugins/scripts/cookies.py +++ b/src/wfuzz/plugins/scripts/cookies.py @@ -2,6 +2,9 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin +KBASE_NEW_COOKIE = "cookies.cookie" + + @moduleman_plugin class cookies(BasePlugin): name = "cookies" @@ -9,7 +12,7 @@ class cookies(BasePlugin): version = "0.1" summary = "Looks for new cookies" description = ("Looks for new cookies",) - category = ["verbose", "passive"] + category = ["info", "passive", "default"] priority = 99 parameters = () @@ -28,8 +31,10 @@ def process(self, fuzzresult): if ( name != "" - and "cookie" not in self.kbase - or name not in self.kbase["cookie"] + and KBASE_NEW_COOKIE not in self.kbase + or name not in self.kbase[KBASE_NEW_COOKIE] ): - self.kbase["cookie"] = name - self.add_result("Cookie first set - %s=%s" % (name, value)) + self.kbase[KBASE_NEW_COOKIE] = name + self.add_result( + "cookie", "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 05835598..ce5c6f70 100644 --- a/src/wfuzz/plugins/scripts/cvs_extractor.py +++ b/src/wfuzz/plugins/scripts/cvs_extractor.py @@ -22,7 +22,7 @@ class cvs_extractor(BasePlugin, DiscoveryPluginMixin): version = "0.1" summary = "Parses CVS/Entries file." description = ("Parses CVS/Entries file and enqueues found entries",) - category = ["default", "active", "discovery"] + category = ["active", "discovery"] priority = 99 parameters = () diff --git a/src/wfuzz/plugins/scripts/errors.py b/src/wfuzz/plugins/scripts/errors.py index 31208f54..e64e3f4e 100644 --- a/src/wfuzz/plugins/scripts/errors.py +++ b/src/wfuzz/plugins/scripts/errors.py @@ -11,7 +11,7 @@ class errors(BasePlugin): version = "0.1" summary = "Looks for error messages" description = ("Looks for common error messages",) - category = ["default", "passive"] + category = ["default", "passive", "info"] priority = 99 parameters = () @@ -111,4 +111,4 @@ def validate(self, fuzzresult): def process(self, fuzzresult): for regex in self.error_regex: for regex_match in regex.findall(fuzzresult.history.content): - self.add_result("Error identified: {}".format(regex_match)) + self.add_result("errors", "Error identified", regex_match) diff --git a/src/wfuzz/plugins/scripts/grep.py b/src/wfuzz/plugins/scripts/grep.py index df6ff295..79e7f160 100644 --- a/src/wfuzz/plugins/scripts/grep.py +++ b/src/wfuzz/plugins/scripts/grep.py @@ -37,4 +37,4 @@ def validate(self, fuzzresult): def process(self, fuzzresult): for r in self.regex.findall(fuzzresult.history.content): - self.add_result("Pattern match %s" % r) + self.add_result("match", "Pattern match", r) diff --git a/src/wfuzz/plugins/scripts/headers.py b/src/wfuzz/plugins/scripts/headers.py index 19732412..661bef82 100644 --- a/src/wfuzz/plugins/scripts/headers.py +++ b/src/wfuzz/plugins/scripts/headers.py @@ -1,15 +1,115 @@ from wfuzz.plugin_api.base import BasePlugin from wfuzz.externals.moduleman.plugin import moduleman_plugin +import re + +KBASE_KEY = "http.servers" +KBASE_KEY_RESP_UNCOMMON = "http.response.headers.uncommon" +KBASE_KEY_REQ_UNCOMMON = "http.request.headers.uncommon" + +SERVER_HEADERS = ["server", "x-powered-by" "via"] + +COMMON_RESPONSE_HEADERS_REGEX_LIST = [ + r"^Server$", + r"^X-Powered-By$", + r"^Via$", + r"^Access-Control.*$", + r"^Accept-.*$", + r"^age$", + r"^allow$", + r"^Cache-control$", + r"^Client-.*$", + r"^Connection$", + r"^Content-.*$", + r"^Cross-Origin-Resource-Policy$", + r"^Date$", + r"^Etag$", + r"^Expires$", + r"^Keep-Alive$", + r"^Last-Modified$", + r"^Link$", + r"^Location$", + r"^P3P$", + r"^Pragma$", + r"^Proxy-.*$", + r"^Refresh$", + r"^Retry-After$", + r"^Referrer-Policy$", + r"^Set-Cookie$", + r"^Server-Timing$", + r"^Status$", + r"^Strict-Transport-Security$", + r"^Timing-Allow-Origin$", + r"^Trailer$", + r"^Transfer-Encoding$", + r"^Upgrade$", + r"^Vary$", + r"^Warning^$", + r"^WWW-Authenticate$", + r"^X-Content-Type-Options$", + r"^X-Download-Options$", + r"^X-Frame-Options$", + r"^X-Microsite$", + r"^X-Request-Handler-Origin-Region$", + r"^X-XSS-Protection$", +] + +COMMON_RESPONSE_HEADERS_REGEX = re.compile( + "({})".format("|".join(COMMON_RESPONSE_HEADERS_REGEX_LIST)), re.IGNORECASE +) + +COMMON_REQ_HEADERS_REGEX_LIST = [ + r"A-IM$", + r"Accept$", + r"Accept-.*$", + r"Access-Control-.*$", + r"Authorization$", + r"Cache-Control$", + r"Connection$", + r"Content-.*$", + r"Cookie$", + r"Date$", + r"Expect$", + r"Forwarded$", + r"From$", + r"Host$", + r"If-.*$", + r"Max-Forwards$", + r"Origin$", + r"Pragma$", + r"Proxy-Authorization$", + r"Range$", + r"Referer$", + r"TE$", + r"User-Agent$", + r"Upgrade$", + r"Upgrade-Insecure-Requests$", + r"Via$", + r"Warning$", + r"X-Requested-With$", + r"X-HTTP-Method-Override$", + r"X-Requested-With$", +] + +COMMON_REQ_HEADERS_REGEX = re.compile( + "({})".format("|".join(COMMON_REQ_HEADERS_REGEX_LIST)), re.IGNORECASE +) + @moduleman_plugin class headers(BasePlugin): name = "headers" author = ("Xavi Mendez (@xmendez)",) version = "0.1" - summary = "Looks for server headers" - description = ("Looks for new server headers",) - category = ["verbose", "passive"] + summary = "Looks for HTTP headers." + description = ( + "Looks for NEW HTTP headers:", + "\t- Response HTTP headers associated to web servers.", + "\t- Uncommon response HTTP headers.", + "\t- Uncommon request HTTP headers.", + "It is worth noting that, only the FIRST match of the above headers is registered.", + ) + category = ["info", "passive", "default"] priority = 99 parameters = () @@ -19,28 +119,60 @@ def __init__(self): def validate(self, fuzzresult): return True + def check_request_header(self, fuzzresult, header, value): + header_value = None + if not COMMON_REQ_HEADERS_REGEX.match(header): + header_value = header + + if header_value is not None: + if ( + header_value.lower() not in self.kbase[KBASE_KEY_REQ_UNCOMMON] + or KBASE_KEY_REQ_UNCOMMON not in self.kbase + ): + self.add_result( + "reqheader", + "New uncommon HTTP request header", + "{}: {}".format(header_value, value), + ) + + self.kbase[KBASE_KEY_REQ_UNCOMMON].append(header_value.lower()) + + def check_response_header(self, fuzzresult, header, value): + header_value = None + if not COMMON_RESPONSE_HEADERS_REGEX.match(header): + header_value = header + + if header_value is not None: + if ( + header_value.lower() not in self.kbase[KBASE_KEY_RESP_UNCOMMON] + or KBASE_KEY_RESP_UNCOMMON not in self.kbase + ): + self.add_result( + "header", + "New uncommon HTTP response header", + "{}: {}".format( + header_value, fuzzresult.history.headers.response[header_value], + ), + ) + + self.kbase[KBASE_KEY_RESP_UNCOMMON].append(header_value.lower()) + + def check_server_header(self, fuzzresult, header, value): + if header.lower() in SERVER_HEADERS: + if ( + value.lower() not in self.kbase[KBASE_KEY] + or KBASE_KEY not in self.kbase + ): + self.add_result( + "server", "New server HTTP response header", "{}".format(value), + ) + + self.kbase[KBASE_KEY].append(value.lower()) + def process(self, fuzzresult): - serverh = "" - poweredby = "" - - if "Server" in fuzzresult.history.headers.response: - serverh = fuzzresult.history.headers.response["Server"] - - if "X-Powered-By" in fuzzresult.history.headers.response: - poweredby = fuzzresult.history.headers.response["X-Powered-By"] - - if serverh != "": - if "server" not in self.kbase: - self.kbase["server"] = serverh - self.add_result("Server header first set - " + serverh) - elif serverh not in self.kbase["server"]: - self.kbase["server"] = serverh - self.add_result("New Server header - " + serverh) - - if poweredby != "": - if "poweredby" not in self.kbase: - self.kbase["poweredby"] = poweredby - self.add_result("Powered-by header first set - " + poweredby) - elif poweredby not in self.kbase["poweredby"]: - self.kbase["poweredby"] = poweredby - self.add_result("New X-Powered-By header - " + poweredby) + for header, value in fuzzresult.history.headers.request.items(): + self.check_request_header(fuzzresult, header, value) + + for header, value in fuzzresult.history.headers.response.items(): + self.check_response_header(fuzzresult, header, value) + self.check_server_header(fuzzresult, header, value) diff --git a/src/wfuzz/plugins/scripts/links.py b/src/wfuzz/plugins/scripts/links.py index 9c148d40..e0d0cd76 100644 --- a/src/wfuzz/plugins/scripts/links.py +++ b/src/wfuzz/plugins/scripts/links.py @@ -12,6 +12,13 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin +KBASE_PARAM_PATH = "links.add_path" +KBASE_PARAM_ENQUEUE = "links.enqueue" +KBASE_PARAM_DOMAIN_REGEX = "links.domain" +KBASE_PARAM_REGEX = "links.regex" +KBASE_NEW_DOMAIN = "links.new_domains" + + @moduleman_plugin class links(BasePlugin, DiscoveryPluginMixin): name = "links" @@ -23,58 +30,101 @@ class links(BasePlugin, DiscoveryPluginMixin): priority = 99 parameters = ( - ("add_path", False, False, "Add parsed paths as results."), - ("regex", None, False, "Regex of accepted domains."), + ("enqueue", "True", False, "If True, enqueue found links.",), + ( + "add_path", + "False", + False, + "if True, re-enqueue found paths. ie. /path/link.html link enqueues also /path/", + ), + ( + "domain", + None, + False, + "Regex of accepted domains tested against url.netloc. This is useful for restricting crawling certain domains.", + ), + ( + "regex", + None, + False, + "Regex of accepted links tested against the full url. If domain is not set and regex is, domain defaults to .*. This is useful for restricting crawling certain file types.", + ), ) def __init__(self): BasePlugin.__init__(self) regex = [ - r'href="((?!mailto:|tel:|#|javascript:).*?)"', - r'src="((?!javascript:).*?)"', - r'action="((?!javascript:).*?)"', - # http://en.wikipedia.org/wiki/Meta_refresh - r'', + r'\b(?:(?', # http://en.wikipedia.org/wiki/Meta_refresh r'getJSON\("(.*?)"', + r"[^/][`'\"]([\/][a-zA-Z0-9_.-]+)+(?!(?:[,;\s]))", # based on https://github.com/nahamsec/JSParser/blob/master/handler.py#L93 ] self.regex = [] - for i in regex: - self.regex.append(re.compile(i, re.MULTILINE | re.DOTALL)) + for regex_str in regex: + self.regex.append(re.compile(regex_str, re.MULTILINE | re.DOTALL)) - self.add_path = self.kbase["links.add_path"] + self.regex_header = [ + ("Link", re.compile(r"<(.*)>;")), + ("Location", re.compile(r"(.*)")), + ] + + self.add_path = self._bool(self.kbase[KBASE_PARAM_PATH][0]) + self.enqueue_links = self._bool(self.kbase[KBASE_PARAM_ENQUEUE][0]) self.domain_regex = None - if self.kbase["links.regex"][0]: + if self.kbase[KBASE_PARAM_DOMAIN_REGEX][0]: self.domain_regex = re.compile( - self.kbase["links.regex"][0], re.MULTILINE | re.DOTALL + self.kbase[KBASE_PARAM_DOMAIN_REGEX][0], re.IGNORECASE + ) + + self.regex_param = None + if self.kbase[KBASE_PARAM_REGEX][0]: + self.regex_param = re.compile( + self.kbase[KBASE_PARAM_REGEX][0], re.IGNORECASE ) + if self.regex_param and self.domain_regex is None: + self.domain_regex = re.compile(".*", re.IGNORECASE) + + self.list_links = set() + def validate(self, fuzzresult): - return fuzzresult.code in [200] + self.list_links = set() + return fuzzresult.code in [200, 301, 302, 303, 307, 308] def process(self, fuzzresult): - list_links = set() # O # ParseResult(scheme='', netloc='', path='www.owasp.org/index.php/OWASP_EU_Summit_2008', params='', query='', fragment='') + for header, regex in self.regex_header: + if header in fuzzresult.history.headers.response: + for link_url in regex.findall( + fuzzresult.history.headers.response[header] + ): + if link_url: + self.process_link(fuzzresult, link_url) + for regex in self.regex: for link_url in regex.findall(fuzzresult.history.content): - if not link_url: - continue + if link_url: + self.process_link(fuzzresult, link_url) - parsed_link = parse_url(link_url) + def process_link(self, fuzzresult, link_url): + 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) + 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 self.list_links: + self.list_links.add(cache_key) + self.enqueue_link(fuzzresult, link_url, parsed_link) def enqueue_link(self, fuzzresult, link_url, parsed_link): # dir path @@ -84,17 +134,20 @@ def enqueue_link(self, fuzzresult, link_url, parsed_link): self.queue_url(urljoin(fuzzresult.url, newpath)) # file path - self.queue_url(urljoin(fuzzresult.url, link_url)) + new_link = urljoin(fuzzresult.url, link_url) + + if not self.regex_param or ( + self.regex_param and self.regex_param.search(new_link) is not None + ): + if self.enqueue_links: + self.queue_url(new_link) + self.add_result("link", "New link found", new_link) def from_domain(self, fuzzresult, parsed_link): # relative path if not parsed_link.netloc and parsed_link.path: return True - # same domain - if parsed_link.netloc == self.base_fuzz_res.history.urlp.netloc: - return True - # regex domain if ( self.domain_regex @@ -102,11 +155,15 @@ def from_domain(self, fuzzresult, parsed_link): ): return True + # same domain + if parsed_link.netloc == self.base_fuzz_res.history.urlp.netloc: + return True + if ( parsed_link.netloc - and parsed_link.netloc not in self.kbase["links.new_domains"] + and parsed_link.netloc not in self.kbase[KBASE_NEW_DOMAIN] ): - self.kbase["links.new_domains"].append(parsed_link.netloc) + self.kbase[KBASE_NEW_DOMAIN].append(parsed_link.netloc) self.add_result( - "New domain found, link not enqueued %s" % parsed_link.netloc + "domain", "New domain found (link not enqueued)", parsed_link.netloc ) diff --git a/src/wfuzz/plugins/scripts/listing.py b/src/wfuzz/plugins/scripts/listing.py index d49b5247..b5f114ce 100644 --- a/src/wfuzz/plugins/scripts/listing.py +++ b/src/wfuzz/plugins/scripts/listing.py @@ -47,5 +47,5 @@ def validate(self, fuzzresult): def process(self, fuzzresult): for r in self.regex: if len(r.findall(fuzzresult.history.content)) > 0: - self.add_result("Directory listing identified") + self.add_result("msg", "Directory listing identified", None) break diff --git a/src/wfuzz/plugins/scripts/npm_deps.py b/src/wfuzz/plugins/scripts/npm_deps.py index c54e9bf2..dde3156a 100644 --- a/src/wfuzz/plugins/scripts/npm_deps.py +++ b/src/wfuzz/plugins/scripts/npm_deps.py @@ -14,7 +14,7 @@ class npm_deps(BasePlugin): description = ( "Extracts npm packages by using regex pattern from the HTTP response and prints it", ) - category = ["default"] + category = ["info"] priority = 99 parameters = () @@ -42,8 +42,8 @@ def validate(self, fuzzresult): 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) + self.add_result("dependency", "npm dependency", name) if self.match: for name, version in self.REGEX_PATT.findall(self.match.group(1)): - self.add_result(name) + self.add_result("dev_dependency", "npm dev dependency", name) diff --git a/src/wfuzz/plugins/scripts/robots.py b/src/wfuzz/plugins/scripts/robots.py index 57851ae6..4ca83e56 100644 --- a/src/wfuzz/plugins/scripts/robots.py +++ b/src/wfuzz/plugins/scripts/robots.py @@ -19,7 +19,7 @@ class robots(BasePlugin, DiscoveryPluginMixin): version = "0.1" summary = "Parses robots.txt looking for new content." description = ("Parses robots.txt looking for new content.",) - category = ["default", "active", "discovery"] + category = ["active", "discovery"] priority = 99 parameters = () @@ -53,4 +53,6 @@ def process(self, fuzzresult): url = url.strip(" *") if url: - self.queue_url(urljoin(fuzzresult.url, url)) + new_link = urljoin(fuzzresult.url, url) + self.queue_url(new_link) + self.add_result("link", "New link found", new_link) diff --git a/src/wfuzz/plugins/scripts/screenshot.py b/src/wfuzz/plugins/scripts/screenshot.py index fa8c4240..bec68bd7 100644 --- a/src/wfuzz/plugins/scripts/screenshot.py +++ b/src/wfuzz/plugins/scripts/screenshot.py @@ -48,4 +48,4 @@ def process(self, fuzzresult): "--print-backgrounds=on", ] ) - self.add_result("Screnshot taken, output at %s" % filename) + self.add_result("file", "Screnshot taken", filename) diff --git a/src/wfuzz/plugins/scripts/sitemap.py b/src/wfuzz/plugins/scripts/sitemap.py index 3328e1e7..310e2da6 100644 --- a/src/wfuzz/plugins/scripts/sitemap.py +++ b/src/wfuzz/plugins/scripts/sitemap.py @@ -13,7 +13,7 @@ class sitemap(BasePlugin, DiscoveryPluginMixin): version = "0.1" summary = "Parses sitemap.xml file" description = ("Parses sitemap.xml file",) - category = ["default", "active", "discovery"] + category = ["active", "discovery"] priority = 99 parameters = () diff --git a/src/wfuzz/plugins/scripts/svn_extractor.py b/src/wfuzz/plugins/scripts/svn_extractor.py index 8e1bf468..724bfb1f 100644 --- a/src/wfuzz/plugins/scripts/svn_extractor.py +++ b/src/wfuzz/plugins/scripts/svn_extractor.py @@ -16,7 +16,7 @@ class svn_extractor(BasePlugin, DiscoveryPluginMixin): version = "0.1" summary = "Parses .svn/entries file." description = ("Parses CVS/Entries file and enqueues found entries",) - category = ["default", "active", "discovery"] + category = ["active", "discovery"] priority = 99 parameters = () @@ -57,7 +57,7 @@ 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("authors", "SVN authors", ", ".join(author_list)) for f in file_list: u = urljoin(base_url.replace("/.svn/", "/"), f) diff --git a/src/wfuzz/plugins/scripts/title.py b/src/wfuzz/plugins/scripts/title.py index 36552854..dd756e37 100644 --- a/src/wfuzz/plugins/scripts/title.py +++ b/src/wfuzz/plugins/scripts/title.py @@ -9,7 +9,7 @@ class title(BasePlugin): version = "0.1" summary = "Parses HTML page title" description = ("Parses HTML page title",) - category = ["verbose", "passive"] + category = ["info", "passive"] priority = 99 parameters = () @@ -30,4 +30,4 @@ def process(self, fuzzresult): or title not in self.kbase["title"] ): self.kbase["title"] = title - self.add_result("Page title: %s" % title) + self.add_result("title", "Page title", title) diff --git a/src/wfuzz/plugins/scripts/wcdb.py b/src/wfuzz/plugins/scripts/wcdb.py index 3d128412..2308f1a1 100644 --- a/src/wfuzz/plugins/scripts/wcdb.py +++ b/src/wfuzz/plugins/scripts/wcdb.py @@ -20,7 +20,7 @@ class wcdb_extractor(BasePlugin, DiscoveryPluginMixin): version = "0.1" summary = "Parses subversion's wc.db file." description = ("Parses subversion's wc.db file.",) - category = ["default", "active", "discovery"] + category = ["active", "discovery"] priority = 99 parameters = () @@ -65,10 +65,10 @@ 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("authors", "SVN authors", ", ".join(author_list)) if list_items: for f, pristine in list_items: u = urljoin(fuzzresult.url.replace("/.svn/wc.db", "/"), f) if self.queue_url(u): - self.add_result("SVN %s source code in %s" % (f, pristine)) + self.add_result("source", "SVN source code", f) diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 3a87676d..392d07c1 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -129,14 +129,15 @@ def show_plugin_ext_help(self, registrant, category="$all$"): for desc_lines in plugin.description: print(" %s" % desc_lines) print("Parameters:") - for param in plugin.parameters: + for name, default_value, mandatory, description in plugin.parameters: print( - " %s %s%s: %s" - % ( - "+" if param[2] else "-", - param[0], - " (= %s)" % str(param[1]) if param[1] else "", - param[3], + " {} {}{}: {}".format( + "+" if mandatory else "-", + name, + " (= %s)" % str(default_value) + if default_value is not None + else "", + description, ) ) print("\n") @@ -612,7 +613,7 @@ def _parse_scripts(self, optsd, options): options["script"] = "default,verbose" if "--AAA" in optsd: - options["script"] = "default,discovery,verbose" + options["script"] = "default,verbose,discovery" if "--script" in optsd: options["script"] = ( diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index cd609268..c5d4fc94 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -128,7 +128,7 @@ \t--req-delay N : Sets the maximum time in seconds the request is allowed to take (CURLOPT_TIMEOUT). Default 90. \t--conn-delay N : Sets the maximum time in seconds the connection phase to the server to take (CURLOPT_CONNECTTIMEOUT). Default 90. \t -\t-A, --AA, --AAA : Alias for --script=default,verbose,discovery -v -c +\t-A, --AA, --AAA : Alias for -v -c and --script=default,verbose,discover respectively \t--no-cache : Disable plugins cache. Every request will be scanned. \t--script= : Equivalent to --script=default \t--script= : Runs script's scan. is a comma separated list of plugin-files or plugin-categories @@ -178,9 +178,11 @@ \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--prev : Print the previous HTTP requests (only when using payloads generating fuzzresults) \t--efield : Show the specified language expression together with the current payload. Repeat option for various fields. \t--field : Do not show the payload but only the specified language expression. Repeat option for various fields. \t +\t-A, --AA, --AAA : Alias for -v -c and --script=default,verbose,discover respectively \t--script= : Equivalent to --script=default \t--script= : Runs script's scan. is a comma separated list of plugin-files or plugin-categories \t--script-help= : Show help about scripts. diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index a0fcb805..37e7bb8e 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -7,7 +7,7 @@ except ImportError: from itertools import izip_longest as zip_longest -from wfuzz.fuzzobjects import FuzzWordType, FuzzType +from wfuzz.fuzzobjects import FuzzWordType, FuzzType, FuzzPlugin from .common import exec_banner, Term from .getch import _Getch @@ -82,6 +82,7 @@ def __init__(self, fuzzer, view): self.fuzzer = fuzzer self.view = view self.__paused = False + self.stats = fuzzer.options.get("compiled_stats") self.view.dispatcher.subscribe(self.on_help, "?") self.view.dispatcher.subscribe(self.on_pause, "p") @@ -91,8 +92,8 @@ def __init__(self, fuzzer, view): # dynamic keyboard bindings def on_exit(self, **event): self.fuzzer.cancel_job() - self.fuzzer.genReq.stats.mark_end() self.view.cancel_job() + self.fuzzer.options.close() def on_help(self, **event): print(usage) @@ -101,61 +102,57 @@ def on_pause(self, **event): self.__paused = not self.__paused if self.__paused: self.fuzzer.pause_job() - - if self._debug: - print("\n=============== Paused ==================") - stats = self.fuzzer.stats() - for k, v in list(stats.items()): - print("%s: %s" % (k, v)) - print("\n=========================================") else: self.fuzzer.resume_job() def on_stats(self, **event): if self._debug: - print("\n=============== Paused ==================") - stats = self.fuzzer.stats() - for k, v in list(stats.items()): - print("%s: %s" % (k, v)) - print("\n=========================================") + self.show_debug_stats() else: - 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(), - ) + self.show_stats() + + def show_debug_stats(self): + print("\n=============== Paused ==================") + stats = self.fuzzer.stats() + for k, v in list(stats.items()): + print("%s: %s" % (k, v)) + print("\n=========================================") + + def show_stats(self): + pending = self.stats.total_req - self.stats.processed() + summary = self.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(), ) - 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 ) - print("Total time: %s\r" % str(summary.totaltime)[:8]) - if req_sec > 0: - print("Requests/sec.: %s\r" % str(req_sec)[:8]) - eta = pending / req_sec - if eta > 60: - print("ET left min.: %s\r\n" % str(eta / 60)[:8]) - else: - print("ET left sec.: %s\r\n" % str(eta)[:8]) + 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 + ) + print("Total time: %s\r" % str(summary.totaltime)[:8]) + if req_sec > 0: + print("Requests/sec.: %s\r" % str(req_sec)[:8]) + eta = pending / req_sec + if eta > 60: + print("ET left min.: %s\r\n" % str(eta / 60)[:8]) + else: + print("ET left sec.: %s\r\n" % str(eta)[:8]) class View: - widths = [10, 8, 6, 6, 9, getTerminalSize()[0] - 65] - verbose_widths = [10, 10, 8, 6, 6, 9, 30, 30, getTerminalSize()[0] - 145] + widths = [10, 8, 6, 8, 9, getTerminalSize()[0] - 65] + verbose_widths = [10, 10, 8, 8, 6, 9, 30, 30, getTerminalSize()[0] - 145] def __init__(self, session_options): self.colour = session_options["colour"] @@ -298,7 +295,7 @@ def result(self, res): else: self._print(res) - if res.item_type == FuzzType.RESULT: + if not res.discarded: if ( self.previous and res.payload_man @@ -311,8 +308,16 @@ def result(self, res): self._print(prev_res, print_nres=False) if res.plugins_res: - for i in res.plugins_res: - sys.stdout.write(" |_ %s\r" % i.issue) + for plugin_res in res.plugins_res: + if not plugin_res.is_visible(self.verbose): + continue + + sys.stdout.write( + " |_ {} {}\r".format( + plugin_res.issue, plugin_res.data if plugin_res.data else "" + ) + ) + sys.stdout.write(" |_ %s\r" % plugin_res.issue) sys.stdout.write("\n\r") self.printed_lines = 0 diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index 41ccb552..559b9df6 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -15,6 +15,25 @@ from .fuzzobjects import FuzzWordType +PROFILING = False + + +def print_profiling(profiling_list, profiling_header): + avg = [float(sum(col)) / len(col) for col in list(zip(*profiling_list))] + maxx = [max(col) for col in list(zip(*profiling_list))] + + print( + ", ".join( + ["{}={}".format(pair[0], pair[1]) for pair in zip(profiling_header, avg)] + ) + ) + print( + ", ".join( + ["{}={}".format(pair[0], pair[1]) for pair in zip(profiling_header, maxx)] + ) + ) + + def main(): kb = None fz = None @@ -41,8 +60,19 @@ def main(): Controller(fz, kb) kb.start() + if PROFILING: + profiling_header = list(fz.qmanager._queues.keys()) + profiling_list = [] + for res in fz: - pass + if PROFILING: + profiling = list(fz.qmanager.get_stats().items()) + profiling_list.append([pair[1] for pair in profiling]) + else: + pass + + if PROFILING: + print_profiling(profiling_list, profiling_header) except FuzzException as e: warnings.warn("Fatal exception: {}".format(str(e))) except KeyboardInterrupt: @@ -71,7 +101,7 @@ def usage(): from .api import fuzz try: - short_opts = "hvce:z:f:w:o:" + short_opts = "hvce:z:f:w:o:A" long_opts = [ "efield=", "ee=", @@ -100,6 +130,8 @@ def usage(): "script-help=", "script=", "script-args=", + "prev", + "AA", ] session_options = CLParser( sys.argv, @@ -126,7 +158,9 @@ def usage(): if payload_type == FuzzWordType.WORD: print(res.description) elif payload_type == FuzzWordType.FUZZRES and session_options["show_field"]: - print(res._field()) + field_to_print = res._field("\n") + if field_to_print: + print(field_to_print) except KeyboardInterrupt: pass @@ -143,28 +177,39 @@ def usage(): print("\n\twfencode --help This help") print("\twfencode -d decoder_name string_to_decode") print("\twfencode -e encoder_name string_to_encode") + print("\twfencode -e encoder_name -i <>") print() from .api import encode, decode import getopt try: - opts, args = getopt.getopt(sys.argv[1:], "he:d:", ["help"]) + opts, args = getopt.getopt(sys.argv[1:], "hie:d:", ["help"]) except getopt.GetoptError as err: warnings.warn(str(err)) usage() sys.exit(2) - if len(args) == 0: + arg_keys = [i for i, j in opts] + + if len(args) == 0 and "-i" not in arg_keys: usage() sys.exit() try: for o, value in opts: if o == "-e": - print((encode(value, args[0]))) + if "-i" in arg_keys: + for std in sys.stdin: + print(encode(value, std.strip())) + else: + print(encode(value, args[0])) elif o == "-d": - print((decode(value, args[0]))) + if "-i" in arg_keys: + for std in sys.stdin: + print(decode(value, std.strip())) + else: + print(decode(value, args[0])) elif o in ("-h", "--help"): usage() sys.exit() @@ -182,6 +227,8 @@ def usage(): ) except FuzzException as e: warnings.warn(("\nFatal exception: %s" % str(e))) + except Exception as e: + warnings.warn(("Unhandled exception: %s" % str(e))) def main_gui(): diff --git a/tests/acceptance/test_saved_filter.py b/tests/acceptance/test_saved_filter.py new file mode 100644 index 00000000..1866c74c --- /dev/null +++ b/tests/acceptance/test_saved_filter.py @@ -0,0 +1,52 @@ +import pytest +import os +import tempfile + +import wfuzz + + +def get_temp_file(): + temp_name = next(tempfile._get_candidate_names()) + defult_tmp_dir = tempfile._get_default_tempdir() + + return os.path.join(defult_tmp_dir, temp_name) + + +def test_filter_prev_payload(): + + filename = get_temp_file() + for res in wfuzz.get_session( + "-z range --zD 0-0 -H test:1 -u http://localhost:9000/anything/FUZZ" + ).fuzz(save=filename): + pass + + filename_new = get_temp_file() + for res in wfuzz.get_session( + "-z wfuzzp --zD {} -u FUZZ -H test:2 --oF {}".format(filename, filename_new) + ).fuzz(save=filename_new): + pass + + assert ( + len( + list( + wfuzz.get_session( + "-z wfuzzp --zD {} --slice r.headers.request.test=2 --dry-run -u FUZZ".format( + filename_new + ) + ).fuzz() + ) + ) + == 1 + ) + assert ( + len( + list( + wfuzz.get_session( + "-z wfuzzp --zD {} --slice FUZZ[r.headers.request.test]=1 --dry-run -u FUZZ".format( + filename_new + ) + ).fuzz() + ) + ) + == 1 + ) diff --git a/tests/filters/test_filter.py b/tests/filters/test_filter.py index 905ed5ad..505bad5c 100644 --- a/tests/filters/test_filter.py +++ b/tests/filters/test_filter.py @@ -46,3 +46,26 @@ def test_filter_ret_values_no_response( filter_obj.is_visible(example_full_fuzzres_no_response, filter_string) == expected_result ) + + +@pytest.mark.parametrize( + "filter_string, expected_result", + [ + ( + "r.cookies.response.name|diff('test')", + "--- prev\n\n+++ current\n\n@@ -1 +1 @@\n\n-test\n+Nicholas", + ), + ("r.cookies.response.nAMe|upper()", "NICHOLAS"), + ("r.cookies.response.name|upper()", "NICHOLAS"), + ("r.cookies.response.name|lower()", "nicholas"), + ("r.cookies.response.name|startswith('N')", True), + ("r.cookies.response.name|replace('N','n')", "nicholas"), + ("'%2e%2e'|unquote()", ".."), + ("'%2e%2f'|decode('urlencode')", "./"), + ("'%%'|encode('urlencode')", "%25%25"), + ], +) +def test_filter_operators( + filter_obj, example_full_fuzzres, filter_string, expected_result +): + assert filter_obj.is_visible(example_full_fuzzres, filter_string) == expected_result diff --git a/tests/plugins/test_burplog.py b/tests/plugins/test_burplog.py new file mode 100644 index 00000000..1f05cb86 --- /dev/null +++ b/tests/plugins/test_burplog.py @@ -0,0 +1,307 @@ +import pytest +import sys +from io import BytesIO + +import wfuzz +from wfuzz.facade import Facade + +try: + # Python >= 3.3 + from unittest import mock +except ImportError: + # Python < 3.3 + import mock + + +@pytest.fixture +def burplog_file(request): + class mock_saved_session(object): + def __init__(self, infile): + self.outfile = BytesIO(bytes(infile, "ascii")) + self.outfile.seek(0) + self.outfile.name = "mockfile" + + def close(self): + pass + + def read(self, *args, **kwargs): + return self.outfile.read(*args, **kwargs) + + def seek(self, *args, **kwargs): + return self.outfile.seek(*args, **kwargs) + + def tell(self): + return self.outfile.tell() + + def readline(self, *args, **kwargs): + line = self.outfile.readline() + if line: + return line.decode("utf-8") + return "" + + return mock_saved_session(request.param) + + +@pytest.mark.parametrize( + "burplog_file, expected_content", + [ + # ( + # ( + # "======================================================\n" + # "22:35:55 https://aus5.mozilla.org:443 [35.244.181.201]\n" + # "======================================================\n" + # "GET /update/3/SystemAddons/81.0/20200917005511/Linux_x86_64-gcc3/null/release-cck-ubuntu/Linux%205.4.0-48-generic%20(GTK%203.24.20%2Clibpulse%2013.99.0)/canonical/1.0/update.xml HTTP/1.1\n" + # "Host: aus5.mozilla.org\n" + # "\n" + # "\n" + # "======================================================\n" + # "HTTP/1.1 200 OK\n" + # "Server: nginx/1.17.9\n" + # "\n" + # "\n" + # "\n" + # "\r\n" + # "======================================================\n" + # "\n" + # "\n" + # "\n" + # ), + # '\n\n', + # ), + ( + ( + "======================================================\n" + "22:35:55 https://aus5.mozilla.org:443 [35.244.181.201]\n" + "======================================================\n" + "GET /update/3/SystemAddons/81.0/20200917005511/Linux_x86_64-gcc3/null/release-cck-ubuntu/Linux%205.4.0-48-generic%20(GTK%203.24.20%2Clibpulse%2013.99.0)/canonical/1.0/update.xml HTTP/1.1\n" + "Host: aus5.mozilla.org\n" + "\n" + "\n" + "======================================================\n" + "HTTP/1.1 200 OK\n" + "Server: nginx/1.17.9\n" + "\n" + '\n' + "\n" + " \n" + "======================================================\n" + "\n" + "\n" + "\n" + ), + '\n\n ', + ), + ( + ( + "======================================================\n" + "22:35:55 https://aus5.mozilla.org:443 [35.244.181.201]\n" + "======================================================\n" + "GET /update/3/SystemAddons/81.0/20200917005511/Linux_x86_64-gcc3/null/release-cck-ubuntu/Linux%205.4.0-48-generic%20(GTK%203.24.20%2Clibpulse%2013.99.0)/canonical/1.0/update.xml HTTP/1.1\n" + "Host: aus5.mozilla.org\n" + "\n" + "\n" + "======================================================\n" + "HTTP/1.1 200 OK\n" + "Server: nginx/1.17.9\n" + "\n" + '\n' + "\n" + "\n" + "\n" + "======================================================\n" + "\n" + "\n" + "\n" + ), + '\n\n\n', + ), + ( + ( + "======================================================\n" + "22:35:55 https://aus5.mozilla.org:443 [35.244.181.201]\n" + "======================================================\n" + "GET /update/3/SystemAddons/81.0/20200917005511/Linux_x86_64-gcc3/null/release-cck-ubuntu/Linux%205.4.0-48-generic%20(GTK%203.24.20%2Clibpulse%2013.99.0)/canonical/1.0/update.xml HTTP/1.1\n" + "Host: aus5.mozilla.org\n" + "\n" + "\n" + "======================================================\n" + "HTTP/1.1 200 OK\n" + "Server: nginx/1.17.9\n" + "\n" + '\n' + "\n" + "\n" + "======================================================\n" + "\n" + "\n" + "\n" + ), + '\n\n', + ), + ( + ( + "======================================================\n" + "2:17:05 PM https://www.xxx.es:443 [2.2.2.1]\n" + "======================================================\n" + "GET /sttc/dbook-fp/ctrip-prod-2.4.0.min.js HTTP/1.1\n" + "Host: www.xxx.es\n" + "\n" + "\n" + "======================================================\n" + "HTTP/1.1 200 OK\n" + "\n" + 'HTTP"," 333D Visionplugin\n' + "======================================================\n" + "\n" + "\n" + "\n" + ), + 'HTTP"," 333D Visionplugin', + ), + ( + ( + "======================================================\n" + "22:26:48 http://testphp.vulnweb.com:80 [176.28.50.165]\n" + "======================================================\n" + "GET /style.css HTTP/1.1\n" + "Host: testphp.vulnweb.com\n" + "\n" + "\n" + "======================================================\n" + "HTTP/1.1 304 Not Modified\n" + "Server: nginx/1.4.1\n" + "Date: Mon, 19 Jan 1970 15:36:40 GMT\n" + "Last-Modified: Wed, 11 May 2011 10:27:48 GMT\n" + "Connection: close\n" + 'ETag: "4dca64a4-156a"\n' + "\n" + "\n" + "======================================================\n" + "\n" + "\n" + "\n" + ), + "", + ), + ], + indirect=["burplog_file"], +) +def test_burplog_content(burplog_file, expected_content): + # load plugins before mocking file object + Facade().payloads + + m = mock.MagicMock(name="open", spec=open) + m.return_value = burplog_file + + mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + with mock.patch(mocked_fun, m, create=True): + payload_list = list( + wfuzz.payload( + **{ + "payloads": [ + ("burplog", {"default": "mockedfile", "encoder": None}, None) + ], + } + ) + ) + + fres = payload_list[0][0] + + assert fres.history.content == expected_content + + +@pytest.mark.parametrize( + "burplog_file, expected_req_headers, expected_resp_headers", + [ + ( + ( + "======================================================\n" + "22:35:55 https://aus5.mozilla.org:443 [35.244.181.201]\n" + "======================================================\n" + "GET /update/3/SystemAddons/81.0/20200917005511/Linux_x86_64-gcc3/null/release-cck-ubuntu/Linux%205.4.0-48-generic%20(GTK%203.24.20%2Clibpulse%2013.99.0)/canonical/1.0/update.xml HTTP/1.1\n" + "Host: aus5.mozilla.org\n" + "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0\n" + "Accept: */*\n" + "Accept-Language: en-GB,en;q=0.5\n" + "Accept-Encoding: gzip, deflate\n" + "Cache-Control: no-cache\n" + "Pragma: no-cache\n" + "Connection: close\n" + "\n" + "\n" + "======================================================\n" + "HTTP/1.1 200 OK\n" + "Server: nginx/1.17.9\n" + "Date: Sun, 01 Nov 2020 21:35:08 GMT\n" + "Content-Type: text/xml; charset=utf-8\n" + "Content-Length: 42\n" + "Strict-Transport-Security: max-age=31536000;\n" + "X-Content-Type-Options: nosniff\n" + "Content-Security-Policy: default-src 'none'; frame-ancestors 'none'\n" + "X-Proxy-Cache-Status: EXPIRED\n" + "Via: 1.1 google\n" + "Age: 47\n" + "Cache-Control: public, max-age=90\n" + "Alt-Svc: clear\n" + "Connection: close\n" + "\n" + '\n' + "\n" + "\n" + "======================================================\n" + "\n" + "\n" + "\n" + ), + { + "Host": "aus5.mozilla.org", + "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0", + "Accept": "*/*", + "Accept-Language": "en-GB,en;q=0.5", + "Accept-Encoding": "gzip, deflate", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Connection": "close", + }, + { + "Server": "nginx/1.17.9", + "Date": "Sun, 01 Nov 2020 21:35:08 GMT", + "Content-Type": "text/xml; charset=utf-8", + "Content-Length": "42", + "Strict-Transport-Security": "max-age=31536000;", + "X-Content-Type-Options": "nosniff", + "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'", + "X-Proxy-Cache-Status": "EXPIRED", + "Via": "1.1 google", + "Age": "47", + "Cache-Control": "public, max-age=90", + "Alt-Svc": "clear", + "Connection": "close", + }, + ), + ], + indirect=["burplog_file"], +) +def test_burplog_headers(burplog_file, expected_req_headers, expected_resp_headers): + # load plugins before mocking file object + Facade().payloads + + m = mock.MagicMock(name="open", spec=open) + m.return_value = burplog_file + + mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" + with mock.patch(mocked_fun, m, create=True): + payload_list = list( + wfuzz.payload( + **{ + "payloads": [ + ("burplog", {"default": "mockedfile", "encoder": None}, None) + ], + } + ) + ) + + fres = payload_list[0][0] + + assert fres.history.headers.request == expected_req_headers + assert fres.history.headers.response == expected_resp_headers diff --git a/tests/plugins/test_links.py b/tests/plugins/test_links.py index a9408d08..388277f5 100644 --- a/tests/plugins/test_links.py +++ b/tests/plugins/test_links.py @@ -7,6 +7,11 @@ @pytest.mark.parametrize( "example_full_fuzzres_content, expected_links", [ + # getting data-href for now (b'\n', [],), + ( + b'\n', + ["http://www.wfuzz.org/1.json", "http://www.wfuzz.org/2.json"], + ), ( b'\n', ["http://www.wfuzz.org/android-chrome-manifest.json"], diff --git a/tests/plugins/test_summary.py b/tests/plugins/test_summary.py new file mode 100644 index 00000000..19ea27be --- /dev/null +++ b/tests/plugins/test_summary.py @@ -0,0 +1,36 @@ +from wfuzz.factories.plugin_factory import plugin_factory +from wfuzz.fuzzobjects import FuzzPlugin + +from queue import Queue + + +def test_sum_plugin_output(example_full_fuzzres): + plugin = plugin_factory.create("plugin_from_summary", "a message") + + assert plugin.is_visible(True) is False + assert plugin.is_visible(False) is True + + +def test_find_plugin_output_from_factory(): + plugin = plugin_factory.create( + "plugin_from_finding", + "a plugin", + "a source", + "an issue", + "some data", + FuzzPlugin.INFO, + ) + + assert plugin.is_visible(True) is True + assert plugin.is_visible(False) is False + + +def test_find_plugin_output(get_plugin): + plugin = get_plugin("links")[0] + plugin.results_queue = Queue() + plugin.add_result("a source", "an issue", "some data", FuzzPlugin.INFO) + + plugin_res = plugin.results_queue.get() + + assert plugin_res.is_visible(True) is True + assert plugin_res.is_visible(False) is False diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index a594a66b..50a0fff3 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -118,7 +118,7 @@ ), # set values ( - "test_desc_concat_number", + "test_desc_concat_number_slice", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ --slice r.c:=302 FUZZ[url]FUZZ[c]", ["http://localhost:9000/1 - 302"], @@ -178,7 +178,7 @@ ( "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]", + "-z wfuzzp,$$PREVFILE$$ --slice r.url:=r.url|replace('1','2') FUZZ[url]", ["http://localhost:9000/2"], None, ), diff --git a/wordlist/general/common.txt b/wordlist/general/common.txt index ba228a38..9644af96 100644 --- a/wordlist/general/common.txt +++ b/wordlist/general/common.txt @@ -190,6 +190,7 @@ composer compressed comunicator con +confluence config configs configuration @@ -451,6 +452,7 @@ jdbc job join jrun +jira js jsp jsps