├── .gitignore ├── Readme.md ├── fuzzer.py ├── opcua_fuzzer.py ├── opcua_services.py ├── opcua_session.py ├── opcua_utils.py ├── raw_messages_opcua.py ├── requirements.txt └── resources ├── DOS_kepwareex_CVE-2022-2848.gif └── boofuzz.png /.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 | # OPCUA NETWORK FUZZER 2 | 3 | We often use fuzzers in our research to find low-hanging bugs in network protocol implementations. Occasionally, we also develop custom fuzzers or harnesses to better go after specific targets. 4 | 5 | This repository contains a network fuzzer that we developed for [Pwn2Own 2022](https://www.zerodayinitiative.com/blog/2022/4/14/pwn2own-miami-2022-results) competition to fuzz OPCUA protocol. The [OPC UA](https://opcfoundation.org/about/opc-technologies/opc-ua/) (Open Platform Communications Unified Architecture) protocol is a standard means of data-exchange between industrial sensors and either on-premises servers or cloud management platforms. 6 | 7 | The fuzzer is based on the [boofuzz](https://github.com/jtpereyda/boofuzz) framework. For more info check out our Claroty Team82 blog [here](https://claroty.com/team82/research/team82-releases-homegrown-opc-ua-network-fuzzer-based-on-boofuzz) 8 | 9 | ![](resources/boofuzz.png) 10 | 11 | 12 | ![](resources/DOS_kepwareex_CVE-2022-2848.gif) 13 | 14 | 15 | ### Usage example 16 | **Install dependencies** 17 | ``` 18 | python3 -m pip install -r requirements.txt 19 | ``` 20 | **Run the fuzzer** 21 | ``` 22 | python3 opcua_fuzzer.py --target_host_ip 10.10.10.10 --target_host_port 4897 --target_app_name softing --request_opcua_to_fuzz browse_request 23 | ``` 24 | 25 | - `target_host_ip` IP of the OPCUA Server 26 | - `target_host_port` PORT which the OPCUA Server Listens to 27 | - `target_app_name` The type of the OPCUA Server to be fuzzed, choose from `kepware`, `dotnetstd`, `softing`, `prosys`, `unified`, `ignition`,`s2opc` 28 | - `request_opcua_to_fuzz` The OPCUA Server request type to fuzz, choose from `read_request`, `browse_request`, `browse_next_request`, `create_subscription_request`, `add_nodes_request`, `history_read_request` 29 | 30 | ### Results 31 | When the application crashes, the fuzzer will stop because no new connections could be made with the server. The last 1000 sent packets are saved in a `sqlite` database in the `boofuzz-results` directory within this repository. The status of the fuzzer can be monitored here: `http://localhost:26000/` 32 | 33 | 34 | ### Sanity 35 | Sometimes before running the fuzzer you want to ensure that the OPC UA session is correctly created and terminated. To send only one mutation with `\x00` payload above ReadRequest, change `IS_TEST_RUN` variable to `True` 36 | 37 | ### Supported OPCUA Server types: 38 | - [KEPServerEX](https://www.kepware.com/en-us/products/kepserverex/) (kepware) 39 | - [UA-.NETStandard](https://github.com/OPCFoundation/UA-.NETStandard) (dotnetstd) 40 | - [Softing OPC Server](https://industrial.softing.com/products/opc-opc-ua-software-platform.html) (softing) 41 | - [Prosys OPC UA Simulation Server](https://www.prosysopc.com/products/opc-ua-simulation-server/) (prosys) 42 | - [OPC UA C++ Demo Server](https://www.unified-automation.com/downloads/opc-ua-servers.html) (unified) 43 | - [Ignition's OPC UA Server](https://docs.inductiveautomation.com/display/DOC81/OPC+UA) (ignition) 44 | - [Systerel S2OPC OPC UA Demo Server](https://gitlab.com/systerel/S2OPC) 45 | 46 | ### Supported request types: 47 | - [Read Service](https://reference.opcfoundation.org/Core/Part4/v105/docs/5.10.2) (read_request): we are fuzzing the nodes to read 48 | - [Browse Service](https://reference.opcfoundation.org/Core/Part4/v105/docs/5.8.2) (browse_request): we are fuzzing the browsed nodes 49 | - [Browse Next Service](https://reference.opcfoundation.org/Core/Part4/v105/docs/5.8.3) (browse_next_request): we are fuzzing the browsed nodes 50 | - [Create Subscription Service](https://reference.opcfoundation.org/Core/Part4/v105/docs/5.13.2) (create_subscription_request): we are fuzzing the entire content 51 | - [Add Nodes Service](https://reference.opcfoundation.org/Core/Part4/v105/docs/5.7.2) (add_nodes_request): we are fuzzing the content that describes the nodes to be added 52 | - [History Read Service](https://reference.opcfoundation.org/v104/Core/docs/Part4/5.10.3/) (history_read_request): we are fuzzing the number of nodes and the entire content of the read history request 53 | 54 | * implementation: `opcua_services.py` 55 | 56 | ## How To Add Support for additional Server Type: 57 | While the majority of OPC-UA protocol stack implementations will work out-of-the-box with the currently supported servers, users can add new support for other OPC-UA implementations. Please note that as we mainly developed this fuzzer for ourselves, the procedure of adding new support is not very developer-friendly. So what do you need to do to add new OPC-UA implementation: 58 | 59 | 1. `raw_messages_opcua.py` - Copy raw packets from Wireshark when a regular client (e.g. [UaExpert](https://www.unified-automation.com/products/development-tools/uaexpert.html)) is connecting to the server. The needed OPC-UA messages are: Hello, Open Channel, Create Session, Activate Session, and Close Session. 60 | 2. `raw_messages_opcua.py` - Add support for your server in `get_raw_open_session_messages` and `get_raw_close_session_messages` functions. 61 | 3. `opcua_utils.py` - Add your new server type to the following functions: `target_apps`, `get_services_list`, `get_sanity_payload` 62 | 4. `opcua_utils.py` - Add hardcoded `ReadRequest` message (copy from Wireshark) for the new server. It will be used for sanity to check the server is functioning. 63 | 5. `opcua_utils.py` - Edit `close_session` function by adding your server to `target_app`. 64 | 6. `opcua_session.py` - Some servers require specific flow upon session creation. Add the changes to `create_session` function if needed. 65 | 66 | 67 | ## Notes 68 | - For each mutated packet separate OPCUA session is created (HELLO, OPEN CHANNEL, CREATE SESSION etc). 69 | - For most of the OPCUA Servers (.NET excluded) the session is terminated after each mutation. 70 | - To ensure fuzzer stability, validate that the target server is configured to allow many concurrently opened sessions (around ~1000) 71 | - When fuzzing, ensure that the fuzzed service doesn't restart automatically after a crash. 72 | 73 | 74 | ## Dependencies 75 | - [boofuzz](https://github.com/jtpereyda/boofuzz) 76 | - [construct](https://github.com/construct/construct) 77 | -------------------------------------------------------------------------------- /fuzzer.py: -------------------------------------------------------------------------------- 1 | from boofuzz import * 2 | import boofuzz.monitors.external_monitor 3 | 4 | 5 | def _s_update(name, value): 6 | """ 7 | Update the value of the named primitive in the currently open request. 8 | 9 | :type name: str 10 | :param name: Name of object whose value we wish to update 11 | :type value: Mixed 12 | :param value: Updated value 13 | """ 14 | 15 | # the blocks.CURRENT.names need to get the whole qualified.name 16 | found_names = [ 17 | n for n in blocks.CURRENT.names if n.rsplit(".")[-1] == name] 18 | if len(found_names) == 1: 19 | blocks.CURRENT.names[found_names[0]]._default_value = value 20 | if len(found_names) > 0 and 'previously_generated_node_id' in name: 21 | for i in found_names: 22 | blocks.CURRENT.names[i]._default_value = value 23 | 24 | 25 | # https://boofuzz.readthedocs.io/en/latest/user/static-protocol-definition.html 26 | class Fuzzer(object): 27 | def __init__(self, target_ip, target_port, packet_name="protocol_packet_type"): 28 | self.target_ip = target_ip 29 | self.target_port = target_port 30 | self.session = None 31 | self.target = None 32 | self.packet_name = packet_name 33 | 34 | def _init_target_connection(self): 35 | # Maybe the target connection was initiated before 36 | target_connection = SocketConnection(self.target_ip, self.target_port) 37 | self.target = Target(connection=target_connection) 38 | 39 | def _init_session(self): 40 | # NOTE: pre, post, start, stop (if not None): they must return True to continue fuzzing 41 | # index_end=1 for only one session 42 | self.session = Session(pre_send_callbacks=[self.session_pre_send], post_test_case_callbacks=[self.post_actions], 43 | restart_threshold=1, ignore_connection_reset=True, fuzz_db_keep_only_n_pass_cases=1000, index_end=None) 44 | self.target.procmon = boofuzz.monitors.external_monitor.External( 45 | pre=self.pre_actions, post=self.post_actions, start=self.start_actions, stop=None) 46 | self.session.add_target(self.target) 47 | 48 | # NOTE: Must be overriden by successors 49 | def _init_protocol_structure(self): 50 | raise NotImplementedError 51 | 52 | # Must call from outside 53 | def init(self): 54 | # Init 55 | self._init_target_connection() 56 | self._init_session() 57 | self._init_protocol_structure() 58 | 59 | def fuzz(self): 60 | self.session.connect(s_get(self.packet_name)) 61 | self.session.fuzz() 62 | 63 | # Actions before sending each packet - in the session context 64 | def session_pre_send(self, target, fuzz_data_logger, session, sock): 65 | pass 66 | 67 | # Actions before sending each packet - global context 68 | def pre_actions(self): 69 | return True 70 | 71 | # Actions after sending each packet (e.g. checking if target is alive) 72 | def post_actions(self, target, fuzz_data_logger, session, sock): 73 | return True 74 | 75 | # Actions before starting to fuzz (happens once) 76 | def start_actions(self): 77 | return True 78 | -------------------------------------------------------------------------------- /opcua_fuzzer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import string 4 | import random 5 | import os 6 | from boofuzz import * 7 | from opcua_session import create_session, OBJECT 8 | from opcua_services import * 9 | from opcua_utils import * 10 | from fuzzer import Fuzzer, _s_update 11 | import argparse 12 | 13 | IS_TEST_RUN = False 14 | 15 | 16 | class OPCUA_Deep_Fuzzer(Fuzzer): 17 | def __init__(self, target_app_name, request_name, *args, **kwargs): 18 | self.session_info = {} 19 | self.target_app = target_app_name 20 | self.sanity_payload = None 21 | self.request_name = request_name 22 | 23 | raise_if_target_app_invalid(target_app_name) 24 | raise_if_request_name_invalid(request_name) 25 | if not os.path.exists("./logs"): 26 | os.makedirs("./logs") 27 | logging.basicConfig(handlers=[logging.FileHandler(filename=f'./logs/fuzzer_runtime_{time.strftime("%m%d-%H%M%S")}.log', 28 | encoding='utf-8', mode='w+')], format='%(asctime)s %(message)s', level=logging.INFO) 29 | super(OPCUA_Deep_Fuzzer, self).__init__(*args, **kwargs) 30 | 31 | def _init_protocol_structure(self): 32 | if IS_TEST_RUN: 33 | s_initialize("opcua_request_sanity") 34 | # we need at least two permutations for fuzzer to work 35 | s_static(b'\x00', name="sanity_payload") 36 | s_group(name="test", values=[b'\x00', b'\x00']) 37 | else: 38 | init_request_by_service(self.request_name) 39 | 40 | def session_pre_send(self, target, fuzz_data_logger, session, sock): 41 | self.sock = sock 42 | try: 43 | secure_channel_id, auth_id, sequence, secure_token_id = create_session( 44 | self.sock, self.target_app) 45 | 46 | self.session_info[AttributeType.SECURE_CHANNEL_ID] = int.to_bytes( 47 | secure_channel_id, 4, "little") 48 | self.session_info[AttributeType.SECURE_TOCKEN_ID] = int.to_bytes( 49 | secure_token_id, 4, "little") 50 | self.session_info[AttributeType.SEQUENCE_ID] = int.to_bytes( 51 | sequence + 3, 4, "little") 52 | self.session_info[AttributeType.AUTH_ID] = OBJECT.build(auth_id) 53 | 54 | # update the fuzzer 55 | _s_update("secure_channel_id", 56 | self.session_info[AttributeType.SECURE_CHANNEL_ID]) 57 | _s_update("security_token_id", 58 | self.session_info[AttributeType.SECURE_TOCKEN_ID]) 59 | _s_update("auth_id", self.session_info[AttributeType.AUTH_ID]) 60 | _s_update("security_sequence_id", 61 | self.session_info[AttributeType.SEQUENCE_ID]) 62 | # node id should be prepared beforehand to cope with fuzzable dependency limitation 63 | _s_update("previously_generated_node_id_1", 64 | self.generate_node_id()) 65 | _s_update("previously_generated_node_id_2", 66 | self.generate_node_id()) 67 | _s_update("previously_generated_node_id_3", 68 | self.generate_node_id()) 69 | _s_update("previously_generated_node_id_4", 70 | self.generate_node_id()) 71 | _s_update("previously_generated_node_id_5", 72 | self.generate_node_id()) 73 | 74 | # Needed for test 75 | if IS_TEST_RUN: 76 | self.prepare_sanity_payload() 77 | _s_update("sanity_payload", self.sanity_payload) 78 | except Exception as e: 79 | logging.error(f"session_pre_send {e}") 80 | 81 | def post_actions(self, target, fuzz_data_logger, session, sock): 82 | try: 83 | if not IS_TEST_RUN: 84 | super().post_actions(target, fuzz_data_logger, session, sock) 85 | 86 | close_session(self.sock, self.target_app, self.session_info) 87 | except Exception as e: 88 | logging.error(f"post_actions {e}") 89 | 90 | def prepare_sanity_payload(self): 91 | payload_prepare_sanity = bytearray(get_sanity_payload(self.target_app)) 92 | 93 | secure_channel_id = self.session_info[AttributeType.SECURE_CHANNEL_ID] 94 | secure_sequence_id = self.session_info[AttributeType.SEQUENCE_ID] 95 | auth_id = self.session_info[AttributeType.AUTH_ID] 96 | 97 | set_data_at_offset(payload_prepare_sanity, 98 | secure_channel_id, AttributeType.SECURE_CHANNEL_ID) 99 | set_data_at_offset(payload_prepare_sanity, 100 | secure_sequence_id, AttributeType.SEQUENCE_ID) 101 | set_data_at_offset(payload_prepare_sanity, 102 | auth_id, AttributeType.AUTH_ID) 103 | 104 | # increase size by one to be able to send single packet on fuzzing 105 | size_offset = offsets_dict[AttributeType.SIZE] 106 | msg_size = int.from_bytes( 107 | payload_prepare_sanity[size_offset: size_offset + 4], "little") 108 | set_data_at_offset(payload_prepare_sanity, int.to_bytes( 109 | msg_size + 1, 4, "little"), AttributeType.SIZE) 110 | self.sanity_payload = bytes(payload_prepare_sanity) 111 | 112 | def fuzz(self): 113 | try: 114 | request_name = "opcua_request_sanity" if IS_TEST_RUN else self.request_name 115 | self.session.connect(s_get(request_name)) 116 | self.session.fuzz() 117 | 118 | except Exception as e: 119 | logging.error("This error happenned during fuzz function") 120 | logging.error(e) 121 | print("ERROR Occured!") 122 | 123 | @staticmethod 124 | def generate_node_id(): 125 | node_id = bytearray() 126 | node_id_type = random.randrange(6) 127 | node_id += pack(" 4, Int8ul), 139 | "requested_lifetime" / Int32ul 140 | ) 141 | 142 | OPEN_SECURE_CHANNEL_RESPONSE = Struct( 143 | # OpenSecureChannelResponse: 144 | "timestamp" / Int64ul, 145 | "request_handle" / Int32ul, 146 | "service_results" / Int32ul, 147 | "service_diagnostics" / Int8ul, 148 | "string_table_size" / Int32ul, 149 | "string_array" / If(this.string_table_size != 0xffffffff, 150 | Array(lambda x: x.string_table_size, OPC_STRING)), 151 | "additional_header_type_id" / Int16ul, 152 | "additional_header_encoding_mask" / Int8ul, 153 | "server_protocol_version" / Int32ul, 154 | "security_token" / SECURITY_TOKEN, 155 | "server_nonce" / OPC_STRING 156 | ) 157 | ######################################################################################################################## 158 | ######################################## CREATE ############################################### 159 | ######################################################################################################################## 160 | CREATE_SESSION_REQUEST = Struct( 161 | "authenticationobject" / OBJECT_HEADER, 162 | "binary_or_xml" / Int8ul, 163 | "application_uri" / OPC_STRING, 164 | "product_uri" / OPC_STRING, 165 | "encoding_mask" / Int8ul, 166 | "client_name" / OPC_STRING, 167 | "application_type" / Int32ul, 168 | "gateway_server_uri" / OPC_STRING, 169 | "discovery_profile_uri" / OPC_STRING, 170 | "num_of_discovery_urls" / Int32ul, 171 | "discovery_urls" / If(this.num_of_discovery_urls != 0xffffffff, 172 | Array(lambda x: x.num_of_discovery_urls, OPC_STRING)), 173 | "server_uri" / OPC_STRING, 174 | "enspoint_url" / OPC_STRING, 175 | "session_name" / OPC_STRING, 176 | "client_nonce_size" / Int32ul, 177 | "client_nonce" / Bytes(this.client_nonce_size), 178 | "client_certificate" / OPC_STRING, 179 | # "uukn" / Int32ul, 180 | "request_session_timeout" / Float64l, 181 | "max_response_message_size" / Int32ul, 182 | ) 183 | 184 | CREATE_SESSION_RESPONSE = Struct( 185 | "timestamp" / Int64ul, 186 | "request_handler" / Int32ul, 187 | "service_results" / Int32ul, 188 | "service_diagnostics_encoding_mask" / Int8ul, 189 | "string_array" / ARRAY_OF_STRINGS, 190 | "ext_obj" / OBJECT, 191 | "encoding_mask" / Int8ul, 192 | "session_id" / OBJECT, 193 | "auth_token" / OBJECT 194 | ) 195 | ACTIVATE_REQUEST = Struct( 196 | "auth_token" / OBJECT_HEADER, 197 | "encoding_mask" / Int8ul, 198 | "algo" / OPC_STRING, 199 | "signature" / OPC_BYTES, 200 | "client_cert_array_size" / Int32ul, 201 | "client_cert_array" / If(this.client_cert_array_size != 202 | 0xffffffff, Array(this.client_cert_array_size, OPC_BYTES)), 203 | "local_ids_array_size" / Int32ul, 204 | "local_ids_array" / If(this.client_cert_array_size != 205 | 0xffffffff, Array(this.local_ids_array_size, OPC_STRING)), 206 | "user_id_token" / OBJECT, 207 | "encoding_mask2" / Int8ul, 208 | "unk" / Int32ul, 209 | "policy_id" / OPC_STRING, 210 | "sign_algo" / OPC_STRING, 211 | "sign_sig" / OPC_BYTES 212 | ) 213 | ######################################################################################################################## 214 | ######################################## ENCODABLES ############################################### 215 | ######################################################################################################################## 216 | ENCODEABLE_OBJECT = Struct( 217 | "node_id_encoding_mask" / Int8ul, 218 | "node_id_namespace_index" / Int8ul, 219 | "node_id_identifier_numeric" / Int16ul, 220 | "object" / Switch(this.node_id_identifier_numeric, 221 | { 222 | 446: OPEN_SECURE_CHANNEL_REQUEST, 223 | 449: OPEN_SECURE_CHANNEL_RESPONSE, 224 | 461: CREATE_SESSION_REQUEST, 225 | 464: CREATE_SESSION_RESPONSE, 226 | 467: ACTIVATE_REQUEST, 227 | }) 228 | ) 229 | 230 | OPEN_REQUEST = Struct( 231 | "secure_channel_id" / Int32ul, 232 | # http://opcfoundation.org/UA/SecurityPolicy#None 233 | "securit_policy_uri" / OPC_STRING, 234 | # ffffff 235 | "sender_certificate" / OPC_STRING, 236 | "reciever_certificate_thumbprint" / OPC_STRING, 237 | "sequence_number" / Int32ul, 238 | "request_id_number" / Int32ul, 239 | 240 | # encodable_object: 241 | "object" / ENCODEABLE_OBJECT 242 | ) 243 | 244 | HELLO_REQUEST = Struct( 245 | "version" / Int32ul, 246 | "receive_buffer_size" / Int32ul, 247 | "send_buffer_size" / Int32ul, 248 | "max_message_size" / Int32ul, 249 | "max_chunk_count" / Int32ul 250 | ) 251 | ######################################################################################################################## 252 | ######################################## HEADERS ############################################### 253 | ######################################################################################################################## 254 | OPEN = Struct( 255 | "secure_channel_id" / Int32ul, 256 | # http://opcfoundation.org/UA/SecurityPolicy#None 257 | "securit_policy_uri" / OPC_STRING, 258 | # ffffff 259 | "sender_certificate" / OPC_STRING, 260 | "reciever_certificate_thumbprint" / OPC_STRING, 261 | "sequence_number" / Int32ul, 262 | "request_id_number" / Int32ul, 263 | 264 | # encodable_object: 265 | "object" / ENCODEABLE_OBJECT 266 | ) 267 | 268 | MSG = Struct( 269 | "secure_channel_id" / Int32ul, 270 | "security_token_id" / Int32ul, 271 | "security_sequence_number" / Int32ul, 272 | "security_request_idr" / Int32ul, 273 | "object" / ENCODEABLE_OBJECT 274 | ) 275 | HELLO = Struct( 276 | "version" / Int32ul, 277 | "receive_buffer_size" / Int32ul, 278 | "send_buffer_size" / Int32ul, 279 | "max_message_size" / Int32ul, 280 | "max_chunk_count" / Int32ul 281 | ) 282 | 283 | HELLO_MSG = Struct( 284 | "hello_header" / HELLO, 285 | # opc.tcp://ip:port 286 | "endpoint_url" / OPC_STRING) 287 | 288 | OPCUA_MESSAGE = Struct( 289 | "message_type" / PaddedString(3, "utf8"), 290 | "chunk_type" / PaddedString(1, "utf8"), 291 | "message_size" / Int32ul, 292 | "opc_data" / Switch(this.message_type, 293 | {"HEL": HELLO_MSG, 294 | "ACK": HELLO, 295 | "OPN": OPEN, 296 | "MSG": MSG}), 297 | "leftover" / GreedyBytes 298 | ) 299 | 300 | 301 | def my_recv(s, prev=b""): 302 | header = s.recv(8) 303 | tmp_resp = b"" 304 | message_size = struct.unpack("I", header[4:8])[0] 305 | header_type = header[3:4] 306 | 307 | payload_size_left = message_size - 8 308 | while payload_size_left > 0: 309 | response = s.recv(max(1024, payload_size_left)) 310 | tmp_resp += response 311 | payload_size_left -= len(response) 312 | 313 | if header_type == b"F": 314 | return header[0:3] + b"F" + struct.pack("I", len(prev + tmp_resp)) + prev + tmp_resp 315 | else: 316 | return my_recv(s, prev + tmp_resp) 317 | 318 | 319 | def send_recv(s, msg): 320 | msg_length = len(msg) 321 | msg = bytearray(msg) 322 | msg[4:8] = struct.pack("I", msg_length) 323 | s.send(msg) 324 | return my_recv(s) 325 | 326 | 327 | def send_recv_parse(s, msg, construct_obj=OPCUA_MESSAGE): 328 | res = send_recv(s, msg) 329 | return construct_obj.parse(res) 330 | 331 | 332 | def recvall(sock, n): 333 | data = bytearray() 334 | while len(data) < n: 335 | packet = sock.recv(n - len(data)) 336 | if not packet: 337 | return None 338 | data.extend(packet) 339 | return data 340 | 341 | 342 | def create_session(sock, program_type, session_timeout=360000, open_timestamp=None, requested_lifetime=279520, session_name=None): 343 | hel_raw, opn_raw, create_raw, activate_raw = get_raw_open_session_messages( 344 | program_type) 345 | # HEL message 346 | send_recv(sock, hel_raw) 347 | 348 | # OPN message 349 | if open_timestamp or requested_lifetime: 350 | opn_parsed = OPCUA_MESSAGE.parse(opn_raw) 351 | 352 | if open_timestamp: 353 | opn_parsed.opc_data.object.object.authentication_token.timestamp = open_timestamp 354 | elif requested_lifetime: 355 | opn_parsed.opc_data.object.object.requested_lifetime = requested_lifetime 356 | open_msg = OPCUA_MESSAGE.build(opn_parsed) 357 | else: 358 | open_msg = opn_raw 359 | 360 | open_resp = send_recv_parse(sock, open_msg) 361 | 362 | secure_channel_id = open_resp.opc_data.secure_channel_id 363 | secure_token_id = open_resp.opc_data.object.object.security_token.token_id 364 | # we take this from request which make the this field be server dependent 365 | sequence = OPCUA_MESSAGE.parse(opn_raw).opc_data.sequence_number 366 | 367 | # Create 368 | create_session_parsed = OPCUA_MESSAGE.parse(create_raw) 369 | create_session_parsed.opc_data.secure_channel_id = secure_channel_id 370 | if (program_type == "ignition") or (program_type == "s2opc"): 371 | create_session_parsed.opc_data.security_token_id = secure_token_id 372 | if session_timeout: 373 | create_session_parsed.opc_data.object.object.request_session_timeout = session_timeout 374 | if session_name: 375 | create_session_parsed.opc_data.object.object.session_name.str = session_name 376 | create_session_parsed.opc_data.object.object.session_name.str_length = len( 377 | session_name) 378 | create_session_built = OPCUA_MESSAGE.build(create_session_parsed) 379 | create_resp = send_recv_parse(sock, create_session_built) 380 | 381 | auth_id = create_resp.opc_data.object.object.auth_token 382 | 383 | # Activate 384 | activate_session_parsed = OPCUA_MESSAGE.parse(activate_raw) 385 | if (program_type == "ignition") or (program_type == "s2opc"): 386 | activate_session_parsed.opc_data.security_token_id = secure_token_id 387 | activate_session_parsed.opc_data.secure_channel_id = secure_channel_id 388 | activate_session_parsed.opc_data.object.object.auth_token.main_object = auth_id 389 | activate_session_build = OPCUA_MESSAGE.build(activate_session_parsed) 390 | 391 | send_recv(sock, activate_session_build) 392 | 393 | return secure_channel_id, auth_id, sequence, secure_token_id 394 | -------------------------------------------------------------------------------- /opcua_utils.py: -------------------------------------------------------------------------------- 1 | # this file is used for pwn2own competition against 5 opc targets 2 | from enum import Enum 3 | from boofuzz import * 4 | from raw_messages_opcua import get_raw_close_session_messages 5 | from struct import pack, unpack 6 | 7 | OPCUA_RESP_SIZE = 1024 8 | 9 | target_apps = ["softing", "dotnetstd", 10 | "prosys", "unified", "kepware", "triangle", "ignition", "s2opc"] 11 | 12 | opcua_services_list = [8917, 631, 11889, 11890, 263, 266, 269, 272, 275, 278, 281, 284, 287, 298, 8251, 13 | 310, 391, 394, 397, 422, 425, 306, 314, 428, 431, 434, 437, 440, 443, 14 | 446, 449, 452, 455, 346, 458, 461, 464, 318, 321, 324, 327, 940, 467, 470, 15 | 473, 476, 479, 482, 351, 354, 357, 360, 363, 366, 369, 372, 375, 378, 485, 16 | 488, 491, 381, 494, 497, 384, 500, 503, 387, 506, 509, 513, 516, 520, 524, 17 | 527, 530, 533, 536, 539, 542, 545, 548, 551, 554, 557, 560, 563, 566, 569, 18 | 333, 337, 343, 572, 575, 579, 582, 585, 588, 591, 594, 597, 600, 603, 606, 19 | 609, 612, 615, 618, 621, 624, 628, 260, 634, 637, 640, 643, 646, 649, 652, 20 | 655, 658, 11226, 11227, 661, 664, 667, 670, 673, 676, 679, 682, 11300, 685, 688, 21 | 691, 694, 697, 931, 700, 703, 706, 709, 712, 715, 721, 724, 727, 950, 730, 22 | 733, 736, 739, 742, 745, 748, 751, 754, 757, 760, 763, 766, 769, 772, 775, 23 | 778, 781, 784, 787, 790, 793, 796, 799, 802, 805, 947, 811, 808, 916, 919, 24 | 922, 820, 823, 826, 829, 832, 835, 838, 841, 844, 847, 850, 401, 404, 407, 25 | 410, 413, 416, 419, 340, 855, 11957, 11958, 858, 861, 864, 867, 870, 873, 301, 26 | 876, 879, 899, 886, 889, 12181, 12182, 12089, 12090, 896, 893] 27 | 28 | OPCUA_SERVICE = [i.to_bytes(2, "little") for i in opcua_services_list] 29 | 30 | 31 | opcua_services_list_unified = [631, 11226, 11227, 11300, 11957, 11958, 12089, 12090, 12181, 12182, 260, 263, 266, 269, 272, 275, 278, 281, 284, 287, 298, 301, 306, 310, 314, 318, 321, 324, 327, 333, 340, 346, 351, 354, 357, 360, 363, 366, 369, 372, 375, 378, 381, 384, 387, 391, 394, 397, 401, 404, 407, 413, 416, 419, 422, 425, 428, 431, 434, 437, 440, 443, 449, 455, 458, 461, 464, 467, 470, 473, 476, 479, 482, 485, 488, 491, 494, 497, 500, 503, 506, 509, 513, 516, 520, 524, 527, 530, 533, 536, 539, 542, 545, 548, 551, 554, 557, 560, 563, 566, 32 | 569, 572, 575, 579, 582, 585, 588, 594, 597, 600, 603, 606, 609, 612, 615, 618, 621, 624, 628, 634, 637, 640, 646, 649, 652, 655, 658, 661, 664, 667, 670, 673, 676, 679, 682, 685, 688, 691, 694, 697, 700, 703, 706, 709, 712, 715, 724, 727, 730, 736, 739, 742, 745, 748, 751, 754, 757, 760, 763, 766, 769, 772, 775, 778, 781, 784, 787, 790, 793, 796, 799, 802, 805, 808, 811, 820, 823, 8251, 826, 829, 832, 835, 838, 841, 844, 847, 850, 855, 858, 861, 864, 867, 870, 873, 876, 879, 886, 889, 8917, 893, 896, 899, 916, 919, 922, 940, 950, ] 33 | opcua_services_list_dotnetstd = [631, 11889, 11890, 263, 266, 269, 272, 275, 278, 281, 284, 287, 298, 8251, 8917, 310, 391, 394, 397, 422, 425, 306, 314, 428, 431, 434, 437, 440, 443, 449, 455, 346, 458, 461, 464, 318, 321, 324, 327, 940, 467, 470, 473, 476, 479, 482, 351, 354, 357, 360, 363, 366, 369, 372, 375, 378, 485, 491, 381, 497, 384, 503, 387, 509, 513, 516, 520, 524, 527, 530, 533, 536, 539, 542, 545, 548, 551, 554, 557, 560, 563, 566, 569, 333, 337, 343, 572, 575, 579, 582, 585, 588, 591, 594, 597, 600, 603, 606, 609, 612, 615, 618, 624, 628, 34 | 260, 634, 637, 640, 643, 646, 649, 652, 655, 658, 11226, 11227, 661, 664, 667, 670, 673, 676, 679, 682, 11300, 685, 688, 691, 694, 697, 931, 700, 703, 706, 709, 712, 715, 721, 724, 727, 950, 730, 733, 736, 739, 742, 745, 748, 751, 754, 757, 760, 763, 766, 769, 772, 775, 778, 781, 784, 787, 790, 793, 796, 799, 802, 805, 947, 811, 808, 916, 919, 922, 820, 823, 826, 829, 832, 835, 838, 844, 847, 850, 401, 404, 407, 410, 413, 416, 419, 340, 855, 11957, 11958, 858, 861, 864, 867, 870, 873, 301, 876, 879, 899, 886, 889, 12181, 12182, 12089, 12090, 896, 893] 35 | opcua_services_list_triangle = [631, 11889, 11890, 263, 266, 269, 272, 275, 278, 281, 284, 287, 298, 8251, 310, 394, 397, 422, 425, 306, 314, 428, 431, 434, 437, 440, 449, 455, 346, 458, 461, 464, 318, 321, 324, 327, 940, 467, 470, 473, 476, 479, 482, 357, 366, 369, 378, 488, 491, 381, 494, 497, 500, 503, 387, 506, 509, 520, 524, 527, 530, 533, 536, 539, 542, 545, 551, 554, 557, 560, 563, 566, 569, 337, 343, 572, 575, 579, 582, 585, 588, 591, 600, 603, 606, 609, 612, 615, 36 | 618, 621, 624, 628, 260, 634, 637, 640, 643, 646, 652, 655, 658, 11226, 11227, 661, 664, 667, 670, 673, 676, 682, 11300, 685, 691, 694, 697, 931, 700, 703, 706, 709, 712, 715, 721, 724, 727, 733, 736, 742, 745, 748, 751, 754, 757, 760, 763, 766, 769, 772, 775, 778, 781, 784, 787, 790, 793, 796, 799, 802, 805, 947, 811, 916, 919, 922, 826, 829, 832, 835, 838, 841, 844, 847, 850, 401, 404, 407, 410, 413, 416, 419, 340, 855, 11957, 11958, 864, 867, 870, 889, 12089, 896, 893] 37 | opcua_services_list_prosys = [631, 11889, 11890, 263, 266, 269, 272, 275, 278, 281, 284, 287, 298, 8251, 310, 394, 397, 425, 306, 314, 431, 434, 440, 446, 449, 452, 455, 346, 458, 461, 464, 318, 321, 324, 327, 940, 470, 473, 476, 479, 482, 357, 366, 369, 378, 488, 491, 381, 494, 497, 500, 387, 506, 509, 516, 520, 524, 527, 530, 533, 539, 545, 554, 557, 560, 563, 566, 569, 337, 343, 38 | 575, 579, 582, 600, 603, 615, 618, 621, 624, 628, 260, 637, 640, 646, 652, 11226, 664, 670, 673, 676, 682, 11300, 685, 691, 694, 931, 700, 706, 712, 730, 742, 745, 748, 751, 754, 757, 760, 763, 766, 769, 775, 778, 781, 784, 787, 790, 793, 796, 799, 802, 919, 826, 832, 841, 844, 847, 401, 404, 407, 410, 413, 416, 419, 340, 855, 11958, 864, 867, 870, 879, 899, 889, 12089, 896, 893, ] 39 | opcua_services_list_kepware = [631, 422, 428, 437, 446, 452, 461, 467, 473, 479, 527, 40 | 533, 554, 560, 566, 673, 751, 763, 769, 775, 781, 787, 793, 799, 826, 832, 841, 847, ] 41 | opcua_services_list_softing = [631, 11889, 11890, 263, 266, 269, 272, 275, 278, 281, 284, 287, 298, 8251, 310, 394, 397, 422, 425, 306, 314, 428, 431, 434, 437, 440, 449, 455, 346, 458, 461, 464, 318, 321, 324, 327, 940, 467, 470, 473, 476, 479, 482, 357, 366, 369, 378, 488, 491, 381, 494, 497, 500, 503, 506, 509, 520, 524, 527, 530, 533, 536, 539, 542, 545, 551, 554, 557, 560, 563, 566, 569, 337, 343, 572, 575, 579, 582, 585, 588, 591, 600, 603, 606, 609, 612, 615, 618, 42 | 621, 624, 628, 260, 634, 637, 640, 643, 646, 652, 655, 658, 11226, 11227, 661, 664, 667, 670, 673, 676, 682, 11300, 685, 691, 694, 697, 931, 700, 703, 706, 709, 712, 715, 721, 724, 727, 733, 736, 742, 745, 748, 751, 754, 757, 760, 763, 766, 769, 772, 775, 778, 781, 784, 787, 790, 793, 796, 799, 802, 805, 947, 811, 916, 919, 922, 826, 829, 832, 835, 838, 841, 844, 847, 850, 401, 404, 407, 410, 413, 416, 419, 340, 855, 11957, 11958, 864, 867, 870, 876, 889, 12089, 896, 893, ] 43 | opcua_services_list_s2opc = [631, 422, 428, 437, 446, 452, 461, 467, 473, 479, 527, 44 | 533, 554, 560, 566, 673, 751, 763, 769, 775, 781, 787, 793, 799, 826, 832, 841, 847, ] 45 | 46 | 47 | class AttributeType(Enum): 48 | SECURE_CHANNEL_ID = 1 49 | SEQUENCE_ID = 2 50 | REQUEST_ID = 3 51 | AUTH_ID = 4 52 | SIZE = 5 53 | SERVICE_ID = 6 54 | SERVICE_RESULT = 8 55 | ERROR_RESULT = 9 56 | MSG_TYPE = 10 57 | SECURE_TOCKEN_ID = 11 58 | 59 | 60 | # Note that those offset are correct only for specific request types, such are close types 61 | offsets_dict = {AttributeType.MSG_TYPE: 0, AttributeType.SIZE: 4, AttributeType.SECURE_CHANNEL_ID: 8, 62 | AttributeType.SECURE_TOCKEN_ID: 12, 63 | AttributeType.ERROR_RESULT: 8, AttributeType.SEQUENCE_ID: 16, AttributeType.REQUEST_ID: 20, 64 | AttributeType.SERVICE_ID: 26, AttributeType.AUTH_ID: 28, AttributeType.SERVICE_RESULT: 40} 65 | 66 | 67 | class ResponseType(Enum): 68 | SERVICE_FAULT = 1 69 | ERROR = 2 70 | REGULAR_RESPONSE = 3 71 | 72 | 73 | class OPCUARepeat(Repeat): 74 | def __init__( 75 | self, 76 | name=None, 77 | block_name=None, 78 | request=None, 79 | bound_block_repetitions=None, 80 | *args, 81 | **kwargs 82 | ): 83 | self.bound_block_repetitions = bound_block_repetitions 84 | super(OPCUARepeat, self).__init__( 85 | name, block_name, request, fuzzable=bound_block_repetitions is None, *args, **kwargs) 86 | 87 | def get_value(self, mutation_context=None): 88 | if self.bound_block_repetitions is not None: 89 | qualified_name_list = [n for n in self.request.names if n.rsplit( 90 | ".")[-1] == self.bound_block_repetitions] 91 | if len(qualified_name_list) != 1: 92 | raise Exception( 93 | "block for repetitions does not exist or there are more than 1!") 94 | qualified_name = qualified_name_list[0] 95 | if mutation_context is None or qualified_name not in mutation_context.mutations: 96 | value = self.request.names[qualified_name].original_value() 97 | else: 98 | value = mutation_context.mutations[qualified_name].value 99 | # the "non repeated" block is already rendered 100 | return 0 if value == b'\x00' else unpack(" 0: 175 | response = sock.recv(OPCUA_RESP_SIZE) 176 | tmp_resp += response 177 | payload_size_left -= len(response) 178 | return tmp_resp 179 | 180 | 181 | def close_session(sock, target_app, ses_info): 182 | try: 183 | close_session_payload, close_channel_payload = get_raw_close_session_messages( 184 | target_app) 185 | if target_app in ["prosys", "kepware", "softing", "unified", "triangle", "ignition", "s2opc"]: 186 | # close session 187 | close_session_payload = bytearray(close_session_payload) 188 | 189 | set_data_at_offset( 190 | close_session_payload, ses_info[AttributeType.SECURE_CHANNEL_ID], AttributeType.SECURE_CHANNEL_ID) 191 | 192 | set_data_at_offset( 193 | close_session_payload, ses_info[AttributeType.SECURE_TOCKEN_ID], AttributeType.SECURE_TOCKEN_ID) 194 | 195 | sequence_id = int.to_bytes(int.from_bytes( 196 | ses_info[AttributeType.SEQUENCE_ID], "little") + 1, 4, "little") 197 | set_data_at_offset(close_session_payload, 198 | sequence_id, AttributeType.SEQUENCE_ID) 199 | 200 | request_id = int.to_bytes(5, 4, "little") 201 | set_data_at_offset(close_session_payload, 202 | request_id, AttributeType.REQUEST_ID) 203 | 204 | set_data_at_offset( 205 | close_session_payload, ses_info[AttributeType.AUTH_ID], AttributeType.AUTH_ID) 206 | 207 | sock.send(close_session_payload) 208 | sock.recv(OPCUA_RESP_SIZE) 209 | # self.receive_rest_of_response(response) 210 | 211 | # close channel 212 | close_channel_payload = bytearray(close_channel_payload) 213 | 214 | set_data_at_offset( 215 | close_channel_payload, ses_info[AttributeType.SECURE_CHANNEL_ID], AttributeType.SECURE_CHANNEL_ID) 216 | set_data_at_offset( 217 | close_channel_payload, ses_info[AttributeType.SECURE_TOCKEN_ID], AttributeType.SECURE_TOCKEN_ID) 218 | sequence_id = int.to_bytes(int.from_bytes( 219 | ses_info[AttributeType.SEQUENCE_ID], "little") + 2, 4, "little") 220 | set_data_at_offset(close_channel_payload, 221 | sequence_id, AttributeType.SEQUENCE_ID) 222 | 223 | request_id = int.to_bytes(6, 4, "little") 224 | set_data_at_offset(close_channel_payload, 225 | request_id, AttributeType.REQUEST_ID) 226 | 227 | sock.send(close_channel_payload) 228 | 229 | except Exception as e: 230 | # some exceptions may occur during the close session, we do not want to count them as failure 231 | # in contrast to boofuzz 232 | pass 233 | 234 | 235 | def set_data_at_offset(payload, value, atype): 236 | offset = offsets_dict[atype] 237 | attribute_size = len(value) 238 | payload[offset: offset + attribute_size] = value 239 | 240 | 241 | def get_size_of_the_payload(payload): 242 | offset = offsets_dict[AttributeType.SIZE] 243 | return int.from_bytes(payload[offset: offset + 4], "little") 244 | 245 | 246 | def check_service_fault_or_error(payload): 247 | service_fault = check_service_fault(payload) 248 | if service_fault is not None: 249 | return ResponseType.SERVICE_FAULT, service_fault 250 | 251 | error = check_error_on_response(payload) 252 | if error is not None: 253 | return ResponseType.ERROR, error 254 | 255 | service_number = get_service_id_as_int(payload) 256 | return ResponseType.REGULAR_RESPONSE, service_number 257 | 258 | 259 | def get_service_id_as_int(payload): 260 | offset = offsets_dict[AttributeType.SERVICE_ID] 261 | return unpack("Q\xd5\xd7\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x80\xee6\x00' 29 | KEPWARE_CREATE_SESSION = b"MSGFK\x01\x00\x00\t\x94\x85>\x01\x00\x00\x004\x00\x00\x00\x02\x00\x00\x00\x01\x00\xcd\x01\x02\x00\x00\x9a\xca\xd4~sq\xd3>Q\xd5\xd7\x01AB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10'\x00\x00\x00\x00\x00.\x00\x00\x00urn:DESKTOP-OOGRRNF:UnifiedAutomation:UaExpert\x1e\x00\x00\x00urn:UnifiedAutomation:UaExpert\x02\x1b\x00\x00\x00Unified Automation UaExpert\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x1b\x00\x00\x00opc.tcp://10.10.6.248:49320.\x00\x00\x00urn:DESKTOP-OOGRRNF:UnifiedAutomation:UaExpert \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x80O2A\x00\x00\x00\x01" 30 | KEPWARE_ACTIVATE_SESSION = b"MSGFu\x00\x00\x00\t\x94\x85>\x01\x00\x00\x005\x00\x00\x00\x03\x00\x00\x00\x01\x00\xd3\x01\x02\x00\x00d\xf3\xa8%sq\xd3>Q\xd5\xd7\x01BB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10'\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00en-US\x01\x00A\x01\x01\r\x00\x00\x00\t\x00\x00\x00Anonymous\xff\xff\xff\xff\xff\xff\xff\xff" 31 | KEPWARE_CLOSE_SESSION = b"\x4d\x53\x47\x46\x3f\x00\x00\x00\x09\x94\x85\x3e\x01\x00\x00\x00\x4a\x00\x00\x00\x18\x00\x00\x00\x01\x00\xd9\x01\x02\x00\x00\x64\xf3\xa8\x25\xa2\xc5\xd2\x41\x51\xd5\xd7\x01\x57\x42\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10\x27\x00\x00\x00\x00\x00\x01" 32 | KEPWARE_CLOSE_CHANNEL = b"\x43\x4c\x4f\x46\x39\x00\x00\x00\x09\x94\x85\x3e\x01\x00\x00\x00\x4b\x00\x00\x00\x19\x00\x00\x00\x01\x00\xc4\x01\x00\x00\xa2\xc5\xd2\x41\x51\xd5\xd7\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00" 33 | 34 | 35 | TRIANGLE_HELLO_MSG = b'HELF<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01\x88\x13\x00\x00\x1c\x00\x00\x00opc.tcp://10.10.7.4:4885/SDG' 36 | TRIANGLE_OPEN_SECURE_CHANNEL = b'OPNF\x85\x00\x00\x00\x00\x00\x00\x00/\x00\x00\x00http://opcfoundation.org/UA/SecurityPolicy#None\xff\xff\xff\xff\xff\xff\xff\xff3\x00\x00\x00\x01\x00\x00\x00\x01\x00\xbe\x01\x00\x00\xe3_\x91\xf0 \xd7\xd7\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\xe0\x93\x04\x00' 37 | TRIANGLE_CREATE_SESSION = b"MSGFG\x01\x00\x00\x04\x00\x00\x00\x01\x00\x00\x004\x00\x00\x00\x02\x00\x00\x00\x01\x00\xcd\x01\x00\x00\xc6\xaa\x94\xf0 \xd7\xd7\x01AB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10'\x00\x00\x00\x00\x00.\x00\x00\x00urn:DESKTOP-OOGRRNF:UnifiedAutomation:UaExpert\x1e\x00\x00\x00urn:UnifiedAutomation:UaExpert\x02\x1b\x00\x00\x00Unified Automation UaExpert\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x1c\x00\x00\x00opc.tcp://10.10.7.4:4885/SDG.\x00\x00\x00urn:DESKTOP-OOGRRNF:UnifiedAutomation:UaExpert \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00L\xdd@\x00\x00\x00\x01" 38 | TRIANGLE_ACTIVATE_SESSION = b"MSGF\x9c\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x005\x00\x00\x00\x03\x00\x00\x00\x01\x00\xd3\x01\x05\x00\x00 \x00\x00\x00\x12h'g\xffSa>\xd3\x9dGy\xd7>d\xe7\xf9,ua\x03\xcd\x92\xf0\xbc\x14R\xe2\x89\xaf\x1c\x83\x83\x04\x97\xf0 \xd7\xd7\x01BB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10'\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00en-US\x01\x00A\x01\x01\x14\x00\x00\x00\x10\x00\x00\x00Anonymous_Policy\xff\xff\xff\xff\xff\xff\xff\xff" 39 | TRIANGLE_CLOSE_SESSION = b"\x4d\x53\x47\x46\x5f\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x50\x00\x00\x00\x1e\x00\x00\x00\x01\x00\xd9\x01\x05\x00\x00\x20\x00\x00\x00\x12\x68\x27\x67\xff\x53\x61\x3e\xd3\x9d\x47\x79\xd7\x3e\x64\xe7\xf9\x2c\x75\x61\x03\xcd\x92\xf0\xbc\x14\x52\xe2\x89\xaf\x1c\x83\x99\xf1\xc9\xf2\x20\xd7\xd7\x01\x5d\x42\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10\x27\x00\x00\x00\x00\x00\x01" 40 | TRIANGLE_CLOSE_CHANNEL = b"\x43\x4c\x4f\x46\x39\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x51\x00\x00\x00\x1f\x00\x00\x00\x01\x00\xc4\x01\x00\x00\x23\x03\xca\xf2\x20\xd7\xd7\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00" 41 | 42 | 43 | DOTNET_HELLO_MSG = b'HELF[\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01\x88\x13\x00\x00;\x00\x00\x00opc.tcp://desktop-eq75855:62541/Quickstarts/ReferenceServer' 44 | DOTNET_OPEN_SECURE_CHANNEL = b'OPNF\x85\x00\x00\x00\x00\x00\x00\x00/\x00\x00\x00http://opcfoundation.org/UA/SecurityPolicy#None\xff\xff\xff\xff\xff\xff\xff\xff3\x00\x00\x00\x01\x00\x00\x00\x01\x00\xbe\x01\x00\x00Z\xa9\x80\xf4a\xd9\xd7\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\xe0\x93\x04\x00' 45 | DOTNET_CREATE_SESSION = b"MSGFf\x01\x00\x002\xb6\x0c\x00\x01\x00\x00\x004\x00\x00\x00\x02\x00\x00\x00\x01\x00\xcd\x01\x00\x00Z\xa9\x80\xf4a\xd9\xd7\x01AB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10'\x00\x00\x00\x00\x00.\x00\x00\x00urn:DESKTOP-OOGRRNF:UnifiedAutomation:UaExpert\x1e\x00\x00\x00urn:UnifiedAutomation:UaExpert\x02\x1b\x00\x00\x00Unified Automation UaExpert\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff;\x00\x00\x00opc.tcp://desktop-eq75855:62541/Quickstarts/ReferenceServer.\x00\x00\x00urn:DESKTOP-OOGRRNF:UnifiedAutomation:UaExpert \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00L\xdd@\x00\x00\x00\x01" 46 | DOTNET_ACTIVATE_SESSION = b"MSGF\x8d\x00\x00\x002\xb6\x0c\x00\x01\x00\x00\x005\x00\x00\x00\x03\x00\x00\x00\x01\x00\xd3\x01\x05\x00\x00 \x00\x00\x00\x0bz\xe4\xac\xa2\xc5}\xf8J\x0c\xea\x11q\xe1A\xf0)q\xa5v^\xfa\xe6\xa5\xe3`\x8d\xb7\xfeN$\xc6\x10V\xb0\xf4a\xd9\xd7\x01BB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10'\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00en-US\x01\x00A\x01\x01\x05\x00\x00\x00\x01\x00\x00\x000\xff\xff\xff\xff\xff\xff\xff\xff" 47 | 48 | 49 | IGNITION_HELLO_MSG = b'HELF:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01\x88\x13\x00\x00\x1a\x00\x00\x00opc.tcp://10.10.6.40:62541' 50 | IGNITION_OPEN_SECURE_CHANNEL = b'OPNF\x85\x00\x00\x00\x00\x00\x00\x00/\x00\x00\x00http://opcfoundation.org/UA/SecurityPolicy#None\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\xbe\x01\x00\x00d\x988\\M\n\xd9\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x80\xee6\x00' 51 | IGNITION_CREATE_SESSION = b"MSGFB\x01\x00\x00\n\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\x00\xcd\x01\x00\x00d\x988\\M\n\xd9\x01AB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10\'\x00\x00\x00\x00\x00.\x00\x00\x00urn:DESKTOP-HN03T7J:UnifiedAutomation:UaExpert\x1e\x00\x00\x00urn:UnifiedAutomation:UaExpert\x02\x18\x00\x00\x00UaExpert@DESKTOP-HN03T7J\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x1a\x00\x00\x00opc.tcp://10.10.6.40:62541.\x00\x00\x00urn:DESKTOP-HN03T7J:UnifiedAutomation:UaExpert \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x80O2A\x00\x00\x00\x01" 52 | IGNITION_ACTIVATE_SESSION = b"MSGF\x95\x00\x00\x00\n\x00\x00\x00\x0c\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x01\x00\xd3\x01\x05\x00\x00 \x00\x00\x00\x89\r\x17\x9f\x8c\xd9\xb0\r\x91RR\xebQ\xa2zt\xe1\xdf\xff\xb4\x7fX\x86\xb2\x04\xce\x1e\x93\x1c\xfd\x18\x90\x7f\x06a\\M\n\xd9\x01BB\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10\'\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00en-US\x01\x00A\x01\x01\r\x00\x00\x00\t\x00\x00\x00anonymous\xff\xff\xff\xff\xff\xff\xff\xff" 53 | IGNITION_CLOSE_SESSION = b'MSGF_\x00\x00\x000\x8a\x0f\x000\x8a\x0f\x00\x05\x00\x00\x00\x05\x00\x00\x00\x01\x00\xd9\x01\x05\x00\x00 \x00\x00\x00\x0c\x94\xa3\xdf;\n\xc4>\xe0\x0e\x85\x9e}{H\x17\xab+\xd1"\x81I\xee\xf8\x87\xa9\x15\xa4\xaf\xfd\x07M\xae\xdd\x7f9\xa4\xea\xd7\x017\x00\x00\x00\xff\x03\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x01' 54 | IGNITION_CLOSE_CHANNEL = b'CLOF9\x00\x00\x00\n\x00\x00\x00\x0c\x00\x00\x000\x00\x00\x000\x00\x00\x00\x01\x00\xc4\x01\x00\x00\xe1\x07\xe7`M\n\xd9\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00' 55 | 56 | # S2OPC anonymous messages 57 | S2OPC_HELLO_MSG = b'\x48\x45\x4c\x46\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01\x88\x13\x00\x00\x18\x00\x00\x00\x6f\x70\x63\x2e\x74\x63\x70\x3a\x2f\x2f\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x3a\x34\x38\x34\x31' 58 | S2OPC_OPEN_SECURE_CHANNEL = b'\x4f\x50\x4e\x46\x85\x00\x00\x00\x00\x00\x00\x00\x2f\x00\x00\x00\x68\x74\x74\x70\x3a\x2f\x2f\x6f\x70\x63\x66\x6f\x75\x6e\x64\x61\x74\x69\x6f\x6e\x2e\x6f\x72\x67\x2f\x55\x41\x2f\x53\x65\x63\x75\x72\x69\x74\x79\x50\x6f\x6c\x69\x63\x79\x23\x4e\x6f\x6e\x65\xff\xff\xff\xff\xff\xff\xff\xff\x33\x00\x00\x00\x01\x00\x00\x00\x01\x00\xbe\x01\x00\x00\x10\xc4\xf4\x42\xc2\x90\xd9\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x80\xee\x36\x00' 59 | S2OPC_CREATE_SESSION = b'\x4d\x53\x47\x46\x12\x01\x00\x00\x6e\x13\x02\x0c\xca\x95\x63\x71\x34\x00\x00\x00\x02\x00\x00\x00\x01\x00\xcd\x01\x02\x00\x00\xc0\xf2\xe1\x15\xf9\xd6\xf4\x42\xc2\x90\xd9\x01\x41\x42\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10\x27\x00\x00\x00\x00\x00\x13\x00\x00\x00\x75\x72\x6e\x3a\x53\x32\x4f\x50\x43\x3a\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x1e\x00\x00\x00\x75\x72\x6e\x3a\x55\x6e\x69\x66\x69\x65\x64\x41\x75\x74\x6f\x6d\x61\x74\x69\x6f\x6e\x3a\x55\x61\x45\x78\x70\x65\x72\x74\x02\x1b\x00\x00\x00\x55\x6e\x69\x66\x69\x65\x64\x20\x41\x75\x74\x6f\x6d\x61\x74\x69\x6f\x6e\x20\x55\x61\x45\x78\x70\x65\x72\x74\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x18\x00\x00\x00\x6f\x70\x63\x2e\x74\x63\x70\x3a\x2f\x2f\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x3a\x34\x38\x34\x31\x13\x00\x00\x00\x75\x72\x6e\x3a\x53\x32\x4f\x50\x43\x3a\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x80\x4f\x32\x41\xfb\xff\x04\x00' 60 | S2OPC_ACTIVATE_SESSION = b'\x4d\x53\x47\x46\x70\x00\x00\x00\x6e\x13\x02\x0c\xca\x95\x63\x71\x35\x00\x00\x00\x03\x00\x00\x00\x01\x00\xd3\x01\x02\x00\x00\x6e\xcd\x5f\x1b\xb8\x0d\xf5\x42\xc2\x90\xd9\x01\x42\x42\x0f\x00\x00\x00\x00\x00\xff\xff\xff\xff\x10\x27\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x65\x6e\x2d\x55\x53\x01\x00\x41\x01\x01\x08\x00\x00\x00\x04\x00\x00\x00\x61\x6e\x6f\x6e\xff\xff\xff\xff\xff\xff\xff\xff' 61 | S2OPC_CLOSE_SESSION = b'\x4d\x53\x47\x46\x3f\x00\x00\x00\x7a\xd4\x44\xc4\x63\x64\x31\x57\x05\x00\x00\x00\x05\x00\x00\x00\x01\x00\xd9\x01\x02\x00\x00\x5f\x80\xe4\x46\xc5\x4c\xb1\x0d\xf0\x81\xda\x01\x04\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x88\x13\x00\x00\x00\x00\x00\x00' 62 | S2OPC_CLOSE_CHANNEL = b'\x43\x4c\x4f\x46\x39\x00\x00\x00\x6e\x13\x02\x0c\xca\x95\x63\x71\x4e\x00\x00\x00\x1c\x00\x00\x00\x01\x00\xc4\x01\x00\x00\x0d\x47\x1b\x44\xc2\x90\xd9\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00' 63 | 64 | 65 | def get_raw_open_session_messages(program_type): 66 | if program_type == "softing": 67 | return SOFTING_HELLO_MSG, SOFTING_OPEN_SECURE_CHANNEL, SOFTING_CREATE_SESSION, SOFTING_ACTIVATE_SESSION 68 | elif program_type == "unified": 69 | return UNA_HELLO_MSG, UNA_OPEN_SECURE_CHANNEL, UNA_CREATE_SESSION, UNA_ACTIVATE_SESSION 70 | elif program_type == "prosys": 71 | return PROSYS_HELLO_MSG, PROSYS_OPEN_SECURE_CHANNEL, PROSYS_CREATE_SESSION, PROSYS_ACTIVATE_SESSION 72 | elif program_type == "kepware": 73 | return KEPWARE_HELLO_MSG, KEPWARE_OPEN_SECURE_CHANNEL, KEPWARE_CREATE_SESSION, KEPWARE_ACTIVATE_SESSION 74 | elif program_type == "triangle": 75 | return TRIANGLE_HELLO_MSG, TRIANGLE_OPEN_SECURE_CHANNEL, TRIANGLE_CREATE_SESSION, TRIANGLE_ACTIVATE_SESSION 76 | elif program_type == "dotnetstd": 77 | return DOTNET_HELLO_MSG, DOTNET_OPEN_SECURE_CHANNEL, DOTNET_CREATE_SESSION, DOTNET_ACTIVATE_SESSION 78 | elif program_type == "ignition": 79 | return IGNITION_HELLO_MSG, IGNITION_OPEN_SECURE_CHANNEL, IGNITION_CREATE_SESSION, IGNITION_ACTIVATE_SESSION 80 | elif program_type == "s2opc": 81 | return S2OPC_HELLO_MSG, S2OPC_OPEN_SECURE_CHANNEL, S2OPC_CREATE_SESSION, S2OPC_ACTIVATE_SESSION 82 | 83 | 84 | def get_raw_close_session_messages(program_type): 85 | if program_type == "softing": 86 | return SOFTING_CLOSE_SESSION, SOFTING_CLOSE_CHANNEL 87 | elif program_type == "unified": 88 | return UNA_CLOSE_SESSION, UNA_CLOSE_CHANNEL 89 | elif program_type == "prosys": 90 | return PROSYS_CLOSE_SESSION, PROSYS_CLOSE_CHANNEL 91 | elif program_type == "kepware": 92 | return KEPWARE_CLOSE_SESSION, KEPWARE_CLOSE_CHANNEL 93 | elif program_type == "triangle": 94 | return TRIANGLE_CLOSE_SESSION, TRIANGLE_CLOSE_CHANNEL 95 | elif program_type == "dotnetstd": 96 | raise NotImplementedError 97 | elif program_type == "ignition": 98 | return IGNITION_CLOSE_SESSION, IGNITION_CLOSE_CHANNEL 99 | elif program_type == "s2opc": 100 | return S2OPC_CLOSE_SESSION, S2OPC_CLOSE_CHANNEL 101 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boofuzz==0.4.1 2 | construct==2.10.68 3 | -------------------------------------------------------------------------------- /resources/DOS_kepwareex_CVE-2022-2848.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claroty/opcua_network_fuzzer/6a4edffd5e09049258cf6c104b47b8d6194ed4cb/resources/DOS_kepwareex_CVE-2022-2848.gif -------------------------------------------------------------------------------- /resources/boofuzz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claroty/opcua_network_fuzzer/6a4edffd5e09049258cf6c104b47b8d6194ed4cb/resources/boofuzz.png --------------------------------------------------------------------------------