DEV Community

Cover image for FortiMail Zero-Day: 3 Checks Before You Trust the Patch
Kiell Tampubolon
Kiell Tampubolon

Posted on

FortiMail Zero-Day: 3 Checks Before You Trust the Patch

Fortinet confirmed a zero-day in FortiMail that lets anyone with network access to the management interface write arbitrary files, no credentials needed. Attackers are not just writing files. They are dropping a Linux loader backdoor that survives reboots and hides itself. CISA added it to the KEV catalog on October 1 and gave federal agencies until October 4.

I was researching this on Saturday and noticed something in how most coverage frames it: the advice is "patch it." That is correct and insufficient. The observed implants live in ld.so.preload, and a patch does not remove an implant. If your management interface was reachable before you patched, you have a hunt job, not just a patch job.

Here is the triage I built from the published IoCs, and the reasoning behind each check.

Why is a file write on an email gateway a full compromise?

On a Linux appliance, arbitrary file write is not an "integrity issue." It is the primitive every foothold starts from. Write a shared object, get it loaded, own every process on the box.

That is exactly what the in-the-wild activity does. The published IoCs show attackers using CVE-2026-104286 to add entries to /data/etc/ld.so.preload. Every dynamically linked process on the system loads those objects first, before the program itself. The planted /data/lib/liblog.so acts as a rootkit that can hide files, processes, and connections from tools running on the same box.

The full observed set:

# Added by attackers (persistence + concealment)
/data/etc/ld.so.preload        # dynamic linker hijack, ATT&CK T1574.006
/data/lib/liblog.so            # preload rootkit, ATT&CK T1014
/data/bin/webconsole           # backdoor service, ATT&CK T1505.003
/data/bin/mailservice          # backdoor service

# Modified
/bin/smit
/data/etc/httpd.conf
/data/migadmin.tar.gz
Enter fullscreen mode Exit fullscreen mode

A mail gateway that quietly beacons to an unfamiliar host is the tell. Fortinet published two C2 addresses: 79.141.169[.]187 and 45.129.0[.]192. A device that historically only speaks SMTP opening outbound web sessions to either one is a compromise signal, with or without the exact IPs in your logs.

What exactly is the bug?

Two weaknesses chained together in the web management interface:

  1. CWE-22, path traversal: a crafted HTTP or HTTPS request escapes the directory the web server intends to confine writes to.
  2. CWE-158, NULL byte neutralization: an embedded NULL character lets the path dodge whatever suffix the server appends.

Neither requires authentication. The CVSS 3.1 vector is AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, a clean 9.8 with every impact dimension maxed.

Which versions are affected?

Branch Affected Fixed
8.0 8.0.0 to 8.0.1 8.0.2+
7.6 7.6.0 to 7.6.6 7.6.7+
7.4 7.4.0 to 7.4.8 7.4.9+
7.2 7.2.0 to 7.2.9 none, migrate to 7.4.9+

NVD additionally lists 7.0.0 to 7.0.9. The advisory table does not enumerate a 7.0 branch, so if you are on 7.0.x, treat yourself as affected and move to a fixed 7.4 build. Note the 7.2 trap: there is no fixed 7.2 build. A "we are current on our branch" team on 7.2.9 has nothing to install.

Check 1: is the loader preload file clean?

This is the one that matters most, because it is the persistence linchpin. On a healthy appliance, I would not expect /data/etc/ld.so.preload to exist at all, or to be empty. Anything listed in it runs inside every process on the box, including your shell and your EDR if you have one.

I built this script from the published IoCs. I have not run it against a real FortiMail yet, so treat the outputs as illustrative and the paths as the thing to verify on your own appliance:

#!/usr/bin/env bash
# fortimail-hunt.sh : run on the FortiMail appliance shell
# Code-derived from Fortinet IoCs, not yet lab-verified

echo "== 1. loader preload (persistence linchpin) =="
if [ -s /data/etc/ld.so.preload ]; then
  echo "NON-EMPTY preload file, treat as compromise until proven otherwise:"
  cat -A /data/etc/ld.so.preload
else
  echo "clean (missing or empty)"
fi

echo "== 2. published IoC file paths =="
for f in /data/lib/liblog.so /data/bin/webconsole /data/bin/mailservice \
         /bin/smit /data/etc/httpd.conf /data/migadmin.tar.gz; do
  if [ -e "$f" ]; then
    stat -c '%n %y %s bytes' "$f"
  fi
done

echo "== 3. egress to published C2 IPs =="
# netstat may not exist on the appliance; ss is the fallback
(netstat -tunp 2>/dev/null || ss -tunp 2>/dev/null) \
  | grep -E '79\.141\.169\.187|45\.129\.0\.192' \
  && echo "MATCH: live session to a published C2 address" \
  || echo "no live session to published C2 (still grep history: see check 3b)"
Enter fullscreen mode Exit fullscreen mode

Expected output on a clean box is three "clean" style lines and no MATCH. Expected output on a hit is the preload contents plus a stat line per IoC file. If liblog.so is present, do not trust on-box tools to show you the truth. A preload rootkit hides from the very processes it loads into. Pull the disk or image it and inspect offline.

Check 2: has anything been calling the C2?

Live connections are only half the picture. The implant may have beaconed before you looked. Fortinet published the two addresses, so both the live check above and your egress logs need them:

grep -E '79\.141\.169\.187|45\.129\.0\.192' \
  /var/log/* /data/log/* 2>/dev/null | head
Enter fullscreen mode Exit fullscreen mode

On the network side, the better signal is behavioral: a mail gateway that historically only emits SMTP suddenly opening outbound web sessions is a compromise indicator on its own. That is worth alerting on permanently, not just this week.

Check 3: does the Sigma rule have something to chew on?

The public Sigma rule for ld.so.preload modification (T1574.006) covers this exact pattern. If you ship appliance logs or file-integrity events to a SIEM, this is a five-minute wiring job:

# if you have FIM or auditd shipping file watches
ausearch -f /data/etc/ld.so.preload -i | tail
Enter fullscreen mode Exit fullscreen mode

If you have no file watch on that path, this incident is your justification to add one. A single watch on a file that, when modified, means "the whole box is suspect" is the cheapest control on this page.

Is patching still step one?

Yes, and the order is: patch first, then hunt. The flaw is unauthenticated, so every reachable management interface should be assumed attempted. Patch to 8.0.2, 7.6.7, or 7.4.9, and if you are on 7.2, plan the migration now because there is no in-branch fix.

But patching does not delete an implant. CISA's BOD entry for this CVE flags forensic triage, and that flag is doing real work here: the observed activity installs a dynamic linker hijack, which is exactly the class of persistence that outlives the patched vulnerability.

One more thing worth admitting: Fortinet has not named the actor, and CISA lists ransomware use as Unknown. I have seen coverage implying a ransomware crew. I found nothing published that confirms one. I would rather under-claim here than repeat a rumor.

How do I make sure this never happens again?

Honestly, the strongest answer I have is unglamorous: management interfaces of security appliances should not be internet reachable, and they should not share a TLS endpoint with anything users touch. I wrote about the same pattern in the Postgres MCP read-only bypass, where an advertised guarantee failed one keyword past the filter, and in the agent sandbox CVE pair, where "local mode" was treated as a permission model. The lesson keeps repeating: a control that filters at one layer is a lint pass, and only what the platform refuses to start without is a guarantee.

My short list

  1. Management plane on a dedicated interface or behind a jump host, full stop.
  2. File watch on ld.so.preload class paths on every appliance you run.
  3. Egress baselining: appliances talk to few destinations. deviations are incidents.

What would you patch first?

Here is the debate I keep going back and forth on. Say an org has 40 FortiMail boxes and can only patch half before the KEV deadline. Is the right move to patch the half with internet-reachable management planes and hunt the rest, or to pull every management plane behind a jump host first and patch after? I lean toward exposing and hunting first, patching second, because the implant outlives the patch. But that assumes the org has hunt capacity, and plenty do not. Where do you land?

Takeaways I am carrying away

  • A file write on a Linux appliance is a foothold primitive, not an integrity footnote.
  • ld.so.preload is the persistence to check first, and it hides from on-box tools when abused.
  • Version checks answer "is the door closed" but never "did someone walk through."
  • 7.2.x has no fix. Migration is the only exit.

Further reading I used: the Fortinet advisory FG-IR-26-175, the CISA KEV entry of October 1, and write-ups from Truesec, BleepingComputer, and The Hacker News.

If this was useful, I have written similar triage pieces on the Windows DNS 9.8 and the Langflow 9.8 trio. Both follow the same pattern: what the advisory says, what the code or IoCs say, and what to run before you feel safe.

Top comments (0)