├── tests ├── dummy_config.toml ├── dummy_output.txt ├── dummy_config_dump.toml ├── dummy_debuglog.txt └── dummy_wordlist.txt ├── src ├── wenum │ ├── ui │ │ ├── __init__.py │ │ └── console │ │ │ ├── __init__.py │ │ │ └── kbhit.py │ ├── externals │ │ ├── __init__.py │ │ ├── moduleman │ │ │ ├── __init__.py │ │ │ ├── plugin.py │ │ │ ├── modulefilter.py │ │ │ ├── registrant.py │ │ │ └── loader.py │ │ ├── settings │ │ │ ├── __init__.py │ │ │ └── settings.py │ │ └── reqresp │ │ │ ├── __init__.py │ │ │ ├── CachedResponse.py │ │ │ ├── cache.py │ │ │ ├── Variables.py │ │ │ ├── TextParser.py │ │ │ ├── Response.py │ │ │ └── Request.py │ ├── factories │ │ ├── __init__.py │ │ ├── fuzzfactory.py │ │ ├── payman.py │ │ ├── dictfactory.py │ │ ├── plugin_factory.py │ │ ├── reqresp_factory.py │ │ └── fuzzresfactory.py │ ├── filters │ │ ├── __init__.py │ │ ├── base_filter.py │ │ └── simplefilter.py │ ├── helpers │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── obj_dyn.py │ │ ├── str_func.py │ │ ├── obj_dic.py │ │ ├── obj_factory.py │ │ └── file_func.py │ ├── plugin_api │ │ ├── __init__.py │ │ ├── mixins.py │ │ ├── urlutils.py │ │ ├── base.py │ │ └── static_data.py │ ├── plugins │ │ ├── __init__.py │ │ └── scripts │ │ │ ├── __init__.py │ │ │ ├── showcontent.py │ │ │ ├── title.py │ │ │ ├── listing.py │ │ │ ├── cookies.py │ │ │ ├── sitemap.py │ │ │ ├── errors.py │ │ │ ├── grep.py │ │ │ ├── screenshot.py │ │ │ ├── backups.py │ │ │ ├── domainpath.py │ │ │ ├── npmdependencies.py │ │ │ ├── webservice_description.py │ │ │ ├── robots.py │ │ │ ├── sourcemap.py │ │ │ ├── gau.py │ │ │ ├── linkparser.py │ │ │ ├── headers.py │ │ │ ├── clone.py │ │ │ ├── context.py │ │ │ ├── links.py │ │ │ └── logfiles.py │ ├── __main__.py │ ├── dictionaries.py │ ├── api.py │ ├── exception.py │ ├── __init__.py │ ├── wordlist_handler.py │ ├── iterators.py │ ├── facade.py │ ├── main.py │ ├── printers.py │ ├── runtime_session.py │ └── fuzzrequest.py └── wenum-cli.py ├── .gitmodules ├── pyproject.toml ├── .gitignore └── README.md /tests/dummy_config.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy_output.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy_config_dump.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy_debuglog.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy_wordlist.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/externals/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/factories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/plugin_api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/ui/console/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/externals/moduleman/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/externals/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wenum/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /src/wenum/externals/reqresp/__init__.py: -------------------------------------------------------------------------------- 1 | from .Request import Request 2 | from .Response import Response 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/linkfinder"] 2 | path = src/linkfinder 3 | url = https://github.com/GerbenJavado/LinkFinder 4 | -------------------------------------------------------------------------------- /src/wenum-cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from wenum.main import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /src/wenum/plugin_api/mixins.py: -------------------------------------------------------------------------------- 1 | # Plugins specializations with common methods useful for their own type 2 | from wenum.plugin_api.urlutils import parse_url 3 | from .base import BasePlugin 4 | 5 | 6 | class DiscoveryPluginMixin: 7 | def queue_url(self, url): 8 | if not parse_url(url).isbllist: 9 | BasePlugin.queue_url(self, url) 10 | return True 11 | return False 12 | -------------------------------------------------------------------------------- /src/wenum/filters/base_filter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseFilter(ABC): 5 | """ 6 | Base class for Filters 7 | """ 8 | 9 | def __init__(self): 10 | pass 11 | 12 | @abstractmethod 13 | def is_filtered(self, fuzz_result) -> bool: 14 | """ 15 | Check if the fuzz_result should be filtered out. 16 | 17 | Returns True if it should be, and False if not. 18 | """ 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /src/wenum/dictionaries.py: -------------------------------------------------------------------------------- 1 | from .iterators import BaseIterator 2 | 3 | 4 | class TupleIt(BaseIterator): 5 | def __init__(self, parent): 6 | self.parent = parent 7 | 8 | def count(self): 9 | return self.parent.count() 10 | 11 | def width(self): 12 | return 1 13 | 14 | def payloads(self): 15 | return [self.parent] 16 | 17 | def next_word(self): 18 | return (next(self.parent),) 19 | 20 | def __next__(self): 21 | return self.next_word() 22 | -------------------------------------------------------------------------------- /src/wenum/externals/moduleman/plugin.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | 3 | 4 | def moduleman_plugin(*args): 5 | method_args = [] 6 | 7 | def inner_decorator(cls): 8 | for method in method_args: 9 | if not (method in dir(cls)): 10 | raise Exception("Required method %s not implemented" % method) 11 | cls.__PLUGIN_MODULEMAN_MARK = "Plugin mark" 12 | 13 | return cls 14 | 15 | if not isinstance(args[0], Callable): 16 | method_args += args 17 | return inner_decorator 18 | 19 | return inner_decorator(args[0]) 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wenum" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["None"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | linkfinder = { path = "./src/linkfinder" } 10 | chardet = "^5.2.0" 11 | pycurl = "^7.45.2" 12 | pyparsing = "^3.1.1" 13 | pySocks = "^1.7.1" 14 | six = "^1.11.0" 15 | requests = "^2.28.1" 16 | tomlkit = "^0.12.3" 17 | beautifulsoup4 = "^4.11.1" 18 | python-dateutil = "^2.8.2" 19 | pytz = "^2023.3.post1" 20 | rich = "^13.7.0" 21 | 22 | [tool.poetry.scripts] 23 | wenum = 'wenum.main:main' 24 | 25 | [tool.poetry.dev-dependencies] 26 | 27 | [build-system] 28 | requires = ["poetry-core>=1.0.0"] 29 | build-backend = "poetry.core.masonry.api" 30 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/showcontent.py: -------------------------------------------------------------------------------- 1 | from wenum.plugin_api.base import BasePlugin 2 | from wenum.externals.moduleman.plugin import moduleman_plugin 3 | 4 | 5 | @moduleman_plugin 6 | class ShowContent(BasePlugin): 7 | name = "show_content" 8 | author = ("MTD",) 9 | version = "0.1" 10 | summary = "Show used HTTP method" 11 | description = ("Show used HTTP method.",) 12 | category = ["debug"] 13 | priority = 99 14 | 15 | parameters = ( 16 | ) 17 | 18 | def __init__(self, session): 19 | BasePlugin.__init__(self, session) 20 | 21 | def validate(self, fuzz_result): 22 | # if(fuzzresult.history.method in ["HEAD"]): 23 | return True 24 | 25 | def process(self, fuzz_result): 26 | self.add_information(f"{fuzz_result.history.content}") 27 | -------------------------------------------------------------------------------- /src/wenum/helpers/utils.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | import difflib 3 | 4 | 5 | class MyCounter: 6 | def __init__(self, count=0): 7 | self._count = count 8 | self._mutex = Lock() 9 | 10 | def inc(self): 11 | return self._operation(1) 12 | 13 | def dec(self): 14 | return self._operation(-1) 15 | 16 | def _operation(self, dec): 17 | with self._mutex: 18 | self._count += dec 19 | return self._count 20 | 21 | def __call__(self): 22 | with self._mutex: 23 | return self._count 24 | 25 | 26 | def diff(param1, param2): 27 | delta = difflib.unified_diff( 28 | str(param1).splitlines(False), 29 | str(param2).splitlines(False), 30 | fromfile="prev", 31 | tofile="current", 32 | n=0, 33 | ) 34 | 35 | return "\n".join(delta) 36 | -------------------------------------------------------------------------------- /src/wenum/api.py: -------------------------------------------------------------------------------- 1 | from .runtime_session import FuzzSession 2 | from .facade import Facade 3 | 4 | """ 5 | wenum API 6 | """ 7 | 8 | 9 | #def fuzz(**kwargs): 10 | # return FuzzSession(**kwargs).fuzz() 11 | 12 | 13 | #def get_payloads(iterator): 14 | # fs = FuzzSession() 15 | # 16 | # return fs.get_payloads(iterator) 17 | 18 | 19 | #def get_payload(iterator): 20 | # fs = FuzzSession() 21 | # return fs.get_payload(iterator) 22 | 23 | 24 | def encode(name, value): 25 | return Facade().encoders.get_plugin(name)().encode(value) 26 | 27 | 28 | def decode(name, value): 29 | return Facade().encoders.get_plugin(name)().decode(value) 30 | 31 | 32 | #def payload(**kwargs): 33 | # return FuzzSession(**kwargs).payload() 34 | 35 | 36 | #def get_session(cline): 37 | # cl = ["wenum"] + cline.split(" ") 38 | # return FuzzSession(**CLParser(cl).parse_cl()) 39 | -------------------------------------------------------------------------------- /src/wenum/factories/fuzzfactory.py: -------------------------------------------------------------------------------- 1 | from ..fuzzrequest import FuzzRequest 2 | 3 | from ..helpers.obj_factory import ObjectFactory, SeedBuilderHelper 4 | 5 | 6 | class FuzzRequestFactory(ObjectFactory): 7 | def __init__(self): 8 | super(FuzzRequestFactory, self).__init__( 9 | { 10 | "request_from_options": RequestBuilder(), 11 | "seed_from_options": SeedBuilder(), 12 | }, 13 | ) 14 | 15 | 16 | class RequestBuilder: 17 | def __call__(self, session) -> FuzzRequest: 18 | fuzz_request = FuzzRequest() 19 | 20 | fuzz_request.url = session.options.url 21 | fuzz_request.update_from_options(session) 22 | 23 | return fuzz_request 24 | 25 | 26 | class SeedBuilder: 27 | def __call__(self, session) -> FuzzRequest: 28 | seed: FuzzRequest = reqfactory.create("request_from_options", session) 29 | 30 | return seed 31 | 32 | 33 | reqfactory = FuzzRequestFactory() 34 | -------------------------------------------------------------------------------- /src/wenum/externals/reqresp/CachedResponse.py: -------------------------------------------------------------------------------- 1 | from wenum.externals.reqresp.Response import Response, get_encoding_from_headers 2 | 3 | 4 | class CachedResponse(Response): 5 | def __init__(self, protocol="", code="", body=None, header=None, length=None): 6 | super().__init__(protocol, code, "") 7 | self._body = body 8 | if header is not None and header != '': 9 | self.parse_response(header) 10 | else: 11 | self.protocol = "HTTP/1.1" 12 | self.code = code 13 | self.charlen = length 14 | 15 | def get_content(self): 16 | if self._body is None: 17 | return super().get_content() 18 | content_encoding = get_encoding_from_headers(dict(self.get_headers())) 19 | 20 | # fallback to default encoding 21 | if content_encoding is None: 22 | content_encoding = "utf-8" 23 | 24 | with open(self._body, "rb") as body_file: 25 | return body_file.read().decode(content_encoding, errors="replace") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | wenum.ini 3 | wenum-config.toml 4 | src/wenum/plugins/scripts/debug_test.py 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # vim 62 | *.swp 63 | *.swo 64 | 65 | # Jetbrains IDE 66 | .idea 67 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/title.py: -------------------------------------------------------------------------------- 1 | from wenum.plugin_api.base import BasePlugin 2 | from wenum.externals.moduleman.plugin import moduleman_plugin 3 | from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning 4 | import warnings 5 | 6 | @moduleman_plugin 7 | class Title(BasePlugin): 8 | name = "title" 9 | author = ("Xavi Mendez (@xmendez)",) 10 | version = "0.1" 11 | summary = "Parses HTML page title" 12 | description = ("Parses HTML page title",) 13 | category = ["info", "passive", "default"] 14 | priority = 99 15 | 16 | parameters = () 17 | 18 | def __init__(self, session): 19 | BasePlugin.__init__(self, session) 20 | 21 | def validate(self, fuzz_result): 22 | return True 23 | 24 | def process(self, fuzz_result): 25 | soup = BeautifulSoup(fuzz_result.history.content, "html.parser") 26 | 27 | title = soup.title.string if soup.title else "" 28 | 29 | if title and title != "" and title not in self.kbase["title"]: 30 | self.kbase["title"] = title 31 | self.add_information(f"[u]{title}[/u]") 32 | -------------------------------------------------------------------------------- /src/wenum/factories/payman.py: -------------------------------------------------------------------------------- 1 | from ..fuzzobjects import FPayloadManager, FuzzWord, FuzzWordType 2 | 3 | from ..helpers.obj_factory import ObjectFactory, SeedBuilderHelper 4 | 5 | 6 | class PayManFactory(ObjectFactory): 7 | def __init__(self): 8 | ObjectFactory.__init__( 9 | self, 10 | { 11 | "payloadman_from_request": FuzzReqPayloadManBuilder(), 12 | "empty_payloadman": OnePayloadManBuilder(), 13 | }, 14 | ) 15 | 16 | 17 | class FuzzReqPayloadManBuilder: 18 | def __call__(self, freq): 19 | fpm = FPayloadManager() 20 | 21 | for pdict in [ 22 | pdict 23 | for pdict in SeedBuilderHelper.get_marker_dict(freq) 24 | if pdict["word"] is not None 25 | ]: 26 | fpm.add(pdict) 27 | 28 | return fpm 29 | 30 | 31 | class OnePayloadManBuilder: 32 | def __call__(self, content): 33 | fpm = FPayloadManager() 34 | fpm.add( 35 | {"full_marker": None, "word": None, "index": None, "field": None}, content 36 | ) 37 | 38 | return fpm 39 | 40 | 41 | payman_factory = PayManFactory() 42 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/listing.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from wenum.plugin_api.base import BasePlugin 4 | from wenum.plugin_api.static_data import LISTING_dir_indexing_regexes 5 | from wenum.externals.moduleman.plugin import moduleman_plugin 6 | 7 | 8 | @moduleman_plugin 9 | class Listing(BasePlugin): 10 | name = "listing" 11 | author = ("Xavi Mendez (@xmendez)",) 12 | version = "0.1" 13 | summary = "Looks for directory listing vulnerabilities" 14 | description = ("Looks for directory listing vulnerabilities",) 15 | category = ["default", "passive", "info"] 16 | priority = 99 17 | 18 | parameters = () 19 | 20 | def __init__(self, session): 21 | BasePlugin.__init__(self, session) 22 | 23 | self.regex = [] 24 | for i in LISTING_dir_indexing_regexes: 25 | self.regex.append(re.compile(i, re.MULTILINE | re.DOTALL)) 26 | 27 | def validate(self, fuzz_result): 28 | return fuzz_result.code in [200] 29 | 30 | def process(self, fuzz_result): 31 | for r in self.regex: 32 | if len(r.findall(fuzz_result.history.content)) > 0: 33 | self.add_information(f"[u]Directory listing[/u] identified") 34 | break 35 | -------------------------------------------------------------------------------- /src/wenum/exception.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class FuzzException(Exception): 5 | def __init__(self, message): 6 | super().__init__(message) 7 | logger = logging.getLogger("debug_log") 8 | logger.error(message) 9 | 10 | 11 | class FuzzExceptBadOptions(FuzzException): 12 | pass 13 | 14 | 15 | class FuzzExceptNoPluginError(FuzzException): 16 | pass 17 | 18 | 19 | class FuzzExceptPluginLoadError(FuzzException): 20 | pass 21 | 22 | 23 | class FuzzExceptIncorrectFilter(FuzzException): 24 | pass 25 | 26 | 27 | class FuzzExceptBadAPI(FuzzException): 28 | pass 29 | 30 | 31 | class FuzzExceptInternalError(FuzzException): 32 | pass 33 | 34 | 35 | class FuzzExceptBadFile(FuzzException): 36 | pass 37 | 38 | 39 | class FuzzExceptBadInstall(FuzzException): 40 | pass 41 | 42 | 43 | class FuzzExceptBadRecipe(FuzzException): 44 | pass 45 | 46 | 47 | class FuzzExceptMissingAPIKey(FuzzException): 48 | pass 49 | 50 | 51 | class FuzzExceptPluginBadParams(FuzzException): 52 | pass 53 | 54 | 55 | class FuzzExceptResourceParseError(FuzzException): 56 | pass 57 | 58 | 59 | class FuzzExceptPluginError(FuzzException): 60 | pass 61 | 62 | 63 | class FuzzExceptNetError(FuzzException): 64 | pass 65 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/cookies.py: -------------------------------------------------------------------------------- 1 | from wenum.plugin_api.base import BasePlugin 2 | from wenum.externals.moduleman.plugin import moduleman_plugin 3 | 4 | KBASE_NEW_COOKIE = "cookies.cookie" 5 | 6 | 7 | @moduleman_plugin 8 | class Cookies(BasePlugin): 9 | name = "cookies" 10 | author = ("Xavi Mendez (@xmendez)",) 11 | version = "0.1" 12 | summary = "Looks for new cookies" 13 | description = ("Looks for new cookies",) 14 | category = ["info", "passive", "default"] 15 | priority = 99 16 | 17 | parameters = () 18 | 19 | def __init__(self, session): 20 | BasePlugin.__init__(self, session) 21 | 22 | def validate(self, fuzz_result): 23 | return True 24 | 25 | def process(self, fuzz_result): 26 | new_cookies = list(fuzz_result.history.cookies.response.items()) 27 | 28 | if len(new_cookies) > 0: 29 | for name, value in new_cookies: 30 | 31 | if ( 32 | name != "" 33 | and KBASE_NEW_COOKIE not in self.kbase 34 | or name not in self.kbase[KBASE_NEW_COOKIE] 35 | ): 36 | self.kbase[KBASE_NEW_COOKIE] = name 37 | self.add_information(f"Cookie first set: [u]{name}[/u]=[u]{value}[/u]") 38 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/sitemap.py: -------------------------------------------------------------------------------- 1 | from wenum.plugin_api.mixins import DiscoveryPluginMixin 2 | from wenum.plugin_api.base import BasePlugin 3 | from wenum.externals.moduleman.plugin import moduleman_plugin 4 | 5 | import xml.dom.minidom 6 | 7 | 8 | @moduleman_plugin 9 | class Sitemap(BasePlugin, DiscoveryPluginMixin): 10 | name = "sitemap" 11 | author = ("Xavi Mendez (@xmendez)",) 12 | version = "0.1" 13 | summary = "Parses sitemap.xml file" 14 | description = ("Parses sitemap.xml file",) 15 | category = ["active", "discovery"] 16 | priority = 99 17 | 18 | parameters = () 19 | 20 | def __init__(self, session): 21 | BasePlugin.__init__(self, session) 22 | 23 | def validate(self, fuzz_result): 24 | return ( 25 | fuzz_result.history.urlparse.ffname == "sitemap.xml" 26 | and fuzz_result.code == 200 27 | ) 28 | 29 | def process(self, fuzz_result): 30 | try: 31 | dom = xml.dom.minidom.parseString(fuzz_result.history.content) 32 | except Exception: 33 | self.add_exception_information(f"Error while parsing {fuzz_result.url}") 34 | 35 | url_list = dom.getElementsByTagName("loc") 36 | for url in url_list: 37 | u = url.childNodes[0].data 38 | 39 | self.queue_url(u) 40 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/errors.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from wenum.plugin_api.base import BasePlugin 4 | from wenum.plugin_api.static_data import ERRORS_regex_list 5 | from wenum.externals.moduleman.plugin import moduleman_plugin 6 | 7 | 8 | @moduleman_plugin 9 | class Errors(BasePlugin): 10 | name = "errors" 11 | author = ("Xavi Mendez (@xmendez)",) 12 | version = "0.1" 13 | summary = "Looks for known error messages" 14 | description = ("Looks for common error messages",) 15 | category = ["default", "passive", "info"] 16 | priority = 99 17 | 18 | parameters = () 19 | 20 | def __init__(self, session): 21 | BasePlugin.__init__(self, session) 22 | 23 | self.error_regex = [] 24 | for regex in ERRORS_regex_list: 25 | self.error_regex.append(re.compile(regex, re.MULTILINE | re.DOTALL)) 26 | 27 | def validate(self, fuzz_result): 28 | return True 29 | 30 | def process(self, fuzz_result): 31 | for regex in self.error_regex: 32 | # Some error pages contain the same error message several times, 33 | # but logging them more than once would not be of interest and clutters the log instead 34 | unique_regex_matches = set(regex.findall(fuzz_result.history.content)) 35 | for regex_match in unique_regex_matches: 36 | self.add_information(f"[u]{regex_match}[/u]") 37 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/grep.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from wenum.plugin_api.base import BasePlugin 4 | from wenum.exception import FuzzExceptPluginBadParams 5 | from wenum.externals.moduleman.plugin import moduleman_plugin 6 | 7 | 8 | @moduleman_plugin 9 | class Grep(BasePlugin): 10 | name = "grep" 11 | author = ("Xavi Mendez (@xmendez)",) 12 | version = "0.1" 13 | summary = "HTTP response grep" 14 | description = ( 15 | "Extracts the given regex pattern from the HTTP response and prints it", 16 | "(It is not a filter operator)", 17 | ) 18 | category = ["tools"] 19 | priority = 99 20 | 21 | parameters = (("regex", "", True, "Regex to perform the grep against."),) 22 | 23 | def __init__(self, session): 24 | BasePlugin.__init__(self, session) 25 | try: 26 | print(self.kbase["grep.regex"]) 27 | self.regex = re.compile( 28 | self.kbase["grep.regex"][0], re.MULTILINE | re.DOTALL 29 | ) 30 | except Exception: 31 | raise FuzzExceptPluginBadParams( 32 | "Incorrect regex or missing regex parameter." 33 | ) 34 | 35 | def validate(self, fuzz_result): 36 | return True 37 | 38 | def process(self, fuzz_result): 39 | for r in self.regex.findall(fuzz_result.history.content): 40 | self.add_information(f"Pattern match {r}") 41 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/screenshot.py: -------------------------------------------------------------------------------- 1 | from wenum.plugin_api.base import BasePlugin 2 | from wenum.externals.moduleman.plugin import moduleman_plugin 3 | 4 | import subprocess 5 | import tempfile 6 | import pipes 7 | import os 8 | import re 9 | 10 | 11 | @moduleman_plugin 12 | class Screenshot(BasePlugin): 13 | name = "screenshot" 14 | author = ("Xavi Mendez (@xmendez)",) 15 | version = "0.1" 16 | summary = "Performs a screen capture using linux cutycapt tool" 17 | description = ( 18 | "Performs a screen capture using linux cutycapt tool", 19 | "The tool must be installed and in the executable path", 20 | ) 21 | category = ["tools", "active"] 22 | priority = 99 23 | 24 | parameters = () 25 | 26 | def __init__(self, session): 27 | BasePlugin.__init__(self, session) 28 | 29 | def validate(self, fuzz_result): 30 | return fuzz_result.code not in [404] 31 | 32 | def process(self, fuzz_result): 33 | temp_name = next(tempfile._get_candidate_names()) 34 | defult_tmp_dir = tempfile._get_default_tempdir() 35 | 36 | filename = os.path.join( 37 | defult_tmp_dir, 38 | (temp_name + "_" + re.sub(r"[^a-zA-Z0-9_-]", "_", fuzz_result.url))[:200] 39 | + ".jpg", 40 | ) 41 | 42 | subprocess.call( 43 | [ 44 | "cutycapt", 45 | "--url=%s" % pipes.quote(fuzz_result.url), 46 | "--out=%s" % filename, 47 | "--insecure", 48 | "--print-backgrounds=on", 49 | ] 50 | ) 51 | self.add_information(f"Screenshot taken, output at {filename}") 52 | -------------------------------------------------------------------------------- /src/wenum/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "wenum" 2 | __version__ = "0.1" 3 | 4 | import logging 5 | import sys 6 | import urllib3 7 | from bs4 import MarkupResemblesLocatorWarning 8 | 9 | import warnings 10 | 11 | #TODO Refactor this file 12 | logger = logging.getLogger("debug_log") 13 | logger.addHandler(logging.NullHandler()) 14 | logger.propagate = False 15 | 16 | # Will throw warnings when a proxy is used 17 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 18 | 19 | # Will throw warnings when the response is very short and has some key chars in it that make BS4 think its a filename 20 | warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) 21 | 22 | 23 | # define warnings format 24 | def warning_on_one_line(message, category, filename, lineno, file=None, line=None): 25 | return " %s:%s: %s:%s\n" % (filename, lineno, category.__name__, message) 26 | 27 | 28 | warnings.formatwarning = warning_on_one_line 29 | 30 | 31 | try: 32 | import pycurl 33 | 34 | if "openssl".lower() not in pycurl.version.lower(): 35 | warnings.warn( 36 | "Pycurl is not compiled against Openssl. wenum might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information." 37 | ) 38 | 39 | if not hasattr(pycurl, "CONNECT_TO"): 40 | warnings.warn( 41 | "Pycurl and/or libcurl version is old. CONNECT_TO option is missing. wenum --ip option will not be available." 42 | ) 43 | 44 | except ImportError: 45 | warnings.warn( 46 | "fuzz needs pycurl to run. Pycurl could be installed using the following command: $ pip install pycurl" 47 | ) 48 | 49 | sys.exit(1) 50 | 51 | from .runtime_session import FuzzSession 52 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/backups.py: -------------------------------------------------------------------------------- 1 | from wenum.externals.moduleman.plugin import moduleman_plugin 2 | from wenum.plugin_api.base import BasePlugin 3 | 4 | from urllib.parse import urljoin 5 | 6 | 7 | @moduleman_plugin 8 | class Backups(BasePlugin): 9 | name = "backups" 10 | summary = "Looks for known backup filenames." 11 | description = ("""Looks for known backup filenames. 12 | For example, given http://localhost.com/dir/index.html, it will perform the following requests, 13 | * http://localhost/dir/index.EXTENSIONS, 14 | * http://localhost/dir/index.html.EXTENSIONS, 15 | * http://localhost/dir.EXTENSIONS""") 16 | author = ("Xavi Mendez (@xmendez)",) 17 | version = "0.1" 18 | category = ["fuzzer", "active"] 19 | priority = 99 20 | 21 | parameters = (( 22 | "ext", 23 | ".bak,.tgz,.zip,.tar.gz,~,.rar,.old,.swp", 24 | False, 25 | "Extensions to look for.", 26 | ),) 27 | 28 | def __init__(self, session): 29 | BasePlugin.__init__(self, session) 30 | self.extensions = self.kbase["backups.ext"][0].split(",") 31 | 32 | def validate(self, fuzz_result): 33 | return fuzz_result.code != 404 and ( 34 | fuzz_result.history.urlparse.fext not in self.extensions 35 | ) 36 | 37 | def process(self, fuzz_result): 38 | for extension in self.extensions: 39 | 40 | # http://localhost/dir/test.html -----> test.BAKKK 41 | self.queue_url(urljoin(fuzz_result.url, fuzz_result.history.urlparse.fname + extension)) 42 | 43 | # http://localhost/dir/test.html ---> test.html.BAKKK 44 | self.queue_url(urljoin(fuzz_result.url, fuzz_result.history.urlparse.ffname + extension)) 45 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/domainpath.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | 3 | from wenum.plugin_api.base import BasePlugin 4 | from wenum.plugin_api.urlutils import parse_url 5 | from wenum.externals.moduleman.plugin import moduleman_plugin 6 | 7 | 8 | @moduleman_plugin 9 | class DomainPath(BasePlugin): 10 | name = "domainpath" 11 | author = ("TKA",) 12 | version = "0.1" 13 | summary = "Enqueues domain name parts as part of the path." 14 | description = ("Enqueues subdomain names as part of the path. E.g. fuzzing something.example.com " 15 | "will throw something.example.com/something, /example, /com",) 16 | category = ["active", "discovery"] 17 | priority = 99 18 | 19 | parameters = () 20 | 21 | def __init__(self, session): 22 | BasePlugin.__init__(self, session) 23 | # To prevent always running a lot of code, already processed domains will not repeatedly validate 24 | self.processed_domains = [] 25 | 26 | def validate(self, fuzz_result): 27 | domain_name = parse_url(fuzz_result.url).netloc 28 | # Only if the domain name is not numeric (which would mean it's an IP address), and if 29 | # the domain has not been processed yet 30 | if domain_name not in self.processed_domains and not domain_name.replace('.', '').replace(':', '').isnumeric(): 31 | self.processed_domains.append(domain_name) 32 | return True 33 | return False 34 | 35 | def process(self, fuzz_result): 36 | parsed_url = parse_url(fuzz_result.url) 37 | domain_name = parsed_url.netloc 38 | # In case there is a port with :123, do not consider it 39 | domain_name_parsed = domain_name.split(':')[0] 40 | split_path = domain_name_parsed.split('.') 41 | 42 | for path in split_path: 43 | self.queue_url(urljoin(fuzz_result.url, path)) 44 | -------------------------------------------------------------------------------- /src/wenum/externals/settings/settings.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | import os 3 | import sys 4 | 5 | 6 | class SettingsBase: 7 | """ 8 | Contains application settings. uses a ConfigParser 9 | """ 10 | 11 | def __init__(self, save=False): 12 | self.cparser = ConfigParser() 13 | 14 | self.set_all(self.set_defaults()) 15 | self.filename = os.path.join( 16 | self._path_to_program_dir(), self.get_config_file() 17 | ) 18 | self.cparser.read(self.filename) 19 | 20 | # Base members should implement 21 | # TODO Mark abstract 22 | def get_config_file(self): 23 | """Returns the name of the file where the config is saved.""" 24 | raise NotImplementedError 25 | 26 | def set_defaults(self): 27 | """ 28 | Returns a dictionary with the default settings in the form of 29 | { \ 30 | Section: [ \ 31 | ("setting_x", '5'), 32 | ... 33 | ("setting_y", '5'), 34 | ], 35 | ... 36 | } 37 | """ 38 | raise NotImplementedError 39 | 40 | def get(self, section, setting): 41 | value = self.cparser.get(section, setting) 42 | return value 43 | 44 | def set_all(self, sett): 45 | self.cparser = ConfigParser() 46 | for section, settings in sett.items(): 47 | self.cparser.add_section(section) 48 | for key, value in settings: 49 | self.cparser.set(section, key, value) 50 | 51 | def _path_to_program_dir(self): 52 | """ 53 | Returns path to program directory 54 | """ 55 | path = sys.argv[0] 56 | 57 | if not os.path.isdir(path): 58 | path = os.path.dirname(path) 59 | 60 | if not path: 61 | return "." 62 | 63 | return path 64 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/npmdependencies.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from wenum.plugin_api.base import BasePlugin 4 | from wenum.externals.moduleman.plugin import moduleman_plugin 5 | 6 | 7 | @moduleman_plugin 8 | class NPMDependencies(BasePlugin): 9 | name = "npm_deps" 10 | author = ("Xavi Mendez (@xmendez)",) 11 | version = "0.1" 12 | summary = "Looks for npm dependencies definition in js code" 13 | description = ( 14 | "Extracts npm packages by using regex pattern from the HTTP response and prints it", 15 | ) 16 | category = ["info", "default"] 17 | priority = 99 18 | 19 | parameters = () 20 | 21 | REGEX_PATT = re.compile(r'"([^"]+)":"([^"]+)"', re.MULTILINE | re.DOTALL) 22 | REGEX_DEP = re.compile( 23 | r"dependencies:\{(.*?)\}", re.MULTILINE | re.DOTALL | re.IGNORECASE 24 | ) 25 | REGEX_DEV_DEP = re.compile( 26 | r"devdependencies:\{(.*?)\}", re.MULTILINE | re.DOTALL | re.IGNORECASE 27 | ) 28 | 29 | def __init__(self, session): 30 | BasePlugin.__init__(self, session) 31 | self.match = None 32 | self.match_dev = None 33 | 34 | def validate(self, fuzz_result): 35 | if fuzz_result.history.urlparse.fext != ".js" or fuzz_result.code != 200: 36 | return False 37 | 38 | self.match = self.REGEX_DEP.search(fuzz_result.history.content) 39 | self.match_dev = self.REGEX_DEV_DEP.search(fuzz_result.history.content) 40 | 41 | return self.match is not None or self.match_dev is not None 42 | 43 | def process(self, fuzz_result): 44 | if self.match_dev: 45 | for name, version in self.REGEX_PATT.findall(self.match_dev.group(1)): 46 | self.add_information(f"npm dependency: {name}") 47 | 48 | if self.match: 49 | for name, version in self.REGEX_PATT.findall(self.match.group(1)): 50 | self.add_information(f"npm dev dependency{name}") 51 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/webservice_description.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | from wenum.externals.moduleman.plugin import moduleman_plugin 3 | from wenum.plugin_api.base import BasePlugin 4 | 5 | 6 | @moduleman_plugin 7 | class WebserviceDescription(BasePlugin): 8 | name = "webservice_description" 9 | summary = "Looks for REST-APIs and queues endpoints that may contain interesting files." 10 | description = """Looks for REST-APIs and queues endpoints that may contain interesting files.""" 11 | author = ("IPA",) 12 | version = "0.1" 13 | category = ["active"] 14 | priority = 99 15 | 16 | parameters = () 17 | 18 | def __init__(self, session): 19 | BasePlugin.__init__(self, session) 20 | self.webservice_endpoint_list = [ 21 | "application.wadl", 22 | "application.wadl?detail=true", 23 | "?_wadl", 24 | "/rs?_wadl", 25 | ] 26 | 27 | def validate(self, fuzz_result): 28 | return fuzz_result.code != 404 29 | 30 | def process(self, fuzz_result): 31 | # We want the check to be case-insensitive 32 | url = fuzz_result.url.lower() 33 | 34 | if url.endswith("/api") or url.endswith("/api/v1") or url.endswith("/api/v2") or \ 35 | url.endswith("/services") or url.endswith("/webservices") or url.endswith("/ws"): 36 | for endpoint in self.webservice_endpoint_list: 37 | self.queue_url(fuzz_result.url + "/" + endpoint) 38 | 39 | parsed_url = urlparse(fuzz_result.url) 40 | split_path = parsed_url.path.split('/') 41 | # If the URL that has been found ends with one of the web service URLs 42 | if split_path[-1] in self.webservice_endpoint_list: 43 | if " 0 39 | and line[0] != "#" 40 | and ( 41 | line.upper().find("ALLOW") == 0 42 | or line.upper().find("DISALLOW") == 0 43 | or line.upper().find("SITEMAP") == 0 44 | ) 45 | ): 46 | 47 | url = line[line.find(":") + 1:] 48 | url = url.strip(" *") 49 | 50 | if url: 51 | new_link = urljoin(fuzz_result.url, url) 52 | self.queue_url(new_link) 53 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/sourcemap.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib.parse import urlparse 3 | import pathlib 4 | 5 | from urllib.parse import urljoin 6 | 7 | from wenum.plugin_api.base import BasePlugin 8 | from wenum.externals.moduleman.plugin import moduleman_plugin 9 | 10 | 11 | @moduleman_plugin 12 | class Sourcemap(BasePlugin): 13 | name = "sourcemap" 14 | author = ("MTD",) 15 | version = "0.1" 16 | summary = "Check whether requested file is a JavaScript file or source map file." 17 | description = ("Checks for JavaScript files",) 18 | category = ["active", "discovery"] 19 | priority = 99 20 | 21 | parameters = ( 22 | ) 23 | 24 | def check_filter_options(self, fuzz_result): 25 | """ 26 | #TODO Same problem as in context.py 27 | """ 28 | if fuzz_result.chars in self.session.options.hs_list or fuzz_result.lines in self.session.options.hl_list or \ 29 | fuzz_result.words in self.session.options.hw_list: 30 | return False 31 | else: 32 | return True 33 | 34 | def __init__(self, session): 35 | BasePlugin.__init__(self, session) 36 | 37 | def validate(self, fuzz_result): 38 | if fuzz_result.code in [200] and self.check_filter_options(fuzz_result): 39 | return True 40 | 41 | return False 42 | 43 | def process(self, fuzz_result): 44 | parsed_url = urlparse(fuzz_result.url) 45 | filename = os.path.basename(parsed_url.path) 46 | extension = pathlib.Path(filename).suffix.lower() 47 | 48 | if extension == ".js" or extension == ".jsx": 49 | self.add_information(f"JavaScript file " 50 | f"{filename} identified, checking for source map") 51 | map_url = urljoin(fuzz_result.url, parsed_url.path) + ".map" 52 | self.queue_url(map_url) 53 | elif extension == ".map": 54 | self.add_information(f"Potential source map {filename} identified") 55 | -------------------------------------------------------------------------------- /src/wenum/wordlist_handler.py: -------------------------------------------------------------------------------- 1 | from wenum.fuzzobjects import FuzzWord 2 | from wenum.helpers.file_func import FileDetOpener 3 | from wenum.helpers.file_func import find_file_in_paths 4 | import os 5 | from wenum.facade import Facade 6 | from wenum.fuzzobjects import FuzzWordType 7 | from wenum.exception import ( 8 | FuzzExceptBadFile, 9 | FuzzExceptBadOptions, 10 | ) 11 | 12 | 13 | class File: 14 | """Class responsible for reading the supplied wordlist in the commandline.""" 15 | 16 | def __init__(self, file_path): 17 | self.file_path = file_path 18 | 19 | try: 20 | self.f = FileDetOpener(self.find_file(self.file_path)) 21 | except IOError as e: 22 | raise FuzzExceptBadFile("Error opening file. %s" % str(e)) 23 | 24 | self.__count = None 25 | 26 | def get_type(self): 27 | return FuzzWordType.WORD 28 | 29 | def get_next(self): 30 | line = next(self.f) 31 | if not line: 32 | self.f.close() 33 | raise StopIteration 34 | return line.strip() 35 | 36 | def __next__(self): 37 | return FuzzWord(self.get_next(), self.get_type()) 38 | 39 | def count(self): 40 | """Counts the amount of lines in the file""" 41 | 42 | if self.__count is None: 43 | self.__count = len(list(self.f)) 44 | self.f.reset() 45 | 46 | return self.__count 47 | 48 | def __iter__(self): 49 | return self 50 | 51 | def close(self): 52 | pass 53 | 54 | @staticmethod 55 | def find_file(name): 56 | """#TODO Verify and phase out, as os.path.isfile() should do this instead. No custom implementation where the stdlib suffices. 57 | Find file of the provided payload's file path in the file system""" 58 | if os.path.exists(name): 59 | return name 60 | 61 | for pa in Facade().settings.get("general", "lookup_dirs").split(","): 62 | fn = find_file_in_paths(name, pa) 63 | 64 | if fn is not None: 65 | return fn 66 | 67 | return name 68 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/gau.py: -------------------------------------------------------------------------------- 1 | from wenum.plugin_api.base import BasePlugin 2 | from wenum.plugin_api.urlutils import parse_url 3 | from wenum.externals.moduleman.plugin import moduleman_plugin 4 | import subprocess 5 | from shutil import which 6 | 7 | 8 | @moduleman_plugin 9 | class Gau(BasePlugin): 10 | name = "gau" 11 | author = ("MTD",) 12 | version = "0.1" 13 | summary = "Execute gau once." 14 | description = ("Execute gau once",) 15 | category = ["active", "discovery"] 16 | priority = 99 17 | output = [] 18 | 19 | parameters = ( 20 | ) 21 | 22 | def __init__(self, session): 23 | BasePlugin.__init__(self, session) 24 | self.proxy_list = session.options.proxy_list 25 | self.run_once = True 26 | 27 | def validate(self, fuzz_result): 28 | return True 29 | 30 | @staticmethod 31 | def exec_cmd(cmd): 32 | cmd_process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, executable='/bin/bash') 33 | stdout, stderr = cmd_process.communicate() 34 | return stdout 35 | 36 | def process(self, fuzz_result): 37 | if not which("gau"): 38 | self.add_information(f"Gau executable is not in PATH") 39 | return 40 | initial_url = fuzz_result.history.fuzzing_url.replace("FUZZ", "") 41 | if self.proxy_list: 42 | # Concatenate protocol + IP + port -> e.g. SOCKS5://127.0.0.1:8081 43 | proxy_string = self.proxy_list[0] 44 | proxy_option = f"--proxy {proxy_string}" 45 | else: 46 | proxy_option = "" 47 | 48 | parsed_link = parse_url(initial_url) 49 | target_url = (parsed_link.hostname + parsed_link.path).rstrip("/") 50 | 51 | gau_cmd = f"gau {target_url} {proxy_option} --threads 10 --blacklist ttf,woff,svg,png,jpg,gif,ico" 52 | gau_urls = self.exec_cmd(gau_cmd) 53 | 54 | gau_urls = gau_urls.decode("utf-8").splitlines() 55 | if not gau_urls: 56 | self.add_information(f"Did not find anything") 57 | return 58 | 59 | for url in gau_urls: 60 | self.queue_url(url) 61 | -------------------------------------------------------------------------------- /src/wenum/helpers/obj_dyn.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from .obj_dic import DotDict 3 | 4 | 5 | def _get_alias(attr): 6 | attr_alias = { 7 | "l": "lines", 8 | "h": "chars", 9 | "w": "words", 10 | "c": "code", 11 | "r": "history", 12 | } 13 | 14 | if attr in attr_alias: 15 | return attr_alias[attr] 16 | 17 | return attr 18 | 19 | 20 | def rsetattr(obj, attr, new_val, operation): 21 | # if not _check_allowed_field(attr): 22 | # raise AttributeError("Unknown field {}".format(attr)) 23 | 24 | pre, _, post = attr.rpartition(".") 25 | 26 | pre_post = None 27 | if len(attr.split(".")) > 3: 28 | pre_post = post 29 | pre, _, post = pre.rpartition(".") 30 | 31 | post = _get_alias(post) 32 | 33 | try: 34 | obj_to_set = rgetattr(obj, pre) if pre else obj 35 | prev_val = rgetattr(obj, attr) 36 | if pre_post is not None: 37 | prev_val = DotDict({pre_post: prev_val}) 38 | 39 | if operation is not None: 40 | val = operation(prev_val, new_val) 41 | else: 42 | if isinstance(prev_val, DotDict): 43 | val = {k: new_val for k, v in prev_val.items()} 44 | else: 45 | val = new_val 46 | 47 | return setattr(obj_to_set, post, val) 48 | except AttributeError: 49 | raise AttributeError( 50 | "rsetattr: Can't set '{}' attribute of {}.".format( 51 | post, obj_to_set.__class__ 52 | ) 53 | ) 54 | 55 | 56 | def rgetattr(obj, attr, *args): 57 | def _getattr(obj, attr): 58 | attr = _get_alias(attr) 59 | try: 60 | return getattr(obj, attr, *args) 61 | except AttributeError: 62 | raise AttributeError( 63 | "rgetattr: Can't get '{}' attribute from '{}'.".format( 64 | attr, obj.__class__ 65 | ) 66 | ) 67 | 68 | # if not _check_allowed_field(attr): 69 | # raise AttributeError("Unknown field {}".format(attr)) 70 | 71 | return functools.reduce(_getattr, [obj] + attr.split(".")) 72 | -------------------------------------------------------------------------------- /src/wenum/factories/dictfactory.py: -------------------------------------------------------------------------------- 1 | from ..helpers.obj_factory import ObjectFactory 2 | from ..exception import FuzzExceptBadOptions 3 | from wenum.wordlist_handler import File 4 | from ..dictionaries import ( 5 | TupleIt 6 | ) 7 | from wenum.iterators import Zip, Product, Chain 8 | 9 | 10 | class DictionaryFactory(ObjectFactory): 11 | def __init__(self): 12 | ObjectFactory.__init__( 13 | self, 14 | { 15 | "dictio_from_payload": DictioFromPayloadBuilder(), 16 | "dictio_from_options": DictioFromOptions(), 17 | }, 18 | ) 19 | 20 | 21 | class BaseDictioBuilder: 22 | @staticmethod 23 | def validate(session, selected_dic): 24 | if not selected_dic: 25 | raise FuzzExceptBadOptions("Empty dictionary! Check payload and filter") 26 | 27 | if len(selected_dic) == 1 and session.options.iterator: 28 | raise FuzzExceptBadOptions( 29 | "Several dictionaries must be used when specifying an iterator" 30 | ) 31 | 32 | @staticmethod 33 | def init_iterator(session, selected_dic): 34 | """ 35 | Returns an iterator according to the user options 36 | """ 37 | if len(selected_dic) == 1: 38 | return TupleIt(selected_dic[0]) 39 | elif session.options.iterator: 40 | if session.options.iterator == "zip": 41 | return Zip(*selected_dic) 42 | elif session.options.iterator == "chain": 43 | return Chain(*selected_dic) 44 | # Using product as the fallback, as it's the most common (and therefore default) anyways 45 | else: 46 | return Product(*selected_dic) 47 | else: 48 | return Product(*selected_dic) 49 | 50 | 51 | class DictioFromPayloadBuilder(BaseDictioBuilder): 52 | def __call__(self, session): 53 | selected_dic = [] 54 | 55 | for wordlist in session.options.wordlist_list: 56 | dictionary = File(wordlist) 57 | selected_dic.append(dictionary) 58 | 59 | self.validate(session, selected_dic) 60 | return self.init_iterator(session, selected_dic) 61 | 62 | 63 | class DictioFromOptions(BaseDictioBuilder): 64 | def __call__(self, session): 65 | return DictioFromPayloadBuilder()(session) 66 | 67 | 68 | dictionary_factory = DictionaryFactory() 69 | -------------------------------------------------------------------------------- /src/wenum/ui/console/kbhit.py: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/a/31550142 2 | 3 | import os 4 | 5 | # Windows 6 | if os.name == 'nt': 7 | import msvcrt 8 | 9 | # Posix (Linux, OS X) 10 | else: 11 | import sys 12 | import termios 13 | import atexit 14 | from select import select 15 | 16 | 17 | class KBHit: 18 | 19 | def __init__(self): 20 | """Creates a KBHit object that you can call to do various keyboard things.""" 21 | 22 | if os.name == "nt": 23 | pass 24 | 25 | else: 26 | 27 | # Save the terminal settings 28 | self.fd = sys.stdin.fileno() 29 | self.new_term = termios.tcgetattr(self.fd) 30 | self.old_term = termios.tcgetattr(self.fd) 31 | 32 | # New terminal setting unbuffered 33 | self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO) 34 | termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term) 35 | 36 | # Support normal-terminal reset at exit 37 | atexit.register(self.set_normal_term) 38 | 39 | def set_normal_term(self): 40 | """ 41 | Resets to normal terminal. On Windows this is a no-op. 42 | """ 43 | 44 | if os.name == 'nt': 45 | pass 46 | 47 | else: 48 | termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term) 49 | 50 | def getch(self): 51 | """ Returns a keyboard character after kbhit() has been called. 52 | Should not be called in the same program as getarrow(). 53 | """ 54 | 55 | if os.name == 'nt': 56 | return msvcrt.getch().decode('utf-8') 57 | 58 | else: 59 | return sys.stdin.read(1) 60 | 61 | def getarrow(self): 62 | """ Returns an arrow-key code after kbhit() has been called. Codes are 63 | 0 : up 64 | 1 : right 65 | 2 : down 66 | 3 : left 67 | Should not be called in the same program as getch(). 68 | """ 69 | 70 | if os.name == 'nt': 71 | msvcrt.getch() # skip 0xE0 72 | c = msvcrt.getch() 73 | vals = [72, 77, 80, 75] 74 | 75 | else: 76 | c = sys.stdin.read(3)[2] 77 | vals = [65, 67, 66, 68] 78 | 79 | return vals.index(ord(c.decode('utf-8'))) 80 | 81 | def kbhit(self): 82 | """ Returns True if keyboard character was hit, False otherwise. 83 | """ 84 | if os.name == 'nt': 85 | return msvcrt.kbhit() 86 | 87 | else: 88 | dr,dw,de = select([sys.stdin], [], [], 0) 89 | return dr != [] 90 | -------------------------------------------------------------------------------- /src/wenum/factories/plugin_factory.py: -------------------------------------------------------------------------------- 1 | from ..helpers.obj_factory import ObjectFactory 2 | 3 | from ..fuzzobjects import FuzzPlugin, FuzzError, FuzzResult 4 | from ..factories.fuzzresfactory import resfactory 5 | 6 | 7 | class PluginFactory(ObjectFactory): 8 | def __init__(self): 9 | ObjectFactory.__init__( 10 | self, 11 | { 12 | "backfeed_plugin": PluginBackfeedBuilder(), 13 | "seed_plugin": PluginSeedBuilder(), 14 | "plugin_from_error": PluginErrorBuilder(), 15 | "plugin_from_finding": PluginFindingBuilder(), 16 | }, 17 | ) 18 | 19 | 20 | class PluginBackfeedBuilder: 21 | """ 22 | When a plugin is built here, the seed attribute becomes set 23 | by resfactory to BACKFEED, adding a request to the queue 24 | """ 25 | def __call__(self, name, originating_fuzzres, url, method) -> FuzzPlugin: 26 | plugin = FuzzPlugin() 27 | plugin.name = name 28 | plugin.exception = None 29 | from_plugin = True 30 | plugin.seed = resfactory.create("fuzzres_from_fuzzres", originating_fuzzres, url, method, from_plugin) 31 | 32 | return plugin 33 | 34 | 35 | class PluginSeedBuilder: 36 | """ 37 | When a plugin is built here, the seed attribute becomes set 38 | by resfactory to SEED, adding a new seed (full recursion) to the queue 39 | """ 40 | def __call__(self, name, seed, seeding_url) -> FuzzPlugin: 41 | plugin = FuzzPlugin() 42 | plugin.name = name 43 | plugin.exception = None 44 | plugin.seed = resfactory.create("seed_from_plugin", seed, seeding_url) 45 | 46 | return plugin 47 | 48 | 49 | class PluginErrorBuilder: 50 | def __call__(self, name, exception) -> FuzzPlugin: 51 | plugin = FuzzPlugin() 52 | plugin.name = name 53 | plugin.message = "Exception within plugin %s: %s" % (name, str(exception)) 54 | plugin.exception = FuzzError(exception) 55 | plugin.severity = FuzzPlugin.HIGH 56 | plugin.seed = None 57 | 58 | return plugin 59 | 60 | 61 | class PluginFindingBuilder: 62 | """ 63 | Creates a Plugin object dedicated to storing message information linked to the fuzzresult for logging purposes 64 | """ 65 | def __call__(self, name, message, severity) -> FuzzPlugin: 66 | plugin = FuzzPlugin() 67 | plugin.name = name 68 | plugin.message = message 69 | plugin.severity = severity 70 | plugin.exception = None 71 | plugin.seed = None 72 | 73 | return plugin 74 | 75 | 76 | plugin_factory = PluginFactory() 77 | -------------------------------------------------------------------------------- /src/wenum/factories/reqresp_factory.py: -------------------------------------------------------------------------------- 1 | import pycurl 2 | 3 | from ..helpers.str_func import convert_to_unicode 4 | from ..externals.reqresp import Response 5 | 6 | 7 | class ReqRespRequestFactory: 8 | 9 | @staticmethod 10 | def to_http_object(fuzz_request, pycurl_c) -> pycurl.Curl: 11 | pycurl_c.setopt(pycurl.MAXREDIRS, 5) 12 | 13 | pycurl_c.setopt(pycurl.WRITEFUNCTION, fuzz_request._request.body_callback) 14 | pycurl_c.setopt(pycurl.HEADERFUNCTION, fuzz_request._request.header_callback) 15 | 16 | pycurl_c.setopt(pycurl.NOSIGNAL, 1) 17 | pycurl_c.setopt(pycurl.SSL_VERIFYPEER, False) 18 | pycurl_c.setopt(pycurl.SSL_VERIFYHOST, 0) 19 | 20 | pycurl_c.setopt(pycurl.PATH_AS_IS, 1) 21 | 22 | pycurl_c.setopt( 23 | pycurl.URL, convert_to_unicode(fuzz_request._request.complete_url) 24 | ) 25 | 26 | pycurl_c.unsetopt(pycurl.USERPWD) 27 | 28 | pycurl_c.setopt( 29 | pycurl.HTTPHEADER, convert_to_unicode(fuzz_request._request.get_headers()) 30 | ) 31 | 32 | curl_options = { 33 | "GET": pycurl.HTTPGET, 34 | "POST": pycurl.POST, 35 | "PATCH": pycurl.UPLOAD, 36 | "HEAD": pycurl.NOBODY, 37 | } 38 | 39 | for verb in curl_options.values(): 40 | pycurl_c.setopt(verb, False) 41 | 42 | if fuzz_request._request.method in curl_options: 43 | pycurl_c.unsetopt(pycurl.CUSTOMREQUEST) 44 | pycurl_c.setopt(curl_options[fuzz_request._request.method], True) 45 | else: 46 | pycurl_c.setopt(pycurl.CUSTOMREQUEST, fuzz_request._request.method) 47 | 48 | if fuzz_request._request._non_parsed_post is not None: 49 | pycurl_c.setopt( 50 | pycurl.POSTFIELDS, 51 | convert_to_unicode(fuzz_request._request._non_parsed_post), 52 | ) 53 | 54 | # We do not want pycurl to automatically follow requests. We wouldn't be able to parse 55 | # The requests inbetween in a modular way 56 | pycurl_c.setopt(pycurl.FOLLOWLOCATION, 0) 57 | 58 | if fuzz_request.ip: 59 | pycurl_c.setopt( 60 | pycurl.CONNECT_TO, 61 | [f"::{fuzz_request.ip}"], 62 | ) 63 | #for i in range(10): 64 | # print(fuzz_request.ip) 65 | 66 | return pycurl_c 67 | 68 | @staticmethod 69 | def from_http_object(fuzz_request, pycurl_c: pycurl.Curl, header, body): 70 | raw_header = header.decode("utf-8", errors="surrogateescape") 71 | 72 | fuzz_request._request.totaltime = pycurl_c.getinfo(pycurl.TOTAL_TIME) 73 | 74 | fuzz_request._request.response = Response() 75 | fuzz_request._request.response.parse_response(raw_header, rawbody=body) 76 | 77 | return fuzz_request._request.response 78 | -------------------------------------------------------------------------------- /src/wenum/helpers/str_func.py: -------------------------------------------------------------------------------- 1 | import re 2 | import six 3 | 4 | from .obj_dic import DotDict 5 | 6 | 7 | def json_minify(string, strip_space=True): 8 | """ 9 | Created on 20/01/2011 10 | v0.2 (C) Gerald Storer 11 | MIT License 12 | Based on JSON.minify.js: 13 | https://github.com/getify/JSON.minify 14 | Contributers: 15 | - Pradyun S. Gedam (conditions and variable names changed) 16 | """ 17 | 18 | tokenizer = re.compile(r'"|(/\*)|(\*/)|(//)|\n|\r') 19 | end_slashes_re = re.compile(r"(\\)*$") 20 | 21 | in_string = False 22 | in_multi = False 23 | in_single = False 24 | 25 | new_str = [] 26 | index = 0 27 | 28 | for match in re.finditer(tokenizer, string): 29 | 30 | if not (in_multi or in_single): 31 | tmp = string[index : match.start()] 32 | if not in_string and strip_space: 33 | # replace white space as defined in standard 34 | tmp = re.sub("[ \t\n\r]+", "", tmp) 35 | new_str.append(tmp) 36 | 37 | index = match.end() 38 | val = match.group() 39 | 40 | if val == '"' and not (in_multi or in_single): 41 | escaped = end_slashes_re.search(string, 0, match.start()) 42 | 43 | # start of string or unescaped quote character to end string 44 | if not in_string or (escaped is None or len(escaped.group()) % 2 == 0): 45 | in_string = not in_string 46 | # include " character in next catch 47 | index -= 1 48 | elif not (in_string or in_multi or in_single): 49 | if val == "/*": 50 | in_multi = True 51 | elif val == "//": 52 | in_single = True 53 | elif val == "*/" and in_multi and not (in_string or in_single): 54 | in_multi = False 55 | elif val in "\r\n" and not (in_multi or in_string) and in_single: 56 | in_single = False 57 | elif not ((in_multi or in_single) or (val in " \r\n\t" and strip_space)): 58 | new_str.append(val) 59 | 60 | new_str.append(string[index:]) 61 | return "".join(new_str) 62 | 63 | 64 | def convert_to_unicode(text): 65 | if isinstance(text, dict) or isinstance(text, DotDict): 66 | return { 67 | convert_to_unicode(key): convert_to_unicode(value) 68 | for key, value in list(text.items()) 69 | } 70 | elif isinstance(text, list): 71 | return [convert_to_unicode(element) for element in text] 72 | elif isinstance(text, six.string_types): 73 | return text.encode("utf-8", errors="ignore") 74 | else: 75 | return text 76 | 77 | 78 | def value_in_any_list_item(value, list_obj): 79 | if isinstance(list_obj, list): 80 | return len([item for item in list_obj if value.lower() in item.lower()]) > 0 81 | elif isinstance(list_obj, str): 82 | return value.lower() in list_obj.lower() 83 | -------------------------------------------------------------------------------- /src/wenum/iterators.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from functools import reduce 3 | from abc import ABC, abstractmethod 4 | 5 | from builtins import zip as builtinzip 6 | 7 | 8 | class BaseIterator(ABC): 9 | """Classes inheriting from this base class are supposed to provide different 10 | means of iterating through supplied FUZZ keywords""" 11 | @abstractmethod 12 | def count(self): 13 | raise NotImplementedError 14 | 15 | @abstractmethod 16 | def width(self) -> int: 17 | """ 18 | Returns amount of FUZZ keywords the iterator consumes 19 | """ 20 | raise NotImplementedError 21 | 22 | @abstractmethod 23 | def payloads(self): 24 | raise NotImplementedError 25 | 26 | def cleanup(self): 27 | """ 28 | Called when runtime is shutting down 29 | """ 30 | for payload in self.payloads(): 31 | payload.close() 32 | 33 | @abstractmethod 34 | def __next__(self): 35 | raise NotImplementedError 36 | 37 | 38 | class Zip(BaseIterator): 39 | """Returns an iterator that aggregates elements from each of the iterables.""" 40 | 41 | def __init__(self, *i): 42 | self._payload_list = i 43 | self.__width = len(i) 44 | self.__count = min([x.count() for x in i]) 45 | self.it = builtinzip(*i) 46 | 47 | def count(self): 48 | return self.__count 49 | 50 | def width(self): 51 | return self.__width 52 | 53 | def payloads(self): 54 | return self._payload_list 55 | 56 | def __next__(self): 57 | return next(self.it) 58 | 59 | 60 | class Product(BaseIterator): 61 | summary = "Returns an iterator cartesian product of input iterables." 62 | 63 | def __init__(self, *i): 64 | self._payload_list = i 65 | self.__width = len(i) 66 | self.__count = reduce(lambda x, y: x * y.count(), i[1:], i[0].count()) 67 | self.it = itertools.product(*i) 68 | 69 | def count(self): 70 | return self.__count 71 | 72 | def width(self): 73 | return self.__width 74 | 75 | def payloads(self): 76 | return self._payload_list 77 | 78 | def __next__(self): 79 | return next(self.it) 80 | 81 | 82 | class Chain(BaseIterator): 83 | summary = "Returns an iterator returns elements from the first iterable until it is exhausted, then proceeds to the next iterable, until all of the iterables are exhausted." 84 | 85 | def __init__(self, *i): 86 | self._payload_list = i 87 | self.__count = sum([x.count() for x in i]) 88 | self.it = itertools.chain(*i) 89 | 90 | def count(self): 91 | return self.__count 92 | 93 | def width(self): 94 | return 1 95 | 96 | def payloads(self): 97 | return self._payload_list 98 | 99 | def __next__(self): 100 | return (next(self.it),) 101 | 102 | -------------------------------------------------------------------------------- /src/wenum/plugin_api/urlutils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import os 3 | 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from wenum.facade import Facade 8 | 9 | from urllib.parse import ParseResult, urlparse, parse_qs 10 | 11 | from wenum.exception import FuzzExceptBadAPI 12 | 13 | 14 | class FuzzRequestParse(ParseResult): 15 | @property 16 | def ffname(self): 17 | """ 18 | Returns script plus extension from an URL. ie. http://www.localhost.com/kk/index.html?id=3 19 | will return index.html 20 | """ 21 | u = self.path.split("/")[-1:][0] 22 | 23 | return u 24 | 25 | @property 26 | def fext(self): 27 | """ 28 | Returns script extension from an URL. ie. http://www.localhost.com/kk/index.html?id=3 29 | will return .html 30 | """ 31 | return os.path.splitext(self.ffname)[1] 32 | 33 | @property 34 | def fname(self): 35 | """ 36 | Returns script name from an URL. ie. http://www.localhost.com/kk/index.html?id=3 37 | will return index 38 | """ 39 | return os.path.splitext(self.ffname)[0] 40 | 41 | @property 42 | def isbllist(self): 43 | fext = self.fext 44 | return fext != "." and fext in Facade().settings.get( 45 | "kbase", "discovery.blacklist" 46 | ).split("-") 47 | 48 | def cache_key(self, base_urlp=None): 49 | scheme = self.scheme 50 | netloc = self.netloc 51 | 52 | if base_urlp: 53 | scheme = self.scheme if self.scheme else base_urlp.scheme 54 | netloc = self.netloc if self.netloc else base_urlp.netloc 55 | 56 | key = "{}-{}-{}-{}".format(scheme, netloc, self.path, self.params) 57 | dicc = {"g{}".format(key): True for key in parse_qs(self.query).keys()} 58 | 59 | # take URL parameters into consideration 60 | url_params = list(dicc.keys()) 61 | url_params.sort() 62 | key += "-" + "-".join(url_params) 63 | 64 | return key 65 | 66 | 67 | def parse_url(url): 68 | # >>> urlparse.urlparse("http://some.page.pl/nothing.py;someparam=some;otherparam=other?query1=val1&query2=val2#frag") 69 | # ParseResult(scheme='http', netloc='some.page.pl', path='/nothing.py', params='someparam=some;otherparam=other', query='query1=val1&query2=val2', fragment='frag') 70 | 71 | scheme, netloc, path, params, query, fragment = urlparse(url) 72 | return FuzzRequestParse(scheme, netloc, path, params, query, fragment) 73 | 74 | 75 | def check_content_type(fuzz_result, which): 76 | ctype = None 77 | if "Content-Type" in fuzz_result.history.headers.response: 78 | ctype = fuzz_result.history.headers.response["Content-Type"] 79 | 80 | if which == "text": 81 | return not ctype or ( 82 | ctype and any([ctype.find(x) >= 0 for x in ["text/plain"]]) 83 | ) 84 | else: 85 | raise FuzzExceptBadAPI("Unknown content type") 86 | -------------------------------------------------------------------------------- /src/wenum/helpers/obj_dic.py: -------------------------------------------------------------------------------- 1 | from collections.abc import MutableMapping 2 | from collections import OrderedDict 3 | 4 | 5 | class CaseInsensitiveDict(MutableMapping): 6 | def __init__(self, *args, **kwargs): 7 | self.store = dict() 8 | self.proxy = dict() 9 | 10 | self.update(dict(*args, **kwargs)) # use the free update to set keys 11 | 12 | def __contains__(self, k): 13 | return k.lower() in self.proxy 14 | 15 | def __delitem__(self, k): 16 | key = self.proxy[k.lower()] 17 | 18 | del self.store[key] 19 | del self.proxy[k.lower()] 20 | 21 | def __getitem__(self, k): 22 | key = self.proxy[k.lower()] 23 | return self.store[key] 24 | 25 | def get(self, k, default=None): 26 | key = self.proxy[k.lower()] 27 | return self.store[key] if key in self.store else default 28 | 29 | def __setitem__(self, k, v): 30 | self.store[k] = v 31 | self.proxy[k.lower()] = k 32 | 33 | def __iter__(self): 34 | return iter(self.store) 35 | 36 | def __len__(self): 37 | return len(self.store) 38 | 39 | 40 | class DotDict(CaseInsensitiveDict): 41 | def __getattr__(obj, name): 42 | # Return {} if non-existent attr 43 | if name not in obj: 44 | return DotDict({}) 45 | 46 | # python 3 val = dict.get(*args, None) 47 | val = obj.get(name) 48 | return DotDict(val) if type(val) is dict else val 49 | # return DotDict(val) if type(val) is dict else DotDict({args[1]: val}) 50 | 51 | def __add__(self, other): 52 | if isinstance(other, str): 53 | return DotDict({k: v + other for k, v in self.items() if v}) 54 | elif isinstance(other, DotDict): 55 | # python 3 return DotDict({**self, **other}) 56 | new_dic = DotDict(self) 57 | new_dic.update(other) 58 | return new_dic 59 | 60 | def __radd__(self, other): 61 | if isinstance(other, str): 62 | return DotDict({k: other + v for k, v in self.items() if v}) 63 | 64 | def __getitem__(self, key): 65 | try: 66 | return super(DotDict, self).__getitem__(key) 67 | except KeyError: 68 | return DotDict({}) 69 | 70 | def __str__(self): 71 | return "\n".join( 72 | [ 73 | "{}{} {}".format(k, "->" if isinstance(v, DotDict) else ":", v) 74 | for k, v in self.items() 75 | ] 76 | ) 77 | 78 | 79 | class FixSizeOrderedDict(OrderedDict): 80 | """ 81 | An OrderedDict with a max length (FIFO when exceeding max). Simply taken over by the nice snippet from 82 | https://stackoverflow.com/questions/49274177/need-python-dictionary-to-act-like-deque-have-maximum-length 83 | """ 84 | 85 | def __init__(self, *args, maximum_length=0, **kwargs): 86 | self._max = maximum_length 87 | super().__init__(*args, **kwargs) 88 | 89 | def __setitem__(self, key, value): 90 | OrderedDict.__setitem__(self, key, value) 91 | if self._max > 0: 92 | if len(self) > self._max: 93 | self.popitem(False) 94 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/linkparser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from wenum.fuzzobjects import FuzzResult 7 | import os 8 | from urllib.parse import urlparse, urljoin 9 | import pathlib 10 | 11 | import linkfinder 12 | from wenum.plugin_api.base import BasePlugin 13 | from wenum.plugin_api.static_data import head_extensions, valid_codes 14 | from wenum.externals.moduleman.plugin import moduleman_plugin 15 | 16 | 17 | @moduleman_plugin 18 | class Linkparser(BasePlugin): 19 | name = "linkparser" 20 | author = ("MTD",) 21 | version = "0.1" 22 | summary = "Parse and extract link from JavaScript files using linkfinder" 23 | description = ("Parses links from JavaScript files using linkfinder",) 24 | category = ["active", "discovery"] 25 | priority = 99 26 | 27 | parameters = () 28 | 29 | def __init__(self, session): 30 | BasePlugin.__init__(self, session) 31 | self.linkparser_log = None 32 | # save to output file if file output active 33 | if self.session.options.output: 34 | self.linkparser_log = f"{self.session.options.output}_{self.name}" 35 | 36 | def validate(self, fuzz_result): 37 | return fuzz_result.code in valid_codes 38 | 39 | def process(self, fuzz_result: FuzzResult): 40 | endpoints = linkfinder.parser_file(fuzz_result.content, linkfinder.regex_str, 0, None) 41 | if not endpoints: 42 | return 43 | 44 | extracted_list = [] 45 | 46 | for result in endpoints: 47 | 48 | extracted_link = result["link"] 49 | 50 | if not extracted_link.isprintable(): 51 | continue 52 | 53 | extracted_list.append(f"{extracted_link}\n") 54 | 55 | target_url = urljoin(fuzz_result.url, extracted_link) # does initial fuzz url make sense? 56 | parsed_url = urlparse(target_url) 57 | 58 | filename = os.path.basename(parsed_url.path) 59 | extension = pathlib.Path(filename).suffix 60 | 61 | # dir path 62 | split_path = parsed_url.path.split("/") 63 | newpath = '/'.join(split_path[:-1]) + "/" 64 | 65 | # Send a request to the dir of the full URL endpoint 66 | dir_request = urljoin(fuzz_result.url, newpath) 67 | self.queue_url(dir_request) 68 | 69 | # add parsed path request 70 | if extension in head_extensions: 71 | self.queue_url(target_url, method="HEAD") 72 | else: 73 | self.queue_url(target_url) 74 | 75 | if self.linkparser_log: 76 | # Open file with a+ to ensure it gets created if it doesn't exist. Use the seek function to reset the 77 | # pointer and be able to read the current contents. 78 | linkparser_log_f = open(self.linkparser_log, 'a+') 79 | linkparser_log_f.seek(0) 80 | file_entries = linkparser_log_f.readlines() 81 | 82 | # All elements that are not yet in the current entries 83 | unique_lines = set(extracted_list) - set(file_entries) 84 | for line in unique_lines: 85 | linkparser_log_f.write(f'{line}') 86 | linkparser_log_f.close() 87 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/headers.py: -------------------------------------------------------------------------------- 1 | from wenum.plugin_api.base import BasePlugin 2 | from wenum.externals.moduleman.plugin import moduleman_plugin 3 | from wenum.plugin_api.static_data import HEADERS_server_headers, HEADERS_common_response_headers_regex_list, \ 4 | HEADERS_common_req_headers_regex_list 5 | 6 | import re 7 | 8 | KBASE_KEY = "http.servers" 9 | KBASE_KEY_RESP_UNCOMMON = "http.response.headers.uncommon" 10 | KBASE_KEY_REQ_UNCOMMON = "http.request.headers.uncommon" 11 | 12 | COMMON_RESPONSE_HEADERS_REGEX = re.compile( 13 | "({})".format("|".join(HEADERS_common_response_headers_regex_list)), re.IGNORECASE 14 | ) 15 | 16 | COMMON_REQ_HEADERS_REGEX = re.compile( 17 | "({})".format("|".join(HEADERS_common_req_headers_regex_list)), re.IGNORECASE 18 | ) 19 | 20 | 21 | @moduleman_plugin 22 | class Headers(BasePlugin): 23 | name = "headers" 24 | author = ("Xavi Mendez (@xmendez)",) 25 | version = "0.1" 26 | summary = "Looks for HTTP headers." 27 | description = ( 28 | "Looks for NEW HTTP headers:", 29 | "\t- Response HTTP headers associated to web servers.", 30 | "\t- Uncommon response HTTP headers.", 31 | "\t- Uncommon request HTTP headers.", 32 | "It is worth noting that, only the FIRST match of the above headers is registered.", 33 | ) 34 | category = ["info", "passive", "default"] 35 | priority = 99 36 | parameters = () 37 | 38 | def __init__(self, session): 39 | BasePlugin.__init__(self, session) 40 | 41 | def validate(self, fuzz_result): 42 | return True 43 | 44 | def check_request_header(self, header, value): 45 | header_value = None 46 | if not COMMON_REQ_HEADERS_REGEX.match(header): 47 | header_value = header 48 | 49 | if header_value is not None: 50 | if ( 51 | header_value.lower() not in self.kbase[KBASE_KEY_REQ_UNCOMMON] 52 | or KBASE_KEY_REQ_UNCOMMON not in self.kbase 53 | ): 54 | self.add_information(f"New uncommon HTTP request header: " 55 | f"[u]{header_value}[/u]: [u]{value}[/u]") 56 | self.kbase[KBASE_KEY_REQ_UNCOMMON].append(header_value.lower()) 57 | 58 | def check_response_header(self, fuzz_result, header): 59 | header_value = None 60 | if not COMMON_RESPONSE_HEADERS_REGEX.match(header): 61 | header_value = header 62 | 63 | if header_value is not None: 64 | if ( 65 | header_value.lower() not in self.kbase[KBASE_KEY_RESP_UNCOMMON] 66 | or KBASE_KEY_RESP_UNCOMMON not in self.kbase 67 | ): 68 | self.add_information(f"New uncommon HTTP response header: " 69 | f"[u]{header_value}[/u]: [u]{header_value}[/u]") 70 | self.kbase[KBASE_KEY_RESP_UNCOMMON].append(header_value.lower()) 71 | 72 | def check_server_header(self, header, value): 73 | if header.lower() in HEADERS_server_headers: 74 | if ( 75 | value.lower() not in self.kbase[KBASE_KEY] 76 | or KBASE_KEY not in self.kbase 77 | ): 78 | self.add_information(f"New HTTP server header: [u]{value}[/u]") 79 | self.kbase[KBASE_KEY].append(value.lower()) 80 | 81 | def process(self, fuzz_result): 82 | for header, value in fuzz_result.history.headers.request.items(): 83 | self.check_request_header(header, value) 84 | 85 | for header, value in fuzz_result.history.headers.response.items(): 86 | self.check_response_header(fuzz_result, header) 87 | self.check_server_header(header, value) 88 | -------------------------------------------------------------------------------- /src/wenum/facade.py: -------------------------------------------------------------------------------- 1 | from .helpers.file_func import get_home, get_path, get_config_dir 2 | from .helpers.obj_factory import Singleton 3 | from . import __version__ as version 4 | from .externals.moduleman.registrant import MulRegistrant 5 | from .externals.moduleman.loader import DirLoader 6 | from .externals.settings.settings import SettingsBase 7 | from .exception import FuzzExceptNoPluginError, FuzzExceptPluginLoadError 8 | 9 | import os 10 | 11 | 12 | ERROR_CODE = -1 13 | 14 | 15 | class Settings(SettingsBase): 16 | def get_config_file(self): 17 | config_file = "wenum-config.toml" 18 | 19 | config = os.path.join(get_config_dir(check=False), config_file) 20 | legacy_config = os.path.join(get_home(check=False), config_file) 21 | 22 | if os.path.exists(config): 23 | return config 24 | elif os.path.exists(legacy_config): 25 | return legacy_config 26 | return os.path.join(get_config_dir(check=True), config_file) 27 | 28 | def set_defaults(self): 29 | """Creating a default config""" 30 | return dict( 31 | plugins=[("bing_apikey", ""), ("shodan_apikey", "")], 32 | kbase=[ 33 | ( 34 | "discovery.blacklist", 35 | ".svg-.css-.js-.jpg-.gif-.png-.jpeg-.mov-.avi-.flv-.ico", 36 | ) 37 | ], 38 | connection=[ 39 | ("conn_delay", "90"), 40 | ("req_delay", "90"), 41 | ("retries", "3"), 42 | ("User-Agent", "wenum/%s" % version), 43 | ], 44 | general=[ 45 | ("default_printer", "json"), 46 | ("cancel_on_plugin_except", "0"), 47 | ("concurrent_plugins", "3"), 48 | ("lookup_dirs", "."), 49 | ("encode_space", "1"), 50 | ], 51 | ) 52 | 53 | 54 | class MyRegistrant(MulRegistrant): 55 | def get_plugin(self, identifier): 56 | try: 57 | return MulRegistrant.get_plugin(self, identifier) 58 | except KeyError as e: 59 | raise FuzzExceptNoPluginError( 60 | "Requested plugin %s. Error: %s" % (identifier, str(e)) 61 | ) 62 | 63 | 64 | class Facade(metaclass=Singleton): 65 | def __init__(self): 66 | 67 | self.__plugins = dict( 68 | printers=None, scripts=None, encoders=None, iterators=None, payloads=None, 69 | ) 70 | 71 | self.settings: Settings = Settings() 72 | 73 | def _load(self, cat): 74 | try: 75 | if cat not in self.__plugins: 76 | raise FuzzExceptNoPluginError("Non-existent plugin category %s" % cat) 77 | 78 | if not self.__plugins[cat]: 79 | loader_list = [] 80 | loader_list.append( 81 | DirLoader(**{"base_dir": cat, "base_path": get_path("../plugins")}) 82 | ) 83 | loader_list.append( 84 | DirLoader(**{"base_dir": cat, "base_path": get_home()}) 85 | ) 86 | self.__plugins[cat] = MyRegistrant(loader_list) 87 | 88 | return self.__plugins[cat] 89 | except Exception as e: 90 | raise FuzzExceptPluginLoadError("Error loading plugins: %s" % str(e)) 91 | 92 | def proxy(self, which): 93 | return self._load(which) 94 | 95 | def get_registrants(self): 96 | return self.__plugins.keys() 97 | 98 | def __getattr__(self, name): 99 | if name in ["printers", "payloads", "iterators", "encoders", "scripts"]: 100 | return self._load(name) 101 | else: 102 | raise AttributeError 103 | -------------------------------------------------------------------------------- /src/wenum/externals/reqresp/cache.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | from collections import defaultdict 5 | from typing import Optional 6 | 7 | from wenum.externals.reqresp.CachedResponse import CachedResponse 8 | from wenum.fuzzobjects import FuzzResult, FuzzType 9 | 10 | 11 | class HttpCache: 12 | """ 13 | The cache keeps track of all the requests that have already been enqueued, to avoid doing it multiple times. 14 | """ 15 | cache_dir = None 16 | __cache_dir_map = {} 17 | 18 | def __init__(self, cache_dir: Optional[str] = None): 19 | # cache control, a dictionary with URLs as keys and their values being lists full of the 20 | # categories that the queries were categorized as 21 | self.__cache_map = defaultdict(list) 22 | if cache_dir: 23 | self.load_cache_dir(cache_dir) 24 | 25 | def check_cache(self, url_key: str, cache_type: str = "processed", update: bool = True) -> bool: 26 | """ 27 | Checks if the URL is in the cache, usually to avoid queueing the same URL a second time. 28 | 29 | The category of the request is relevant (default: "processed"). 30 | If '/robots.txt, init_request' exists in the cache, 31 | the new request won't count as cached if it is checked against '/robots.txt, seed'. 32 | 33 | if the update bool is True (default), the function will also add the key to the cache if it did not exist yet. 34 | 35 | Returns True if it was in the cache. 36 | Returns False if it was not in the cache. 37 | """ 38 | if url_key in self.__cache_map and cache_type in self.__cache_map[url_key]: 39 | cached = True 40 | else: 41 | cached = False 42 | if update: 43 | self.__cache_map[url_key].append(cache_type) 44 | return cached 45 | 46 | def get_object_from_object_cache(self, fuzz_result: FuzzResult, key=False) -> Optional[FuzzResult]: 47 | """ 48 | Return entry in object_cache based on fuzzresult or key if provided (function for --cache-file option) 49 | """ 50 | if not self.cache_dir: 51 | return None 52 | if key is False: 53 | key = fuzz_result.history.to_cache_key() 54 | return self._fuzz_result_from_cache(key, fuzz_result) 55 | 56 | def load_cache_dir(self, directory: str) -> None: 57 | """ 58 | Method to load a cache dir into the runtime if option is set. 59 | Will keep track of a separate cache than the core cache. Responses loaded into this should run through 60 | all queues but the HttpQueue, which will check for this cache in specific 61 | """ 62 | if not os.path.isdir(directory): 63 | return 64 | cache_file = os.path.join(directory, "cache.json") 65 | if not os.path.isfile(cache_file): 66 | return 67 | self.cache_dir = directory 68 | with open(cache_file, "rb") as cache_data: 69 | self.__cache_dir_map = json.load(cache_data) 70 | 71 | def _fuzz_result_from_cache(self, key: str, fuzz_result: FuzzResult) -> FuzzResult | None: 72 | if key not in self.__cache_dir_map: 73 | return None 74 | cached = self.__cache_dir_map[key] 75 | res_copy = copy.deepcopy(fuzz_result) 76 | res_copy.item_type = FuzzType.RESULT 77 | 78 | # fuzz_result.code = cached["status"] 79 | res_copy.lines = cached["lines"] if cached["lines"] is not None else 0 80 | res_copy.words = cached["words"] if cached["words"] is not None else 0 81 | res_copy.chars = cached["chars"] if cached["chars"] is not None else 0 82 | body = None 83 | if "body" in cached and cached["body"] is not None: 84 | body = os.path.join(self.cache_dir, "body", cached["body"]) 85 | header = cached.get("headers", None) 86 | 87 | response = CachedResponse("https" if "https" in key else "http", cached["status"], body=body, header=header, 88 | length=cached["chars"]) 89 | res_copy.history._request.response = response 90 | 91 | return res_copy 92 | -------------------------------------------------------------------------------- /src/wenum/externals/reqresp/Variables.py: -------------------------------------------------------------------------------- 1 | from .TextParser import TextParser 2 | import json 3 | 4 | 5 | class Variable: 6 | def __init__(self, name, value="", extraInfo=""): 7 | self.name = name 8 | self.value = value 9 | self.initValue = value 10 | self.extraInfo = extraInfo 11 | 12 | def restore(self): 13 | self.value = self.initValue 14 | 15 | def change(self, newval): 16 | self.initValue = self.value = newval 17 | 18 | def update(self, val): 19 | self.value = val 20 | 21 | def append(self, val): 22 | self.value += val 23 | 24 | def __str__(self): 25 | return "[ %s : %s ]" % (self.name, self.value) 26 | 27 | 28 | class VariablesSet: 29 | def __init__(self): 30 | self.variables = [] 31 | self.boundary = None 32 | 33 | def names(self): 34 | dicc = [] 35 | for i in self.variables: 36 | dicc.append(i.name) 37 | 38 | return dicc 39 | 40 | def existsVar(self, name): 41 | return name in self.names() 42 | 43 | def addVariable(self, name, value="", extraInfo=""): 44 | self.variables.append(Variable(name, value, extraInfo)) 45 | 46 | def getVariable(self, name): 47 | dicc = [] 48 | for i in self.variables: 49 | if i.name == name: 50 | dicc.append(i) 51 | 52 | if len(dicc) > 1: 53 | raise Exception("Variable exists more than one time!!! :D" % name) 54 | 55 | if not dicc: 56 | var = Variable(name) 57 | self.variables.append(var) 58 | return var 59 | 60 | return dicc[0] 61 | 62 | def urlEncoded(self): 63 | return "&".join( 64 | [ 65 | "=".join([i.name, i.value]) if i.value is not None else i.name 66 | for i in self.variables 67 | ] 68 | ) 69 | 70 | def json_encoded(self): 71 | dicc = {i.name: i.value for i in self.variables} 72 | 73 | return json.dumps(dicc) 74 | 75 | def parse_json_encoded(self, cad): 76 | dicc = [] 77 | 78 | for key, value in json.loads(cad).items(): 79 | dicc.append(Variable(key, value)) 80 | 81 | self.variables = dicc 82 | 83 | def parseUrlEncoded(self, cad): 84 | dicc = [] 85 | 86 | if cad == "": 87 | dicc.append(Variable("", None)) 88 | 89 | for i in cad.split("&"): 90 | if i: 91 | var_list = i.split("=", 1) 92 | if len(var_list) == 1: 93 | dicc.append(Variable(var_list[0], None)) 94 | elif len(var_list) == 2: 95 | dicc.append(Variable(var_list[0], var_list[1])) 96 | 97 | self.variables = dicc 98 | 99 | def multipartEncoded(self): 100 | if not self.boundary: 101 | self.boundary = "---------------------------D33PB1T0R3QR3SP0B0UND4RY2203" 102 | pd = "" 103 | for i in self.variables: 104 | pd += "--" + self.boundary + "\r\n" 105 | pd += "%s\r\n\r\n%s\r\n" % ("\r\n".join(i.extraInfo), i.value) 106 | pd += "--" + self.boundary + "--\r\n" 107 | return pd 108 | 109 | def parseMultipart(self, cad, boundary): 110 | self.boundary = boundary 111 | dicc = [] 112 | tp = TextParser() 113 | tp.set_source("string", cad) 114 | 115 | while True: 116 | headers = [] 117 | if not tp.read_until('name="([^"]+)"'): 118 | break 119 | var = tp[0][0] 120 | headers.append(tp.lastFull_line.strip()) 121 | while True: 122 | tp.read_line() 123 | if tp.search("^([^:]+): (.*)$"): 124 | headers.append(tp.lastFull_line.strip()) 125 | else: 126 | break 127 | 128 | value = "" 129 | while True: 130 | tp.read_line() 131 | if not tp.search(boundary): 132 | value += tp.lastFull_line 133 | else: 134 | break 135 | 136 | if value[-2:] == "\r\n": 137 | value = value[:-2] 138 | 139 | dicc.append(Variable(var, value.strip(), headers)) 140 | 141 | self.variables = dicc 142 | -------------------------------------------------------------------------------- /src/wenum/helpers/obj_factory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from wenum.fuzzrequest import FuzzRequest 7 | from wenum.fuzzobjects import FPayloadManager 8 | import re 9 | 10 | from ..helpers.obj_dyn import ( 11 | rgetattr, 12 | rsetattr, 13 | ) 14 | from ..exception import FuzzExceptBadOptions 15 | 16 | 17 | class Singleton(type): 18 | """ Singleton metaclass. Use by defining the metaclass of a class Singleton, 19 | e.g.: class ThereCanBeOnlyOne: 20 | __metaclass__ = Singleton 21 | """ 22 | 23 | def __call__(class_, *args, **kwargs): 24 | if not class_.hasInstance(): 25 | class_.instance = super(Singleton, class_).__call__(*args, **kwargs) 26 | return class_.instance 27 | 28 | def deleteInstance(class_): 29 | """ Delete the (only) instance. This method is mainly for unittests so 30 | they can start with a clean slate. """ 31 | if class_.hasInstance(): 32 | del class_.instance 33 | 34 | def hasInstance(class_): 35 | """ Has the (only) instance been created already? """ 36 | return hasattr(class_, "instance") 37 | 38 | 39 | # #TODO it is pain for IDEs to cross reference create()functions that are taken through kwargs in the parent class. 40 | # It would be reasonable to make the create()function abstract here and let the inherited classes implement them 41 | class ObjectFactory: 42 | def __init__(self, builders: dict): 43 | # Store all builders in a str:builderclass fashion, e.g. "seed_from_options":SeedResultBuilder 44 | self._builders: dict = builders 45 | 46 | def create(self, key: str, *args, **kwargs): 47 | """ The factories are called by the create()-method. 48 | Depending on the combination of key and arguments passed, 49 | appropriate builders are called to then create objects such as FuzzPlugin""" 50 | builder = self._builders.get(key) 51 | if not builder: 52 | raise ValueError(key) 53 | return builder(*args, **kwargs) 54 | 55 | 56 | class SeedBuilderHelper: 57 | FUZZ_MARKERS_REGEX = re.compile( 58 | #TODO field value is probably irrelevant. Should be phased out after verifying 59 | r"(?P(?PFUZ(?P\d)*Z)(?P(?:\[(?P.*?)\])?))" 60 | ) 61 | REQ_ATTR = ["raw_request", "scheme", "method"] 62 | 63 | @staticmethod 64 | def _get_markers(text): 65 | return [ 66 | m.groupdict() for m in SeedBuilderHelper.FUZZ_MARKERS_REGEX.finditer(text) 67 | ] 68 | 69 | @staticmethod 70 | def get_marker_dict(fuzz_request: FuzzRequest): 71 | marker_dict_list = [] 72 | 73 | for text in [rgetattr(fuzz_request, field) for field in SeedBuilderHelper.REQ_ATTR]: 74 | marker_dict_list += SeedBuilderHelper._get_markers(text) 75 | 76 | return marker_dict_list 77 | 78 | @staticmethod 79 | def _remove_markers(freq, markers, mark_name): 80 | scheme = freq.scheme 81 | for mark in [ 82 | mark[mark_name] for mark in markers if mark[mark_name] is not None 83 | ]: 84 | for field in SeedBuilderHelper.REQ_ATTR: 85 | old_value = rgetattr(freq, field) 86 | new_value = old_value.replace(mark, "") 87 | 88 | if field == "raw_request": 89 | freq.update_from_raw_http(new_value, scheme) 90 | else: 91 | rsetattr(freq, field, new_value, None) 92 | 93 | @staticmethod 94 | def remove_nonfuzz_markers(freq, markers): 95 | SeedBuilderHelper._remove_markers(markers, "nonfuzz_marker") 96 | return freq 97 | 98 | @staticmethod 99 | def replace_markers(freq: FuzzRequest, fpm: FPayloadManager): 100 | rawReq = str(freq) 101 | rawUrl = freq.url 102 | scheme = freq.scheme 103 | 104 | for payload in [ 105 | payload for payload in fpm.get_payloads() if payload.marker is not None 106 | ]: 107 | rawUrl = rawUrl.replace(payload.marker, str(payload.value)) 108 | rawReq = rawReq.replace(payload.marker, str(payload.value)) 109 | scheme = scheme.replace(payload.marker, str(payload.value)) 110 | 111 | freq.update_from_raw_http(rawReq, scheme) 112 | freq.url = rawUrl 113 | 114 | return freq 115 | -------------------------------------------------------------------------------- /src/wenum/filters/simplefilter.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from ..exception import FuzzExceptBadOptions 4 | from wenum.filters.base_filter import BaseFilter 5 | 6 | import re 7 | 8 | 9 | class FuzzResSimpleFilter(BaseFilter): 10 | """ 11 | Filter class triggered when options such as --hc 404 are used on the cli. 12 | """ 13 | def __init__(self): 14 | super().__init__() 15 | self.show_identifier: Optional[bool] = None 16 | self.hide_identifier: Optional[bool] = None 17 | self.show_regex: Optional[bool] = None 18 | self.hide_regex: Optional[bool] = None 19 | self.codes: list[int] = [] 20 | self.words: list[int] = [] 21 | self.lines: list[int] = [] 22 | self.chars: list[int] = [] 23 | # Previously referred to as "chars" 24 | self.size: list[int] = [] 25 | self.regex: Optional[re.Pattern] = None 26 | 27 | def is_filtered(self, fuzz_result): 28 | # Check if the response should be filtered according to the identifiers (hide words, show lines, etc.) 29 | if self.show_identifier: 30 | # Filter out if not in show identifiers 31 | if ( 32 | fuzz_result.code in self.codes 33 | or fuzz_result.lines in self.lines 34 | or fuzz_result.words in self.words 35 | or fuzz_result.chars in self.size 36 | ): 37 | filtered_via_identifier = False 38 | else: 39 | filtered_via_identifier = True 40 | elif self.hide_identifier: 41 | # Filter out if in hide identifiers 42 | if ( 43 | fuzz_result.code in self.codes 44 | or fuzz_result.lines in self.lines 45 | or fuzz_result.words in self.words 46 | or fuzz_result.chars in self.size 47 | ): 48 | filtered_via_identifier = True 49 | else: 50 | filtered_via_identifier = False 51 | else: 52 | filtered_via_identifier = False 53 | 54 | if self.show_regex: 55 | # Filter if not found in regex 56 | if self.regex.search(fuzz_result.history.content): 57 | filtered_via_regex = False 58 | else: 59 | filtered_via_regex = True 60 | elif self.hide_regex: 61 | # Filter if found in regex 62 | if self.regex.search(fuzz_result.history.content): 63 | filtered_via_regex = True 64 | else: 65 | filtered_via_regex = False 66 | else: 67 | filtered_via_regex = False 68 | 69 | return filtered_via_identifier or filtered_via_regex 70 | 71 | @staticmethod 72 | def from_options(session): 73 | """ 74 | Builds Filter from user options. 75 | 76 | Returns None if no filter options have been set. 77 | """ 78 | ffilter = FuzzResSimpleFilter() 79 | 80 | try: 81 | if session.options.sr: 82 | ffilter.show_regex = True 83 | ffilter.regex = re.compile( 84 | session.options.sr, re.MULTILINE | re.DOTALL 85 | ) 86 | 87 | elif session.options.hr: 88 | ffilter.hide_regex = True 89 | ffilter.regex = re.compile( 90 | session.options.hr, re.MULTILINE | re.DOTALL 91 | ) 92 | except Exception as e: 93 | raise FuzzExceptBadOptions( 94 | "Invalid regex expression used in filter: %s" % str(e) 95 | ) 96 | 97 | if session.options.sc_list or session.options.sw_list or session.options.ss_list or session.options.sl_list: 98 | ffilter.show_identifier = True 99 | ffilter.codes = session.options.sc_list 100 | ffilter.words = session.options.sw_list 101 | ffilter.lines = session.options.sl_list 102 | ffilter.size = session.options.ss_list 103 | elif session.options.hc_list or session.options.hw_list or session.options.hs_list or session.options.hl_list: 104 | ffilter.hide_identifier = True 105 | ffilter.codes = session.options.hc_list 106 | ffilter.words = session.options.hw_list 107 | ffilter.lines = session.options.hl_list 108 | ffilter.size = session.options.hs_list 109 | 110 | if ffilter.show_regex or ffilter.hide_regex or ffilter.show_identifier or ffilter.hide_identifier: 111 | return ffilter 112 | else: 113 | return None 114 | -------------------------------------------------------------------------------- /src/wenum/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import annotations 3 | 4 | import datetime 5 | import sys 6 | import time 7 | from typing import TYPE_CHECKING, Optional 8 | 9 | if TYPE_CHECKING: 10 | from wenum.runtime_session import FuzzSession 11 | import warnings 12 | import traceback 13 | import logging 14 | 15 | from .core import Fuzzer 16 | from .exception import FuzzException 17 | from .ui.console.mvc import Controller, KeyPress 18 | from .runtime_session import FuzzSession 19 | from wenum.user_opts import Options 20 | from rich.console import Console 21 | 22 | from .fuzzobjects import FuzzStats 23 | 24 | 25 | def main(): 26 | """ 27 | Executing core wenum 28 | """ 29 | keypress: Optional[KeyPress] = None 30 | fuzzer: Optional[Fuzzer] = None 31 | session: Optional[FuzzSession] = None 32 | logger = logging.getLogger("debug_log") 33 | console = Console() 34 | exit_code = 0 35 | 36 | try: 37 | console.clear() 38 | # parse command line 39 | options = Options() 40 | parsed_args = options.configure_parser().parse_args() 41 | options.read_args(parsed_args, console) 42 | session: FuzzSession = FuzzSession(options, console).compile() 43 | 44 | fuzzer = Fuzzer(session) 45 | 46 | if not session.options.noninteractive: 47 | # initialise controller 48 | keypress = KeyPress() 49 | Controller(fuzzer, keypress) 50 | keypress.start() 51 | 52 | # Logging startup options on startup 53 | logger.info("Starting") 54 | 55 | # This loop causes the main loop of wenum to execute 56 | for res in fuzzer: 57 | pass 58 | 59 | except FuzzException as e: 60 | if fuzzer: 61 | fuzzer.session.compiled_stats.cancelled = True 62 | exception_message = "Fatal exception: {}".format(str(e)) 63 | warnings.warn(exception_message) 64 | logger.exception(exception_message) 65 | exit_code = 1 66 | except KeyboardInterrupt as e: 67 | fuzzer.session.compiled_stats.cancelled = True 68 | user_message = "Keyboard interrupt registered." 69 | warnings.warn(user_message) 70 | logger.info(user_message) 71 | exit_code = 130 # 128 + 2 for SIGINT 72 | except NotImplementedError as e: 73 | fuzzer.session.compiled_stats.cancelled = True 74 | exception_message = "Fatal exception: Error importing wenum extensions: {}".format(str(e)) 75 | logger.exception(exception_message) 76 | warnings.warn(exception_message) 77 | exit_code = 1 78 | except Exception as e: 79 | fuzzer.session.compiled_stats.cancelled = True 80 | exception_message = "Unhandled exception: {}".format(str(e)) 81 | logger.exception(exception_message) 82 | warnings.warn(exception_message) 83 | traceback.print_exc() 84 | exit_code = 1 85 | finally: 86 | if fuzzer: 87 | # When cancelling, unpause if currently paused 88 | fuzzer.resume_job() 89 | fuzzer.qmanager.stop_queues() 90 | if session: 91 | _log_runtime_stats(logger, session.compiled_stats) 92 | session.close() 93 | if keypress: 94 | keypress.cancel_job() 95 | logger.debug("Ended") 96 | sys.exit(exit_code) 97 | 98 | 99 | def _log_runtime_stats(logger: logging.Logger, stats: FuzzStats): 100 | total_time = time.time() - stats.starttime 101 | time_formatted = datetime.timedelta(seconds=int(total_time)) 102 | frequent_hits = _filter_subdirectory_hits(stats) 103 | runtime_info = f""" 104 | # Processed Requests: {stats.processed()} 105 | # Generated Plugin Requests: {stats.backfeed()} 106 | # Filtered Requests: {stats.filtered()} 107 | # Amount of Seeds: {len(stats.seed_list)} 108 | 109 | # Seed List: 110 | {stats.seed_list} 111 | 112 | # Big subdirectories: 113 | {frequent_hits} 114 | # Total Time: {str(time_formatted)}""" 115 | logger.info(runtime_info) 116 | 117 | 118 | def _filter_subdirectory_hits(stats: FuzzStats) -> str: 119 | """ 120 | stats keeps a record of the hit count within each subdirectory. This function is called to do some processing. 121 | It's only considered relevant for the user to know if there are at least x amount of hits within a subdir 122 | """ 123 | # Order alphabetically 124 | sorted_hits = dict(sorted(stats.subdir_hits.items())) 125 | line_separated_dirs = "" 126 | # Only count those that have a minimum amount of hits 127 | for subdir, hits in sorted_hits.items(): 128 | if hits > 50: 129 | line_separated_dirs += subdir + ": " + str(hits) + "\n" 130 | 131 | return line_separated_dirs 132 | -------------------------------------------------------------------------------- /src/wenum/externals/moduleman/modulefilter.py: -------------------------------------------------------------------------------- 1 | # mimicking nmap script filter 2 | 3 | 4 | # nmap --script "http-*" 5 | # Loads all scripts whose name starts with http-, such as http-auth and http-open-proxy. The argument to --script had to be in quotes to protect the wildcard from the shell. 6 | # not valid for categories! 7 | # 8 | # More complicated script selection can be done using the and, or, and not operators to build Boolean expressions. The operators have the same precedence[12] as in Lua: not is the 9 | # highest, followed by and and then or. You can alter precedence by using parentheses. Because expressions contain space characters it is necessary to quote them. 10 | # 11 | # nmap --script "not intrusive" 12 | # Loads every script except for those in the intrusive category. 13 | # 14 | # nmap --script "default or safe" 15 | # This is functionally equivalent to nmap --script "default,safe". It loads all scripts that are in the default category or the safe category or both. 16 | # 17 | # nmap --script "default and safe" 18 | # Loads those scripts that are in both the default and safe categories. 19 | # 20 | # nmap --script "(default or safe or intrusive) and not http-*" 21 | # Loads scripts in the default, safe, or intrusive categories, except for those whose names start with http-. 22 | from pyparsing import ( 23 | Word, 24 | Group, 25 | oneOf, 26 | Optional, 27 | Suppress, 28 | ZeroOrMore, 29 | Literal, 30 | alphas, 31 | alphanums, 32 | ) 33 | 34 | 35 | class Filter: 36 | def __init__(self): 37 | category = Word(alphas + "_-*", alphanums + "_-*") 38 | operator = oneOf("and or ,") 39 | neg_operator = "not" 40 | elementRef = category 41 | definition = elementRef + ZeroOrMore(operator + elementRef) 42 | nestedformula = Group( 43 | Suppress(Optional(Literal("("))) 44 | + definition 45 | + Suppress(Optional(Literal(")"))) 46 | ) 47 | neg_nestedformula = Optional(neg_operator) + nestedformula 48 | self.finalformula = neg_nestedformula + ZeroOrMore( 49 | operator + neg_nestedformula 50 | ) 51 | elementRef.setParseAction(self.__compute_element) 52 | neg_nestedformula.setParseAction(self.__compute_neg_formula) 53 | nestedformula.setParseAction(self.__compute_formula) 54 | self.finalformula.setParseAction(self.__myreduce) 55 | 56 | def __compute_neg_formula(self, tokens): 57 | if len(tokens) > 1 and tokens[0] == "not": 58 | return not tokens[1] 59 | else: 60 | return tokens[0] 61 | 62 | def __compute_element(self, tokens): 63 | item = tokens[0] 64 | wildc_index = item.find("*") 65 | 66 | if wildc_index > 0: 67 | return self.plugin.name.startswith(item[:wildc_index]) 68 | else: 69 | if isinstance(self.plugin.category, list): 70 | return item in self.plugin.category or self.plugin.name == item 71 | else: 72 | return self.plugin.category == item or self.plugin.name == item 73 | 74 | def __myreduce(self, elements): 75 | first = elements[0] 76 | for i in range(1, len(elements), 2): 77 | if elements[i] == "and": 78 | first = first and elements[i + 1] 79 | elif elements[i] == "or" or elements[i] == ",": 80 | first = first or elements[i + 1] 81 | 82 | return first 83 | 84 | def __compute_formula(self, tokens): 85 | return self.__myreduce(tokens[0]) 86 | 87 | def simple_filter(self, plugin, filter_string): 88 | ret = [] 89 | 90 | for item in filter_string.split(","): 91 | wildc_index = item.find("*") 92 | if wildc_index > 0: 93 | ret.append( 94 | ( 95 | item in plugin.category 96 | or plugin.name.startswith(item[:wildc_index]) 97 | ) 98 | ) 99 | else: 100 | ret.append((item in plugin.category or plugin.name == item)) 101 | 102 | return any(ret) 103 | 104 | def simple_filter_banned_keywords(self, filter_string): 105 | if filter_string.find("(") >= 0: 106 | return True 107 | elif filter_string.find(")") >= 0: 108 | return True 109 | elif any(x in ["or", "not", "and"] for x in filter_string.split(" ")): 110 | return True 111 | else: 112 | return False 113 | 114 | def is_visible(self, plugin, plugin_list) -> bool: 115 | self.plugin = plugin 116 | for plugin in plugin_list: 117 | if self.finalformula.parseString(plugin)[0]: 118 | return True 119 | else: 120 | return False 121 | -------------------------------------------------------------------------------- /src/wenum/externals/reqresp/TextParser.py: -------------------------------------------------------------------------------- 1 | # Covered by GPL V2.0 2 | # Coded by Carlos del Ojo Elias (deepbit@gmail.com) 3 | 4 | import sys 5 | import re 6 | 7 | 8 | class TextParser: 9 | def __init__(self): 10 | self.string = "" 11 | self.oldindex = 0 12 | self.newindex = 0 13 | self.type = "" 14 | self.lastFull_line = None 15 | self.lastline = None 16 | 17 | self.actualIndex = 0 18 | 19 | def __del__(self): 20 | if self.type == "file": 21 | self.fd.close() 22 | 23 | def __str__(self): 24 | return str(self.matches) 25 | 26 | def __iter__(self): 27 | self.actualIndex = 0 28 | return self 29 | 30 | def __next__(self): 31 | try: 32 | value = self.matches[self.actualIndex] 33 | self.actualIndex += 1 34 | return value 35 | except Exception: 36 | raise StopIteration 37 | 38 | def set_source(self, t, *args): 39 | """Se especifica el tipo de entrada. Puede ser fichero o entrada estandard 40 | 41 | Ejemplos: setSource("file","/tmp/file") 42 | setSource("stdin")\n""" 43 | 44 | if t == "file": 45 | self.type = t 46 | self.fd = open(args[0], "r") 47 | elif t == "stdin": 48 | if self.type == "file": 49 | self.fd.close() 50 | self.type = t 51 | elif t == "string": 52 | if self.type == "file": 53 | self.fd.close() 54 | self.type = t 55 | self.string = args[0] 56 | self.oldindex = 0 57 | self.newindex = 0 58 | else: 59 | print("Bad argument -- TextParser.setSource()\n") 60 | sys.exit(-1) 61 | 62 | def seekinit(self): 63 | self.oldindex = 0 64 | self.newindex = 0 65 | 66 | def read_until(self, pattern, case_sensitive=True): 67 | """Lee lineas hasta que el patron (pattern) conincide en alguna linea""" 68 | 69 | while True: 70 | if self.read_line() == 0: 71 | return False 72 | if self.search(pattern, case_sensitive) is True: 73 | break 74 | 75 | return True 76 | 77 | def search(self, pattern, case_sens=True, debug=0): 78 | """Intenta hacer Matching entre el pattern pasado por parametro y la ultima linea leida""" 79 | 80 | if not case_sens: 81 | self.regexp = re.compile(pattern, re.IGNORECASE) 82 | else: 83 | self.regexp = re.compile(pattern) 84 | self.matches = self.regexp.findall(self.lastline) 85 | j = 0 86 | for i in self.matches: 87 | if not isinstance(i, tuple): 88 | self.matches[j] = tuple([self.matches[j]]) 89 | j += 1 90 | 91 | # DEBUG PARA MATCHING 92 | if debug == 1: 93 | print(("[", self.lastline, "-", pattern, "]")) 94 | print((len(self.matches))) 95 | print(self.matches) 96 | 97 | if len(self.matches) == 0: 98 | return False 99 | else: 100 | return True 101 | 102 | def __getitem__(self, key): 103 | """Para acceder a cada uno de los patrones que coinciden, 104 | esta preparado paragrupos de patrones, no para solo un patron""" 105 | 106 | return self.matches[key] 107 | 108 | def skip(self, lines): 109 | """Salta las lines que se indiquen en el parametro""" 110 | 111 | for i in range(lines): 112 | if self.read_line() == 0: 113 | return False 114 | 115 | return True 116 | 117 | def read_line(self): 118 | """Lee la siguiente linea eliminando retornos de carro""" 119 | 120 | if self.type == "file": 121 | self.lastFull_line = self.fd.readline() 122 | elif self.type == "stdin": 123 | self.lastFull_line = input() 124 | elif self.type == "string": 125 | if self.newindex == -1: 126 | return 0 127 | 128 | if self.oldindex >= 0: 129 | self.newindex = self.string.find("\n", self.oldindex, len(self.string)) 130 | if self.newindex == -1: 131 | self.newindex = len(self.string) - 1 132 | self.lastFull_line = self.string[self.oldindex : self.newindex + 1] 133 | 134 | self.oldindex = self.newindex + 1 135 | else: 136 | self.lastFull_line = "" 137 | 138 | bytes_read = len(self.lastFull_line) 139 | 140 | s = self.lastFull_line 141 | self.lastline = s 142 | 143 | if s[-2:] == "\r\n": 144 | self.lastline = s[:-2] 145 | elif s[-1:] == "\r" or s[-1:] == "\n": 146 | self.lastline = s[:-1] 147 | 148 | return bytes_read 149 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/clone.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | from urllib.parse import urlparse 4 | 5 | from wenum.plugin_api.base import BasePlugin 6 | from wenum.externals.moduleman.plugin import moduleman_plugin 7 | import string 8 | 9 | 10 | @moduleman_plugin 11 | class Clone(BasePlugin): 12 | name = "clone" 13 | author = ("MTD",) 14 | version = "0.1" 15 | summary = "Save obtained content to disk." 16 | description = ("Save obtained content to disk. The headers and contents will be split and " 17 | "saved in separate dirs for easier post processing purposes",) 18 | category = ["active", "discovery"] 19 | priority = 99 20 | headers_folder = "headers" 21 | content_folder = "content" 22 | 23 | parameters = ( 24 | ) 25 | 26 | def __init__(self, session): 27 | BasePlugin.__init__(self, session) 28 | self.safe_chars = string.ascii_lowercase + string.ascii_uppercase + string.digits + '._/' 29 | if session.options.output: 30 | output_dir = f"{session.options.output}_{self.name}" 31 | os.makedirs(output_dir, exist_ok=True) 32 | self.output_dir = output_dir 33 | else: 34 | self.disabled = True 35 | 36 | def validate(self, fuzz_result): 37 | if fuzz_result.code != 404: 38 | return True 39 | return False 40 | 41 | def process(self, fuzz_result): 42 | # e.g. http://example.com/admin/login.php -> admin/login.php 43 | requested_path = f"{urlparse(fuzz_result.url).path.lstrip('/')}" 44 | # Sanitize the path 45 | requested_path = ''.join([c for c in requested_path if c in self.safe_chars]) 46 | 47 | # Magic taken from 48 | # https://stackoverflow.com/questions/49695477/removing-specific-duplicated-characters-from-a-string-in-python 49 | # Strips repeated slashes within the path. 50 | requested_path = "".join(k if k in "/" else "".join(v) for k, v in 51 | itertools.groupby(requested_path, lambda c: c)) 52 | 53 | # If it is the base of the dir, e.g. /admin/, add an artificial file name 54 | if requested_path.endswith('/') or requested_path == '': 55 | requested_path += 'f_index_wenum_created' 56 | else: 57 | filename = os.path.basename(requested_path) 58 | new_filename = f"f_{filename}" 59 | # Replace the filename with the new_filename 60 | requested_path = requested_path[:-len(filename)] + new_filename 61 | 62 | # Split the path. If the path was "admin/login.php", it will be ["admin", "login.php"], 63 | # and temp_path_list will be of length 2. Meaning login.php is the file f_login.php, and the entries 64 | # before are the dirs with d_admin 65 | temp_path_list = requested_path.split('/') 66 | if len(temp_path_list) > 1: 67 | first_index = 0 68 | for i in range(len(temp_path_list) - 1): 69 | temp_path_list[first_index + i] = f"d_{temp_path_list[first_index + i]}" 70 | requested_path = '/'.join(temp_path_list) 71 | 72 | # create separate paths for header file and content file to be written 73 | # prepend protocol/scheme as dir 74 | output_path_headers = os.path.join(self.output_dir, 75 | f"{fuzz_result.history.scheme}/{self.headers_folder}/{requested_path}") 76 | output_path_content = os.path.join(self.output_dir, 77 | f"{fuzz_result.history.scheme}/{self.content_folder}/{requested_path}") 78 | 79 | # If any of the final output paths is outside the root output dir, 80 | # something unwanted has happened during parsing. 81 | # Do not save the file, simply return for safety purposes. 82 | if not os.path.realpath(output_path_headers).startswith(os.path.realpath(self.output_dir)) \ 83 | or not os.path.realpath(output_path_content).startswith(os.path.realpath(self.output_dir)): 84 | self.add_exception_information(f"{output_path_headers} or {output_path_content} " 85 | f"is outside the root write directory.") 86 | return 87 | 88 | # Join response headers 89 | headers_joined = "" 90 | for item in fuzz_result.history._request.response._headers: 91 | headers_joined += f"{item[0]}:{item[1]}\n" 92 | headers_joined += "\n" 93 | 94 | # Create dir path and save headers and content 95 | os.makedirs(os.path.dirname(output_path_headers), exist_ok=True) 96 | with open(output_path_headers, "w") as f: 97 | f.write(headers_joined) 98 | 99 | content = fuzz_result.content 100 | if content and len(content) > 0: 101 | os.makedirs(os.path.dirname(output_path_content), exist_ok=True) 102 | with open(output_path_content, "w") as f: 103 | f.write(fuzz_result.content) 104 | -------------------------------------------------------------------------------- /src/wenum/helpers/file_func.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pkg_resources 4 | 5 | from chardet.universaldetector import UniversalDetector 6 | import chardet 7 | 8 | from ..exception import FuzzExceptInternalError 9 | 10 | 11 | def get_filter_help_file(): 12 | FILTER_HELP_FILE = "advanced.rst" 13 | FILTER_HELP_DEV_FILE = "../../../docs/user/advanced.rst" 14 | 15 | filter_help_text = None 16 | try: 17 | fname = pkg_resources.resource_filename("wenum", FILTER_HELP_FILE) 18 | filter_help_text = open(fname).read() 19 | except IOError: 20 | filter_help_text = open(get_path(FILTER_HELP_DEV_FILE)).read() 21 | 22 | return filter_help_text 23 | 24 | 25 | def create_dir(dir_path): 26 | if not os.path.exists(dir_path): 27 | os.makedirs(dir_path) 28 | 29 | 30 | def get_home(check=False, directory=None): 31 | path = os.path.join(os.path.expanduser("~"), ".wenum") 32 | if check: 33 | create_dir(path) 34 | 35 | return os.path.join(path, directory) if directory else path 36 | 37 | 38 | def get_config_dir(check=False): 39 | config_dir = os.environ.get("XDG_CONFIG_HOME") or os.path.join( 40 | os.path.expanduser("~"), ".config" 41 | ) 42 | wenum_config_dir = os.path.join(config_dir, "wenum") 43 | if check: 44 | create_dir(wenum_config_dir) 45 | return wenum_config_dir 46 | 47 | 48 | def get_path(directory=None): 49 | abspath = os.path.abspath(__file__) 50 | ret = os.path.dirname(abspath) 51 | 52 | return os.path.join(ret, directory) if directory else ret 53 | 54 | 55 | def find_file_in_paths(name, path): 56 | for root, dirs, files in os.walk(path): 57 | if name in files: 58 | return os.path.join(root, name) 59 | 60 | return None 61 | 62 | 63 | class FileDetOpener: 64 | typical_encodings = [ 65 | "UTF-8", 66 | "ISO-8859-1", 67 | "Windows-1251", 68 | "Shift JIS", 69 | "Windows-1252", 70 | "GB2312", 71 | "EUC-KR", 72 | "EUC-JP", 73 | "GBK", 74 | "ISO-8859-2", 75 | "Windows-1250", 76 | "ISO-8859-15", 77 | "Windows-1256", 78 | "ISO-8859-9", 79 | "Big5", 80 | "Windows-1254", 81 | ] 82 | 83 | def __init__(self, file_path, encoding=None): 84 | self.cache = [] 85 | self.file_des = open(file_path, mode="rb") 86 | self.det_encoding = encoding 87 | self.encoding_forced = False 88 | 89 | def close(self): 90 | self.file_des.close() 91 | 92 | def reset(self): 93 | self.file_des.seek(0) 94 | 95 | def __iter__(self): 96 | return self 97 | 98 | def __next__(self): 99 | decoded_line = None 100 | line = None 101 | last_error = None 102 | 103 | while decoded_line is None: 104 | 105 | while self.det_encoding is None: 106 | detect_encoding = self.detect_encoding().get("encoding", "utf-8") 107 | self.det_encoding = ( 108 | detect_encoding if detect_encoding is not None else "utf-8" 109 | ) 110 | 111 | if line is None: 112 | if self.cache: 113 | line = self.cache.pop() 114 | else: 115 | line = next(self.file_des) 116 | if not line: 117 | raise StopIteration 118 | 119 | try: 120 | decoded_line = line.decode(self.det_encoding) 121 | except UnicodeDecodeError: 122 | if last_error is not None and last_error: 123 | self.det_encoding = last_error.pop() 124 | elif last_error is None and not self.encoding_forced: 125 | last_error = list(reversed(self.typical_encodings)) 126 | last_error.append(chardet.detect(line).get("encoding")) 127 | elif not last_error: 128 | raise FuzzExceptInternalError("Unable to decode wordlist file!") 129 | 130 | decoded_line = None 131 | 132 | return decoded_line 133 | 134 | def detect_encoding(self): 135 | detector = UniversalDetector() 136 | detector.reset() 137 | 138 | for line in self.file_des: 139 | detector.feed(line) 140 | self.cache.append(line) 141 | if detector.done: 142 | break 143 | 144 | detector.close() 145 | 146 | return detector.result 147 | 148 | next = __next__ # for Python 2 149 | 150 | 151 | def open_file_detect_encoding(file_path): 152 | def detect_encoding(file_path): 153 | detector = UniversalDetector() 154 | detector.reset() 155 | 156 | with open(file_path, mode="rb") as file_to_detect: 157 | for line in file_to_detect: 158 | detector.feed(line) 159 | if detector.done: 160 | break 161 | detector.close() 162 | 163 | return detector.result 164 | 165 | if sys.version_info >= (3, 0): 166 | return open( 167 | file_path, "r", encoding=detect_encoding(file_path).get("encoding", "utf-8") 168 | ) 169 | else: 170 | return open(file_path, "r") 171 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/context.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from wenum.fuzzobjects import FuzzResult 7 | import os 8 | import pathlib 9 | from urllib.parse import urljoin, urlparse 10 | 11 | from wenum.plugin_api.base import BasePlugin 12 | from wenum.plugin_api.static_data import extension_list, extension_to_tech, dir_to_tech 13 | from wenum.externals.moduleman.plugin import moduleman_plugin 14 | 15 | 16 | @moduleman_plugin 17 | class Context(BasePlugin): 18 | """ 19 | Add requests depending on the detected file extensions 20 | E.g. will enqueue more .php stuff if .php was detected 21 | Needs a specific set of wordlists to be available 22 | """ 23 | name = "context" 24 | author = ("MTD",) 25 | version = "0.1" 26 | summary = "Create context awareness." 27 | description = ("Create context awareness.",) 28 | category = ["active", "discovery"] 29 | priority = 99 30 | 31 | parameters = () 32 | 33 | @staticmethod 34 | def last_dir_replace(newpath): 35 | path = os.path.dirname(os.path.normpath(newpath)) 36 | return os.path.join(path, '') 37 | 38 | def __init__(self, session): 39 | BasePlugin.__init__(self, session) 40 | 41 | def check_filter_options(self, fuzz_result): 42 | """ 43 | Return False if the request is filtered out 44 | """ 45 | # TODO We want to use the filter as only a display filter. Should context.py therefore also not consider 46 | # the filter when processing? Additionally, this is a check for simple filter statements and is ignored by 47 | # the autofilter or complex filter statements. If we want to uphold this logic, we need to build on it, 48 | # this is rather hacky 49 | if fuzz_result.chars in self.session.options.hs_list or fuzz_result.lines in self.session.options.hl_list or \ 50 | fuzz_result.words in self.session.options.hw_list: 51 | return False 52 | else: 53 | return True 54 | 55 | def validate(self, fuzz_result: FuzzResult): 56 | # Don't process if filtered out 57 | if not self.check_filter_options(fuzz_result) or fuzz_result.code not in [403, 200, 401]: 58 | return False 59 | 60 | # If a dir was found or if the response redirects to a dir 61 | found_dir = (fuzz_result.history.request_found_directory() or 62 | fuzz_result.history.response_redirects_to_directory()) 63 | # found a dir path AND the dir is a known tech 64 | if found_dir and \ 65 | os.path.basename(os.path.normpath(fuzz_result.history.urlparse.path)).lower() in dir_to_tech: 66 | return True 67 | # If not a dir, it must be a file 68 | else: 69 | filename = os.path.basename(fuzz_result.history.urlparse.path) 70 | extension = pathlib.Path(filename).suffix.lower() 71 | # If the file extension is a known tech 72 | if extension in extension_to_tech: 73 | return True 74 | 75 | return False 76 | 77 | def process(self, fuzz_result): 78 | path = os.path.basename(os.path.normpath(fuzz_result.history.urlparse.path)).lower() 79 | # If a dir was found or if the response redirects to a dir 80 | found_dir = (fuzz_result.history.request_found_directory() or 81 | fuzz_result.history.response_redirects_to_directory()) 82 | # found a dir path 83 | if found_dir: 84 | # If it could not determine the tech 85 | if not os.path.basename(os.path.normpath(fuzz_result.history.urlparse.path)).lower() in dir_to_tech: 86 | return 87 | tech = dir_to_tech[path] 88 | directory = path 89 | original_path = fuzz_result.history.urlparse.path 90 | 91 | # If not a dir, it must be a file 92 | else: 93 | filename = os.path.basename(fuzz_result.history.urlparse.path) 94 | extension = pathlib.Path(filename).suffix.lower() 95 | 96 | if extension not in extension_to_tech: 97 | self.add_information(f"Could not determine tech") 98 | return 99 | 100 | tech = extension_to_tech[extension] 101 | 102 | parsed_url = urlparse(fuzz_result.url) 103 | filename = os.path.basename(parsed_url.path) 104 | directory = pathlib.Path(filename).suffix.lower() 105 | # dir path 106 | split_path = parsed_url.path.split("/") 107 | original_path = '/'.join(split_path[:-1]) + "/" 108 | 109 | self.add_information(f"Detected tech [u]{tech}[/u] in path {original_path}") 110 | extensions = extension_list[tech] 111 | 112 | try: 113 | # Enqueue seeds 114 | for extension in extensions: 115 | if directory == "api": 116 | fuzzing_path = self.last_dir_replace(original_path) + "FUZZ" + extension 117 | else: 118 | fuzzing_path = original_path + "FUZZ" + extension 119 | fuzzing_path = fuzzing_path.lstrip("/") 120 | fuzzing_path = "/" + fuzzing_path 121 | fuzzing_url = urljoin(fuzz_result.url, fuzzing_path) 122 | self.queue_seed(seeding_url=fuzzing_url) 123 | except: 124 | self.add_exception_information("Failed creating queue items") 125 | -------------------------------------------------------------------------------- /src/wenum/printers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from wenum.fuzzobjects import FuzzResult, FuzzStats 8 | import json 9 | from .exception import FuzzExceptPluginError 10 | import sys 11 | from abc import abstractmethod, ABC 12 | 13 | 14 | class BasePrinter(ABC): 15 | """ 16 | Base class from which all printers should inherit. 17 | 18 | The design of splitting up the methods for printing to file and updating results serves two purposes: 19 | - Allow for reducing the amount of file writes, which may become costly (though admittedly untested assumption) 20 | - Ensure that the user has a valid output file even *during* the runtime, instead of an architecture where 21 | the file is only valid after the footer has been inserted at the end of a properly closed runtime 22 | """ 23 | def __init__(self, output: str, verbose: bool): 24 | self.outputfile_handle = None 25 | # List containing every result information 26 | self.result_list = [] 27 | if output: 28 | self.outputfile_handle = open(output, "w") 29 | else: 30 | self.outputfile_handle = sys.stdout 31 | 32 | self.verbose = verbose 33 | 34 | @abstractmethod 35 | def header(self, summary: FuzzStats): 36 | """ 37 | Print at the beginning of the file 38 | """ 39 | raise FuzzExceptPluginError("Method header not implemented") 40 | 41 | @abstractmethod 42 | def footer(self, summary: FuzzStats): 43 | """ 44 | Print at the end of the file. Will also be called when runtime is done 45 | """ 46 | raise FuzzExceptPluginError("Method footer not implemented") 47 | 48 | @abstractmethod 49 | def update_results(self, fuzz_result: FuzzResult, stats: FuzzStats): 50 | """ 51 | Update the result list during runtime. This does not print to file yet 52 | """ 53 | raise FuzzExceptPluginError("Method result not implemented") 54 | 55 | @abstractmethod 56 | def print_to_file(self) -> None: 57 | """ 58 | Overwrite output file contents with data 59 | """ 60 | raise FuzzExceptPluginError("Method result not implemented") 61 | 62 | 63 | class HTML(BasePrinter): 64 | def __init__(self, output, verbose): 65 | super().__init__(output, verbose) 66 | 67 | def header(self, stats): 68 | pass 69 | 70 | def update_results(self, fuzz_result, stats): 71 | pass 72 | 73 | def print_to_file(self): 74 | pass 75 | 76 | def footer(self, summary: FuzzStats): 77 | pass 78 | 79 | 80 | class JSON(BasePrinter): 81 | name = "json" 82 | summary = "Results in json format" 83 | 84 | def __init__(self, output, verbose): 85 | super().__init__(output, verbose) 86 | 87 | def header(self, stats: FuzzStats): 88 | # Empty JSON header to avoid messing up the file structure 89 | pass 90 | 91 | def update_results(self, fuzz_result: FuzzResult, stats: FuzzStats): 92 | location = "" 93 | if fuzz_result.history.redirect_header: 94 | location = fuzz_result.history.full_redirect_url 95 | server = "" 96 | if "Server" in fuzz_result.history.headers.response: 97 | server = fuzz_result.history.headers.response["Server"] 98 | post_data = [] 99 | if fuzz_result.history.method.lower() == "post": 100 | for n, v in list(fuzz_result.history.params.post.items()): 101 | post_data.append({"parameter": n, "value": v}) 102 | 103 | plugin_dict = {} 104 | 105 | for plugin in fuzz_result.plugins_res: 106 | # Removing ansi color escapes when logging, which plugins may 107 | # have inserted (magic from https://stackoverflow.com/a/14693789) 108 | # 7-bit C1 ANSI sequences 109 | ansi_escape = re.compile(r""" 110 | \x1B # ESC 111 | (?: # 7-bit C1 Fe (except CSI) 112 | [@-Z\\-_] 113 | | # or [ for CSI, followed by a control sequence 114 | \[ 115 | [0-?]* # Parameter bytes 116 | [ -/]* # Intermediate bytes 117 | [@-~] # Final byte 118 | ) 119 | """, re.VERBOSE) 120 | result = ansi_escape.sub('', plugin.message) 121 | plugin_dict[plugin.name] = result 122 | 123 | res_entry = { 124 | "result_number": fuzz_result.result_number, 125 | "code": fuzz_result.code, 126 | "lines": fuzz_result.lines, 127 | "words": fuzz_result.words, 128 | "chars": fuzz_result.chars, 129 | "method": fuzz_result.history.method, 130 | "url": fuzz_result.url, 131 | "location": location, 132 | "post_data": post_data, 133 | "server": server, 134 | "description": fuzz_result.description, 135 | "plugins": plugin_dict 136 | } 137 | self.result_list.append(res_entry) 138 | return self.result_list 139 | 140 | def print_to_file(self): 141 | self.outputfile_handle.write(json.dumps(self.result_list)) 142 | self.outputfile_handle.flush() 143 | # Resetting the file pointer so that the next file write overwrites the content 144 | self.outputfile_handle.seek(0) 145 | 146 | def footer(self, stats: FuzzStats): 147 | # Empty JSON footer to avoid messing up the file structure 148 | pass 149 | -------------------------------------------------------------------------------- /src/wenum/externals/moduleman/registrant.py: -------------------------------------------------------------------------------- 1 | from .modulefilter import Filter 2 | from collections import defaultdict 3 | 4 | from collections.abc import MutableMapping 5 | from threading import Lock 6 | 7 | 8 | class IRegistrant: 9 | def __init__(self, loader, plg_filter): 10 | self.plg_filter = plg_filter 11 | self.loader = loader 12 | 13 | self.start_loading() 14 | self.load() 15 | self.end_loading() 16 | 17 | def register(self, identifier, module): 18 | raise NotImplementedError 19 | 20 | def start_loading(self): 21 | raise NotImplementedError 22 | 23 | def load(self): 24 | raise NotImplementedError 25 | 26 | def end_loading(self): 27 | raise NotImplementedError 28 | 29 | def modify_instance(self, module): 30 | raise NotImplementedError 31 | 32 | 33 | class KnowledgeBase(MutableMapping): 34 | def __init__(self, *args, **kwargs): 35 | self.__data = defaultdict(list) 36 | self.mutex = Lock() 37 | 38 | def __getitem__(self, key): 39 | with self.mutex: 40 | return self.__data[key] 41 | 42 | def __setitem__(self, key, value): 43 | with self.mutex: 44 | self.__data[key].append(value) 45 | 46 | def __delitem__(self, key): 47 | with self.mutex: 48 | del self.__data[key] 49 | 50 | def __len__(self): 51 | with self.mutex: 52 | return len(self.__data) 53 | 54 | def __str__(self): 55 | with self.mutex: 56 | return str(self.__data) 57 | 58 | def __iter__(self): 59 | return iter(self.__data) 60 | 61 | 62 | class BRegistrant(IRegistrant): 63 | def __init__(self, loader, plg_filter=Filter()): 64 | self.__plugins = {} 65 | self.__active_plugins = {} 66 | self.kbase = KnowledgeBase() 67 | 68 | IRegistrant.__init__(self, loader, plg_filter) 69 | 70 | def register(self, identifier, module): 71 | self.__plugins[identifier] = self.modify_instance(module) 72 | self.__active_plugins[identifier] = True 73 | 74 | def load(self): 75 | self.loader.load(self) 76 | 77 | def start_loading(self): 78 | pass 79 | 80 | def end_loading(self): 81 | pass 82 | 83 | def modify_instance(self, module): 84 | module.kbase = self.kbase 85 | 86 | return module 87 | 88 | # ------------------------------------------------ 89 | # plugin management functions 90 | # ------------------------------------------------ 91 | def plugin_state(self, identifier, state): 92 | self.__active_plugins[identifier] = state 93 | 94 | def __get_plugins(self, plugin_list: list[str], sorting): 95 | def plugin_filter(plugin_tuple) -> bool: 96 | """ 97 | Takes a plugin tuple and checks if it is within the user supplied plugin_list 98 | 99 | Returns False if it isn't, and true if it is 100 | 101 | :param plugin_tuple: 102 | :return: 103 | """ 104 | plugin_name, plugin_class = plugin_tuple 105 | 106 | if plugin_list == "$all$": 107 | return True 108 | elif not self.__active_plugins[plugin_name]: 109 | return False 110 | else: 111 | return self.plg_filter.is_visible(plugin_class, plugin_list) 112 | 113 | def key_funtion(x): 114 | return x[1].priority 115 | 116 | plugin_list = list(filter(plugin_filter, list(self.__plugins.items()))) 117 | 118 | if sorting: 119 | plugin_list.sort(key=key_funtion) 120 | 121 | return plugin_list 122 | 123 | def get_plugin(self, identifier): 124 | # strict and fuzzy search 125 | if identifier in self.__plugins: 126 | return self.__plugins[identifier] 127 | else: 128 | plugin_list = [ 129 | plg 130 | for plg_id, plg in self.__get_plugins("$all$", True) 131 | if identifier in plg_id 132 | ] 133 | 134 | if not plugin_list: 135 | raise KeyError("No plugins found!") 136 | elif len(plugin_list) == 1: 137 | return plugin_list[0] 138 | else: 139 | raise KeyError( 140 | "Multiple plugins found: %s" 141 | % ",".join([plg.name for plg in plugin_list]) 142 | ) 143 | 144 | def get_plugins(self, category="$all$", sorting="true"): 145 | return [plg for plg_id, plg in self.__get_plugins(category, sorting)] 146 | 147 | def get_plugins_ext(self, category="$all$", sorting="true"): 148 | plugin_list = [["Id", "Priority", "Category", "Name", "Summary"]] 149 | 150 | for plg_id, plg in self.__get_plugins(category, sorting): 151 | plugin_list.append( 152 | [ 153 | plg_id, 154 | str(plg.priority), 155 | ", ".join(plg.category), 156 | str(plg.name), 157 | str(plg.summary), 158 | ] 159 | ) 160 | 161 | return plugin_list 162 | 163 | def get_plugins_names(self, category="$all$", sorting="true"): 164 | return [plg.name for plg_id, plg in self.__get_plugins(category, sorting)] 165 | 166 | def get_plugins_ids(self, category="$all$", sorting="true"): 167 | return [plg_id for plg_id, plg in self.__get_plugins(category, sorting)] 168 | 169 | 170 | class MulRegistrant(BRegistrant): 171 | def load(self): 172 | for loader in self.loader: 173 | loader.load(self) 174 | -------------------------------------------------------------------------------- /src/wenum/runtime_session.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from typing import Optional 4 | from rich.console import Console 5 | 6 | from .exception import ( 7 | FuzzExceptBadOptions, FuzzExceptInternalError, 8 | ) 9 | 10 | from .factories.fuzzresfactory import resfactory 11 | from .factories.dictfactory import dictionary_factory 12 | from .fuzzobjects import FuzzStats, FuzzResult 13 | from .filters.complexfilter import FuzzResFilter 14 | from .filters.simplefilter import FuzzResSimpleFilter 15 | 16 | from .core import Fuzzer 17 | from .iterators import BaseIterator 18 | from .httppool import HttpPool 19 | 20 | from .externals.reqresp.cache import HttpCache 21 | from .printers import JSON, HTML, BasePrinter 22 | from wenum.user_opts import Options 23 | 24 | # The priority moves in steps of 10 to allow a buffer zone for future finegrained control. This way, one group of 25 | # requests (such as within a seed) has leverage over queuing with less prio than the other requests while being 26 | # prioritized higher than the next group of requests (e.g. the next seed) 27 | PRIORITY_STEP = 10 28 | 29 | 30 | class FuzzSession: 31 | """Class designed to carry runtime information relevant for conditional decisions""" 32 | def __init__(self, options: Options, console: Console): 33 | self.options: Options = options 34 | self.logger = logging.getLogger("debug_log") 35 | self.console = console 36 | 37 | self.compiled_stats: Optional[FuzzStats] = None 38 | self.compiled_filter: Optional[FuzzResFilter] = None 39 | self.compiled_simple_filter: Optional[FuzzResSimpleFilter] = None 40 | self.compiled_seed: Optional[FuzzResult] = None 41 | self.compiled_printer_list: list[BasePrinter] = [] 42 | self.compiled_iterator: Optional[BaseIterator] = None 43 | self.current_priority_level: int = PRIORITY_STEP 44 | 45 | self.cache: HttpCache = HttpCache(cache_dir=self.options.cache_dir) 46 | self.http_pool: Optional[HttpPool] = None 47 | 48 | #TODO Unused? 49 | self.stats = FuzzStats() 50 | 51 | def assign_next_priority_level(self): 52 | """ 53 | Pulls current priority level, increases it and returns the value. Useful for assigning new level 54 | to new recursions 55 | """ 56 | self.current_priority_level += PRIORITY_STEP 57 | return self.current_priority_level 58 | 59 | def fuzz(self, **kwargs): 60 | """Method used by the API""" 61 | #self.data.update(kwargs) 62 | 63 | fuzzer = None 64 | try: 65 | fuzzer = Fuzzer(self.compile()) 66 | 67 | for f in fuzzer: 68 | yield f 69 | 70 | finally: 71 | if fuzzer: 72 | fuzzer.qmanager.stop_queues() 73 | self.stats.update(self.compiled_stats) 74 | 75 | def __exit__(self, *args): 76 | self.close() 77 | 78 | def get_fuzz_words(self) -> set: 79 | """ 80 | #TODO Verify this is polling the amount of FUZZ words supplied by the user 81 | """ 82 | if self.compiled_filter: 83 | fuzz_words = self.compiled_filter.get_fuzz_words() 84 | else: 85 | fuzz_words = [] 86 | 87 | if self.compiled_seed: 88 | fuzz_words += self.compiled_seed.payload_man.get_fuzz_words() 89 | 90 | return set(fuzz_words) 91 | 92 | def compile_iterator(self): 93 | self.compiled_iterator = dictionary_factory.create( 94 | "dictio_from_options", self 95 | ) 96 | 97 | def compile_seeds(self): 98 | self.compiled_seed = resfactory.create("seed_from_options", self) 99 | 100 | def compile(self): 101 | """ 102 | Sets some things before actually running 103 | """ 104 | 105 | self.options.basic_validate() 106 | 107 | if self.options.dump_config: 108 | self.options.export_config() 109 | print(f"Config written into {self.options.dump_config}.") 110 | sys.exit(0) 111 | 112 | if self.options.output: 113 | if self.options.output_format == "html": 114 | self.compiled_printer_list.append(HTML(self.options.output, self.options.verbose)) 115 | elif self.options.output_format == "json": 116 | self.compiled_printer_list.append(JSON(self.options.output, self.options.verbose)) 117 | elif self.options.output_format == "all": 118 | self.compiled_printer_list.append(JSON(self.options.output + ".json", self.options.verbose)) 119 | self.compiled_printer_list.append(HTML(self.options.output + ".html", self.options.verbose)) 120 | else: 121 | raise FuzzExceptInternalError("Error encountered while parsing the printers. This state should not" 122 | "be reachable. It would be very much appreciated if " 123 | "you created an issue.") 124 | 125 | self.compile_seeds() 126 | self.compile_iterator() 127 | 128 | # filter options 129 | self.compiled_simple_filter = FuzzResSimpleFilter.from_options(self) 130 | if self.options.filter: 131 | self.compiled_filter = FuzzResFilter(self.options.filter) 132 | 133 | self.compiled_stats = FuzzStats.from_options(self) 134 | 135 | # Check payload num 136 | fuzz_words = self.get_fuzz_words() 137 | 138 | if self.compiled_iterator.width() != len(fuzz_words): 139 | raise FuzzExceptBadOptions("FUZZ words and number of payloads do not match!") 140 | 141 | if not self.http_pool: 142 | self.http_pool = HttpPool(self) 143 | 144 | return self 145 | 146 | def close(self): 147 | """ 148 | Actions to execute before shutting down the runtime. 149 | """ 150 | if self.compiled_iterator: 151 | self.compiled_iterator.cleanup() 152 | -------------------------------------------------------------------------------- /src/wenum/externals/moduleman/loader.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import logging 4 | import os.path 5 | 6 | 7 | class IModuleLoader: 8 | def __init__(self, **params): 9 | self.set_params(**params) 10 | 11 | def set_params(self, **params): 12 | raise NotImplementedError 13 | 14 | def load(self, registrant): 15 | raise NotImplementedError 16 | 17 | 18 | class FileLoader(IModuleLoader): 19 | def __init__(self, **params): 20 | IModuleLoader.__init__(self, **params) 21 | self.__logger = logging.getLogger("libraries.FileLoader") 22 | 23 | def set_params(self, **params): 24 | if "base_path" not in params: 25 | return 26 | elif "filename" not in params: 27 | return 28 | 29 | self.filename = params["filename"] 30 | self.base_path = params["base_path"] 31 | if self.base_path.endswith("/"): 32 | self.base_path = self.base_path[:-1] 33 | 34 | def load(self, registrant): 35 | self.module_registrant = registrant 36 | 37 | self._load_py_from_file(os.path.join(self.base_path, self.filename)) 38 | 39 | def _build_id(self, filename, objname): 40 | filepath, filename = os.path.split(filename) 41 | 42 | relative_path = os.path.relpath(filepath, self.base_path) 43 | identifier = relative_path + "/" + objname 44 | if identifier.startswith("./"): 45 | identifier = identifier[2:] 46 | 47 | return identifier 48 | 49 | def _load_py_from_file(self, filename): 50 | """ 51 | Opens "filename", inspects it and calls the registrant 52 | """ 53 | self.__logger.debug("__load_py_from_file. START, file=%s" % (filename,)) 54 | 55 | #dirname, filename = os.path.split(filename) 56 | fn = os.path.splitext(filename)[0] 57 | exten_file = None 58 | module = None 59 | 60 | try: 61 | spec = importlib.util.spec_from_file_location(fn, filename) 62 | if not spec: 63 | raise ImportError("No spec for %s" % (filename,)) 64 | module = importlib.util.module_from_spec(spec) 65 | spec.loader.exec_module(module) 66 | except ImportError as msg: 67 | self.__logger.critical( 68 | "__load_py_from_file. Filename: %s Exception, msg=%s" % (filename, msg) 69 | ) 70 | # raise msg 71 | pass 72 | except SyntaxError as msg: 73 | # incorrect python syntax in file 74 | self.__logger.critical( 75 | "__load_py_from_file. Filename: %s Exception, msg=%s" % (filename, msg) 76 | ) 77 | # raise msg 78 | pass 79 | finally: 80 | if exten_file: 81 | exten_file.close() 82 | 83 | if module is None: 84 | return 85 | 86 | for objname in dir(module): 87 | obj = getattr(module, objname) 88 | self.__logger.debug("__load_py_from_file. inspecting=%s" % (objname,)) 89 | if inspect.isclass(obj): 90 | if "__PLUGIN_MODULEMAN_MARK" in dir(obj): 91 | if self.module_registrant: 92 | self.module_registrant.register( 93 | self._build_id(filename, objname), obj 94 | ) 95 | 96 | self.__logger.debug("__load_py_from_file. END, loaded file=%s" % (filename,)) 97 | 98 | 99 | class DirLoader(FileLoader): 100 | def __init__(self, **params): 101 | FileLoader.__init__(self, **params) 102 | self.__logger = logging.getLogger("libraries.DirLoader") 103 | 104 | def set_params(self, **params): 105 | if "base_dir" not in params: 106 | return 107 | elif "base_path" not in params: 108 | return 109 | 110 | self.base_dir = params["base_dir"] 111 | self.base_path = params["base_path"] 112 | if self.base_path.endswith("/"): 113 | self.base_path = self.base_path[:-1] 114 | 115 | def load(self, registrant): 116 | self.module_registrant = registrant 117 | self.structure = self.__load_all(self.base_dir) 118 | 119 | def _build_id(self, filename, objname): 120 | filepath, filename = os.path.split(filename) 121 | 122 | relative_path = os.path.relpath( 123 | filepath, os.path.join(self.base_path, self.base_dir) 124 | ) 125 | identifier = relative_path + "/" + objname 126 | if identifier.startswith("./"): 127 | identifier = identifier[2:] 128 | 129 | return identifier 130 | 131 | def __load_all(self, dir_name): 132 | """ 133 | loads all plugins and creates a loaded list of scripts from directory plugins like: 134 | [ ( category,[script1, script2,...] ), (category2,[script1, (subcategory,[script1,script2]),...]) ] 135 | """ 136 | walked = [] 137 | 138 | current = os.path.join(self.base_path, dir_name) 139 | if os.path.isdir(current): 140 | dir_list = self.__walk_dir_tree(current) 141 | walked.append((current, dir_list)) 142 | if self.module_registrant: 143 | self.module_registrant.end_loading() 144 | 145 | return walked 146 | 147 | def __walk_dir_tree(self, dirname): 148 | dir_list = [] 149 | 150 | self.__logger.debug("__walk_dir_tree. START dir=%s", dirname) 151 | 152 | for f in os.listdir(dirname): 153 | current = os.path.join(dirname, f) 154 | if os.path.isfile(current) and f.endswith("py"): 155 | if self.module_registrant: 156 | self._load_py_from_file(current) 157 | 158 | dir_list.append(current) 159 | elif os.path.isdir(current): 160 | ret = self.__walk_dir_tree(current) 161 | if ret: 162 | dir_list.append((f, ret)) 163 | 164 | return dir_list 165 | -------------------------------------------------------------------------------- /src/wenum/plugin_api/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import TYPE_CHECKING, Optional 5 | 6 | if TYPE_CHECKING: 7 | from wenum.runtime_session import FuzzSession 8 | from queue import Queue 9 | from wenum.fuzzobjects import FuzzPlugin, FuzzResult 10 | from wenum.exception import ( 11 | FuzzExceptBadOptions, 12 | FuzzExceptPluginError, 13 | ) 14 | from wenum.factories.plugin_factory import plugin_factory 15 | from wenum.externals.reqresp.cache import HttpCache 16 | 17 | from abc import abstractmethod 18 | from distutils import util 19 | from threading import Event, Condition 20 | 21 | 22 | class BasePlugin: 23 | """ 24 | Base class for all other plugins that exist 25 | """ 26 | 27 | def __init__(self, session: FuzzSession): 28 | # Setting disabled to true will cause it not to execute for future requests anymore 29 | self.disabled = False 30 | # The results queue is the queue receiving all the plugin output. PluginExecutor will later read it 31 | self.results_queue: Optional[Queue] = None 32 | # Bool indicating whether plugin should only be run once. PluginExecutor will disable after first execution 33 | self.run_once = False 34 | # Plugins might adjust the FuzzResult object passed into them. This contains the original state 35 | self.base_fuzz_res: Optional[FuzzResult] = None 36 | self.cache = HttpCache() 37 | self.interrupt: Optional[Event] = None 38 | self.session: FuzzSession = session 39 | self.logger = logging.getLogger("debug_log") 40 | 41 | # check mandatory params, assign default values 42 | for name, default_value, required, description in self.parameters: 43 | param_name = f"{self.name}.{name}" 44 | 45 | if required and param_name not in list(self.kbase.keys()): 46 | raise FuzzExceptBadOptions( 47 | "Plugins, missing parameter %s!" % (param_name,) 48 | ) 49 | 50 | if param_name not in list(self.kbase.keys()): 51 | self.kbase[param_name] = default_value 52 | 53 | def run(self, fuzz_result: FuzzResult, plugin_finished: Event, condition: Condition, interrupt_signal: Event, results_queue: Queue) -> None: 54 | """ 55 | Will be triggered by PluginExecutor 56 | """ 57 | try: 58 | self.interrupt = interrupt_signal 59 | self.results_queue = results_queue 60 | self.base_fuzz_res = fuzz_result 61 | self.process(fuzz_result) 62 | except Exception as e: 63 | self.logger.exception(f"An exception occured while running the plugin {self.name}") 64 | exception_plugin = plugin_factory.create("plugin_from_error", self.name, e) 65 | results_queue.put(exception_plugin) 66 | finally: 67 | # Signal back completion of execution 68 | plugin_finished.set() 69 | with condition: 70 | condition.notify() 71 | return 72 | 73 | @abstractmethod 74 | def process(self, fuzz_result: FuzzResult) -> None: 75 | """ 76 | This is where the plugin processing is done. Any wenum plugin must implement this method 77 | 78 | A kbase (get_kbase, has_kbase, add_kbase) is shared between all plugins. this can be used to store and 79 | retrieve relevant "collaborative" information. 80 | """ 81 | raise NotImplementedError 82 | 83 | @abstractmethod 84 | def validate(self, fuzz_result: FuzzResult) -> bool: 85 | """ 86 | Function to poll whether the plugin should be executed for the current result. 87 | PluginExecutor skips the plugin if it does not validate 88 | """ 89 | raise FuzzExceptPluginError("Method count not implemented") 90 | 91 | def add_information(self, message: str, severity=FuzzPlugin.INFO) -> None: 92 | """ 93 | Add some information to the result queue. It will be printed out for the user to see. 94 | Optionally specify severity 95 | """ 96 | self.put_if_okay(plugin_factory.create("plugin_from_finding", self.name, message, severity)) 97 | 98 | def add_exception_information(self, exception: str) -> None: 99 | """ 100 | Add some exception information to the result queue. It will be printed out for the user to see 101 | """ 102 | self.logger.warning(f"The plugin {self.name} has added exception information: {exception}") 103 | self.put_if_okay(plugin_factory.create("plugin_from_error", self.name, exception)) 104 | 105 | def queue_url(self, url: str, method: str = "GET") -> None: 106 | """ 107 | Enqueue a new full URL. It will be processed by PluginExecutor, and if it is valid 108 | (not already in cache + in scope) will be queued to be sent by wenum 109 | """ 110 | self.put_if_okay(plugin_factory.create( 111 | "backfeed_plugin", self.name, self.base_fuzz_res, url, method)) 112 | 113 | def queue_seed(self, seeding_url): 114 | """ 115 | Enqueue a new SEED (full recursion). It will be processed by PluginExecutor, and if 116 | it is valid will be queued to be sent by wenum 117 | Optionally takes seeding_url. Can be arbitrarily specified to use as a new FUZZ 118 | """ 119 | # Stop queueing seeds if the limit is reached already 120 | if self.session.options.limit_requests and self.session.http_pool.queued_requests > \ 121 | self.session.options.limit_requests: 122 | return 123 | self.put_if_okay(plugin_factory.create( 124 | "seed_plugin", self.name, self.base_fuzz_res, seeding_url)) 125 | 126 | def put_if_okay(self, fuzz_plugin) -> None: 127 | """ 128 | Checks for the interrupt signal for plugins. If the interrupt is set, nothing will be put into 129 | the result queue. Otherwise, it will simply do so. 130 | """ 131 | if self.interrupt.is_set(): 132 | return 133 | else: 134 | self.results_queue.put(fuzz_plugin) 135 | 136 | @staticmethod 137 | def _bool(value) -> bool: 138 | return bool(util.strtobool(value)) 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/links.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from urllib.parse import urljoin 5 | import pathlib 6 | 7 | from wenum.plugin_api.mixins import DiscoveryPluginMixin 8 | from wenum.plugin_api.base import BasePlugin 9 | from wenum.plugin_api.static_data import valid_codes, head_extensions 10 | from wenum.plugin_api.urlutils import parse_url 11 | from wenum.externals.moduleman.plugin import moduleman_plugin 12 | 13 | 14 | @moduleman_plugin 15 | class Links(BasePlugin, DiscoveryPluginMixin): 16 | """ 17 | # TODO We want to phase this out. Functionality should be replaced by follow_redirects and linkparser.py. 18 | Make sure linkparser does not miss things this plugin catches, and delete plugin 19 | """ 20 | name = "links" 21 | author = ("Xavi Mendez (@xmendez)",) 22 | version = "0.1" 23 | summary = "Parses HTML looking for new content." 24 | description = ("Parses HTML looking for new content. Redirects to dirs of URLs and to files of them if proper " 25 | "URLs are detected through regex statements.",) 26 | category = ["active", "discovery"] 27 | priority = 99 28 | 29 | limit = 10 30 | 31 | parameters = ( 32 | ( 33 | "add_path", 34 | "False", 35 | False, 36 | "if True, re-enqueue found paths. ie. /path/link.html link enqueues also /path/", 37 | ), 38 | ( 39 | "domain", 40 | None, 41 | False, 42 | "Regex of accepted domains tested against url.netloc. This is useful for restricting crawling certain domains.", 43 | ), 44 | ( 45 | "regex", 46 | None, 47 | False, 48 | "Regex of accepted links tested against the full url. If domain is not set and regex is, domain defaults to .*. This is useful for restricting crawling certain file types.", 49 | ), 50 | ) 51 | 52 | def __init__(self, session): 53 | BasePlugin.__init__(self, session) 54 | 55 | # Detect links based on these regex statements 56 | regex = [ 57 | r'\b(?:(?', # http://en.wikipedia.org/wiki/Meta_refresh 61 | r'getJSON\("(.*?)"', 62 | r"[^/][`'\"]([\/][a-zA-Z0-9_.-]+)+(?!(?:[,;\s]))", # based on https://github.com/nahamsec/JSParser/blob/master/handler.py#L93 63 | ] 64 | 65 | self.regex = [] 66 | for regex_str in regex: 67 | self.regex.append(re.compile(regex_str, re.MULTILINE | re.DOTALL)) 68 | 69 | self.regex_header = [ 70 | ("Link", re.compile(r"<(.*)>;")), 71 | ("Location", re.compile(r"(.*)")), 72 | ] 73 | 74 | self.add_path = self._bool(self.kbase["links.add_path"][0]) 75 | 76 | self.domain_regex = None 77 | if self.kbase["links.domain"][0]: 78 | self.domain_regex = re.compile( 79 | self.kbase["links.domain"][0], re.IGNORECASE 80 | ) 81 | 82 | self.regex_param = None 83 | if self.kbase["links.regex"][0]: 84 | self.regex_param = re.compile( 85 | self.kbase["links.regex"][0], re.IGNORECASE 86 | ) 87 | 88 | if self.regex_param and self.domain_regex is None: 89 | self.domain_regex = re.compile(".*", re.IGNORECASE) 90 | 91 | self.list_links = set() 92 | 93 | def validate(self, fuzz_result): 94 | self.list_links = set() 95 | return fuzz_result.code in valid_codes 96 | 97 | def process(self, fuzz_result): 98 | # O 99 | # ParseResult(scheme='', netloc='', path='www.owasp.org/index.php/OWASP_EU_Summit_2008', params='', query='', fragment='') 100 | 101 | for header, regex in self.regex_header: 102 | if header in fuzz_result.history.headers.response: 103 | all_links = regex.findall(fuzz_result.history.headers.response[header]) 104 | for link_url in all_links: 105 | if link_url: 106 | self.process_link(fuzz_result, link_url) 107 | 108 | for regex in self.regex: 109 | all_links = regex.findall(fuzz_result.history.content) 110 | if not all_links: 111 | continue 112 | for link_url in all_links: 113 | if link_url: 114 | self.process_link(fuzz_result, link_url) 115 | 116 | def process_link(self, fuzz_result, link_url): 117 | parsed_link = parse_url(link_url) 118 | 119 | if ( 120 | not parsed_link.scheme 121 | or parsed_link.scheme == "http" 122 | or parsed_link.scheme == "https" 123 | ) and self.from_domain(parsed_link): 124 | #TODO Cache key does not need to be manually checked. PluginManager handles centrally 125 | cache_key = parsed_link.cache_key(self.base_fuzz_res.history.urlparse) 126 | if cache_key not in self.list_links: 127 | self.list_links.add(cache_key) 128 | self.enqueue_link(fuzz_result, link_url, parsed_link) 129 | 130 | def enqueue_link(self, fuzz_result, link_url, parsed_link): 131 | filename = os.path.basename(parsed_link.path) 132 | extension = pathlib.Path(filename).suffix 133 | 134 | # dir path 135 | if self.add_path: 136 | split_path = parsed_link.path.split("/") 137 | newpath = "/".join(split_path[:-1]) + "/" 138 | full_path_url = urljoin(fuzz_result.url, newpath) 139 | self.queue_url(full_path_url) 140 | 141 | # file path 142 | new_link = urljoin(fuzz_result.url, link_url) 143 | 144 | if not self.regex_param or ( 145 | self.regex_param and self.regex_param.search(new_link) is not None 146 | ): 147 | if extension in head_extensions: 148 | self.queue_url(new_link, method="HEAD") 149 | else: 150 | self.queue_url(new_link) 151 | 152 | def from_domain(self, parsed_link): 153 | # Returns True if it is a relative path 154 | if not parsed_link.netloc and parsed_link.path: 155 | return True 156 | 157 | # regex domain 158 | if ( 159 | self.domain_regex 160 | and self.domain_regex.search(parsed_link.netloc) is not None 161 | ): 162 | return True 163 | 164 | # same domain 165 | if parsed_link.netloc == self.base_fuzz_res.history.urlparse.netloc: 166 | return True 167 | 168 | if ( 169 | parsed_link.netloc 170 | and parsed_link.netloc not in self.kbase["links.new_domains"] 171 | ): 172 | self.kbase["links.new_domains"].append(parsed_link.netloc) 173 | -------------------------------------------------------------------------------- /src/wenum/factories/fuzzresfactory.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from .fuzzfactory import reqfactory 4 | from .payman import payman_factory 5 | 6 | from ..fuzzobjects import FuzzResult, FuzzType, FuzzWord, FuzzWordType 7 | from ..helpers.obj_factory import ObjectFactory, SeedBuilderHelper 8 | import logging 9 | 10 | 11 | class FuzzResultFactory(ObjectFactory): 12 | def __init__(self): 13 | ObjectFactory.__init__( 14 | self, 15 | { 16 | "fuzzres_from_options_and_dict": FuzzResultDictioBuilder(), 17 | "fuzzres_from_fuzzres": FuzzResBackfeedBuilder(), 18 | "fuzzres_from_message": FuzzResMessageBuilder(), 19 | "seed_from_recursion": FuzzResSeedBuilder(), 20 | "seed_from_plugin": FuzzResPluginSeedBuilder(), 21 | "seed_from_options": FuzzResOptionsSeedBuilder(), 22 | }, 23 | ) 24 | 25 | 26 | class FuzzResultDictioBuilder: 27 | def __call__(self, session, dictio_item): 28 | fuzz_result: FuzzResult = copy.deepcopy(session.compiled_seed) 29 | fuzz_result.item_type = FuzzType.RESULT 30 | fuzz_result.payload_man.update_from_dictio(dictio_item) 31 | fuzz_result.from_plugin = False 32 | 33 | SeedBuilderHelper.replace_markers(fuzz_result.history, fuzz_result.payload_man) 34 | fuzz_result.result_number = next(FuzzResult.newid) 35 | 36 | return fuzz_result 37 | 38 | 39 | class FuzzResOptionsSeedBuilder: 40 | def __call__(self, session) -> FuzzResult: 41 | seed = reqfactory.create("seed_from_options", session) 42 | fuzz_result = FuzzResult(seed) 43 | fuzz_result.payload_man = payman_factory.create("payloadman_from_request", seed) 44 | fuzz_result.from_plugin = False 45 | 46 | return fuzz_result 47 | 48 | 49 | class FuzzResSeedBuilder: 50 | """ 51 | Create a new seed. Polls the recursion URL from the seed object's response. 52 | """ 53 | 54 | def __call__(self, originating_fuzzresult: FuzzResult) -> FuzzResult: 55 | try: 56 | seeding_url = originating_fuzzresult.history.parse_recursion_url() + "FUZZ" 57 | new_seed: FuzzResult = copy.deepcopy(originating_fuzzresult) 58 | new_seed.history.url = seeding_url 59 | # Plugin rlevel should be increased in case the new seed results out of a backfed 60 | # (and therefore plugin) object 61 | if originating_fuzzresult.from_plugin: 62 | new_seed.plugin_rlevel += 1 63 | elif not originating_fuzzresult.from_plugin: 64 | new_seed.rlevel += 1 65 | # The plugin results of the response before are irrelevant for the new request 66 | new_seed.plugins_res = [] 67 | if new_seed.rlevel_desc: 68 | new_seed.rlevel_desc += " - " 69 | new_seed.rlevel_desc += f"Seed originating from URL {originating_fuzzresult.url}" 70 | new_seed.item_type = FuzzType.SEED 71 | new_seed.from_plugin = False 72 | new_seed.discarded = False 73 | new_seed.payload_man = payman_factory.create( 74 | "payloadman_from_request", new_seed.history 75 | ) 76 | except RuntimeError as exception: 77 | logger = logging.getLogger("debug_log") 78 | logger.exception("An exception occured in FuzzResSeedBuilder") 79 | exit() 80 | 81 | return new_seed 82 | 83 | 84 | class FuzzResPluginSeedBuilder: 85 | """ 86 | Create a new seed. Used by plugins. 87 | Takes a seeding_url that will be taken as a FUZZ URL instead of directly polling the recursion URL 88 | """ 89 | 90 | def __call__(self, seed: FuzzResult, seeding_url: str) -> FuzzResult: 91 | try: 92 | if not seeding_url: 93 | seeding_url = seed.history.parse_recursion_url() + "FUZZ" 94 | new_seed: FuzzResult = copy.deepcopy(seed) 95 | new_seed.history.url = seeding_url 96 | new_seed.plugin_rlevel += 1 97 | # The plugin results of the response before are irrelevant for the new request 98 | new_seed.plugins_res = [] 99 | if new_seed.rlevel_desc: 100 | new_seed.rlevel_desc += " - " 101 | new_seed.rlevel_desc += f"Seed originating from URL {seed.url}" 102 | new_seed.item_type = FuzzType.SEED 103 | new_seed.from_plugin = False 104 | new_seed.discarded = False 105 | new_seed.payload_man = payman_factory.create( 106 | "payloadman_from_request", new_seed.history 107 | ) 108 | except RuntimeError as exception: 109 | logger = logging.getLogger("debug_log") 110 | logger.exception("An exception occured in FuzzResPluginSeedBuilder") 111 | exit() 112 | return new_seed 113 | 114 | 115 | class FuzzResBackfeedBuilder: 116 | """ 117 | Can be called to create a BACKFEED object from fuzzresult object 118 | """ 119 | 120 | def __call__(self, originating_fuzzres: FuzzResult, url, method: str, from_plugin: bool, 121 | custom_description: str = "") -> FuzzResult: 122 | try: 123 | backfeed_fuzzresult: FuzzResult = copy.deepcopy(originating_fuzzres) 124 | backfeed_fuzzresult.history.url = str(url) 125 | backfeed_fuzzresult.history.method = method 126 | # The plugin results of the response before are irrelevant for the new request and should be cleared 127 | backfeed_fuzzresult.plugins_res = [] 128 | backfeed_fuzzresult.result_number = next(FuzzResult.newid) 129 | if custom_description: 130 | backfeed_fuzzresult.rlevel_desc = custom_description 131 | else: 132 | backfeed_fuzzresult.rlevel_desc = f"Backfeed originating from {originating_fuzzres.url}" 133 | backfeed_fuzzresult.item_type = FuzzType.BACKFEED 134 | # Bool is set by plugins to True to signal where the backfeed is coming from 135 | backfeed_fuzzresult.from_plugin = True if from_plugin else False 136 | backfeed_fuzzresult.backfeed_level += 1 137 | backfeed_fuzzresult.discarded = False 138 | 139 | backfeed_fuzzresult.payload_man = payman_factory.create("empty_payloadman", 140 | FuzzWord(url, FuzzWordType.WORD)) 141 | except RuntimeError as exception: 142 | logger = logging.getLogger("debug_log") 143 | logger.exception("An exception occured in FuzzResBackfeedBuilder") 144 | exit() 145 | 146 | return backfeed_fuzzresult 147 | 148 | 149 | class FuzzResMessageBuilder: 150 | """ 151 | Can be called to create a message object to be printed out by the PrinterQ. 152 | At its core it is not a FuzzResult object, but rather a way of displaying information not related to a specific 153 | result in a clean manner. 154 | """ 155 | 156 | def __call__(self, message: str) -> FuzzResult: 157 | message_result = FuzzResult() 158 | message_result.item_type = FuzzType.MESSAGE 159 | message_result.rlevel_desc = message 160 | return message_result 161 | 162 | 163 | resfactory = FuzzResultFactory() 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wenum 2 | 3 | A wfuzz fork 4 | 5 | We have taken the tool wfuzz as a base and gave it a little twist in its direction. 6 | We want to ease the process of mapping a web application's directory structure, and not spend too much attention on anything else (e.g. determining vulnerable states). 7 | The focus is therefore different, and unfortunately, some features will even be removed. 8 | That may be due to a feature clashing with the intended direction of wenum (e.g. the AllVarQueue), or simply because there are convenience features that we think are not important enough to maintain (e.g. manipulating the wordlist entries on the tool startup). 9 | 10 | Maintained for Debian 10 and Kali, Python3.10+ 11 | 12 | ## Usage 13 | ``` 14 | wenum --help 15 | 16 | usage: wenum [-h] [-c] [-q] [-n] [-v] [-w [WORDLIST ...]] [-o OUTPUT] [-f {json,html,all}] [-l DEBUG_LOG] [--dump-config DUMP_CONFIG] [-K CONFIG] [--plugins [PLUGINS ...]] [--cache-dir CACHE_DIR] [-u URL] [-p [PROXY ...]] [-t THREADS] [-s SLEEP] [-X METHOD] [-d DATA] [-H [HEADER ...]] [-b COOKIE] [--dry-run] [--ip IP] [-i {product,zip,chain}] [-e [EXT ...]] [--hc [HC ...]] [--hl [HL ...]] [--hw [HW ...]] 17 | [--hs [HS ...]] [--hr HR] [--sc [SC ...]] [--sl [SL ...]] [--sw [SW ...]] [--ss [SS ...]] [--sr SR] [--filter FILTER] [--hard-filter] [--auto-filter] [-L] [-R RECURSION] [-r PLUGIN_RECURSION] [-E] [--limit-requests LIMIT_REQUESTS] [--request-timeout REQUEST_TIMEOUT] [--domain-scope] [--plugin-threads PLUGIN_THREADS] [-V] 18 | 19 | A Web Fuzzer. The options follow the curl schema where possible. 20 | 21 | options: 22 | -h, --help show this help message and exit 23 | -V, --version Print version and exit. 24 | 25 | Request building options: 26 | -u URL, --url URL Specify a URL for the request. 27 | -p [PROXY ...], --proxy [PROXY ...] 28 | Proxy requests. Use format 'protocol://ip:port'. Protocols SOCKS4, SOCKS5 and HTTP are supported. If supplied multipletimes, the requests will be split between all supplied proxies. 29 | -t THREADS, --threads THREADS 30 | Modify the number of concurrent "threads"/connections for requests. (default: 40) 31 | -s SLEEP, --sleep SLEEP 32 | Wait supplied seconds between requests. 33 | -X METHOD, --method METHOD 34 | Set the HTTP method used for requests. (default: GET) 35 | -d DATA, --data DATA Use POST method with supplied data (e.g. "id=FUZZ&catalogue=1"). Method can be overridden with --method. 36 | -H [HEADER ...], --header [HEADER ...] 37 | Add/modify a header, e.g. "User-Agent: Changed". Multiple flags accepted. 38 | -b COOKIE, --cookie COOKIE 39 | Add cookies, e.g. "Cookie1=foo; Cookie2=bar". 40 | --dry-run Test run without actually making any HTTP request. 41 | --ip IP Specify an IP to connect to. Format ip:port. Uses port 80 if none specified. This can help if you want to force connecting to a specific IP and still present a host name in the SNI, which will remain the URL's host. 42 | -i {product,zip,chain}, --iterator {product,zip,chain} 43 | Set the iterator used when combining multiple wordlists. (default: product) 44 | -e [EXT ...], --ext [EXT ...] 45 | Specify extensions to be appended to the wordlist items e.g. ".aspx,.asmx" 46 | 47 | Response processing options: 48 | -L, --location Follow redirections by sending an additional request to the redirection URL if it's in scope. 49 | -R RECURSION, --recursion RECURSION 50 | Enable recursive path discovery by specifying a maximum depth. 51 | -r PLUGIN_RECURSION, --plugin-recursion PLUGIN_RECURSION 52 | Adjust the max depth for recursions originating from plugins. Matches --recursion by default. 53 | -E, --stop-error Stop on any connection error. 54 | --limit-requests LIMIT_REQUESTS 55 | Limit recursions. Once specified amount of requests are sent, recursions will be deactivated 56 | --request-timeout REQUEST_TIMEOUT 57 | Change the maximum seconds the request is allowed to take. (default: 40) 58 | --domain-scope Base the scope check on the domain name instead of IP. 59 | --plugin-threads PLUGIN_THREADS 60 | Modify the amount of threads used for concurrent execution of plugins. (default: 3) 61 | 62 | Input/Output options: 63 | -w [WORDLIST ...], --wordlist [WORDLIST ...] 64 | Specify a wordlist file to iterate through. 65 | -o OUTPUT, --output OUTPUT 66 | Store results in the specified output file. 67 | -f {json,html,all}, --output-format {json,html,all} 68 | Set the format of the output file. If you specify "all", each format will be used as a suffix for the specified output path. (default: json) 69 | -l DEBUG_LOG, --debug-log DEBUG_LOG 70 | Store runtime information in the specified file. 71 | --dump-config DUMP_CONFIG 72 | Write all supplied options to a config file and exit. 73 | -K CONFIG, --config CONFIG 74 | Read config from specified path. 75 | --plugins [PLUGINS ...] 76 | Plugins to be run, supplied as a list of plugin-files or plugin-categories 77 | --cache-dir CACHE_DIR 78 | Specify a directory to read cached requests from. 79 | 80 | Terminal options: 81 | -c, --colorless Disable colors in CLI output. 82 | -q, --quiet Disable progress messages in CLI output. 83 | -n, --noninteractive Disable runtime interactions. 84 | -v, --verbose Enable verbose information in CLI output. 85 | 86 | Filter options: 87 | --hc [HC ...] Hide responses matching the supplied codes (e.g. --hc 302 404 405). 88 | --hl [HL ...] Hide responses matching the supplied lines. 89 | --hw [HW ...] Hide responses matching the supplied words. 90 | --hs [HS ...] Hide responses matching the supplied sizes/chars. 91 | --hr HR Hide responses matching the supplied regex. 92 | --sc [SC ...] Show responses matching the supplied codes. 93 | --sl [SL ...] Show responses matching the supplied lines. 94 | --sw [SW ...] Show responses matching the supplied words. 95 | --ss [SS ...] Show responses matching the supplied sizes/chars. 96 | --sr SR Show responses matching the supplied regex. 97 | --filter FILTER Show/hide responses using the supplied regex. 98 | --hard-filter Don't only hide the responses, but also prevent post processing of them (e.g. sending to plugins). 99 | --auto-filter Filter automatically during runtime. If a response occurs too often, it will get filtered out. 100 | ``` 101 | 102 | ### Example 103 | ``` 104 | wenum --hard-filter --plugins=default,gau,domainpath,clone,context,linkparser,sourcemap,robots,listing,sitemap,headers,backups,errors,title,links --hc 404 --auto-filter -R 2 -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0' -w ~/wordlists/onelistforallmicro.txt -e .aspx,.html -e .txt -f json -o example.com -u http://example.com/FUZZ 105 | ``` 106 | 107 | For a detailed documentation, please refer to the [wiki](https://github.com/WebFuzzForge/wenum/wiki). 108 | -------------------------------------------------------------------------------- /src/wenum/plugin_api/static_data.py: -------------------------------------------------------------------------------- 1 | # File to contain static data of plugins that may be shared or simply clutters the plugin file itself 2 | 3 | head_extensions = [".gif", ".jpg", ".zip", ".png", ".exe", ".pdf", ".apk", ".ipa"] 4 | valid_codes = [200, 301, 302, 303, 307, 308] 5 | 6 | # Dictionary containing dir names that map to a specific technology 7 | dir_to_tech = { 8 | "js": "js", 9 | "jsx": "jsx", 10 | "javascript": "js", 11 | "scripts": "js", 12 | "script": "js", 13 | "jsp": "jsp", 14 | "backup": "backup", 15 | "bak": "backup", 16 | "service": "service", 17 | "services": "service", 18 | "webservice": "service", 19 | "webservices": "service", 20 | "pdf": "pdf", 21 | "api": "api", 22 | "debug": "debug", 23 | "wsdl": "wsdl", 24 | "log": "log", 25 | "logs": "log" 26 | } 27 | extension_to_tech = { 28 | ".js": "js", 29 | ".jsx": "jsx", 30 | ".asp": "net", 31 | ".aspx": "net", 32 | ".ashx": "net", 33 | ".ashx": "service", 34 | ".htm": "html", 35 | ".php": "php" 36 | } 37 | 38 | extension_list = { 39 | "js": [ 40 | ".js" 41 | ], 42 | "jsx": [ 43 | ".jsx" 44 | ], 45 | 46 | "jsp": [ 47 | ".jsp" 48 | ], 49 | 50 | "backup": [ 51 | ".bak", 52 | ".zip", 53 | ".7z", 54 | ".tar.gz" 55 | ], 56 | 57 | "service": [ 58 | ".svc", 59 | ".asmx", 60 | ".ashx" 61 | ], 62 | 63 | "api": [ 64 | "" 65 | ], 66 | 67 | "debug": [ 68 | ".dll" 69 | ], 70 | 71 | "net": [ 72 | ".asp", 73 | ".aspx" 74 | ], 75 | 76 | "html": [ 77 | ".htm", 78 | ".html" 79 | ], 80 | 81 | "pdf": [ 82 | ".pdf" 83 | ], 84 | 85 | "php": [ 86 | ".php" 87 | ], 88 | 89 | "wsdl": [ 90 | ".wsdl" 91 | ], 92 | 93 | "log": [ 94 | ".log", 95 | ".txt", 96 | ".zip", 97 | ".tar.gz" 98 | ] 99 | } 100 | 101 | ERRORS_regex_list = [ 102 | "A syntax error has occurred", 103 | "ADODB.Field error", 104 | "ASP.NET is configured to show verbose error messages", 105 | "ASP.NET_SessionId", 106 | "Active Server Pages error", 107 | "An illegal character has been found in the statement", 108 | 'An unexpected token "END-OF-STATEMENT" was found', 109 | "Can't connect to local", 110 | "Custom Error Message", 111 | "DB2 Driver", 112 | "DB2 Error", 113 | "DB2 ODBC", 114 | "Disallowed Parent Path", 115 | "Error Diagnostic Information", 116 | "Error Message : Error loading required libraries.", 117 | "Error Report", 118 | "Error converting data type varchar to numeric", 119 | "Fatal error", 120 | "Incorrect syntax near", 121 | "Internal Server Error", 122 | "Invalid Path Character", 123 | "Invalid procedure call or argument", 124 | "Invision Power Board Database Error", 125 | "JDBC Driver", 126 | "JDBC Error", 127 | "JDBC MySQL", 128 | "JDBC Oracle", 129 | "JDBC SQL", 130 | "Microsoft OLE DB Provider for ODBC Drivers", 131 | "Microsoft VBScript compilation error", 132 | "Microsoft VBScript error", 133 | "MySQL Driver", 134 | "MySQL Error", 135 | "MySQL ODBC", 136 | "ODBC DB2", 137 | "ODBC Driver", 138 | "ODBC Error", 139 | "ODBC Microsoft Access", 140 | "ODBC Oracle", 141 | "ODBC SQL", 142 | "ODBC SQL Server", 143 | "OLE/DB provider returned message", 144 | "ORA-0", 145 | "ORA-1", 146 | "Oracle DB2", 147 | "Oracle Driver", 148 | "Oracle Error", 149 | "Oracle ODBC", 150 | "PHP Error", 151 | "PHP Parse error", 152 | "PHP Warning", 153 | "Permission denied: 'GetObject'", 154 | "PostgreSQL query failed: ERROR: parser: parse error", 155 | r"SQL Server Driver\]\[SQL Server", 156 | "SQL command not properly ended", 157 | "SQLException", 158 | "Supplied argument is not a valid PostgreSQL result", 159 | "Syntax error in query expression", 160 | "The error occurred in", 161 | "The script whose uid is", 162 | "Type mismatch", 163 | "Unable to jump to row", 164 | "Unclosed quotation mark before the character string", 165 | "Unterminated string constant", 166 | "Warning: Cannot modify header information - headers already sent", 167 | "Warning: Supplied argument is not a valid File-Handle resource in", 168 | r"Warning: mysql_query\(\)", 169 | r"Warning: mysql_fetch_array\(\)", 170 | r"Warning: pg_connect\(\): Unable to connect to PostgreSQL server: FATAL", 171 | "You have an error in your SQL syntax near", 172 | "data source=", 173 | "detected an internal error [IBM][CLI Driver][DB2/6000]", 174 | "invalid query", 175 | "is not allowed to access", 176 | "missing expression", 177 | "mySQL error with query", 178 | "mysql error", 179 | "on MySQL result index", 180 | "supplied argument is not a valid MySQL result resource", 181 | ] 182 | 183 | HEADERS_server_headers = ["server", "x-powered-by" "via"] 184 | 185 | HEADERS_common_response_headers_regex_list = [ 186 | r"^Server$", 187 | r"^X-Powered-By$", 188 | r"^Via$", 189 | r"^Access-Control.*$", 190 | r"^Accept-.*$", 191 | r"^age$", 192 | r"^allow$", 193 | r"^Cache-control$", 194 | r"^Client-.*$", 195 | r"^Connection$", 196 | r"^Content-.*$", 197 | r"^Cross-Origin-Resource-Policy$", 198 | r"^Date$", 199 | r"^Etag$", 200 | r"^Expires$", 201 | r"^Keep-Alive$", 202 | r"^Last-Modified$", 203 | r"^Link$", 204 | r"^Location$", 205 | r"^P3P$", 206 | r"^Pragma$", 207 | r"^Proxy-.*$", 208 | r"^Refresh$", 209 | r"^Retry-After$", 210 | r"^Referrer-Policy$", 211 | r"^Set-Cookie$", 212 | r"^Server-Timing$", 213 | r"^Status$", 214 | r"^Strict-Transport-Security$", 215 | r"^Timing-Allow-Origin$", 216 | r"^Trailer$", 217 | r"^Transfer-Encoding$", 218 | r"^Upgrade$", 219 | r"^Vary$", 220 | r"^Warning^$", 221 | r"^WWW-Authenticate$", 222 | r"^X-Content-Type-Options$", 223 | r"^X-Download-Options$", 224 | r"^X-Frame-Options$", 225 | r"^X-Microsite$", 226 | r"^X-Request-Handler-Origin-Region$", 227 | r"^X-XSS-Protection$", 228 | ] 229 | 230 | HEADERS_common_req_headers_regex_list = [ 231 | r"A-IM$", 232 | r"Accept$", 233 | r"Accept-.*$", 234 | r"Access-Control-.*$", 235 | r"Authorization$", 236 | r"Cache-Control$", 237 | r"Connection$", 238 | r"Content-.*$", 239 | r"Cookie$", 240 | r"Date$", 241 | r"Expect$", 242 | r"Forwarded$", 243 | r"From$", 244 | r"Host$", 245 | r"If-.*$", 246 | r"Max-Forwards$", 247 | r"Origin$", 248 | r"Pragma$", 249 | r"Proxy-Authorization$", 250 | r"Range$", 251 | r"Referer$", 252 | r"TE$", 253 | r"User-Agent$", 254 | r"Upgrade$", 255 | r"Upgrade-Insecure-Requests$", 256 | r"Via$", 257 | r"Warning$", 258 | r"X-Requested-With$", 259 | r"X-HTTP-Method-Override$", 260 | r"X-Requested-With$", 261 | ] 262 | 263 | LISTING_dir_indexing_regexes = ["Index of /", 264 | '<a href="\\?C=N;O=D">Name</a>', 265 | "Last modified</a>", 266 | "Parent Directory</a>", 267 | "Directory Listing for", 268 | "<TITLE>Folder Listing.", 269 | "<TITLE>Folder Listing.", 270 | '<table summary="Directory Listing" ', 271 | "- Browsing directory ", 272 | '">\\[To Parent Directory\\]</a><br><br>', 273 | '<A HREF=".*?">.*?</A><br></pre><hr></body></html>'] 274 | -------------------------------------------------------------------------------- /src/wenum/plugins/scripts/logfiles.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, date 2 | from urllib.parse import urljoin 3 | 4 | from wenum.externals.moduleman.plugin import moduleman_plugin 5 | from wenum.plugin_api.base import BasePlugin 6 | from wenum.plugin_api.mixins import DiscoveryPluginMixin 7 | 8 | 9 | @moduleman_plugin 10 | class Logfiles(BasePlugin, DiscoveryPluginMixin): 11 | name = "logfiles" 12 | author = ("Ivo Palazzolo (@palaziv)",) 13 | version = "0.1" 14 | summary = "Checks for exposed log files." 15 | description = ("Checks for exposed log files.",) 16 | category = ["active", "discovery"] 17 | priority = 99 18 | 19 | parameters = () 20 | 21 | MAX_DAYS = 10 # determines the date range to generate 22 | 23 | def __init__(self, session): 24 | BasePlugin.__init__(self, session) 25 | 26 | def validate(self, fuzz_result): 27 | log_paths = ["log", "logs", "access_log", "access_logs", "error_log", "error_logs", "errorlog", "errorlogs", "accesslog", "accesslogs"] 28 | 29 | return ( 30 | fuzz_result.history.response_redirects_to_directory() and 31 | fuzz_result.history.urlparse.path.rsplit('/')[-1].lower() in log_paths 32 | ) 33 | 34 | def process(self, fuzz_result): 35 | self.add_information(f"Log directory found at {fuzz_result.url}. Checking for exposed log files.") 36 | 37 | # check for common log file names first 38 | common_log_file_names = ["access.log", "error.log", "errors.log", "access.txt", "error.txt", "errors.txt", "errorlog.txt", "accesslog.txt", "errorlog.log", "accesslog.log", "log.txt", "application.log", "debug.log"] 39 | for log_file_name in common_log_file_names: 40 | url = urljoin(fuzz_result.url + "/", log_file_name) 41 | self.queue_url(url) 42 | 43 | # now also check for log files containing dates 44 | # check for log files from MAX_DAYS ago to today 45 | end_date = date.today() 46 | start_date = end_date - timedelta(days=self.MAX_DAYS) 47 | 48 | current_date = start_date 49 | while current_date < end_date: 50 | # YYYY-MM-DD.log, YYYYMMDD.log, YYYY_MM_DD.log 51 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y-%m-%d") + ".log") 52 | self.queue_url(url) 53 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y%m%d") + ".log") 54 | self.queue_url(url) 55 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y_%m_%d") + ".log") 56 | self.queue_url(url) 57 | 58 | # YYYY-MM-DD.txt, YYYYMMDD.txt, YYYY_MM_DD.txt 59 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y-%m-%d") + ".txt") 60 | self.queue_url(url) 61 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y%m%d") + ".txt") 62 | self.queue_url(url) 63 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y_%m_%d") + ".txt") 64 | self.queue_url(url) 65 | 66 | # DD-MM-YYYY.log, DDMMYYYY.log, DD_MM_YYYY.log 67 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d-%m-%Y") + ".log") 68 | self.queue_url(url) 69 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d%m%Y") + ".log") 70 | self.queue_url(url) 71 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d_%m_%Y") + ".log") 72 | self.queue_url(url) 73 | 74 | # DD-MM-YYYY.txt, DDMMYYYY.txt, DD_MM_YYYY.txt 75 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d-%m-%Y") + ".txt") 76 | self.queue_url(url) 77 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d%m%Y") + ".txt") 78 | self.queue_url(url) 79 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d_%m_%Y") + ".txt") 80 | self.queue_url(url) 81 | 82 | # MM-DD-YYYY.log, MMDDYYYY.log, MM_DD_YYYY.log 83 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m-%d-%Y") + ".log") 84 | self.queue_url(url) 85 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m%d%Y") + ".log") 86 | self.queue_url(url) 87 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m_%d_%Y") + ".log") 88 | self.queue_url(url) 89 | 90 | # MM-DD-YYYY.txt, MMDDYYYY.txt, MM_DD_YYYY.txt 91 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m-%d-%Y") + ".txt") 92 | self.queue_url(url) 93 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m%d%Y") + ".txt") 94 | self.queue_url(url) 95 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m_%d_%Y") + ".txt") 96 | self.queue_url(url) 97 | 98 | # now do the same as above but append _log to the file name 99 | # YYYY-MM-DD.log, YYYYMMDD.log, YYYY_MM_DD.log 100 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y-%m-%d") + "_log.log") 101 | self.queue_url(url) 102 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y%m%d") + "_log.log") 103 | self.queue_url(url) 104 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y_%m_%d") + "_log.log") 105 | self.queue_url(url) 106 | 107 | # YYYY-MM-DD.txt, YYYYMMDD.txt, YYYY_MM_DD.txt 108 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y-%m-%d") + "_log.txt") 109 | self.queue_url(url) 110 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y%m%d") + "_log.txt") 111 | self.queue_url(url) 112 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%Y_%m_%d") + "_log.txt") 113 | self.queue_url(url) 114 | 115 | # DD-MM-YYYY.log, DDMMYYYY.log, DD_MM_YYYY.log 116 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d-%m-%Y") + "_log.log") 117 | self.queue_url(url) 118 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d%m%Y") + "_log.log") 119 | self.queue_url(url) 120 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d_%m_%Y") + "_log.log") 121 | self.queue_url(url) 122 | 123 | # DD-MM-YYYY.txt, DDMMYYYY.txt, DD_MM_YYYY.txt 124 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d-%m-%Y") + "_log.txt") 125 | self.queue_url(url) 126 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d%m%Y") + "_log.txt") 127 | self.queue_url(url) 128 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%d_%m_%Y") + "_log.txt") 129 | self.queue_url(url) 130 | 131 | # MM-DD-YYYY.log, MMDDYYYY.log, MM_DD_YYYY.log 132 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m-%d-%Y") + "_log.log") 133 | self.queue_url(url) 134 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m%d%Y") + "_log.log") 135 | self.queue_url(url) 136 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m_%d_%Y") + "_log.log") 137 | self.queue_url(url) 138 | 139 | # MM-DD-YYYY.txt, MMDDYYYY.txt, MM_DD_YYYY.txt 140 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m-%d-%Y") + "_log.txt") 141 | self.queue_url(url) 142 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m%d%Y") + "_log.txt") 143 | self.queue_url(url) 144 | url = urljoin(fuzz_result.url + "/", current_date.strftime("%m_%d_%Y") + "_log.txt") 145 | self.queue_url(url) 146 | 147 | current_date += timedelta(days=1) 148 | -------------------------------------------------------------------------------- /src/wenum/externals/reqresp/Response.py: -------------------------------------------------------------------------------- 1 | import re 2 | import cgi 3 | 4 | from io import BytesIO 5 | import gzip 6 | import zlib 7 | 8 | from .TextParser import TextParser 9 | 10 | 11 | def get_encoding_from_headers(headers): 12 | """Returns encodings from given HTTP Header Dict. 13 | 14 | :param headers: dictionary to extract encoding from. 15 | :rtype: str 16 | """ 17 | 18 | content_type = headers.get("Content-Type") 19 | 20 | if not content_type: 21 | return None 22 | 23 | content_type, params = cgi.parse_header(content_type) 24 | 25 | if "charset" in params: 26 | return params["charset"].strip("'\"") 27 | 28 | if "text" in content_type: 29 | return "ISO-8859-1" 30 | 31 | if "image" in content_type: 32 | return "utf-8" 33 | 34 | if "application/json" in content_type: 35 | return "utf-8" 36 | 37 | 38 | def get_encodings_from_content(content): 39 | """Returns encodings from given content string. 40 | 41 | :param content: bytestring to extract encodings from. 42 | """ 43 | charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I) 44 | pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I) 45 | xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]') 46 | 47 | return ( 48 | charset_re.findall(content) 49 | + pragma_re.findall(content) 50 | + xml_re.findall(content) 51 | ) 52 | 53 | 54 | class Response: 55 | def __init__(self, protocol="", code="", message=""): 56 | self.protocol = protocol # HTTP/1.1 57 | self.code = code # 200 58 | self.message = message # OK 59 | self._headers = [] # well then the headers are the same as in the request 60 | self.__content = ( 61 | "" # content of the response (only if Content-Length exists) 62 | ) 63 | self.md5 = "" # hash of the result contents 64 | self.charlen = "" # Number of characters in the response 65 | 66 | def add_header(self, key, value): 67 | self._headers += [(key, value)] 68 | 69 | def del_header(self, key): 70 | for i in self._headers: 71 | if i[0].lower() == key.lower(): 72 | self._headers.remove(i) 73 | 74 | def add_content(self, text): 75 | self.__content = self.__content + text 76 | 77 | def __getitem__(self, key): 78 | for i, j in self._headers: 79 | if key == i: 80 | return j 81 | print("Error al obtener header!!!") 82 | 83 | def get_cookie(self): 84 | str = [] 85 | for i, j in self._headers: 86 | if i.lower() == "set-cookie": 87 | str.append(j.split(";")[0]) 88 | return "; ".join(str) 89 | 90 | def has_header(self, key): 91 | for i, j in self._headers: 92 | if i.lower() == key.lower(): 93 | return True 94 | return False 95 | 96 | def get_location(self): 97 | for i, j in self._headers: 98 | if i.lower() == "location": 99 | return j 100 | return None 101 | 102 | def header_equal(self, header, value): 103 | for i, j in self._headers: 104 | if i == header and j.lower() == value.lower(): 105 | return True 106 | return False 107 | 108 | def get_headers(self): 109 | return self._headers 110 | 111 | def get_content(self): 112 | return self.__content 113 | 114 | def get_text_headers(self): 115 | string = ( 116 | str(self.protocol) + " " + str(self.code) + " " + str(self.message) + "\r\n" 117 | ) 118 | for i, j in self._headers: 119 | string += i + ": " + j + "\r\n" 120 | 121 | return string 122 | 123 | def get_all(self): 124 | string = self.get_text_headers() + "\r\n" + self.get_content() 125 | return string 126 | 127 | def substitute(self, src, dst): 128 | a = self.get_all() 129 | b = a.replace(src, dst) 130 | self.parse_response(b) 131 | 132 | def get_all_wpost(self): 133 | string = ( 134 | str(self.protocol) + " " + str(self.code) + " " + str(self.message) + "\r\n" 135 | ) 136 | for i, j in self._headers: 137 | string += i + ": " + j + "\r\n" 138 | return string 139 | 140 | def parse_response(self, rawheader, rawbody=None): 141 | self.__content = "" 142 | self._headers = [] 143 | 144 | text_parser: TextParser = TextParser() 145 | text_parser.set_source("string", rawheader) 146 | 147 | text_parser.read_until(r"(HTTP/[0-9.]+) ([0-9]+)") 148 | while True: 149 | while True: 150 | try: 151 | self.protocol = text_parser[0][0] 152 | except Exception: 153 | self.protocol = "unknown" 154 | 155 | try: 156 | self.code = text_parser[0][1] 157 | except Exception: 158 | self.code = "0" 159 | 160 | if self.code != "100": 161 | break 162 | else: 163 | text_parser.read_until(r"(HTTP/[0-9.]+) ([0-9]+)") 164 | 165 | self.code = int(self.code) 166 | 167 | while True: 168 | text_parser.read_line() 169 | if text_parser.search("^([^:]+): ?(.*)$"): 170 | self.add_header(text_parser[0][0], text_parser[0][1]) 171 | else: 172 | break 173 | 174 | # curl sometimes sends two headers when using follow, 302 and the final header 175 | # also when using proxies 176 | text_parser.read_line() 177 | if not text_parser.search(r"(HTTP/[0-9.]+) ([0-9]+)"): 178 | break 179 | else: 180 | self._headers = [] 181 | 182 | # ignore CRLFs until request line 183 | while text_parser.lastline == "" and text_parser.read_line(): 184 | pass 185 | 186 | # TODO: this should be added to rawbody not directly to __content 187 | if text_parser.lastFull_line: 188 | self.add_content(text_parser.lastFull_line) 189 | 190 | while text_parser.skip(1): 191 | self.add_content(text_parser.lastFull_line) 192 | 193 | self.del_header("Transfer-Encoding") 194 | 195 | if self.header_equal("Transfer-Encoding", "chunked"): 196 | result = "" 197 | content = BytesIO(rawbody) 198 | hexa = content.readline() 199 | nchunk = int(hexa.strip(), 16) 200 | 201 | while nchunk: 202 | result += content.read(nchunk) 203 | content.readline() 204 | hexa = content.readline() 205 | nchunk = int(hexa.strip(), 16) 206 | 207 | rawbody = result 208 | 209 | if self.header_equal("Content-Encoding", "gzip"): 210 | compressedstream = BytesIO(rawbody) 211 | gzipper = gzip.GzipFile(fileobj=compressedstream) 212 | rawbody = gzipper.read() 213 | self.del_header("Content-Encoding") 214 | elif self.header_equal("Content-Encoding", "deflate"): 215 | try: 216 | deflater = zlib.decompressobj() 217 | deflated_data = deflater.decompress(rawbody) 218 | deflated_data += deflater.flush() 219 | except zlib.error: 220 | try: 221 | deflater = zlib.decompressobj(-zlib.MAX_WBITS) 222 | deflated_data = deflater.decompress(rawbody) 223 | deflated_data += deflater.flush() 224 | except zlib.error: 225 | deflated_data = "" 226 | rawbody = deflated_data 227 | self.del_header("Content-Encoding") 228 | 229 | if rawbody is not None: 230 | # Try to get charset encoding from headers 231 | content_encoding = get_encoding_from_headers(dict(self.get_headers())) 232 | 233 | # fallback to default encoding 234 | if content_encoding is None: 235 | content_encoding = "utf-8" 236 | 237 | self.__content = rawbody.decode(content_encoding, errors="replace") 238 | -------------------------------------------------------------------------------- /src/wenum/externals/reqresp/Request.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from urllib.parse import urlparse 3 | from urllib.parse import urlunparse 4 | 5 | import re 6 | 7 | from .Variables import VariablesSet 8 | from .Response import Response 9 | 10 | from wenum.helpers.obj_dic import CaseInsensitiveDict 11 | 12 | from .TextParser import TextParser 13 | 14 | 15 | class Request: 16 | """ 17 | Lower level Request class, though in the long term could be merged with FuzzRequest, 18 | as Request is only used in the FuzzRequest context 19 | """ 20 | def __init__(self): 21 | self.host = None # www.google.com:80 22 | self.path = None # /index.php 23 | self.params = None # Mierdaza de index.php;lskjflkasjflkasjfdlkasdf? 24 | self.schema = "http" # http 25 | 26 | self.ContentType = ( 27 | "application/x-www-form-urlencoded" # Default 28 | ) 29 | self.multiPOSThead = {} 30 | 31 | self.__variablesGET = VariablesSet() 32 | self._variablesPOST = VariablesSet() 33 | self._non_parsed_post = None 34 | 35 | # Dict, for example headers["Cookie"] 36 | self._headers = CaseInsensitiveDict( 37 | { 38 | "Content-Type": "application/x-www-form-urlencoded", 39 | "User-Agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1)", 40 | } 41 | ) 42 | 43 | self.response: Optional[Response] = None # The response created out of the request 44 | 45 | # ################## lo de debajo no se deberia acceder directamente 46 | 47 | self.time = None # 23:00:00 48 | self.ip = None # 192.168.1.1 49 | self._method = None 50 | self.protocol = "HTTP/1.1" # HTTP/1.1 51 | self._performHead = "" 52 | self._performBody = "" 53 | 54 | self._authMethod = None 55 | self._userpass = "" 56 | 57 | self.description = "" # For storing information temporarily 58 | 59 | self._timeout = None 60 | self._totaltimeout = None 61 | 62 | self.totaltime = None 63 | self.date = None 64 | 65 | @property 66 | def complete_url(self): 67 | """ 68 | e.g. http://www.google.es/index.php?a=b 69 | """ 70 | return urlunparse((self.schema, self.host, self.path, 71 | self.params, self.__variablesGET.urlEncoded(), "",)) 72 | 73 | @property 74 | def method(self): 75 | if self._method is None: 76 | return "POST" if self._non_parsed_post is not None else "GET" 77 | 78 | return self._method 79 | 80 | @method.setter 81 | def method(self, value): 82 | if value == "None": 83 | value = None 84 | 85 | self._method = value 86 | 87 | @property 88 | def postdata(self): 89 | if self.ContentType == "application/x-www-form-urlencoded": 90 | return self._variablesPOST.urlEncoded() 91 | elif self.ContentType == "multipart/form-data": 92 | return self._variablesPOST.multipartEncoded() 93 | elif self.ContentType == "application/json": 94 | return self._variablesPOST.json_encoded() 95 | else: 96 | return self._variablesPOST.urlEncoded() 97 | 98 | @property 99 | def url_without_variables(self): 100 | """ 101 | e.g. http://www.google.es/index.php 102 | """ 103 | return urlunparse((self.schema, self.host, self.path, "", "", "")) 104 | 105 | @property 106 | def path_with_variables(self): 107 | """ 108 | e.g. /index.php?a=b&c=d 109 | """ 110 | return urlunparse(("", "", self.path, "", self.__variablesGET.urlEncoded(), "")) 111 | 112 | def __str__(self): 113 | request_string = "[ URL: %s" % self.complete_url 114 | if self.postdata: 115 | request_string += ' - {}: "{}"'.format(self.method, self.postdata) 116 | if "Cookie" in self._headers: 117 | request_string += ' - COOKIE: "%s"' % self._headers["Cookie"] 118 | request_string += " ]" 119 | return request_string 120 | 121 | def set_url(self, urltmp): 122 | self.__variablesGET = VariablesSet() 123 | self.schema, self.host, self.path, self.params, variables, f \ 124 | = urlparse(urltmp) 125 | if "Host" not in self._headers or (not self._headers["Host"]): 126 | self._headers["Host"] = self.host 127 | 128 | if variables: 129 | self.__variablesGET.parseUrlEncoded(variables) 130 | 131 | def set_variable_post(self, key, value): 132 | v = self._variablesPOST.getVariable(key) 133 | v.update(value) 134 | 135 | def set_variable_get(self, key, value): 136 | v = self.__variablesGET.getVariable(key) 137 | v.update(value) 138 | 139 | def get_get_vars(self): 140 | return self.__variablesGET.variables 141 | 142 | def get_post_vars(self): 143 | return self._variablesPOST.variables 144 | 145 | def set_post_data(self, pd, boundary=None): 146 | self._non_parsed_post = pd 147 | self._variablesPOST = VariablesSet() 148 | 149 | try: 150 | if self.ContentType == "multipart/form-data": 151 | self._variablesPOST.parseMultipart(pd, boundary) 152 | elif self.ContentType == "application/json": 153 | self._variablesPOST.parse_json_encoded(pd) 154 | else: 155 | self._variablesPOST.parseUrlEncoded(pd) 156 | except Exception: 157 | try: 158 | self._variablesPOST.parseUrlEncoded(pd) 159 | except Exception: 160 | print("Warning: POST parameters not parsed") 161 | pass 162 | 163 | ############################################################################ 164 | 165 | def add_header(self, key, value): 166 | self._headers[key] = value 167 | 168 | def __getitem__(self, key): 169 | if key in self._headers: 170 | return self._headers[key] 171 | else: 172 | return "" 173 | 174 | def get_headers(self): 175 | header_list = [] 176 | for i, j in self._headers.items(): 177 | header_list += ["%s: %s" % (i, j)] 178 | return header_list 179 | 180 | # ######## ESTE conjunto de funciones no es necesario para el uso habitual de la clase 181 | 182 | def get_all(self): 183 | pd = self._non_parsed_post if self._non_parsed_post else "" 184 | string = ( 185 | str(self.method) 186 | + " " 187 | + str(self.path_with_variables) 188 | + " " 189 | + str(self.protocol) 190 | + "\n" 191 | ) 192 | for i, j in self._headers.items(): 193 | string += i + ": " + j + "\n" 194 | string += "\n" + pd 195 | 196 | return string 197 | 198 | # ######################################################################### 199 | 200 | def header_callback(self, data): 201 | self._performHead += data 202 | 203 | def body_callback(self, data): 204 | self._performBody += data 205 | 206 | def substitute(self, src, dst): 207 | a = self.get_all() 208 | rx = re.compile(src) 209 | b = rx.sub(dst, a) 210 | del rx 211 | self.parse_request(b, self.schema) 212 | 213 | def parse_request(self, raw_request, prot="http") -> None: 214 | """ 215 | Receives raw request and sets plenty parameters of Request object instance 216 | """ 217 | text_parser = TextParser() 218 | text_parser.set_source("string", raw_request) 219 | 220 | self._variablesPOST = VariablesSet() 221 | self._headers = {} 222 | 223 | text_parser.read_line() 224 | try: 225 | text_parser.search(r"^(\S+) (.*) (HTTP\S*)$") 226 | self.method = text_parser[0][0] 227 | self.protocol = text_parser[0][2] 228 | except Exception as a: 229 | print(raw_request) 230 | raise a 231 | 232 | path_tmp = text_parser[0][1].replace(" ", "%20") 233 | path_tmp = ("", "") + urlparse(path_tmp)[2:] 234 | path_tmp = urlunparse(path_tmp) 235 | 236 | while True: 237 | text_parser.read_line() 238 | if text_parser.search("^([^:]+): (.*)$"): 239 | self.add_header(text_parser[0][0], text_parser[0][1]) 240 | else: 241 | break 242 | 243 | self.set_url(prot + "://" + self._headers["Host"] + path_tmp) 244 | 245 | # ignore CRLFs until request line 246 | while text_parser.lastline == "" and text_parser.read_line(): 247 | pass 248 | 249 | pd = "" 250 | if text_parser.lastFull_line: 251 | pd += text_parser.lastFull_line 252 | 253 | while text_parser.read_line(): 254 | pd += text_parser.lastFull_line 255 | 256 | if pd: 257 | boundary = None 258 | if "Content-Type" in self._headers: 259 | values = self._headers["Content-Type"].split(";") 260 | self.ContentType = values[0].strip().lower() 261 | if self.ContentType == "multipart/form-data": 262 | boundary = values[1].split("=")[1].strip() 263 | 264 | self.set_post_data(pd, boundary) 265 | -------------------------------------------------------------------------------- /src/wenum/fuzzrequest.py: -------------------------------------------------------------------------------- 1 | from .facade import Facade 2 | from urllib.parse import urlparse 3 | 4 | from .externals.reqresp import Request, Response 5 | from .exception import FuzzExceptBadAPI, FuzzExceptBadOptions 6 | from .mixins import FuzzRequestUrlMixing 7 | 8 | from .helpers.obj_dic import DotDict 9 | 10 | 11 | class Headers: 12 | class Header(DotDict): 13 | def __str__(self): 14 | return "\n".join(["{}: {}".format(k, v) for k, v in self.items()]) 15 | 16 | def __init__(self, req: Request): 17 | self._req: Request = req 18 | 19 | @property 20 | def response(self): 21 | return ( 22 | Headers.Header(self._req.response.get_headers()) 23 | if self._req.response 24 | else Headers.Header() 25 | ) 26 | 27 | @property 28 | def request(self): 29 | return Headers.Header(self._req._headers) 30 | 31 | @request.setter 32 | def request(self, values_dict: dict): 33 | self._req._headers.update(values_dict) 34 | if "Content-Type" in values_dict: 35 | self._req.ContentType = values_dict["Content-Type"] 36 | 37 | @property 38 | def all(self): 39 | return Headers.Header(self.request + self.response) 40 | 41 | 42 | class Cookies: 43 | class Cookie(DotDict): 44 | def __str__(self): 45 | return "\n".join(["{}={}".format(k, v) for k, v in self.items()]) 46 | 47 | def __init__(self, req: Request): 48 | self.req: Request = req 49 | 50 | @property 51 | def response(self): 52 | if self.req.response: 53 | c = self.req.response.get_cookie().split("; ") 54 | if c[0]: 55 | return Cookies.Cookie( 56 | {x[0]: x[2] for x in [x.partition("=") for x in c]} 57 | ) 58 | 59 | return Cookies.Cookie({}) 60 | 61 | @property 62 | def request(self): 63 | if "Cookie" in self.req._headers: 64 | c = self.req._headers["Cookie"].split("; ") 65 | if c[0]: 66 | return Cookies.Cookie( 67 | {x[0]: x[2] for x in [x.partition("=") for x in c]} 68 | ) 69 | 70 | return Cookies.Cookie({}) 71 | 72 | @request.setter 73 | def request(self, values): 74 | self.req._headers["Cookie"] = values 75 | 76 | @property 77 | def all(self): 78 | return Cookies.Cookie(self.request + self.response) 79 | 80 | 81 | class Params: 82 | class Param(DotDict): 83 | def __str__(self): 84 | return "\n".join(["{}={}".format(k, v) for k, v in self.items()]) 85 | 86 | def __init__(self, request: Request): 87 | self._req: Request = request 88 | 89 | @property 90 | def get(self): 91 | return Params.Param({x.name: x.value for x in self._req.get_get_vars()}) 92 | 93 | @get.setter 94 | def get(self, values): 95 | if isinstance(values, dict) or isinstance(values, DotDict): 96 | for key, value in values.items(): 97 | self._req.set_variable_get(key, str(value)) 98 | else: 99 | raise FuzzExceptBadAPI("GET Parameters must be specified as a dictionary") 100 | 101 | @property 102 | def post(self): 103 | return Params.Param({x.name: x.value for x in self._req.get_post_vars()}) 104 | 105 | @post.setter 106 | def post(self, pp): 107 | if isinstance(pp, dict) or isinstance(pp, DotDict): 108 | for key, value in pp.items(): 109 | self._req.set_variable_post( 110 | key, str(value) if value is not None else value 111 | ) 112 | 113 | self._req._non_parsed_post = self._req._variablesPOST.urlEncoded() 114 | 115 | elif isinstance(pp, str): 116 | self._req.set_post_data(pp) 117 | 118 | @property 119 | def raw_post(self): 120 | return self._req._non_parsed_post 121 | 122 | @property 123 | def all(self): 124 | return Params.Param(self.get + self.post) 125 | 126 | @all.setter 127 | def all(self, values): 128 | self.get = values 129 | self.post = values 130 | 131 | 132 | class FuzzRequest(FuzzRequestUrlMixing): 133 | def __init__(self): 134 | self._request: Request = Request() 135 | 136 | self._proxy = None 137 | self.retries = 0 138 | self.ip = None 139 | # Original url retains the URL that has been specified for fuzzing, such as http://example.com/FUZZ 140 | self.fuzzing_url = "" 141 | 142 | self.headers.request = {"User-Agent": Facade().settings.get("connection", "user-agent")} 143 | 144 | # methods for accessing HTTP requests information consistently across the codebase 145 | 146 | def __str__(self): 147 | return self._request.get_all() 148 | 149 | @property 150 | def raw_request(self): 151 | return self._request.get_all() 152 | 153 | @raw_request.setter 154 | def raw_request(self, raw_req, scheme): 155 | self.update_from_raw_http(raw_req, scheme) 156 | 157 | @property 158 | def raw_content(self): 159 | if self._request.response: 160 | return self._request.response.get_all() 161 | 162 | return "" 163 | 164 | @property 165 | def headers(self): 166 | return Headers(self._request) 167 | 168 | @property 169 | def params(self): 170 | return Params(self._request) 171 | 172 | @property 173 | def cookies(self): 174 | return Cookies(self._request) 175 | 176 | @property 177 | def method(self): 178 | return self._request.method 179 | 180 | @method.setter 181 | def method(self, method): 182 | self._request.method = method 183 | 184 | @property 185 | def scheme(self): 186 | return self._request.schema 187 | 188 | @scheme.setter 189 | def scheme(self, s): 190 | self._request.schema = s 191 | 192 | @property 193 | def host(self): 194 | return self._request.host 195 | 196 | @property 197 | def path(self) -> str: 198 | return self._request.path 199 | 200 | @property 201 | def url(self): 202 | """ 203 | Returns the complete request URL 204 | """ 205 | return self._request.complete_url 206 | 207 | @url.setter 208 | def url(self, u): 209 | # urlparse goes wrong with IP:port without scheme (https://bugs.python.org/issue754016) 210 | if not u.startswith("FUZ") and urlparse(u).netloc == "" or urlparse(u).scheme == "": 211 | u = "http://" + u 212 | 213 | if urlparse(u).path == "": 214 | u += "/" 215 | 216 | if Facade().settings.get("general", "encode_space") == "1": 217 | u = u.replace(" ", "%20") 218 | 219 | self._request.set_url(u) 220 | if self.scheme.startswith("fuz") and self.scheme.endswith("z"): 221 | # avoid FUZZ to become fuzz 222 | self.scheme = self.scheme.upper() 223 | 224 | @property 225 | def content(self): 226 | return self._request.response.get_content() if self._request.response else "" 227 | 228 | @content.setter 229 | def content(self, content): 230 | self._request.content = content 231 | 232 | @property 233 | def code(self): 234 | """ 235 | Returns response HTTP status code 236 | """ 237 | return self._request.response.code if self._request.response else 0 238 | 239 | @code.setter 240 | def code(self, c): 241 | self._request.response.code = int(c) 242 | 243 | @property 244 | def reqtime(self): 245 | return self._request.totaltime 246 | 247 | @reqtime.setter 248 | def reqtime(self, time): 249 | self._request.totaltime = time 250 | 251 | @property 252 | def date(self): 253 | return self._request.date 254 | 255 | # methods wenum needs to perform HTTP requests (this might change in the future). 256 | 257 | def update_from_raw_http(self, raw, scheme, raw_response=None, raw_content=None) -> Request: 258 | self._request.parse_request(raw, scheme) 259 | 260 | # Parse request sets postdata = '' when there's POST request without data 261 | if self.method == "POST" and self.params.raw_post is None: 262 | self.params.post = "" 263 | 264 | if raw_response: 265 | rp = Response() 266 | if not isinstance(raw_response, str): 267 | raw_response = raw_response.decode("utf-8", errors="surrogateescape") 268 | 269 | rp.parse_response(raw_response, raw_content) 270 | self._request.response = rp 271 | 272 | return self._request 273 | 274 | def to_cache_key(self): 275 | key = self._request.url_without_variables 276 | cleaned_key = FuzzRequestUrlMixing.strip_redundant_parts(key) 277 | return cleaned_key 278 | 279 | # methods wenum needs for substituting payloads and building dictionaries 280 | 281 | def update_from_options(self, session): 282 | if session.options.url != "FUZZ": 283 | self.url = session.options.url 284 | self.fuzzing_url = session.options.url 285 | 286 | self.headers.request = session.options.header_dict() 287 | 288 | if session.options.data: 289 | self.params.post = session.options.data 290 | 291 | if session.options.ip: 292 | self.ip = session.options.ip 293 | 294 | if session.options.method: 295 | self.method = session.options.method 296 | 297 | if session.options.cookie: 298 | self.cookies.request = session.options.cookie 299 | --------------------------------------------------------------------------------