CISA added CVE-2025-61624 to its Known Exploited Vulnerabilities catalog on 16 October 2026, with a due date three days later. On paper it's a medium-severity FortiOS bug that only an administrator can trigger, and Fortinet fixed it in April. Unit 42's report from 14 October shows why it matters anyway. In a long intrusion at a telecommunications provider, attackers who already had FortiGate admin credentials used it to get root on the firewall and load PEERSTITCH, an implant that runs only in memory.
This teardown is for teams that run FortiGate, FortiProxy, FortiPAM or FortiSwitchManager, and especially for anyone who manages firewalls for other organizations. It covers what the advisory says, how Unit 42 saw the bug used, why an admin-only bug is a real boundary break on FortiOS, and a script that checks configuration backups for the traces the attackers left.
The short answer
CVE-2025-61624 is a path traversal in the FortiOS command line. An administrator with a read-write profile can use it to write or delete any file on the appliance. Fortinet fixed it in FortiOS 7.6.5 and 7.4.10, published on 14 April 2026. FortiOS 7.2, 7.0 and 6.4 have no fixed release, so devices on those branches have to move to a fixed one. CISA added it to the KEV catalog on 16 October with a due date of 19 October and a forensic triage requirement. Upgrade, rotate any admin and VPN credentials that may have leaked, and look for auto-script objects with ../ in their names, in old configuration revisions as well as the current one, because the attackers deleted theirs.
What the advisory says
Fortinet's advisory, FG-IR-26-122, describes a path traversal in the CLI that lets "a privileged attacker" write or delete files "via specifically crafted arguments to existing commands". Fortinet found it internally and rates it medium, with a CVSS 3.1 base score of 6.0. The CVE record shows 5.4, which is the temporal score for the same vector. The vector explains the rating: local access through the management CLI and high privileges required. The advisory now marks it as known exploited.
| Product | Affected | Fixed |
|---|---|---|
| FortiOS 7.6 | 7.6.0 to 7.6.4 | 7.6.5 |
| FortiOS 7.4 | 7.4.0 to 7.4.9 | 7.4.10 |
| FortiOS 7.2, 7.0, 6.4 | all versions | migrate to a fixed release |
| FortiProxy 7.6 | 7.6.0 to 7.6.4 | 7.6.5 |
| FortiProxy 7.4 | 7.4.0 to 7.4.11 | 7.4.12 |
| FortiProxy 7.2, 7.0 | all versions | migrate to a fixed release |
| FortiPAM 1.7 | 1.7.0 | 1.7.1 (1.8 is not affected) |
| FortiPAM 1.0 to 1.6 | all versions | migrate to a fixed release |
| FortiSwitchManager 7.2 | 7.2.0 to 7.2.7 | 7.2.8 |
| FortiSwitchManager 7.0 | 7.0.0 to 7.0.6 | 7.0.7 |
The CVE record adds that Fortinet fixed FortiSASE in release 25.4.b, so FortiSASE customers don't need to act. For devices that can't be upgraded yet, the advisory lists an IPS virtual patch, FG-VD-59270.0day, in FortiGuard's FMWP database update 25.120.
CISA's KEV entry gives a due date of 19 October and sets the new forensic triage flag, which under BOD 26-04 means federal agencies must check affected devices for compromise as well as patch them. CISA's own scoring in the CVE record marks exploitation as active and the technical impact as total. CISA added one other entry on 16 October with the same due date: CVE-2026-102764 in BeyondTrust Remote Support, which the CVE record lists as affecting versions up to 26.2.3, under BeyondTrust advisory BT26-05.
How Unit 42 saw it used
Unit 42's report, by Daniel Gott and Shawn Westfall, covers an intrusion at a telecommunications managed service provider that Unit 42 dates to September 2024 and tracks as CL-STA-0954, also known as UNC5820. CVE-2025-61624 was one step in a longer chain:
- FortiManager first. The attackers exploited CVE-2024-47575, known as FortiJump, on the provider's central FortiManager. They registered a fake FortiGate and took an archive holding the running configurations of more than 500 managed firewalls, management databases, certificates and private keys, and administrator and SSL VPN credentials stored as reversible ciphertext.
- Credentials from the configurations. Unit 42 confirmed the passwords in those files could be recovered by brute force. The attackers then logged in to perimeter firewalls over SSL VPN with real credentials.
- Reconnaissance from the management plane. Through FortiManager, they pushed packet sniffer rules to downstream firewalls, aimed at unencrypted HTTP and a set of internal services, and later at TFTP, RADIUS and LDAP.
- Admin to root with CVE-2025-61624. A FortiOS administrator, even
super_admin, works in a restricted CLI with no shell. The bug sits in thesystem auto-scriptfeature: the object's name isn't sanitized, so a name containing../writes the script body anywhere on the file system. The attackers created objects named../../../../setup, which staged the implant, and../../tmp/apache2/conf/md-acme.conf. - PEERSTITCH. The staged binary attached to two FortiOS daemons, the logging daemon
miglogdand the IPsec daemoniked, injected code into their memory, and deleted itself. The implant opens no port, makes no outbound connection and leaves no file on disk. It reads markers in traffic the daemons already handle and passes them between the two processes through/proc/<pid>/mem. - A second channel through Apache. The
md-acme.conffile turned the firewall's built-in Apache certificate module into a reverse shell, by pointing its ACME client at an attacker server and mapping the DNS challenge step to Node.js.
They also covered their tracks. They killed the logging daemons before running their scripts, cleared crash logs with diagnose debug crashlog clear, and deleted the rogue admin accounts, VPN groups and auto-script objects shortly after use. An admin account named FortiGateCloud downloaded configurations from several firewalls, although the victim had no FortiManager Cloud tenant. Unit 42 assesses with medium confidence that the attackers impersonated the management platform.
The report doesn't say when the CVE-2025-61624 step happened relative to Fortinet's April 2026 advisory.
Why an admin-only bug is a boundary break
CVSS scores a bug that needs high privileges as less severe, and for most software that's right. An attacker with admin rights can already do most of what they want. FortiOS is different. Its administrators manage the device through a restricted shell, and the daemons underneath run as root. Unit 42 makes the point directly: an administrator has "no shell access and no direct control over the appliance file system". CVE-2025-61624 removes that line.
In this intrusion, that's what turned stolen credentials into an implant. Admin access alone let the attackers change policy, add sniffers and pull configurations, all of which show up in logs and configuration revisions. Root let them patch running daemons in memory, where the firewall's own logging can't see them and a configuration backup records nothing.
The chain also shows why "requires admin credentials" is weak comfort for firewalls. Firewall configurations hold credentials, and a compromised management platform hands over hundreds of them at once. Unit 42's conclusion is that the trust material taken from a management platform "remains valid until it is rotated", whether or not the platform is patched.
Check your FortiGates
Two kinds of evidence matter here. Some lives in the configuration and its history. The rest lives in process memory, below the CLI, which needs Fortinet support or a forensic process to reach.
These read-only CLI commands show the parts of the configuration the attackers touched:
get system status
show system auto-script
show firewall sniffer
show system admin
diagnose debug crashlog read
Unit 42 suggests looking in the crash log for segmentation faults (signal 11) in miglogd, iked or syslogd, which its investigation found from failed injection attempts.
The script below reads FortiOS configuration backups, from the GUI, FortiManager's revision history or your own archive. For each file it reads the version from the backup header and compares it with FG-IR-26-122. It flags auto-script names that contain a path, auto-script bodies that kill processes or fetch from URLs, sniffer rules on the ports Unit 42 listed, an admin account named FortiGateCloud, and the four IP addresses Unit 42 published. It reads files only and uses the Python standard library.
#!/usr/bin/env python3
"""Check FortiOS/FortiProxy config backups for CVE-2025-61624 exposure and
the configuration traces Unit 42 published for the PEERSTITCH intrusion.
Usage: python3 fortios_61624_check.py [--product FortiProxy] backup.conf [...]
Reads files only. Standard library only.
"""
import re
import sys
# Fixed releases from FG-IR-26-122. Branches not listed here have no fix:
# Fortinet says "migrate to a fixed release".
FIXED = {
"FortiOS": {(7, 6): (7, 6, 5), (7, 4): (7, 4, 10)},
"FortiProxy": {(7, 6): (7, 6, 5), (7, 4): (7, 4, 12)},
}
AFFECTED_BRANCHES = {
"FortiOS": {(7, 6), (7, 4), (7, 2), (7, 0), (6, 4)},
"FortiProxy": {(7, 6), (7, 4), (7, 2), (7, 0)},
}
# FortiGate and FortiWiFi model codes. Pass --product for anything else.
PREFIX = ("FG", "FW")
# Network indicators from Unit 42's PEERSTITCH report, Table 4.
IOC_IPS = ["23.240.77.233", "47.18.167.159", "45.32.41.202", "198.199.122.22"]
# Ports Unit 42 saw in rogue sniffer rules (Table 1 plus the later phase).
SNIFF_PORTS = set(range(3696, 3707)) | {69, 389, 1812, 1813}
SCRIPT_HINTS = ["killall", "crashlog clear", "/tmp/", "/data2/", "wget",
"curl", "node", "http://"]
def header_version(text, product=None):
m = re.search(r"#config-version=([A-Z0-9]+)-(\d+)\.(\d+)\.(\d+)", text)
if not m:
return None, None, None
model = m.group(1)
if product is None:
product = "FortiOS" if model.startswith(PREFIX) else "unknown"
return model, product, tuple(int(x) for x in m.groups()[1:])
def version_verdict(product, ver):
if product not in FIXED:
return "UNKNOWN", "product not recognised, check FG-IR-26-122 by hand"
branch = ver[:2]
if branch not in AFFECTED_BRANCHES[product]:
return "OK", "branch not listed as affected"
fix = FIXED[product].get(branch)
if fix is None:
return "VULNERABLE", "no fix on this branch, migrate to a fixed release"
if ver >= fix:
return "OK", "at or above fixed %d.%d.%d" % fix
return "VULNERABLE", "upgrade to %d.%d.%d or later" % fix
def blocks(text, name):
"""Yield (edit_name, body) for each edit in every 'config <name>' block."""
for m in re.finditer(r"^\s*config %s\s*$(.*?)^\s*end\s*$" % re.escape(name),
text, re.M | re.S):
for e in re.finditer(r'^\s*edit "?([^"\n]*?)"?\s*$(.*?)^\s*next\s*$',
m.group(1), re.M | re.S):
yield e.group(1), e.group(2)
def setting(body, key):
m = re.search(r'^\s*set %s (.+)$' % re.escape(key), body, re.M)
return m.group(1).strip().strip('"') if m else None
def check(path, product=None):
text = open(path, encoding="utf-8", errors="replace").read()
findings = []
model, product, ver = header_version(text, product)
print("== %s" % path)
if ver:
status, why = version_verdict(product, ver)
print(" %s %d.%d.%d (%s): %s, %s" % (product, *ver, model, status, why))
else:
print(" no #config-version header; check the version by hand")
for name, body in blocks(text, "system auto-script"):
script = (setting(body, "script") or "")
if ".." in name or "/" in name:
findings.append("auto-script name contains a path: %r" % name)
hits = [h for h in SCRIPT_HINTS if h in script]
if hits:
findings.append("auto-script %r body mentions %s" % (name, ", ".join(hits)))
for name, body in blocks(text, "firewall sniffer"):
ports = set()
for lo, hi in re.findall(r"(\d+)(?:-(\d+))?", setting(body, "port") or ""):
ports.update(range(int(lo), int(hi or lo) + 1))
odd = sorted(ports & SNIFF_PORTS)
if odd:
findings.append("sniffer %s captures port(s) %s, matching Unit 42's list"
% (name, ", ".join(map(str, odd))))
for name, body in blocks(text, "system admin"):
if name.lower() == "fortigatecloud":
findings.append("admin account named %r exists; confirm it belongs to a "
"real FortiGate Cloud enrollment" % name)
for ip in IOC_IPS:
if ip in text:
findings.append("published PEERSTITCH IP %s appears in the config" % ip)
for f in findings:
print(" FINDING: " + f)
if not findings:
print(" no configuration indicators found (this cannot see memory-only implants)")
return bool(findings) or (ver is not None and version_verdict(product, ver)[0] == "VULNERABLE")
def main(argv):
product = None
if argv[:1] == ["--product"] and len(argv) > 1:
product, argv = argv[1], argv[2:]
if not argv:
print(__doc__.strip())
return 2
bad = [p for p in argv if check(p, product)]
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
We ran it on three synthetic backups: a branch firewall seeded with the report's artifacts, a patched core firewall and an unpatched 7.2 device.
$ python3 fortios_61624_check.py fw-branch.conf fw-core.conf fw-legacy.conf
== fw-branch.conf
FortiOS 7.4.8 (FGT60F): VULNERABLE, upgrade to 7.4.10 or later
FINDING: auto-script name contains a path: '../../../../setup'
FINDING: auto-script '../../../../setup' body mentions killall
FINDING: sniffer 1 captures port(s) 1812, 1813, matching Unit 42's list
FINDING: admin account named 'FortiGateCloud' exists; confirm it belongs to a real FortiGate Cloud enrollment
FINDING: published PEERSTITCH IP 23.240.77.233 appears in the config
== fw-core.conf
FortiOS 7.6.5 (FGT400F): OK, at or above fixed 7.6.5
no configuration indicators found (this cannot see memory-only implants)
== fw-legacy.conf
FortiOS 7.2.11 (FGT100E): VULNERABLE, no fix on this branch, migrate to a fixed release
no configuration indicators found (this cannot see memory-only implants)
A clean result is weak evidence. The attackers deleted their auto-script objects, so the current configuration of a compromised device may show nothing. Run the script on every revision you have from before the upgrade. And it can't see PEERSTITCH itself. Unit 42's memory checks, an anonymous 16,384-byte region with read, write and execute permission in miglogd or iked, and patched instructions in those daemons, need access below the FortiOS CLI.
What to do
- Upgrade to FortiOS 7.6.5 or 7.4.10 or later, or the fixed FortiProxy, FortiPAM or FortiSwitchManager release. Plan the migration for any device on FortiOS 7.2, 7.0 or 6.4, since those branches won't get a fix.
- Check FortiManager. If it ran a version affected by CVE-2024-47575 while reachable from untrusted networks, treat every configuration it held as stolen. Our FortiManager CVE-2024-47575 teardown covers that bug.
- Rotate what the configurations held. Change admin, local user and SSL VPN passwords, and replace certificates and private keys. Patching doesn't revoke credentials that already left.
- Hunt in history. Run the checks above against old revisions. Review admin accounts you don't recognize, including
FortiGateCloudon devices with no FortiGate Cloud enrollment. Our post on CVE-2025-59718 covers another case where that account name was misused. - Escalate if anything matches. A traversal-named auto-script, an unexplained sniffer or one of the published IP addresses is a reason to involve Fortinet support for a memory-level check, not just to upgrade.
- Keep management interfaces off the internet. Most steps in this chain, from FortiManager's port 541 to the firewall CLI, went through a management channel.
The fair view
Most FortiGates aren't at risk from this bug alone. It needs a read-write administrator, and a device whose admin credentials are safe and whose management interfaces are restricted gives an attacker nothing to start from. Fortinet found and fixed the bug itself, months before CISA listed it. And Unit 42 describes one intrusion by one capable group, not a mass exploitation campaign.
But the case for treating it as urgent is the one Unit 42 makes. Admin credentials for firewalls don't stay with one person. They sit in configuration files, management platforms and backups, and one breach of a managed service provider exposed hundreds at once. On FortiOS, a bug that turns those credentials into root is the step that keeps an attacker in after the passwords change. The upgrade costs far less than finding out you needed memory forensics.