Hello world! long time no see. I was so busy, mainly with working on symbol.exchange (btw opened a new “Bug Driven Development” community) and started to try my way in academia.

Intro

This is a story about how I leveraged a limited, blind out-of-bound read primitive into a full authentication bypass in Fortinet’s Web-Application-Firewall: FortiWeb :D

This bug was assigned as CVE-2025-52970, I named this exploit Fort-Majeure. Fort-Majeure represents the kind of silent failure that wasn’t meant to happen — where a system built to protect ends up trusting nothing as everything.

It is essentially a sequel for the other Pre-Auth RCE bug I posted earlier.

Description

There’s an OOB access in the cookie handling/parsing code, allowing an unauthenticated attacker to force the server to use a predictable secret key for session encryption/signing.

Analysis

The session cookie is composed of three parts:

  • Era: type of session(? or tenant, idk)
  • Payload: Encrypted data, contains session info such as: your username, role, etc.
  • AuthHash: An HMAC sha1 hash, contains the encrypted ciphertext of Payload, signed with the same secret key of Payload.

Example of a cookie:

APSCOOKIE_FWEB_8672793038565212270=Era=0&Payload=WBMi/LiqL9kMv5cas1WGGllbY2ehe9N98VN6szVLYhAfcAC+SFOpkVw5AbY2SgKP%0aEmAhJuYrSgJOaIPBUmz2rk/+fDhHldpZ8SGBG3NaeB2Rajs2rcsHwQ==%0a&AuthHash=Bnbx+Plvdm9anOc2zabqhFZR36U=%0a

During cookie parsing, the backend performs the following steps:

  1. It selects a secret key based on the Era value from a shared memory array.
  2. It decrypts the Payload using the selected key.
  3. It verifies the AuthHash using the same key and the ciphertext.

This is how it’s implemented in the httpd binary:

The vulnerability lies in the fact that the Era parameter is not validated properly. This allows setting an arbitrary index beyond the expected values (0 or 1), resulting in out-of-bounds read access:

In other words, by supplying a cookie with Era=9 (or any value in the range ), the backend reads uninitialized memory and may end up using a null or zeroed key for both encryption and HMAC signing. This vulnerability forces the key to be an -bit string of zeros:

This effectively removes all entropy from the key space. Under normal circumstances, the probability of a specific key being chosen from a key space of size is:

However, due to the vulnerability, the key is fixed to the all-zero value. Therefore, the probability of the key being this specific value is 1:

PoC

The following PoC demonstrates impersonation of an admin user in a REST endpoint /api/v2.0/system/status.systemstatus:

avivnix@wsl:ext-root$ ./exp.py
[*] got magic :: 8672793038565212270
[*] Exploit started
[*] Cooking payload
[*] Triggering OOB
[+] worked :D
    > res = o2IFGFubw+ikCQE0GZWk6lPHLqTEwZHmcHf2IRwBV9oIgaubXxdeKkX9zNBcYa0KOQF2H+80IJFezE/0Ric8bq31olXD5woq0tIdUwYx9a8=
    > sig = 6dqtHqF070L2M5Jm/jvrZ+hDSyg=
[+] path: https://192.168.10.143/api/v2.0/system/status.systemstatus
[+] response:
{'results': {'administrativeDomain': 'Disabled',
             'advancedBotProtection': 'Disabled',
             'advancedBotProtectionAccountStatus': 'License Pending',
             'bufferSizeMax': 102400,
             'fileUploadLimitMax': 102400,
             'firmwareVersion': 'FortiWeb-VM 7.6.1,build1010(GA.F),241126',
             'firmware_partition': 2,
             'haStatus': 'Standalone',
             'hostName': 'FortiWeb',
             'operationMode': 'Reverse Proxy',
             'readonly': False,
             'registration': {'label': '[Unregistered]',
                              'text': '[Register]',
                              'url': 'https://support.fortinet.com'},
             'serialNumber': 'REDACTED',
             'threatanalytics': 'Disabled',
             'up_days': '3',
             'up_hrs': '7',
             'up_mins': '27',

Full Exploit

It is possible to use the /ws/cli/open endpoint to open a connection to the FortiWeb CLI:

The full exploit will be shared shortly. Since the advisory just went out, I’m holding off on publishing the link for a little while. Consider it an exercise for the reader in the meantime! :^)

Limitations

One of the fields in the signed cookie is an unknown number, validated by refresh_total_logins()(in libncfg.so). This requires:

  • Brute-force of this number(shouldn’t be too high/usually not above 30)
  • The target user must have an active session while the exploit is running.

Brute-force cost is , typically with expected attempts

Or, in other terms:

I hope you enjoyed reading this blog post! cheers.

(Also wanted to say thanks for the PSIRT team for sending me a special medal in the mail! lmao)