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 ofPayload
, signed with the same secret key ofPayload
.
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:
- It selects a secret key based on the
Era
value from a shared memory array. - It decrypts the
Payload
using the selected key. - 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)