This is a side door straight into the guard booth, not a harder way through the front gate
CVE-2024-55591 is an unauthenticated auth bypass in the FortiOS/FortiProxy management plane. Fortinet says crafted requests to the Node.js websocket module can yield super_admin privileges; affected versions are FortiOS 7.0.0–7.0.16, FortiProxy 7.0.0–7.0.19, and FortiProxy 7.2.0–7.2.12, with fixes in FortiOS 7.0.17, FortiProxy 7.0.20, and FortiProxy 7.2.13.
The vendor's CRITICAL label matches reality once you focus on how these boxes are used: they sit on the perimeter, hold policy control, VPN access, and routing trust, and this bug has confirmed in-the-wild exploitation and KEV status. The only real downward pressure is that the management interface must be reachable, but that friction is not enough to move this out of CRITICAL because exposed admin planes were exactly what attackers targeted in observed campaigns.
5 steps from start to impact.
Find exposed admin planes
- FortiGate/FortiProxy HTTP or HTTPS administrative interface is reachable from the internet
- Target is running an affected version
- Many mature enterprises keep management off the public internet or pin it to jump-host IPs
- Version fingerprinting is imperfect from the outside, so some scans produce false targets
Open a pre-auth WebSocket
- HTTPS management service responds
- Node.js websocket path is exposed on the target build
- WAFs rarely sit in front of firewall admin planes
- TLS inspection usually does not apply to inbound management sessions
Race or abuse the auth context to become super_admin
local_access_token shortcut, and a race into the CLI/Telnet auth path where no unique secret is needed to assert a privileged context. The attacker can effectively choose an access profile and land in the appliance CLI as an administrator.- Target is vulnerable and unpatched
- Exploit timing lands correctly or the crafted request path succeeds
- This is not a one-packet memory corruption bug; the attacker needs the right protocol behavior
- Fortinet notes that knowing an admin username helps CLI login, though brute forcing usernames remains feasible
jsconsole, Local_Process_Access, and suspicious admin object creation. Signature-only detection is incomplete.Establish durable control on the appliance
- Attacker has gained
super_adminprivileges - Configuration changes are permitted and saved
- Change monitoring or config drift alerting can surface this quickly
- Organizations with out-of-band config backups may detect unexpected deltas
ui="jsconsole", random admin usernames, and config changes under system.admin or SSL VPN groups.Pivot through the firewall into the enterprise
- Internal networks, VPN settings, or trust relationships are present behind the device
- The compromised box is a meaningful choke point in the environment
- Segmentation and identity controls still matter after initial access
- EDR on downstream hosts can still stop follow-on intrusion activity
local-in-policy controls immediately, within hours so only jump hosts or admin networks can reach it. Fortinet explicitly prefers local-in policies over relying on trusthost alone.ui="jsconsole", Local_Process_Access, random admin creation, local user creation, SSL VPN group changes, and unexpected firewall policy edits immediately, within hours. Active exploitation means exposure may already equal compromise.MFAon admin accounts does not fix a pre-auth management-plane bypass; the attacker is not following the normal login flow.- Blocking the IoC
srcip/dstipvalues from Fortinet logs does not help; Fortinet says those fields were attacker-generated parameters, not reliable source addresses. trusthostby itself is not enough unless every GUI user is configured with it; Fortinet explicitly sayslocal-in-policyis the preferred workaround.- Endpoint controls alone do nothing on the firewall itself; EDR only becomes useful after the attacker pivots beyond the appliance.
The supporting signals.
| In-the-wild status | Confirmed exploited. Fortinet says it is exploited in the wild, and Arctic Wolf tied pre-disclosure activity from mid-November 2024 to this vulnerability. |
|---|---|
| KEV status | Yes. Added to CISA KEV on 2025-01-14 with a federal due date of 2025-01-21. |
| Proof-of-concept availability | Public research and tooling exist. watchTowr Labs published deep technical analysis plus a GitHub detection script and a PoC repo. |
| EPSS | 0.94124 from the user-supplied intel block — extremely high exploit probability and directionally consistent with real exploitation. |
| CVSS and interpretation | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H means unauthenticated remote takeover of the management plane with full confidentiality, integrity, and availability impact. |
| Affected versions | FortiOS 7.0.0–7.0.16; FortiProxy 7.0.0–7.0.19; FortiProxy 7.2.0–7.2.12. FortiOS 7.2, 7.4, and 7.6 are listed as not affected in Fortinet's advisory. |
| Fixed versions | Upgrade to FortiOS 7.0.17+, FortiProxy 7.0.20+, or FortiProxy 7.2.13+ per Fortinet PSIRT. |
| Observed operator behavior | Fortinet observed random admin creation, local user creation, group/policy changes, and SSL VPN login using attacker-added users. Arctic Wolf observed public-exposure cases with jsconsole logins and follow-on configuration changes. |
| Exposure data | Censys reported 3,445,797 exposed FortiOS/FortiProxy devices total at the time of its advisory, including 51 exposed FortiProxy instances; 16% were geolocated in the United States. Censys notes not all exposed instances could be version-confirmed as vulnerable. |
| Disclosure and reporting | Fortinet published the advisory on 2025-01-14. Fortinet credits Sonny of watchTowr for the CSF-related vulnerability work later added to the same advisory stream. |
Sources.
Why this verdict
- Baseline stays near max: start from vendor/NVD criticality because the bug is
AV:N/PR:N/UI:Non a perimeter device, not a niche internal service. - One real friction lowers it slightly: exploitation generally requires the HTTP/HTTPS administrative interface to be reachable, which narrows the exposed population versus 'every FortiGate on the internet.'
- That discount gets erased by field evidence: CISA KEV on
2025-01-14, Fortinet-confirmed exploitation, and Arctic Wolf's pre-disclosure campaign reporting show attackers were already doing exactly this against exposed admin planes. - Blast radius is exceptional:
super_adminon a firewall/VPN appliance means policy tampering, credential or config theft, rogue VPN access, and a clean path to internal-network pivoting.
Why not higher?
There is no higher bucket than CRITICAL, and this is close to the top of that range already. The one thing keeping it from a perfect 10 is that the attack path is not universal across all deployments; organizations that never expose the admin interface have materially less reachability.
Why not lower?
Downgrading this would ignore the two facts that matter most: active exploitation and initial-access utility on a perimeter choke point. This is not a post-compromise local bug, not an authenticated admin edge case, and not a narrow feature toggle affecting an obscure subset.
Crowdsourced verification payload.
Run this from an auditor workstation that can reach the target's HTTPS management interface; invoke it exactly as python3 fortios_cve_2024_55591_check.py 10.0.0.1 443. It needs only network access to the admin port and does not require credentials or privileged local access; it performs a lightweight pre-auth websocket behavior check and prints VULNERABLE, PATCHED, or UNKNOWN.
#!/usr/bin/env python3
# CVE-2024-55591 pre-auth websocket behavior check
# Usage: python3 fortios_cve_2024_55591_check.py <host> [port]
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN, 3=USAGE
import base64
import os
import random
import socket
import ssl
import sys
TIMEOUT = 7
def build_request(host, path):
key = base64.b64encode(os.urandom(16)).decode()
headers = [
f"GET {path} HTTP/1.1",
f"Host: {host}",
"User-Agent: noisgate-cve-2024-55591-check/1.0",
"Upgrade: websocket",
"Connection: Upgrade",
f"Sec-WebSocket-Key: {key}",
"Sec-WebSocket-Version: 13",
"Origin: https://%s" % host,
"\r\n",
]
return "\r\n".join(headers).encode()
def recv_some(sock):
data = b""
try:
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
if b"\r\n\r\n" in data:
break
except socket.timeout:
pass
return data
def main():
if len(sys.argv) not in (2, 3):
print("UNKNOWN")
print("Usage: python3 fortios_cve_2024_55591_check.py <host> [port]", file=sys.stderr)
sys.exit(3)
host = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) == 3 else 443
# Use a few candidate paths because deployed behavior can vary by build.
candidates = [
"/ws/events",
"/ws/cli/open",
"/ws/%d" % random.randint(10000, 99999),
]
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
saw_rejection = False
errors = []
for path in candidates:
try:
raw = socket.create_connection((host, port), timeout=TIMEOUT)
raw.settimeout(TIMEOUT)
tls = ctx.wrap_socket(raw, server_hostname=host)
req = build_request(host, path)
tls.sendall(req)
resp = recv_some(tls)
tls.close()
head = resp.decode(errors="ignore")
if "101 Switching Protocols" in head:
print("VULNERABLE")
sys.exit(1)
if any(code in head for code in ["401 Unauthorized", "403 Forbidden", "404 Not Found", "400 Bad Request"]):
saw_rejection = True
continue
errors.append(f"{path}: unexpected response: {head[:120]!r}")
except Exception as e:
errors.append(f"{path}: {e}")
if saw_rejection:
print("PATCHED")
sys.exit(0)
print("UNKNOWN")
if errors:
print("; ".join(errors[:3]), file=sys.stderr)
sys.exit(2)
if __name__ == "__main__":
main()