├── requirements.txt
├── examples
├── fuzzing_payloads.txt
└── websocket_messages.txt
├── CHANGELOG.md
├── LICENSE
├── .gitignore
├── README.md
└── websocket-fuzzer.py
/requirements.txt:
--------------------------------------------------------------------------------
1 | websocket-client
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------