From 32e4b0f18bac3accf820796b0e04d2b79f5394eb Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 12 Jan 2019 18:10:20 +0100 Subject: [PATCH 001/119] updte flake8 ignore list in makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ce7b2519..820caafa 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ test: tox --recreate flake8: pip install flake8 - flake8 --ignore=E501,E402,F401 src tests + flake8 --ignore=E501,E402,F401,W504 src tests publish: pip install 'twine>=1.5.0' python setup.py sdist From 121c0876ef4615bb082f3d69b86325646160db3f Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 13 Jan 2019 18:12:28 +0100 Subject: [PATCH 002/119] description value on exec time --- src/wfuzz/filter.py | 2 +- src/wfuzz/fuzzobjects.py | 73 ++++++++++++++++++++++--------------- src/wfuzz/ui/console/mvc.py | 8 ++-- tests/test_reqresp.py | 21 +++++++++++ 4 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index f70cc04e..042d0de1 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -112,7 +112,7 @@ def __compute_fuzz_symbol(self, tokens): i = tokens[0] try: - return self.res.payload[i] + return self.res.payload[i].content except IndexError: raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") except AttributeError: diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 62c8f676..abe4088b 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -476,12 +476,12 @@ class FuzzResultFactory: def replace_fuzz_word(text, fuzz_word, payload): marker_regex = re.compile(r"(%s)(?:\[(.*?)\])?" % (fuzz_word,), re.MULTILINE | re.DOTALL) - for fw, field in marker_regex.findall(text): + for fuzz_word, field in marker_regex.findall(text): if field: marker_regex = re.compile(r"(%s)(?:\[(.*?)\])?" % (fuzz_word,), re.MULTILINE | re.DOTALL) - subs_array = [] + fields_array = [] - for fw, field in marker_regex.findall(text): + 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.") @@ -490,15 +490,15 @@ def replace_fuzz_word(text, fuzz_word, payload): except AttributeError: raise FuzzExceptBadOptions("A FUZZ[field] expression must be used with a fuzzresult payload not a string.") - text = text.replace("%s[%s]" % (fw, field), subs) - subs_array.append(subs) + text = text.replace("%s[%s]" % (fuzz_word, field), subs) + fields_array.append(field) - return (text, subs_array) + return (text, fields_array) else: try: - return (text.replace(fuzz_word, payload), [payload]) + return (text.replace(fuzz_word, payload), [None]) except TypeError: - raise FuzzExceptBadOptions("Tried to replace FUZZ with a whole fuzzresult payload.") + raise FuzzExceptBadOptions("Tried to replace {} with a whole fuzzresult payload.".format(fuzz_word)) @staticmethod def from_seed(seed, payload, seed_options): @@ -509,29 +509,26 @@ def from_seed(seed, payload, seed_options): scheme = newres.history.scheme auth_method, userpass = newres.history.auth - descr_array = [] - for payload_pos, payload_content in enumerate(payload, start=1): fuzz_word = "FUZ" + str(payload_pos) + "Z" if payload_pos > 1 else "FUZZ" - newres.payload.append(payload_content) + 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 = [] - descr_array.append(newres.history.redirect_url) + fuzz_values_array.append(None) - newres.payload = [payload_content] newres.history.update_from_options(seed_options) - newres._description = "" rawReq = str(newres.history) rawUrl = newres.history.redirect_url scheme = newres.history.scheme auth_method, userpass = newres.history.auth - desc = None + desc = [] if auth_method and (userpass.count(fuzz_word)): userpass, desc = FuzzResultFactory.replace_fuzz_word(userpass, fuzz_word, payload_content) @@ -544,20 +541,18 @@ def from_seed(seed, payload, seed_options): scheme, desc = FuzzResultFactory.replace_fuzz_word(scheme, fuzz_word, payload_content) if desc: - descr_array += desc + fuzz_values_array += desc - if len(descr_array) == 0: + if len(fuzz_values_array) == 0: raise FuzzExceptBadOptions("No %s word!" % fuzz_word) + 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) - if newres._description: - newres._description += " - " - - newres._description += " - ".join(descr_array) newres.type = FuzzResult.result return newres @@ -614,9 +609,7 @@ def from_baseline(fuzzresult, options): baseline_res.history.update_from_raw_http(rawReq, scheme) baseline_res = FuzzResultFactory.from_seed(baseline_res, baseline_payload, options) - baseline_res.is_baseline = True - baseline_res.payload = baseline_payload return baseline_res @@ -629,8 +622,7 @@ def from_all_fuzz_request(seed, payload): for var_name in seed.history.wf_allvars_set.keys(): payload_content = payload[0] fuzzres = seed.from_soft_copy() - fuzzres._description = var_name + "=" + payload_content - fuzzres.payload.append(payload_content) + fuzzres.payload.append(FuzzPayload(payload_content, [None])) fuzzres.history.wf_allvars_set = {var_name: payload_content} @@ -755,6 +747,27 @@ def update(self, fuzzstats2): self.pending_seeds._operation(fuzzstats2.pending_seeds()) +class FuzzPayload(): + def __init__(self, content, fields): + self.content = content + self.fields = fields + + def description(self): + ret_str_values = [] + + for fuzz_value in self.fields: + if fuzz_value is None and isinstance(self.content, FuzzResult): + ret_str_values.append(self.content.get_field('url')) + elif fuzz_value is not None and isinstance(self.content, FuzzResult): + ret_str_values.append(self.content.get_field(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) + + class FuzzResult: seed, backfeed, result, error, startseed, endseed, cancel, discarded = list(range(8)) newid = itertools.count(0) @@ -766,7 +779,6 @@ def __init__(self, history=None, exception=None, track_id=True): self.type = None self.exception = exception - self._description = "" self.is_baseline = False self.rlevel = 1 self.nres = next(FuzzResult.newid) if track_id else 0 @@ -788,7 +800,6 @@ def update(self, exception=None): if exception: self.exception = exception - self._description = self._description + "! " + str(self.exception) if self.history and self.history.content: m = hashlib.md5() @@ -827,7 +838,11 @@ def __str__(self): @property def description(self): - return self._description + ret_str = ' - '.join([payload.description() for payload in self.payload]) + if self.exception: + return ret_str + "! " + str(self.exception) + + return ret_str # parameters in common with fuzzrequest @property @@ -865,7 +880,6 @@ def from_soft_copy(self, track_id=True): fr = FuzzResult(self.history.from_copy(), track_id=track_id) fr.exception = self.exception - fr._description = self._description fr.is_baseline = self.is_baseline fr.type = self.type fr.rlevel = self.rlevel @@ -890,7 +904,6 @@ def to_new_signal(signal): def to_new_url(self, url): fr = self.from_soft_copy() fr.history.url = str(url) - fr._description = fr.history.path fr.rlevel = self.rlevel + 1 fr.type = FuzzResult.backfeed fr.is_baseline = False diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index 8b2d2737..b4063383 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -218,12 +218,12 @@ def result(self, res): self._print(res) if res.type == FuzzResult.result: - if self.previous and len(res.payload) > 0 and isinstance(res.payload[0], FuzzResult): - sys.stdout.write("\n\r |__ ") + if self.previous and len(res.payload) > 0 and isinstance(res.payload[0].content, FuzzResult): + sys.stdout.write("\n\r |_ ") if self.verbose: - self._print_verbose(res.payload[0], print_nres=False) + self._print_verbose(res.payload[0].content, print_nres=False) else: - self._print(res.payload[0], print_nres=False) + self._print(res.payload[0].content, print_nres=False) sys.stdout.write("\n\r") for i in res.plugins_res: diff --git a/tests/test_reqresp.py b/tests/test_reqresp.py index 4bd1a7c3..5ddeec3a 100644 --- a/tests/test_reqresp.py +++ b/tests/test_reqresp.py @@ -3,6 +3,8 @@ # Python 2 and 3: urlib.parse from wfuzz.fuzzobjects import FuzzRequest +from wfuzz.fuzzobjects import FuzzResultFactory +from wfuzz.ui.console.clparser import CLParser from wfuzz import __version__ as wfuzz_version @@ -14,6 +16,25 @@ """.format(wfuzz_version) +class FuzzResultFactoryTest(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(FuzzResultFactoryTest, self).__init__(*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') + + class FuzzRequestTest(unittest.TestCase): def __init__(self, *args, **kwargs): super(FuzzRequestTest, self).__init__(*args, **kwargs) From 19064a0401c5cff464631d49b6072c4d8833865a Mon Sep 17 00:00:00 2001 From: javi Date: Mon, 14 Jan 2019 22:30:04 +0100 Subject: [PATCH 003/119] mycounter to utils --- src/wfuzz/fuzzobjects.py | 21 +-------------------- src/wfuzz/utils.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index abe4088b..177ed695 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -21,6 +21,7 @@ from .mixins import FuzzRequestUrlMixing, FuzzRequestSoupMixing from .utils import python2_3_convert_to_unicode +from .utils import MyCounter auth_header = namedtuple("auth_header", "method credentials") @@ -639,26 +640,6 @@ def from_options(options): return FuzzResult(fr) -class MyCounter: - def __init__(self, count=0): - self._count = count - self._mutex = Lock() - - def inc(self): - self._operation(1) - - def dec(self): - self._operation(-1) - - def _operation(self, dec): - with self._mutex: - self._count += dec - - def __call__(self): - with self._mutex: - return self._count - - class FuzzStats: def __init__(self): self.mutex = Lock() diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 10ff5c1b..74f4c658 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -2,6 +2,8 @@ import os import sys import six +from threading import Lock + from chardet.universaldetector import UniversalDetector @@ -151,3 +153,23 @@ def detect_encoding(file_path): 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): + self._operation(1) + + def dec(self): + self._operation(-1) + + def _operation(self, dec): + with self._mutex: + self._count += dec + + def __call__(self): + with self._mutex: + return self._count From 6e83b6a9cb2dfae5322c058096e144f9a477e43a Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 17 Jan 2019 23:16:32 +0100 Subject: [PATCH 004/119] setter for headers and cookies classes --- src/wfuzz/fuzzobjects.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 177ed695..2f33429e 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -26,7 +26,7 @@ auth_header = namedtuple("auth_header", "method credentials") -class headers: +class headers(object): def __init__(self, req): self._req = req @@ -38,9 +38,9 @@ def response(self): def request(self): return OrderedDict([x.split(": ", 1) for x in self._req.getHeaders()]) - def add(self, dd): - for k, v in list(dd.items()): - self._req._headers[k] = v + @request.setter + def request(self, dd): + self._req._headers.update(dd) def get_field(self, field): attr = field.split(".") @@ -70,7 +70,7 @@ def get_field(self, field): return ret.strip() -class cookies: +class cookies(object): def __init__(self, req): self._req = req @@ -92,6 +92,10 @@ def request(self): return {} + @request.setter + def request(self, values): + self._req._headers["Cookie"] = "; ".join(values) + def get_field(self, field): attr = field.split(".") num_fields = len(attr) @@ -190,7 +194,7 @@ def __init__(self): self.wf_fuzz_methods = None self.wf_retries = 0 - self.headers.add({"User-Agent": Facade().sett.get("connection", "user-agent")}) + self.headers.request = {"User-Agent": Facade().sett.get("connection", "user-agent")} # methods for accessing HTTP requests information consistenly accross the codebase @@ -362,7 +366,7 @@ def wf_allvars_set(self, varset): elif self.wf_allvars == "allpost": self.params.post = varset elif self.wf_allvars == "allheaders": - self._request.headers.add(varset) + self._request.headers.request = varset else: raise FuzzExceptBadOptions("Unknown variable set: " + self.wf_allvars) except TypeError: @@ -445,9 +449,9 @@ def update_from_options(self, options): self.wf_fuzz_methods = options['method'] if options['cookie']: - self.headers.add({"Cookie": "; ".join(options['cookie'])}) + self.cookies.request = options['cookie'] - self.headers.add(dict(options['headers'])) + self.headers.request = dict(options['headers']) if options['allvars']: self.wf_allvars = options['allvars'] @@ -459,7 +463,7 @@ def from_copy(self): newreq.wf_allvars = self.wf_allvars newreq.wf_fuzz_methods = self.wf_fuzz_methods - newreq.headers.add(self.headers.request) + newreq.headers.request = self.headers.request newreq.params.post = self.params.post newreq.follow = self.follow From 85a379f157ffbc298c835c944f09836686632a40 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 23 Jan 2019 23:45:16 +0100 Subject: [PATCH 005/119] update_from_raw_http missing rawbody --- src/wfuzz/fuzzobjects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 2f33429e..da9eca8e 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -406,12 +406,12 @@ def to_http_object(self, c): def from_http_object(self, c, bh, bb): return self._request.response_from_conn_object(c, bh, bb) - def update_from_raw_http(self, raw, scheme, raw_response=None): + def update_from_raw_http(self, raw, scheme, raw_response=None, raw_content=None): self._request.parseRequest(raw, scheme) if raw_response: rp = Response() - rp.parseResponse(raw_response) + rp.parseResponse(raw_response, raw_content) self._request.response = rp return self._request From 168d8cf66e2f7bff6eb40ce99fa24fab19aef315 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 24 Jan 2019 00:09:05 +0100 Subject: [PATCH 006/119] pyparsing convert numbers to int --- src/wfuzz/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 042d0de1..26be01c6 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -25,7 +25,7 @@ class FuzzResFilter: def __init__(self, ffilter=None, filter_string=None): if PYPARSING: quoted_str_value = QuotedString('\'', unquoteResults=True, escChar='\\') - int_values = Word("0123456789") + 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 + "." + "_" + "-") From 075c7d88ea0aabc707612cda42b39e476b7ab97c Mon Sep 17 00:00:00 2001 From: javi Date: Fri, 25 Jan 2019 11:07:41 +0100 Subject: [PATCH 007/119] improved instropection and setting fields in filter language --- docs/user/advanced.rst | 60 ++++---- src/wfuzz/filter.py | 39 +++-- src/wfuzz/fuzzobjects.py | 181 +++++------------------- src/wfuzz/mixins.py | 8 ++ src/wfuzz/plugins/payloads/autorize.py | 3 +- src/wfuzz/plugins/payloads/burplog.py | 3 +- src/wfuzz/plugins/payloads/burpstate.py | 4 +- src/wfuzz/plugins/payloads/wfuzzp.py | 3 +- src/wfuzz/utils.py | 137 ++++++++++++++++++ tests/test_acceptance.py | 4 +- tests/test_dotdict.py | 17 +++ tests/test_filterintro.py | 156 ++++++++++++++++++++ 12 files changed, 419 insertions(+), 196 deletions(-) create mode 100644 tests/test_dotdict.py create mode 100644 tests/test_filterintro.py diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 8a77924d..a66eafd5 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -415,16 +415,18 @@ value|startswith('value') value|sw('param') Returns true if the val ============ ============== ============================================= Name Short version Description ============ ============== ============================================= +url Wfuzz's result HTTP request url description Wfuzz's result description nres Wfuzz's result identifier -code c HTTP response's code +code c Wfuzz's result HTTP response's code chars h Wfuzz's result HTTP response chars 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 ============ ============== ============================================= -Or FuzzRequest object's attribute such as: +FuzzRequest object's attribute (you need to use the r. prefix) such as: ============================ ============================================= Name Description @@ -435,41 +437,43 @@ scheme HTTP request's scheme host HTTP request's host content HTTP response's content raw_content HTTP response's content including headers -cookies.request HTTP request cookie -cookies.response HTTP response cookie -cookies.request.<> HTTP request cookie -cookies.response.<> HTTP response cookie -headers.request All HTTP request headers -headers.response All HTTP response headers -headers.request.<> HTTP request given header -headers.response.<> HTTP response given header -params All HTTP request GET and POST parameters +cookies.all All HTTP request and response cookies +cookies.request HTTP requests cookieS +cookies.response HTTP response cookies +cookies.request.<> Specified HTTP request cookie +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 +params.all All HTTP request GET and POST parameters params.get All HTTP request GET parameters params.post All HTTP request POST parameters -params.get/post.<> A given HTTP request GET/POST parameter +params.get.<> Spcified HTTP request GET parameter +params.post.<> Spcified HTTP request POST parameter +pstrip Returns a signature of the HTTP request using the parameter's names without values (useful for unique operations) +is_path Returns true when the HTTP request path refers to a directory. ============================ ============================================= -URL field is broken in smaller parts using the urlparse Python's module, which parses a URL into: scheme://netloc/path;parameters?query#fragment. +FuzzRequest URL field is broken in smaller (read only) parts using the urlparse Python's module in the urlp attribute. -For example, for the "http://www.google.com/dir/test.php?id=1" URL you can get the following values: +Urlparse parses a URL into: scheme://netloc/path;parameters?query#fragment. For example, for the "http://www.google.com/dir/test.php?id=1" URL you can get the following values: =================== ============================================= Name Value =================== ============================================= -url.scheme http -url.netloc www.google.com -url.path /dir/test.php -url.params -url.query id=1 -url.fragment -url.domain google.com -url.ffname test.php -url.fext .php -url.fname test -url.pstrip Returns a hash of the request using the parameter's names without values (useful for unique operations) -url.hasquery Returns true when the URL contains a query string. -url.ispath Returns true when the URL path refers to a directory. -url.isbllist Returns true when the URL file extension is included in the configuration discovery's blacklist +urlp.scheme http +urlp.netloc www.google.com +urlp.path /dir/test.php +urlp.params +urlp.query id=1 +urlp.fragment +urlp.ffname test.php +urlp.fext .php +urlp.fname test +urlp.hasquery Returns true when the URL contains a query string. +urlp.isbllist Returns true when the URL file extension is included in the configuration discovery's blacklist =================== ============================================= Payload instrospection can also be performed by using the keyword FUZZ: diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 26be01c6..511993f4 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -1,8 +1,11 @@ from .exception import FuzzExceptIncorrectFilter, FuzzExceptBadOptions, FuzzExceptInternalError, FuzzException from .fuzzobjects import FuzzResult +from .utils import rgetattr, rsetattr + import re import collections +import operator # Python 2 and 3: alternative 4 try: @@ -49,7 +52,7 @@ def __init__(self, ffilter=None, filter_string=None): operator = oneOf("and or") not_operator = Optional(oneOf("not"), "notpresent") - symbol_expr = Group(fuzz_statement + oneOf("= != < > >= <= =~ !~ ~") + (bbb_value ^ error_value ^ fuzz_statement ^ basic_primitives)).setParseAction(self.__compute_expr) + symbol_expr = Group(fuzz_statement + oneOf("= == != < > >= <= =~ !~ ~ := =+ =-") + (bbb_value ^ error_value ^ fuzz_statement ^ basic_primitives)).setParseAction(self.__compute_expr) definition = fuzz_statement ^ symbol_expr definition_not = not_operator + definition @@ -106,7 +109,7 @@ def set_baseline(self, res): def __compute_res_value(self, tokens): self.stack["field"] = tokens[0] - return self.res.get_field(self.stack["field"]) + return rgetattr(self.res, self.stack["field"]) def __compute_fuzz_symbol(self, tokens): i = tokens[0] @@ -127,7 +130,7 @@ def __compute_fuzz_value(self, tokens): self.stack["field"] = field try: - return fuzz_val.get_field(field) if field else fuzz_val + return rgetattr(fuzz_val, self.stack["field"]) if field else fuzz_val except IndexError: raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") except AttributeError as e: @@ -190,33 +193,43 @@ def __compute_xxx_value(self, tokens): return FuzzResult.ERROR_CODE def __compute_expr(self, tokens): - leftvalue, operator, rightvalue = tokens[0] + leftvalue, exp_operator, rightvalue = tokens[0] + + field_to_set = self.stack.get('field', None) try: - if operator == "=": + if exp_operator in ["=", '==']: return leftvalue == rightvalue - elif operator == "<=": + elif exp_operator == "<=": return leftvalue <= rightvalue - elif operator == ">=": + elif exp_operator == ">=": return leftvalue >= rightvalue - elif operator == "<": + elif exp_operator == "<": return leftvalue < rightvalue - elif operator == ">": + elif exp_operator == ">": return leftvalue > rightvalue - elif operator == "!=": + elif exp_operator == "!=": return leftvalue != rightvalue - elif operator == "=~": + elif exp_operator == "=~": regex = re.compile(rightvalue, re.MULTILINE | re.DOTALL) return regex.search(leftvalue) is not None - elif operator == "!~": + elif exp_operator == "!~": return rightvalue.lower() not in leftvalue.lower() - elif operator == "~": + elif exp_operator == "~": return rightvalue.lower() in leftvalue.lower() + 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 TypeError as e: raise FuzzExceptBadOptions("Invalid regex expression used in filter: %s" % str(e)) except ParseException as e: raise FuzzExceptBadOptions("Invalid regex expression used in filter: %s" % str(e)) + return True + def __myreduce(self, elements): first = elements[0] for i in range(1, len(elements), 2): diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index da9eca8e..cefefd0e 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -22,6 +22,8 @@ from .utils import python2_3_convert_to_unicode from .utils import MyCounter +from .utils import rgetattr +from .utils import DotDict auth_header = namedtuple("auth_header", "method credentials") @@ -32,42 +34,19 @@ def __init__(self, req): @property def response(self): - return OrderedDict(self._req.response.getHeaders()) if self._req.response else {} + return DotDict(OrderedDict(self._req.response.getHeaders())) if self._req.response else {} @property def request(self): - return OrderedDict([x.split(": ", 1) for x in self._req.getHeaders()]) + return DotDict(OrderedDict([x.split(": ", 1) for x in self._req.getHeaders()])) @request.setter - def request(self, dd): - self._req._headers.update(dd) - - def get_field(self, field): - attr = field.split(".") - num_fields = len(attr) - - if num_fields == 2: - if attr[1] == "request": - return ", ".join(["%s:%s" % (x[0], x[1]) for x in list(self.request.items())]) - elif attr[1] == "response": - return ", ".join(["%s:%s" % (x[0], x[1]) for x in list(self.response.items())]) - else: - raise FuzzExceptBadAPI("headers must be specified in the form of headers.[request|response].
") - elif num_fields != 3: - raise FuzzExceptBadAPI("headers must be specified in the form of headers.[request|response].
") - - ret = "" - try: - if attr[1] == "request": - ret = self.request[attr[2]] - elif attr[1] == "response": - ret = self.response[attr[2]] - else: - raise FuzzExceptBadAPI("headers must be specified in the form of headers.[request|response].
") - except KeyError: - pass + def request(self, values_dict): + self._req._headers.update(values_dict) - return ret.strip() + @property + def all(self): + return DotDict({**self.request, **self.response}) class cookies(object): @@ -79,7 +58,7 @@ def response(self): if self._req.response: c = self._req.response.getCookie().split("; ") if c[0]: - return OrderedDict([[x[0], x[2]] for x in [x.partition("=") for x in c]]) + return DotDict(OrderedDict([[x[0], x[2]] for x in [x.partition("=") for x in c]])) return {} @@ -88,7 +67,7 @@ def request(self): if 'Cookie' in self._req._headers: c = self._req._headers['Cookie'].split("; ") if c[0]: - return OrderedDict([[x[0], x[2]] for x in [x.partition("=") for x in c]]) + return DotDict(OrderedDict([[x[0], x[2]] for x in [x.partition("=") for x in c]])) return {} @@ -96,34 +75,9 @@ def request(self): def request(self, values): self._req._headers["Cookie"] = "; ".join(values) - def get_field(self, field): - attr = field.split(".") - num_fields = len(attr) - - if num_fields == 2: - - if attr[1] == "response": - if self._req.response: - return self._req.response.getCookie() - elif attr[1] == "request": - return self._req['COOKIE'] - else: - raise FuzzExceptBadAPI("Cookie must be specified in the form of cookies.[request|response]") - elif num_fields == 3: - try: - if attr[1] == "request": - return self.request[attr[2]] - elif attr[1] == "response": - return self.response[attr[2]] - else: - raise FuzzExceptBadAPI("headers must be specified in the form of headers.[request|response].
") - except KeyError: - return "" - - else: - raise FuzzExceptBadAPI("Cookie must be specified in the form of cookies.[request|response].<>") - - return "" + @property + def all(self): + return DotDict({**self.request, **self.response}) class params(object): @@ -132,7 +86,7 @@ def __init__(self, req): @property def get(self): - return OrderedDict([(x.name, x.value) for x in self._req.getGETVars()]) + return DotDict(OrderedDict([(x.name, x.value) for x in self._req.getGETVars()])) @get.setter def get(self, values): @@ -144,7 +98,7 @@ def get(self, values): @property def post(self): - return OrderedDict([(x.name, x.value) for x in self._req.getPOSTVars()]) + return DotDict(OrderedDict([(x.name, x.value) for x in self._req.getPOSTVars()])) @post.setter def post(self, pp): @@ -154,35 +108,14 @@ def post(self, pp): elif isinstance(pp, str): self._req.setPostData(pp) - def get_field(self, field): - attr = field.split(".") - num_fields = len(attr) - - if num_fields == 1 and attr[0] == "params": - pp = ", ".join(["%s:%s" % (x[0], x[1]) for x in list(dict(list(self.get.items()) + list(self.post.items())).items())]) - return "" if not pp else pp - elif num_fields == 2: - if attr[1] == "get": - return ", ".join(["%s=%s" % (x[0], x[1]) for x in list(self.get.items())]) - elif attr[1] == "post": - return ", ".join(["%s=%s" % (x[0], x[1]) for x in list(self.post.items())]) - else: - raise FuzzExceptBadAPI("Parameters must be specified as params.[get/post].") - elif num_fields == 3: - ret = "" - try: - if attr[1] == "get": - ret = self.get[attr[2]] - elif attr[1] == "post": - ret = self.post[attr[2]] - else: - raise FuzzExceptBadAPI("Parameters must be specified as params.[get/post].") - except KeyError: - pass - - return ret - else: - raise FuzzExceptBadAPI("Parameters must be specified as params.[get/post].") + @property + def all(self): + return DotDict({**self.get, **self.post}) + + @all.setter + def all(self, values): + self.get = values + self.post = values class FuzzRequest(FuzzRequestUrlMixing, FuzzRequestSoupMixing): @@ -278,7 +211,7 @@ def code(self): @code.setter def code(self, c): - self._request.response.code = c + self._request.response.code = int(c) @property def auth(self): @@ -306,46 +239,6 @@ def reqtime(self): def reqtime(self, t): self._request.totaltime = t - def set_field(self, field, value): - if field in ["url"]: - self.url = value - - def get_field(self, field): - alias = dict([('c', 'code')]) - - if field in alias: - field = alias[field] - - if field in ["url", "method", "scheme", "host", "content", "raw_content", "code"]: - return getattr(self, field) - elif field in ["code"]: - return str(getattr(self, field)) - elif field.startswith("cookies"): - return self.cookies.get_field(field).strip() - elif field.startswith("headers"): - return self.headers.get_field(field) - elif field.startswith("params"): - return self.params.get_field(field) - elif field.startswith("url."): - attr = field.split(".") - allowed_attr = ["scheme", "netloc", "path", "params", "query", "fragment", "ffname", "fext", "fname", "isbllist", "hasquery"] - - if len(attr) != 2: - raise FuzzExceptBadAPI("Url must be specified as url.") - - if attr[1] in allowed_attr: - return getattr(self.urlparse, attr[1]) - elif attr[1] == "pstrip": - return self.to_cache_key() - elif attr[1] == "ispath": - return self.is_path - else: - raise FuzzExceptBadAPI("Unknown url attribute. It must be one of %s" % ",".join(allowed_attr)) - - return "" - else: - raise FuzzExceptBadAPI("Unknown FuzzResult attribute: %s." % (field,)) - # Info extra that wfuzz needs within an HTTP request @property def wf_allvars_set(self): @@ -491,7 +384,7 @@ def replace_fuzz_word(text, fuzz_word, payload): 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 = payload.get_field(field) + subs = str(rgetattr(payload, field)) except AttributeError: raise FuzzExceptBadOptions("A FUZZ[field] expression must be used with a fuzzresult payload not a string.") @@ -742,9 +635,9 @@ def description(self): for fuzz_value in self.fields: if fuzz_value is None and isinstance(self.content, FuzzResult): - ret_str_values.append(self.content.get_field('url')) + ret_str_values.append(rgetattr(self.content, 'url')) elif fuzz_value is not None and isinstance(self.content, FuzzResult): - ret_str_values.append(self.content.get_field(fuzz_value)) + ret_str_values.append(rgetattr(self.content, fuzz_value)) elif fuzz_value is None: ret_str_values.append(self.content) else: @@ -797,20 +690,6 @@ def update(self, exception=None): return self - def set_field(self, field, value): - return self.history.set_field(field, value) - - def get_field(self, field): - alias = dict([('l', 'lines'), ('h', 'chars'), ('w', 'words'), ('c', 'code')]) - - if field in alias: - field = alias[field] - - if field in ["code", "description", "nres", "chars", "lines", "words", "md5"]: - return str(getattr(self, field)) - else: - return self.history.get_field(field) - 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) @@ -830,6 +709,10 @@ def description(self): return ret_str # parameters in common with fuzzrequest + @property + def content(self): + return self.history.content if self.history else "" + @property def url(self): return self.history.url if self.history else "" diff --git a/src/wfuzz/mixins.py b/src/wfuzz/mixins.py index c1ca51c1..7fe3c79f 100644 --- a/src/wfuzz/mixins.py +++ b/src/wfuzz/mixins.py @@ -27,6 +27,14 @@ class FuzzRequestUrlMixing(object): def urlparse(self): return parse_url(self.url) + @property + def urlp(self): + return parse_url(self.url) + + @property + def pstrip(self): + return self.to_cache_key() + @property def is_path(self): if self.code == 200 and self.url[-1] == '/': diff --git a/src/wfuzz/plugins/payloads/autorize.py b/src/wfuzz/plugins/payloads/autorize.py index 93a9de16..1560ab72 100644 --- a/src/wfuzz/plugins/payloads/autorize.py +++ b/src/wfuzz/plugins/payloads/autorize.py @@ -6,6 +6,7 @@ from wfuzz.fuzzobjects import FuzzRequest from wfuzz.plugin_api.base import BasePayload from wfuzz.externals.moduleman.plugin import moduleman_plugin +from wfuzz.utils import rgetattr @moduleman_plugin @@ -41,7 +42,7 @@ def count(self): def __next__(self): next_item = next(self._it) - return next_item if not self.attr else next_item.get_field(self.attr) + return next_item if not self.attr else rgetattr(next_item, self.attr) def _gen_wfuzz(self, output_fn): try: diff --git a/src/wfuzz/plugins/payloads/burplog.py b/src/wfuzz/plugins/payloads/burplog.py index 9115840f..d26f2c0d 100644 --- a/src/wfuzz/plugins/payloads/burplog.py +++ b/src/wfuzz/plugins/payloads/burplog.py @@ -2,6 +2,7 @@ from wfuzz.exception import FuzzExceptBadFile from wfuzz.fuzzobjects import FuzzResult, FuzzRequest from wfuzz.plugin_api.base import BasePayload +from wfuzz.utils import rgetattr import re @@ -44,7 +45,7 @@ def count(self): def __next__(self): next_item = next(self._it) - return next_item if not self.attr else next_item.get_field(self.attr) + return next_item if not self.attr else rgetattr(next_item, self.attr) def parse_burp_log(self, burp_log): burp_file = None diff --git a/src/wfuzz/plugins/payloads/burpstate.py b/src/wfuzz/plugins/payloads/burpstate.py index 2f3a1946..5f3259dc 100644 --- a/src/wfuzz/plugins/payloads/burpstate.py +++ b/src/wfuzz/plugins/payloads/burpstate.py @@ -2,6 +2,8 @@ from wfuzz.exception import FuzzExceptBadFile, FuzzExceptBadOptions from wfuzz.fuzzobjects import FuzzResult, FuzzRequest from wfuzz.plugin_api.base import BasePayload +from wfuzz.utils import rgetattr + import datetime import string @@ -69,7 +71,7 @@ def count(self): def __next__(self): next_item = next(self._it) - return next_item if not self.attr else next_item.get_field(self.attr) + 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: diff --git a/src/wfuzz/plugins/payloads/wfuzzp.py b/src/wfuzz/plugins/payloads/wfuzzp.py index bac1a598..21a1e6e5 100644 --- a/src/wfuzz/plugins/payloads/wfuzzp.py +++ b/src/wfuzz/plugins/payloads/wfuzzp.py @@ -5,6 +5,7 @@ from wfuzz.exception import FuzzExceptBadFile from wfuzz.fuzzobjects import FuzzResult from wfuzz.plugin_api.base import BasePayload +from wfuzz.utils import rgetattr @moduleman_plugin @@ -45,7 +46,7 @@ def count(self): def __next__(self): next_item = next(self._it) - return next_item if not self.attr else next_item.get_field(self.attr) + return next_item if not self.attr else rgetattr(next_item, self.attr) def _gen_wfuzz(self, output_fn): try: diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 74f4c658..71467bdb 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -3,8 +3,10 @@ import sys import six from threading import Lock +import functools from chardet.universaldetector import UniversalDetector +from .exception import FuzzExceptIncorrectFilter def json_minify(string, strip_space=True): @@ -173,3 +175,138 @@ def _operation(self, dec): def __call__(self): with self._mutex: return self._count + + +def _check_allowed_field(attr): + allowed_fields = [ + "description", + "nres", + "code", + "chars", + "lines", + "words", + "md5", + 'l', + 'h', + 'w', + 'c', + 'r', + 'history', + + '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.url", + "r.method", + "r.scheme", + "r.host", + "r.content", + "r.raw_content" + "r.is_path", + "r.pstrip", + "r.cookies", + "r.headers", + "r.params", + ] + + 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 FuzzExceptIncorrectFilter("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 = {pre_post: new_val} + else: + val = new_val + + return setattr(obj_to_set, post, val) + except AttributeError: + raise FuzzExceptIncorrectFilter("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 FuzzExceptIncorrectFilter("rgetattr: Can't get '{}' attribute from '{}'.".format(attr, obj.__class__)) + + if not _check_allowed_field(attr): + raise FuzzExceptIncorrectFilter("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])) + + val = dict.get(*args, None) + 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()}) + elif isinstance(other, DotDict): + self.update(other) + return self + + def __radd__(self, other): + if isinstance(other, str): + return DotDict({k: other + v for k, v in self.items()}) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index a1410bbf..4dd25820 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -52,7 +52,7 @@ ("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="headers.response.Location='%C3%A3%C2%81%C2%AF%C3%A5%C2%9B%C2%BD'"), [(302, '/redirect-to')], 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), @@ -78,7 +78,7 @@ ("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 method='POST'"), [(200, '/anything')], None), + ("test_empty_postdata", "%s/FUZZ" % HTTPBIN_URL, [["anything"]], dict(postdata='', filter="content~'POST' and r.method='POST'"), [(200, '/anything')], 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), diff --git a/tests/test_dotdict.py b/tests/test_dotdict.py new file mode 100644 index 00000000..e752f61c --- /dev/null +++ b/tests/test_dotdict.py @@ -0,0 +1,17 @@ +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 new file mode 100644 index 00000000..1b919495 --- /dev/null +++ b/tests/test_filterintro.py @@ -0,0 +1,156 @@ +import unittest + +# Python 2 and 3: urlib.parse + +from wfuzz.fuzzobjects import FuzzRequest +from wfuzz.fuzzobjects import FuzzResult +from wfuzz.filter import FuzzResFilter + + +raw_req = """GET / HTTP/1.1 +Host: www.wfuzz.org +User-Agent: curl/7.58.0 +Accept: */* +""" + +raw_resp = b"""HTTP/1.1 302 Found +Content-Type: text/html; charset=utf-8 +Content-Language: en +Location: https://wfuzz.readthedocs.io/en/latest/ +Vary: Accept-Language, Cookie +Server: nginx/1.14.0 (Ubuntu) +X-Fallback: True +X-Served: Django +X-Deity: web01 +Date: Wed, 23 Jan 2019 21:43:59 GMT +Content-Length: 0 +""" + + +class FilterTest(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(FilterTest, self).__init__(*args, **kwargs) + self.maxDiff = 1000 + + def test_code_set(self): + fr = FuzzRequest() + fr.update_from_raw_http(raw_req, "http", raw_resp, b"") + + fuzz_res = FuzzResult(history=fr) + + ffilter = FuzzResFilter(filter_string="r.code:=429") + ffilter.is_visible(fuzz_res) + self.assertEqual(fuzz_res.code, 429) + + ffilter = FuzzResFilter(filter_string="r.c:=404") + ffilter.is_visible(fuzz_res) + self.assertEqual(fuzz_res.code, 404) + + ffilter = FuzzResFilter(filter_string="r.c=+404") + ffilter.is_visible(fuzz_res) + self.assertEqual(fuzz_res.code, 808) + + ffilter = FuzzResFilter(filter_string="r.c=-404") + ffilter.is_visible(fuzz_res) + self.assertEqual(fuzz_res.code, 1212) + + 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" + + fuzz_res = FuzzResult(history=fr) + + with self.assertRaises(Exception) as context: + ffilter = FuzzResFilter(filter_string="url=-'test'") + ffilter.is_visible(fuzz_res) + self.assertTrue("rsetattr: Can't set" in str(context.exception)) + + with self.assertRaises(Exception) as context: + ffilter = FuzzResFilter(filter_string="notthere=-'test'") + ffilter.is_visible(fuzz_res) + self.assertTrue("rgetattr: Can't get" in str(context.exception)) + + with self.assertRaises(Exception) as context: + 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(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"}) + + 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") From 8786a273fe0546be4c7ce83514bc17f79bb4d29d Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 26 Jan 2019 23:37:26 +0100 Subject: [PATCH 008/119] python2 compatibility issues and dotdict assignment --- src/wfuzz/filter.py | 6 +++--- src/wfuzz/fuzzobjects.py | 6 +++--- src/wfuzz/utils.py | 12 +++++++----- tests/test_filterintro.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 511993f4..f89d19ed 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -44,15 +44,15 @@ def __init__(self, ffilter=None, filter_string=None): 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 = (Word(alphas + "." + "_" + "-").setParseAction(self.__compute_res_value) + Optional(operator_call, None)).setParseAction(self.__compute_perl_value) + res_value_op = (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 = fuzz_value ^ fuzz_value_op ^ fuzz_value_op2 ^ res_value_op ^ basic_primitives_op + 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 ^ fuzz_statement ^ basic_primitives)).setParseAction(self.__compute_expr) + symbol_expr = Group(fuzz_statement + oneOf("= == != < > >= <= =~ !~ ~ := =+ =-") + (bbb_value ^ error_value ^ basic_primitives ^ fuzz_statement)).setParseAction(self.__compute_expr) definition = fuzz_statement ^ symbol_expr definition_not = not_operator + definition diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index cefefd0e..cab2a740 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -46,7 +46,7 @@ def request(self, values_dict): @property def all(self): - return DotDict({**self.request, **self.response}) + return self.request + self.response class cookies(object): @@ -77,7 +77,7 @@ def request(self, values): @property def all(self): - return DotDict({**self.request, **self.response}) + return self.request + self.response class params(object): @@ -110,7 +110,7 @@ def post(self, pp): @property def all(self): - return DotDict({**self.get, **self.post}) + return self.get + self.post @all.setter def all(self, values): diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 71467bdb..c931e331 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -257,7 +257,6 @@ def rsetattr(obj, attr, new_val, operation): 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}) @@ -265,7 +264,7 @@ def rsetattr(obj, attr, new_val, operation): val = operation(prev_val, new_val) else: if isinstance(prev_val, DotDict): - val = {pre_post: new_val} + val = {k: new_val for k, v in prev_val.items()} else: val = new_val @@ -296,7 +295,8 @@ def __getattr__(*args): if args[1] not in args[0]: raise KeyError("DotDict: Non-existing field {}".format(args[1])) - val = dict.get(*args, None) + # 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}) @@ -304,8 +304,10 @@ def __add__(self, other): if isinstance(other, str): return DotDict({k: v + other for k, v in self.items()}) elif isinstance(other, DotDict): - self.update(other) - return self + # 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): diff --git a/tests/test_filterintro.py b/tests/test_filterintro.py index 1b919495..d12692e6 100644 --- a/tests/test_filterintro.py +++ b/tests/test_filterintro.py @@ -114,6 +114,19 @@ def test_params_set(self): 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" @@ -154,3 +167,23 @@ def test_ispath(self): 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)) From c5ef1aa2b5c086def401e33a065ca1750ff3c076 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 00:18:54 +0100 Subject: [PATCH 009/119] --filter-language --- docs/user/advanced.rst | 16 ++++- src/wfuzz/ui/console/clparser.py | 113 ++++++++++++++++++++++++++++++- src/wfuzz/ui/console/common.py | 1 + 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index a66eafd5..f8ff063a 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -364,6 +364,10 @@ Filter Language Wfuzz's filter language grammar is build using `pyparsing `_, therefore it must be installed before using the command line parameters "--filter, --prefilter, --slice". +The information about the filter language can be also obtained executing:: + + wfuzz --filter-help + A filter expression must be built using the following symbols and operators: * Boolean Operators @@ -372,7 +376,7 @@ A filter expression must be built using the following symbols and operators: * Expression Operators -Expressions operators such as "= != < > >= <=" could be used to check values. Additionally, the following for matching text are available: +Expressions operators such as "= != < > >= <=" could be used to check values. Additionally, the following operators for matching text are available: ============ ==================================================================== Operator Description @@ -382,6 +386,16 @@ Operator Description !~ Equivalent to Python's "str2" not in "str1" (case insensitive) ============ ==================================================================== +Also, assignment operators: + +============ ==================================================================== +Operator Description +============ ==================================================================== +:= Assigns a value +=+ Concatenates value at the left +=- Concatenates value at the right +============ ==================================================================== + Where values could be: * Basic primitives: diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 4016c4c8..0f71919d 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -30,6 +30,113 @@ def show_usage(self): print(help_banner) print(usage) + def show_filter_usage(self): + print(""" + * Operators: and or not = != < > >= <= =~ !~ ~ := =+ =- + + * Basic primitives: + + ============ ==================== + Long Name Description + ============ ==================== + 'string' Quoted string + 0..9+ Integer values + XXX HTTP request error code + BBB Baseline + ============ ==================== + + * Values can also be modified using the following operators: + + ================================ ======================= ============================================= + Name Short version Description + ================================ ======================= ============================================= + value|unquote() value|un() Unquotes the value + value|lower() value|l() lowercase of the value + value|upper() uppercase of the value + value|encode('encoder', 'value') value|e('enc', 'val') Returns encoder.encode(value) + value|decode('decoder', 'value') value|d('dec', 'val') Returns encoder.decode(value) + value|replace('what', 'with') value|r('what', 'with') Returns value replacing what for with + value|unique(value) value|u(value) Returns True if a value is unique. + value|startswith('value') value|sw('param') Returns true if the value string starts with param + ================================ ======================= ============================================= + + * When a FuzzResult is available, you could perform runtime introspection of the objects using the following symbols + + ============ ============== ============================================= + Name Short version Description + ============ ============== ============================================= + url Wfuzz's result HTTP request url + description Wfuzz's result description + nres Wfuzz's result identifier + code c Wfuzz's result HTTP response's code + chars h Wfuzz's result HTTP response chars + 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 + ============ ============== ============================================= + + FuzzRequest object's attribute (you need to use the r. prefix) such as: + + ============================ ============================================= + Name Description + ============================ ============================================= + url HTTP request's value + method HTTP request's verb + scheme HTTP request's scheme + host HTTP request's host + content HTTP response's content + raw_content HTTP response's content including headers + cookies.all All HTTP request and response cookies + cookies.request HTTP requests cookieS + cookies.response HTTP response cookies + cookies.request.<> Specified HTTP request cookie + 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 + params.all All HTTP request GET and POST parameters + params.get All HTTP request GET parameters + params.post All HTTP request POST parameters + params.get.<> Spcified HTTP request GET parameter + params.post.<> Spcified HTTP request POST parameter + pstrip Returns a signature of the HTTP request using the parameter's names without values (useful for unique operations) + is_path Returns true when the HTTP request path refers to a directory. + ============================ ============================================= + + FuzzRequest URL field is broken in smaller (read only) parts using the urlparse Python's module in the urlp attribute. + + Urlparse parses a URL into: scheme://netloc/path;parameters?query#fragment. For example, for the "http://www.google.com/dir/test.php?id=1" URL you can get the following values: + + =================== ============================================= + Name Value + =================== ============================================= + urlp.scheme http + urlp.netloc www.google.com + urlp.path /dir/test.php + urlp.params + urlp.query id=1 + urlp.fragment + urlp.ffname test.php + urlp.fext .php + urlp.fname test + urlp.hasquery Returns true when the URL contains a query string. + urlp.isbllist Returns true when the URL file extension is included in the configuration discovery's blacklist + =================== ============================================= + + Payload instrospection can also be performed by using the keyword FUZZ: + + ============ ============================================== + Name Description + ============ ============================================== + FUZnZ Allows to access the Nth payload string + FUZnZ[field] Allows to access the Nth payload attributes + ============ ============================================== + """) + sys.exit(0) + 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)]) @@ -54,7 +161,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:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) + opts, args = getopt.getopt(self.argv[1:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) optsd = defaultdict(list) payload_cache = {} @@ -148,6 +255,10 @@ def _parse_help_opt(self, optsd): self.show_verbose_usage() sys.exit(0) + if "--filter-help" in optsd: + self.show_filter_usage() + sys.exit(0) + # Extensions help if "--script-help" in optsd: script_string = optsd["--script-help"][0] diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index ccab41e2..37c1f33d 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -80,6 +80,7 @@ verbose_usage = '''%s\n\nOptions: \t-h/--help : This help \t--help : Advanced help +\t--filter-help : Filter language specification \t--version : Wfuzz version details \t-e : List of available encoders/payloads/iterators/printers/scripts \t From f27515bd702caa4de320f461bb7fe36001ecd259 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 00:28:12 +0100 Subject: [PATCH 010/119] trailing space --- src/wfuzz/ui/console/clparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 0f71919d..ea1dae45 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -118,7 +118,7 @@ def show_filter_usage(self): urlp.path /dir/test.php urlp.params urlp.query id=1 - urlp.fragment + urlp.fragment urlp.ffname test.php urlp.fext .php urlp.fname test From 3142e8ab516dc64e57460fab8a467066e8b78469 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 00:29:13 +0100 Subject: [PATCH 011/119] bump version --- src/wfuzz/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/__init__.py b/src/wfuzz/__init__.py index 63ec622a..1f9af334 100644 --- a/src/wfuzz/__init__.py +++ b/src/wfuzz/__init__.py @@ -1,5 +1,5 @@ __title__ = 'wfuzz' -__version__ = "2.3.4" +__version__ = "2.4" __build__ = 0x023000 __author__ = 'Xavier Mendez' __license__ = 'GPL 2.0' From 48fc6c03579e180e62841486a5724d4eed77bfc8 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 00:35:12 +0100 Subject: [PATCH 012/119] add python 3.7 to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ac295fec..e3b71733 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - "3.4" - "3.5" - "3.6" + - "3.7" before_install: - docker-compose -f tests/server_dir/docker-compose.yml up -d install: From 31d27ee3b9cbbca66dbab67064fe5f004c471e68 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 00:37:47 +0100 Subject: [PATCH 013/119] remove python 3.7 from travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e3b71733..ac295fec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ python: - "3.4" - "3.5" - "3.6" - - "3.7" before_install: - docker-compose -f tests/server_dir/docker-compose.yml up -d install: From 32b76c315a33439af376237aab2ec8ed6e824a69 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 21:27:01 +0100 Subject: [PATCH 014/119] tidy up test filter code --- tests/test_filterintro.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/test_filterintro.py b/tests/test_filterintro.py index d12692e6..c9df4e3a 100644 --- a/tests/test_filterintro.py +++ b/tests/test_filterintro.py @@ -32,27 +32,22 @@ def __init__(self, *args, **kwargs): super(FilterTest, self).__init__(*args, **kwargs) self.maxDiff = 1000 - def test_code_set(self): + def get_filtered_fuzzrequest(self, filter_str): fr = FuzzRequest() fr.update_from_raw_http(raw_req, "http", raw_resp, b"") fuzz_res = FuzzResult(history=fr) - ffilter = FuzzResFilter(filter_string="r.code:=429") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.code, 429) - - ffilter = FuzzResFilter(filter_string="r.c:=404") + ffilter = FuzzResFilter(filter_string=filter_str) ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.code, 404) - ffilter = FuzzResFilter(filter_string="r.c=+404") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.code, 808) + return fuzz_res - ffilter = FuzzResFilter(filter_string="r.c=-404") - ffilter.is_visible(fuzz_res) - self.assertEqual(fuzz_res.code, 1212) + 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() From 1248b837172e0c9bca9c45530fd85800b7a48412 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 21:39:53 +0100 Subject: [PATCH 015/119] update coverage command --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0ad95c72..4089d33d 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ commands = coverage erase deps = coverage [testenv:end] -commands = coverage report --skip-covered --include '*python3.5/site-packages/wfuzz*' -m +commands = coverage report --skip-covered --include '*python3.6/site-packages/wfuzz*' -m deps = coverage [testenv:codecov] From 152fc379478053c938d187f7e3ee6c8a7dc43b83 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 21:40:29 +0100 Subject: [PATCH 016/119] remove __main__ from test clparser --- tests/test_clparser.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_clparser.py b/tests/test_clparser.py index 81c8c3fa..a477f3a9 100644 --- a/tests/test_clparser.py +++ b/tests/test_clparser.py @@ -9,7 +9,3 @@ def test_listplugins(self): CLParser(['wfuzz', '-e', 'iterators']).parse_cl() self.assertEqual(cm.exception.code, 0) - - -if __name__ == '__main__': - unittest.main() From 19da4a9c31d992eb699fa8d5ce0b348e52a72be8 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 27 Jan 2019 23:07:59 +0100 Subject: [PATCH 017/119] url as default description --- src/wfuzz/fuzzobjects.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index cab2a740..7ad64122 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -630,14 +630,13 @@ def __init__(self, content, fields): self.content = content self.fields = fields - def description(self): + 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(rgetattr(self.content, 'url')) + ret_str_values.append(default) elif fuzz_value is not None and isinstance(self.content, FuzzResult): - ret_str_values.append(rgetattr(self.content, fuzz_value)) + ret_str_values.append(str(rgetattr(self.content, fuzz_value))) elif fuzz_value is None: ret_str_values.append(self.content) else: @@ -702,7 +701,7 @@ def __str__(self): @property def description(self): - ret_str = ' - '.join([payload.description() for payload in self.payload]) + ret_str = ' - '.join([payload.description(self.url) for payload in self.payload]) if self.exception: return ret_str + "! " + str(self.exception) From a59f40e79057dbd2f281f3ee80e6a952cc10f7d2 Mon Sep 17 00:00:00 2001 From: javi Date: Mon, 28 Jan 2019 20:38:17 +0100 Subject: [PATCH 018/119] add payload to plugin item --- src/wfuzz/fuzzobjects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 7ad64122..4550b11e 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -822,5 +822,6 @@ 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 From 8d220dcf2a84040daa00624c046c0e9f5a26ef33 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 29 Jan 2019 00:52:46 +0100 Subject: [PATCH 019/119] seed_payload option compiled in options --- src/wfuzz/options.py | 4 +++- src/wfuzz/ui/console/clparser.py | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 8affad48..5c92f158 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -44,7 +44,6 @@ def __init__(self, **kwargs): def _defaults(self): return dict( - seed_payload=False, send_discarded=False, console_printer="", hs=None, @@ -90,6 +89,7 @@ def _defaults(self): dictio=None, # these will be compiled + seed_payload=False, filter="", prefilter="", compiled_genreq=None, @@ -226,6 +226,8 @@ def compile(self): if error: raise FuzzExceptBadOptions(error) + self.data["seed_payload"] = True if self.data["url"] == "FUZZ" else False + # printer try: filename, printer = self.data["printer"] diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index ea1dae45..40565b94 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -200,9 +200,6 @@ def parse_cl(self): cli_url = optsd["-u"][0] - if url == "FUZZ" or cli_url == "FUZZ": - options["seed_payload"] = True - if cli_url: url = cli_url From fada98db55379968afd145a800f3e5c83631fe1d Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 5 Feb 2019 00:16:35 +0100 Subject: [PATCH 020/119] add __str__ to fuzzpayload --- src/wfuzz/fuzzobjects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 4550b11e..e03aa324 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -644,6 +644,9 @@ def description(self, default): return " - ".join(ret_str_values) + def __str__(self): + return "content: {} fields: {}".format(self.content, self.fields) + class FuzzResult: seed, backfeed, result, error, startseed, endseed, cancel, discarded = list(range(8)) From 80d92ecfe81fe51c40addb6ddbc04701d7ef3617 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 5 Feb 2019 00:42:26 +0100 Subject: [PATCH 021/119] sub filter for slice --- src/wfuzz/core.py | 4 ++-- src/wfuzz/filter.py | 30 ++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index 85ef74c3..88658b65 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -7,7 +7,7 @@ from .facade import Facade from .exception import FuzzExceptBadOptions, FuzzExceptNoPluginError -from .filter import FuzzResFilter +from .filter import FuzzResFilterSlice import re @@ -24,7 +24,7 @@ class sliceit(object): def __init__(self, payload, slicestr): - self.ffilter = FuzzResFilter(filter_string=slicestr) + self.ffilter = FuzzResFilterSlice(filter_string=slicestr) self.payload = payload def __iter__(self): diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index f89d19ed..6556c3b6 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -37,7 +37,7 @@ def __init__(self, ffilter=None, filter_string=None): operator_names = oneOf("m d e un u r l sw 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) + 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) @@ -111,18 +111,18 @@ def __compute_res_value(self, tokens): return rgetattr(self.res, self.stack["field"]) - def __compute_fuzz_symbol(self, tokens): + 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.") - except AttributeError: - if i == 0: - return self.res - else: + if isinstance(self.res, FuzzResult): + try: + return self.res.payload[i].content + except IndexError: raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") + elif isinstance(self.res, str) and i == 0: + return self.res + else: + raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") def __compute_fuzz_value(self, tokens): fuzz_val, field = tokens @@ -320,3 +320,13 @@ def from_options(filter_options): ffilter.hideparams['chars'] = filter_options["hh"] return ffilter + + +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 From 2533bfec1bda8b6e12f3059be6e845e57ca2cf3f Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 5 Feb 2019 00:43:05 +0100 Subject: [PATCH 022/119] acceptance tests for slice savedres --- tests/test_acceptance.py | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 4dd25820..bb7c936f 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -38,6 +38,30 @@ # conn delays? # script args +testing_savedsession_tests = [ +] + +savedsession_tests = [ + # set values + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_fuzz", "FUZZ", dict(), None, ["http://localhost:9000/1"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_attr", "FUZZ[url]", dict(), None, ["http://localhost:9000/1"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_concat_number", "FUZZ[url]FUZZ[c]", dict(), None, ["http://localhost:9000/1 - 404"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_url_number", "FUZZ[c]", dict(), None, ["http://localhost:9000/1 - 404"], "Pycurl error 7:"), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_concat_number", "FUZZ[url]FUZZ[c]", dict(), "r.c:=302", ["http://localhost:9000/1 - 302"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_rewrite_url", "FUZZ", dict(prefilter="r.url:=r.url|replace('1','2')"), None, ["http://localhost:9000/2"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_rewrite_url2", "FUZZ[url]", dict(), "r.url:=r.url|replace('1','2')", ["http://localhost:9000/2"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_assign_fuzz_symbol_op", "FUZZ[url]", dict(), "FUZZ[r.url]:=FUZZ[r.url|replace('1','2')]", ["http://localhost:9000/2"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_concat_fuzz_symbol_op", "FUZZ", dict(), "FUZZ[r.url]=+'2'", ["http://localhost:9000/12"], None), + + # fuzz value slice filters + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_symbol_code", "FUZZ", dict(), "FUZZ[c]=404", ["http://localhost:9000/1"], None), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_value_code", "FUZZ", dict(), "c=404", ["http://localhost:9000/1"], None), + + # fuzz value exceptions + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_symbol_code", "FUZZ", dict(), "FUZ1Z[c]=404", ["http://localhost:9000/1"], "Unknown field"), + ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_symbol_code", "FUZZ", dict(), "FUZ2Z[c]=404", ["http://localhost:9000/1"], "Non existent FUZZ payload"), +] + testing_tests = [ ] @@ -119,6 +143,7 @@ # 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), # follow ("test_follow", "%s:8000/FUZZ" % LOCAL_DOMAIN, [["redirect"]], dict(follow=True, filter="content~'path=/echo'"), [(200, '/echo')], None), @@ -304,6 +329,28 @@ def test(self): return test +def wfuzz_me_test_generator_previous_session(prev_session_cli, url, params, slicestr, expected_list): + def test(self): + temp_name = next(tempfile._get_candidate_names()) + defult_tmp_dir = tempfile._get_default_tempdir() + + filename = os.path.join(defult_tmp_dir, temp_name) + + # first session + with wfuzz.get_session(prev_session_cli) as s: + ret_list = [x.description for x in s.fuzz(save=filename)] + + # second session wfuzzp as payload + with wfuzz.FuzzSession(url=url, **params) as s: + fuzzed = s.fuzz(payloads=[("wfuzzp", dict(fn=filename), slicestr)]) + + ret_list = [x.description for x in fuzzed] + + self.assertEqual(sorted(ret_list), sorted(expected_list)) + + return test + + 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: @@ -358,11 +405,29 @@ def duplicate_tests(test_list, group, test_gen_fun): setattr(DynamicTests, new_test, test_fn) +def create_savedsession_tests(test_list, test_gen_fun): + """ + generates wfuzz tests that run 2 times with recipe input, expecting same results. + + """ + for prev_cli, test_name, url, params, slicestr, expected_res, exception_str in test_list: + test_fn = test_gen_fun(prev_cli, url, params, slicestr, expected_res) + if exception_str: + test_fn_exc = wfuzz_me_test_generator_exception(test_fn, exception_str) + setattr(DynamicTests, test_name, test_fn_exc) + else: + setattr(DynamicTests, test_name, test_fn) + + def create_tests(): """ Creates all dynamic tests """ + if testing_savedsession_tests: + create_savedsession_tests(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) @@ -375,6 +440,9 @@ def create_tests(): for t in basic_functioning_tests: create_tests_from_list(t) + # description tests + 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) From 221f673afc68f95d0fa7db2dc617da6e9cc84caf Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 5 Feb 2019 23:51:00 +0100 Subject: [PATCH 023/119] fix over-indented lines --- src/wfuzz/core.py | 250 +++---- src/wfuzz/externals/reqresp/Request.py | 784 +++++++++++----------- src/wfuzz/externals/reqresp/Response.py | 338 +++++----- src/wfuzz/externals/reqresp/TextParser.py | 248 +++---- src/wfuzz/externals/reqresp/Variables.py | 220 +++--- src/wfuzz/filter.py | 2 +- src/wfuzz/fuzzobjects.py | 16 +- src/wfuzz/options.py | 4 +- src/wfuzz/plugins/encoders/encoders.py | 4 +- src/wfuzz/plugins/payloads/burpstate.py | 136 ++-- src/wfuzz/plugins/payloads/names.py | 8 +- src/wfuzz/plugins/printers/printers.py | 4 +- src/wfuzz/ui/console/mvc.py | 2 +- src/wfuzz/utils.py | 8 +- tests/test_acceptance.py | 1 + tests/test_api.py | 4 - tests/test_moduleman.py | 30 +- 17 files changed, 1026 insertions(+), 1033 deletions(-) diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index 88658b65..6c11d171 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -56,164 +56,164 @@ def __iter__(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 __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 + def count(self): + return (self.__payload.count() * len(self.__encoders)) if self.__encoders else self.__payload.count() - 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)") + def __iter__(self): + return self - for e in plugin_list: - yield e().encode(payload_list) + 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) + 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.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 - - def restart(self, seed): - self.seed = seed - self.dictio = self.get_dictio() + def __init__(self, options): + self.options = options + self.seed = FuzzResultFactory.from_options(options) + self.baseline = FuzzResultFactory.from_baseline(self.seed, options) + self.dictio = self.get_dictio() - def _check_dictio_len(self, element): - 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 + self.stats = FuzzStats.from_requestGenerator(self) - fuzz_words += marker_regex.findall(self.seed.history.scheme) + self._allvar_gen = None + if self.seed.history.wf_allvars is not None: + self._allvar_gen = self.__allvars_gen(self.dictio) - if method: - fuzz_words += marker_regex.findall(userpass) + def stop(self): + self.stats.cancelled = True - if self.options["seed_payload"]: - fuzz_words += ["FUZZ"] + def restart(self, seed): + self.seed = seed + self.dictio = self.get_dictio() - if len(element) != len(set(fuzz_words)): - raise FuzzExceptBadOptions("FUZZ words and number of payloads do not match!") + def _check_dictio_len(self, element): + 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 - def count(self): - v = self.dictio.count() - if self.seed.history.wf_allvars is not None: - v *= len(self.seed.history.wf_allvars_set) + fuzz_words += marker_regex.findall(self.seed.history.scheme) - if self.baseline: - v += 1 + if method: + fuzz_words += marker_regex.findall(userpass) - return v + if self.options["seed_payload"]: + fuzz_words += ["FUZZ"] - def __iter__(self): - return self + if len(element) != len(set(fuzz_words)): + raise FuzzExceptBadOptions("FUZZ words and number of payloads do not match!") - 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.") + 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 len(self.seed.history.wf_allvars_set) == 0: - raise FuzzExceptBadOptions("No variables on specified variable set: " + self.seed.history.wf_allvars) + if self.baseline: + v += 1 - for payload in dic: - for r in FuzzResultFactory.from_all_fuzz_request(self.seed, payload): - yield r + return v - def __next__(self): - if self.stats.cancelled: - raise StopIteration + def __iter__(self): + return self - if self.baseline and self.stats.processed() == 0 and self.stats.pending_seeds() <= 1: - return self.baseline + 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 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) + if len(self.seed.history.wf_allvars_set) == 0: + raise FuzzExceptBadOptions("No variables on specified variable set: " + self.seed.history.wf_allvars) - return FuzzResultFactory.from_seed(self.seed, n, self.options) + for payload in dic: + for r in FuzzResultFactory.from_all_fuzz_request(self.seed, payload): + yield r - def get_dictio(self): - class wrapper(object): - def __init__(self, iterator): - self._it = iter(iterator) + def __next__(self): + if self.stats.cancelled: + raise StopIteration - def __iter__(self): - return self + if self.baseline and self.stats.processed() == 0 and self.stats.pending_seeds() <= 1: + return self.baseline - def count(self): - return -1 + 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) - def __next__(self): - return str(next(self._it)) + return FuzzResultFactory.from_seed(self.seed, n, self.options) - selected_dic = [] + def get_dictio(self): + class wrapper(object): + def __init__(self, iterator): + self._it = iter(iterator) - 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}), ... ]") + def __iter__(self): + return self - if not params: - raise FuzzExceptBadOptions("You must supply a list of payloads in the form of [(name, {params}), ... ]") + def count(self): + return -1 - p = Facade().payloads.get_plugin(name)(params) - pp = dictionary(p, params["encoder"]) if "encoder" in params else p - selected_dic.append(sliceit(pp, slicestr) if slicestr else pp) + def __next__(self): + return str(next(self._it)) - if not selected_dic: - raise FuzzExceptBadOptions("Empty dictionary! Check payload and filter") + selected_dic = [] - 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) + 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) + 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): diff --git a/src/wfuzz/externals/reqresp/Request.py b/src/wfuzz/externals/reqresp/Request.py index 81bd9cd3..342716ab 100644 --- a/src/wfuzz/externals/reqresp/Request.py +++ b/src/wfuzz/externals/reqresp/Request.py @@ -30,427 +30,427 @@ 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 - - # #### Variables calculadas por getters NO SE PUEDEN MODIFICAR - # self.urlWithoutPath # http://www.google.es - # self.pathWithVariables # /index.php?a=b&c=d - # self.urlWithoutVariables=None # http://www.google.es/index.php - # self.completeUrl="" # http://www.google.es/index.php?a=b - # self.finalUrl="" # Url despues de hacer el FollowLocation - # self.redirectUrl="" # Url redirected - # self.postdata="" # Datos por POST, toto el string - # ############### - - self.ContentType = "application/x-www-form-urlencoded" # None es normal encoding - self.multiPOSThead = {} - - self.__variablesGET = VariablesSet() - self.__variablesPOST = VariablesSet() - - # 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.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._method = None - self.protocol = "HTTP/1.1" # HTTP/1.1 - self.__performHead = "" - self.__performBody = "" - - self.__authMethod = None - self.__userpass = "" - - self.description = "" # For temporally store imformation - - self.__proxy = None - self.proxytype = None - self.__timeout = None - self.__totaltimeout = None - self.__finalurl = "" - - self.followLocation = False - self.__userpass = "" - - self.totaltime = None - - @property - def method(self): - if self._method is None: - return "POST" if self.getPOSTVars() else "GET" - - return self._method - - @method.setter - def method(self, value): - if value == "None": - value = None - - self._method = value - - def setFinalUrl(self, fu): - self.__finalurl = fu - - def __str__(self): - str = "[ URL: %s" % (self.completeUrl) - if self.postdata: - str += " - {}: \"{}\"".format(self.method, self.postdata) - if "Cookie" in self._headers: - str += " - COOKIE: \"%s\"" % self._headers["Cookie"] - str += " ]" - return str - - def getHost(self): - return self.__host - - def getXML(self, obj): - r = obj.createElement("request") - r.setAttribute("method", self.method) - url = obj.createElement("URL") - url.appendChild(obj.createTextNode(self.completeUrl)) - r.appendChild(url) - if self.postdata: - pd = obj.createElement("PostData") - pd.appendChild(obj.createTextNode(self.postdata)) - r.appendChild(pd) - if "Cookie" in self._headers: - ck = obj.createElement("Cookie") - ck.appendChild(obj.createTextNode(self._headers["Cookie"])) - r.appendChild(ck) - - return r - - def __getattr__(self, name): - if name == "urlWithoutVariables": - return urlunparse((self.schema, self.__host, self.__path, '', '', '')) - elif name == "pathWithVariables": - return urlunparse(('', '', self.__path, '', self.__variablesGET.urlEncoded(), '')) - elif name == "completeUrl": - return urlunparse((self.schema, self.__host, self.__path, self.__params, self.__variablesGET.urlEncoded(), '')) - elif name == "finalUrl": - if self.__finalurl: - return self.__finalurl - return self.completeUrl - elif name == "urlWithoutPath": - return "%s://%s" % (self.schema, self._headers["Host"]) - elif name == "path": - return self.__path - elif name == "postdata": - if self.ContentType == "application/x-www-form-urlencoded": - return self.__variablesPOST.urlEncoded() - elif self.ContentType == "multipart/form-data": - return self.__variablesPOST.multipartEncoded() - else: - return self.__uknPostData - else: - raise AttributeError - - def setUrl(self, urltmp): - self.__variablesGET = VariablesSet() - 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) + 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 + + # #### Variables calculadas por getters NO SE PUEDEN MODIFICAR + # self.urlWithoutPath # http://www.google.es + # self.pathWithVariables # /index.php?a=b&c=d + # self.urlWithoutVariables=None # http://www.google.es/index.php + # self.completeUrl="" # http://www.google.es/index.php?a=b + # self.finalUrl="" # Url despues de hacer el FollowLocation + # self.redirectUrl="" # Url redirected + # self.postdata="" # Datos por POST, toto el string + # ############### + + self.ContentType = "application/x-www-form-urlencoded" # None es normal encoding + self.multiPOSThead = {} + + self.__variablesGET = VariablesSet() + self.__variablesPOST = VariablesSet() + + # 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.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._method = None + self.protocol = "HTTP/1.1" # HTTP/1.1 + self.__performHead = "" + self.__performBody = "" + + self.__authMethod = None + self.__userpass = "" + + self.description = "" # For temporally store imformation + + self.__proxy = None + self.proxytype = None + self.__timeout = None + self.__totaltimeout = None + self.__finalurl = "" + + self.followLocation = False + self.__userpass = "" + + self.totaltime = None + + @property + def method(self): + if self._method is None: + return "POST" if self.getPOSTVars() else "GET" + + return self._method + + @method.setter + def method(self, value): + if value == "None": + value = None + + self._method = value + + def setFinalUrl(self, fu): + self.__finalurl = fu + + def __str__(self): + str = "[ URL: %s" % (self.completeUrl) + if self.postdata: + str += " - {}: \"{}\"".format(self.method, self.postdata) + if "Cookie" in self._headers: + str += " - COOKIE: \"%s\"" % self._headers["Cookie"] + str += " ]" + return str + + def getHost(self): + return self.__host + + def getXML(self, obj): + r = obj.createElement("request") + r.setAttribute("method", self.method) + url = obj.createElement("URL") + url.appendChild(obj.createTextNode(self.completeUrl)) + r.appendChild(url) + if self.postdata: + pd = obj.createElement("PostData") + pd.appendChild(obj.createTextNode(self.postdata)) + r.appendChild(pd) + if "Cookie" in self._headers: + ck = obj.createElement("Cookie") + ck.appendChild(obj.createTextNode(self._headers["Cookie"])) + r.appendChild(ck) + + return r + + def __getattr__(self, name): + if name == "urlWithoutVariables": + return urlunparse((self.schema, self.__host, self.__path, '', '', '')) + elif name == "pathWithVariables": + return urlunparse(('', '', self.__path, '', self.__variablesGET.urlEncoded(), '')) + elif name == "completeUrl": + return urlunparse((self.schema, self.__host, self.__path, self.__params, self.__variablesGET.urlEncoded(), '')) + elif name == "finalUrl": + if self.__finalurl: + return self.__finalurl + return self.completeUrl + elif name == "urlWithoutPath": + return "%s://%s" % (self.schema, self._headers["Host"]) + elif name == "path": + return self.__path + elif name == "postdata": + if self.ContentType == "application/x-www-form-urlencoded": + return self.__variablesPOST.urlEncoded() + elif self.ContentType == "multipart/form-data": + return self.__variablesPOST.multipartEncoded() + else: + return self.__uknPostData + else: + raise AttributeError + + def setUrl(self, urltmp): + self.__variablesGET = VariablesSet() + 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 ################################## - def getProxy(self): - return self.__proxy + def getProxy(self): + return self.__proxy - def setProxy(self, prox, ptype): - self.__proxy = prox - self.proxytype = ptype + def setProxy(self, prox, ptype): + self.__proxy = prox + self.proxytype = ptype # ############## FOLLOW LOCATION ######################## - def setFollowLocation(self, value): - self.followLocation = value + def setFollowLocation(self, value): + self.followLocation = value # ############# TIMEOUTS ################################ - def setConnTimeout(self, time): - self.__timeout = time + def setConnTimeout(self, time): + self.__timeout = time - def getConnTimeout(self): - return self.__timeout + def getConnTimeout(self): + return self.__timeout - def setTotalTimeout(self, time): - self.__totaltimeout = time + def setTotalTimeout(self, time): + self.__totaltimeout = time - def getTotalTimeout(self): - return self.__totaltimeout + def getTotalTimeout(self): + return self.__totaltimeout # ############# Autenticacion ########################### - def setAuth(self, method, string): - self.__authMethod = method - self.__userpass = string + def setAuth(self, method, string): + self.__authMethod = method + self.__userpass = string - def getAuth(self): - return self.__authMethod, self.__userpass + def getAuth(self): + return self.__authMethod, self.__userpass # ############# TRATAMIENTO VARIABLES GET & POST ######################### - def existsGETVar(self, key): - return self.__variablesGET.existsVar(key) + def existsGETVar(self, key): + return self.__variablesGET.existsVar(key) - def existPOSTVar(self, key): - return self.__variablesPOST.existsVar(key) + def existPOSTVar(self, key): + return self.__variablesPOST.existsVar(key) - def setVariablePOST(self, key, value): - v = self.__variablesPOST.getVariable(key) - v.update(value) -# self._headers["Content-Length"] = str(len(self.postdata)) + def setVariablePOST(self, key, value): + v = self.__variablesPOST.getVariable(key) + v.update(value) +# self._headers["Content-Length"] = str(len(self.postdata)) - def setVariableGET(self, key, value): - v = self.__variablesGET.getVariable(key) - v.update(value) + def setVariableGET(self, key, value): + v = self.__variablesGET.getVariable(key) + v.update(value) - def getGETVars(self): - return self.__variablesGET.variables + def getGETVars(self): + return self.__variablesGET.variables - def getPOSTVars(self): - return self.__variablesPOST.variables + def getPOSTVars(self): + return self.__variablesPOST.variables - def setPostData(self, pd, boundary=None): - self.__variablesPOST = VariablesSet() - if self.ContentType == "application/x-www-form-urlencoded": - self.__variablesPOST.parseUrlEncoded(pd) - elif self.ContentType == "multipart/form-data": - self.__variablesPOST.parseMultipart(pd, boundary) - else: - self.__uknPostData = pd + def setPostData(self, pd, boundary=None): + self.__variablesPOST = VariablesSet() + if self.ContentType == "application/x-www-form-urlencoded": + self.__variablesPOST.parseUrlEncoded(pd) + elif self.ContentType == "multipart/form-data": + self.__variablesPOST.parseMultipart(pd, boundary) + else: + self.__uknPostData = pd ############################################################################ - def addHeader(self, key, value): - k = string.capwords(key, "-") - self._headers[k] = value - - def delHeader(self, key): - k = string.capwords(key, "-") - if k in self._headers: - del self._headers[k] - - def __getitem__(self, key): - k = string.capwords(key, "-") - if k in self._headers: - return self._headers[k] - else: - return "" - - def getHeaders(self): - header_list = [] - for i, j in self._headers.items(): - header_list += ["%s: %s" % (i, j)] - return header_list - - def head(self): - conn = pycurl.Curl() - conn.setopt(pycurl.SSL_VERIFYPEER, False) - conn.setopt(pycurl.SSL_VERIFYHOST, 0) - conn.setopt(pycurl.URL, self.completeUrl) - - conn.setopt(pycurl.NOBODY, True) # para hacer un pedido HEAD - - conn.setopt(pycurl.WRITEFUNCTION, self.header_callback) - conn.perform() - - rp = Response() - rp.parseResponse(self.__performHead) - self.response = rp - - def createPath(self, newpath): - '''Creates new url from a location header || Hecho para el followLocation=true''' - if "http" in newpath[:4].lower(): - return newpath - - parts = urlparse(self.completeUrl) - if "/" != newpath[0]: - newpath = "/".join(parts[2].split("/")[:-1]) + "/" + newpath - - return urlunparse([parts[0], parts[1], newpath, '', '', '']) - - # pycurl - reqresp conversions - @staticmethod - def to_pycurl_object(c, req): - - c.setopt(pycurl.MAXREDIRS, 5) - - c.setopt(pycurl.WRITEFUNCTION, req.body_callback) - c.setopt(pycurl.HEADERFUNCTION, req.header_callback) + def addHeader(self, key, value): + k = string.capwords(key, "-") + self._headers[k] = value + + def delHeader(self, key): + k = string.capwords(key, "-") + if k in self._headers: + del self._headers[k] - c.setopt(pycurl.NOSIGNAL, 1) - c.setopt(pycurl.SSL_VERIFYPEER, False) - c.setopt(pycurl.SSL_VERIFYHOST, 0) + def __getitem__(self, key): + k = string.capwords(key, "-") + if k in self._headers: + return self._headers[k] + else: + return "" + + def getHeaders(self): + header_list = [] + for i, j in self._headers.items(): + header_list += ["%s: %s" % (i, j)] + return header_list + + def head(self): + conn = pycurl.Curl() + conn.setopt(pycurl.SSL_VERIFYPEER, False) + conn.setopt(pycurl.SSL_VERIFYHOST, 0) + conn.setopt(pycurl.URL, self.completeUrl) + + conn.setopt(pycurl.NOBODY, True) # para hacer un pedido HEAD - if PYCURL_PATH_AS_IS: - c.setopt(pycurl.PATH_AS_IS, 1) - - c.setopt(pycurl.URL, python2_3_convert_to_unicode(req.completeUrl)) - - if req.getConnTimeout(): - c.setopt(pycurl.CONNECTTIMEOUT, req.getConnTimeout()) - - if req.getTotalTimeout(): - c.setopt(pycurl.TIMEOUT, req.getTotalTimeout()) - - authMethod, userpass = req.getAuth() - if authMethod or userpass: - if authMethod == "basic": - c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) - elif authMethod == "ntlm": - c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_NTLM) - elif authMethod == "digest": - c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST) - c.setopt(pycurl.USERPWD, python2_3_convert_to_unicode(userpass)) + conn.setopt(pycurl.WRITEFUNCTION, self.header_callback) + conn.perform() + + rp = Response() + rp.parseResponse(self.__performHead) + self.response = rp + + def createPath(self, newpath): + '''Creates new url from a location header || Hecho para el followLocation=true''' + if "http" in newpath[:4].lower(): + return newpath + + parts = urlparse(self.completeUrl) + if "/" != newpath[0]: + newpath = "/".join(parts[2].split("/")[:-1]) + "/" + newpath + + return urlunparse([parts[0], parts[1], newpath, '', '', '']) + + # pycurl - reqresp conversions + @staticmethod + def to_pycurl_object(c, req): + + c.setopt(pycurl.MAXREDIRS, 5) + + c.setopt(pycurl.WRITEFUNCTION, req.body_callback) + c.setopt(pycurl.HEADERFUNCTION, req.header_callback) + + c.setopt(pycurl.NOSIGNAL, 1) + c.setopt(pycurl.SSL_VERIFYPEER, False) + c.setopt(pycurl.SSL_VERIFYHOST, 0) + + if PYCURL_PATH_AS_IS: + c.setopt(pycurl.PATH_AS_IS, 1) + + c.setopt(pycurl.URL, python2_3_convert_to_unicode(req.completeUrl)) + + if req.getConnTimeout(): + c.setopt(pycurl.CONNECTTIMEOUT, req.getConnTimeout()) + + if req.getTotalTimeout(): + c.setopt(pycurl.TIMEOUT, req.getTotalTimeout()) + + authMethod, userpass = req.getAuth() + if authMethod or userpass: + if authMethod == "basic": + c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) + elif authMethod == "ntlm": + c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_NTLM) + elif authMethod == "digest": + c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST) + c.setopt(pycurl.USERPWD, python2_3_convert_to_unicode(userpass)) + else: + c.unsetopt(pycurl.USERPWD) + + c.setopt(pycurl.HTTPHEADER, python2_3_convert_to_unicode(req.getHeaders())) + + curl_options = { + "GET": pycurl.HTTPGET, + "POST": pycurl.POST, + "PATCH": pycurl.UPLOAD, + "HEAD": pycurl.NOBODY, + } + + for o in curl_options.values(): + c.setopt(o, False) + + if req.method in curl_options: + c.unsetopt(pycurl.CUSTOMREQUEST) + c.setopt(curl_options[req.method], True) + else: + c.setopt(pycurl.CUSTOMREQUEST, req.method) + + if req.getPOSTVars(): + c.setopt(pycurl.POSTFIELDS, python2_3_convert_to_unicode(req.postdata)) + + c.setopt(pycurl.FOLLOWLOCATION, 1 if req.followLocation else 0) + + proxy = req.getProxy() + if proxy is not None: + c.setopt(pycurl.PROXY, python2_3_convert_to_unicode(proxy)) + if req.proxytype == "SOCKS5": + c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5) + elif req.proxytype == "SOCKS4": + c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS4) + req.delHeader("Proxy-Connection") + else: + c.setopt(pycurl.PROXY, "") + + return c + + def response_from_conn_object(self, conn, header, body): + # followlocation + if conn.getinfo(pycurl.EFFECTIVE_URL) != self.completeUrl: + self.setFinalUrl(conn.getinfo(pycurl.EFFECTIVE_URL)) + + self.totaltime = conn.getinfo(pycurl.TOTAL_TIME) + + rp = Response() + rp.parseResponse(header, rawbody=body) + + if self.schema == "https" and self.__proxy: + self.response = Response() + self.response.parseResponse(rp.getContent()) + else: + self.response = rp + + return rp + + def perform(self): + self.__performHead = "" + self.__performBody = "" + self.__headersSent = "" + + try: + conn = Request.to_pycurl_object(pycurl.Curl(), self) + conn.perform() + self.response_from_conn_object(conn, self.__performHead, self.__performBody) + except pycurl.error as error: + errno, errstr = error + raise ReqRespException(ReqRespException.FATAL, errstr) + finally: + conn.close() + + # ######## ESTE conjunto de funciones no es necesario para el uso habitual de la clase + + def getAll(self): + pd = self.postdata + 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 + + return string + + # ######################################################################### + + def header_callback(self, data): + self.__performHead += data + + def body_callback(self, data): + self.__performBody += data + + def Substitute(self, src, dst): + a = self.getAll() + rx = re.compile(src) + b = rx.sub(dst, a) + del rx + self.parseRequest(b, self.schema) + + def parseRequest(self, rawRequest, prot="http"): + ''' Aun esta en fase BETA y por probar''' + tp = TextParser() + tp.setSource("string", rawRequest) + + self.__variablesPOST = VariablesSet() + self._headers = {} # diccionario, por ejemplo headers["Cookie"] + + tp.readLine() + try: + tp.search(r"^(\w+) (.*) (HTTP\S*)$") + self.method = tp[0][0] + self.protocol = tp[0][2] + except Exception as a: + print(rawRequest) + raise a + + pathTMP = tp[0][1].replace(" ", "%20") + pathTMP = ('', '') + urlparse(pathTMP)[2:] + pathTMP = urlunparse(pathTMP) + + while True: + tp.readLine() + if (tp.search("^([^:]+): (.*)$")): + self.addHeader(tp[0][0], tp[0][1]) else: - c.unsetopt(pycurl.USERPWD) + break - c.setopt(pycurl.HTTPHEADER, python2_3_convert_to_unicode(req.getHeaders())) - - curl_options = { - "GET": pycurl.HTTPGET, - "POST": pycurl.POST, - "PATCH": pycurl.UPLOAD, - "HEAD": pycurl.NOBODY, - } - - for o in curl_options.values(): - c.setopt(o, False) - - if req.method in curl_options: - c.unsetopt(pycurl.CUSTOMREQUEST) - c.setopt(curl_options[req.method], True) - else: - c.setopt(pycurl.CUSTOMREQUEST, req.method) + self.setUrl(prot + "://" + self._headers["Host"] + pathTMP) - if req.getPOSTVars(): - c.setopt(pycurl.POSTFIELDS, python2_3_convert_to_unicode(req.postdata)) + pd = "" + while tp.readLine(): + pd += tp.lastFull_line - c.setopt(pycurl.FOLLOWLOCATION, 1 if req.followLocation else 0) + if pd: + boundary = None + if "Content-Type" in self._headers: + values = self._headers["Content-Type"].split(";") + self.ContentType = values[0].strip().lower() + if self.ContentType == "multipart/form-data": + boundary = values[1].split("=")[1].strip() - proxy = req.getProxy() - if proxy is not None: - c.setopt(pycurl.PROXY, python2_3_convert_to_unicode(proxy)) - if req.proxytype == "SOCKS5": - c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5) - elif req.proxytype == "SOCKS4": - c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS4) - req.delHeader("Proxy-Connection") - else: - c.setopt(pycurl.PROXY, "") - - return c - - def response_from_conn_object(self, conn, header, body): - # followlocation - if conn.getinfo(pycurl.EFFECTIVE_URL) != self.completeUrl: - self.setFinalUrl(conn.getinfo(pycurl.EFFECTIVE_URL)) - - self.totaltime = conn.getinfo(pycurl.TOTAL_TIME) - - rp = Response() - rp.parseResponse(header, rawbody=body) - - if self.schema == "https" and self.__proxy: - self.response = Response() - self.response.parseResponse(rp.getContent()) - else: - self.response = rp - - return rp - - def perform(self): - self.__performHead = "" - self.__performBody = "" - self.__headersSent = "" - - try: - conn = Request.to_pycurl_object(pycurl.Curl(), self) - conn.perform() - self.response_from_conn_object(conn, self.__performHead, self.__performBody) - except pycurl.error as error: - errno, errstr = error - raise ReqRespException(ReqRespException.FATAL, errstr) - finally: - conn.close() - - # ######## ESTE conjunto de funciones no es necesario para el uso habitual de la clase - - def getAll(self): - pd = self.postdata - 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 - - return string - - # ######################################################################### - - def header_callback(self, data): - self.__performHead += data - - def body_callback(self, data): - self.__performBody += data - - def Substitute(self, src, dst): - a = self.getAll() - rx = re.compile(src) - b = rx.sub(dst, a) - del rx - self.parseRequest(b, self.schema) - - def parseRequest(self, rawRequest, prot="http"): - ''' Aun esta en fase BETA y por probar''' - tp = TextParser() - tp.setSource("string", rawRequest) - - self.__variablesPOST = VariablesSet() - self._headers = {} # diccionario, por ejemplo headers["Cookie"] - - tp.readLine() - try: - tp.search(r"^(\w+) (.*) (HTTP\S*)$") - self.method = tp[0][0] - self.protocol = tp[0][2] - except Exception as a: - print(rawRequest) - raise a - - pathTMP = tp[0][1].replace(" ", "%20") - pathTMP = ('', '') + urlparse(pathTMP)[2:] - pathTMP = urlunparse(pathTMP) - - while True: - tp.readLine() - if (tp.search("^([^:]+): (.*)$")): - self.addHeader(tp[0][0], tp[0][1]) - else: - break - - self.setUrl(prot + "://" + self._headers["Host"] + pathTMP) - - pd = "" - while tp.readLine(): - pd += tp.lastFull_line - - if pd: - boundary = None - if "Content-Type" in self._headers: - values = self._headers["Content-Type"].split(";") - self.ContentType = values[0].strip().lower() - if self.ContentType == "multipart/form-data": - boundary = values[1].split("=")[1].strip() - - self.setPostData(pd, boundary) + self.setPostData(pd, boundary) diff --git a/src/wfuzz/externals/reqresp/Response.py b/src/wfuzz/externals/reqresp/Response.py index f1392442..b5d1c012 100644 --- a/src/wfuzz/externals/reqresp/Response.py +++ b/src/wfuzz/externals/reqresp/Response.py @@ -53,174 +53,174 @@ def get_encodings_from_content(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 - - def addHeader(self, key, value): - k = string.capwords(key, "-") - self._headers += [(k, value)] - - def delHeader(self, key): - for i in self._headers: - if i[0].lower() == key.lower(): - self._headers.remove(i) - - def addContent(self, text): - self.__content = self.__content + text - - def __getitem__(self, key): - for i, j in self._headers: - if key == i: - return j - print("Error al obtener header!!!") - - def getCookie(self): - str = [] - for i, j in self._headers: - if i.lower() == "set-cookie": - str.append(j.split(";")[0]) - return "; ".join(str) - - def has_header(self, key): - for i, j in self._headers: - if i.lower() == key.lower(): - return True - return False - - def getLocation(self): - for i, j in self._headers: - if i.lower() == "location": - return j - return None - - def header_equal(self, header, value): - for i, j in self._headers: - if i == header and j.lower() == value.lower(): - return True - return False - - def getHeaders(self): - return self._headers - - def getContent(self): - return self.__content - - def getTextHeaders(self): - 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 - - def getAll(self): - string = self.getTextHeaders() + "\r\n" + self.getContent() - return string - - def Substitute(self, src, dst): - a = self.getAll() - b = a.replace(src, dst) - self.parseResponse(b) - - def getAll_wpost(self): - 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 - - def parseResponse(self, rawheader, rawbody=None, type="curl"): - self.__content = "" + 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 + + def addHeader(self, key, value): + k = string.capwords(key, "-") + self._headers += [(k, value)] + + def delHeader(self, key): + for i in self._headers: + if i[0].lower() == key.lower(): + self._headers.remove(i) + + def addContent(self, text): + self.__content = self.__content + text + + def __getitem__(self, key): + for i, j in self._headers: + if key == i: + return j + print("Error al obtener header!!!") + + def getCookie(self): + str = [] + for i, j in self._headers: + if i.lower() == "set-cookie": + str.append(j.split(";")[0]) + return "; ".join(str) + + def has_header(self, key): + for i, j in self._headers: + if i.lower() == key.lower(): + return True + return False + + def getLocation(self): + for i, j in self._headers: + if i.lower() == "location": + return j + return None + + def header_equal(self, header, value): + for i, j in self._headers: + if i == header and j.lower() == value.lower(): + return True + return False + + def getHeaders(self): + return self._headers + + def getContent(self): + return self.__content + + def getTextHeaders(self): + 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 + + def getAll(self): + string = self.getTextHeaders() + "\r\n" + self.getContent() + return string + + def Substitute(self, src, dst): + a = self.getAll() + b = a.replace(src, dst) + self.parseResponse(b) + + def getAll_wpost(self): + 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 + + def parseResponse(self, rawheader, rawbody=None, type="curl"): + self.__content = "" + self._headers = [] + + tp = TextParser() + rawheader = python2_3_convert_from_unicode(rawheader.decode("utf-8", errors='replace')) + tp.setSource("string", rawheader) + + tp.readUntil(r"(HTTP\S*) ([0-9]+)") + while True: + while True: + try: + self.protocol = tp[0][0] + except Exception: + self.protocol = "unknown" + + try: + self.code = tp[0][1] + except Exception: + self.code = "0" + + if self.code != "100": + break + else: + tp.readUntil(r"(HTTP\S*) ([0-9]+)") + + self.code = int(self.code) + + while True: + tp.readLine() + if (tp.search("^([^:]+): ?(.*)$")): + self.addHeader(tp[0][0], tp[0][1]) + else: + break + + # curl sometimes sends two headers when using follow, 302 and the final header + tp.readLine() + if not tp.search(r"(HTTP\S*) ([0-9]+)"): + break + else: self._headers = [] - tp = TextParser() - rawheader = python2_3_convert_from_unicode(rawheader.decode("utf-8", errors='replace')) - tp.setSource("string", rawheader) - - tp.readUntil(r"(HTTP\S*) ([0-9]+)") - while True: - while True: - try: - self.protocol = tp[0][0] - except Exception: - self.protocol = "unknown" - - try: - self.code = tp[0][1] - except Exception: - self.code = "0" - - if self.code != "100": - break - else: - tp.readUntil(r"(HTTP\S*) ([0-9]+)") - - self.code = int(self.code) - - while True: - tp.readLine() - if (tp.search("^([^:]+): ?(.*)$")): - self.addHeader(tp[0][0], tp[0][1]) - else: - break - - # curl sometimes sends two headers when using follow, 302 and the final header - tp.readLine() - if not tp.search(r"(HTTP\S*) ([0-9]+)"): - break - else: - self._headers = [] - - while tp.skip(1): - self.addContent(tp.lastFull_line) - - if type == 'curl': - self.delHeader("Transfer-Encoding") - - if self.header_equal("Transfer-Encoding", "chunked"): - result = "" - content = BytesIO(rawbody) - hexa = content.readline() - nchunk = int(hexa.strip(), 16) - - while nchunk: - result += content.read(nchunk) - content.readline() - hexa = content.readline() - nchunk = int(hexa.strip(), 16) - - rawbody = result - - if self.header_equal("Content-Encoding", "gzip"): - compressedstream = BytesIO(rawbody) - gzipper = gzip.GzipFile(fileobj=compressedstream) - rawbody = gzipper.read() - self.delHeader("Content-Encoding") - elif self.header_equal("Content-Encoding", "deflate"): - deflated_data = None - try: - deflater = zlib.decompressobj() - deflated_data = deflater.decompress(rawbody) - deflated_data += deflater.flush() - except zlib.error: - try: - deflater = zlib.decompressobj(-zlib.MAX_WBITS) - deflated_data = deflater.decompress(rawbody) - deflated_data += deflater.flush() - except zlib.error: - deflated_data = '' - rawbody = deflated_data - self.delHeader("Content-Encoding") - - # Try to get charset encoding from headers - content_encoding = get_encoding_from_headers(dict(self.getHeaders())) - - # fallback to default encoding - if content_encoding is None: - content_encoding = "utf-8" - - self.__content = python2_3_convert_from_unicode(rawbody.decode(content_encoding, errors='replace')) + while tp.skip(1): + self.addContent(tp.lastFull_line) + + if type == 'curl': + self.delHeader("Transfer-Encoding") + + if self.header_equal("Transfer-Encoding", "chunked"): + result = "" + content = BytesIO(rawbody) + hexa = content.readline() + nchunk = int(hexa.strip(), 16) + + while nchunk: + result += content.read(nchunk) + content.readline() + hexa = content.readline() + nchunk = int(hexa.strip(), 16) + + rawbody = result + + if self.header_equal("Content-Encoding", "gzip"): + compressedstream = BytesIO(rawbody) + gzipper = gzip.GzipFile(fileobj=compressedstream) + rawbody = gzipper.read() + self.delHeader("Content-Encoding") + elif self.header_equal("Content-Encoding", "deflate"): + deflated_data = None + try: + deflater = zlib.decompressobj() + deflated_data = deflater.decompress(rawbody) + deflated_data += deflater.flush() + except zlib.error: + try: + deflater = zlib.decompressobj(-zlib.MAX_WBITS) + deflated_data = deflater.decompress(rawbody) + deflated_data += deflater.flush() + except zlib.error: + deflated_data = '' + rawbody = deflated_data + self.delHeader("Content-Encoding") + + # Try to get charset encoding from headers + content_encoding = get_encoding_from_headers(dict(self.getHeaders())) + + # fallback to default encoding + if content_encoding is None: + content_encoding = "utf-8" + + 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 287d1862..c9780478 100755 --- a/src/wfuzz/externals/reqresp/TextParser.py +++ b/src/wfuzz/externals/reqresp/TextParser.py @@ -9,143 +9,143 @@ class TextParser(object): - def __init__(self): - self.string = "" - self.oldindex = 0 - self.newindex = 0 - self.type = "" - self.lastFull_line = None - self.lastline = None - - self.actualIndex = 0 - - def __del__(self): - if self.type == "file": - self.fd.close() - - def __str__(self): - return str(self.matches) - - def __iter__(self): - self.actualIndex = 0 - return self - - def __next__(self): - try: - value = self.matches[self.actualIndex] - self.actualIndex += 1 - return value - except Exception: - raise StopIteration - - def setSource(self, t, *args): - '''Se especifica el tipo de entrada. Puede ser fichero o entrada estandard - - Ejemplos: setSource("file","/tmp/file") - setSource("stdin")\n''' - - if t == "file": - self.type = t - self.fd = open(args[0], "r") - elif t == "stdin": - if self.type == "file": - self.fd.close() - self.type = t - elif t == "string": - if self.type == "file": - self.fd.close() - self.type = t - self.string = args[0] - self.oldindex = 0 - self.newindex = 0 - else: - print("Bad argument -- TextParser.setSource()\n") - sys.exit(-1) - - def seekinit(self): - self.oldindex = 0 - self.newindex = 0 - - def readUntil(self, pattern, caseSens=True): - "Lee lineas hasta que el patron (pattern) conincide en alguna linea" - - while True: - if (self.readLine() == 0): - return False - if (self.search(pattern, caseSens) is True): - break - - return True - - def search(self, pattern, caseSens=True, debug=0): - "Intenta hacer Matching entre el pattern pasado por parametro y la ultima linea leida" - - if not caseSens: - self.regexp = re.compile(pattern, re.IGNORECASE) - else: - self.regexp = re.compile(pattern) - self.matches = self.regexp.findall(self.lastline) - j = 0 - for i in self.matches: - if not isinstance(i, tuple): - self.matches[j] = tuple([self.matches[j]]) - j += 1 + def __init__(self): + self.string = "" + self.oldindex = 0 + self.newindex = 0 + self.type = "" + self.lastFull_line = None + self.lastline = None + + self.actualIndex = 0 + + def __del__(self): + if self.type == "file": + self.fd.close() + + def __str__(self): + return str(self.matches) + + def __iter__(self): + self.actualIndex = 0 + return self + + def __next__(self): + try: + value = self.matches[self.actualIndex] + self.actualIndex += 1 + return value + except Exception: + raise StopIteration + + def setSource(self, t, *args): + '''Se especifica el tipo de entrada. Puede ser fichero o entrada estandard + + Ejemplos: setSource("file","/tmp/file") + setSource("stdin")\n''' + + if t == "file": + self.type = t + self.fd = open(args[0], "r") + elif t == "stdin": + if self.type == "file": + self.fd.close() + self.type = t + elif t == "string": + if self.type == "file": + self.fd.close() + self.type = t + self.string = args[0] + self.oldindex = 0 + self.newindex = 0 + else: + print("Bad argument -- TextParser.setSource()\n") + sys.exit(-1) + + def seekinit(self): + self.oldindex = 0 + self.newindex = 0 + + def readUntil(self, pattern, caseSens=True): + "Lee lineas hasta que el patron (pattern) conincide en alguna linea" + + while True: + if (self.readLine() == 0): + return False + if (self.search(pattern, caseSens) is True): + break + + return True + + def search(self, pattern, caseSens=True, debug=0): + "Intenta hacer Matching entre el pattern pasado por parametro y la ultima linea leida" + + if not caseSens: + self.regexp = re.compile(pattern, re.IGNORECASE) + else: + self.regexp = re.compile(pattern) + self.matches = self.regexp.findall(self.lastline) + j = 0 + for i in self.matches: + if not isinstance(i, tuple): + self.matches[j] = tuple([self.matches[j]]) + j += 1 # DEBUG PARA MATCHING - if (debug == 1): - print(("[", self.lastline, "-", pattern, "]")) - print((len(self.matches))) - print((self.matches)) + if (debug == 1): + print(("[", self.lastline, "-", pattern, "]")) + print((len(self.matches))) + print((self.matches)) - if len(self.matches) == 0: - return False - else: - return True + if len(self.matches) == 0: + return False + else: + return True - def __getitem__(self, key): - "Para acceder a cada uno de los patrones que coinciden, esta preparado paragrupos de patrones, no para solo un patron" + def __getitem__(self, key): + "Para acceder a cada uno de los patrones que coinciden, esta preparado paragrupos de patrones, no para solo un patron" - return self.matches[key] + return self.matches[key] - def skip(self, lines): - "Salta las lines que se indiquen en el parametro" + def skip(self, lines): + "Salta las lines que se indiquen en el parametro" - for i in range(lines): - if (self.readLine() == 0): - return False + for i in range(lines): + if (self.readLine() == 0): + return False - return True + return True - def readLine(self): - "Lee la siguiente linea eliminando retornos de carro" + def readLine(self): + "Lee la siguiente linea eliminando retornos de carro" - if self.type == "file": - self.lastFull_line = self.fd.readline() - elif self.type == "stdin": - self.lastFull_line = input() - elif self.type == "string": - if self.newindex == -1: - return 0 + if self.type == "file": + self.lastFull_line = self.fd.readline() + elif self.type == "stdin": + self.lastFull_line = input() + elif self.type == "string": + if self.newindex == -1: + return 0 - 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] + 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.oldindex = self.newindex + 1 - else: - self.lastFull_line = '' + self.oldindex = self.newindex + 1 + else: + self.lastFull_line = '' - bytes_read = len(self.lastFull_line) + bytes_read = len(self.lastFull_line) - s = self.lastFull_line - self.lastline = s + s = self.lastFull_line + self.lastline = s - if s[-2:] == '\r\n': - self.lastline = s[:-2] - elif s[-1:] == '\r' or s[-1:] == '\n': - self.lastline = s[:-1] + if s[-2:] == '\r\n': + self.lastline = s[:-2] + elif s[-1:] == '\r' or s[-1:] == '\n': + self.lastline = s[:-1] - return bytes_read + return bytes_read diff --git a/src/wfuzz/externals/reqresp/Variables.py b/src/wfuzz/externals/reqresp/Variables.py index f08aeefb..90f0820b 100644 --- a/src/wfuzz/externals/reqresp/Variables.py +++ b/src/wfuzz/externals/reqresp/Variables.py @@ -2,121 +2,121 @@ class Variable: - def __init__(self, name, value="", extraInfo=""): - self.name = name - self.value = value - self.initValue = value - self.extraInfo = extraInfo + def __init__(self, name, value="", extraInfo=""): + self.name = name + self.value = value + self.initValue = value + self.extraInfo = extraInfo - def restore(self): - self.value = self.initValue + def restore(self): + self.value = self.initValue - def change(self, newval): - self.initValue = self.value = newval + def change(self, newval): + self.initValue = self.value = newval - def update(self, val): - self.value = val + def update(self, val): + self.value = val - def append(self, val): - self.value += val + def append(self, val): + self.value += val - def __str__(self): - return "[ %s : %s ]" % (self.name, self.value) + def __str__(self): + return "[ %s : %s ]" % (self.name, self.value) class VariablesSet: - def __init__(self): - self.variables = [] - self.boundary = None - - def names(self): - dicc = [] - for i in self.variables: - dicc.append(i.name) - - return dicc - - def existsVar(self, name): - return name in self.names() - - def addVariable(self, name, value="", extraInfo=""): - self.variables.append(Variable(name, value, extraInfo)) - - def getVariable(self, name): - dicc = [] - for i in self.variables: - if i.name == name: - dicc.append(i) - - if len(dicc) > 1: - raise Exception("Variable exists more than one time!!! :D" % (name)) - - if not dicc: - var = Variable(name) - self.variables.append(var) - return var - - 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]) - - def parseUrlEncoded(self, cad): - dicc = [] - - if cad == '': - dicc.append(Variable('', None)) - - for i in cad.split("&"): - if i: - list = i.split("=", 1) - if len(list) == 1: - dicc.append(Variable(list[0], None)) - elif len(list) == 2: - dicc.append(Variable(list[0], list[1])) - - self.variables = dicc - - def multipartEncoded(self): - if not self.boundary: - self.boundary = "---------------------------D33PB1T0R3QR3SP0B0UND4RY2203" - pd = "" - for i in self.variables: - pd += "--" + self.boundary + "\r\n" - pd += "%s\r\n\r\n%s\r\n" % ("\r\n".join(i.extraInfo), i.value) - pd += "--" + self.boundary + "--\r\n" - return pd - - def parseMultipart(self, cad, boundary): - self.boundary = boundary - dicc = [] - tp = TextParser() - tp.setSource("string", cad) - - while True: - headers = [] - if not tp.readUntil("name=\"([^\"]+)\""): - break - var = tp[0][0] - headers.append(tp.lastFull_line.strip()) - while True: - tp.readLine() - if tp.search("^([^:]+): (.*)$"): - headers.append(tp.lastFull_line.strip()) - else: - break - - value = "" - while True: - tp.readLine() - if not tp.search(boundary): - value += tp.lastFull_line - else: - break - - if value[-2:] == "\r\n": - value = value[:-2] - - dicc.append(Variable(var, value, headers)) - - self.variables = dicc + def __init__(self): + self.variables = [] + self.boundary = None + + def names(self): + dicc = [] + for i in self.variables: + dicc.append(i.name) + + return dicc + + def existsVar(self, name): + return name in self.names() + + def addVariable(self, name, value="", extraInfo=""): + self.variables.append(Variable(name, value, extraInfo)) + + def getVariable(self, name): + dicc = [] + for i in self.variables: + if i.name == name: + dicc.append(i) + + if len(dicc) > 1: + raise Exception("Variable exists more than one time!!! :D" % (name)) + + if not dicc: + var = Variable(name) + self.variables.append(var) + return var + + 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]) + + def parseUrlEncoded(self, cad): + dicc = [] + + if cad == '': + dicc.append(Variable('', None)) + + for i in cad.split("&"): + if i: + list = i.split("=", 1) + if len(list) == 1: + dicc.append(Variable(list[0], None)) + elif len(list) == 2: + dicc.append(Variable(list[0], list[1])) + + self.variables = dicc + + def multipartEncoded(self): + if not self.boundary: + self.boundary = "---------------------------D33PB1T0R3QR3SP0B0UND4RY2203" + pd = "" + for i in self.variables: + pd += "--" + self.boundary + "\r\n" + pd += "%s\r\n\r\n%s\r\n" % ("\r\n".join(i.extraInfo), i.value) + pd += "--" + self.boundary + "--\r\n" + return pd + + def parseMultipart(self, cad, boundary): + self.boundary = boundary + dicc = [] + tp = TextParser() + tp.setSource("string", cad) + + while True: + headers = [] + if not tp.readUntil("name=\"([^\"]+)\""): + break + var = tp[0][0] + headers.append(tp.lastFull_line.strip()) + while True: + tp.readLine() + if tp.search("^([^:]+): (.*)$"): + headers.append(tp.lastFull_line.strip()) + else: + break + + value = "" + while True: + tp.readLine() + if not tp.search(boundary): + value += tp.lastFull_line + else: + break + + if value[-2:] == "\r\n": + value = value[:-2] + + dicc.append(Variable(var, value, headers)) + + self.variables = dicc diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 6556c3b6..c8f11d99 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -281,7 +281,7 @@ def is_visible(self, res): 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'] + cond1 = self.hideparams['codes_show'] if self.hideparams['regex']: if self.hideparams['regex'].search(res.history.content): diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index e03aa324..8da0952a 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -380,16 +380,16 @@ def replace_fuzz_word(text, fuzz_word, payload): 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.") + 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.") + 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) + text = text.replace("%s[%s]" % (fuzz_word, field), subs) + fields_array.append(field) return (text, fields_array) else: diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 5c92f158..bdc02d89 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -139,7 +139,7 @@ def validate(self): 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']: - return "Bad usage: Advanced and filter flags are mutually exclusive. Only one could be specified." + return "Bad usage: Advanced and filter flags are mutually exclusive. Only one could be specified." except TypeError: return "Bad options: Filter must be specified in the form of [int, ... , int]." @@ -262,7 +262,7 @@ def compile(self): if self.data["compiled_genreq"].baseline is None and (FuzzResult.BASELINE_CODE in self.data['hc'] or FuzzResult.BASELINE_CODE in self.data['hl'] or FuzzResult.BASELINE_CODE in self.data['hw'] or FuzzResult.BASELINE_CODE in self.data['hh']): - raise FuzzExceptBadOptions("Bad options: specify a baseline value when using BBB") + raise FuzzExceptBadOptions("Bad options: specify a baseline value when using BBB") if self.data["script"]: Facade().scripts.kbase.update(self.data["script_args"]) diff --git a/src/wfuzz/plugins/encoders/encoders.py b/src/wfuzz/plugins/encoders/encoders.py index 516838ca..76f7219a 100644 --- a/src/wfuzz/plugins/encoders/encoders.py +++ b/src/wfuzz/plugins/encoders/encoders.py @@ -232,8 +232,8 @@ def encode(self, string): for c in strt: if not c == "%": if s.search(c): - fin += c - continue + fin += c + continue fin += con % ord(c) else: fin += c diff --git a/src/wfuzz/plugins/payloads/burpstate.py b/src/wfuzz/plugins/payloads/burpstate.py index 5f3259dc..0e851a95 100644 --- a/src/wfuzz/plugins/payloads/burpstate.py +++ b/src/wfuzz/plugins/payloads/burpstate.py @@ -74,48 +74,48 @@ def __next__(self): 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: - 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.''' - 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) - except ValueError: # Bad date, just return the milliseconds - date = str(milliseconds) - return None - return date + '''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.''' + 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) + 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.''' - 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] - date = self.milliseconds_to_date(ms) - 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]) - 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] - # 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 - return value, 5 + length # ** TODO: Verify length by matching end tag ** - print("Unknown binary format", repr(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.''' + 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] + date = self.milliseconds_to_date(ms) + 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]) + 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] + # 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 + 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('" and value not in ["65", "67"]: - raise FuzzExceptBadFile("Unknown burp log version %s" % value) + if self.params["checkversion"] and etag == "" and value not in ["65", "67"]: + raise FuzzExceptBadFile("Unknown burp log version %s" % value) - if etag == "": - https_tag = value == "True" + if etag == "": + https_tag = value == "True" - if etag in self.request_tags: - raw_request = self.strip_cdata(value) + if etag in self.request_tags: + raw_request = self.strip_cdata(value) - 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)) - frr = FuzzResult(history=fr) + 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)) + frr = FuzzResult(history=fr) - raw_request = "" - https_tag = "" + raw_request = "" + https_tag = "" - yield frr.update() + yield frr.update() diff --git a/src/wfuzz/plugins/payloads/names.py b/src/wfuzz/plugins/payloads/names.py index 42dd935d..e887103e 100644 --- a/src/wfuzz/plugins/payloads/names.py +++ b/src/wfuzz/plugins/payloads/names.py @@ -45,10 +45,10 @@ def __init__(self, params): str3 = "" str4 = "" for i in range(0, len(parts) - 1): - str1 = str1 + parts[i] + "." - str2 = str2 + parts[i] - str3 = str3 + parts[i][0] + "." - str4 = str4 + parts[i][0] + str1 = str1 + parts[i] + "." + str2 = str2 + parts[i] + str3 = str3 + parts[i][0] + "." + str4 = str4 + parts[i][0] str5 = str1 + parts[-1] str6 = str2 + parts[-1] str7 = str4 + parts[-1] diff --git a/src/wfuzz/plugins/printers/printers.py b/src/wfuzz/plugins/printers/printers.py index efb6a5b9..a5cc689b 100644 --- a/src/wfuzz/plugins/printers/printers.py +++ b/src/wfuzz/plugins/printers/printers.py @@ -234,7 +234,7 @@ def _print_verbose(self, res): 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) + self.f.write(" |_ %s\n" % i.issue) def _print(self, res): if res.exception: @@ -245,7 +245,7 @@ def _print(self, res): 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) + self.f.write(" |_ %s\n" % i.issue) def result(self, res): if res.type == FuzzResult.result: diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index b4063383..8a32394c 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -198,7 +198,7 @@ def header(self, summary): if summary.total_req > 0: print("Total requests: %d\r\n" % summary.total_req) else: - print("Total requests: <>\r\n") + print("Total requests: <>\r\n") if self.verbose: print("==============================================================================================================================================\r") diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index c931e331..3597c441 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -243,7 +243,7 @@ def _get_alias(attr): def rsetattr(obj, attr, new_val, operation): if not _check_allowed_field(attr): - raise FuzzExceptIncorrectFilter("Unknown field {}".format(attr)) + raise AttributeError("Unknown field {}".format(attr)) pre, _, post = attr.rpartition('.') @@ -270,7 +270,7 @@ def rsetattr(obj, attr, new_val, operation): return setattr(obj_to_set, post, val) except AttributeError: - raise FuzzExceptIncorrectFilter("rsetattr: Can't set '{}' attribute of {}.".format(post, obj_to_set.__class__)) + raise AttributeError("rsetattr: Can't set '{}' attribute of {}.".format(post, obj_to_set.__class__)) def rgetattr(obj, attr, *args): @@ -279,10 +279,10 @@ def _getattr(obj, attr): try: return getattr(obj, attr, *args) except AttributeError: - raise FuzzExceptIncorrectFilter("rgetattr: Can't get '{}' attribute from '{}'.".format(attr, obj.__class__)) + raise AttributeError("rgetattr: Can't get '{}' attribute from '{}'.".format(attr, obj.__class__)) if not _check_allowed_field(attr): - raise FuzzExceptIncorrectFilter("Unknown field {}".format(attr)) + raise AttributeError("Unknown field {}".format(attr)) return functools.reduce(_getattr, [obj] + attr.split('.')) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index bb7c936f..e7038e92 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -63,6 +63,7 @@ ] testing_tests = [ + ("test_slice3", "%s/FUZZ" % URL_LOCAL, None, dict(payloads=[("range", dict(default="1-10"), "FUZZ[c]=1")]), [(404, '/dir/1')], "aaaaaaaaaaa"), ] basic_tests = [ diff --git a/tests/test_api.py b/tests/test_api.py index 04a184d1..014c1e3c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -114,7 +114,3 @@ def test_iterator(self): 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')])) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_moduleman.py b/tests/test_moduleman.py index c5014f55..dddee523 100644 --- a/tests/test_moduleman.py +++ b/tests/test_moduleman.py @@ -110,22 +110,22 @@ 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)) - mocked_load_module.return_value = sys.modules[__name__] - - br = BRegistrant(FileLoader(**{"filename": 'project1.py', "base_path": 'any'})) + 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__] - 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 = BRegistrant(FileLoader(**{"filename": 'project1.py', "base_path": 'any'})) + with self.assertRaises(Exception) as context: 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([])) + 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([])) def test_plugin_decorator(self): with self.assertRaises(Exception) as context: @@ -135,7 +135,3 @@ class test_plugin4: test_plugin4() self.assertTrue("Required method method4 not implemented" in str(context.exception)) - - -if __name__ == '__main__': - unittest.main() From 597e3ae13abb1c81307455d1407ccceb173d0f12 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 7 Feb 2019 22:36:04 +0100 Subject: [PATCH 024/119] call testing list for prev session tests --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index e7038e92..e9b9d304 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -426,7 +426,7 @@ def create_tests(): """ if testing_savedsession_tests: - create_savedsession_tests(savedsession_tests, wfuzz_me_test_generator_previous_session) + create_savedsession_tests(testing_savedsession_tests, wfuzz_me_test_generator_previous_session) return if testing_tests: From fe3d64861f09b50851f3d5e519bc40097c403e57 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 7 Feb 2019 22:37:02 +0100 Subject: [PATCH 025/119] str before equal --- src/wfuzz/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index c8f11d99..4fab1375 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -199,7 +199,7 @@ def __compute_expr(self, tokens): try: if exp_operator in ["=", '==']: - return leftvalue == rightvalue + return str(leftvalue) == str(rightvalue) elif exp_operator == "<=": return leftvalue <= rightvalue elif exp_operator == ">=": From 10950e4c0054e525d382bd71383958863a8ebc31 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 7 Feb 2019 22:37:54 +0100 Subject: [PATCH 026/119] remove nonused import --- src/wfuzz/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 3597c441..0388fe46 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -6,7 +6,6 @@ import functools from chardet.universaldetector import UniversalDetector -from .exception import FuzzExceptIncorrectFilter def json_minify(string, strip_space=True): From 567da236435c5520a9a984330663af29ac9af407 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 7 Feb 2019 23:59:20 +0100 Subject: [PATCH 027/119] more flexible prev test acc cases --- tests/test_acceptance.py | 41 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index e9b9d304..87c1150b 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -42,28 +42,29 @@ ] savedsession_tests = [ + # 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:"), + # set values - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_fuzz", "FUZZ", dict(), None, ["http://localhost:9000/1"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_attr", "FUZZ[url]", dict(), None, ["http://localhost:9000/1"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_concat_number", "FUZZ[url]FUZZ[c]", dict(), None, ["http://localhost:9000/1 - 404"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_url_number", "FUZZ[c]", dict(), None, ["http://localhost:9000/1 - 404"], "Pycurl error 7:"), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_concat_number", "FUZZ[url]FUZZ[c]", dict(), "r.c:=302", ["http://localhost:9000/1 - 302"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_rewrite_url", "FUZZ", dict(prefilter="r.url:=r.url|replace('1','2')"), None, ["http://localhost:9000/2"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_rewrite_url2", "FUZZ[url]", dict(), "r.url:=r.url|replace('1','2')", ["http://localhost:9000/2"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_assign_fuzz_symbol_op", "FUZZ[url]", dict(), "FUZZ[r.url]:=FUZZ[r.url|replace('1','2')]", ["http://localhost:9000/2"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_desc_concat_fuzz_symbol_op", "FUZZ", dict(), "FUZZ[r.url]=+'2'", ["http://localhost:9000/12"], 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 - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_symbol_code", "FUZZ", dict(), "FUZZ[c]=404", ["http://localhost:9000/1"], None), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_value_code", "FUZZ", dict(), "c=404", ["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 - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_symbol_code", "FUZZ", dict(), "FUZ1Z[c]=404", ["http://localhost:9000/1"], "Unknown field"), - ("-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "test_fuzz_symbol_code", "FUZZ", dict(), "FUZ2Z[c]=404", ["http://localhost:9000/1"], "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), ] testing_tests = [ - ("test_slice3", "%s/FUZZ" % URL_LOCAL, None, dict(payloads=[("range", dict(default="1-10"), "FUZZ[c]=1")]), [(404, '/dir/1')], "aaaaaaaaaaa"), ] basic_tests = [ @@ -330,7 +331,7 @@ def test(self): return test -def wfuzz_me_test_generator_previous_session(prev_session_cli, url, params, slicestr, 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() @@ -342,10 +343,8 @@ def test(self): ret_list = [x.description for x in s.fuzz(save=filename)] # second session wfuzzp as payload - with wfuzz.FuzzSession(url=url, **params) as s: - fuzzed = s.fuzz(payloads=[("wfuzzp", dict(fn=filename), slicestr)]) - - ret_list = [x.description for x in fuzzed] + with wfuzz.get_session(next_session_cli.replace("$$PREVFILE$$", filename)) as s: + ret_list = [x.description for x in s.fuzz()] self.assertEqual(sorted(ret_list), sorted(expected_list)) @@ -411,8 +410,8 @@ def create_savedsession_tests(test_list, test_gen_fun): generates wfuzz tests that run 2 times with recipe input, expecting same results. """ - for prev_cli, test_name, url, params, slicestr, expected_res, exception_str in test_list: - test_fn = test_gen_fun(prev_cli, url, params, slicestr, expected_res) + for test_name, prev_cli, next_cli, expected_res, exception_str in test_list: + test_fn = test_gen_fun(prev_cli, next_cli, expected_res) if exception_str: test_fn_exc = wfuzz_me_test_generator_exception(test_fn, exception_str) setattr(DynamicTests, test_name, test_fn_exc) From fe71942fac5df97e5b1cf52d018f5f125d67f318 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 16 Feb 2019 13:02:31 +0100 Subject: [PATCH 028/119] filter stack always to use left operand --- src/wfuzz/core.py | 9 +++++++-- src/wfuzz/filter.py | 21 ++++++++++++++------- src/wfuzz/fuzzobjects.py | 7 +++---- src/wfuzz/options.py | 6 ++++++ tests/test_acceptance.py | 14 ++++++++++++-- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index 6c11d171..d3b95f5d 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -113,6 +113,12 @@ def restart(self, seed): self.dictio = self.get_dictio() def _check_dictio_len(self, element): + 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 @@ -125,8 +131,7 @@ def _check_dictio_len(self, element): if self.options["seed_payload"]: fuzz_words += ["FUZZ"] - if len(element) != len(set(fuzz_words)): - raise FuzzExceptBadOptions("FUZZ words and number of payloads do not match!") + return fuzz_words def count(self): v = self.dictio.count() diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 4fab1375..d7eb1f19 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -90,7 +90,7 @@ def __init__(self, ffilter=None, filter_string=None): self.hideparams['filter_string'] = filter_string self.baseline = None - self.stack = {} + self.stack = [] self._cache = collections.defaultdict(set) @@ -107,9 +107,9 @@ def set_baseline(self, res): self.baseline = res def __compute_res_value(self, tokens): - self.stack["field"] = tokens[0] + self.stack.append(tokens[0]) - return rgetattr(self.res, self.stack["field"]) + return rgetattr(self.res, tokens[0]) def _compute_fuzz_symbol(self, tokens): i = tokens[0] @@ -127,17 +127,17 @@ def _compute_fuzz_symbol(self, tokens): def __compute_fuzz_value(self, tokens): fuzz_val, field = tokens - self.stack["field"] = field + self.stack.append(field) try: - return rgetattr(fuzz_val, self.stack["field"]) if field else fuzz_val + 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["field"] + element = self.stack[0] if self.stack else None if self.baseline is None: raise FuzzExceptBadOptions("FilterQ: specify a baseline value when using BBB") @@ -195,7 +195,7 @@ def __compute_xxx_value(self, tokens): def __compute_expr(self, tokens): leftvalue, exp_operator, rightvalue = tokens[0] - field_to_set = self.stack.get('field', None) + field_to_set = self.stack[0] if self.stack else None try: if exp_operator in ["=", '==']: @@ -238,6 +238,7 @@ def __myreduce(self, elements): elif elements[i] == "or": first = (first or elements[i + 1]) + self.stack = [] return first def __compute_not_operator(self, tokens): @@ -321,6 +322,12 @@ def from_options(filter_options): 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): diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 8da0952a..a59364bb 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -441,9 +441,6 @@ def from_seed(seed, payload, seed_options): if desc: fuzz_values_array += desc - if len(fuzz_values_array) == 0: - raise FuzzExceptBadOptions("No %s word!" % fuzz_word) - newres.payload.append(FuzzPayload(payload_content, fuzz_values_array)) newres.history.update_from_raw_http(rawReq, scheme) @@ -704,7 +701,9 @@ def __str__(self): @property def description(self): - ret_str = ' - '.join([payload.description(self.url) for payload in self.payload]) + 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]) + if self.exception: return ret_str + "! " + str(self.exception) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index bdc02d89..1054c966 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -259,6 +259,12 @@ def compile(self): # seed self.data["compiled_genreq"] = requestGenerator(self) + # Check payload num + fuzz_words = self.data["compiled_prefilter"].get_fuzz_words() + self.data["compiled_genreq"].get_fuzz_words() + + if self.data['allvars'] is None and len(set(fuzz_words)) == 0: + raise FuzzExceptBadOptions("You must specify at least a FUZZ word!") + if self.data["compiled_genreq"].baseline is None and (FuzzResult.BASELINE_CODE in self.data['hc'] or FuzzResult.BASELINE_CODE in self.data['hl'] or FuzzResult.BASELINE_CODE in self.data['hw'] or FuzzResult.BASELINE_CODE in self.data['hh']): diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 87c1150b..463ead76 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -41,6 +41,9 @@ testing_savedsession_tests = [ ] +testing_tests = [ +] + savedsession_tests = [ # field fuzz values ("test_desc_fuzz", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ FUZZ", ["http://localhost:9000/1"], None), @@ -62,9 +65,16 @@ ("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), -] -testing_tests = [ + # 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), + + # 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_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=1", "http://localhost:9000/1?param=1"], None), ] basic_tests = [ From 1d448ce554966b52494128293cacad5b62c9da0c Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 17 Feb 2019 20:44:53 +0100 Subject: [PATCH 029/119] assignment example --- docs/user/advanced.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index f8ff063a..69ebd421 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -667,6 +667,18 @@ 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:: + + $ 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" + ... + 000004: C=200 99 L 272 W 3868 Ch "4" + + $ wfuzz -z wfuzzp,/tmp/session --prefilter "r.params.get=+'\''" -A FUZZ + 00010: 0.161s C=200 101 L 287 W 3986 Ch nginx/1.4.1 "http://testphp.vulnweb.com/artists.php?artist=1'" + |_ Error identified: Warning: mysql_fetch_array() + ... + wfpayload ^^^^^^^^^ From 4899832693f33e71405fceee359465a881180184 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 17 Feb 2019 23:01:45 +0100 Subject: [PATCH 030/119] str param set --- src/wfuzz/fuzzobjects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index a59364bb..8419a621 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -92,7 +92,7 @@ def get(self): def get(self, values): if isinstance(values, dict): for key, value in values.items(): - self._req.setVariableGET(key, value) + self._req.setVariableGET(key, str(value)) else: raise FuzzExceptBadAPI("GET Parameters must be specified as a dictionary") From 5a356be84cd0d94644444a18e81484b4efb8ded9 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 17 Feb 2019 23:16:53 +0100 Subject: [PATCH 031/119] additional test cases --- tests/test_acceptance.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 463ead76..c40d1ecc 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -65,6 +65,7 @@ ("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), @@ -74,7 +75,10 @@ # 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_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=1", "http://localhost:9000/1?param=1"], 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), + ] basic_tests = [ From d9267218d88ccd906f309fce2ebd4f29c47bac43 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 19 Feb 2019 00:04:34 +0100 Subject: [PATCH 032/119] HTTP per HTML in proxy --- src/wfuzz/options.py | 4 ++-- src/wfuzz/ui/console/clparser.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 1054c966..c76ffc8d 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -128,8 +128,8 @@ def validate(self): if self.data['proxies']: for ip, port, ttype in self.data['proxies']: - if ttype not in ("SOCKS5", "SOCKS4", "HTML"): - raise FuzzExceptBadOptions("Bad proxy type specified, correct values are HTML, SOCKS4 or SOCKS5.") + 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 \ diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 40565b94..f560fdfc 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -472,7 +472,7 @@ def _parse_conn_options(self, optsd, conn_options): vals = p.split(":") if len(vals) == 2: - proxy.append((vals[0], vals[1], "HTML")) + proxy.append((vals[0], vals[1], "HTTP")) elif len(vals) == 3: proxy.append((vals[0], vals[1], vals[2])) else: From a0d20ce83b94e4f989ac1524a4e90c8517dce2ed Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 19 Feb 2019 00:06:32 +0100 Subject: [PATCH 033/119] HTTP per HTML in proxy --- src/wfuzz/myhttp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/myhttp.py b/src/wfuzz/myhttp.py index 39ea176a..55bb4fe1 100644 --- a/src/wfuzz/myhttp.py +++ b/src/wfuzz/myhttp.py @@ -152,10 +152,10 @@ def _set_extra_options(self, c, freq, poolid): c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5) elif ptype == "SOCKS4": c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS4) - elif ptype == "HTML": + elif ptype == "HTTP": pass else: - raise FuzzExceptBadOptions("Bad proxy type specified, correct values are HTML, SOCKS4 or SOCKS5.") + raise FuzzExceptBadOptions("Bad proxy type specified, correct values are HTTP, SOCKS4 or SOCKS5.") else: c.setopt(pycurl.PROXY, "") From 5825600f8bf915bcbb5f1885aca56858c78fc567 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 19 Feb 2019 00:21:01 +0100 Subject: [PATCH 034/119] HTTP per HTML in proxy --- tests/test_acceptance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index c40d1ecc..57819e4a 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -229,7 +229,7 @@ ("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, "HTML")]), [], 'Failed to connect to localhost port 888'), + ("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'), ] @@ -446,7 +446,7 @@ def create_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, "HTML")]), 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] @@ -464,7 +464,7 @@ 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, "HTML")]), None) + duplicate_tests_diff_params(basic_tests, "_proxy_", dict(proxies=[("localhost", 8080, "HTTP")]), None) create_tests() From 4099ebe351f2264667f0185c3090dc7c6132835d Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 27 Feb 2019 20:16:16 +0100 Subject: [PATCH 035/119] connect to ip option --- docs/user/advanced.rst | 10 ++++++++++ src/wfuzz/fuzzobjects.py | 13 ++++++++++++- src/wfuzz/options.py | 1 + src/wfuzz/ui/console/clparser.py | 12 +++++++++++- src/wfuzz/ui/console/common.py | 1 + tests/test_acceptance.py | 16 +++++++++++++--- tests/test_clparser.py | 20 ++++++++++++++++++++ 7 files changed, 68 insertions(+), 5 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 69ebd421..abd349c6 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -284,6 +284,16 @@ You can combine a recipe with additional command line options, for example:: In case of repeated options, command line options have precedence over options included in the recipe. +Connect to an specific host +--------------------------------------- + +The --ip option can be used to connect to a specific host and port instead of the URL's host and port:: + + wfuzz -z range,1-1 --ip 127.0.0.1 http://www.google.com/anything/FUZZ + +This useful, for example, to test if a reverse proxy can be manipulated into misrouting requests to a destination of our choice. + + Scan Mode: Ignore Errors and Exceptions --------------------------------------- diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 8419a621..717fed6e 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -3,6 +3,7 @@ import re import itertools import operator +import pycurl # Python 2 and 3 import sys @@ -126,6 +127,7 @@ def __init__(self): 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")} @@ -294,7 +296,12 @@ def perform(self): return Facade().http_pool.perform(res) def to_http_object(self, c): - return Request.to_pycurl_object(c, self._request) + 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): return self._request.response_from_conn_object(c, bh, bb) @@ -337,6 +344,9 @@ def update_from_options(self, options): 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'] @@ -355,6 +365,7 @@ def from_copy(self): 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.post diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index c76ffc8d..6f41c1bf 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -84,6 +84,7 @@ def _defaults(self): allvars=None, script="", script_args={}, + connect_to_ip=None, # this is equivalent to payloads but in a different format dictio=None, diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index f560fdfc..5c137c40 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -161,7 +161,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:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) + opts, args = getopt.getopt(self.argv[1:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) optsd = defaultdict(list) payload_cache = {} @@ -437,6 +437,16 @@ def _parse_seed(self, url, optsd, options): if "--follow" in optsd or "-L" in optsd: options['follow'] = True + if "--ip" in optsd: + splitted = optsd["--ip"][0].partition(":") + if not splitted[0]: + raise FuzzExceptBadOptions("An IP must be specified") + + options["connect_to_ip"] = { + "ip": splitted[0], + "port": splitted[2] if splitted[2] else "80" + } + if "-d" in optsd: options['postdata'] = optsd["-d"][0] diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index 37c1f33d..8038a7ff 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -103,6 +103,7 @@ \t-s N : Specify time delay between requests (0 default) \t-R depth : Recursive path discovery being depth the maximum recursion 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). \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. diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 57819e4a..9900fd9d 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -1,6 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +import copy import os import unittest import tempfile @@ -42,6 +43,7 @@ ] testing_tests = [ + ("test_url_all_url_fuzz2", "FUZZ", [["http://webscantest.com/datastore/search_get_by_name.php?name=Rake"]], dict(), [(200, '/datastore/search_get_by_name.php')], None), ] savedsession_tests = [ @@ -82,6 +84,9 @@ ] basic_tests = [ + # different connect host ip + ("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), @@ -263,6 +268,10 @@ def test(self): 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' + 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)] @@ -393,12 +402,13 @@ def duplicate_tests_diff_params(test_list, group, next_extra_params, previous_ex if group == "_proxy_" and "encode" in test_name: continue - next_extra = dict(list(params.items()) + list(next_extra_params.items())) + next_extra = copy.deepcopy(params) + next_extra.update(next_extra_params) new_test = "%s_%s" % (test_name, group) - prev_extra = params + prev_extra = copy.deepcopy(params) if previous_extra_params: - prev_extra = dict(list(params.items()) + list(previous_extra_params.items())) + prev_extra.update(previous_extra_params) create_test(new_test, url, payloads, prev_extra, None, next_extra, exception_str) diff --git a/tests/test_clparser.py b/tests/test_clparser.py index a477f3a9..cbf4e4a8 100644 --- a/tests/test_clparser.py +++ b/tests/test_clparser.py @@ -9,3 +9,23 @@ def test_listplugins(self): 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() + + 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() + + 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() + + 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() + self.assertTrue("An IP must be specified" in str(cm.exception)) From 9e446f9c161e3292bd4ca46d2dd9c5c534b3a3c9 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 27 Feb 2019 23:01:51 +0100 Subject: [PATCH 036/119] --field cli option --- src/wfuzz/fuzzobjects.py | 15 ++++++++++++++- src/wfuzz/options.py | 3 ++- src/wfuzz/ui/console/clparser.py | 5 ++++- src/wfuzz/ui/console/common.py | 1 + src/wfuzz/wfuzz.py | 12 +++++++----- tests/test_acceptance.py | 4 +++- 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 717fed6e..f6a1ef32 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -432,6 +432,7 @@ def from_seed(seed, payload, seed_options): 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 @@ -542,7 +543,10 @@ def from_options(options): fr.wf_fuzz_methods = options['method'] fr.update_from_options(options) - return FuzzResult(fr) + fuzz_res = FuzzResult(fr) + fuzz_res.update_from_options(options) + + return fuzz_res class FuzzStats: @@ -683,6 +687,8 @@ def __init__(self, history=None, exception=None, track_id=True): self.payload = [] + self._description = None + def update(self, exception=None): self.type = FuzzResult.result @@ -712,6 +718,9 @@ def __str__(self): @property def description(self): + if self._description: + return str(rgetattr(self, self._description)) + 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]) @@ -764,9 +773,13 @@ def from_soft_copy(self, track_id=True): fr.type = self.type fr.rlevel = self.rlevel fr.payload = list(self.payload) + fr._description = self._description return fr + def update_from_options(self, options): + self._description = options['description'] + @staticmethod def to_new_exception(exception): fr = FuzzResult(exception=exception, track_id=False) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 6f41c1bf..78390495 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -28,7 +28,7 @@ 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"] + self.keys_not_to_dump = ["interactive", "recipe", "seed_payload", "send_discarded", "compiled_genreq", "compiled_filter", "compiled_prefilter", "compiled_printer", "description"] # recipe must be superseded by options if "recipe" in kwargs and kwargs["recipe"]: @@ -85,6 +85,7 @@ def _defaults(self): script="", script_args={}, connect_to_ip=None, + description=None, # this is equivalent to payloads but in a different format dictio=None, diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 5c137c40..f266feb9 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -161,7 +161,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:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) + opts, args = getopt.getopt(self.argv[1:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) optsd = defaultdict(list) payload_cache = {} @@ -437,6 +437,9 @@ def _parse_seed(self, url, optsd, options): if "--follow" in optsd or "-L" in optsd: options['follow'] = True + if "--field" in optsd: + options['description'] = optsd["--field"][0] + if "--ip" in optsd: splitted = optsd["--ip"][0].partition(":") if not splitted[0]: diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index 8038a7ff..871a6666 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -95,6 +95,7 @@ \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--field : Show the specified FuzzResult field instead of the current payload \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. diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index b6e2ca57..bce168cc 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -68,13 +68,14 @@ def usage(): \n\twfpayload [Options]\n\n \nOptions:\n \t--help : This help -\t--slice : Filter payload\'s elements using the specified expression. \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--zP : Arguments for the specified payload (it must be preceded by -z or -w). +\t--slice : 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 : Show a FuzzResult field instead of current payload """) from .api import payload @@ -82,7 +83,7 @@ def usage(): import getopt try: - opts, args = getopt.getopt(sys.argv[1:], "hz:m:w:", ["help", "slice=", "zP="]) + opts, args = getopt.getopt(sys.argv[1:], "hz:m:w:", ["field=", "help", "slice=", "zP="]) except getopt.GetoptError as err: print((str(err))) usage() @@ -92,10 +93,13 @@ def usage(): usage() sys.exit() + field = None for o, value in opts: if o in ("-h", "--help"): usage() sys.exit() + if o in ("--field"): + field = value try: for res in payload(**CLParser(sys.argv).parse_cl()): @@ -104,9 +108,7 @@ def usage(): else: r = res[0] - if "FuzzResult" in str(r.__class__): - r._description = r.url - + r._description = field print(r) except KeyboardInterrupt: diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 9900fd9d..25a4a1a1 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -43,7 +43,6 @@ ] testing_tests = [ - ("test_url_all_url_fuzz2", "FUZZ", [["http://webscantest.com/datastore/search_get_by_name.php?name=Rake"]], dict(), [(200, '/datastore/search_get_by_name.php')], None), ] savedsession_tests = [ @@ -81,6 +80,9 @@ ("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), + ] basic_tests = [ From 888b131fe7a19ba977170c85a858b6630c29eef4 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 7 Mar 2019 00:20:03 +0100 Subject: [PATCH 037/119] printer wraps lines --- src/wfuzz/ui/console/common.py | 26 +++--- src/wfuzz/ui/console/mvc.py | 154 +++++++++++++++++++++++---------- src/wfuzz/wfuzz.py | 10 ++- 3 files changed, 128 insertions(+), 62 deletions(-) diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index 871a6666..b575a239 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -170,42 +170,40 @@ class Term: bgCyan = "\x1b[46m" bgWhite = "\x1b[47m" + noColour = "" + def get_colour(self, code): + cc = "" + if code == 0: cc = Term.fgYellow - wc = 12 elif code >= 400 and code < 500: cc = Term.fgRed - wc = 12 elif code >= 300 and code < 400: cc = Term.fgBlue - wc = 11 elif code >= 200 and code < 300: cc = Term.fgGreen - wc = 10 else: cc = Term.fgMagenta - wc = 1 - return (cc, wc) + return cc def delete_line(self): sys.stdout.write("\r" + Term.delete) def set_colour(self, colour): - cc, wc = colour - - sys.stdout.write(cc) + sys.stdout.write(colour) def write(self, string, colour): - cc, wc = colour - - sys.stdout.write(cc + string + Term.reset) + sys.stdout.write(colour + string + Term.reset) def go_up(self, lines): sys.stdout.write("\033[" + str(lines) + "A") def erase_lines(self, lines): - for i in range(lines): + if lines <= 1: sys.stdout.write("\r" + Term.delete) - sys.stdout.write(Term.oneup) + else: + for i in range(lines - 1): + sys.stdout.write("\r" + Term.delete) + sys.stdout.write(Term.oneup) diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index 8a32394c..b397be0e 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -1,11 +1,18 @@ import sys from collections import defaultdict import threading +import operator +from functools import reduce +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest from wfuzz.fuzzobjects import FuzzResult from .common import exec_banner, Term from .getch import _Getch +from .output import getTerminalSize, wrap_always usage = '''\r\n Interactive keyboard commands:\r\n @@ -136,22 +143,20 @@ def on_stats(self, **event): class View: + widths = [10, 8, 6, 6, 9, getTerminalSize()[0] - 65] + verbose_widths = [10, 10, 8, 6, 6, 9, 30, 30, getTerminalSize()[0] - 145] + def __init__(self, session_options): self.colour = session_options["colour"] self.verbose = session_options["verbose"] self.previous = session_options["previous"] self.term = Term() + self.printed_lines = 1 def _print_verbose(self, res, print_nres=True): - txt_colour = ("", 8) if not res.is_baseline or not self.colour else (Term.fgCyan, 8) + 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, 8 - - self.term.set_colour(txt_colour) - - if print_nres: - self.term.write("%05d: " % (res.nres), txt_colour) - self.term.write("%.3fs C=" % (res.timer), txt_colour) + txt_colour = Term.fgCyan location = "" if 'Location' in res.history.headers.response: @@ -163,54 +168,101 @@ def _print_verbose(self, res, print_nres=True): if 'Server' in res.history.headers.response: server = res.history.headers.response['Server'] - if res.exception: - self.term.write("XXX", self.term.get_colour(res.code) if self.colour else ("", 8)) - else: - self.term.write("%03d" % (res.code), self.term.get_colour(res.code) if self.colour else ("", 8)) + rows = [ + ("%09d:" % res.nres if print_nres else "", txt_colour), + ("%.3fs" % res.timer, txt_colour), + ("C=%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\"" % str(res.description), txt_colour), + ] - self.term.write(" %4d L\t %5d W\t %5d Ch %20.20s %51.51s \"%s\"" % (res.lines, res.words, res.chars, server[:17], location[:48], res.description), txt_colour) + self.term.set_colour(txt_colour) + self.printed_lines = self._print_line(rows, self.verbose_widths) - sys.stdout.flush() + 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(self, res, print_nres=True): - txt_colour = ("", 8) if not res.is_baseline or not self.colour else (Term.fgCyan, 8) - if self.previous and self.colour and not print_nres: - txt_colour = Term.fgCyan, 8 + 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)] - self.term.set_colour(txt_colour) + new_rows = wrap_row(rows, maxWidths) - if print_nres: - self.term.write("%06d: C=" % (res.nres), txt_colour) - else: - self.term.write(" C=", txt_colour) + 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.write("\n\r") - if res.exception: - self.term.write("XXX", self.term.get_colour(res.code) if self.colour else ("", 8)) - else: - self.term.write("%03d" % (res.code), self.term.get_colour(res.code) if self.colour else ("", 8)) - self.term.write(" %4d L\t %5d W\t %5d Ch\t \"%s\"" % (res.lines, res.words, res.chars, res.description), txt_colour) + 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 + if self.previous and self.colour and not print_nres: + txt_colour = Term.fgCyan + + rows = [ + ("%09d:" % res.nres if print_nres else " |_", txt_colour), + ("C=%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\"" % str(res.description), txt_colour), + ] + + self.term.set_colour(txt_colour) + self.printed_lines = self._print_line(rows, self.widths) def header(self, summary): print(exec_banner) - print("Target: %s\r" % summary.url) - if summary.total_req > 0: - print("Total requests: %d\r\n" % summary.total_req) - else: - print("Total requests: <>\r\n") + if summary: + print("Target: %s\r" % summary.url) + if summary.total_req > 0: + print("Total requests: %d\r\n" % summary.total_req) + else: + print("Total requests: <>\r\n") if self.verbose: - print("==============================================================================================================================================\r") - print("ID C.Time Response Lines Word Chars Server Redirect Payload \r") - print("==============================================================================================================================================\r\n") + rows = [ + ("ID", Term.noColour), + ("C.Time", Term.noColour), + ("Response", Term.noColour), + ("Lines", Term.noColour), + ("Word", Term.noColour), + ("Chars", Term.noColour), + ("Server", Term.noColour), + ("Redirect", Term.noColour), + ("Payload", Term.noColour), + ] + + widths = self.verbose_widths else: - print("==================================================================\r") - print("ID Response Lines Word Chars Payload \r") - print("==================================================================\r\n") + rows = [ + ("ID", Term.noColour), + ("Response", Term.noColour), + ("Lines", Term.noColour), + ("Word", Term.noColour), + ("Chars", Term.noColour), + ("Payload", Term.noColour), + ] + + widths = self.widths + + self._print_header(rows, widths) def result(self, res): - self.term.delete_line() + self.term.erase_lines(self.printed_lines) if self.verbose: self._print_verbose(res) @@ -219,18 +271,28 @@ def result(self, 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 |_ ") + sys.stdout.write("\n\r") if self.verbose: self._print_verbose(res.payload[0].content, print_nres=False) else: self._print(res.payload[0].content, print_nres=False) - sys.stdout.write("\n\r") - for i in res.plugins_res: - print(" |_ %s\r" % i.issue) + if res.plugins_res: + sys.stdout.write("\n\r") + + for i in res.plugins_res[:-1]: + 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) + + for i in range(self.printed_lines): + sys.stdout.write("\n\r") def footer(self, summary): - self.term.delete_line() - sys.stdout.write("\r\n") + 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/wfuzz.py b/src/wfuzz/wfuzz.py index bce168cc..2d433b02 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -102,14 +102,20 @@ def usage(): field = value try: - for res in payload(**CLParser(sys.argv).parse_cl()): + session_options = CLParser(sys.argv).parse_cl() + printer = View(session_options) + printer.header(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] r._description = field - print(r) + + printer.result(r) + print("") except KeyboardInterrupt: pass From a922427d0f437575da6b08b1141a0191755ba562 Mon Sep 17 00:00:00 2001 From: javi Date: Fri, 8 Mar 2019 19:57:23 +0100 Subject: [PATCH 038/119] zE and zD parameters --- docs/user/advanced.rst | 4 ++++ docs/user/getting.rst | 6 +++++- src/wfuzz/ui/console/clparser.py | 15 +++++++++++++-- tests/test_clparser.py | 23 +++++++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index abd349c6..cae0b56e 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -108,6 +108,10 @@ Encoders are specified as a payload parameter. There are two equivalent ways of 00004: C=404 7 L 12 W 168 Ch "a2ef406e2c2351e0b9e80029c909242d" ... +* The not so long way using the zE command line switch:: + + $ wfuzz -z file --zD wordlist/general/common.txt --zE md5 http://testphp.vulnweb.com/FUZZ + * The not so long way:: $ wfuzz -z file,wordlist/general/common.txt,md5 http://testphp.vulnweb.com/FUZZ diff --git a/docs/user/getting.rst b/docs/user/getting.rst index 9d47aa35..fe439bf3 100644 --- a/docs/user/getting.rst +++ b/docs/user/getting.rst @@ -100,7 +100,11 @@ Each FUZZ keyword must have its corresponding payload. There are several equival * The long way explicitly defining the payload's parameter name through the command line:: - $ wfuzz -z file --zP fn=wordlist/general/common.txt http://testphp.vulnweb.com/FUZZ + $ wfuzz -z file --zP fn=wordlist/general/common.txt http://testphp.vulnweb.com/FUZZ + +* The not so long way explicitly defining the payload's default parameter through the --zD command line option:: + + $ wfuzz -z file --zD wordlist/general/common.txt http://testphp.vulnweb.com/FUZZ * The not so long way defining only the value of the payload's default parameter:: diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index f266feb9..c706cf1e 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -161,12 +161,12 @@ 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:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) + opts, args = getopt.getopt(self.argv[1:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) optsd = defaultdict(list) payload_cache = {} for i, j in opts: - if i in ["-z", "--zP", "--slice", "-w"]: + if i in ["-z", "--zP", "--slice", "-w", "--zD", "--zE"]: if i in ["-z", "-w"]: if payload_cache: optsd["payload"].append(payload_cache) @@ -382,12 +382,23 @@ def _parse_payload(self, optsd, options): else: name = vals[0] + default_param_cli = payload["--zD"] if "--zD" in payload else None + if default_param_cli and default_param: + raise FuzzExceptBadOptions("--zD and -z parameters are exclusive.") + elif default_param_cli: + default_param = default_param_cli + if extraparams: params = dict([x.split("=", 1) for x in extraparams.split(",")]) if 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 + if encoders_cli and encoders: + raise FuzzExceptBadOptions("--zE and -z encoders are exclusive.") + elif encoders_cli: + encoders = encoders_cli if encoders: params['encoder'] = encoders.split("-") diff --git a/tests/test_clparser.py b/tests/test_clparser.py index cbf4e4a8..e8701ab0 100644 --- a/tests/test_clparser.py +++ b/tests/test_clparser.py @@ -29,3 +29,26 @@ def test_ip_option(self): with self.assertRaises(Exception) as cm: 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() + 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)]) + + + From 80a361c1a19fec1a1ed7e1c30a0afcc17a43b4a5 Mon Sep 17 00:00:00 2001 From: javi Date: Fri, 8 Mar 2019 20:04:13 +0100 Subject: [PATCH 039/119] check valid field in options obj --- src/wfuzz/options.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 78390495..9c841201 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -6,7 +6,8 @@ from .core import requestGenerator from .utils import ( json_minify, - python2_3_convert_from_unicode + python2_3_convert_from_unicode, + _check_allowed_field ) from .core import Fuzzer @@ -125,6 +126,9 @@ def validate(self): if self.data["rlevel"] < 0: return "Bad usage: Recursion level must be a positive int." + if self.data["description"] and not _check_allowed_field(self.data["description"]): + return "Bad usage: '{}' is not a valid field.".format(self.data['description']) + 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.") From 6149c03ffb46d10111aa0e49483cf7e715861e8c Mon Sep 17 00:00:00 2001 From: javi Date: Fri, 8 Mar 2019 20:10:30 +0100 Subject: [PATCH 040/119] help for zE and zD --- src/wfuzz/ui/console/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index b575a239..0492605e 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -122,6 +122,8 @@ \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 : Arguments for the specified payload (it must be preceded by -z or -w). +\t--zD : Default parameter for the specified payload (it must be preceded by -z or -w). +\t--zE : Encoder for the specified payload (it must be preceded by -z or -w). \t--slice : 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-V alltype : All parameters bruteforcing (allvars and allpost). No need for FUZZ keyword. From 269c65c382ecb7b373a38fe82869c757acf396be Mon Sep 17 00:00:00 2001 From: javi Date: Fri, 8 Mar 2019 20:20:40 +0100 Subject: [PATCH 041/119] reqtime fields and blacklist objs refs fields --- docs/user/advanced.rst | 1 + src/wfuzz/utils.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index cae0b56e..d5e5eb3a 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -482,6 +482,7 @@ params.get.<> Spcified HTTP request GET parameter params.post.<> Spcified HTTP request POST parameter pstrip Returns a signature of the HTTP request using the parameter's names without values (useful for unique operations) is_path Returns true when the HTTP request path refers to a directory. +reqtime Returns the total time that HTTP request took to be retrieved ============================ ============================================= FuzzRequest URL field is broken in smaller (read only) parts using the urlparse Python's module in the urlp attribute. diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 0388fe46..e83c694a 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -177,6 +177,11 @@ def __call__(self): def _check_allowed_field(attr): + blacklisted_fields = [ + "r.headers", + "r.params", + "r.cookies", + ] allowed_fields = [ "description", "nres", @@ -207,6 +212,7 @@ def _check_allowed_field(attr): "history.headers", "history.params", + "r.reqtime", "r.url", "r.method", "r.scheme", @@ -220,6 +226,9 @@ def _check_allowed_field(attr): "r.params", ] + if attr in blacklisted_fields: + return False + if [field for field in allowed_fields if attr.startswith(field)]: return True return False From 0957b2b94f804cff23f2c57fb492be5dfc4c8447 Mon Sep 17 00:00:00 2001 From: javi Date: Fri, 8 Mar 2019 20:22:14 +0100 Subject: [PATCH 042/119] wfpayload verbose output --- src/wfuzz/wfuzz.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index 2d433b02..903ddacf 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -68,6 +68,7 @@ def 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 @@ -83,7 +84,7 @@ def usage(): import getopt try: - opts, args = getopt.getopt(sys.argv[1:], "hz:m:w:", ["field=", "help", "slice=", "zP="]) + opts, args = getopt.getopt(sys.argv[1:], "vhz:m:w:", ["field=", "help", "slice=", "zP="]) except getopt.GetoptError as err: print((str(err))) usage() From 5d91e5eb44690c5032c96cd7799ccd09155b1560 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 9 Mar 2019 13:35:18 +0100 Subject: [PATCH 043/119] bash completion --- src/wfuzz/facade.py | 3 + src/wfuzz/ui/console/clparser.py | 33 ++++++++++- src/wfuzz/utils.py | 98 ++++++++++++++++---------------- wfuzz_bash_completion | 66 +++++++++++++++++++++ 4 files changed, 150 insertions(+), 50 deletions(-) create mode 100644 wfuzz_bash_completion diff --git a/src/wfuzz/facade.py b/src/wfuzz/facade.py index e902f191..eacb7241 100644 --- a/src/wfuzz/facade.py +++ b/src/wfuzz/facade.py @@ -76,6 +76,9 @@ def _load(self, cat): def proxy(self, which): return self._load(which) + def get_registrants(self): + return self.__plugins.keys() + def __getattr__(self, name): if name in ["printers", "payloads", "iterators", "encoders", "scripts"]: return self._load(name) diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index c706cf1e..11b7048d 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -2,6 +2,7 @@ import getopt from collections import defaultdict +from wfuzz.utils import allowed_fields from wfuzz.filter import PYPARSING from wfuzz.facade import Facade from wfuzz.options import FuzzSession @@ -13,6 +14,9 @@ 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 = ['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'] + class CLParser: def __init__(self, argv): @@ -142,6 +146,9 @@ def show_plugins_help(self, registrant, cols=3, category="$all$"): 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)) @@ -161,7 +168,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:], "hLAZX:vcb:e:R:d:z:r:f:t:w:V:H:m:f:o:s:p:w:u:", ['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']) + opts, args = getopt.getopt(self.argv[1:], short_opts, long_opts) optsd = defaultdict(list) payload_cache = {} @@ -264,6 +271,30 @@ def _parse_help_opt(self, optsd): self.show_plugin_ext_help("scripts", category=script_string) + if "--ee" in optsd: + if "payloads" in optsd["--ee"]: + self.show_plugins_names("payloads") + elif "encoders" in optsd["--ee"]: + self.show_plugins_names("encoders") + elif "iterators" in optsd["--ee"]: + self.show_plugins_names("iterators") + elif "printers" in optsd["--ee"]: + self.show_plugins_names("printers") + elif "scripts" in optsd["--ee"]: + self.show_plugins_names("scripts") + elif "fields" in optsd["--ee"]: + print('\n'.join(allowed_fields)) + elif "files" in optsd["--ee"]: + print('\n'.join(Facade().sett.get('general', 'lookup_dirs').split(","))) + elif "registrants" in optsd["--ee"]: + 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])) + else: + raise FuzzExceptBadOptions("Unknown category. Valid values are: payloads, encoders, iterators, printers or scripts.") + sys.exit(0) + if "-e" in optsd: if "payloads" in optsd["-e"]: self.show_plugins_help("payloads") diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index e83c694a..743aa935 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -7,6 +7,55 @@ from chardet.universaldetector import UniversalDetector +blacklisted_fields = [ + "r.headers", + "r.params", + "r.cookies", +] +allowed_fields = [ + "description", + "nres", + "code", + "chars", + "lines", + "words", + "md5", + 'l', + 'h', + 'w', + 'c', + 'r', + 'history', + + '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.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): ''' @@ -177,55 +226,6 @@ def __call__(self): def _check_allowed_field(attr): - blacklisted_fields = [ - "r.headers", - "r.params", - "r.cookies", - ] - allowed_fields = [ - "description", - "nres", - "code", - "chars", - "lines", - "words", - "md5", - 'l', - 'h', - 'w', - 'c', - 'r', - 'history', - - '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.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", - ] - if attr in blacklisted_fields: return False diff --git a/wfuzz_bash_completion b/wfuzz_bash_completion new file mode 100644 index 00000000..f30f708b --- /dev/null +++ b/wfuzz_bash_completion @@ -0,0 +1,66 @@ +# wfuzz bash completion file +# by Xavier Mendez (xavi.mendez@gmail.com) aka Javi + +_wfuzz() { + + COMPREPLY=() + local cur prev + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + WFUZZ_EX="./wfuzz" + + # Change to your wordlists' base directory + WLDIR=$($WFUZZ_EX --ee files) + + common_options="-z[PAYLOAD] --zD[DEFAULT] --zE[ENCODERS] --hc[HIDE_HTTP_CODES] -d[POST_DATA] " + + case "$prev" in + -u) + COMPREPLY=( $( compgen -W "http https" -- $cur ) ) + ;; + -w) + COMPREPLY=( $(compgen -W "$(find $WLDIR -type f -iname "*.txt")" -- $cur) ) + ;; + -w) + COMPREPLY=( $(compgen -W "$(find $WLDIR -type f -iname "*.txt")" -- $cur) ) + ;; + -z) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee payloads)" -- $cur)) + ;; + -e) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee registrants)" -- $cur)) + ;; + -m) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee iterators)" -- $cur)) + ;; + -o) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee printers)" -- $cur)) + ;; + --script-help) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee scripts)" -- $cur)) + ;; + --script) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee scripts)" -- $cur)) + ;; + --field) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee fields)" -- $cur)) + ;; + --zE) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee encoders)" -- $cur)) + ;; + -V) + COMPREPLY=( $( compgen -W "allvars allpost allheaders" -- $cur ) ) + ;; + -X) + COMPREPLY=( $( compgen -W "FUZZ OPTIONS PUT DELETE POST GET TRACE CONNECT HEAD" -- $cur ) ) + ;; + --hc) + COMPREPLY=( $( compgen -W "400 401 301 302 500 404 200" -- $cur ) ) + ;; + *) + COMPREPLY=($(compgen -W "$($WFUZZ_EX --ee options)" -- $cur)) + ;; + esac +} + +complete -F _wfuzz -o default ./wfuzz From d48f41d4a06e147205ead9a981f8d98c9521f61b Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 9 Mar 2019 15:40:10 +0100 Subject: [PATCH 044/119] rollback blacklist fields --- src/wfuzz/utils.py | 30 +++++++++++------------------- tests/test_acceptance.py | 1 + tests/test_clparser.py | 3 --- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 743aa935..b76df3cc 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -7,11 +7,6 @@ from chardet.universaldetector import UniversalDetector -blacklisted_fields = [ - "r.headers", - "r.params", - "r.cookies", -] allowed_fields = [ "description", "nres", @@ -20,15 +15,14 @@ "lines", "words", "md5", - 'l', - 'h', - 'w', - 'c', - 'r', - 'history', + "l", + "h", + "w", + "c", + "history", - 'url', - 'content', + "url", + "content", "history.url", "history.method", @@ -42,6 +36,7 @@ "history.headers", "history.params", + "r", "r.reqtime", "r.url", "r.method", @@ -51,9 +46,9 @@ "r.raw_content" "r.is_path", "r.pstrip", - "r.cookies", - "r.headers", - "r.params", + "r.cookies.", + "r.headers.", + "r.params.", ] @@ -226,9 +221,6 @@ def __call__(self): def _check_allowed_field(attr): - if attr in blacklisted_fields: - return False - if [field for field in allowed_fields if attr.startswith(field)]: return True return False diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 25a4a1a1..73cd4324 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -40,6 +40,7 @@ # script args testing_savedsession_tests = [ + ("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), ] testing_tests = [ diff --git a/tests/test_clparser.py b/tests/test_clparser.py index e8701ab0..a4180a2a 100644 --- a/tests/test_clparser.py +++ b/tests/test_clparser.py @@ -49,6 +49,3 @@ def test_ze_zd_option(self): options = CLParser(['wfuzz', '-z', 'range,0-1']).parse_cl() self.assertEqual(options.data['payloads'], [('range', {'default': '0-1', 'encoder': None}, None)]) - - - From c92290c03e900b6c3c79c63a14f65378ca2201a0 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 9 Mar 2019 19:56:45 +0100 Subject: [PATCH 045/119] improved encoding support in file payload --- src/wfuzz/plugins/payloads/file.py | 16 ++++-- src/wfuzz/utils.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/wfuzz/plugins/payloads/file.py b/src/wfuzz/plugins/payloads/file.py index a9b42fd0..f5f2084f 100644 --- a/src/wfuzz/plugins/payloads/file.py +++ b/src/wfuzz/plugins/payloads/file.py @@ -1,7 +1,7 @@ from wfuzz.externals.moduleman.plugin import moduleman_plugin from wfuzz.exception import FuzzExceptBadFile from wfuzz.plugin_api.base import BasePayload -from wfuzz.utils import open_file_detect_encoding +from wfuzz.utils import FileDetOpener @moduleman_plugin @@ -18,6 +18,8 @@ class file(BasePayload): 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."), ) default_parameter = "fn" @@ -26,23 +28,27 @@ def __init__(self, params): BasePayload.__init__(self, params) try: - self.f = open_file_detect_encoding(self.find_file(self.params["fn"])) + 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): - line = self.f.readline() + line = next(self.f) if not line: self.f.close() raise StopIteration return line.strip() def count(self): + if self.params["count"].lower() == 'false': + return -1 + if self.__count is None: - self.__count = len(self.f.readlines()) - self.f.seek(0) + self.__count = len(list(self.f)) + self.f.reset() return self.__count diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index b76df3cc..ec51442e 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -6,6 +6,8 @@ import functools from chardet.universaldetector import UniversalDetector +import chardet +from .exception import FuzzExceptInternalError allowed_fields = [ "description", @@ -180,6 +182,87 @@ def convert_to_unicode(text): 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 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 + + def open_file_detect_encoding(file_path): def detect_encoding(file_path): detector = UniversalDetector() From 5b6e026b27d8cf9ce7f4602cb83245d2a33cc881 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 9 Mar 2019 22:02:03 +0100 Subject: [PATCH 046/119] restablish all tests --- tests/test_acceptance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 73cd4324..25a4a1a1 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -40,7 +40,6 @@ # script args testing_savedsession_tests = [ - ("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), ] testing_tests = [ From 9cf4cdf77c7cc82c17ce7ff8915e5b04fa28f0af Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 9 Mar 2019 22:06:36 +0100 Subject: [PATCH 047/119] filter bug when param value is none --- src/wfuzz/utils.py | 4 ++-- tests/test_filterintro.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index ec51442e..81402ea3 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -385,7 +385,7 @@ def __getattr__(*args): def __add__(self, other): if isinstance(other, str): - return DotDict({k: v + other for k, v in self.items()}) + 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) @@ -394,4 +394,4 @@ def __add__(self, other): def __radd__(self, other): if isinstance(other, str): - return DotDict({k: other + v for k, v in self.items()}) + return DotDict({k: other + v for k, v in self.items() if v}) diff --git a/tests/test_filterintro.py b/tests/test_filterintro.py index c9df4e3a..37b9e5c9 100644 --- a/tests/test_filterintro.py +++ b/tests/test_filterintro.py @@ -88,6 +88,16 @@ def test_nonexisting(self): 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" From 37ac302337aa17855c07b6d2aea5ed8f9993b501 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 10 Mar 2019 00:16:38 +0100 Subject: [PATCH 048/119] fix api payload tests --- src/wfuzz/utils.py | 2 ++ tests/test_api.py | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 81402ea3..51870fe8 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -262,6 +262,8 @@ def detect_encoding(self): return detector.result + next = __next__ # for Python 2 + def open_file_detect_encoding(file_path): def detect_encoding(file_path): diff --git a/tests/test_api.py b/tests/test_api.py index 014c1e3c..4b40ff08 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -79,10 +79,29 @@ def test_payload(self): payload_list = list(wfuzz.payload(**{'payloads': [('dirwalk', {'default': 'foo', 'encoder': None}, None)]})) self.assertEqual(payload_list, [('baz',), ('bar/spam',), ('bar/eggs',)]) + class mock_file(object): + def __init__(self): + self.my_iter = iter([b"one", b"two"]) + + def __iter__(self): + return self + + def __next__(self): + return next(self.my_iter) + + def seek(self, pos): + self.my_iter = iter([b"one", b"two"]) + + next = __next__ # for Python 2 + + m = mock.MagicMock(name='open', spec=open) + m.return_value = iter([b"one", b"two"]) + m.return_value = mock_file() + mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" - with mock.patch(mocked_fun, mock.mock_open(read_data="one\ntwo\n")): + with mock.patch(mocked_fun, m): payload_list = list(wfuzz.payload(**{'payloads': [('file', {'default': 'mockedfile', 'encoder': None}, None)]})) - self.assertEqual(payload_list, [('one',), ('two',)]) + 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',)]) From aa940a04d493c848b72d9be0a4aa167a0cc9de6c Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 10 Mar 2019 00:49:21 +0100 Subject: [PATCH 049/119] CONNECT_TO warning --- src/wfuzz/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wfuzz/__init__.py b/src/wfuzz/__init__.py index 1f9af334..79ff961c 100644 --- a/src/wfuzz/__init__.py +++ b/src/wfuzz/__init__.py @@ -23,6 +23,9 @@ 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") + 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") + 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") From c4f89711e968743519b9f6c276012f6fb87cb9a2 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 10 Mar 2019 00:50:14 +0100 Subject: [PATCH 050/119] test_static_strquery_set_ip does not work on travis ci --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 25a4a1a1..1b0cf0b0 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -87,7 +87,7 @@ basic_tests = [ # different connect host ip - ("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), + # 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), From 10eaca17984c7fcf8eb8c4cb6e8684fa6bc3c91d Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 10 Mar 2019 13:06:40 +0100 Subject: [PATCH 051/119] add zD to wfpayload --- src/wfuzz/wfuzz.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index 903ddacf..d57b86bc 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -72,6 +72,7 @@ def usage(): \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 : Arguments for the specified payload (it must be preceded by -z or -w). \t--slice : Filter payload\'s elements using the specified expression. \t-w wordlist : Specify a wordlist file (alias for -z file,wordlist). @@ -84,7 +85,7 @@ def usage(): import getopt try: - opts, args = getopt.getopt(sys.argv[1:], "vhz:m:w:", ["field=", "help", "slice=", "zP="]) + opts, args = getopt.getopt(sys.argv[1:], "vhz:m:w:", ["field=", "help", "slice=", "zD=", "zP="]) except getopt.GetoptError as err: print((str(err))) usage() From 79defa8555255574a51dcbc6270e16e63eb84ba5 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 10 Mar 2019 21:29:03 +0100 Subject: [PATCH 052/119] update docs --- docs/user/advanced.rst | 8 +++++++- docs/user/basicusage.rst | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index d5e5eb3a..b335dd8e 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -293,7 +293,7 @@ Connect to an specific host The --ip option can be used to connect to a specific host and port instead of the URL's host and port:: - wfuzz -z range,1-1 --ip 127.0.0.1 http://www.google.com/anything/FUZZ + $ wfuzz -z range,1-1 --ip 127.0.0.1 http://www.google.com/anything/FUZZ This useful, for example, to test if a reverse proxy can be manipulated into misrouting requests to a destination of our choice. @@ -704,3 +704,9 @@ For example, the following will return a unique list of HTTP requests including $ 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 thefore 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 + 000000002: 200 118 L 455 W 5384 Ch "{'artist': '1'}" + ... diff --git a/docs/user/basicusage.rst b/docs/user/basicusage.rst index 94a369b7..31237bc8 100644 --- a/docs/user/basicusage.rst +++ b/docs/user/basicusage.rst @@ -253,3 +253,12 @@ 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 in using the default output you can also select what FuzzResult's field to show instead of the payload:: + + $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --field r + ... + 000000001: 200 99 L 272 W 3868 Ch GET /artists.php?artist=0 HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + User-Agent: Wfuzz/2.4 + Host: testphp.vulnweb.com + ... From a75cd9ee4b74d4c79d7f35fa0a98736a1f414c6d Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 10 Mar 2019 21:34:03 +0100 Subject: [PATCH 053/119] allow various --zD option --- src/wfuzz/ui/console/clparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 11b7048d..5bb4b775 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -325,7 +325,7 @@ 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 ["-z", "--zP", "--slice", "payload", "-w", "-b", "-H", "-p"] and len(optsd[i]) > 1] + opt_list = [i for i in optsd if i not in ["-z", "--zP", "--zD", "--slice", "payload", "-w", "-b", "-H", "-p"] 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)) From e23621883a02200b98aa73522a9b6538533deec2 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 10 Mar 2019 23:25:34 +0100 Subject: [PATCH 054/119] advanced use of filters values as boolean --- docs/user/advanced.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index b335dd8e..95e3f459 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -555,6 +555,11 @@ A more interesting variation of the above examples could be:: $ wfuzz -w fuzzdb/attack/xss/xss-rsnake.txt -d searchFor=FUZZ --filter "content~FUZZ" http://testphp.vulnweb.com/search.php?test=query +You can use the fields as boolean values as well. For example, this filter will show only the requests with parameters:: + + $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --filter 'r.params.all' + + Filtering a payload ^^^^^^^^^^^^^^^^^^^^^^^^^^ From 823285c014e12a4d16170199ba157c42770104cd Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 17 Mar 2019 23:38:23 +0100 Subject: [PATCH 055/119] fix host when url has port and no scheme --- src/wfuzz/fuzzobjects.py | 3 ++- tests/test_reqresp.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index f6a1ef32..2464ea9f 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -189,7 +189,8 @@ def url(self): @url.setter def url(self, u): - if not u.startswith("FUZ") and urlparse(u).scheme == "": + # 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 == "": diff --git a/tests/test_reqresp.py b/tests/test_reqresp.py index 5ddeec3a..5591b870 100644 --- a/tests/test_reqresp.py +++ b/tests/test_reqresp.py @@ -82,6 +82,21 @@ def test_seturl(self): self.assertEqual(fr.path, "FUZZ") self.assertEqual(fr.follow, False) + fr.url = "http://www.wfuzz.org:80/a" + self.assertEqual(fr.host, "www.wfuzz.org:80") + + fr.url = "https://www.wfuzz.org:80/a" + self.assertEqual(fr.host, "www.wfuzz.org:80") + + fr.url = "www.wfuzz.org:80/a" + self.assertEqual(fr.host, "www.wfuzz.org:80") + + fr.url = "www.wfuzz.org:80" + self.assertEqual(fr.host, "www.wfuzz.org:80") + + fr.url = "www.wfuzz.org" + self.assertEqual(fr.host, "www.wfuzz.org") + def test_setpostdata(self): fr = FuzzRequest() fr.url = "http://www.wfuzz.org/" @@ -119,6 +134,19 @@ def test_setpostdata(self): self.assertEqual(fr.method, "POST") self.assertEqual(fr.params.post, {'a': '1'}) + fr = FuzzRequest() + fr.url = "http://www.wfuzz.org/" + fr.params.post = "{'a': '1'}" + self.assertEqual(fr.method, "POST") + self.assertEqual(fr.params.post, {"{'a': '1'}": None}) + + fr = FuzzRequest() + fr.url = "http://www.wfuzz.org/" + fr.params.post = '1' + fr.headers.request = {'Content-Type': 'application/json'} + self.assertEqual(fr.method, "POST") + self.assertEqual(fr.params.post, {'1': None}) + def test_setgetdata(self): fr = FuzzRequest() From d9d95581e3871d201c2f13435ee762d59bbe89e1 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 19 Mar 2019 20:22:46 +0100 Subject: [PATCH 056/119] rename and fix acc test case --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 1b0cf0b0..9e552549 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -119,7 +119,7 @@ ("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", "%s/FUZZ?var=1&var2=2" % HTTPBIN_URL, [["anything"], ['PUT', 'GET', 'POST', 'DELETE']], dict(method='FUZ2Z', filter="content~'\"args\":{\"var\":\"1\",\"var2\":\"2\"}'"), [(200, '/anything')] * 4, None), + ("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), # 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), From 03632b11e7aed1f72720559995faa19a8e75a3e5 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 19 Mar 2019 21:00:52 +0100 Subject: [PATCH 057/119] fix postdata when no value or data --- src/wfuzz/fuzzobjects.py | 6 +++++- tests/test_acceptance.py | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 2464ea9f..d245d0bf 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -105,7 +105,7 @@ def post(self): def post(self, pp): if isinstance(pp, dict): for key, value in pp.items(): - self._req.setVariablePOST(key, str(value)) + self._req.setVariablePOST(key, str(value) if value is not None else value) elif isinstance(pp, str): self._req.setPostData(pp) @@ -310,6 +310,10 @@ def from_http_object(self, c, bh, 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 not self.params.post: + self.params.post = {'': None} + if raw_response: rp = Response() rp.parseResponse(raw_response, raw_content) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 9e552549..d06671e5 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -122,10 +122,10 @@ ("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), # 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 r.method='POST'"), [(200, '/anything')], 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_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), From a66acd0ac529e3a6a26265b2ca70e1932e5fd7d0 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 21 Mar 2019 22:14:43 +0100 Subject: [PATCH 058/119] parse post vars for any content type --- src/wfuzz/externals/reqresp/Request.py | 4 ++-- tests/test_acceptance.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/externals/reqresp/Request.py b/src/wfuzz/externals/reqresp/Request.py index 342716ab..9e8f8235 100644 --- a/src/wfuzz/externals/reqresp/Request.py +++ b/src/wfuzz/externals/reqresp/Request.py @@ -152,7 +152,7 @@ def __getattr__(self, name): elif self.ContentType == "multipart/form-data": return self.__variablesPOST.multipartEncoded() else: - return self.__uknPostData + return self.__variablesPOST.urlEncoded() else: raise AttributeError @@ -228,7 +228,7 @@ def setPostData(self, pd, boundary=None): elif self.ContentType == "multipart/form-data": self.__variablesPOST.parseMultipart(pd, boundary) else: - self.__uknPostData = pd + self.__variablesPOST.parseUrlEncoded(pd) ############################################################################ diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index d06671e5..6ecf0269 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -46,6 +46,8 @@ ] savedsession_tests = [ + ("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), + # 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), @@ -126,6 +128,9 @@ ("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$' 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), From 328957a3577eceeee277e0eda1548d3ddee1db0d Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 31 Mar 2019 22:19:40 +0200 Subject: [PATCH 059/119] req keeps content type as an internal var. update it when modified --- src/wfuzz/fuzzobjects.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index d245d0bf..aad30cdd 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -44,6 +44,8 @@ def request(self): @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): From d599319ac1ebb22dea53e56e9cfaf3752d25cca2 Mon Sep 17 00:00:00 2001 From: javi Date: Mon, 1 Apr 2019 00:28:25 +0200 Subject: [PATCH 060/119] add json support to Variables --- src/wfuzz/externals/reqresp/Request.py | 4 ++++ src/wfuzz/externals/reqresp/Variables.py | 14 ++++++++++++++ src/wfuzz/fuzzobjects.py | 5 +++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/externals/reqresp/Request.py b/src/wfuzz/externals/reqresp/Request.py index 9e8f8235..0b18ddba 100644 --- a/src/wfuzz/externals/reqresp/Request.py +++ b/src/wfuzz/externals/reqresp/Request.py @@ -151,6 +151,8 @@ def __getattr__(self, name): return self.__variablesPOST.urlEncoded() elif self.ContentType == "multipart/form-data": return self.__variablesPOST.multipartEncoded() + elif self.ContentType == 'application/json': + return self.__variablesPOST.json_encoded() else: return self.__variablesPOST.urlEncoded() else: @@ -227,6 +229,8 @@ def setPostData(self, pd, boundary=None): self.__variablesPOST.parseUrlEncoded(pd) elif self.ContentType == "multipart/form-data": self.__variablesPOST.parseMultipart(pd, boundary) + elif self.ContentType == 'application/json': + self.__variablesPOST.parse_json_encoded(pd) else: self.__variablesPOST.parseUrlEncoded(pd) diff --git a/src/wfuzz/externals/reqresp/Variables.py b/src/wfuzz/externals/reqresp/Variables.py index 90f0820b..1416f95f 100644 --- a/src/wfuzz/externals/reqresp/Variables.py +++ b/src/wfuzz/externals/reqresp/Variables.py @@ -1,4 +1,5 @@ from .TextParser import TextParser +import json class Variable: @@ -61,6 +62,19 @@ def getVariable(self, name): def urlEncoded(self): 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} + + return json.dumps(dicc) + + def parse_json_encoded(self, cad): + dicc = [] + + for key, value in json.loads(cad).items(): + dicc.append(Variable(key, value)) + + self.variables = dicc + def parseUrlEncoded(self, cad): dicc = [] diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index aad30cdd..8b9f7ffd 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -342,6 +342,9 @@ 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]) @@ -361,8 +364,6 @@ def update_from_options(self, options): if options['cookie']: self.cookies.request = options['cookie'] - self.headers.request = dict(options['headers']) - if options['allvars']: self.wf_allvars = options['allvars'] From 566b8aea4955731d5c569add316132100d825512 Mon Sep 17 00:00:00 2001 From: javi Date: Mon, 1 Apr 2019 21:40:51 +0200 Subject: [PATCH 061/119] parse json params fix exceptions --- src/wfuzz/externals/reqresp/Request.py | 29 ++++++++++++++++---------- src/wfuzz/filter.py | 5 ++++- src/wfuzz/fuzzobjects.py | 5 ++++- tests/test_acceptance.py | 3 +++ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/wfuzz/externals/reqresp/Request.py b/src/wfuzz/externals/reqresp/Request.py index 0b18ddba..259b44f8 100644 --- a/src/wfuzz/externals/reqresp/Request.py +++ b/src/wfuzz/externals/reqresp/Request.py @@ -51,6 +51,7 @@ def __init__(self): self.__variablesGET = VariablesSet() self.__variablesPOST = VariablesSet() + self._non_parsed_post = None # diccionario, por ejemplo headers["Cookie"] self._headers = { @@ -88,7 +89,7 @@ def __init__(self): @property def method(self): if self._method is None: - return "POST" if self.getPOSTVars() else "GET" + return "POST" if (self.getPOSTVars() or self._non_parsed_post is not None) else "GET" return self._method @@ -147,6 +148,9 @@ def __getattr__(self, name): elif name == "path": return self.__path elif name == "postdata": + if self._non_parsed_post is not None: + return self._non_parsed_post + if self.ContentType == "application/x-www-form-urlencoded": return self.__variablesPOST.urlEncoded() elif self.ContentType == "multipart/form-data": @@ -224,15 +228,18 @@ def getPOSTVars(self): return self.__variablesPOST.variables def setPostData(self, pd, boundary=None): - self.__variablesPOST = VariablesSet() - if self.ContentType == "application/x-www-form-urlencoded": - self.__variablesPOST.parseUrlEncoded(pd) - elif self.ContentType == "multipart/form-data": - self.__variablesPOST.parseMultipart(pd, boundary) - elif self.ContentType == 'application/json': - self.__variablesPOST.parse_json_encoded(pd) - else: - self.__variablesPOST.parseUrlEncoded(pd) + try: + self.__variablesPOST = VariablesSet() + if self.ContentType == "application/x-www-form-urlencoded": + self.__variablesPOST.parseUrlEncoded(pd) + elif self.ContentType == "multipart/form-data": + self.__variablesPOST.parseMultipart(pd, boundary) + elif self.ContentType == 'application/json': + self.__variablesPOST.parse_json_encoded(pd) + else: + self.__variablesPOST.parseUrlEncoded(pd) + except Exception: + self._non_parsed_post = pd ############################################################################ @@ -338,7 +345,7 @@ def to_pycurl_object(c, req): else: c.setopt(pycurl.CUSTOMREQUEST, req.method) - if req.getPOSTVars(): + if req.getPOSTVars() or req._non_parsed_post is not None: c.setopt(pycurl.POSTFIELDS, python2_3_convert_to_unicode(req.postdata)) c.setopt(pycurl.FOLLOWLOCATION, 1 if req.followLocation else 0) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index d7eb1f19..8dcc12ce 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -109,7 +109,10 @@ def set_baseline(self, res): def __compute_res_value(self, tokens): self.stack.append(tokens[0]) - return rgetattr(self.res, 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] diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 8b9f7ffd..d4ea270c 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -101,7 +101,10 @@ def get(self, values): @property def post(self): - return DotDict(OrderedDict([(x.name, x.value) for x in self._req.getPOSTVars()])) + if self._req._non_parsed_post is None: + return DotDict(OrderedDict([(x.name, x.value) for x in self._req.getPOSTVars()])) + else: + return self._req.postdata @post.setter def post(self, pp): diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 6ecf0269..f2f21caf 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -46,7 +46,10 @@ ] 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), # field fuzz values ("test_desc_fuzz", "-z range,1-1 {}/FUZZ".format(HTTPBIN_URL), "-z wfuzzp,$$PREVFILE$$ FUZZ", ["http://localhost:9000/1"], None), From e070ef8cb1d998d2a0c657697904b93d8b494a5b Mon Sep 17 00:00:00 2001 From: javi Date: Mon, 1 Apr 2019 21:52:31 +0200 Subject: [PATCH 062/119] replace external URL for internal web server --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index f2f21caf..4a480745 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -161,7 +161,7 @@ ("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", [["http://webscantest.com/datastore/search_get_by_name.php?name=Rake"]], dict(), [(200, '/datastore/search_get_by_name.php')], 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), From 171eac59b83141b861cdf4d2c88b10c30a90e1e6 Mon Sep 17 00:00:00 2001 From: javi Date: Mon, 1 Apr 2019 22:52:26 +0200 Subject: [PATCH 063/119] add some use cases to docs --- docs/user/advanced.rst | 2 ++ docs/user/basicusage.rst | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 95e3f459..70f259ae 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -485,6 +485,8 @@ is_path Returns true when the HTTP request path refers to a reqtime Returns the total time that HTTP request took to be retrieved ============================ ============================================= +It is worth noting that Wfuzz will try to parse the POST parameters according to the specified content type header. Currently, application/x-www-form-urlencoded, multipart/form-dat and application/json are supported. + FuzzRequest URL field is broken in smaller (read only) parts using the urlparse Python's module in the urlp attribute. Urlparse parses a URL into: scheme://netloc/path;parameters?query#fragment. For example, for the "http://www.google.com/dir/test.php?id=1" URL you can get the following values: diff --git a/docs/user/basicusage.rst b/docs/user/basicusage.rst index 31237bc8..ad4db796 100644 --- a/docs/user/basicusage.rst +++ b/docs/user/basicusage.rst @@ -252,8 +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 in using the default output you can also select what FuzzResult's field to show instead of the payload:: +When in using the default output you can also select what FuzzResult's field to show instead of the payload description:: $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --field r ... @@ -262,3 +261,6 @@ When in using the default output you can also select what FuzzResult's field to User-Agent: Wfuzz/2.4 Host: testphp.vulnweb.com ... + + +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. From b72d43143b0bb31500741ac409e25b850809e01e Mon Sep 17 00:00:00 2001 From: javi Date: Mon, 1 Apr 2019 23:34:45 +0200 Subject: [PATCH 064/119] no-cache option --- docs/library/guide.rst | 1 + docs/user/advanced.rst | 25 +++++++++++++++++++++++++ src/wfuzz/fuzzqueues.py | 4 ++-- src/wfuzz/options.py | 4 ++++ src/wfuzz/ui/console/clparser.py | 5 ++++- src/wfuzz/ui/console/common.py | 1 + 6 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/library/guide.rst b/docs/library/guide.rst index 72cc1059..06ff8901 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -19,6 +19,7 @@ CLI Option Library Option -Z scanmode=True --req-delay N req_delay=0 --conn-delay N conn_delay=0.0 +--no-cache no_cache=True --script= script="plugins" --script-args n1=v1,... script_args={n1: v1} -m iterator iterator="iterator" diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 70f259ae..575a3ec4 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -241,6 +241,31 @@ An example, parsing a "robots.txt" file is shown below:: Filtered Requests: 0 Requests/sec.: 0 +In order to not scan the same requests (with the same parameters) over an over again, there is a cache,the cache can be disabled with the --no-cache flag. + +For example, if we target a web server with the same URL but different parameter values, we get:: + + $ wfuzz -z range --zD 0-3 -z list --zD "'" -u http://testphp.vulnweb.com/artists.php?artist=FUZZFUZ2Z -A + + 000000004: 0.195s 200 101 L 287 W 3986 Ch nginx/1.4.1 "3 - '" + |_ Error identified: Warning: mysql_fetch_array() + 000000001: 0.198s 200 101 L 287 W 3986 Ch nginx/1.4.1 "0 - '" + 000000002: 0.198s 200 101 L 287 W 3986 Ch nginx/1.4.1 "1 - '" + 000000003: 0.198s 200 101 L 287 W 3986 Ch nginx/1.4.1 "2 - '" + +But, if we do the same but disabling the cache:: + + $ wfuzz -z range --zD 0-3 -z list --zD "'" -u http://testphp.vulnweb.com/artists.php?artist=FUZZFUZ2Z -A --no-cache + + 000000004: 1.170s 200 101 L 287 W 3986 Ch nginx/1.4.1 "3 - '" + |_ Error identified: Warning: mysql_fetch_array() + 000000002: 1.173s 200 101 L 287 W 3986 Ch nginx/1.4.1 "1 - '" + |_ Error identified: Warning: mysql_fetch_array() + 000000001: 1.174s 200 101 L 287 W 3986 Ch nginx/1.4.1 "0 - '" + |_ Error identified: Warning: mysql_fetch_array() + 000000003: 1.173s 200 101 L 287 W 3986 Ch nginx/1.4.1 "2 - '" + |_ Error identified: Warning: mysql_fetch_array() + Custom scripts ^^^^^^^^^^^^^^ diff --git a/src/wfuzz/fuzzqueues.py b/src/wfuzz/fuzzqueues.py index 37064c1c..e49c7bd3 100644 --- a/src/wfuzz/fuzzqueues.py +++ b/src/wfuzz/fuzzqueues.py @@ -188,7 +188,7 @@ def get_name(self): def process(self, res): # process request through plugins if not res.exception: - if self.cache.update_cache(res.history, "processed"): + if self.options['no_cache'] or self.cache.update_cache(res.history, "processed"): plugins_res_queue = Queue() @@ -212,7 +212,7 @@ def process(self, res): self._throw(FuzzExceptPluginError(item.issue)) res.plugins_res.append(item) elif item.plugintype == PluginItem.backfeed: - if self.cache.update_cache(item.fuzzitem.history, "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!") diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 9c841201..d043da20 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -87,6 +87,7 @@ def _defaults(self): script_args={}, connect_to_ip=None, description=None, + no_cache=False, # this is equivalent to payloads but in a different format dictio=None, @@ -114,6 +115,9 @@ def validate(self): if self.data['script'] and self.data['dryrun']: return "Bad usage: Plugins cannot work without making any HTTP request." + if self.data['no_cache'] not in [True, False]: + return "Bad usage: No-cache is a boolean value" + if not self.data['url']: return "Bad usage: You must specify an URL." diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 5bb4b775..cea648c5 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -15,7 +15,7 @@ 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 = ['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'] +long_opts = ['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'] class CLParser: @@ -573,6 +573,9 @@ def _parse_options(self, optsd, options): if "--prev" in optsd: options["previous"] = True + if "--no-cache" in optsd: + options["no_cache"] = True + if "-c" in optsd: options["colour"] = True diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index 0492605e..28121010 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -110,6 +110,7 @@ \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--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 \t--script-help= : Show help about scripts. From 9cda86e5915221e8095da2bdb6bb954800572994 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 2 Apr 2019 00:08:38 +0200 Subject: [PATCH 065/119] change lib code snippets to terminate session --- docs/library/guide.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/library/guide.rst b/docs/library/guide.rst index 06ff8901..1d904832 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -68,7 +68,7 @@ A FuzzSession object has all the methods of the main wfuzz API. The FuzzSession object allows you to persist certain parameters across fuzzing sessions:: >>> import wfuzz - >>> s=wfuzz.FuzzSession(url="http://testphp.vulnweb.com/FUZZ") + >>> s = wfuzz.FuzzSession(url="http://testphp.vulnweb.com/FUZZ") >>> for r in s.fuzz(hc=[404], payloads=[("file",dict(fn="wordlist/general/common.txt"))]): ... print r ... @@ -95,7 +95,8 @@ The get_payload function generates a Wfuzz payload from a Python iterable. It is Generating a new payload and start fuzzing is really simple:: >>> import wfuzz - >>> for r in wfuzz.get_payload(range(5)).fuzz(url="http://testphp.vulnweb.com/FUZZ"): + >>> s = wfuzz.get_payload(range(5)) + >>> for r in s.fuzz(url="http://testphp.vulnweb.com/FUZZ"): ... print r ... 00012: C=404 7 L 12 W 168 Ch "0" @@ -103,12 +104,13 @@ Generating a new payload and start fuzzing is really simple:: 00014: C=404 7 L 12 W 168 Ch "2" 00015: C=404 7 L 12 W 168 Ch "3" 00016: C=404 7 L 12 W 168 Ch "4" - >>> + >>> s.close() The get_payloads method can be used when various payloads are needed:: >>> import wfuzz - >>> for r in wfuzz.get_payloads([range(5), ["a","b"]]).fuzz(url="http://testphp.vulnweb.com/FUZZ/FUZ2Z"): + >>> s = wfuzz.get_payloads([range(5), ["a","b"]]) + >>> for r in s.fuzz(url="http://testphp.vulnweb.com/FUZZ/FUZ2Z"): ... print r ... 00028: C=404 7 L 12 W 168 Ch "4 - b" @@ -121,7 +123,7 @@ The get_payloads method can be used when various payloads are needed:: 00020: C=404 7 L 12 W 168 Ch "0 - b" 00023: C=404 7 L 12 W 168 Ch "2 - a" 00019: C=404 7 L 12 W 168 Ch "0 - a" - >>> + >>> s.close() Get session =========== @@ -130,7 +132,8 @@ The get_session function generates a Wfuzz session object from the specified com $ python >>> import wfuzz - >>> for r in wfuzz.get_session("-z range,0-10 http://testphp.vulnweb.com/FUZZ").fuzz(): + >>> s = wfuzz.get_session("-z range,0-10 http://testphp.vulnweb.com/FUZZ") + >>> for r in s.fuzz(): ... print r ... 00002: C=404 7 L 12 W 168 Ch "1" @@ -144,4 +147,4 @@ The get_session function generates a Wfuzz session object from the specified com 00007: C=404 7 L 12 W 168 Ch "6" 00009: C=404 7 L 12 W 168 Ch "8" 00010: C=404 7 L 12 W 168 Ch "9" - + >>> s.close() From 5d87ad4bde0fdb7b02dede8379ae4e27ece43dd6 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 2 Apr 2019 22:19:26 +0200 Subject: [PATCH 066/119] plugins introspection field --- docs/user/advanced.rst | 4 ++++ src/wfuzz/filter.py | 25 ++++++++++++++++++------- src/wfuzz/fuzzobjects.py | 10 ++++++++++ src/wfuzz/utils.py | 8 ++++++++ tests/test_acceptance.py | 2 ++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 575a3ec4..519ff265 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -477,6 +477,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']} ============ ============== ============================================= FuzzRequest object's attribute (you need to use the r. prefix) such as: @@ -586,6 +587,9 @@ You can use the fields as boolean values as well. For example, this filter will $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --filter 'r.params.all' +Results with plugin issues can be filter as well:: + + $ wfuzz -z list --zD index -u http://testphp.vulnweb.com/FUZZ.php --script headers --filter "'nginx'~plugins" Filtering a payload ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 8dcc12ce..b348a452 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -1,7 +1,7 @@ from .exception import FuzzExceptIncorrectFilter, FuzzExceptBadOptions, FuzzExceptInternalError, FuzzException from .fuzzobjects import FuzzResult -from .utils import rgetattr, rsetattr +from .utils import rgetattr, rsetattr, value_in_any_list_item import re import collections @@ -216,20 +216,31 @@ def __compute_expr(self, tokens): elif exp_operator == "=~": regex = re.compile(rightvalue, re.MULTILINE | re.DOTALL) return regex.search(leftvalue) is not None - elif exp_operator == "!~": - return rightvalue.lower() not in leftvalue.lower() - elif exp_operator == "~": - return rightvalue.lower() in leftvalue.lower() + elif exp_operator in ["!~", "~"]: + ret = True + + if isinstance(rightvalue, str): + ret = rightvalue.lower() in leftvalue.lower() + elif isinstance(rightvalue, list): + ret = value_in_any_list_item(leftvalue, rightvalue) + elif isinstance(rightvalue, dict): + return len({k: v for (k, v) in rightvalue.items() if leftvalue.lower() in k.lower() or value_in_any_list_item(leftvalue, 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 regex expression used in filter: %s" % str(e)) + raise FuzzExceptBadOptions("Invalid operand types used in expression: %s" % str(e)) except ParseException as e: - raise FuzzExceptBadOptions("Invalid regex expression used in filter: %s" % str(e)) + raise FuzzExceptBadOptions("Invalid filter: %s" % str(e)) return True diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index d4ea270c..98670d4c 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -15,6 +15,7 @@ from threading import Lock from collections import namedtuple from collections import OrderedDict +from collections import defaultdict from .externals.reqresp import Request, Response from .exception import FuzzExceptBadAPI, FuzzExceptBadOptions, FuzzExceptInternalError @@ -700,6 +701,15 @@ def __init__(self, history=None, exception=None, track_id=True): self._description = None + @property + def plugins(self): + dic = defaultdict(list) + + for pl in self.plugins_res: + dic[pl.source].append(pl.issue) + + return dic + def update(self, exception=None): self.type = FuzzResult.result diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 51870fe8..650f0588 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -22,6 +22,7 @@ "w", "c", "history", + "plugins", "url", "content", @@ -397,3 +398,10 @@ def __add__(self, other): 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/tests/test_acceptance.py b/tests/test_acceptance.py index 4a480745..cd2f77da 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -208,6 +208,7 @@ ("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="'value1'~r.params.get"), [(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), @@ -228,6 +229,7 @@ # 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="'unicorn'~plugins"), [(200, '/anything')], None), ] scanmode_tests = [ From f024291d940989273ac2d12cf158817f39e3df8c Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 3 Apr 2019 00:19:14 +0200 Subject: [PATCH 067/119] added interacting with res to docs --- docs/library/guide.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/library/guide.rst b/docs/library/guide.rst index 1d904832..d28a1ff6 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -148,3 +148,25 @@ The get_session function generates a Wfuzz session object from the specified com 00009: C=404 7 L 12 W 168 Ch "8" 00010: C=404 7 L 12 W 168 Ch "9" >>> s.close() + +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:: + + $ python + >>> import wfuzz + + >>> with wfuzz.get_session("-z list --zD test -u http://testphp.vulnweb.com/userinfo.php -d uname=FUZZ&pass=FUZZ") as s: + ... for r in s.fuzz(): + ... print(r.history.cookies.response) + ... print(r.history.params.all) + ... print(r.history.params.post) + ... print(r.history.params.post.uname) + ... print(r.history.params.post['pass']) + {'login': 'test%2Ftest'} + {'uname': 'test', 'pass': 'test'} + {'uname': 'test', 'pass': 'test'} + test + test + >>> From 230c44d3bc87f2bd881181e030c1663b5265e403 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 3 Apr 2019 19:56:57 +0200 Subject: [PATCH 068/119] wrong order in operator ~ --- src/wfuzz/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index b348a452..d519a44b 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -220,7 +220,7 @@ def __compute_expr(self, tokens): ret = True if isinstance(rightvalue, str): - ret = rightvalue.lower() in leftvalue.lower() + ret = leftvalue.lower() in rightvalue.lower() elif isinstance(rightvalue, list): ret = value_in_any_list_item(leftvalue, rightvalue) elif isinstance(rightvalue, dict): From d831648ce4fcdbaa7c112a6f591c53229f796ee1 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 3 Apr 2019 20:02:13 +0200 Subject: [PATCH 069/119] pretty print cookies,params,headers --- src/wfuzz/fuzzobjects.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 98670d4c..402f10e5 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -14,7 +14,6 @@ from threading import Lock from collections import namedtuple -from collections import OrderedDict from collections import defaultdict from .externals.reqresp import Request, Response @@ -31,16 +30,20 @@ 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 DotDict(OrderedDict(self._req.response.getHeaders())) if self._req.response else {} + return headers.header(self._req.response.getHeaders()) if self._req.response else {} @property def request(self): - return DotDict(OrderedDict([x.split(": ", 1) for x in self._req.getHeaders()])) + return headers.header([x.split(": ", 1) for x in self._req.getHeaders()]) @request.setter def request(self, values_dict): @@ -50,10 +53,14 @@ def request(self, values_dict): @property def all(self): - return self.request + self.response + 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 @@ -62,7 +69,7 @@ def response(self): if self._req.response: c = self._req.response.getCookie().split("; ") if c[0]: - return DotDict(OrderedDict([[x[0], x[2]] for x in [x.partition("=") for x in c]])) + return cookies.cookie([[x[0], x[2]] for x in [x.partition("=") for x in c]]) return {} @@ -71,7 +78,7 @@ def request(self): if 'Cookie' in self._req._headers: c = self._req._headers['Cookie'].split("; ") if c[0]: - return DotDict(OrderedDict([[x[0], x[2]] for x in [x.partition("=") for x in c]])) + return cookies.cookie([[x[0], x[2]] for x in [x.partition("=") for x in c]]) return {} @@ -81,16 +88,20 @@ def request(self, values): @property def all(self): - return self.request + self.response + 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 DotDict(OrderedDict([(x.name, x.value) for x in self._req.getGETVars()])) + return params.param([(x.name, x.value) for x in self._req.getGETVars()]) @get.setter def get(self, values): @@ -103,7 +114,7 @@ def get(self, values): @property def post(self): if self._req._non_parsed_post is None: - return DotDict(OrderedDict([(x.name, x.value) for x in self._req.getPOSTVars()])) + return params.param([(x.name, x.value) for x in self._req.getPOSTVars()]) else: return self._req.postdata @@ -117,7 +128,7 @@ def post(self, pp): @property def all(self): - return self.get + self.post + return params.param(self.get + self.post) @all.setter def all(self, values): From 279a60d3f1156f1886fd7d4d2d0552b42ad045e2 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 3 Apr 2019 21:05:55 +0200 Subject: [PATCH 070/119] language docs errors --- docs/user/advanced.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 519ff265..9dd6e741 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -459,8 +459,8 @@ value|upper() uppercase of the value value|encode('encoder', 'value') value|e('enc', 'val') Returns encoder.encode(value) value|decode('decoder', 'value') value|d('dec', 'val') Returns encoder.decode(value) value|replace('what', 'with') value|r('what', 'with') Returns value replacing what for with -value|unique(value) value|u(value) Returns True if a value is unique. -value|startswith('value') value|sw('param') Returns true if the value string starts with param +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 ================================ ======================= ============================================= * When a FuzzResult is available, you could perform runtime introspection of the objects using the following symbols From 269459eb085d64bab3ef56267df656c9937d6eea Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 3 Apr 2019 21:52:53 +0200 Subject: [PATCH 071/119] gre operator --- docs/user/advanced.rst | 1 + src/wfuzz/filter.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 9dd6e741..b3491a25 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -461,6 +461,7 @@ value|decode('decoder', 'value') value|d('dec', 'val') Returns encoder.decode( value|replace('what', 'with') value|r('what', 'with') Returns value replacing what for with 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 ================================ ======================= ============================================= * When a FuzzResult is available, you could perform runtime introspection of the objects using the following symbols diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index d519a44b..33b50165 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -35,7 +35,7 @@ def __init__(self, ffilter=None, filter_string=None): basic_primitives = int_values | quoted_str_value - operator_names = oneOf("m d e un u r l sw unique startswith decode encode unquote replace lower upper").setParseAction(lambda s, l, t: [(l, t[0])]) + 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(")")) @@ -160,6 +160,7 @@ def __compute_bbb_value(self, tokens): def __compute_perl_value(self, tokens): leftvalue, exp = tokens + # import pdb; pdb.set_trace() if exp: loc_op, middlevalue, rightvalue = exp @@ -179,6 +180,12 @@ def __compute_perl_value(self, tokens): return leftvalue.upper() elif op == "lower" or op == "l": return leftvalue.lower() + elif op == 'gregex' or op == "gre": + regex = re.compile(middlevalue, re.MULTILINE | re.DOTALL) + search_res = regex.search(leftvalue) + 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": From 5175c13a9da58d58f5fed111cd39e1860800d210 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 3 Apr 2019 21:53:35 +0200 Subject: [PATCH 072/119] allow FUZZ in filter only as well --- src/wfuzz/core.py | 2 +- src/wfuzz/options.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index d3b95f5d..04ac3839 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -113,7 +113,7 @@ def restart(self, seed): self.dictio = self.get_dictio() def _check_dictio_len(self, element): - fuzz_words = self.options["compiled_prefilter"].get_fuzz_words() + self.get_fuzz_words() + 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!") diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index d043da20..31c3a137 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -270,7 +270,7 @@ def compile(self): self.data["compiled_genreq"] = requestGenerator(self) # Check payload num - fuzz_words = self.data["compiled_prefilter"].get_fuzz_words() + self.data["compiled_genreq"].get_fuzz_words() + fuzz_words = self.data["compiled_filter"].get_fuzz_words() + self.data["compiled_prefilter"].get_fuzz_words() + self.data["compiled_genreq"].get_fuzz_words() if self.data['allvars'] is None and len(set(fuzz_words)) == 0: raise FuzzExceptBadOptions("You must specify at least a FUZZ word!") From b6e89591e43bef73f3f7fb8e114541499dc4cc30 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 3 Apr 2019 23:44:32 +0200 Subject: [PATCH 073/119] rollback ~ operator order --- docs/user/advanced.rst | 2 +- src/wfuzz/filter.py | 12 ++++++------ tests/test_acceptance.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index b3491a25..2a106495 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -590,7 +590,7 @@ You can use the fields as boolean values as well. For example, this filter will Results with plugin issues can be filter as well:: - $ wfuzz -z list --zD index -u http://testphp.vulnweb.com/FUZZ.php --script headers --filter "'nginx'~plugins" + $ wfuzz -z list --zD index -u http://testphp.vulnweb.com/FUZZ.php --script headers --filter "plugins~'nginx'" Filtering a payload ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 33b50165..1e6a3212 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -226,12 +226,12 @@ def __compute_expr(self, tokens): elif exp_operator in ["!~", "~"]: ret = True - if isinstance(rightvalue, str): - ret = leftvalue.lower() in rightvalue.lower() - elif isinstance(rightvalue, list): - ret = value_in_any_list_item(leftvalue, rightvalue) - elif isinstance(rightvalue, dict): - return len({k: v for (k, v) in rightvalue.items() if leftvalue.lower() in k.lower() or value_in_any_list_item(leftvalue, v)}) > 0 + 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)) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index cd2f77da..b6ac72cf 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -208,7 +208,7 @@ ("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="'value1'~r.params.get"), [(200, '/echo')], 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), @@ -229,7 +229,7 @@ # 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="'unicorn'~plugins"), [(200, '/anything')], None), + ("test_plugins_filter", "%s/FUZZ" % HTTPBIN_URL, [["anything"]], dict(script='headers', filter="plugins~'unicorn'"), [(200, '/anything')], None), ] scanmode_tests = [ From 626372314abf31baf0b9c8166fa34bead3ccafee Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 4 Apr 2019 00:07:35 +0200 Subject: [PATCH 074/119] gre operator test cases --- src/wfuzz/filter.py | 9 +++++++-- tests/test_acceptance.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 1e6a3212..e00c76df 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -181,8 +181,13 @@ def __compute_perl_value(self, tokens): elif op == "lower" or op == "l": return leftvalue.lower() elif op == 'gregex' or op == "gre": - regex = re.compile(middlevalue, re.MULTILINE | re.DOTALL) - search_res = regex.search(leftvalue) + 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) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index b6ac72cf..e824247a 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -125,6 +125,7 @@ ("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), # 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), From 7076b37074299fe253ab165e55630bee2528c9ed Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 6 Apr 2019 00:10:15 +0200 Subject: [PATCH 075/119] Removing unnecesary code due to FuzzResFilterSlice --- src/wfuzz/filter.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index e00c76df..8d4c703d 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -117,14 +117,9 @@ def __compute_res_value(self, tokens): def _compute_fuzz_symbol(self, tokens): i = tokens[0] - if isinstance(self.res, FuzzResult): - try: - return self.res.payload[i].content - except IndexError: - raise FuzzExceptIncorrectFilter("Non existent FUZZ payload! Use a correct index.") - elif isinstance(self.res, str) and i == 0: - return self.res - else: + 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): From a9c9b64d9b4c838a68dd5b62acd6ca1a79c83807 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 6 Apr 2019 00:17:17 +0200 Subject: [PATCH 076/119] remove FuzzResult import from filter --- src/wfuzz/facade.py | 2 ++ src/wfuzz/filter.py | 14 ++++++-------- src/wfuzz/fuzzobjects.py | 6 ++---- src/wfuzz/options.py | 28 ++++++++++++++-------------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/wfuzz/facade.py b/src/wfuzz/facade.py index eacb7241..5aa5c254 100644 --- a/src/wfuzz/facade.py +++ b/src/wfuzz/facade.py @@ -10,6 +10,8 @@ # python2 and 3: metaclass from future.utils import with_metaclass +ERROR_CODE = -1 +BASELINE_CODE = -2 class Settings(SettingsBase): def get_config_file(self): diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 8d4c703d..3604d5bf 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -1,6 +1,4 @@ from .exception import FuzzExceptIncorrectFilter, FuzzExceptBadOptions, FuzzExceptInternalError, FuzzException -from .fuzzobjects import FuzzResult - from .utils import rgetattr, rsetattr, value_in_any_list_item import re @@ -13,7 +11,7 @@ except ImportError: from urllib import unquote -from .facade import Facade +from .facade import Facade, ERROR_CODE, BASELINE_CODE PYPARSING = True @@ -95,13 +93,13 @@ def __init__(self, ffilter=None, filter_string=None): self._cache = collections.defaultdict(set) def set_baseline(self, res): - if FuzzResult.BASELINE_CODE in self.hideparams['lines']: + if BASELINE_CODE in self.hideparams['lines']: self.hideparams['lines'].append(res.lines) - if FuzzResult.BASELINE_CODE in self.hideparams['codes']: + if BASELINE_CODE in self.hideparams['codes']: self.hideparams['codes'].append(res.code) - if FuzzResult.BASELINE_CODE in self.hideparams['words']: + if BASELINE_CODE in self.hideparams['words']: self.hideparams['words'].append(res.words) - if FuzzResult.BASELINE_CODE in self.hideparams['chars']: + if BASELINE_CODE in self.hideparams['chars']: self.hideparams['chars'].append(res.chars) self.baseline = res @@ -200,7 +198,7 @@ def __compute_perl_value(self, tokens): return ret def __compute_xxx_value(self, tokens): - return FuzzResult.ERROR_CODE + return ERROR_CODE def __compute_expr(self, tokens): leftvalue, exp_operator, rightvalue = tokens[0] diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 402f10e5..91c6078f 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -18,7 +18,7 @@ from .externals.reqresp import Request, Response from .exception import FuzzExceptBadAPI, FuzzExceptBadOptions, FuzzExceptInternalError -from .facade import Facade +from .facade import Facade, ERROR_CODE from .mixins import FuzzRequestUrlMixing, FuzzRequestSoupMixing from .utils import python2_3_convert_to_unicode @@ -686,8 +686,6 @@ def __str__(self): class FuzzResult: seed, backfeed, result, error, startseed, endseed, cancel, discarded = list(range(8)) newid = itertools.count(0) - ERROR_CODE = -1 - BASELINE_CODE = -2 def __init__(self, history=None, exception=None, track_id=True): self.history = history @@ -777,7 +775,7 @@ def code(self): # elif not self.history.code: # return 0 else: - return FuzzResult.ERROR_CODE + return ERROR_CODE @property def timer(self): diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 31c3a137..ceb72d44 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -1,7 +1,7 @@ from .exception import FuzzExceptBadRecipe, FuzzExceptBadOptions, FuzzExceptBadFile -from .facade import Facade +from .facade import Facade, ERROR_CODE, BASELINE_CODE -from .fuzzobjects import FuzzResult, FuzzStats +from .fuzzobjects import FuzzStats from .filter import FuzzResFilter from .core import requestGenerator from .utils import ( @@ -250,15 +250,15 @@ def compile(self): self.data["compiled_printer"] = Facade().printers.get_plugin(printer)(filename) try: - self.data['hc'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['hc']] - self.data['hw'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['hw']] - self.data['hl'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['hl']] - self.data['hh'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['hh']] - - self.data['sc'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['sc']] - self.data['sw'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['sw']] - self.data['sl'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['sl']] - self.data['sh'] = [FuzzResult.BASELINE_CODE if i == "BBB" else FuzzResult.ERROR_CODE if i == "XXX" else int(i) for i in self.data['sh']] + 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']] except ValueError: raise FuzzExceptBadOptions("Bad options: Filter must be specified in the form of [int, ... , int, BBB, XXX].") @@ -275,9 +275,9 @@ def compile(self): if self.data['allvars'] is None and len(set(fuzz_words)) == 0: raise FuzzExceptBadOptions("You must specify at least a FUZZ word!") - if self.data["compiled_genreq"].baseline is None and (FuzzResult.BASELINE_CODE in self.data['hc'] or - FuzzResult.BASELINE_CODE in self.data['hl'] or FuzzResult.BASELINE_CODE in self.data['hw'] or - FuzzResult.BASELINE_CODE in self.data['hh']): + 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["script"]: From d9f0cd9606475d075087ef13352184bdf68b6b6d Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 6 Apr 2019 19:09:04 +0200 Subject: [PATCH 077/119] --field as an expression --- docs/library/guide.rst | 5 +++++ docs/user/basicusage.rst | 4 ++-- src/wfuzz/facade.py | 1 + src/wfuzz/fuzzobjects.py | 13 ++++++++++--- src/wfuzz/options.py | 4 ---- src/wfuzz/ui/console/common.py | 2 +- src/wfuzz/ui/console/mvc.py | 6 ++---- tests/test_acceptance.py | 7 +++---- 8 files changed, 24 insertions(+), 18 deletions(-) diff --git a/docs/library/guide.rst b/docs/library/guide.rst index d28a1ff6..14bda172 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -170,3 +170,8 @@ Once a Wfuzz result is available the grammar defined in the filter language can test test >>> + +The result object has also a method to evaluate a language expression:: + + >> print(r.eval("r.cookies.response")) + login=test%2Ftest diff --git a/docs/user/basicusage.rst b/docs/user/basicusage.rst index ad4db796..ac5ec878 100644 --- a/docs/user/basicusage.rst +++ b/docs/user/basicusage.rst @@ -252,11 +252,11 @@ 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 in using the default output you can also select what FuzzResult's field to show instead of the payload description:: +When in using the default output you can also select an additional FuzzResult's field to show together with the payload description:: $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --field r ... - 000000001: 200 99 L 272 W 3868 Ch GET /artists.php?artist=0 HTTP/1.1 + 000000001: 200 99 L 272 W 3868 Ch 0 | GET /artists.php?artist=0 HTTP/1.1 Content-Type: application/x-www-form-urlencoded User-Agent: Wfuzz/2.4 Host: testphp.vulnweb.com diff --git a/src/wfuzz/facade.py b/src/wfuzz/facade.py index 5aa5c254..0f470eaf 100644 --- a/src/wfuzz/facade.py +++ b/src/wfuzz/facade.py @@ -13,6 +13,7 @@ ERROR_CODE = -1 BASELINE_CODE = -2 + class Settings(SettingsBase): def get_config_file(self): return os.path.join(utils.get_home(check=True), "wfuzz.ini") diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 91c6078f..25bc27ff 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -16,6 +16,7 @@ from collections import namedtuple from collections import defaultdict +from .filter import FuzzResFilter from .externals.reqresp import Request, Response from .exception import FuzzExceptBadAPI, FuzzExceptBadOptions, FuzzExceptInternalError from .facade import Facade, ERROR_CODE @@ -748,9 +749,6 @@ def __str__(self): @property def description(self): - if self._description: - return str(rgetattr(self, self._description)) - 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]) @@ -759,6 +757,15 @@ def description(self): return ret_str + def get_full_description(self): + if self._description is not None: + return "{} | {}".format(self.description, self.eval(self._description)) + + return self.description + + def eval(self, expr): + return FuzzResFilter(filter_string=expr).is_visible(self) + # parameters in common with fuzzrequest @property def content(self): diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index ceb72d44..4d68d304 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -7,7 +7,6 @@ from .utils import ( json_minify, python2_3_convert_from_unicode, - _check_allowed_field ) from .core import Fuzzer @@ -130,9 +129,6 @@ def validate(self): if self.data["rlevel"] < 0: return "Bad usage: Recursion level must be a positive int." - if self.data["description"] and not _check_allowed_field(self.data["description"]): - return "Bad usage: '{}' is not a valid field.".format(self.data['description']) - 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.") diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index 28121010..9eed45c1 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -95,7 +95,7 @@ \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--field : Show the specified FuzzResult field instead of the current payload +\t--field : Show the specified language expression together with the current payload \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. diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index b397be0e..eca6ebfb 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -1,8 +1,6 @@ import sys from collections import defaultdict import threading -import operator -from functools import reduce try: from itertools import zip_longest except ImportError: @@ -177,7 +175,7 @@ def _print_verbose(self, res, print_nres=True): ("%d Ch" % res.chars, txt_colour), (server, txt_colour), (location, txt_colour), - ("\"%s\"" % str(res.description), txt_colour), + ("\"%s\"" % res.get_full_description(), txt_colour), ] self.term.set_colour(txt_colour) @@ -218,7 +216,7 @@ def _print(self, res, print_nres=True): ("%d L" % res.lines, txt_colour), ("%d W" % res.words, txt_colour), ("%d Ch" % res.chars, txt_colour), - ("\"%s\"" % str(res.description), txt_colour), + ("\"%s\"" % res.get_full_description(), txt_colour), ] self.term.set_colour(txt_colour) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index e824247a..49bd4258 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -8,7 +8,6 @@ import wfuzz - LOCAL_DOMAIN = "http://localhost" URL_LOCAL = "%s:8000/dir" % (LOCAL_DOMAIN) HTTPD_PORT = 8000 @@ -86,7 +85,7 @@ # 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), ] @@ -376,11 +375,11 @@ def test(self): # first session with wfuzz.get_session(prev_session_cli) as s: - ret_list = [x.description for x in s.fuzz(save=filename)] + ret_list = [x.eval(x._description) if x._description 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.description for x in s.fuzz()] + ret_list = [x.eval(x._description) if x._description else x.description for x in s.fuzz()] self.assertEqual(sorted(ret_list), sorted(expected_list)) From 72c5badfdf70b0645b66e4b7e2addee5d51bf97d Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 6 Apr 2019 19:25:50 +0200 Subject: [PATCH 078/119] update docs --- docs/user/advanced.rst | 2 +- docs/user/basicusage.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 2a106495..0205052e 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -401,7 +401,7 @@ These timeouts are really handy when you are using Wfuzz to bruteforce resources Filter Language --------------- -Wfuzz's filter language grammar is build using `pyparsing `_, therefore it must be installed before using the command line parameters "--filter, --prefilter, --slice". +Wfuzz's filter language grammar is build using `pyparsing `_, therefore it must be installed before using the command line parameters "--filter, --prefilter, --slice, --field". The information about the filter language can be also obtained executing:: diff --git a/docs/user/basicusage.rst b/docs/user/basicusage.rst index ac5ec878..3dece545 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 in 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 an additional FuzzResult's field to show together with the payload description:: $ wfuzz -z range --zD 0-1 -u http://testphp.vulnweb.com/artists.php?artist=FUZZ --field r ... From 5845abafcad61b89774e5a6ca20cf2743e8b47b5 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 6 Apr 2019 19:58:46 +0200 Subject: [PATCH 079/119] change filter of test_all_params_post --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 49bd4258..a50411c1 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -181,7 +181,7 @@ # 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~'POST_DATA=a=onevalue&b=2' or content~'POST_DATA=a=1&b=onevalue'"), [(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), From aa95f5c51f5ddcf57d47d92de86e63c51cc9729d Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 6 Apr 2019 22:14:11 +0200 Subject: [PATCH 080/119] change filter of test_static_postdata3_set2 --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index a50411c1..7df307f3 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -132,7 +132,7 @@ ("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$' and content=~'command=POST$' and content~'Content-Type: aaaa'"), [(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), From c25c25f8087b57368291802b5ce1f0fdfd8c0eef Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 6 Apr 2019 22:19:16 +0200 Subject: [PATCH 081/119] change filter of test_static_postdata3_set2 --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 7df307f3..3a21cd58 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -132,7 +132,7 @@ ("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_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), From 31009862d814cd9756cb3f0bf11f0365adaa9494 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 10:22:22 +0200 Subject: [PATCH 082/119] reqresp: check if there's rawbody before processing response --- src/wfuzz/externals/reqresp/Response.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/wfuzz/externals/reqresp/Response.py b/src/wfuzz/externals/reqresp/Response.py index b5d1c012..db2be4f3 100644 --- a/src/wfuzz/externals/reqresp/Response.py +++ b/src/wfuzz/externals/reqresp/Response.py @@ -175,6 +175,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): else: self._headers = [] + # TODO: this might add to rawbody not directly to __content while tp.skip(1): self.addContent(tp.lastFull_line) @@ -216,11 +217,12 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): rawbody = deflated_data self.delHeader("Content-Encoding") - # Try to get charset encoding from headers - content_encoding = get_encoding_from_headers(dict(self.getHeaders())) + if rawbody is not None: + # Try to get charset encoding from headers + content_encoding = get_encoding_from_headers(dict(self.getHeaders())) - # fallback to default encoding - if content_encoding is None: - content_encoding = "utf-8" + # fallback to default encoding + 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')) From c73b73d14e37d852cae8128cdecc4da8cf909a80 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 10:23:30 +0200 Subject: [PATCH 083/119] add close to filedetopener --- src/wfuzz/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 650f0588..312fc56d 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -209,6 +209,9 @@ def __init__(self, file_path, encoding=None): self.det_encoding = encoding self.encoding_forced = False + def close(self): + self.file_des.close() + def reset(self): self.file_des.seek(0) From a9475115c12fcb56856e9f95d77170dbfa422b44 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 10:26:46 +0200 Subject: [PATCH 084/119] default to url if there's no payload associated to a fr object --- src/wfuzz/fuzzobjects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 25bc27ff..858c3ed8 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -749,6 +749,9 @@ def __str__(self): @property def description(self): + if not self.payload: + return self.url + 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]) From 921370a8a53ca8869099881afb0d9803e4aeedea Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 10:31:35 +0200 Subject: [PATCH 085/119] use wfuzz in the bash completion script --- wfuzz_bash_completion | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wfuzz_bash_completion b/wfuzz_bash_completion index f30f708b..f0f9861a 100644 --- a/wfuzz_bash_completion +++ b/wfuzz_bash_completion @@ -7,7 +7,7 @@ _wfuzz() { local cur prev cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} - WFUZZ_EX="./wfuzz" + WFUZZ_EX="wfuzz" # Change to your wordlists' base directory WLDIR=$($WFUZZ_EX --ee files) @@ -63,4 +63,4 @@ _wfuzz() { esac } -complete -F _wfuzz -o default ./wfuzz +complete -F _wfuzz -o default wfuzz From 02ae8b93a07a1ed8e6c78f0096c7578ee6c01c7b Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 11:22:17 +0200 Subject: [PATCH 086/119] use lastline in parse response to ignore CRLF lines --- src/wfuzz/externals/reqresp/Request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wfuzz/externals/reqresp/Request.py b/src/wfuzz/externals/reqresp/Request.py index 259b44f8..6e5f77d0 100644 --- a/src/wfuzz/externals/reqresp/Request.py +++ b/src/wfuzz/externals/reqresp/Request.py @@ -453,8 +453,9 @@ def parseRequest(self, rawRequest, prot="http"): self.setUrl(prot + "://" + self._headers["Host"] + pathTMP) pd = "" + # TODO: hacky, might need to change tp.readline returning read bytes instead while tp.readLine(): - pd += tp.lastFull_line + pd += tp.lastline if pd: boundary = None From d3fc07f807495749288b2b3f0c1f99a1fadb106f Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 21:08:06 +0200 Subject: [PATCH 087/119] alert about burp state files not available anymore --- src/wfuzz/plugins/payloads/burpstate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wfuzz/plugins/payloads/burpstate.py b/src/wfuzz/plugins/payloads/burpstate.py index 0e851a95..7bf2872f 100644 --- a/src/wfuzz/plugins/payloads/burpstate.py +++ b/src/wfuzz/plugins/payloads/burpstate.py @@ -21,6 +21,8 @@ class burpstate(BasePayload): author = ("Xavi Mendez (@xmendez)", ) version = "0.1" description = ( + "*ALERT*: https://portswigger.net/blog/goodbye-state-files-we-wont-miss-you", + "", "Returns fuzz results' from a Burp saved state file. This payload's code is based on burp2xml.py:", "Developed by Paul Haas, under Redspin. Inc.", "Licensed under the GNU Public License version 3.0 (2008-2009)", From 0bceb1b7fe009741603e39b7132b7ad99ccc8887 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 21:41:59 +0200 Subject: [PATCH 088/119] return dotdic when {} in cookies --- src/wfuzz/fuzzobjects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 858c3ed8..ad44dc16 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -72,7 +72,7 @@ def response(self): if c[0]: return cookies.cookie([[x[0], x[2]] for x in [x.partition("=") for x in c]]) - return {} + return cookies.cookie({}) @property def request(self): @@ -81,7 +81,7 @@ def request(self): if c[0]: return cookies.cookie([[x[0], x[2]] for x in [x.partition("=") for x in c]]) - return {} + return cookies.cookie({}) @request.setter def request(self, values): From f46009f2de35438e57b3345c865b26a87173ba07 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 9 Apr 2019 23:14:14 +0200 Subject: [PATCH 089/119] forgot |_ in -v for --prev --- src/wfuzz/ui/console/mvc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index eca6ebfb..5bc23a29 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -167,7 +167,7 @@ def _print_verbose(self, res, print_nres=True): server = res.history.headers.response['Server'] rows = [ - ("%09d:" % res.nres if print_nres else "", txt_colour), + ("%09d:" % res.nres if print_nres else " |_", txt_colour), ("%.3fs" % res.timer, txt_colour), ("C=%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), From f7435a7bacc39e1c7a974a0472c87d3153cc6509 Mon Sep 17 00:00:00 2001 From: psytester Date: Wed, 10 Apr 2019 00:01:19 +0200 Subject: [PATCH 090/119] Null byte injection added This works for some HTTP server --- wordlist/vulns/dirTraversal-nix.txt | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/wordlist/vulns/dirTraversal-nix.txt b/wordlist/vulns/dirTraversal-nix.txt index f0a4ca47..b2a88a86 100644 --- a/wordlist/vulns/dirTraversal-nix.txt +++ b/wordlist/vulns/dirTraversal-nix.txt @@ -845,3 +845,27 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA .\\..\\.\\..\\.\\..\\.\\..\\.\\..\\.\\..\\/etc/passwd .\\..\\.\\..\\.\\..\\.\\..\\.\\..\\.\\..\\.\\..\\/etc/passwd .\\..\\.\\..\\.\\..\\.\\..\\.\\..\\.\\..\\.\\..\\.\\..\\/etc/passwd +%00../etc/passwd +%00../%00../etc/passwd +%00../%00../%00../etc/passwd +%00../%00../%00../%00../etc/passwd +%00../%00../%00../%00../%00../etc/passwd +%00../%00../%00../%00../%00../%00../etc/passwd +%00../%00../%00../%00../%00../%00../%00../etc/passwd +%00../%00../%00../%00../%00../%00../%00../%00../etc/passwd +.%00./etc/passwd +.%00./.%00./etc/passwd +.%00./.%00./.%00./etc/passwd +.%00./.%00./.%00./.%00./etc/passwd +.%00./.%00./.%00./.%00./.%00./etc/passwd +.%00./.%00./.%00./.%00./.%00./.%00./etc/passwd +.%00./.%00./.%00./.%00./.%00./.%00./.%00./etc/passwd +.%00./.%00./.%00./.%00./.%00./.%00./.%00./.%00./etc/passwd +..%00/etc/passwd +..%00/..%00/etc/passwd +..%00/..%00/..%00/etc/passwd +..%00/..%00/..%00/..%00/etc/passwd +..%00/..%00/..%00/..%00/..%00/etc/passwd +..%00/..%00/..%00/..%00/..%00/..%00/etc/passwd +..%00/..%00/..%00/..%00/..%00/..%00/..%00/etc/passwd +..%00/..%00/..%00/..%00/..%00/..%00/..%00/..%00/etc/passwd From ae012c5cb96b066a7a627f55a83c916c764ab3fc Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 10 Apr 2019 17:02:17 +0200 Subject: [PATCH 091/119] wfpayload, fix non fuzzres payload --- src/wfuzz/wfuzz.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index d57b86bc..22b1647b 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -9,6 +9,8 @@ from .ui.console.common import help_banner2 from .ui.console.clparser import CLParser +from .fuzzobjects import FuzzResult + def main(): kb = None @@ -105,8 +107,7 @@ def usage(): try: session_options = CLParser(sys.argv).parse_cl() - printer = View(session_options) - printer.header(None) + printer = None for res in payload(**session_options): if len(res) > 1: @@ -114,10 +115,18 @@ def usage(): else: r = res[0] - r._description = field + # TODO: option to not show headers in fuzzres + # TODO: all should be same object + if isinstance(r, FuzzResult): + if printer is None: + printer = View(session_options) + printer.header(None) - printer.result(r) - print("") + if field: + r._description = field + printer.result(r) + else: + print(r) except KeyboardInterrupt: pass From 7268d0f7be1f1b6a5c109b68ef16cced1c40b2f8 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 10 Apr 2019 18:50:20 +0200 Subject: [PATCH 092/119] graceful exit for payloads --- src/wfuzz/core.py | 7 +++++++ src/wfuzz/options.py | 12 ++++++++++-- src/wfuzz/plugin_api/base.py | 3 +++ src/wfuzz/wfuzz.py | 14 +++++++++++--- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index 04ac3839..14c223e3 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -97,6 +97,7 @@ 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) @@ -175,6 +176,10 @@ def __next__(self): 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): @@ -190,6 +195,7 @@ 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"]]: @@ -205,6 +211,7 @@ def __next__(self): 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) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 4d68d304..cb44253f 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -194,7 +194,9 @@ def export_json(self): def payload(self, **kwargs): self.data.update(kwargs) - return requestGenerator(self).get_dictio() + # TODO: maybe compile here? or think how to make work and not deadlock from the api + self.data['compiled_genreq'] = requestGenerator(self) + return self.data['compiled_genreq'].get_dictio() def fuzz(self, **kwargs): self.data.update(kwargs) @@ -290,6 +292,12 @@ def compile(self): return self def close(self): - self.http_pool.deregister() + if self.http_pool: + self.http_pool.deregister() + if self.fz: self.fz.cancel_job() + + # TODO: deadlock when using api.payload() + elif self.data['compiled_genreq'] is not None: + self.data['compiled_genreq'].close() diff --git a/src/wfuzz/plugin_api/base.py b/src/wfuzz/plugin_api/base.py index cebe6a60..34b91c05 100644 --- a/src/wfuzz/plugin_api/base.py +++ b/src/wfuzz/plugin_api/base.py @@ -121,6 +121,9 @@ def count(self): def __iter__(self): raise FuzzExceptPluginError("Method iter not implemented") + def close(self): + pass + def find_file(self, name): if os.path.exists(name): return name diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index 22b1647b..86da450d 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -82,7 +82,8 @@ def usage(): \t--field : Show a FuzzResult field instead of current payload """) - from .api import payload + # TODO: from .api import payload + from .api import FuzzSession from .exception import FuzzExceptBadOptions import getopt @@ -105,18 +106,22 @@ def usage(): if o in ("--field"): field = value + session = None + try: session_options = CLParser(sys.argv).parse_cl() + # TODO: api.payload will block with new shodanp + session = FuzzSession(**session_options) printer = None - for res in payload(**session_options): + for res in session.payload(): if len(res) > 1: raise FuzzExceptBadOptions("wfpayload can only be used to generate one word dictionaries") else: r = res[0] # TODO: option to not show headers in fuzzres - # TODO: all should be same object + # TODO: all should be same object type and no need for isinstance if isinstance(r, FuzzResult): if printer is None: printer = View(session_options) @@ -134,6 +139,9 @@ def usage(): print(("\nFatal exception: %s" % str(e))) except Exception as e: print(("\nUnhandled exception: %s" % str(e))) + finally: + if session: + session.close() def main_encoder(): From a39e792b418234cd822c87996be4fb6e35cf4d05 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 10 Apr 2019 18:51:58 +0200 Subject: [PATCH 093/119] counter to return value on operations --- src/wfuzz/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/utils.py b/src/wfuzz/utils.py index 312fc56d..5300bab8 100644 --- a/src/wfuzz/utils.py +++ b/src/wfuzz/utils.py @@ -295,14 +295,15 @@ def __init__(self, count=0): self._mutex = Lock() def inc(self): - self._operation(1) + return self._operation(1) def dec(self): - self._operation(-1) + return self._operation(-1) def _operation(self, dec): with self._mutex: self._count += dec + return self._count def __call__(self): with self._mutex: From 6231727edc7a28c24c2852fd3b22c1fba69f240e Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 10 Apr 2019 18:53:22 +0200 Subject: [PATCH 094/119] shodan payload --- src/wfuzz/facade.py | 5 +- src/wfuzz/plugin_api/payloadtools.py | 112 ++++++++++++++++++++++++++ src/wfuzz/plugins/payloads/shodanp.py | 51 ++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/wfuzz/plugins/payloads/shodanp.py diff --git a/src/wfuzz/facade.py b/src/wfuzz/facade.py index 0f470eaf..a1cd80b7 100644 --- a/src/wfuzz/facade.py +++ b/src/wfuzz/facade.py @@ -20,7 +20,10 @@ def get_config_file(self): def set_defaults(self): return dict( - plugins=[("bing_apikey", '')], + plugins=[ + ("bing_apikey", ''), + ("shodan_apikey", '') + ], kbase=[("discovery.blacklist", '.svg-.css-.js-.jpg-.gif-.png-.jpeg-.mov-.avi-.flv-.ico')], connection=[ ("concurrent", '10'), diff --git a/src/wfuzz/plugin_api/payloadtools.py b/src/wfuzz/plugin_api/payloadtools.py index 212d2a4c..a0c9a92c 100644 --- a/src/wfuzz/plugin_api/payloadtools.py +++ b/src/wfuzz/plugin_api/payloadtools.py @@ -1,5 +1,7 @@ from wfuzz.exception import FuzzExceptMissingAPIKey, FuzzExceptResourceParseError from wfuzz.facade import Facade +from wfuzz.utils import MyCounter + # Python 2 and 3: alternative 4 try: @@ -13,6 +15,18 @@ # python 2 and 3: iterator from builtins import object +from threading import Thread +from queue import Queue + +import shodan + +# 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'}, + ] +} class BingIter(object): @@ -115,3 +129,101 @@ def __next__(self): return elem.encode('utf-8') else: return elem + + +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 not key: + raise FuzzExceptMissingAPIKey("A Shodan api key is needed. Please check ~/.wfuzz/wfuzz.ini") + + self.api = shodan.Shodan(key) + self._dork = dork + self._page = MyCounter(page) + + self.results_queue = Queue(self.MAX_ENQUEUED_RES) + self.page_queue = Queue() + + self._threads = [] + + self._started = False + self._cancel_job = False + + def _do_search(self): + while 1: + page = self.page_queue.get() + if page is None: + self.page_queue.task_done() + break + + if self._cancel_job: + self.page_queue.task_done() + continue + + try: + results = self.api.search(self._dork, page=page) + for item in results['matches']: + if not self._cancel_job: + self.results_queue.put(item) + + self.page_queue.task_done() + if not self._cancel_job: + self.page_queue.put(self._page.inc()) + except shodan.APIError as e: + self.page_queue.task_done() + self.results_queue.put(e) + continue + + def __iter__(self): + return 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))) + self._threads.append(worker) + worker.start() + + self.page_queue.put(self._page()) + if not self.SLOW_START: + for _ in range(self.NUM_OF_WORKERS - 1): + self.page_queue.put(self._page.inc()) + + def _stop(self): + self._cancel_job = True + + for th in self._threads: + self.page_queue.put(None) + + self.page_queue.join() + + for th in self._threads: + th.join() + + self._threads = [] + + self.results_queue.put(None) + self._cancel_job = False + self._started = False + + def __next__(self): + if not self._started: + self._start() + self._started = True + + res = self.results_queue.get() + self.results_queue.task_done() + + if res is None: + self._stop() + raise StopIteration + elif isinstance(res, Exception): + self._stop() + raise res + + return res diff --git a/src/wfuzz/plugins/payloads/shodanp.py b/src/wfuzz/plugins/payloads/shodanp.py new file mode 100644 index 00000000..5b39ab9d --- /dev/null +++ b/src/wfuzz/plugins/payloads/shodanp.py @@ -0,0 +1,51 @@ +from wfuzz.externals.moduleman.plugin import moduleman_plugin +from wfuzz.plugin_api.payloadtools import ShodanIter +from wfuzz.plugin_api.base import BasePayload + + +@moduleman_plugin +class shodanp(BasePayload): + name = "shodanp" + author = ("Xavi Mendez (@xmendez)",) + version = "0.1" + description = ( + "Queries the Shodan API", + ) + + summary = "Returns hostnames or IPs of a given Shodan API search (needs api key)." + category = ["default"] + priority = 99 + + parameters = ( + ("search", "", True, "Shodan search string."), + ("page", "0", False, "Offset page, starting at zero."), + # TODO: ("limit", "0", False, "Number of results (1 query credit = 100 results). Zero for all."), + ) + + default_parameter = "search" + + def __init__(self, params): + BasePayload.__init__(self, params) + + search = params["search"] + page = int(params["page"]) + limit = int(params["limit"]) + + 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): + match = next(self._it) + if match['hostnames']: + for hostname in match['hostnames']: + return hostname + else: + return match['ip_str'] From 97f4cc8e46d3dac298209cbf06bca1a675cd70f0 Mon Sep 17 00:00:00 2001 From: javi Date: Fri, 12 Apr 2019 03:55:31 +0200 Subject: [PATCH 095/119] remove c= from code in view --- src/wfuzz/ui/console/mvc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index 5bc23a29..81711db3 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -169,7 +169,7 @@ def _print_verbose(self, res, print_nres=True): rows = [ ("%09d:" % res.nres if print_nres else " |_", txt_colour), ("%.3fs" % res.timer, txt_colour), - ("C=%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), @@ -212,7 +212,7 @@ def _print(self, res, print_nres=True): rows = [ ("%09d:" % res.nres if print_nres else " |_", txt_colour), - ("C=%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), From f61797d5e21ad2481eb80654e2f7bf5e0b4dd88e Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 14 Apr 2019 10:15:08 +0200 Subject: [PATCH 096/119] exec banner in recipe output --- src/wfuzz/ui/console/clparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index cea648c5..eff571fc 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -7,7 +7,7 @@ from wfuzz.facade import Facade from wfuzz.options import FuzzSession from wfuzz.exception import FuzzException, FuzzExceptBadOptions, FuzzExceptBadInstall -from .common import help_banner +from .common import help_banner, exec_banner from .common import usage from .common import brief_usage from .common import verbose_usage @@ -231,7 +231,7 @@ def parse_cl(self): raise FuzzExceptBadOptions(error) options.export_to_file(optsd["--dump-recipe"][0]) - print(help_banner) + print(exec_banner) print("Recipe written to %s." % (optsd["--dump-recipe"][0],)) sys.exit(0) From 62527e25ec26048d6a7bebbd6e34777453674353 Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 14 Apr 2019 11:04:45 +0200 Subject: [PATCH 097/119] recipe to warn no to fail --- src/wfuzz/options.py | 28 ++++++++++++++++------------ src/wfuzz/ui/console/clparser.py | 11 +++++++---- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index cb44253f..4c1da5cc 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -105,29 +105,31 @@ def update(self, options): self.data.update(options) def validate(self): + error_list = [] + if self.data['dictio'] and self.data['payloads']: - return "Bad usage: Dictio and payloads options are mutually exclusive. Only one could be specified." + 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']: - return "Bad usage: Recursion cannot work without making any HTTP request." + error_list.append("Bad usage: Recursion cannot work without making any HTTP request.") if self.data['script'] and self.data['dryrun']: - return "Bad usage: Plugins cannot work without making any HTTP request." + error_list.append("Bad usage: Plugins cannot work without making any HTTP request.") if self.data['no_cache'] not in [True, False]: - return "Bad usage: No-cache is a boolean value" + raise FuzzExceptBadOptions("Bad usage: No-cache is a boolean value") if not self.data['url']: - return "Bad usage: You must specify an URL." + error_list.append("Bad usage: You must specify an URL.") if not self.data['payloads'] and not self.data["dictio"]: - return "Bad usage: You must specify a payload." + error_list.append("Bad usage: You must specify a payload.") if self.data["hs"] and self.data["ss"]: - return "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: - return "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.") @@ -140,14 +142,16 @@ def validate(self): 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]: - return "Bad usage: Hide and show filters flags are mutually exclusive. Only one group could be specified." + 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']: - return "Bad usage: Advanced and filter flags are mutually exclusive. Only one could be specified." + raise FuzzExceptBadOptions("Bad usage: Advanced and filter flags are mutually exclusive. Only one could be specified.") except TypeError: - return "Bad options: Filter must be specified in the form of [int, ... , int]." + raise FuzzExceptBadOptions("Bad options: Filter must be specified in the form of [int, ... , int].") + + return error_list def export_to_file(self, filename): try: @@ -232,7 +236,7 @@ def compile(self): # Validate options error = self.validate() if error: - raise FuzzExceptBadOptions(error) + raise FuzzExceptBadOptions(error[0]) self.data["seed_payload"] = True if self.data["url"] == "FUZZ" else False diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index eff571fc..08ca762d 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -226,12 +226,15 @@ def parse_cl(self): self._parse_scripts(optsd, options) if "--dump-recipe" in optsd: - error = options.validate() - if error: - raise FuzzExceptBadOptions(error) + print(exec_banner) + + for error_msg in options.validate(): + print("WARNING: {}".format(error_msg)) + + if error_msg: + print("") options.export_to_file(optsd["--dump-recipe"][0]) - print(exec_banner) print("Recipe written to %s." % (optsd["--dump-recipe"][0],)) sys.exit(0) From cb509e80bd17e2041de7a6e79844e234c5d84629 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 16 Apr 2019 00:04:33 +0200 Subject: [PATCH 098/119] efield/field cli options --- docs/user/advanced.rst | 10 ++++-- src/wfuzz/fuzzobjects.py | 17 ++++++---- src/wfuzz/options.py | 3 +- src/wfuzz/ui/console/clparser.py | 8 ++++- src/wfuzz/ui/console/common.py | 3 +- src/wfuzz/ui/console/mvc.py | 4 +-- src/wfuzz/wfuzz.py | 29 ++++++++++------ tests/test_api.py | 57 +++++++++++++++++++++++++++++++- 8 files changed, 107 insertions(+), 24 deletions(-) diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 0205052e..b5cd7865 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -401,7 +401,7 @@ These timeouts are really handy when you are using Wfuzz to bruteforce resources Filter Language --------------- -Wfuzz's filter language grammar is build using `pyparsing `_, therefore it must be installed before using the command line parameters "--filter, --prefilter, --slice, --field". +Wfuzz's filter language grammar is build using `pyparsing `_, therefore it must be installed before using the command line parameters "--filter, --prefilter, --slice, --field and --efield". The information about the filter language can be also obtained executing:: @@ -745,5 +745,11 @@ Authtoken is the parameter used by BEA WebLogic Commerce Servers (TM) as a CSRF You can also select the field to show, for example:: $ wfpayload -z wfuzzp --zD /tmp/session --field r.params.get - 000000002: 200 118 L 455 W 5384 Ch "{'artist': '1'}" + artist=5 + ... + +Or:: + + $ wfpayload -z wfuzzp --zD /tmp/session --efield r.params.get + 000000006: 200 99 L 272 W 3868 Ch "5 | artist=5" ... diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index ad44dc16..75d3e270 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -710,6 +710,7 @@ def __init__(self, history=None, exception=None, track_id=True): self.payload = [] self._description = None + self._show_field = False @property def plugins(self): @@ -747,8 +748,7 @@ def __str__(self): else: return "Control result, type: %s" % ("seed", "backfeed", "result", "error", "startseed", "endseed", "cancel", "discarded")[self.type] - @property - def description(self): + def _payload_description(self): if not self.payload: return self.url @@ -760,11 +760,14 @@ def description(self): return ret_str - def get_full_description(self): - if self._description is not None: - return "{} | {}".format(self.description, self.eval(self._description)) + @property + def description(self): + if self._show_field is True: + return self.eval(self._description) + elif self._show_field is False and self._description is not None: + return "{} | {}".format(self._payload_description(), self.eval(self._description)) - return self.description + return self._payload_description() def eval(self, expr): return FuzzResFilter(filter_string=expr).is_visible(self) @@ -814,11 +817,13 @@ def from_soft_copy(self, track_id=True): 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'] @staticmethod def to_new_exception(exception): diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 4c1da5cc..cccd1f3b 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -28,7 +28,7 @@ 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"] + self.keys_not_to_dump = ["interactive", "recipe", "seed_payload", "send_discarded", "compiled_genreq", "compiled_filter", "compiled_prefilter", "compiled_printer", "description", "show_field"] # recipe must be superseded by options if "recipe" in kwargs and kwargs["recipe"]: @@ -87,6 +87,7 @@ def _defaults(self): connect_to_ip=None, description=None, no_cache=False, + show_field=None, # this is equivalent to payloads but in a different format dictio=None, diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 08ca762d..a83a823f 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -15,7 +15,7 @@ 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 = ['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'] +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'] class CLParser: @@ -484,6 +484,12 @@ def _parse_seed(self, url, optsd, options): if "--field" in optsd: options['description'] = optsd["--field"][0] + options["show_field"] = True + elif "--efield" in optsd: + options['description'] = optsd["--efield"][0] + options["show_field"] = False + else: + options["show_field"] = None if "--ip" in optsd: splitted = optsd["--ip"][0].partition(":") diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index 9eed45c1..f6ae4c04 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -95,7 +95,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--field : Show the specified language expression together with the current payload +\t--efield : Show the specified language expression together with the current payload +\t--field : Do not show the payload but only the specified language expression \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. diff --git a/src/wfuzz/ui/console/mvc.py b/src/wfuzz/ui/console/mvc.py index 81711db3..dac7070e 100644 --- a/src/wfuzz/ui/console/mvc.py +++ b/src/wfuzz/ui/console/mvc.py @@ -175,7 +175,7 @@ def _print_verbose(self, res, print_nres=True): ("%d Ch" % res.chars, txt_colour), (server, txt_colour), (location, txt_colour), - ("\"%s\"" % res.get_full_description(), txt_colour), + ("\"%s\"" % res.description, txt_colour), ] self.term.set_colour(txt_colour) @@ -216,7 +216,7 @@ def _print(self, res, print_nres=True): ("%d L" % res.lines, txt_colour), ("%d W" % res.words, txt_colour), ("%d Ch" % res.chars, txt_colour), - ("\"%s\"" % res.get_full_description(), txt_colour), + ("\"%s\"" % res.description, txt_colour), ] self.term.set_colour(txt_colour) diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index 86da450d..6d4330ff 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -79,7 +79,8 @@ def usage(): \t--slice : 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 : Show a FuzzResult field instead of current payload +\t--field : Do not show the payload but the specified language expression +\t--efield : Show the specified language expression together with the current payload """) # TODO: from .api import payload @@ -88,7 +89,7 @@ def usage(): import getopt try: - opts, args = getopt.getopt(sys.argv[1:], "vhz:m:w:", ["field=", "help", "slice=", "zD=", "zP="]) + 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() @@ -99,12 +100,17 @@ def usage(): sys.exit() 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 session = None @@ -120,16 +126,19 @@ def usage(): else: r = res[0] - # TODO: option to not show headers in fuzzres # TODO: all should be same object type and no need for isinstance if isinstance(r, FuzzResult): - if printer is None: - printer = View(session_options) - printer.header(None) - - if field: - r._description = field - printer.result(r) + 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) diff --git a/tests/test_api.py b/tests/test_api.py index 4b40ff08..38e92f58 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,13 @@ import unittest import sys +from io import BytesIO +import gzip +import pickle as pickle import wfuzz +from wfuzz.facade import Facade +from wfuzz.fuzzobjects import FuzzRequest +from wfuzz.fuzzobjects import FuzzResult try: # Python >= 3.3 @@ -64,6 +70,56 @@ def test_get_session(self): 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): + fr = FuzzRequest() + fr.url = "http://www.wfuzz.org/path?param=1¶m2=2" + fuzz_res = FuzzResult(history=fr) + fuzz_res._description = description + fuzz_res._show_field = show_field + + self.outfile = BytesIO() + + with gzip.GzipFile(fileobj=self.outfile, mode="wb") as f: + pickle.dump(fuzz_res, f) + + self.outfile.seek(0) + self.outfile.name = "mockfile" + + def close(self): + pass + + def read(self, pos): + return self.outfile.read(pos) + + # 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) + + 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" + 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" + 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']) + 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',)]) @@ -95,7 +151,6 @@ def seek(self, pos): next = __next__ # for Python 2 m = mock.MagicMock(name='open', spec=open) - m.return_value = iter([b"one", b"two"]) m.return_value = mock_file() mocked_fun = "builtins.open" if sys.version_info >= (3, 0) else "__builtin__.open" From fa28eb2b35f48908224594dbdbe6a7a2ff42c787 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 16 Apr 2019 00:17:25 +0200 Subject: [PATCH 099/119] limit in shodanp --- src/wfuzz/plugin_api/payloadtools.py | 6 ++++++ src/wfuzz/plugins/payloads/shodanp.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/wfuzz/plugin_api/payloadtools.py b/src/wfuzz/plugin_api/payloadtools.py index a0c9a92c..f2b7f826 100644 --- a/src/wfuzz/plugin_api/payloadtools.py +++ b/src/wfuzz/plugin_api/payloadtools.py @@ -145,6 +145,7 @@ def __init__(self, dork, page, limit): self.api = shodan.Shodan(key) self._dork = dork self._page = MyCounter(page) + self._page_limit = limit self.results_queue = Queue(self.MAX_ENQUEUED_RES) self.page_queue = Queue() @@ -165,6 +166,11 @@ def _do_search(self): self.page_queue.task_done() continue + if page > self._page_limit: + self.page_queue.task_done() + self.results_queue.put(None) + continue + try: results = self.api.search(self._dork, page=page) for item in results['matches']: diff --git a/src/wfuzz/plugins/payloads/shodanp.py b/src/wfuzz/plugins/payloads/shodanp.py index 5b39ab9d..f3605681 100644 --- a/src/wfuzz/plugins/payloads/shodanp.py +++ b/src/wfuzz/plugins/payloads/shodanp.py @@ -19,7 +19,7 @@ class shodanp(BasePayload): parameters = ( ("search", "", True, "Shodan search string."), ("page", "0", False, "Offset page, starting at zero."), - # TODO: ("limit", "0", False, "Number of results (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" From 1c00d499cfb4ba685d0ab35dccbe8a9f7442340e Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 16 Apr 2019 00:26:09 +0200 Subject: [PATCH 100/119] shodan: finish on invalid page --- src/wfuzz/plugin_api/payloadtools.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/wfuzz/plugin_api/payloadtools.py b/src/wfuzz/plugin_api/payloadtools.py index f2b7f826..82f08e97 100644 --- a/src/wfuzz/plugin_api/payloadtools.py +++ b/src/wfuzz/plugin_api/payloadtools.py @@ -145,7 +145,7 @@ def __init__(self, dork, page, limit): self.api = shodan.Shodan(key) self._dork = dork self._page = MyCounter(page) - self._page_limit = limit + self._page_limit = self._page() + limit self.results_queue = Queue(self.MAX_ENQUEUED_RES) self.page_queue = Queue() @@ -166,7 +166,7 @@ def _do_search(self): self.page_queue.task_done() continue - if page > self._page_limit: + if page >= self._page_limit: self.page_queue.task_done() self.results_queue.put(None) continue @@ -182,7 +182,10 @@ def _do_search(self): self.page_queue.put(self._page.inc()) except shodan.APIError as e: self.page_queue.task_done() - self.results_queue.put(e) + if "Invalid page size" in str(e): + self.results_queue.put(None) + else: + self.results_queue.put(e) continue def __iter__(self): From 2f3fa9b29379de81da56103a5c27376a18f19297 Mon Sep 17 00:00:00 2001 From: Xavi Mendez Date: Tue, 16 Apr 2019 00:36:38 +0200 Subject: [PATCH 101/119] add build badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f6ce0a14..e27e79f2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ +[![Build Status](https://travis-ci.org/xmendez/wfuzz.svg?branch=master)](https://travis-ci.org/xmendez/wfuzz) 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. From faa9d040af849470743855cbdd143928503a8c30 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 16 Apr 2019 23:43:27 +0200 Subject: [PATCH 102/119] fix test_api in python2 --- tests/test_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 38e92f58..5b953230 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -90,8 +90,14 @@ def __init__(self, description, show_field): def close(self): pass - def read(self, pos): - return self.outfile.read(pos) + 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() # load plugins before mocking file object Facade().payloads From 7892b4642f569cff72e6c66321375c3d01e57a50 Mon Sep 17 00:00:00 2001 From: javi Date: Tue, 16 Apr 2019 23:52:42 +0200 Subject: [PATCH 103/119] bug in recipe error_msg --- src/wfuzz/ui/console/clparser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index a83a823f..bbf63e8f 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -231,8 +231,7 @@ def parse_cl(self): for error_msg in options.validate(): print("WARNING: {}".format(error_msg)) - if error_msg: - print("") + print("") options.export_to_file(optsd["--dump-recipe"][0]) print("Recipe written to %s." % (optsd["--dump-recipe"][0],)) From 779b505c5c746d20c38ef27da082314cbd2b6355 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 17 Apr 2019 01:07:28 +0200 Subject: [PATCH 104/119] handle default limit in shodanp --- src/wfuzz/plugin_api/payloadtools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/plugin_api/payloadtools.py b/src/wfuzz/plugin_api/payloadtools.py index 82f08e97..19b827c1 100644 --- a/src/wfuzz/plugin_api/payloadtools.py +++ b/src/wfuzz/plugin_api/payloadtools.py @@ -145,7 +145,7 @@ def __init__(self, dork, page, limit): self.api = shodan.Shodan(key) self._dork = dork self._page = MyCounter(page) - self._page_limit = self._page() + limit + self._page_limit = self._page() + limit if limit > 0 else -1 self.results_queue = Queue(self.MAX_ENQUEUED_RES) self.page_queue = Queue() @@ -166,7 +166,7 @@ def _do_search(self): self.page_queue.task_done() continue - if page >= self._page_limit: + if self._page_limit > 0 and page >= self._page_limit: self.page_queue.task_done() self.results_queue.put(None) continue From 58ed857fd558bd780691ff1796db2635b47e0ba0 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 17 Apr 2019 01:08:00 +0200 Subject: [PATCH 105/119] several --recipe options --- docs/library/guide.rst | 2 +- docs/user/advanced.rst | 6 +++++- src/wfuzz/options.py | 5 +++-- src/wfuzz/ui/console/clparser.py | 5 +++-- src/wfuzz/ui/console/common.py | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/library/guide.rst b/docs/library/guide.rst index 14bda172..3b2cb431 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -7,7 +7,7 @@ All options that are available within the Wfuzz command line interface are avail CLI Option Library Option ======================== ===================================================================================== url="url" ---recipe recipe="filename" +--recipe recipe=["filename"] --oF save="filename" -f filename,printer printer=("filename", "printer") --dry-run dryrun=True diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index b5cd7865..10e1219a 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -311,7 +311,11 @@ You can combine a recipe with additional command line options, for example:: $ wfuzz --recipe /tmp/recipe -b cookie1=value -In case of repeated options, command line options have precedence over options included in the recipe. +Several recipes can also be combined:: + + $ wfuzz --recipe /tmp/recipe --recipe /tmp/recipe2 + +In case of repeated options, command line options have precedence over options included in the recipe. Last recipe has precedence. Connect to an specific host --------------------------------------- diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index cccd1f3b..51f4b569 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -32,7 +32,8 @@ def __init__(self, **kwargs): # recipe must be superseded by options if "recipe" in kwargs and kwargs["recipe"]: - self.import_from_file(kwargs["recipe"]) + for recipe in kwargs["recipe"]: + self.import_from_file(recipe) self.update(kwargs) @@ -64,7 +65,7 @@ def _defaults(self): verbose=False, interactive=False, dryrun=False, - recipe="", + recipe=[], save="", proxies=None, conn_delay=int(Facade().sett.get('connection', 'conn_delay')), diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index bbf63e8f..32cc46ca 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -215,7 +215,8 @@ def parse_cl(self): # parse options from recipe first if "--recipe" in optsd: - options.import_from_file(optsd["--recipe"][0]) + for recipe in optsd["--recipe"]: + options.import_from_file(recipe) # command line has priority over recipe self._parse_options(optsd, options) @@ -327,7 +328,7 @@ 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 ["-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 ["--recipe", "-z", "--zP", "--zD", "--slice", "payload", "-w", "-b", "-H", "-p"] 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)) diff --git a/src/wfuzz/ui/console/common.py b/src/wfuzz/ui/console/common.py index f6ae4c04..6eae245b 100644 --- a/src/wfuzz/ui/console/common.py +++ b/src/wfuzz/ui/console/common.py @@ -84,7 +84,7 @@ \t--version : Wfuzz version details \t-e : List of available encoders/payloads/iterators/printers/scripts \t -\t--recipe : Reads options from a recipe +\t--recipe : Reads options from a recipe. Repeat for various recipes. \t--dump-recipe : Prints current options as a recipe \t--oF : Saves fuzz results to a file. These can be consumed later using the wfuzz payload. \t From 04ac5205dd28287f6adf5e91ea66f1374ef2dc11 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 17 Apr 2019 23:10:20 +0200 Subject: [PATCH 106/119] fix filter grammar issue --- src/wfuzz/filter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 3604d5bf..8cab474c 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -29,7 +29,7 @@ def __init__(self, ffilter=None, filter_string=None): 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 + "." + "_" + "-") + field_value = Word(alphas.lower() + "." + "_" + "-") basic_primitives = int_values | quoted_str_value @@ -42,7 +42,7 @@ def __init__(self, ffilter=None, filter_string=None): 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 = (Word("0123456789" + alphas + "." + "_" + "-").setParseAction(self.__compute_res_value) + Optional(operator_call, None)).setParseAction(self.__compute_perl_value) + res_value_op = (Word("0123456789" + alphas.lower() + "." + "_" + "-").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 @@ -52,7 +52,7 @@ def __init__(self, ffilter=None, filter_string=None): symbol_expr = Group(fuzz_statement + oneOf("= == != < > >= <= =~ !~ ~ := =+ =-") + (bbb_value ^ error_value ^ basic_primitives ^ fuzz_statement)).setParseAction(self.__compute_expr) - definition = fuzz_statement ^ symbol_expr + definition = symbol_expr ^ fuzz_statement definition_not = not_operator + definition definition_expr = definition_not + ZeroOrMore(operator + definition_not) From d247fcc5a8e5e4ce7de7930f2fb5ccffd7a2fc12 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 18 Apr 2019 00:12:49 +0200 Subject: [PATCH 107/119] description in case of exception --- src/wfuzz/fuzzobjects.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 75d3e270..07620d6c 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -755,19 +755,23 @@ def _payload_description(self): 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]) - if self.exception: - return ret_str + "! " + str(self.exception) - return ret_str @property def description(self): + ret_str = "" + if self._show_field is True: - return self.eval(self._description) + ret_str = self.eval(self._description) elif self._show_field is False and self._description is not None: - return "{} | {}".format(self._payload_description(), self.eval(self._description)) + ret_str = "{} | {}".format(self._payload_description(), self.eval(self._description)) + else: + ret_str = self._payload_description() - return self._payload_description() + if self.exception: + return ret_str + "! " + str(self.exception) + + return ret_str def eval(self, expr): return FuzzResFilter(filter_string=expr).is_visible(self) From d0220bc6ece0aad29037c80b690a5ad2c134cd94 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 18 Apr 2019 22:56:56 +0200 Subject: [PATCH 108/119] shodanp returns urls --- src/wfuzz/plugins/payloads/shodanp.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/wfuzz/plugins/payloads/shodanp.py b/src/wfuzz/plugins/payloads/shodanp.py index f3605681..6664ab55 100644 --- a/src/wfuzz/plugins/payloads/shodanp.py +++ b/src/wfuzz/plugins/payloads/shodanp.py @@ -12,7 +12,7 @@ class shodanp(BasePayload): "Queries the Shodan API", ) - summary = "Returns hostnames or IPs of a given Shodan API search (needs api key)." + summary = "Returns URLs of a given Shodan API search (needs api key)." category = ["default"] priority = 99 @@ -44,8 +44,12 @@ def close(self): def __next__(self): match = next(self._it) + + port = match['port'] + scheme = 'https' if 'ssl' in match or port == 443 else 'http' + if match['hostnames']: for hostname in match['hostnames']: - return hostname + return "{}://{}:{}".format(scheme, hostname, port) else: - return match['ip_str'] + return "{}://{}:{}".format(scheme, match['ip_str'], port) From 88dab6bf13854fd26d827276e31af513d1727cef Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 18 Apr 2019 23:14:42 +0200 Subject: [PATCH 109/119] fix recipe tests --- tests/test_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 3a21cd58..086f2528 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -355,7 +355,7 @@ def test(self): ret_list = [(x.code, x.history.urlparse.path) for x in fuzzed] # repeat test with recipe as only parameter - with wfuzz.FuzzSession(recipe=filename) as s: + with wfuzz.FuzzSession(recipe=[filename]) as s: if payloads is None: same_list = [(x.code, x.history.urlparse.path) for x in s.fuzz()] else: From 08c2e3c842fb303f684356d96b82fc8f7f335bb2 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 18 Apr 2019 23:41:24 +0200 Subject: [PATCH 110/119] revert previous filter fix and new fix --- src/wfuzz/filter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/filter.py b/src/wfuzz/filter.py index 8cab474c..8c9abd7f 100644 --- a/src/wfuzz/filter.py +++ b/src/wfuzz/filter.py @@ -29,7 +29,8 @@ def __init__(self, ffilter=None, filter_string=None): 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.lower() + "." + "_" + "-") + field_value = Word(alphas + "." + "_" + "-") + reserverd_words = oneOf("BBB XXX") basic_primitives = int_values | quoted_str_value @@ -42,7 +43,7 @@ def __init__(self, ffilter=None, filter_string=None): 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 = (Word("0123456789" + alphas.lower() + "." + "_" + "-").setParseAction(self.__compute_res_value) + Optional(operator_call, None)).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 From 83c9b9f2b80779f376d3bee7467ac9ee48a0bb2e Mon Sep 17 00:00:00 2001 From: javi Date: Sun, 21 Apr 2019 23:10:58 +0200 Subject: [PATCH 111/119] typos in docs --- docs/library/guide.rst | 6 +++--- docs/user/advanced.rst | 34 +++++++++++++++++----------------- docs/user/basicusage.rst | 8 ++++---- docs/user/getting.rst | 4 ++-- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/library/guide.rst b/docs/library/guide.rst index 3b2cb431..445a102a 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -46,7 +46,7 @@ Fuzzing a URL with wfuzz library is very simple. Firstly, import the wfuzz modul >>> import wfuzz -Now, let's try to fuzz a webpage to look for hidden content, such as directories. For this example, let's use Acunetix's testphp (http://testphp.vulnweb.com/):: +Now, let's try to fuzz a web page to look for hidden content, such as directories. For this example, let's use Acunetix's testphp (http://testphp.vulnweb.com/):: >>> import wfuzz >>> for r in wfuzz.fuzz(url="http://testphp.vulnweb.com/FUZZ", hc=[404], payloads=[("file",dict(fn="wordlist/general/common.txt"))]): @@ -90,7 +90,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 programatically without using Wfuzz payloads plugins. +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. Generating a new payload and start fuzzing is really simple:: @@ -128,7 +128,7 @@ The get_payloads method can be used when various payloads are needed:: 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 programatically from a string representing CLI options:: +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:: $ python >>> import wfuzz diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index 10e1219a..56829100 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -4,7 +4,7 @@ Advanced Usage Wfuzz global options -------------------- -Wfuzz global options can be tweaked by modifying the "wfuzz.ini" at the user's home direcory:: +Wfuzz global options can be tweaked by modifying the "wfuzz.ini" at the user's home directory:: ~/.wfuzz$ cat wfuzz.ini @@ -196,9 +196,9 @@ Wfuzz's web application vulnerability scanner is supported by plugins. A list of Scripts are grouped in categories. A script could belong to several categories at the same time. -Thre are two general categories: +There are two general categories: -* passive: Passive scripts analyze existing requests and responses without performing new requests. +* passive: Passive scripts analyse existing requests and responses without performing new requests. * active: Active scripts perform new requests to the application to probe it for vulnerabilities. Additional categories are: @@ -330,7 +330,7 @@ This useful, for example, to test if a reverse proxy can be manipulated into mis Scan Mode: Ignore Errors and Exceptions --------------------------------------- -In the event of a network problem (e.g. DNS failure, refused connection, etc), Wfuzz will raise an exception and stop execution as shown below:: +In the event of a network problem (e.g. DNS failure, refused connection, etc.), Wfuzz will raise an exception and stop execution as shown below:: $ wfuzz -z list,support-web-none http://FUZZ.google.com/ ******************************************************** @@ -400,7 +400,7 @@ Timeouts You can tell Wfuzz to stop waiting for server to response a connection request after a given number of seconds --conn-delay and also the maximum number of seconds that the response is allowed to take using --req-delay parameter. -These timeouts are really handy when you are using Wfuzz to bruteforce resources behind a proxy, ports, hostnames, virtual hosts, etc. +These timeouts are really handy when you are using Wfuzz to brute force resources behind a proxy, ports, hostnames, virtual hosts, etc. Filter Language --------------- @@ -458,8 +458,8 @@ BBB Baseline Name Short version Description ================================ ======================= ============================================= value|unquote() value|un() Unquotes the value -value|lower() value|l() lowercase of the value -value|upper() uppercase of the value +value|lower() value|l() lower-case of the value +value|upper() upper-case of the value value|encode('encoder', 'value') value|e('enc', 'val') Returns encoder.encode(value) value|decode('decoder', 'value') value|d('dec', 'val') Returns encoder.decode(value) value|replace('what', 'with') value|r('what', 'with') Returns value replacing what for with @@ -538,7 +538,7 @@ urlp.hasquery Returns true when the URL contains a query string. urlp.isbllist Returns true when the URL file extension is included in the configuration discovery's blacklist =================== ============================================= -Payload instrospection can also be performed by using the keyword FUZZ: +Payload introspection can also be performed by using the keyword FUZZ: ============ ============================================== Name Description @@ -552,7 +552,7 @@ Where field is one of the described above. Filtering results ^^^^^^^^^^^^^^^^^ -The --filter command line parameter in conjuntion with the described filter language allows you to peform more complex result triage than the standard filter switches such as "--hc/hl/hw/hh", "--sc/sl/sw/sh" and "-ss/hs". +The --filter command line parameter in conjunction with the described filter language allows you to perform more complex result triage than the standard filter switches such as "--hc/hl/hw/hh", "--sc/sl/sw/sh" and "-ss/hs". An example below:: @@ -576,7 +576,7 @@ An example below:: Filtered Requests: 9 Requests/sec.: 7.572076 -Using result and payload instrospection to look for specific content returned in the response:: +Using result and payload introspection to look for specific content returned in the response:: $ wfuzz -z list,echoedback -d searchFor=FUZZ --filter "content~FUZZ" http://testphp.vulnweb.com/search.php?test=query @@ -602,8 +602,8 @@ Filtering a payload Slice """"""" -The --slice command line parameter in conjuntion with the described filter language allows you to filter a payload. -The payload to filter, specified by the -z switch must preceed --slice comamand line parameter. +The --slice command line parameter in conjunction with the described filter 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:: @@ -642,7 +642,7 @@ In this context you are filtering a FuzzResult object, which is the result of co Reutilising previous results -------------------------------------- -Previously performed HTTP requests/responses contain a treasure trove of data. Wfuzz payloads and object instrospection (explained in the filter grammar section) exposes a Python object interface to requests/responses recorded by Wfuzz or other tools. +Previously performed HTTP requests/responses contain a treasure trove of data. Wfuzz payloads and object introspection (explained in the filter grammar section) exposes a Python object interface to requests/responses recorded by Wfuzz or other tools. 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. @@ -665,7 +665,7 @@ $ wfuzz --oF /tmp/session -z range,0-10 http://www.google.com/dir/test.php?id=FU Wfuzz can read burp's (TM) log or saved states. This allows to filter or reutilise burp proxy requests and responses. -Then, you can reutilise those results by using the denoted payloads. To repeat a request exactly how it was stored, you must use the FUZZ keywork on the command line:: +Then, you can reutilise those results by using the denoted payloads. To repeat a request exactly how it was stored, you must use the FUZZ keyword on the command line:: $ wfuzz -z burpstate,a_burp_state.burp FUZZ @@ -691,7 +691,7 @@ Previous requests can also be modified by using the usual command line switches. |__ C=200 114 L 373 W 5347 Ch "http://testphp.vulnweb.com/userinfo.php" -* Same request against another url:: +* Same request against another URL:: $ wfuzz -z burpstate,a_burp_state.burp -H "addme: header" -u http://www.otherhost.com FUZZ @@ -701,7 +701,7 @@ If you do not want to use the full saved request: $ wfuzz -z wfuzzp,/tmp/session --zP attr=url FUZZ -* Or by specyfing the FUZZ keyword and a field name in the form of FUZZ[field]:: +* Or by specifying the FUZZ keyword and a field name in the form of FUZZ[field]:: $ wfuzz -z wfuzzp,/tmp/session FUZZ[url] @@ -744,7 +744,7 @@ For example, the following will return a unique list of HTTP requests including $ 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 thefore the above will find all the requests exposing the CSRF token in the URL. +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:: diff --git a/docs/user/basicusage.rst b/docs/user/basicusage.rst index 3dece545..bf96b7c8 100644 --- a/docs/user/basicusage.rst +++ b/docs/user/basicusage.rst @@ -116,7 +116,7 @@ Headers can also be fuzzed:: Fuzzing HTTP Verbs ------------------ -HTTP verbs fuzzing can be specified using the -X swith:: +HTTP verbs fuzzing can be specified using the -X switch:: $ wfuzz -z list,GET-HEAD-POST-TRACE-OPTIONS -X FUZZ http://testphp.vulnweb.com/ ******************************************************** @@ -193,7 +193,7 @@ If you want to fuzz a resource from a protected website you can also use "--basi Recursion --------- -The -R swith can be used to specify a payload recursion's depth. For example, if you want to search for existing directories and then fuzz within these directories again using the same payload you can use the following command:: +The -R switch can be used to specify a payload recursion's depth. For example, if you want to search for existing directories and then fuzz within these directories again using the same payload you can use the following command:: $ wfuzz -z list,"admin-CVS-cgi\-bin" -R1 http://testphp.vulnweb.com/FUZZ ******************************************************** @@ -236,7 +236,7 @@ Wfuzz supports writing the results to a file in a different format. This is perf $ wfuzz -e printers -For example, to write results to an output file in json format use the following command:: +For example, to write results to an output file in JSON format use the following command:: $ wfuzz -f /tmp/outfile,json -w wordlist/general/common.txt http://testphp.vulnweb.com/FUZZ @@ -248,7 +248,7 @@ Wfuzz supports showing the results in various formats. This is performed by plug $ wfuzz -e printers -For example, to show results in json format use the following command:: +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 diff --git a/docs/user/getting.rst b/docs/user/getting.rst index fe439bf3..125d3772 100644 --- a/docs/user/getting.rst +++ b/docs/user/getting.rst @@ -31,7 +31,7 @@ The obtained output is shown below:: Filtered Requests: 0 Requests/sec.: 225.4143 -Wfuzz output allows to analyze the web server responses and filter the desired results based on the HTTP response message obtained, for example, response codes, response length, etc. +Wfuzz output allows to analyse the web server responses and filter the desired results based on the HTTP response message obtained, for example, response codes, response length, etc. Each line provides the following information: @@ -236,7 +236,7 @@ Here the {} defines the value of the FUZZ word for this first HTTP request, and Regex filters ^^^^^^^^^^^^^ -The command line parameters "--ss" and "--hs" allow to filter the responses using a regular expression against the returned content. For example, the following allows to find web servers vulnerables to "shellshock" (see http://edge-security.blogspot.co.uk/2014/10/scan-for-shellshock-with-wfuzz.html for more information):: +The command line parameters "--ss" and "--hs" allow to filter the responses using a regular expression against the returned content. For example, the following allows to find web servers vulnerable to "shellshock" (see http://edge-security.blogspot.co.uk/2014/10/scan-for-shellshock-with-wfuzz.html for more information):: $ wfuzz -H "User-Agent: () { :;}; echo; echo vulnerable" --ss vulnerable -w cgis.txt http://localhost:8000/FUZZ From 43553f7336377edfe3c8b1d9ba70b28bf6b5f772 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 24 Apr 2019 23:37:45 +0200 Subject: [PATCH 112/119] burplog python2/3 compatible --- src/wfuzz/externals/reqresp/Response.py | 1 - src/wfuzz/fuzzobjects.py | 6 ++++-- src/wfuzz/plugins/payloads/burplog.py | 8 ++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/wfuzz/externals/reqresp/Response.py b/src/wfuzz/externals/reqresp/Response.py index db2be4f3..323d95ce 100644 --- a/src/wfuzz/externals/reqresp/Response.py +++ b/src/wfuzz/externals/reqresp/Response.py @@ -138,7 +138,6 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): self._headers = [] tp = TextParser() - rawheader = python2_3_convert_from_unicode(rawheader.decode("utf-8", errors='replace')) tp.setSource("string", rawheader) tp.readUntil(r"(HTTP\S*) ([0-9]+)") diff --git a/src/wfuzz/fuzzobjects.py b/src/wfuzz/fuzzobjects.py index 07620d6c..0ecfe7d9 100644 --- a/src/wfuzz/fuzzobjects.py +++ b/src/wfuzz/fuzzobjects.py @@ -22,7 +22,7 @@ from .facade import Facade, ERROR_CODE from .mixins import FuzzRequestUrlMixing, FuzzRequestSoupMixing -from .utils import python2_3_convert_to_unicode +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 @@ -323,7 +323,8 @@ def to_http_object(self, c): return pycurl_c def from_http_object(self, c, bh, bb): - return self._request.response_from_conn_object(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) @@ -334,6 +335,7 @@ def update_from_raw_http(self, raw, scheme, raw_response=None, raw_content=None) if raw_response: rp = Response() + raw_response = python2_3_convert_from_unicode(raw_response.decode("utf-8", errors='surrogateescape')) rp.parseResponse(raw_response, raw_content) self._request.response = rp diff --git a/src/wfuzz/plugins/payloads/burplog.py b/src/wfuzz/plugins/payloads/burplog.py index d26f2c0d..36b6a1a4 100644 --- a/src/wfuzz/plugins/payloads/burplog.py +++ b/src/wfuzz/plugins/payloads/burplog.py @@ -6,7 +6,11 @@ import re -CRLF = "\r\n" +import sys +if sys.version_info < (3, 0): + from io import open + +CRLF = "\n" 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)\])?') @@ -51,7 +55,7 @@ def parse_burp_log(self, burp_log): burp_file = None try: - burp_file = open(self.find_file(burp_log), 'rb') + burp_file = open(self.find_file(burp_log), 'r', encoding="utf-8", errors="surrogateescape") history = 'START' From 682e77af2dc73ca4cde6ea63f05463a179a008d1 Mon Sep 17 00:00:00 2001 From: javi Date: Wed, 24 Apr 2019 23:45:44 +0200 Subject: [PATCH 113/119] shodan to stop on no credits --- src/wfuzz/plugin_api/payloadtools.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wfuzz/plugin_api/payloadtools.py b/src/wfuzz/plugin_api/payloadtools.py index 19b827c1..ee91833a 100644 --- a/src/wfuzz/plugin_api/payloadtools.py +++ b/src/wfuzz/plugin_api/payloadtools.py @@ -184,6 +184,8 @@ def _do_search(self): self.page_queue.task_done() if "Invalid page size" in str(e): self.results_queue.put(None) + elif "Insufficient query credits" in str(e): + self.results_queue.put(None) else: self.results_queue.put(e) continue @@ -217,8 +219,6 @@ def _stop(self): self._threads = [] self.results_queue.put(None) - self._cancel_job = False - self._started = False def __next__(self): if not self._started: @@ -230,6 +230,8 @@ def __next__(self): if res is None: self._stop() + self._cancel_job = False + self._started = False raise StopIteration elif isinstance(res, Exception): self._stop() From 64cf2ce4f14e9899ffe7169d6ab5fed0f2daa181 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 25 Apr 2019 00:49:28 +0200 Subject: [PATCH 114/119] remove 2 times response parse --- src/wfuzz/externals/reqresp/Request.py | 12 +++--------- src/wfuzz/externals/reqresp/Response.py | 1 + tests/test_reqresp.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/wfuzz/externals/reqresp/Request.py b/src/wfuzz/externals/reqresp/Request.py index 6e5f77d0..90870662 100644 --- a/src/wfuzz/externals/reqresp/Request.py +++ b/src/wfuzz/externals/reqresp/Request.py @@ -370,16 +370,10 @@ def response_from_conn_object(self, conn, header, body): self.totaltime = conn.getinfo(pycurl.TOTAL_TIME) - rp = Response() - rp.parseResponse(header, rawbody=body) - - if self.schema == "https" and self.__proxy: - self.response = Response() - self.response.parseResponse(rp.getContent()) - else: - self.response = rp + self.response = Response() + self.response.parseResponse(header, rawbody=body) - return rp + return self.response def perform(self): self.__performHead = "" diff --git a/src/wfuzz/externals/reqresp/Response.py b/src/wfuzz/externals/reqresp/Response.py index 323d95ce..2a7a0e95 100644 --- a/src/wfuzz/externals/reqresp/Response.py +++ b/src/wfuzz/externals/reqresp/Response.py @@ -168,6 +168,7 @@ def parseResponse(self, rawheader, rawbody=None, type="curl"): break # 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]+)"): break diff --git a/tests/test_reqresp.py b/tests/test_reqresp.py index 5591b870..c7fe7598 100644 --- a/tests/test_reqresp.py +++ b/tests/test_reqresp.py @@ -15,6 +15,20 @@ """.format(wfuzz_version) +raw_response_header = b"""HTTP/1.0 200 Connection established + +HTTP/1.1 404 Not Found +Content-Type: text/html; charset=UTF-8 +Referrer-Policy: no-referrer +Content-Length: 1564 +Date: Wed, 24 Apr 2019 22:03:52 GMT +Alt-Svc: quic=":443"; ma=2592000; v="46,44,43,39" +Connection: close + +""" + +raw_response_body = b'\n\n \n \n Error 404 (Not Found)!!1\n \n \n

404. That\xe2\x80\x99s an error.\n

The requested URL /one was not found on this server. That\xe2\x80\x99s all we know.\n' + class FuzzResultFactoryTest(unittest.TestCase): def __init__(self, *args, **kwargs): @@ -34,6 +48,13 @@ def test_baseline(self): 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) + + self.assertEqual(fr.code, 404) + self.assertEqual(len(fr.content), 1560) + class FuzzRequestTest(unittest.TestCase): def __init__(self, *args, **kwargs): From 3a35d171e25f04c2dbfef289149ca9c72b58793a Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 25 Apr 2019 01:05:07 +0200 Subject: [PATCH 115/119] fix python2 content test --- tests/test_reqresp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_reqresp.py b/tests/test_reqresp.py index c7fe7598..7797cc2c 100644 --- a/tests/test_reqresp.py +++ b/tests/test_reqresp.py @@ -53,7 +53,7 @@ def test_from_conn(self): fr.update_from_raw_http(raw_req, 'https', raw_response_header, raw_response_body) self.assertEqual(fr.code, 404) - self.assertEqual(len(fr.content), 1560) + self.assertEqual(fr.content.count("\n"), 11) class FuzzRequestTest(unittest.TestCase): From 73307b55747d66d1b7442f31dc1561fec59c1702 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 27 Apr 2019 00:45:43 +0200 Subject: [PATCH 116/119] fs closes payload --- src/wfuzz/core.py | 1 + src/wfuzz/options.py | 15 +++++++-------- src/wfuzz/wfuzz.py | 12 ++---------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/wfuzz/core.py b/src/wfuzz/core.py index 14c223e3..845a5f39 100644 --- a/src/wfuzz/core.py +++ b/src/wfuzz/core.py @@ -108,6 +108,7 @@ def __init__(self, options): def stop(self): self.stats.cancelled = True + self.close() def restart(self, seed): self.seed = seed diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 51f4b569..782c4eda 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -199,10 +199,13 @@ def export_json(self): return json.dumps(tmp, sort_keys=True, indent=4, separators=(',', ': ')) def payload(self, **kwargs): - self.data.update(kwargs) - # TODO: maybe compile here? or think how to make work and not deadlock from the api - self.data['compiled_genreq'] = requestGenerator(self) - return self.data['compiled_genreq'].get_dictio() + try: + self.data.update(kwargs) + self.data['compiled_genreq'] = requestGenerator(self) + for r in self.data['compiled_genreq'].get_dictio(): + yield r + finally: + self.data['compiled_genreq'].close() def fuzz(self, **kwargs): self.data.update(kwargs) @@ -303,7 +306,3 @@ def close(self): if self.fz: self.fz.cancel_job() - - # TODO: deadlock when using api.payload() - elif self.data['compiled_genreq'] is not None: - self.data['compiled_genreq'].close() diff --git a/src/wfuzz/wfuzz.py b/src/wfuzz/wfuzz.py index 6d4330ff..c36a4a4d 100644 --- a/src/wfuzz/wfuzz.py +++ b/src/wfuzz/wfuzz.py @@ -83,8 +83,7 @@ def usage(): \t--efield : Show the specified language expression together with the current payload """) - # TODO: from .api import payload - from .api import FuzzSession + from .api import payload from .exception import FuzzExceptBadOptions import getopt @@ -112,15 +111,11 @@ def usage(): field = value raw_output = True - session = None - try: session_options = CLParser(sys.argv).parse_cl() - # TODO: api.payload will block with new shodanp - session = FuzzSession(**session_options) printer = None - for res in session.payload(): + for res in payload(**session_options): if len(res) > 1: raise FuzzExceptBadOptions("wfpayload can only be used to generate one word dictionaries") else: @@ -148,9 +143,6 @@ def usage(): print(("\nFatal exception: %s" % str(e))) except Exception as e: print(("\nUnhandled exception: %s" % str(e))) - finally: - if session: - session.close() def main_encoder(): From b84f051483fb94cf75548e000683ef12394a7e46 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 27 Apr 2019 00:52:02 +0200 Subject: [PATCH 117/119] remove global fz --- src/wfuzz/options.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 782c4eda..2fe29f12 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -40,7 +40,6 @@ def __init__(self, **kwargs): self.cache = HttpCache() self.http_pool = None - self.fz = None self.stats = FuzzStats() def _defaults(self): @@ -210,16 +209,17 @@ def payload(self, **kwargs): def fuzz(self, **kwargs): self.data.update(kwargs) + fz = None try: - self.fz = Fuzzer(self.compile()) + fz = Fuzzer(self.compile()) - for f in self.fz: + for f in fz: yield f finally: - if self.fz: - self.fz.cancel_job() - self.stats.update(self.fz.genReq.stats) + if fz: + fz.cancel_job() + self.stats.update(fz.genReq.stats) def get_payloads(self, iterator): self.data["dictio"] = iterator @@ -303,6 +303,3 @@ def compile(self): def close(self): if self.http_pool: self.http_pool.deregister() - - if self.fz: - self.fz.cancel_job() From e25f41850b00cabd0296bb2cdeabf177a12889bc Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 27 Apr 2019 01:00:53 +0200 Subject: [PATCH 118/119] close httpool in fuzz --- docs/library/guide.rst | 4 ---- src/wfuzz/options.py | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/library/guide.rst b/docs/library/guide.rst index 445a102a..e5f157c9 100644 --- a/docs/library/guide.rst +++ b/docs/library/guide.rst @@ -75,7 +75,6 @@ The FuzzSession object allows you to persist certain parameters across fuzzing s 00060: C=301 7 L 12 W 184 Ch "admin" 00183: C=403 10 L 29 W 263 Ch "cgi-bin" ... - >>> s.close() FuzzSession can also be used as context manager:: @@ -104,7 +103,6 @@ Generating a new payload and start fuzzing is really simple:: 00014: C=404 7 L 12 W 168 Ch "2" 00015: C=404 7 L 12 W 168 Ch "3" 00016: C=404 7 L 12 W 168 Ch "4" - >>> s.close() The get_payloads method can be used when various payloads are needed:: @@ -123,7 +121,6 @@ The get_payloads method can be used when various payloads are needed:: 00020: C=404 7 L 12 W 168 Ch "0 - b" 00023: C=404 7 L 12 W 168 Ch "2 - a" 00019: C=404 7 L 12 W 168 Ch "0 - a" - >>> s.close() Get session =========== @@ -147,7 +144,6 @@ The get_session function generates a Wfuzz session object from the specified com 00007: C=404 7 L 12 W 168 Ch "6" 00009: C=404 7 L 12 W 168 Ch "8" 00010: C=404 7 L 12 W 168 Ch "9" - >>> s.close() Interacting with the results ============================ diff --git a/src/wfuzz/options.py b/src/wfuzz/options.py index 2fe29f12..3c208a08 100644 --- a/src/wfuzz/options.py +++ b/src/wfuzz/options.py @@ -221,6 +221,10 @@ def fuzz(self, **kwargs): fz.cancel_job() self.stats.update(fz.genReq.stats) + if self.http_pool: + self.http_pool.deregister() + self.http_pool = None + def get_payloads(self, iterator): self.data["dictio"] = iterator @@ -303,3 +307,4 @@ def compile(self): def close(self): if self.http_pool: self.http_pool.deregister() + self.http_pool = None From 2552265cfbf31201f78af466bc63721672f2f520 Mon Sep 17 00:00:00 2001 From: javi Date: Sat, 27 Apr 2019 02:01:30 +0200 Subject: [PATCH 119/119] --filter-help reads from docs --- MANIFEST.in | 1 + setup.py | 2 + src/wfuzz/ui/console/clparser.py | 117 +++---------------------------- 3 files changed, 11 insertions(+), 109 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 04f196ac..4164cd47 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.md include LICENSE +include docs/* diff --git a/setup.py b/setup.py index 2ca0868d..a8243b00 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,8 @@ 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={ diff --git a/src/wfuzz/ui/console/clparser.py b/src/wfuzz/ui/console/clparser.py index 32cc46ca..0622ecb8 100644 --- a/src/wfuzz/ui/console/clparser.py +++ b/src/wfuzz/ui/console/clparser.py @@ -1,8 +1,9 @@ +import re import sys import getopt from collections import defaultdict -from wfuzz.utils import allowed_fields +from wfuzz.utils import allowed_fields, get_path from wfuzz.filter import PYPARSING from wfuzz.facade import Facade from wfuzz.options import FuzzSession @@ -34,113 +35,6 @@ def show_usage(self): print(help_banner) print(usage) - def show_filter_usage(self): - print(""" - * Operators: and or not = != < > >= <= =~ !~ ~ := =+ =- - - * Basic primitives: - - ============ ==================== - Long Name Description - ============ ==================== - 'string' Quoted string - 0..9+ Integer values - XXX HTTP request error code - BBB Baseline - ============ ==================== - - * Values can also be modified using the following operators: - - ================================ ======================= ============================================= - Name Short version Description - ================================ ======================= ============================================= - value|unquote() value|un() Unquotes the value - value|lower() value|l() lowercase of the value - value|upper() uppercase of the value - value|encode('encoder', 'value') value|e('enc', 'val') Returns encoder.encode(value) - value|decode('decoder', 'value') value|d('dec', 'val') Returns encoder.decode(value) - value|replace('what', 'with') value|r('what', 'with') Returns value replacing what for with - value|unique(value) value|u(value) Returns True if a value is unique. - value|startswith('value') value|sw('param') Returns true if the value string starts with param - ================================ ======================= ============================================= - - * When a FuzzResult is available, you could perform runtime introspection of the objects using the following symbols - - ============ ============== ============================================= - Name Short version Description - ============ ============== ============================================= - url Wfuzz's result HTTP request url - description Wfuzz's result description - nres Wfuzz's result identifier - code c Wfuzz's result HTTP response's code - chars h Wfuzz's result HTTP response chars - 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 - ============ ============== ============================================= - - FuzzRequest object's attribute (you need to use the r. prefix) such as: - - ============================ ============================================= - Name Description - ============================ ============================================= - url HTTP request's value - method HTTP request's verb - scheme HTTP request's scheme - host HTTP request's host - content HTTP response's content - raw_content HTTP response's content including headers - cookies.all All HTTP request and response cookies - cookies.request HTTP requests cookieS - cookies.response HTTP response cookies - cookies.request.<> Specified HTTP request cookie - 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 - params.all All HTTP request GET and POST parameters - params.get All HTTP request GET parameters - params.post All HTTP request POST parameters - params.get.<> Spcified HTTP request GET parameter - params.post.<> Spcified HTTP request POST parameter - pstrip Returns a signature of the HTTP request using the parameter's names without values (useful for unique operations) - is_path Returns true when the HTTP request path refers to a directory. - ============================ ============================================= - - FuzzRequest URL field is broken in smaller (read only) parts using the urlparse Python's module in the urlp attribute. - - Urlparse parses a URL into: scheme://netloc/path;parameters?query#fragment. For example, for the "http://www.google.com/dir/test.php?id=1" URL you can get the following values: - - =================== ============================================= - Name Value - =================== ============================================= - urlp.scheme http - urlp.netloc www.google.com - urlp.path /dir/test.php - urlp.params - urlp.query id=1 - urlp.fragment - urlp.ffname test.php - urlp.fext .php - urlp.fname test - urlp.hasquery Returns true when the URL contains a query string. - urlp.isbllist Returns true when the URL file extension is included in the configuration discovery's blacklist - =================== ============================================= - - Payload instrospection can also be performed by using the keyword FUZZ: - - ============ ============================================== - Name Description - ============ ============================================== - FUZnZ Allows to access the Nth payload string - FUZnZ[field] Allows to access the Nth payload attributes - ============ ============================================== - """) - sys.exit(0) - 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)]) @@ -263,7 +157,12 @@ def _parse_help_opt(self, optsd): sys.exit(0) if "--filter-help" in optsd: - self.show_filter_usage() + 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)) + sys.exit(0) # Extensions help