├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── fuzzing_payloads.txt └── websocket_messages.txt ├── requirements.txt └── websocket-fuzzer.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 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 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## 2.0.0 - 15.06.2023 6 | 7 | + Added support for other authentication headers 8 | + Added support for http and https protocol 9 | + Added progress output 10 | + Improved proxy handling 11 | + Clarified assumptions and default values script uses 12 | + Rewritten readme to reflect changes 13 | 14 | ## 1.0.0 - 20.04.2023 15 | 16 | Released initial version 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 scip ag 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket Fuzzer 2 | 3 | _WebSocket Fuzzer_ is a simple WebSocket fuzzing script. Part of its creation process is described in the article [WebSocket Fuzzing - Development of a custom fuzzer](https://www.scip.ch/en/?labs.20230420). 4 | 5 | ## Installation and usage 6 | 7 | The script only runs with Python 3. To install the necessary modules use `pip3 install -r requirements.txt`. 8 | 9 | It is generally recommended to use a proxy like Burp Suite or OWASP ZAP to record the WebSocket traffic which is created by this script since this script does not generate a log file of all the messages that have been sent. Furthermore, the analysis of all the generated server responses has to happen manually by the tester. The script creates a new WebSocket for every fuzzed message and closes it again after processing the response. This was done since it could not be guaranteed that a WebSocket was still valid after a previous fuzzing round. 10 | 11 | Common execution examples for the WebSocket Fuzzer would be: 12 | 13 | ```bash 14 | # fuzz http://example.com with specified session cookie and proxy 15 | python3 websocket-fuzzer.py -c "session=example_value" -f fuzzing_payloads.txt -m websocket_messages.txt -p "127.0.0.1:8080" http://example.com 16 | 17 | # fuzz https://example.com with specified Authorization header and proxy, includes a timout of 5 seconds to wait for responses and runs in verbose mode 18 | python3 websocket-fuzzer.py -a "Authorization: Bearer " -f fuzzing_payloads.txt -m websocket_messages.txt -p "127.0.0.1:8080" -t 5 -v https://example.com 19 | ``` 20 | 21 | The script was developed for fuzzing websocket applications which send their messages with JSON. If this is not the case for you, please customize the following function: `payload_parsing`. 22 | 23 | ## Usage options 24 | 25 | ``` 26 | usage: websocket-fuzzer.py [-h] [-a AUTH_HEADER] [-c COOKIE] [-e ERROR_MESSAGES] -f FUZZ_FILE -m MESSAGE_FILE [-p PROXY] [-t TIMEOUT] [-u URL_PATH] [-v] [--version] target 27 | 28 | Simple WebSocket fuzzer: Manuall analysis of results needed! Author: Andrea Hauser - scip AG 29 | 30 | positional arguments: 31 | target Defines target to fuzz in format protocol://hostname:port where protocol is either http/https and :port is optional 32 | 33 | options: 34 | -h, --help show this help message and exit 35 | -a AUTH_HEADER, --auth_header AUTH_HEADER 36 | Sets user defined header(s), for applications which are not using cookies. 37 | For multiple headers use option more than once 38 | -c COOKIE, --cookie COOKIE 39 | Specifies a cookie for setting up WebSocket 40 | -e ERROR_MESSAGES, --error_messages ERROR_MESSAGES 41 | Specifies what error messages a potential response should be analyzed for. Expected format 42 | is a comma separated string like value1,value2. The default strings that will be looked for 43 | are error, stacktrace and trace 44 | -f FUZZ_FILE, --fuzz_file FUZZ_FILE 45 | File which contains the fuzzing attack payloads, one payload per line 46 | -m MESSAGE_FILE, --message_file MESSAGE_FILE 47 | File which contains the WebSocket messages prepared to be fuzzed. Assumes one message per line. 48 | The string FUZZ_VALUE will be replaced with the content of the fuzzing payloads file. 49 | If non fuzzed pre messages are required before successfully fuzzing a message, list those 50 | pre messages line by line before the actual message and start them with PRE_MESSAGE 51 | -p PROXY, --proxy PROXY 52 | Specifies proxy in format proxy:port 53 | -t TIMEOUT, --timeout TIMEOUT 54 | Specifies how long a WebSocket connection is kept open to receive responses 55 | -u URL_PATH, --url_path URL_PATH 56 | URL path where protocol switching happens 57 | -v, --verbose Increases program output in the console 58 | --version show program's version number and exit 59 | ``` 60 | 61 | ## Examples 62 | 63 | The _examples_ directory contains example files which show what potential _fuzzing\_payloads_ and _websocket\_messages_ files need to look like. 64 | 65 | If _websocket\_messages.txt_ contains the following values: 66 | ``` 67 | PRE_MESSAGE READY 68 | {"message":"FUZZ_VALUE"} 69 | {"other":"FUZZ_VALUE"} 70 | ``` 71 | 72 | and the _fuzzing\_payloads.txt_ file contains the following values: 73 | ``` 74 | 75 | sql test with space '-- 76 | ``` 77 | 78 | the script will send `READY` as a pre message for the message where it was specified befor sending a fuzzed message. 79 | 80 | Therefore the script will send the following WebSocket messages with the provided example files: 81 | 82 | ``` 83 | READY 84 | {"message":""} 85 | READY 86 | {"message":"sql test with space '-- "} 87 | {"other":""} 88 | {"other":"sql test with space '-- "} 89 | ``` 90 | 91 | ## Ideas for future enhancements 92 | 93 | - [x] The established WebSocket connection is closed again relatively quickly after sending the fuzzed messages. The aim is to build in a timeout so that the response time of the server can also be somewhat longer and still be captured. A good balance must be found between extending the time to fuzz and the time to wait for delayed responses. 94 | - [x] Currently the script can only be used with cookies. This should be generalised so that other authentication methods such as Authentication: Bearer can also be used. 95 | - [x] Despite the initially contrary decision, it could still be helpful to include a primitive detection option for successful attacks in the script, for example matching on error or stack trace or similar, so that the tester already has some good ideas for further manual investigations. 96 | - [x] Including a progress indicator could be helpful for the tester, as it is currently difficult to see how many payloads the script has already processed if there are many payloads in the fuzzing file. 97 | -------------------------------------------------------------------------------- /examples/fuzzing_payloads.txt: -------------------------------------------------------------------------------- 1 | 2 | sql test with space '-- 3 | -------------------------------------------------------------------------------- /examples/websocket_messages.txt: -------------------------------------------------------------------------------- 1 | PRE_MESSAGE READY 2 | {"message":"FUZZ_VALUE"} 3 | {"other":"FUZZ_VALUE"} 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client 2 | -------------------------------------------------------------------------------- /websocket-fuzzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Provides a simple WebSocket fuzzer. Analysis of the results must still happen manually after running this script. 6 | """ 7 | 8 | import websocket 9 | import ssl 10 | import argparse 11 | import sys 12 | import datetime 13 | 14 | __version__ = "2.0.0" 15 | __author__ = "Andrea Hauser - scip AG" 16 | 17 | class colors: 18 | RED = '\033[31m' 19 | ENDC = '\033[m' 20 | 21 | # Payload parsing is optimized for JSON, if other message types are used, change parsing here 22 | def payload_parsing(payload): 23 | payload = payload.replace('"', '\\"') 24 | return payload 25 | 26 | 27 | def fuzzer(auth_header, cookie, target, url, fuzz_values_file, websocket_messages_file, proxy_host, proxy_port, timeout, verbose, error_messages): 28 | websocket.enableTrace(verbose) 29 | 30 | if target.startswith("https://"): 31 | wss_target = "wss://" + target.split("https://")[1] 32 | else: 33 | wss_target = "ws://" + target.split("http://")[1] 34 | 35 | # Read fuzzing payloads from text file 36 | with open(fuzz_values_file, "r") as f: 37 | fuzz_values = [payload_parsing(line.rstrip('\n')) for line in f.readlines()] 38 | fuzz_total = len(fuzz_values) 39 | 40 | # Read WebSocket message from text file 41 | with open(websocket_messages_file, "r") as messages_file: 42 | premessage_count = 0 43 | message_count = 0 44 | premessages = [] 45 | 46 | for websocket_message in messages_file: 47 | websocket_message = websocket_message.strip() 48 | message_count += 1 49 | 50 | if websocket_message.startswith("PRE_MESSAGE"): 51 | premessage_count += 1 52 | premessages.append(websocket_message.replace("PRE_MESSAGE", "").strip()) 53 | else: 54 | fuzz_count = 0 55 | for fuzz_value in fuzz_values: 56 | fuzz_count += 1 57 | # Create the WebSocket 58 | if proxy_host is None: 59 | ws = websocket.WebSocket() 60 | ws.connect(wss_target+url, cookie=cookie, header=auth_header, origin=target) 61 | else: 62 | ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE}) 63 | ws.connect(wss_target+url, cookie=cookie, header=auth_header, origin=target, 64 | http_proxy_host=proxy_host, http_proxy_port=proxy_port, proxy_type="http") 65 | 66 | # Send pre messages if there are any 67 | if premessage_count > 0: 68 | for premessage in premessages: 69 | ws.send(premessage) 70 | print("\n<----> Pre message that was sent: " + premessage) 71 | 72 | # Replace FUZZ_VALUE with attack payload from the file 73 | message = websocket_message.replace("FUZZ_VALUE", fuzz_value) 74 | 75 | # Send the fuzzed message over the WebSocket connection 76 | ws.send(message) 77 | print("\n<----> WebSocket message that was sent/fuzzed: " + message) 78 | print("<----> Fuzzing message " + str(fuzz_count) + " of " + str(fuzz_total) + " sent for WebSocket message on line " + str(message_count)) 79 | 80 | # Receive answers for specified amount of seconds and process them 81 | while True: 82 | try: 83 | ws.settimeout(timeout) 84 | result = ws.recv() 85 | print("Received response with length: " + str(len(result)) + " on time: " 86 | + datetime.datetime.now().strftime('%H:%M:%S %d.%m.%Y')) 87 | 88 | for error_message in error_messages: 89 | if error_message in result.lower(): 90 | print(colors.RED + "The sent fuzz value " + message + " should be checked further, response contains " + error_message + colors.ENDC) 91 | except websocket._exceptions.WebSocketException: 92 | break 93 | ws.close() 94 | 95 | premessages = [] 96 | premessage_count = 0 97 | 98 | 99 | def main(): 100 | # Parse command line arguments 101 | parser = argparse.ArgumentParser(description='Simple WebSocket fuzzer: Manuall analysis of results needed! Author: ' + __author__) 102 | parser.add_argument('-a', '--auth_header', action='append', help='Sets user defined header(s), for applications which are not using cookies. ' 103 | + 'For multiple headers use option more than once', default=[]) 104 | parser.add_argument('-c', '--cookie', type=str, help='Specifies a cookie for setting up WebSocket') 105 | parser.add_argument('-e', '--error_messages', help='Specifies what error messages a potential response should be analyzed for. ' 106 | + 'Expected format is a comma separated string like value1,value2. The default strings that will be looked ' 107 | + 'for are error, stacktrace and trace', default='error,stacktrace,trace') 108 | parser.add_argument('-f', '--fuzz_file', type=str, 109 | help='File which contains the fuzzing attack payloads, one payload per line', required=True) 110 | parser.add_argument('-m', '--message_file', type=str, 111 | help='File which contains the WebSocket messages prepared to be fuzzed. Assumes one message per line. The string FUZZ_VALUE ' 112 | + 'will be replaced with the content of the fuzzing payloads file. If non fuzzed pre messages are required before successfully ' 113 | + 'fuzzing a message, list those pre messages line by line before the actual message and start them with PRE_MESSAGE', required=True) 114 | parser.add_argument('-p', '--proxy', type=str, help='Specifies proxy in format proxy:port') 115 | parser.add_argument('-t', '--timeout', type=int, help='Specifies how many seconds a WebSocket connection is kept open to receive responses', default=3) 116 | parser.add_argument('-u', '--url_path', type=str, help='URL path where protocol switching happens', default="/") 117 | parser.add_argument('-v', '--verbose', action='store_true', help='Increases program output in the console', default=False) 118 | parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) 119 | parser.add_argument('target', type=str, help='Defines target to fuzz in format protocol://hostname:port where protocol is either http/https and :port is optional') 120 | args = parser.parse_args() 121 | 122 | proxy = args.proxy 123 | if proxy is not None: 124 | if ":" in proxy: 125 | proxy_host = proxy.split(":")[0] 126 | proxy_port = proxy.split(":")[1] 127 | else: 128 | print("The proxy needs to be defined in the format hostname:port or ip:port!") 129 | sys.exit(1) 130 | else: 131 | proxy_host = None 132 | proxy_port = None 133 | 134 | if not args.target.startswith("http://") is not args.target.startswith("https://"): 135 | print("The target needs to be defined in the format protocol://hostname:port where protocol is either http or https!") 136 | sys.exit(1) 137 | 138 | error_messages = args.error_messages.split(',') 139 | 140 | fuzzer(args.auth_header, args.cookie, args.target, args.url_path, args.fuzz_file, args.message_file, proxy_host, proxy_port, args.timeout, args.verbose, error_messages) 141 | 142 | 143 | if __name__ == "__main__": 144 | main() 145 | --------------------------------------------------------------------------------